You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/continue.md

12 KiB

Continue tomorrow

Fecha de corte: 2026-05-24. Rama: active-uix (worktree: .claude/worktrees/keen-mcnulty-383812 → branch claude/keen-mcnulty-383812, pero los cambios reales viven en el árbol principal).

TL;DR de la sesión

Sesión de saneamiento + ampliación:

  1. NumberField demo: re-construido para exponer Intl.NumberFormatOptions completo (locale, style, currency, currencyDisplay, unit, unitDisplay, decimales, useGrouping, notation, signDisplay) con 9 presets canónicos.
  2. Listas hardcoded eliminadas: el demo ahora deriva locales, monedas y unidades del ecosistema ($libs/langs, $libs/currency, $format/units).
  3. Bug crítico de locale propagation: descubierto y arreglado en soma/NumberField + soma/Calendar + soma/RangeCalendar. El prop locale era ignorado cuando UIX tenía format service activo.
  4. TextArea construido de cero — full stack (morfo + soma + eidos + sema + demo canónico 6-tabs).
  5. Decisión arquitectónica: NO se crean CurrencyField / UnitField separados. Se confirma el patrón canónico headless mainstream (React Aria, Ark, Bits, Chakra v3, Radix Vue) — un solo NumberField con formatOptions.

Decisiones tomadas hoy

1. Patrón canónico para number/currency/unit inputs

Decisión final: un solo <NumberField> con formatOptions: Intl.NumberFormatOptions. NO se crean componentes separados.

Razones (validadas contra 12 librerías):

  • React Aria, Ark UI, Bits UI, Chakra v3, Radix Vue, shadcn-vue → todos usan NumberField + formatOptions con los modos W3C estándar (style: 'decimal' | 'currency' | 'unit' | 'percent').
  • PrimeReact es el único outlier: usa mode="currency" enum, pero sigue siendo UN solo componente.
  • Mantine y Ant Design usan prefix / suffix strings (lo más débil).
  • En la rama histórica morfo-driven-soma el air/number-field tampoco tenía Currency/Unit — solo skin sobre terra.NumberField.

Implicación: para usar como moneda o unidad, el consumidor pasa formatOptions={{ style: 'currency', currency: 'EUR' }} o formatOptions={{ style: 'unit', unit: 'kilogram' }}. CLDR-aligned.

2. Bug de locale prop ignorado (soma)

Origen: soma/NumberFieldProvider delegaba todas las operaciones a soma.nums (servicio activo UIX) cuando estaba disponible. soma.nums.format(value, opts) solo acepta Intl.NumberFormatOptions — no acepta locale como argumento. El locale interno viene de prefs.locale, no del prop del componente.

Mismo bug en soma/Calendar y soma/RangeCalendar: dateOrder leía directamente soma.dates.getDateOrder() ignorando el prop locale. DateField ya lo hacía bien (precedencia explícita prop > service > locale default); las dos restantes no.

Patrón de fix aplicado (en los 3 archivos):

// Bypass del servicio activo cuando el consumidor pasa locale explícito.
private get hasOwnLocale(): boolean {
    return this.opts.locale.current !== undefined;
}

readonly decimalSeparator = $derived.by(() => {
    if (this.hasOwnLocale) return resolveDecimalSeparator(this.locale.current);
    return this.soma.nums?.getDecimalSeparator() ?? resolveDecimalSeparator(this.locale.current);
});

5 puntos parcheados en number-field-provider: decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue. En calendar y range-calendar: solo dateOrder (la única ruta que ignoraba el prop).

Semántica resultante (consistente en NumberField + DateField + Calendar + RangeCalendar):

Caso Comportamiento
<NumberField> sin prop locale Respeta prefs.locale del app (sin cambio)
<NumberField locale="en-US"> Bypassa soma.nums → formato en-US garantizado

3. Listas hardcoded en demo → ecosistema centralizado

Antes: el demo de NumberField tenía 3 arrays inventados (7 locales, 8 currencies, 12 units).

Después:

