|
|
# 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):
|
|
|
|
|
|
```ts
|
|
|
// 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**:
|
|
|
|
|
|
```ts
|
|
|
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):
|
|
|
|
|
|
```svelte
|
|
|
<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
|
|
|
|
|
|
```bash
|
|
|
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).
|