17 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:
- NumberField demo: re-construido para exponer
Intl.NumberFormatOptionscompleto (locale, style, currency, currencyDisplay, unit, unitDisplay, decimales, useGrouping, notation, signDisplay) con 9 presets canónicos. - Listas hardcoded eliminadas: el demo ahora deriva locales, monedas y
unidades del ecosistema (
$libs/langs,$libs/currency,$format/units). - Bug crítico de locale propagation: descubierto y arreglado en
soma/NumberField + soma/Calendar + soma/RangeCalendar. El prop
localeera ignorado cuando UIX teníaformatservice activo. - TextArea construido de cero — full stack (morfo + soma + eidos + sema + demo canónico 6-tabs).
- Decisión arquitectónica: NO se crean
CurrencyField/UnitFieldseparados. Se confirma el patrón canónico headless mainstream (React Aria, Ark, Bits, Chakra v3, Radix Vue) — un soloNumberFieldconformatOptions.
Actualización final — Words cerrado para retomar mañana
Después del corte anterior se completó el componente Words hasta demo funcional:
src/uix/morfo/components/words.ts— contrato Morfo con 8 partes y 6 eventos.src/uix/soma/components/words/— motor JSON, selection, input pipeline, commands, history, render HTML seguro, hidden input y tests.src/uix/eidos/components/words/— wrapper compound<Words>+<Words.Toolbar>,<Words.Content>,<Words.CommandButton>, etc. y receta CSS.src/uix/sema/components/words.ts— pack Sema para focus, commit, format, history, clear e invalid input.src/uix/langs/components/words.ts— catálogo de strings para eliminar warnings de fallback.web/routes/uix/components/words/+page.svelte— demo canónica 6 tabs con stage permanente, snippets Soma/Eidos, Morfo/Sema/Recipe/A11y.
Verificación específica de Words
Comandos ejecutados:
npx vitest run src/uix/soma/components/words/engine/engine.test.ts src/uix/soma/components/words/words-provider.svelte.test.ts
node --import tsx/esm scripts/component-audit.ts --only words
npx prettier --check src/uix/eidos/components/words src/uix/sema/components/words.ts src/uix/sema/components/words/README.md web/routes/uix/components/words src/uix/eidos/lib/recipes/base.ts src/uix/eidos/index.css web/routes/uix/+layout@.svelte src/uix/sema/components/index.ts src/uix/soma/components/words/README.md src/uix/words/README.md src/uix/langs/components/words.ts src/uix/langs/components/index.ts
npm run check 2>&1 | Select-String -Pattern 'words|Words'
Resultado:
- Tests Words: 23 passed.
- Component audit
--only words: PASS. - Prettier: PASS.
npm run checkfiltrado porwords: sin errores ni warnings de Words.- Visual en navegador: demo abierta en
http://localhost:5173/uix/components/words. Se verificó render inicial, presetsempty/long, placeholder, escritura, commit button, trace de Sema y tab Sema con 6 botonesPlay. - Browser logs después de añadir
wordsLangs: sin warnings nuevos.
Captura temporal local:
tmp/words-demo-empty.png
Problemas detectados y corregidos durante Words
- El wrapper Eidos de
Words.Contentpasaba un snippet vacío y bloqueaba el render HTML por defecto de Soma. Corregido: solo se pasa snippet cuando el consumidor aporta children. - Mismo patrón corregido para
Placeholder,CommandButtonyStatus, para no pisar defaults Soma. - Faltaba catálogo de traducciones
components.words.*, lo que generaba warnings de fallback en navegador. Corregido ensrc/uix/langs/components.
Check global pendiente
npm run check global sigue fallando por deuda ajena a Words:
web/routes/uix/components/tree-grid/+page.svelte:bind:expandedybind:valuesobre props no bindables.web/routes/uix/components/tree-view/+page.svelte:bind:expandedValueybind:selectedValuesobre props no bindables.src/uix/eidos/components/virtual-grid/virtual-grid.svelte: tipo destylepermite objeto, pero el markup espera string.src/uix/eidos/components/virtual-list/virtual-list.svelte: mismo problema destyle.src/uix/active-uix/active-uix.svelte.test.ts:announceMorfousa eventoabsent-clearconfamily: 'absent'/intent: 'absent', incompatible con el vocabulario actual de Morfo.- Además hay warnings no bloqueantes en
navigation-menu(href="#") ytable(state_referenced_locally).
Punto exacto para continuar mañana
- Primero limpiar
npm run checkglobal, empezando por los errores anteriores. Es mejor cerrar eso antes de seguir creando componentes, porque ahora Words ya pasa su auditoría aislada. - Revisar visualmente Words en responsive y dark/light: desktop ya fue comprobado; falta una pasada móvil/estrecha, vertical toolbar y tabs Morfo/Recipe/A11y con scroll largo.
- Si se sigue en Words V1.1, el orden recomendado es:
- sincronizar cambios externos de
valuesin remount en la demo; - import/export HTML seguro;
- Markdown import/export;
- BubbleMenu + LinkPopover como partes Soma reales;
- agrupar input/composition en batches de history menos ruidosos.
- sincronizar cambios externos de
- Si se sigue con el ecosistema UIX, no abrir más componentes hasta que
component-audit --only wordssiga en PASS ynpm run checkglobal esté limpio.
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+formatOptionscon 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/suffixstrings (lo más débil). - En la rama histórica
morfo-driven-somaelair/number-fieldtampoco tenía Currency/Unit — solo skin sobreterra.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 porUnitKinden<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— getterhasOwnLocale+ 5 puntos bypass.src/uix/soma/components/calendar/calendar-provider.svelte.ts—dateOrderderivado respeta prop locale.src/uix/soma/components/range-calendar/range-calendar-provider.svelte.ts— idemdateOrder.
TextArea nuevo (12 archivos)
src/uix/morfo/components/textarea.tssrc/uix/soma/components/textarea/types.ts,langs.ts,textarea-provider.svelte.ts,exports.ts,index.tscomponents/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.tssrc/uix/soma/types/html.ts— añadidoPrimitiveTextareaAttributessrc/uix/soma/types/index.ts— export añadidosrc/uix/soma/components/index.ts— barrelsrc/uix/sema/components/index.ts— barrel
Demo NumberField
web/routes/uix/components/number-field/+page.svelte— imports del ecosistema, locale delformatservice, 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:
- NumberField: cambiar el
<select>de locale dees-ESaen-US— el separador decimal debe pasar de coma a punto inmediatamente. - Compact preset con locale en-US debe mostrar
1.2M(no1,2 M). - TextArea: probar autosize escribiendo texto largo, ver el counter
ponerse rojo al pasar maxLength, probar
submitOn="mod+enter". - Calendar: con prop
locale="en-US"los segmentos del header deben ordenarse MDY, no DMY.
Próximos pasos (orden recomendado)
- Verificación visual (1h) — los 4 puntos de arriba.
- Skeleton + Spinner (2-3h) — primitivos eidos puros, bajo coste, alto
retorno. Skeleton genérico con
shape(rect / circle / text), Spinner consize+color. NO morfo nuevo (son layout primitives, scope['eidos']solo). - PasswordField (1-2h) — wrapper sobre
editablecontype="password", toggle de visibility y eidos chrome consistente con SearchField. Reusa el morfo de editable (mismo APG textbox); solo añade un eidos wrapper. - 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 getterhasOwnLocaleque toggea entre Intl directo (locale del prop) y el servicio (locale del appprefs.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:
git addselectivo de TODO el trabajo acumulado.- Commit unificado o split por dominio.
- 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).