import { REGION_CURRENCY } from '$libs/currency';
import { UNIT_DEFINITIONS } from '$format/units';
import type { LangBase } from '$libs/langs';
  • Languages: 10 base langs de LangBase × tabla regional curada → 30 BCP47 locales reales (en-US, es-AR, pt-BR, ar-EG, etc).
  • Currencies: [...new Set(Object.values(REGION_CURRENCY))].sort() → ~155 monedas ISO 4217.
  • Units: Object.entries(UNIT_DEFINITIONS) agrupadas por UnitKind en <optgroup> por tipo (length / distance / mass / volume / temperature / speed) → 25 unidades CLDR.

4. TextArea — full stack

API (sigue Mantine + Radix + PrimeReact, no inventa nada):

<TextArea
  bind:value
  autosize minRows={3} maxRows={8}
  resize="vertical"        {/* none | vertical | horizontal | both */}
  submitOn="mod+enter"      {/* false | enter | shift+enter | mod+enter */}
  maxLength={280}
  size="md" variant="surface" color="primary"
  onSubmit={(v) => …}
>
  <TextArea.Input placeholder="…" />
  <TextArea.Count />        {/* default: count / max */}
</TextArea>

Morfo: 3 parts (provider, input, count), 3 events (commit-input, commit-submit, shift-count-overflow).

Autosize: scrollHeight measurement por evento de input. Reset height=auto → mide → clamp entre minRows*lineHeight y maxRows*lineHeight. Respeta padding/border. Cuando autosize=true, la handle de resize se suprime (suppresses CSS resize) porque lucharían entre sí.

Submit shortcut: intercepta Enter selectivamente. Por defecto OFF (deja que Enter inserte newline). Las 3 modalidades:

  • 'enter' — Enter (sin modifier) → onSubmit, prevent newline.
  • 'shift+enter' — Shift+Enter → onSubmit, Enter sigue insertando newline.
  • 'mod+enter' — Ctrl/Cmd+Enter → onSubmit.

maxLength: doble bloqueo (atributo nativo + soft clamp en commit() para paste events que excedan). Emite shift-count-overflow con intent threat cuando el usuario intenta pasarse.

Field integration: OR-merge de disabled/readonly/required/invalid con el <Field> padre. Field.Label gana sobre aria-label propio.

5. NO crear más componentes ahora

Catálogo actual (97 morfos) ya excede shadcn (~50), Radix (~30). Las únicas ausencias notables vs mainstream:

Faltante Equivalente parcial Prioridad
Skeleton (loader placeholder) — media
Spinner (determinate loader) progress indeterminate media
PasswordField editable + custom toggle baja
HoverCard link-preview (similar) baja
DataTable (sort/filter compuesto) table media-alta
Sidebar (app shell pattern) — baja (composición)

Archivos modificados/creados hoy

Fix soma (3 providers)

  • src/uix/soma/components/number-field/number-field-provider.svelte.ts — getter hasOwnLocale + 5 puntos bypass.
  • src/uix/soma/components/calendar/calendar-provider.svelte.ts — dateOrder derivado respeta prop locale.
  • src/uix/soma/components/range-calendar/range-calendar-provider.svelte.ts — idem dateOrder.

TextArea nuevo (12 archivos)

  • src/uix/morfo/components/textarea.ts
  • src/uix/soma/components/textarea/
    • types.ts, langs.ts, textarea-provider.svelte.ts, exports.ts, index.ts
    • components/textarea.svelte, textarea-input.svelte, textarea-count.svelte
  • src/uix/eidos/components/textarea/
    • textarea.svelte, textarea-input.svelte, textarea-count.svelte, textarea.css, types.ts, index.ts
  • src/uix/sema/components/textarea.ts
  • src/uix/soma/types/html.ts — añadido PrimitiveTextareaAttributes
  • src/uix/soma/types/index.ts — export añadido
  • src/uix/soma/components/index.ts — barrel
  • src/uix/sema/components/index.ts — barrel

Demo NumberField

  • web/routes/uix/components/number-field/+page.svelte — imports del ecosistema, locale del format service, presets canónicos.

Demo TextArea + layout

  • web/routes/uix/components/textarea/+page.svelte — 410 líneas, 6 tabs.
  • web/routes/uix/+layout@.svelte — TextArea añadido al sidebar Forms + sema registrada.

