diff --git a/src/lib/actx/lib/engine.svelte.ts b/src/lib/actx/lib/engine.svelte.ts index 0323e1df8..14299a995 100644 --- a/src/lib/actx/lib/engine.svelte.ts +++ b/src/lib/actx/lib/engine.svelte.ts @@ -1,10 +1,6 @@ import type { LingNode } from '../../ling/lib/types'; import type { GlobLocale } from '../../glob/lib/comn_types'; import type { ACtxConfig, ReactiveACtx } from './types'; -import type { UiuxConfig } from '../../uiux/lib'; - - -import { createUiuxState } from '../../uiux/lib/engine.svelte'; import { createNumrState } from '../../glob/lib/numr_engine.svelte'; import { createDatiState } from '../../glob/lib/dati_engine.svelte'; import { createCurrState } from '../../glob/lib/curr_engine.svelte'; @@ -53,7 +49,6 @@ export function createACtx( getLocale, reactiveLocale ); - const _uiux = createUiuxState(config.uiux ?? {} as UiuxConfig); // ── Instancia ───────────────────────────────────────────────────────────── return { @@ -67,7 +62,6 @@ export function createACtx( dati : _dati, curr : _curr, unit : _unit, - uiux : _uiux, get locale() { return locale; }, }; diff --git a/src/lib/actx/lib/engine.ts b/src/lib/actx/lib/engine.ts index 1bf730226..eb191a267 100644 --- a/src/lib/actx/lib/engine.ts +++ b/src/lib/actx/lib/engine.ts @@ -7,7 +7,6 @@ import { createNumr } from '../../glob/lib/numr_engine'; import { createDati } from '../../glob/lib/dati_engine'; import { createCurr } from '../../glob/lib/curr_engine'; import { createUnit } from '../../glob/lib/unit_engine'; -import { createUiux } from '../../uiux/lib'; // ============================================================================ // HELPERS @@ -44,7 +43,6 @@ export function createACtx( let _dati: ReturnType | undefined; let _curr: ReturnType | undefined; let _unit: ReturnType | undefined; - let _uiux: ReturnType | undefined; return { // Delegates de ling @@ -92,13 +90,5 @@ export function createACtx( getLocale ); }, - - get uiux() { - if (!config.uiux) missingModule('uiux', 'uiux: { theme: { mode: "auto" } }'); - if (!_uiux) { - _uiux = createUiux(config.uiux); - } - return _uiux; - }, }; } diff --git a/src/lib/actx/lib/instance_actx.svelte.ts b/src/lib/actx/lib/instance_actx.svelte.ts index c7fd083e4..6f69442a8 100644 --- a/src/lib/actx/lib/instance_actx.svelte.ts +++ b/src/lib/actx/lib/instance_actx.svelte.ts @@ -79,9 +79,4 @@ export const actx = createACtx({ selectedSystem: 'metric', // categories: { weight: myCustomWeight }, // sobreescribe si necesitas }, - - uiux: { - theme: { mode: 'auto' }, - toast: { defaultDuration: 4000 }, - }, }); \ No newline at end of file diff --git a/src/lib/actx/lib/instance_actx.ts b/src/lib/actx/lib/instance_actx.ts index 271d95243..d80336fc5 100644 --- a/src/lib/actx/lib/instance_actx.ts +++ b/src/lib/actx/lib/instance_actx.ts @@ -45,9 +45,4 @@ export const actx = createACtx({ unit: { selectedSystem: 'metric', }, - - uiux: { - theme: { mode: 'light' }, // sin 'auto' — no hay window en SSR - toast: { defaultDuration: 4000 }, - }, }); \ No newline at end of file diff --git a/src/lib/actx/lib/types.ts b/src/lib/actx/lib/types.ts index 6a6695fef..974fe85dc 100644 --- a/src/lib/actx/lib/types.ts +++ b/src/lib/actx/lib/types.ts @@ -9,7 +9,6 @@ import type { UnitInstance, UnitCategoryData, UnitCategoryKey, UnitSystem } from import type { DatiInstance, DatiConfig } from '../../glob/lib/dati_types'; import type { NumrInstance, NumrConfig } from '../../glob/lib/numr_types'; import type { GlobLocale } from '../../glob/lib/comn_types'; -import type { ReactiveUiux, Uiux, UiuxConfig } from '../../uiux/lib/types'; // ============================================================================ @@ -30,7 +29,6 @@ import type { ReactiveUiux, Uiux, UiuxConfig } from '../../uiux/lib/types'; * dati : { selectedTimeFormat: '24h' }, * curr : { definitions: { EUR, USD }, selectedCurrency: 'EUR' }, * unit : { selectedSystem: 'metric' }, - * uiux : { theme: { mode: 'auto' } }, * }); */ export interface ACtxConfig { @@ -50,7 +48,6 @@ export interface ACtxConfig { categories? : Partial>; }; /** Configuración del contexto de UI (tema, breakpoints, toasts, modales, menús). */ - uiux? : UiuxConfig; } // ============================================================================ @@ -69,10 +66,6 @@ export interface ACtxConfig { * actx.dati.f(new Date()) * actx.curr.f(99.99, 'name') * actx.unit.weight.fTo(75, 'kg', 'lb') - * actx.uiux.theme.toggle() - * actx.uiux.toast.show({ es: 'Guardado', en: 'Saved' }, { type: 'success' }) - * actx.uiux.modal.open('confirm', { title: '¿Eliminar?' }) - * actx.uiux.break.isMobile */ export interface ACtx { t : LingInstance['t']; @@ -84,7 +77,6 @@ export interface ACtx { dati : DatiInstance; curr : CurrInstance; unit : UnitInstance; - uiux : Uiux; } @@ -96,9 +88,7 @@ export interface ACtx { /** * Contexto global reactivo. * Devuelto por `createACtx` de `actx_engine.svelte.ts`. - * Las propiedades de `uiux` y `locale` están respaldadas por `$state`. + * La propiedad `locale` está respaldada por `$state`. */ export interface ReactiveACtx - extends Omit, 'uiux'> { - uiux: ReactiveUiux; -} \ No newline at end of file + extends ACtx {} diff --git a/src/lib/actx/test/actx.svelte.test.ts.skip b/src/lib/actx/test/actx.svelte.test.ts.skip deleted file mode 100644 index 55b7005b8..000000000 --- a/src/lib/actx/test/actx.svelte.test.ts.skip +++ /dev/null @@ -1,321 +0,0 @@ -/** - * actx.svelte — test suite (versión reactiva) - * - * Estos tests requieren un entorno Svelte 5 con soporte para runes ($state). - * - * Ejecutar: npx vitest run src/lib/actx/test/actx.svelte.test.ts - * - * Módulos cubiertos: - * - engine.svelte (createACtx reactivo) - * - reactividad de $state - * - sincronización engines → $state - * - uiux reactivo - */ - -import type { CurrDefinition } from '../../glob/lib'; -import { describe, it, expect } from 'vitest'; -import { createLing } from '../../ling/lib'; -import { createACtx } from '../lib/engine.svelte'; -import { LogLevel } from '../../logr/lib'; - -// ============================================================================ -// FIXTURES -// ============================================================================ - -const translations = { - common: { - ok: { es: 'Aceptar', en: 'OK' }, - cancel: { es: 'Cancelar', en: 'Cancel' }, - }, - messages: { - welcome: { es: 'Bienvenido', en: 'Welcome' }, - }, -}; - -const EUR: CurrDefinition = { - symbol: '€', - code: 'EUR', - name: ({ count }) => ({ - es: count === 1 ? 'euro' : 'euros', - en: count === 1 ? 'euro' : 'euros', - }), - conversionValue: 1, -}; - -const USD: CurrDefinition = { - symbol: '$', - code: 'USD', - name: ({ count }) => ({ - es: count === 1 ? 'dólar' : 'dólares', - en: count === 1 ? 'dollar' : 'dollars', - }), - conversionValue: 1.08, -}; - -// ============================================================================ -// CREATE ACTX (REACTIVE) -// ============================================================================ - -describe('createACtx (reactivo con Svelte 5)', () => { - describe('creación básica', () => { - it('crea una instancia con ling', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ ling }); - - expect(actx.ling).toBeDefined(); - expect(actx.locale).toBe('es'); - }); - - it('t() funciona directamente en actx', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ ling }); - - expect(actx.t('common.ok')).toBe('Aceptar'); - }); - - it('ts() funciona directamente en actx', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ ling }); - - expect(actx.ts({ es: 'Hola', en: 'Hello' })).toBe('Hola'); - }); - }); - - describe('módulos configurados', () => { - it('crea instancia con todos los módulos configurados', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ - ling, - logr: { level: LogLevel.WARN }, - numr: { maxDecimals: 2 }, - dati: { selectedTimeFormat: '24h' }, - curr: { definitions: { EUR, USD }, selectedCurrency: 'EUR' }, - unit: { selectedSystem: 'metric' }, - uiuxConfig: { theme: { mode: 'auto' } }, - }); - - // Módulos básicos - expect(actx.ling).toBeDefined(); - expect(actx.logr).toBeDefined(); - expect(actx.numr).toBeDefined(); - expect(actx.dati).toBeDefined(); - expect(actx.curr).toBeDefined(); - expect(actx.unit).toBeDefined(); - - // UIX reactivo - expect(actx.uiux).toBeDefined(); - expect(actx.uiux.theme).toBeDefined(); - expect(actx.uiux.toast).toBeDefined(); - expect(actx.uiux.modal).toBeDefined(); - expect(actx.uiux.break).toBeDefined(); - }); - }); - - describe('reactividad de locale', () => { - it('locale es reactivo y cambia con ling.setLocale()', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ ling }); - - expect(actx.locale).toBe('es'); - - // El cambio en ling debería reflejarse en el estado reactivo - // (en un entorno Svelte real, esto sería automático) - actx.ling.setLocale('en'); - - // En el entorno de test, verificamos que ling cambió - expect(actx.ling.getLocale()).toBe('en'); - }); - }); - - describe('uiux reactivo', () => { - it('theme tiene propiedades de lectura', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ - ling, - uiuxConfig: { theme: { mode: 'dark' } }, - }); - - // Props de lectura (reactivas en Svelte) - expect(actx.uiux.theme.resolved).toBeDefined(); - expect(actx.uiux.theme.mode).toBe('dark'); - }); - - it('theme tiene métodos de acción', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ - ling, - uiuxConfig: { theme: { mode: 'light' } }, - }); - - // Métodos de acción - expect(actx.uiux.theme.set).toBeInstanceOf(Function); - expect(actx.uiux.theme.toggle).toBeInstanceOf(Function); - expect(actx.uiux.theme.onChange).toBeInstanceOf(Function); - }); - - it('toast tiene propiedades de lectura', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ - ling, - uiuxConfig: { toast: { defaultDuration: 4000 } }, - }); - - // Props de lectura - expect(actx.uiux.toast.queue).toBeDefined(); - expect(Array.isArray(actx.uiux.toast.queue)).toBe(true); - }); - - it('toast tiene métodos de acción', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ ling }); - - expect(actx.uiux.toast.show).toBeInstanceOf(Function); - expect(actx.uiux.toast.dismiss).toBeInstanceOf(Function); - expect(actx.uiux.toast.clear).toBeInstanceOf(Function); - expect(actx.uiux.toast.onChange).toBeInstanceOf(Function); - }); - - it('modal tiene propiedades de lectura', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ ling }); - - expect(actx.uiux.modal.stack).toBeDefined(); - expect(Array.isArray(actx.uiux.modal.stack)).toBe(true); - }); - - it('modal tiene métodos de acción', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ ling }); - - expect(actx.uiux.modal.open).toBeInstanceOf(Function); - expect(actx.uiux.modal.close).toBeInstanceOf(Function); - expect(actx.uiux.modal.closeAll).toBeInstanceOf(Function); - expect(actx.uiux.modal.isOpen).toBeInstanceOf(Function); - expect(actx.uiux.modal.onChange).toBeInstanceOf(Function); - }); - - it('break tiene propiedades de lectura', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ ling }); - - expect(actx.uiux.break.current).toBeDefined(); - // Las propiedades computadas dependen del entorno - expect(typeof actx.uiux.break.isMobile).toBe('boolean'); - }); - - it('break tiene métodos de acción', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ ling }); - - expect(actx.uiux.break.is).toBeInstanceOf(Function); - expect(actx.uiux.break.onChange).toBeInstanceOf(Function); - }); - }); - - describe('integración con módulos', () => { - it('numr funciona correctamente', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ - ling, - numr: { maxDecimals: 2 }, - }); - - const result = actx.numr.f(1234.5); - expect(result).toBeTruthy(); - expect(result).toContain('5'); - }); - - it('curr funciona correctamente con pluralización', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ - ling, - curr: { definitions: { EUR }, selectedCurrency: 'EUR' }, - }); - - expect(actx.curr.f(1, 'name')).toBe('1,00 euro'); - expect(actx.curr.f(2, 'name')).toBe('2,00 euros'); - }); - - it('dati funciona correctamente', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ - ling, - dati: { selectedTimeFormat: '24h' }, - }); - - const date = new Date('2024-03-15'); - const result = actx.dati.f(date); - expect(result).toBeTruthy(); - }); - - it('unit funciona correctamente', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ - ling, - unit: { selectedSystem: 'metric' }, - }); - - const result = actx.unit.weight.f(75); - expect(result).toBeTruthy(); - expect(result).toContain('75'); - }); - }); - - describe('logr integrado con ling', () => { - it('logr usa ling para resolver mensajes', () => { - const ling = createLing(translations, 'es'); - const captured: string[] = []; - const actx = createACtx({ - ling, - logr: { - level: LogLevel.INFO, - transports: [ - { - write: (_entry: any, resolvedMessage: string) => captured.push(resolvedMessage) - } - ], - }, - }); - - actx.logr.info('test', '#?common.ok'); - expect(captured[0]).toBe('Aceptar'); - }); - }); - - describe('casos de uso reales', () => { - it('simula una aplicación multilingüe completa', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ - ling, - numr: { maxDecimals: 2 }, - curr: { definitions: { EUR, USD }, selectedCurrency: 'EUR' }, - dati: { selectedTimeFormat: '24h' }, - unit: { selectedSystem: 'metric' }, - uiuxConfig: { - theme: { mode: 'auto' }, - toast: { defaultDuration: 4000 }, - }, - }); - - // Traducciones - expect(actx.t('common.ok')).toBe('Aceptar'); - expect(actx.ts({ es: 'Hola', en: 'Hello' })).toBe('Hola'); - - // Formateo - expect(actx.numr.f(1000)).toBeTruthy(); - expect(actx.curr.f(100)).toBe('100,00 €'); - expect(actx.dati.f(new Date())).toBeTruthy(); - expect(actx.unit.weight.f(75)).toContain('75'); - - // Locale - expect(actx.locale).toBe('es'); - - // UIX - expect(actx.uiux.theme.mode).toBe('auto'); - - // Cambio de locale - actx.ling.setLocale('en'); - expect(actx.t('common.ok')).toBe('OK'); - }); - }); -}); diff --git a/src/lib/actx/test/actx.test.ts b/src/lib/actx/test/actx.test.ts index 5d9a3e486..b69d6d6f0 100644 --- a/src/lib/actx/test/actx.test.ts +++ b/src/lib/actx/test/actx.test.ts @@ -143,12 +143,6 @@ describe('createACtx (no reactivo)', () => { expect(() => actx.unit).toThrow('[actx] \'unit\' no está configurado'); }); - it('lanza error al acceder a uiux si no está configurado', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ ling }); - - expect(() => actx.uiux).toThrow('[actx] \'uiux\' no está configurado'); - }); }); describe('lazy initialization', () => { @@ -195,14 +189,6 @@ describe('createACtx (no reactivo)', () => { expect(actx.unit.weight).toBeDefined(); }); - it('uiux se inicializa en el primer acceso', () => { - const ling = createLing(translations, 'es'); - const actx = createACtx({ ling, uiux: { theme: { mode: 'light' } } }); - - expect(actx.uiux).toBeDefined(); - expect(actx.uiux.theme).toBeDefined(); - }); - it('múltiples accesos devuelven la misma instancia (singleton)', () => { const ling = createLing(translations, 'es'); const actx = createACtx({ ling, numr: {} }); @@ -401,7 +387,6 @@ describe('createACtx (no reactivo)', () => { decimals: 2, }, unit: { selectedSystem: 'metric' }, - uiux: { theme: { mode: 'dark' } }, }); // Todos los módulos están disponibles @@ -411,7 +396,6 @@ describe('createACtx (no reactivo)', () => { expect(actx.dati).toBeDefined(); expect(actx.curr).toBeDefined(); expect(actx.unit).toBeDefined(); - expect(actx.uiux).toBeDefined(); // Configuración de numr const numrResult = actx.numr.f(1234); @@ -428,8 +412,6 @@ describe('createACtx (no reactivo)', () => { // Configuración de unit expect(actx.unit.getSystem()).toBe('metric'); - // Configuración de uiux - expect(actx.uiux.theme.mode).toBe('dark'); // Configuración de logr expect(() => actx.logr.warn('test', 'msg')).not.toThrow(); @@ -446,7 +428,6 @@ describe('createACtx (no reactivo)', () => { curr: { definitions: { EUR }, selectedCurrency: 'EUR' }, unit: {}, logr: { level: LogLevel.NONE }, - uiux: { theme: { mode: 'auto' } }, }); // Traducción directa @@ -473,8 +454,6 @@ describe('createACtx (no reactivo)', () => { // Unidades expect(actx.unit.weight.f(75)).toContain('75'); - // UIX - expect(actx.uiux.theme.mode).toBe('auto'); // Cambio de locale actx.ling.setLocale('en'); diff --git a/src/lib/index.ts b/src/lib/index.ts index 856f2b6c3..3dc2d6b60 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1 +1,8 @@ -// place files you want to import through the `$lib` alias in this folder. + + + +export * from './actx/lib'; +export * from './glob/lib'; +export * from './ling/lib'; +export * from './logr/lib'; +export * from './util'; diff --git a/src/lib/uiux/lib/break_engine.ts b/src/lib/uiux/lib/break_engine.ts deleted file mode 100644 index 375244846..000000000 --- a/src/lib/uiux/lib/break_engine.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { BreakConfig, BreakInstance, BreakpointKey } from './types.ts'; - -// Breakpoints por defecto — coinciden con Tailwind v3 -const DEFAULT_POINTS: Record = { - sm : 640, - md : 768, - lg : 1024, - xl : 1280, - '2xl': 1536, -}; - -/** - * Pre-ordena los breakpoints de mayor a menor una sola vez. - * Evita re-ordenar en cada resize. - */ -function sortedEntries(points: Record): [string, number][] { - return Object.entries(points).sort(([, a], [, b]) => b - a); -} - -/** - * Resuelve el breakpoint activo dado un ancho y entries pre-ordenados. - * Devuelve la clave del mayor breakpoint que el ancho cumple, o 'xs' si ninguno. - */ -function resolveBreakpoint(width: number, sorted: [string, number][]): BreakpointKey { - for (const [key, value] of sorted) { - if (width >= value) return key; - } - return 'xs'; -} - -export function createBreak(config: BreakConfig = {}): BreakInstance { - const points = config.points ?? DEFAULT_POINTS; - const sorted = sortedEntries(points); - - const getWidth = () => typeof window !== 'undefined' ? window.innerWidth : 0; - - let current : BreakpointKey = resolveBreakpoint(getWidth(), sorted); - const listeners = new Set<(breakpoint: BreakpointKey) => void>(); - - let observer: ResizeObserver | null = null; - - // ResizeObserver en browser, noop en SSR - if (typeof window !== 'undefined') { - observer = new ResizeObserver(() => { - const next = resolveBreakpoint(window.innerWidth, sorted); - if (next === current) return; - current = next; - listeners.forEach(fn => fn(current)); - }); - observer.observe(document.documentElement); - } - - function is(breakpoint: BreakpointKey): boolean { - const min = points[breakpoint]; - if (min === undefined) return false; - return getWidth() >= min; - } - - function onChange(fn: (breakpoint: BreakpointKey) => void): () => void { - listeners.add(fn); - return () => listeners.delete(fn); - } - - function dispose(): void { - observer?.disconnect(); - observer = null; - listeners.clear(); - } - - return { - get current() { return current; }, - get isMobile() { return !is('md'); }, - get isTablet() { return is('md') && !is('lg'); }, - get isDesktop() { return is('lg'); }, - is, - onChange, - dispose, - }; -} diff --git a/src/lib/uiux/lib/engine.svelte.ts b/src/lib/uiux/lib/engine.svelte.ts deleted file mode 100644 index 54c16d78f..000000000 --- a/src/lib/uiux/lib/engine.svelte.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * uiux_engine.svelte.ts — Factory reactivo del contexto de UI. - * - * Encapsula todo el $state de uiux en su propio módulo, - * igual que numr_engine.ts o curr_engine.ts. - * - * Si uiux crece con nuevos módulos (notifications, shortcuts, etc.) - * solo hay que tocar este fichero y ReactiveUiux — actx no se entera. - */ - -import type { UiuxConfig, ReactiveUiux, ThemeMode, Toast, ModalEntry } from './types.ts'; -import { createUiux } from './engine.ts'; - - - -export function createUiuxState(config: UiuxConfig = {}): ReactiveUiux { - - const _uiux = createUiux(config); - - // ── $state ──────────────────────────────────────────────────────────────── - - let themeResolved = $state<'light' | 'dark'>(_uiux.theme.resolved); - let themeMode = $state(_uiux.theme.mode); - let toastQueue = $state([]); - let modalStack = $state([]); - let breakCurrent = $state(_uiux.break.current); - let openMenus = $state>(new Set()); - - // ── Sincronización engines → $state ────────────────────────────────────── - - _uiux.theme.onChange((mode, resolved) => { - themeMode = mode; - themeResolved = resolved; - }); - - _uiux.toast.onChange(q => { toastQueue = q; }); - _uiux.modal.onChange(s => { modalStack = s; }); - _uiux.break.onChange(bp => { breakCurrent = bp; }); - _uiux.menu.onChange(ids => { openMenus = new Set(ids); }); - - // ── ReactiveUiux ────────────────────────────────────────────────────────── - - return { - theme: { - get resolved() { return themeResolved; }, - get mode() { return themeMode; }, - set : _uiux.theme.set.bind(_uiux.theme), - toggle : _uiux.theme.toggle.bind(_uiux.theme), - onChange: _uiux.theme.onChange.bind(_uiux.theme), - }, - toast: { - get queue() { return toastQueue; }, - show : _uiux.toast.show.bind(_uiux.toast), - dismiss : _uiux.toast.dismiss.bind(_uiux.toast), - clear : _uiux.toast.clear.bind(_uiux.toast), - onChange: _uiux.toast.onChange.bind(_uiux.toast), - }, - modal: { - get stack() { return modalStack; }, - get active() { - return modalStack.length > 0 - ? modalStack[modalStack.length - 1] - : null; - }, - open : _uiux.modal.open.bind(_uiux.modal), - close : _uiux.modal.close.bind(_uiux.modal), - closeAll: _uiux.modal.closeAll.bind(_uiux.modal), - isOpen : _uiux.modal.isOpen.bind(_uiux.modal), - onChange: _uiux.modal.onChange.bind(_uiux.modal), - }, - menu: { - open : _uiux.menu.open.bind(_uiux.menu), - close : _uiux.menu.close.bind(_uiux.menu), - toggle : _uiux.menu.toggle.bind(_uiux.menu), - isOpen : (id: string) => openMenus.has(id), - closeAll: _uiux.menu.closeAll.bind(_uiux.menu), - onChange: _uiux.menu.onChange.bind(_uiux.menu), - }, - break: { - get current() { return breakCurrent; }, - get isMobile() { return !_uiux.break.is('md'); }, - get isTablet() { return _uiux.break.is('md') && !_uiux.break.is('lg'); }, - get isDesktop() { return _uiux.break.is('lg'); }, - is : _uiux.break.is.bind(_uiux.break), - onChange: _uiux.break.onChange.bind(_uiux.break), - }, - }; -} \ No newline at end of file diff --git a/src/lib/uiux/lib/engine.ts b/src/lib/uiux/lib/engine.ts deleted file mode 100644 index 8cd6021b9..000000000 --- a/src/lib/uiux/lib/engine.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Uiux, UiuxConfig } from './types.ts'; -import { createTheme } from './theme_engine.ts'; -import { createBreak } from './break_engine.ts'; -import { createToast } from './toast_engine.ts'; -import { createModal } from './modal_engine.ts'; -import { createMenu } from './menu_engine.ts'; - -/** - * Crea el contexto de UI de la aplicación. - * - * Todos los módulos están siempre disponibles — no requieren config explícita. - * La config solo ajusta los defaults (tema inicial, breakpoints, duración de toasts). - * - * Diseñado para funcionar como módulo independiente o como parte de `ACtx`: - * - * @example - * // Standalone - * const uiux = createUiux({ theme: { mode: 'auto' } }); - * - * @example - * // Integrado en ACtx (cuando se añada uiux a ACtxConfig) - * actx.uiux.theme.toggle() - * actx.uiux.toast.show({ es: 'Guardado', en: 'Saved' }, { type: 'success' }) - * actx.uiux.modal.open('confirm', { title: '¿Eliminar?' }) - * actx.uiux.menu.toggle('sidebar') - * actx.uiux.break.isMobile - * - * @remarks - * **Persistencia**: el tema se guarda en memoria en esta versión. - * Cuando `stor` esté disponible, `ACtx` conectará `uiux.theme` con - * `stor.local.get/set('uiux:theme')` sin modificar esta librería. - */ -export function createUiux(config: UiuxConfig = {}): Uiux { - return { - theme : createTheme(config.theme), - break : createBreak(config.break), - toast : createToast(config.toast?.defaultDuration), - modal : createModal(), - menu : createMenu(), - }; -} diff --git a/src/lib/uiux/lib/index.ts b/src/lib/uiux/lib/index.ts deleted file mode 100644 index 1e30c8a0e..000000000 --- a/src/lib/uiux/lib/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './types.ts'; -export * from './engine.ts'; -export * from './theme_engine.ts'; -export * from './break_engine.ts'; -export * from './toast_engine.ts'; -export * from './modal_engine.ts'; -export * from './menu_engine.ts'; diff --git a/src/lib/uiux/lib/menu_engine.ts b/src/lib/uiux/lib/menu_engine.ts deleted file mode 100644 index d53e65d3b..000000000 --- a/src/lib/uiux/lib/menu_engine.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { MenuInstance } from './types.ts'; - -export function createMenu(): MenuInstance { - const openIds = new Set(); - const listeners = new Set<(openIds: Set) => void>(); - - function notify(): void { - const snapshot = new Set(openIds); - listeners.forEach(fn => fn(snapshot)); - } - - function open(id: string): void { - if (openIds.has(id)) return; - openIds.add(id); - notify(); - } - - function close(id: string): void { - if (!openIds.has(id)) return; - openIds.delete(id); - notify(); - } - - function toggle(id: string): void { - openIds.has(id) ? close(id) : open(id); - } - - function isOpen(id: string): boolean { - return openIds.has(id); - } - - function closeAll(): void { - if (openIds.size === 0) return; - openIds.clear(); - notify(); - } - - function onChange(fn: (openIds: Set) => void): () => void { - listeners.add(fn); - return () => listeners.delete(fn); - } - - return { open, close, toggle, isOpen, closeAll, onChange }; -} diff --git a/src/lib/uiux/lib/modal_engine.ts b/src/lib/uiux/lib/modal_engine.ts deleted file mode 100644 index 5578a1534..000000000 --- a/src/lib/uiux/lib/modal_engine.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { ModalEntry, ModalInstance } from './types.ts'; - -export function createModal(): ModalInstance { - let stack: ModalEntry[] = []; - const listeners = new Set<(stack: ModalEntry[]) => void>(); - - function notify(): void { - const snapshot = [...stack]; - listeners.forEach(fn => fn(snapshot)); - } - - function open

