parent
9bf050f990
commit
aa34f88cae
@ -1,346 +1,327 @@
|
||||
<!--
|
||||
============================================================================
|
||||
CONFIGURATOR - Example Svelte Component
|
||||
CONFIGURATOR - Svelte 5 Component
|
||||
============================================================================
|
||||
|
||||
Ejemplo de cómo usar el ConfigurationStore en un componente Svelte.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { createConfigurationStore } from '@/svelte/stores';
|
||||
import type { ConfigurationCatalog } from '@/types';
|
||||
|
||||
// Props
|
||||
export let catalog: ConfigurationCatalog;
|
||||
export let objectId: string;
|
||||
|
||||
// Crear el store
|
||||
const config = createConfigurationStore({
|
||||
catalog,
|
||||
objectId,
|
||||
autoValidate: true
|
||||
});
|
||||
|
||||
// Acceso reactivo al estado
|
||||
$: currentSection = $config.currentSection;
|
||||
$: currentView = $config.currentView;
|
||||
$: selection = $config.selection;
|
||||
$: validation = $config.validation;
|
||||
$: isValid = $config.isValid;
|
||||
|
||||
// Handlers
|
||||
function handleOptionSelect(attrId: string, optionId: string) {
|
||||
config.selectOption(attrId, optionId);
|
||||
}
|
||||
|
||||
function handleSectionChange(sectionId: string) {
|
||||
config.setCurrentSection(sectionId);
|
||||
}
|
||||
|
||||
function handleViewChange(viewId: string) {
|
||||
config.setCurrentView(viewId);
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
config.reset();
|
||||
}
|
||||
|
||||
function handleUndo() {
|
||||
config.undo();
|
||||
}
|
||||
import { createConfigurationStore } from '@/svelte/stores/configuration.svelte';
|
||||
import type {AttrID, ConfigurationCatalog, ObjectID, OptionID, SectionID, ViewID} from '@/types';
|
||||
|
||||
let { catalog, objectId }: {
|
||||
catalog : ConfigurationCatalog;
|
||||
objectId: ObjectID;
|
||||
} = $props();
|
||||
|
||||
const config = createConfigurationStore({
|
||||
catalog ,
|
||||
objectId,
|
||||
autoValidate: true
|
||||
});
|
||||
|
||||
// Valores reactivos
|
||||
const currentSection = $derived(config.current.currentSection);
|
||||
const currentView = $derived(config.current.currentView);
|
||||
const selection = $derived(config.current.selection);
|
||||
const validation = $derived(config.current.validation);
|
||||
const isValid = $derived(config.current.isValid);
|
||||
const object = $derived(config.current.object);
|
||||
|
||||
// Helper de traducción
|
||||
const t = (i18nString: any) => config.t(i18nString);
|
||||
|
||||
// Handlers con tipos correctos
|
||||
function handleOptionSelect(attrId: AttrID, optionId: string) {
|
||||
config.selectOption(attrId, optionId as OptionID);
|
||||
}
|
||||
|
||||
function handleSectionChange(sectionId: SectionID) {
|
||||
config.setCurrentSection(sectionId);
|
||||
}
|
||||
|
||||
function handleViewChange(viewId: ViewID) {
|
||||
config.setCurrentView(viewId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="configurator">
|
||||
<!-- Header -->
|
||||
<header class="configurator-header">
|
||||
<h1>{$config.object.name.es}</h1>
|
||||
<p>{$config.object.description.es}</p>
|
||||
|
||||
<div class="actions">
|
||||
<button on:click={handleUndo}>
|
||||
⬅️ Deshacer
|
||||
</button>
|
||||
<button on:click={handleReset}>
|
||||
🔄 Resetear
|
||||
</button>
|
||||
<button disabled={!isValid}>
|
||||
✅ Confirmar configuración
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="configurator-content">
|
||||
<!-- Section Navigation -->
|
||||
<nav class="section-nav">
|
||||
<h2>Secciones</h2>
|
||||
<ul>
|
||||
{#each Object.values($config.object.sections) as section}
|
||||
<li>
|
||||
<button
|
||||
class:active={section.id === currentSection?.id}
|
||||
on:click={() => handleSectionChange(section.id)}
|
||||
>
|
||||
{section.icon || '📦'} {section.name.es}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Attributes Panel -->
|
||||
<div class="attributes-panel">
|
||||
{#if currentSection}
|
||||
<h2>{currentSection.name.es}</h2>
|
||||
<p>{currentSection.description.es}</p>
|
||||
|
||||
<!-- Atributos de la sección -->
|
||||
{#each currentSection.attrs as attr}
|
||||
{#if attr.type === 'dynamic' && attr.display?.uiVisible}
|
||||
<div class="attribute">
|
||||
<label for={attr.id}>
|
||||
{attr.name.es}
|
||||
{#if config.isRequired(attr.id)}
|
||||
<span class="required">*</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<select
|
||||
id={attr.id}
|
||||
value={selection[attr.id] || ''}
|
||||
on:change={(e) => handleOptionSelect(attr.id, e.target.value)}
|
||||
class:error={config.hasErrorsForAttribute(attr.id)}
|
||||
>
|
||||
<option value="">-- Seleccionar --</option>
|
||||
{#each attr.options as opt}
|
||||
{@const option = catalog.options[opt.optionId]}
|
||||
{#if option}
|
||||
<option value={option.id}>
|
||||
{option.name.es}
|
||||
{#if option.pricing?.baseAmount}
|
||||
(+{option.pricing.baseAmount}€)
|
||||
{/if}
|
||||
</option>
|
||||
{/if}
|
||||
<header class="configurator-header">
|
||||
<h1>{t(object.name)}</h1>
|
||||
<p>{t(object.description)}</p>
|
||||
|
||||
<div class="actions">
|
||||
<button onclick={() => config.undo()}>⬅️ Deshacer</button>
|
||||
<button onclick={() => config.reset()}>🔄 Resetear</button>
|
||||
<button disabled={!isValid}>✅ Confirmar</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="configurator-content">
|
||||
<!-- Section Nav -->
|
||||
<nav class="section-nav">
|
||||
<h2>Secciones</h2>
|
||||
<ul>
|
||||
{#each Object.values(object.sections) as section}
|
||||
<li>
|
||||
<button
|
||||
class:active={section.id === currentSection?.id}
|
||||
onclick={() => handleSectionChange(section.id)}
|
||||
>
|
||||
{section.icon || '📦'} {t(section.name)}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</select>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Attributes -->
|
||||
<div class="attributes-panel">
|
||||
{#if currentSection}
|
||||
<h2>{t(currentSection.name)}</h2>
|
||||
<p>{t(currentSection.description)}</p>
|
||||
|
||||
{#each currentSection.attrs as attr}
|
||||
{@const selectedValue = selection[attr.id] || ''}
|
||||
{#if attr.type === 'dynamic' && attr.display?.uiVisible}
|
||||
<div class="attribute">
|
||||
<label for={attr.id}>
|
||||
{t(attr.name)}
|
||||
{#if config.isRequired(attr.id)}
|
||||
<span class="required">*</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<select
|
||||
id={attr.id}
|
||||
onchange={(e) => handleOptionSelect(attr.id, e.currentTarget.value)}
|
||||
class:error={config.hasErrorsForAttribute(attr.id)}
|
||||
>
|
||||
<option value="" selected={!selectedValue}></option>
|
||||
{#each attr.options as opt}
|
||||
{@const option = catalog.options[opt.optionId]}
|
||||
{#if option}
|
||||
<option value={option.id} selected={option.id === selectedValue}>
|
||||
{t(option.name)}
|
||||
{#if option.pricing?.baseAmount}
|
||||
(+{option.pricing.baseAmount}€)
|
||||
{/if}
|
||||
</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
{#each config.getViolationsForAttribute(attr.id) as violation}
|
||||
<div class="violation {violation.severity}">
|
||||
{t(violation.rule.message)}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Violaciones -->
|
||||
{#each config.getViolationsForAttribute(attr.id) as violation}
|
||||
<div class="violation {violation.severity}">
|
||||
{violation.rule.message.es}
|
||||
<!-- Visual -->
|
||||
<div class="visual-panel">
|
||||
{#if currentView}
|
||||
<div class="view-selector">
|
||||
<h3>Vistas</h3>
|
||||
{#each Object.values(currentSection?.views || {}) as view}
|
||||
<button
|
||||
class:active={view.id === currentView.id}
|
||||
onclick={() => handleViewChange(view.id)}
|
||||
>
|
||||
{view.icon || '📷'} {t(view.name)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Visual Preview -->
|
||||
<div class="visual-panel">
|
||||
{#if currentView}
|
||||
<div class="view-selector">
|
||||
<h3>Vistas</h3>
|
||||
{#each Object.values(currentSection.views || {}) as view}
|
||||
<button
|
||||
class:active={view.id === currentView.id}
|
||||
on:click={() => handleViewChange(view.id)}
|
||||
>
|
||||
{view.icon || '📷'} {view.name.es}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="preview">
|
||||
<h3>{currentView.name.es}</h3>
|
||||
<!-- Aquí iría el renderizado de la imagen -->
|
||||
<div class="image-placeholder">
|
||||
<p>Vista: {currentView.id}</p>
|
||||
<p>Template: {currentView.visualConfig.imageUrlTemplate}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="no-visual">
|
||||
<p>Esta sección no tiene visualización</p>
|
||||
<div class="preview">
|
||||
<h3>{t(currentView.name)}</h3>
|
||||
<div class="image-placeholder">
|
||||
<p>Vista: {currentView.id}</p>
|
||||
<p>Template: {currentView.visualConfig.imageUrlTemplate}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="no-visual">
|
||||
<p>Esta sección no tiene visualización</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validation Summary -->
|
||||
{#if validation}
|
||||
<footer class="validation-summary">
|
||||
{#if validation.hasErrors}
|
||||
<div class="errors">
|
||||
❌ {validation.violations.filter(v => v.severity === 'error').length} errores
|
||||
</div>
|
||||
{/if}
|
||||
{#if validation.hasWarnings}
|
||||
<div class="warnings">
|
||||
⚠️ {validation.violations.filter(v => v.severity === 'warning').length} advertencias
|
||||
</div>
|
||||
{/if}
|
||||
{#if validation.isValid}
|
||||
<div class="success">
|
||||
✅ Configuración válida
|
||||
</div>
|
||||
{/if}
|
||||
</footer>
|
||||
{/if}
|
||||
|
||||
{#if validation}
|
||||
<footer class="validation-summary">
|
||||
{#if validation.hasErrors}
|
||||
<div class="errors">
|
||||
❌ {validation.violations.filter(v => v.severity === 'error').length} errores
|
||||
</div>
|
||||
{/if}
|
||||
{#if validation.hasWarnings}
|
||||
<div class="warnings">
|
||||
⚠️ {validation.violations.filter(v => v.severity === 'warning').length} advertencias
|
||||
</div>
|
||||
{/if}
|
||||
{#if validation.isValid}
|
||||
<div class="success">✅ Configuración válida</div>
|
||||
{/if}
|
||||
</footer>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.configurator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.configurator-header {
|
||||
padding: 1rem 2rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.configurator-content {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr 1fr;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-nav {
|
||||
border-right: 1px solid #e0e0e0;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section-nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.section-nav button {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.section-nav button:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.section-nav button.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.attributes-panel {
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.attribute {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.attribute label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.attribute select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.attribute select.error {
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
.violation {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.violation.error {
|
||||
background: #fee;
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
.violation.warning {
|
||||
background: #ffc;
|
||||
color: #840;
|
||||
}
|
||||
|
||||
.visual-panel {
|
||||
padding: 2rem;
|
||||
background: #f9f9f9;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.view-selector {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.view-selector button {
|
||||
margin-right: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ccc;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.view-selector button.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
background: #ddd;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.validation-summary {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem 2rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.errors {
|
||||
color: #c00;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.warnings {
|
||||
color: #840;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #080;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
.configurator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.configurator-header {
|
||||
padding: 1rem 2rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.configurator-content {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr 1fr;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-nav {
|
||||
border-right: 1px solid #e0e0e0;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section-nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.section-nav button {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.section-nav button:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.section-nav button.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.attributes-panel {
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.attribute {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.attribute label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.attribute select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.attribute select.error {
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
.violation {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.violation.error {
|
||||
background: #fee;
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
.violation.warning {
|
||||
background: #ffc;
|
||||
color: #840;
|
||||
}
|
||||
|
||||
.visual-panel {
|
||||
padding: 2rem;
|
||||
background: #f9f9f9;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.view-selector {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.view-selector button {
|
||||
margin-right: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ccc;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.view-selector button.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
background: #ddd;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.validation-summary {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem 2rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.errors {
|
||||
color: #c00;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.warnings {
|
||||
color: #840;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #080;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,190 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* SVELTE 5 CONFIGURATION STORE
|
||||
* ============================================================================
|
||||
*
|
||||
* Adapter que expone ConfigurationState usando Svelte 5 runes.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ConfigurableObject,
|
||||
ConfigurationCatalog,
|
||||
AttrID,
|
||||
OptionID,
|
||||
SectionID,
|
||||
ViewID,
|
||||
Section,
|
||||
SectionView
|
||||
} from '@/types';
|
||||
import {
|
||||
ConfigurationState,
|
||||
type ConfigurationStateOptions
|
||||
} from '@/state/ConfigurationState';
|
||||
import type { ValidationResult } from '@/state/ValidationState';
|
||||
import type { SelectionStateData } from '@/state/SelectionState';
|
||||
|
||||
// ============================================================================
|
||||
// STORE STATE INTERFACE
|
||||
// ============================================================================
|
||||
|
||||
export interface ConfigurationStoreState {
|
||||
catalog : ConfigurationCatalog;
|
||||
object : ConfigurableObject;
|
||||
selection : SelectionStateData;
|
||||
validation : ValidationResult | null;
|
||||
currentSection : Section | null;
|
||||
currentSectionId: SectionID | null;
|
||||
currentView : SectionView | null;
|
||||
currentViewId : ViewID | null;
|
||||
isValid : boolean;
|
||||
locale : string; // Idioma actual
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION STORE (SVELTE 5 RUNES)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Crea un store de configuración reactivo para Svelte 5
|
||||
*/
|
||||
export function createConfigurationStore(options: ConfigurationStateOptions) {
|
||||
// Estado core (framework agnostic)
|
||||
const coreState = new ConfigurationState(options);
|
||||
|
||||
// Función helper para obtener el estado actual
|
||||
function getStoreState(): ConfigurationStoreState {
|
||||
return {
|
||||
catalog : coreState.getCatalog(),
|
||||
object : coreState.getObject(),
|
||||
selection : coreState.getAllSelections(),
|
||||
validation : coreState.getValidationResult(),
|
||||
currentSection : coreState.getCurrentSection(),
|
||||
currentSectionId: coreState.getCurrentSectionId(),
|
||||
currentView : coreState.getCurrentView(),
|
||||
currentViewId : coreState.getCurrentViewId(),
|
||||
isValid : coreState.isValid(),
|
||||
locale : coreState.getLocale()
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ SVELTE 5: Usar $state() para reactividad
|
||||
let state = $state<ConfigurationStoreState>(getStoreState());
|
||||
|
||||
// Suscribirse a cambios del estado core
|
||||
coreState.subscribe(() => {
|
||||
state = getStoreState();
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// PUBLIC API
|
||||
// ========================================================================
|
||||
|
||||
return {
|
||||
// ✅ SVELTE 5: Getter reactivo
|
||||
get current() {
|
||||
return state;
|
||||
},
|
||||
|
||||
// Selection actions
|
||||
selectOption(attrId: AttrID, optionId: OptionID) {
|
||||
coreState.selectOption(attrId, optionId);
|
||||
},
|
||||
|
||||
clearSelection(attrId: AttrID) {
|
||||
coreState.clearSelection(attrId);
|
||||
},
|
||||
|
||||
// Navigation actions
|
||||
setCurrentSection(sectionId: SectionID) {
|
||||
coreState.setCurrentSection(sectionId);
|
||||
},
|
||||
|
||||
setCurrentView(viewId: ViewID) {
|
||||
coreState.setCurrentView(viewId);
|
||||
},
|
||||
|
||||
// Locale actions
|
||||
getLocale() {
|
||||
return coreState.getLocale();
|
||||
},
|
||||
|
||||
setLocale(locale: string) {
|
||||
coreState.setLocale(locale as any);
|
||||
},
|
||||
|
||||
// Translation helper
|
||||
t(i18nString: any) {
|
||||
const locale = coreState.getLocale();
|
||||
if (typeof i18nString === 'string') return i18nString;
|
||||
return i18nString[locale] || i18nString.es || i18nString.en || '';
|
||||
},
|
||||
|
||||
// Validation actions
|
||||
validate() {
|
||||
return coreState.validate();
|
||||
},
|
||||
|
||||
// Helpers
|
||||
getSelection(attrId: AttrID) {
|
||||
return coreState.getSelection(attrId);
|
||||
},
|
||||
|
||||
getAllowedValues(attrId: AttrID) {
|
||||
return coreState.getAllowedValues(attrId);
|
||||
},
|
||||
|
||||
isRequired(attrId: AttrID) {
|
||||
return coreState.isRequired(attrId);
|
||||
},
|
||||
|
||||
hasErrorsForAttribute(attrId: AttrID) {
|
||||
return coreState.hasErrorsForAttribute(attrId);
|
||||
},
|
||||
|
||||
getViolationsForAttribute(attrId: AttrID) {
|
||||
return coreState.getViolationsForAttribute(attrId);
|
||||
},
|
||||
|
||||
// Persistence
|
||||
toJSON() {
|
||||
return coreState.toJSON();
|
||||
},
|
||||
|
||||
fromJSON(data: ReturnType<typeof coreState.toJSON>) {
|
||||
coreState.fromJSON(data);
|
||||
},
|
||||
|
||||
// Utilities
|
||||
reset() {
|
||||
coreState.reset();
|
||||
},
|
||||
|
||||
undo() {
|
||||
return coreState.undo();
|
||||
},
|
||||
|
||||
// Direct access to state (for advanced use)
|
||||
_coreState: coreState
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DERIVED VALUES (SVELTE 5 $DERIVED)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Helper para crear valores derivados en Svelte 5
|
||||
*
|
||||
* Uso en componentes:
|
||||
* ```ts
|
||||
* const config = createConfigurationStore({...});
|
||||
* const currentSection = $derived(config.current.currentSection);
|
||||
* const isValid = $derived(config.current.isValid);
|
||||
* ```
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// TYPE EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export type ConfigurationStore = ReturnType<typeof createConfigurationStore>;
|
||||
@ -1,197 +0,0 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* SVELTE CONFIGURATION STORE
|
||||
* ============================================================================
|
||||
*
|
||||
* Adapter que expone ConfigurationState como Svelte store.
|
||||
*/
|
||||
|
||||
import { writable, derived, type Readable } from 'svelte/store';
|
||||
import type {
|
||||
ConfigurableObject,
|
||||
ConfigurationCatalog,
|
||||
AttrID,
|
||||
OptionID,
|
||||
SectionID,
|
||||
ViewID,
|
||||
Section,
|
||||
SectionView
|
||||
} from '@/types';
|
||||
import {
|
||||
ConfigurationState,
|
||||
type ConfigurationStateOptions
|
||||
} from '@/state/ConfigurationState';
|
||||
import type { ValidationResult } from '@/state/ValidationState';
|
||||
import type { SelectionStateData } from '@/state/SelectionState';
|
||||
|
||||
// ============================================================================
|
||||
// STORE STATE INTERFACE
|
||||
// ============================================================================
|
||||
|
||||
export interface ConfigurationStoreState {
|
||||
catalog : ConfigurationCatalog;
|
||||
object : ConfigurableObject;
|
||||
selection : SelectionStateData;
|
||||
validation : ValidationResult | null;
|
||||
currentSection : Section | null;
|
||||
currentSectionId: SectionID | null;
|
||||
currentView : SectionView | null;
|
||||
currentViewId : ViewID | null;
|
||||
isValid : boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION STORE
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Crea un store de configuración reactivo para Svelte
|
||||
*/
|
||||
export function createConfigurationStore(options: ConfigurationStateOptions) {
|
||||
// Estado core (framework agnostic)
|
||||
const state = new ConfigurationState(options);
|
||||
|
||||
// Store interno con versión para forzar actualizaciones
|
||||
const { subscribe, set, update } = writable(getStoreState());
|
||||
|
||||
// Función helper para obtener el estado actual del store
|
||||
function getStoreState(): ConfigurationStoreState {
|
||||
return {
|
||||
catalog : state.getCatalog(),
|
||||
object : state.getObject(),
|
||||
selection : state.getAllSelections(),
|
||||
validation : state.getValidationResult(),
|
||||
currentSection : state.getCurrentSection(),
|
||||
currentSectionId: state.getCurrentSectionId(),
|
||||
currentView : state.getCurrentView(),
|
||||
currentViewId : state.getCurrentViewId(),
|
||||
isValid : state.isValid()
|
||||
};
|
||||
}
|
||||
|
||||
// Suscribirse a cambios del estado core
|
||||
state.subscribe(() => {
|
||||
set(getStoreState());
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// PUBLIC API
|
||||
// ========================================================================
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
// Selection actions
|
||||
selectOption: (attrId: AttrID, optionId: OptionID) => {
|
||||
state.selectOption(attrId, optionId);
|
||||
},
|
||||
|
||||
clearSelection: (attrId: AttrID) => {
|
||||
state.clearSelection(attrId);
|
||||
},
|
||||
|
||||
// Navigation actions
|
||||
setCurrentSection: (sectionId: SectionID) => {
|
||||
state.setCurrentSection(sectionId);
|
||||
},
|
||||
|
||||
setCurrentView: (viewId: ViewID) => {
|
||||
state.setCurrentView(viewId);
|
||||
},
|
||||
|
||||
// Validation actions
|
||||
validate: () => {
|
||||
return state.validate();
|
||||
},
|
||||
|
||||
// Helpers
|
||||
getSelection: (attrId: AttrID) => {
|
||||
return state.getSelection(attrId);
|
||||
},
|
||||
|
||||
getAllowedValues: (attrId: AttrID) => {
|
||||
return state.getAllowedValues(attrId);
|
||||
},
|
||||
|
||||
isRequired: (attrId: AttrID) => {
|
||||
return state.isRequired(attrId);
|
||||
},
|
||||
|
||||
hasErrorsForAttribute: (attrId: AttrID) => {
|
||||
return state.hasErrorsForAttribute(attrId);
|
||||
},
|
||||
|
||||
getViolationsForAttribute: (attrId: AttrID) => {
|
||||
return state.getViolationsForAttribute(attrId);
|
||||
},
|
||||
|
||||
// Persistence
|
||||
toJSON: () => state.toJSON(),
|
||||
|
||||
fromJSON: (data: ReturnType<typeof state.toJSON>) => {
|
||||
state.fromJSON(data);
|
||||
},
|
||||
|
||||
// Utilities
|
||||
reset: () => state.reset(),
|
||||
undo: () => state.undo(),
|
||||
|
||||
// Direct access to state (for advanced use)
|
||||
_state: state
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DERIVED STORES (COMPUTED VALUES)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Derived store para la sección actual
|
||||
*/
|
||||
export function deriveCurrentSection(
|
||||
configStore: ReturnType<typeof createConfigurationStore>
|
||||
): Readable<Section | null> {
|
||||
return derived(configStore, $config => $config.currentSection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derived store para la vista actual
|
||||
*/
|
||||
export function deriveCurrentView(
|
||||
configStore: ReturnType<typeof createConfigurationStore>
|
||||
): Readable<SectionView | null> {
|
||||
return derived(configStore, $config => $config.currentView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derived store para validación
|
||||
*/
|
||||
export function deriveValidation(
|
||||
configStore: ReturnType<typeof createConfigurationStore>
|
||||
): Readable<ValidationResult | null> {
|
||||
return derived(configStore, $config => $config.validation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derived store para verificar si es válido
|
||||
*/
|
||||
export function deriveIsValid(
|
||||
configStore: ReturnType<typeof createConfigurationStore>
|
||||
): Readable<boolean> {
|
||||
return derived(configStore, $config => $config.isValid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derived store para selección
|
||||
*/
|
||||
export function deriveSelection(
|
||||
configStore: ReturnType<typeof createConfigurationStore>
|
||||
): Readable<SelectionStateData> {
|
||||
return derived(configStore, $config => $config.selection);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TYPE EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export type ConfigurationStore = ReturnType<typeof createConfigurationStore>;
|
||||
@ -0,0 +1,9 @@
|
||||
import type {I18nString, ObjectID} from "@/types";
|
||||
|
||||
|
||||
export const ERRORS_CONFIG = {
|
||||
OBJECT_NOT_FOUND: (objectId: ObjectID): I18nString => ({
|
||||
es: `Objeto '${objectId}' no encontrado en el catálogo`,
|
||||
en: `Object '${objectId}' not found in catalog`
|
||||
})
|
||||
} as const;
|
||||
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* WARNINGS - Configuration State
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
import type { I18nString, AttrID, OptionID, SectionID, ViewID } from '@/types';
|
||||
|
||||
export const WARNINGS_CONFIG = {
|
||||
OPTION_NOT_ALLOWED: (optionId: OptionID, attrId: AttrID): I18nString => ({
|
||||
es: `La opción '${optionId}' no está permitida para el atributo '${attrId}'`,
|
||||
en: `Option '${optionId}' is not allowed for attribute '${attrId}'`
|
||||
}),
|
||||
|
||||
SECTION_NOT_FOUND: (sectionId: SectionID): I18nString => ({
|
||||
es: `Sección '${sectionId}' no encontrada`,
|
||||
en: `Section '${sectionId}' not found`
|
||||
}),
|
||||
|
||||
VIEW_NOT_FOUND: (viewId: ViewID): I18nString => ({
|
||||
es: `Vista '${viewId}' no encontrada en la sección actual`,
|
||||
en: `View '${viewId}' not found in current section`
|
||||
})
|
||||
} as const;
|
||||
@ -0,0 +1,70 @@
|
||||
// LocaleState.ts
|
||||
|
||||
/**
|
||||
* ============================================================================
|
||||
* LOCALE STATE - Framework Agnostic
|
||||
* ============================================================================
|
||||
*
|
||||
* Gestiona el idioma actual de la aplicación.
|
||||
*/
|
||||
|
||||
import type { SupportedLocale } from '@/types';
|
||||
|
||||
export interface LocaleStateChangeEvent {
|
||||
previousLocale : SupportedLocale;
|
||||
newLocale : SupportedLocale;
|
||||
timestamp : number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estado del idioma actual
|
||||
*/
|
||||
export class LocaleState {
|
||||
private currentLocale : SupportedLocale;
|
||||
private listeners : Set<(event: LocaleStateChangeEvent) => void> = new Set();
|
||||
|
||||
constructor(initialLocale: SupportedLocale = 'es') {
|
||||
this.currentLocale = initialLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el idioma actual
|
||||
*/
|
||||
get(): SupportedLocale {
|
||||
return this.currentLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establece el idioma actual
|
||||
*/
|
||||
set(locale: SupportedLocale): void {
|
||||
if (this.currentLocale === locale) return;
|
||||
|
||||
const event: LocaleStateChangeEvent = {
|
||||
previousLocale : this.currentLocale,
|
||||
newLocale : locale,
|
||||
timestamp : Date.now()
|
||||
};
|
||||
|
||||
this.currentLocale = locale;
|
||||
this.notifyListeners(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Suscribe un listener a cambios de idioma
|
||||
*/
|
||||
subscribe(listener: (event: LocaleStateChangeEvent) => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifica a todos los listeners
|
||||
*/
|
||||
private notifyListeners(event: LocaleStateChangeEvent): void {
|
||||
this.listeners.forEach(listener => listener(event));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
// ============================================================================
|
||||
// HOTSPOT
|
||||
// ============================================================================
|
||||
|
||||
|
||||
|
||||
import type {AttrID, HotspotID, I18nString, JsonLogic} from "@/types";
|
||||
|
||||
/**
|
||||
* Punto interactivo en una vista
|
||||
*/
|
||||
export interface Hotspot {
|
||||
/** ID único del hotspot */
|
||||
id: HotspotID;
|
||||
|
||||
/** Nombre descriptivo */
|
||||
name: I18nString;
|
||||
/** Posición X en porcentaje (0-100) */
|
||||
x: number;
|
||||
|
||||
/** Posición Y en porcentaje (0-100) */
|
||||
y: number;
|
||||
|
||||
/** Radio del hotspot en píxeles */
|
||||
radius?: number;
|
||||
|
||||
/** Color del hotspot */
|
||||
color?: string;
|
||||
|
||||
/** Icono del hotspot */
|
||||
icon?: string;
|
||||
|
||||
/** Tipo de scope al que referencia */
|
||||
scopeType: 'global' | 'section';
|
||||
|
||||
/** ID del atributo que este hotspot controla */
|
||||
targetAttributeId: AttrID;
|
||||
|
||||
/** Si es visible (puede ser condicional) */
|
||||
visible?: boolean | JsonLogic;
|
||||
|
||||
/** Tooltip al hacer hover */
|
||||
tooltip?: I18nString;
|
||||
|
||||
/** Orden z-index para superposición */
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
|
||||
|
||||
export * from './logger'
|
||||
|
||||
Loading…
Reference in new issue