Trabajo pre-sesión (uncommitted previo)

El working tree también contiene trabajo de sesiones anteriores no commiteado todavía:

  • Ports a eidos: announce, clipboard, drag-drop, feed, table, tree-grid, tree-view, virtual-grid, virtual-list, words
  • Sus demos en web/routes/uix/components/
  • Sema events de utility morfos
  • Tree indent guides via ::before (TreeView) y stacked gradients (TreeGrid)
  • Sidebar reorganizado con grupos Data + Utilities

Verificación

svelte-check: 0 errores en archivos modificados/creados.

NO verificado visualmente en navegador — el preview server vivía en worktree, hubo confusión entre worktree y main project durante la sesión. Para mañana: arrancar npm run dev desde el árbol principal (G:/dev/svelte/vicen, NO desde el worktree) y comprobar:

  1. NumberField: cambiar el <select> de locale de es-ES a en-US — el separador decimal debe pasar de coma a punto inmediatamente.
  2. Compact preset con locale en-US debe mostrar 1.2M (no 1,2 M).
  3. TextArea: probar autosize escribiendo texto largo, ver el counter ponerse rojo al pasar maxLength, probar submitOn="mod+enter".
  4. Calendar: con prop locale="en-US" los segmentos del header deben ordenarse MDY, no DMY.

Próximos pasos (orden recomendado)

  1. Verificación visual (1h) — los 4 puntos de arriba.
  2. Skeleton + Spinner (2-3h) — primitivos eidos puros, bajo coste, alto retorno. Skeleton genérico con shape (rect / circle / text), Spinner con size + color. NO morfo nuevo (son layout primitives, scope ['eidos'] solo).
  3. PasswordField (1-2h) — wrapper sobre editable con type="password", toggle de visibility y eidos chrome consistente con SearchField. Reusa el morfo de editable (mismo APG textbox); solo añade un eidos wrapper.
  4. Audit visual completo del demo de TextArea — comprobar tabs Live / API / Morfo / Sema / Recipe / A11y, todas las combinaciones de variant
    • color + size.

Patrones canon establecidos hoy

Locale prop precedence en componentes que consumen format/dates service:

Si el componente expone un prop locale, debe puentear el servicio activo de UIX (soma.nums, soma.dates) cuando el prop es defined. El patrón es un getter hasOwnLocale que toggea entre Intl directo (locale del prop) y el servicio (locale del app prefs.locale). NUNCA delegar ciegamente al servicio si el componente expone el prop.

Catálogo de listas dependientes de locale:

Lista Fuente canónica
Locales LangBase ($libs/langs) + tabla regional curada
Currencies REGION_CURRENCY ($libs/currency) → Set.values
Units UNIT_DEFINITIONS ($format/units) → group por UnitKind
Date orders resolveDateOrder() de $libs/days
Hour cycles getDefaultHourCycle() de $libs/days

Cualquier demo que enumere alguna de estas listas DEBE importarlas; nunca hardcodear.

**TextArea = referencia para nuevos form controls de una sola parte

  • count + Field integration.** Si construyes un componente con:
  • input/control único
  • value bindable
  • Field integration (OR-merge)
  • counter o indicator opcional
  • shortcut keys configurable

Clónalo. La estructura provider + input + count con sus 3 wrappers svelte es replicable.

Comandos útiles

cd G:/dev/svelte/vicen
npm run check               # svelte-check
npm run dev                 # dev server (DESDE el árbol principal, NO worktree)
npx vitest run src/uix/soma/components/number-field

Estado del working tree

Sucio. ~17 archivos modificados + 32 paths nuevos (incluye trabajo de sesiones anteriores aún no commiteado). Hoy se debería hacer:

  1. git add selectivo de TODO el trabajo acumulado.
  2. Commit unificado o split por dominio.
  3. Push a gita/active-uix (34 commits locales pendientes de push).

Branch principal: active-uix. Worktree de trabajo de la sesión: .claude/worktrees/keen-mcnulty-383812 — los archivos del worktree NO están en sync con los del árbol principal; los edits de la sesión fueron al árbol principal (correcto, era lo que el preview server necesitaba).

Powered by TurnKey Linux.