= Record>(id: string, props?: P): void { - stack = [...stack, { id, props: props ?? ({} as P) }]; - notify(); - } - - function close(): void { - if (stack.length === 0) return; - stack = stack.slice(0, -1); - notify(); - } - - function closeAll(): void { - if (stack.length === 0) return; - stack = []; - notify(); - } - - function isOpen(id: string): boolean { - return stack.some(m => m.id === id); - } - - function onChange(fn: (stack: ModalEntry[]) => void): () => void { - listeners.add(fn); - return () => listeners.delete(fn); - } - - return { - get stack() { return stack; }, - get active() { return stack.length > 0 ? stack[stack.length - 1] : null; }, - open, - close, - closeAll, - isOpen, - onChange, - }; -} diff --git a/src/lib/uiux/lib/theme_engine.ts b/src/lib/uiux/lib/theme_engine.ts deleted file mode 100644 index cfeea4b5a..000000000 --- a/src/lib/uiux/lib/theme_engine.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { ThemeConfig, ThemeInstance, ThemeMode } from './types.ts'; - -const DEFAULT_DARK_CLASS = 'dark'; - -/** Resuelve 'auto' al modo del sistema operativo. */ -function resolveSystemMode(): 'light' | 'dark' { - if (typeof window === 'undefined') return 'light'; - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; -} - -function applyTheme( - resolved : 'light' | 'dark', - darkClass : string, - vars? : ThemeConfig['vars'] -): void { - if (typeof document === 'undefined') return; - - const html = document.documentElement; - - // Clase dark de Tailwind - resolved === 'dark' - ? html.classList.add(darkClass) - : html.classList.remove(darkClass); - - // Variables CSS custom - if (vars) { - const themeVars = vars[resolved] ?? {}; - for (const [key, value] of Object.entries(themeVars)) { - html.style.setProperty(key, value); - } - } -} - -export function createTheme(config: ThemeConfig = {}): ThemeInstance { - const darkClass = config.darkClass ?? DEFAULT_DARK_CLASS; - - let currentMode: ThemeMode = config.mode ?? 'auto'; - let resolved : 'light' | 'dark' = currentMode === 'auto' - ? resolveSystemMode() - : currentMode; - - const listeners = new Set<(mode: ThemeMode, resolved: 'light' | 'dark') => void>(); - - // Escucha cambios del sistema cuando el modo es 'auto' - let mediaUnsubscribe: (() => void) | undefined; - - function subscribeToSystem(): void { - if (typeof window === 'undefined') return; - const mq = window.matchMedia('(prefers-color-scheme: dark)'); - const handler = () => { - if (currentMode !== 'auto') return; - resolved = mq.matches ? 'dark' : 'light'; - applyTheme(resolved, darkClass, config.vars); - listeners.forEach(fn => fn(currentMode, resolved)); - }; - mq.addEventListener('change', handler); - mediaUnsubscribe = () => mq.removeEventListener('change', handler); - } - - function unsubscribeFromSystem(): void { - mediaUnsubscribe?.(); - mediaUnsubscribe = undefined; - } - - function set(mode: ThemeMode): void { - currentMode = mode; - - if (mode === 'auto') { - resolved = resolveSystemMode(); - subscribeToSystem(); - } else { - unsubscribeFromSystem(); - resolved = mode; - } - - applyTheme(resolved, darkClass, config.vars); - listeners.forEach(fn => fn(currentMode, resolved)); - } - - function toggle(): void { - set(resolved === 'dark' ? 'light' : 'dark'); - } - - function onChange(fn: (mode: ThemeMode, resolved: 'light' | 'dark') => void): () => void { - listeners.add(fn); - return () => listeners.delete(fn); - } - - // Aplicar tema inicial - applyTheme(resolved, darkClass, config.vars); - if (currentMode === 'auto') subscribeToSystem(); - - return { - get resolved() { return resolved; }, - get mode() { return currentMode; }, - set, - toggle, - onChange, - }; -} diff --git a/src/lib/uiux/lib/toast_engine.ts b/src/lib/uiux/lib/toast_engine.ts deleted file mode 100644 index 77a963cb9..000000000 --- a/src/lib/uiux/lib/toast_engine.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { Toast, ToastInstance, ToastOptions } from './types.ts'; -import type { LingString } from '@/ling/lib'; - -const DEFAULT_DURATION = 4000; - -let _id = 0; -function nextId(): string { return `toast-${++_id}`; } - -export function createToast(defaultDuration = DEFAULT_DURATION): ToastInstance { - let queue: Toast[] = []; - const listeners = new Set<(queue: Toast[]) => void>(); - const timers = new Map>(); - - function notify(): void { - const snapshot = [...queue]; - listeners.forEach(fn => fn(snapshot)); - } - - function dismiss(id: string): void { - const timer = timers.get(id); - if (timer) { clearTimeout(timer); timers.delete(id); } - queue = queue.filter(t => t.id !== id); - notify(); - } - - function show(message: LingString, opts: ToastOptions = {}): string { - const id = nextId(); - const duration = opts.duration ?? defaultDuration; - - const toast: Toast = { - id, - type : opts.type ?? 'info', - message, - duration, - context : opts.context, - }; - - queue = [...queue, toast]; - notify(); - - // Auto-dismiss si duration > 0 - if (duration > 0) { - timers.set(id, setTimeout(() => dismiss(id), duration)); - } - - return id; - } - - function clear(): void { - timers.forEach(t => clearTimeout(t)); - timers.clear(); - queue = []; - notify(); - } - - function onChange(fn: (queue: Toast[]) => void): () => void { - listeners.add(fn); - return () => listeners.delete(fn); - } - - return { - get queue() { return queue; }, - show, - dismiss, - clear, - onChange, - }; -} diff --git a/src/lib/uiux/lib/types.ts b/src/lib/uiux/lib/types.ts deleted file mode 100644 index ee5f4cf24..000000000 --- a/src/lib/uiux/lib/types.ts +++ /dev/null @@ -1,208 +0,0 @@ -// ============================== -// UIUX TYPES -// ============================== - -import type { LingString } from '@/ling/lib'; - -// ============================================================================ -// THEME -// ============================================================================ - -export type ThemeMode = 'light' | 'dark' | 'auto'; - - - -export interface ThemeConfig { - /** Modo inicial. Default: `'auto'`. */ - mode? : ThemeMode; - /** - * Clase CSS aplicada a `` en modo dark. - * Debe coincidir con la configuración de Tailwind (`darkMode: 'class'`). - * Default: `'dark'`. - */ - darkClass? : string; - /** - * Variables CSS custom por tema. Se inyectan en `` como `style`. - * Útil para temas custom más allá de light/dark. - * @example - * vars: { light: { '--color-brand': '#3B82F6' }, dark: { '--color-brand': '#60A5FA' } } - */ - vars? : Partial>>; -} - -export interface ThemeInstance { - /** Modo activo resuelto — nunca `'auto'`, siempre `'light'` o `'dark'`. */ - readonly resolved : 'light' | 'dark'; - /** Modo configurado — puede ser `'auto'`. */ - readonly mode : ThemeMode; - set : (mode: ThemeMode) => void; - toggle : () => void; - onChange : (fn: (mode: ThemeMode, resolved: 'light' | 'dark') => void) => () => void; -} - -// ============================================================================ -// BREAKPOINTS -// ============================================================================ - -export type BreakpointKey = string; - -export interface BreakConfig { - /** - * Mapa de breakpoints en px. Default: breakpoints de Tailwind v3. - * @example { sm: 640, md: 768, lg: 1024, xl: 1280, '2xl': 1536 } - */ - points?: Record; -} - -export interface BreakInstance { - /** Breakpoint activo según el ancho actual de la ventana. */ - readonly current : BreakpointKey; - readonly isMobile : boolean; // < md - readonly isTablet : boolean; // >= md && < lg - readonly isDesktop: boolean; // >= lg - /** Devuelve true si el viewport cumple el breakpoint mínimo indicado. */ - is : (breakpoint: BreakpointKey) => boolean; - onChange : (fn: (breakpoint: BreakpointKey) => void) => () => void; - /** Desconecta el ResizeObserver y limpia los listeners. */ - dispose : () => void; -} - -// ============================================================================ -// TOAST -// ============================================================================ - -export type ToastType = 'success' | 'error' | 'warn' | 'info'; - -export interface ToastOptions { - /** Tipo visual del toast. Default: `'info'`. */ - type? : ToastType; - /** - * Duración en ms antes de auto-dismiss. `0` = permanente. - * Default: `4000`. - */ - duration?: number; - /** Contexto adicional — por ejemplo un código de error. */ - context? : Record; -} - -export interface Toast { - id : string; - type : ToastType; - message : LingString; - duration: number; - context?: Record; -} - -export interface ToastInstance { - /** Cola de toasts activos. */ - readonly queue : Toast[]; - show : (message: LingString, opts?: ToastOptions) => string; - dismiss : (id: string) => void; - clear : () => void; - onChange : (fn: (queue: Toast[]) => void) => () => void; -} - -// ============================================================================ -// MODAL -// ============================================================================ - -export interface ModalEntry

= Record> { - id : string; - props : P; -} - -export interface ModalInstance { - /** Stack de modales abiertos. El último es el activo. */ - readonly stack : ModalEntry[]; - /** Modal activo (último del stack) o `null` si no hay ninguno. */ - readonly active : ModalEntry | null; - open :

= Record>(id: string, props?: P) => void; - close : () => void; - closeAll : () => void; - isOpen : (id: string) => boolean; - onChange : (fn: (stack: ModalEntry[]) => void) => () => void; -} - -// ============================================================================ -// MENU -// ============================================================================ - -export interface MenuInstance { - open : (id: string) => void; - close : (id: string) => void; - toggle : (id: string) => void; - isOpen : (id: string) => boolean; - closeAll: () => void; - onChange: (fn: (openIds: Set) => void) => () => void; -} - -// ============================================================================ -// UIUX CONFIG & INSTANCE -// ============================================================================ - -export interface UiuxConfig { - theme? : ThemeConfig; - break? : BreakConfig; - toast? : { defaultDuration?: number }; -} - -/** - * Contexto de UI de la aplicación. - * Gestiona tema, breakpoints, toasts, modales y menús. - * - * Todos los módulos son siempre disponibles — no requieren config explícita. - * La config solo ajusta los defaults. - * - * @example - * uiux.theme.toggle() - * uiux.toast.show({ es: 'Guardado', en: 'Saved' }, { type: 'success' }) - * uiux.modal.open('confirm', { title: '¿Eliminar?' }) - * uiux.menu.toggle('sidebar') - * uix.break.isMobile - */ -export interface Uiux { - theme : ThemeInstance; - break : BreakInstance; - toast : ToastInstance; - modal : ModalInstance; - menu : MenuInstance; -} - - - - -// ============================================================================ -// REACTIVE UIX -// ============================================================================ - -/** - * Versión reactiva de `Uiux` para el singleton de Svelte (`actx.svelte.ts`). - * - * Idéntica a `Uiux` en métodos y estructura, pero las propiedades de solo - * lectura (`resolved`, `mode`, `queue`, `stack`, `active`, `current`, - * `isMobile`, `isTablet`, `isDesktop`) están respaldadas por `$state` — - * Svelte las observa y re-renderiza automáticamente cuando cambian. - * - * No añade ni elimina ninguna funcionalidad — solo garantiza reactividad - * en las propiedades que los componentes Svelte necesitan leer. - */ -export interface ReactiveUiux { - theme: Omit & { - readonly resolved: 'light' | 'dark'; // $state - readonly mode : ThemeMode; // $state - }; - toast: Omit & { - readonly queue: Toast[]; // $state - }; - modal: Omit & { - readonly stack : ModalEntry[]; // $state - readonly active: ModalEntry | null; // $state derivado - }; - menu : MenuInstance; // sin estado reactivo - break: Omit & { - readonly current : string; // $state - readonly isMobile : boolean; // derivado de $state - readonly isTablet : boolean; // derivado de $state - readonly isDesktop: boolean; // derivado de $state - }; -} \ No newline at end of file diff --git a/src/routes/test/+page.svelte b/src/routes/test/+page.svelte index 77ff0dd9e..2b5b9c012 100644 --- a/src/routes/test/+page.svelte +++ b/src/routes/test/+page.svelte @@ -42,10 +42,6 @@ {actx.locale.toUpperCase()} - {actx.uiux.break.current} - @@ -302,7 +298,7 @@ .logo em { color: #f59e0b; font-style: normal; } .header-right { display: flex; align-items: center; gap: .75rem; } - .locale-badge, .break-badge { + .locale-badge { padding: .2rem .6rem; border: 1px solid #f59e0b; border-radius: 2px;