Advance Eidos demos and form validation docs

active-uix
dev 5 months ago
parent 09e2618787
commit fb5dd0919c

@ -71,6 +71,12 @@ capa. Esta es la guia que debe leerse antes de tocar cualquier componente.
- `index.ts` exporta named namespace y default del mismo namespace.
- Para single-part, el default/root es el componente completo y no se inventan
partes vacias.
- Dentro de wrappers Eidos, las partes de Soma se importan como namespace con
el nombre real del componente: `import * as Collapsible from
'$soma/components/collapsible'`, y se usan como
`<Collapsible.Provider>`, `<Collapsible.Trigger>`, etc.
- Prohibido en wrappers Eidos: `Parts`, `Base`, `SomaXxxProvider`, tags
sueltas `<Provider>`/`<Trigger>`/`<Content>` y cualquier alias `Root`.
### Naming y contratos
@ -139,7 +145,155 @@ capa. Esta es la guia que debe leerse antes de tocar cualquier componente.
### Estado actual de la tanda Eidos
- Ultimo componente corregido/verificado: `Switch`.
- Ultimo componente corregido/verificado: `FileUpload` demo.
- Correccion/demo 2026-05-19:
- Anadio ruta `web/routes/uix/components/file-upload/+page.svelte` siguiendo
`web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md`.
- La demo incluye tabs `Live`, `API`, `Morfo`, `Sema`, `Recipe`, `A11y`,
traza por `MutationObserver`, botones Play de Sema y pasos
`data-perm-step` para `remove -> clear`.
- No se instrumenta el boton que abre el picker nativo porque bloquearia las
pruebas automaticas; el evento `trigger-picker` queda cubierto en el tab
Sema mediante Play.
- `web/routes/uix/+layout@.svelte` registra `fileUploadSema` y anade la
entrada de navegacion `File upload`.
- Bug real corregido en `Eidos FileUpload`: el root ahora declara
`files = $bindable([])` y reenvia `bind:files` a Soma.
- Bug real corregido en `Eidos FileUpload.Item`: el snippet argumentado ya no
usa `children` como nombre local; usa `bodyContent`.
- Bug real corregido en `Eidos FileUpload.ItemName` e `ItemSize`: si el
consumidor no pasa children, el wrapper ya no inyecta un snippet vacio que
anulaba el fallback de Soma (`file.name` y `formatBytes(file.size)`).
- Bug de contrato corregido en `Morfo FileUpload`: `ItemPreview.data-type`
pasa a ser opcional porque la prop `type` de Soma es un filtro opcional y
no se emite cuando no se usa.
- Validado despues de `FileUpload` demo:
- `npm run check` -> 0 errores, 0 warnings.
- `SMOKE_SCOPE=/uix/components/file-upload npm run smoke` -> ruta OK.
- `npm run morfo:check` -> `file-upload` PASS; 29 rutas validadas,
38/67 morfos sin demo.
- `npm run perm:check` -> `file-upload` PASS; baseline elimina y limpia;
disabled queda pasivo; 17 demos instrumentadas OK.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> `file-upload`
invalid 0. Persisten solo hotspots conocidos de `progress`/`meter` por
`data-shape='circular'`.
- Navegador integrado: ruta carga sin errores de consola, monta root,
dropzone, trigger, list, dos items, dos progress bars, seis tabs y Sema
muestra cinco botones Play.
- Todos los componentes presentes hoy en `src/uix/eidos/components` salvo
`svg` tienen ruta demo en `web/routes/uix/components`.
- Ultimo componente corregido/verificado antes de esta demo: `TagsInput` demo.
- Correccion/demo 2026-05-19:
- Anadio ruta `web/routes/uix/components/tags-input/+page.svelte` siguiendo
`web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md`.
- La demo incluye tabs `Live`, `API`, `Morfo`, `Sema`, `Recipe`, `A11y`,
traza por `MutationObserver`, botones Play de Sema y pasos
`data-perm-step` para `type delimiter -> remove -> clear`.
- `web/routes/uix/+layout@.svelte` registra `tagsInputSema` y anade la
entrada de navegacion `Tags input`.
- Bug real corregido en `Eidos TagsInput`: el root ahora declara
`value = $bindable([])` e `inputValue = $bindable('')`, reenviando ambos
estados bindables a Soma.
- Validado despues de `TagsInput` demo:
- `npm run check` -> 0 errores, 0 warnings.
- `SMOKE_SCOPE=/uix/components/tags-input npm run smoke` -> ruta OK.
- `npm run morfo:check` -> `tags-input` PASS; 28 rutas validadas,
39/67 morfos sin demo.
- `npm run perm:check` -> `tags-input` PASS; baseline e invalid escriben,
eliminan y limpian; disabled/readonly quedan pasivos; 16 demos
instrumentadas OK.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> `tags-input` invalid 0.
- Navegador integrado: tags visibles, input presente, Sema con cuatro eventos
y sin errores de consola.
- Ultimo componente corregido/verificado antes de esta demo: `Editable` demo.
- Correccion/demo 2026-05-19:
- Correccion/demo 2026-05-19:
- Anadio ruta `web/routes/uix/components/editable/+page.svelte` siguiendo
`web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md`.
- La demo incluye tabs `Live`, `API`, `Morfo`, `Sema`, `Recipe`, `A11y`,
traza por `MutationObserver`, botones Play de Sema y pasos
`data-perm-step` para `Edit -> type draft -> Save`.
- `web/routes/uix/+layout@.svelte` registra `editableSema` y anade la
entrada de navegacion `Editable`.
- Bug real corregido en `Eidos Editable`: el root ahora declara
`value = $bindable('')` y `editing = $bindable(false)`, reenviando ambos
estados bindables a Soma.
- Bug real corregido en `Eidos Editable.Preview`: si el consumidor no pasa
children, el wrapper ya no inyecta un snippet vacio que anulaba el fallback
`state.displayValue` de Soma.
- Correccion posterior de arquitectura: `Editable` no debe parecer un
`Field/Input` permanente. La baseline real de la rama anterior esta en
`src/uix/terra/editable` (no en Air components) y plantea contenido inline
editable.
- `Morfo/Soma Editable` anade `Control` para agrupar acciones. Los triggers
ya no fuerzan `hidden`; Soma expone `data-editing`, `data-disabled` y
`data-readonly`, y Eidos decide visualmente si muestra `Edit` o
`Save/Cancel`.
- La receta Eidos de `Editable` vuelve a preview inline: caja sutil solo en
hover/focus o al entrar en edicion. Si se quiere una caja permanente debe
usarse `Field`/`Input`.
- Validado despues de `Editable` demo:
- `npm run check` -> 0 errores, 0 warnings.
- `SMOKE_SCOPE=/uix/components/editable npm run smoke` -> ruta OK.
- `npm run morfo:check` -> `editable` PASS; 27 rutas validadas,
40/67 morfos sin demo.
- `npm run perm:check` -> `editable` PASS; baseline edita, escribe y guarda;
disabled/readonly quedan pasivos; 15 demos instrumentadas OK.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> `editable` invalid 0.
- Navegador integrado: preview muestra el valor inicial, input oculto en
preview y sin errores de consola.
- Ultimo componente corregido/verificado antes de esta demo: `Stepper` demo.
- Correccion/demo 2026-05-19:
- Correccion/demo 2026-05-19:
- Anadio ruta `web/routes/uix/components/stepper/+page.svelte` siguiendo
`web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md`.
- La demo incluye tabs `Live`, `API`, `Morfo`, `Sema`, `Recipe`, `A11y`,
traza por `MutationObserver`, botones Play de Sema y pasos
`data-perm-step` que avanzan hasta `commit-complete`.
- `web/routes/uix/+layout@.svelte` registra `stepperSema` y anade la entrada
de navegacion `Stepper`.
- Bug real corregido en `Eidos Stepper`: el root ahora declara
`value = $bindable(0)` y reenvia `bind:value` a Soma, tal como prometia
su API documentada.
- Bug real corregido en `Eidos Stepper.Item` y `Stepper.Indicator`: los
snippets argumentados `{ state }` ya no usan `children` como nombre local,
evitando la recursividad/`undefined is not a function` vista en SSR.
- Validado despues de `Stepper` demo:
- `npm run check` -> 0 errores, 0 warnings.
- `SMOKE_SCOPE=/uix/components/stepper npm run smoke` -> ruta OK.
- `npm run morfo:check` -> `stepper` PASS; 26 rutas validadas,
41/67 morfos sin demo.
- `npm run perm:check` -> `stepper` PASS; baseline, RTL y vertical avanzan
los tres pasos hasta completar; 14 demos instrumentadas OK.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> `stepper` invalid 0.
- Navegador integrado: ruta carga sin errores de consola, seis tabs y Sema
muestra dos eventos con Play.
- Ultimo componente corregido/verificado antes de esta demo: `TagGroup` demo.
- Correccion/demo 2026-05-19:
- Anadio ruta `web/routes/uix/components/tag-group/+page.svelte` siguiendo
`web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md`.
- La demo incluye tabs `Live`, `API`, `Morfo`, `Sema`, `Recipe`, `A11y`,
traza por `MutationObserver`, botones Play de Sema y pasos
`data-perm-step` para seleccionar y eliminar un tag.
- `web/routes/uix/+layout@.svelte` registra `tagGroupSema` en el shell de
docs y anade la entrada de navegacion `Tag group`.
- Bug real corregido en `Eidos TagGroup`: el root ahora declara
`value = $bindable([])` y reenvia `bind:value` a Soma, tal como prometia
su API documentada.
- Bug real corregido en `Eidos TagGroup.Item`: el snippet argumentado ya no
usa `children` como nombre local; usa `bodyContent` para evitar la
recursividad/`undefined is not a function` vista en SSR.
- Validado despues de `TagGroup` demo:
- `npm run check` -> 0 errores, 0 warnings.
- `SMOKE_SCOPE=/uix/components/tag-group npm run smoke` -> ruta OK.
- `npm run morfo:check` -> `tag-group` PASS; 25 rutas validadas,
42/67 morfos sin demo.
- `npm run perm:check` -> `tag-group` PASS; baseline selecciona y elimina,
variante disabled queda pasiva; 13 demos instrumentadas OK.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> `tag-group` invalid 0.
- Navegador integrado: ruta carga sin errores de consola, seis tabs, Sema
muestra cuatro eventos y Recipe lista selectors morfo/eidos esperados.
- Ultimo componente corregido/verificado antes de esta demo: `Switch`.
- Correccion 2026-05-19:
- `Soma SwitchThumb` expone `{ checked }` al snippet del thumb.
- `Eidos Switch` monta `SwitchThumb` por defecto si el root no recibe
@ -164,16 +318,17 @@ capa. Esta es la guia que debe leerse antes de tocar cualquier componente.
-> 11 archivos, 129 tests OK.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> 0 invalid.
- `npm run check` -> 0 errores, 0 warnings.
- Siguientes candidatos recomendados: `toolbar`, `tag-group`, despues
`file-upload`, `tags-input`, `editable`, `stepper`. Los grandes
(`select`, `combobox`, `calendar`, `date-field`, `date-picker`,
`date-range-picker`, `time-field`, `time-picker`) requieren tabla previa mas
detallada antes de tocar codigo.
- Siguiente componente recomendado: iniciar la tabla previa de uno de los
componentes grandes aun no migrados a Eidos (`select`, `combobox`,
`calendar`, `date-field`, `date-picker`, `date-range-picker`, `time-field`,
`time-picker`). Antes de tocar codigo, comparar Air si existe, Soma/Morfo y
referencias externas, y decidir gaps reales.
### Archivos temporales
- No commitear logs temporales:
`.codex-vite-*.log`, `.codex-vite-*.err.log`, `debug.log`, `md`.
`.codex-vite-*.log`, `.codex-vite-*.err.log`,
`.codex-taggroup-dev.*.log`, `debug.log`, `md`.
## Corte para continuar manana
@ -1553,3 +1708,119 @@ a su `portalTo`. No usar nombres como `somaPortalTo` en `ActiveUix`.
- Antes de seguir con componentes, releer `src/uix/active_architecture.md`,
`src/uix/active-uix/README.md`, `src/uix/eidos/README.md`,
`src/uix/soma/README.md` y este archivo.
## Actualizacion 2026-05-20
- Se releyo la guia vigente de demos (`web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md`)
y se contrasto contra el estado real de `src/uix/eidos/components`.
- Auditoria `src/uix/kimi-audit-eidos.md` aplicada sobre demos/componentes Eidos:
- `date-field` ya tiene README y demo.
- `svg` ya tiene README y queda documentado como primitiva interna sin ruta demo.
- Todas las demos routadas de Eidos tienen las seis tabs canonicas:
`Live`, `API`, `Morfo`, `Sema`, `Recipe`, `A11y`.
- Los demos con eventos tienen `MutationObserver` de `data-event` y botones
`Play` en la tab Sema; los demos 0-event mantienen tab Sema con estado vacio
justificado en su README.
- Los snippets de demos usan `.filter(Boolean)` y no renderizan props falsas.
- Se elimino la tab extra `Gallery` de `Icon`.
- `Checkbox` y `RadioGroup` vuelven a tener `<div data-uix-canvas-inner>` como
primer nodo de la demo.
- `Field`, `NumberField`, `SearchField`, `Slider` y `DateField` tienen tabla
Recipe con selectores clasificados como `morfo` / `eidos`.
- Guardias/correcciones colaterales cerradas:
- `NumberField` usa `ActiveTimers` en `SpinPressRepeater`.
- `SearchField` no registra listeners locales imperativos innecesarios.
- `RatingGroup` usa la key `components.rating-group.value-text`.
- `Field`, `NumberField`, `SearchField` y `Editable` consumen tokens recipe
declarados; `layout-column-gap` queda publicado en el tema base.
- Validacion ejecutada tras esos cambios:
- `npm run check` -> 0 errores, 0 warnings.
- `npm run test` -> 238 archivos, 2335 tests OK.
- `npm run morfo:check` -> 32 demos routadas PASS.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> invalid=0.
- `SMOKE_SCOPE=/uix/components npm run smoke` -> 32 rutas OK.
- Estado actual de Eidos components:
- Todos los directorios Eidos tienen README.
- Todos los componentes visuales Eidos tienen demo routada salvo `svg`, que es
primitiva interna documentada.
- No se detectan `data-soma-*`, `--soma-*` ni `--eidos-*` en CSS/componentes
Eidos fuera de tests/guardias documentales.
- Siguiente foco:
1. Continuar migracion componente por componente desde Soma a Eidos.
2. Seguir el protocolo obligatorio: Air (`gita/glm-5`) -> Morfo/Soma ->
Morfo/Sema -> referencias externas -> README -> wrapper/recipe/demo.
3. La tanda en curso venia por componentes relacionados con forms; el primer
candidato natural no presente aun en Eidos es `form`.
- No commitear logs temporales ni auditorias sueltas:
`.codex-*.log`, `.codex-*.err.log`, `debug.log`, `md`,
`src/uix/kimi-audit-eidos.md`.
### Correccion de convencion Eidos -> Soma 2026-05-20
- Se corrigio la deriva introducida en wrappers Eidos: no usar `Parts`,
`Base`, `SomaXxxProvider`, `Root` ni tags sueltas para referirse a Soma.
- Convencion obligatoria documentada:
- import interno: `import * as Nombre from '$soma/components/nombre'`;
- uso interno: `<Nombre.Provider>`, `<Nombre.Trigger>`,
`<Nombre.Content>`, etc.;
- API publica Eidos: `<Nombre>`, `<Nombre.Trigger>`, etc.; sin
`<Nombre.Provider>` publico.
- Componentes ajustados en esta correccion: `combobox`, `select` y `form`.
Los errores concretos eran imports namespace correctos pero tags sin prefijo
(`<Input>`, `<Trigger>`, `<Content>`, `<Submit>`, `<Reset>`,
`<ErrorSummary>`).
- `Soma Form.Provider` ahora tipa sus atributos DOM con
`PrimitiveFormAttributes` en vez de `HTMLFormAttributes` crudo. Motivo:
`HTMLFormAttributes.children?: Snippet<[]>` chocaba con el snippet
argumentado `children(snippetProps)` de `WithChild<..., FormProviderSnippetProps>`.
- Documentacion actualizada:
- `src/uix/eidos/components/README.md`: patron interno obligatorio y formas
prohibidas;
- `src/uix/eidos/README.md`: consumo de Soma mediante namespace, sin aliases;
- `src/uix/soma/COMPONENT_GUIDE.md`: regla de atributos HTML primitivos para
evitar choques de `children`.
- Validacion ejecutada:
- `npm run check` -> 0 errores, 0 warnings.
- Guardia manual:
`rg "import \\* as Parts|<Parts\\.|</Parts\\.|<Provider\\b|</Provider>|import \\{ Provider \\}|Provider as Soma|Soma[A-Za-z0-9]+Provider|\\b[A-Za-z0-9]+Base\\b" src/uix/eidos/components -g "*.svelte" -g "*.ts"`
-> sin resultados.
### Correccion Form demo / SIUM 2026-05-20
- `Form` demo creado en `web/routes/uix/components/form/+page.svelte` y
registrado en el sidebar `/uix` junto con `formSema`.
- La demo cubre dos modos:
- manual: `Form + Field` con `createForm` y SIUM;
- auto: `Form.AutoFields` leyendo el AST SIUM.
- Correccion importante: el submit de `$libs/forms/createForm` espera la
validacion Standard Schema asincrona antes de llamar `onValidSubmit`. SIUM
expone `~standard.validate` como async, por lo que el submit no puede
decidir con un placeholder sincronico.
- Validacion en tiempo real:
- el modo visible de la demo es `validationBehaviour: 'onChange'`;
- `onChange` no se llama `onTime`;
- los defaults iniciales de la demo deben ser validos para no pintar todos
los campos como invalidos al montar;
- si una demo quiere mostrar estado inicial invalido, debe documentarlo
explicitamente.
- Regla nueva para demos:
- los snippets son contrato, no decoracion;
- schema, imports, validators, defaults y campos visibles deben coincidir con
el preview;
- no se permite que el preview valide `age` mientras el snippet solo declare
`fullName/email`;
- si el snippet es minimo, debe estar marcado como ejemplo minimo separado,
no como snippet del preview.
- Documentacion actualizada para no repetir los errores:
- `web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md`: seccion `Snippet parity is
mandatory`;
- `src/uix/eidos/components/README.md`: reglas especificas para demos de
formularios y snippets schema-driven;
- `src/uix/eidos/components/form/README.md`: ejemplo con `age`, defaults
validos y `validationBehaviour: 'onChange'`;
- `src/uix/soma/components/form/README.md`: `validationBehaviour` actualizado
a `progressive | onSubmit | onBlur | onChange`, default `progressive`.
- Validacion ejecutada despues de la correccion:
- `npm run check` -> 0 errores, 0 warnings.
- `npx vitest run src/libs/forms/form-core.svelte.test.ts` -> 4 tests OK.
- `SMOKE_SCOPE=/uix/components/form npm run smoke` -> ruta OK.

@ -18,6 +18,7 @@ const EIDOS_ONLY_ATTRS = new Set([
'data-archetype',
'data-block',
'data-collapsed',
'data-combobox-control',
'data-color',
'data-columns',
'data-dragging',

@ -105,6 +105,46 @@ describe('createForm', () => {
cleanup();
});
it('awaits async Standard Schema validation before submit callbacks', async () => {
const submitted = vi.fn();
const invalid = vi.fn();
const asyncSchema: StandardSchemaV1<LoginValues> = {
'~standard': {
version: 1,
vendor: 'test',
async validate(value) {
const values = value as LoginValues;
if (values.email.includes('@')) return { value: values };
return {
issues: [
{
message: 'Invalid email',
path: ['email']
}
]
};
}
}
};
const { result: form, cleanup } = withEffectRoot(() =>
createLoginForm({
schema: asyncSchema,
onValidSubmit: submitted,
onInvalidSubmit: invalid
})
);
form.registerField('email', {});
await expect(form.submit(new SubmitEvent('submit'))).resolves.toBe(false);
flushSync();
expect(submitted).not.toHaveBeenCalled();
expect(invalid).toHaveBeenCalledWith({ email: ['Invalid email'] }, expect.any(SubmitEvent));
expect(form.errors).toEqual({ email: ['Invalid email'] });
cleanup();
});
it('marks pending while an async submit handler is running', async () => {
let release!: () => void;
const submitted = vi.fn();

@ -264,6 +264,25 @@ export function createForm<
return res.issues ? groupIssues(res.issues) : { errors: {}, issues: {} };
}
function runValidateForSubmit():
| { errors: FormErrors; issues: FormIssues }
| Promise<{ errors: FormErrors; issues: FormIssues }> {
if (!schema) {
return {
errors: {},
issues: {}
};
}
const res = schema['~standard'].validate(values);
if (isPromiseLike(res)) {
return res.then((resolved) =>
resolved.issues ? groupIssues(resolved.issues) : { errors: {}, issues: {} }
);
}
return res.issues ? groupIssues(res.issues) : { errors: {}, issues: {} };
}
function isDirtyField(name: string): boolean {
// Shallow compare using JSON serialization — good enough for the kinds
// of values forms typically hold (primitives, small objects). Consumers
@ -380,7 +399,10 @@ export function createForm<
submitCount++;
// Mark all registered fields as touched so errors display.
for (const name of registry.keys()) touched.set(name, true);
const validation = runValidate();
const validationResult = runValidateForSubmit();
const validation = isPromiseLike(validationResult)
? await validationResult
: validationResult;
errors = validation.errors;
issues = validation.issues;
const ok = !hasErrors(validation.errors);

@ -474,13 +474,26 @@ facade intermedio por componente. Ejemplo:
```ts
// eidos/components/toggle/types.ts
import type { ProviderProps as SomaToggleProviderProps } from '$soma/components/toggle';
export type ToggleProps = SomaToggleProviderProps & { variant?: ToggleVariant; size?: ToggleSize; … };
import type { ProviderProps } from '$soma/components/toggle';
export type ToggleProps = ProviderProps & { variant?: ToggleVariant; size?: ToggleSize; … };
```
El wrapper `.svelte` reexporta el provider headless y le añade los
data-attrs de tokens visuales (`data-variant`, `data-size`, `data-block`,
`data-icon-only`).
El wrapper `.svelte` importa el namespace público de Soma y usa sus partes con
ese mismo namespace:
```svelte
import * as Select from '$soma/components/select';
<Select.Provider {...rest}>
<Select.Trigger />
<Select.Content />
</Select.Provider>
```
No usar aliases como `Parts`, `SelectBase` o `SomaSelectProvider`. Tampoco usar
etiquetas sueltas `<Provider>` / `<Trigger>` dentro de Eidos. El wrapper añade
los data-attrs de tokens visuales (`data-variant`, `data-size`, `data-block`,
`data-icon-only`) y no reimplementa el estado.
### De sema (DOM)
@ -576,8 +589,10 @@ duras:
Plus reglas de implementación:
8. **Wrapper, no fork.** El `.svelte` de eidos consume el provider del
Soma y le añade los data-attrs de tokens visuales. No
8. **Wrapper, no fork.** El `.svelte` de eidos importa el namespace
público de Soma (`import * as Drawer from '$soma/components/drawer'`),
usa `<Drawer.Provider>` / `<Drawer.Trigger>` internamente y añade
los data-attrs de tokens visuales. No
reimplementa estado.
9. **`Size` desde `lib/types.ts`.** Componentes que aceptan tamaños
reusan el tipo compartido y narrowingan al subset que su recipe

@ -39,7 +39,7 @@ function readIndexSource(component: string): string {
}
function readNamespaceContract(source: string): NamespaceContract | undefined {
const match = /type\s+([A-Z][A-Za-z0-9]+)Namespace\s*=\s*typeof\s+\w+Root\s*&\s*\{([\s\S]*?)\n\}/.exec(
const match = /type\s+([A-Z][A-Za-z0-9]+)Namespace\s*=\s*typeof\s+\w+\s*&\s*\{([\s\S]*?)\n\}/.exec(
source
)
if (!match) return undefined

@ -20,17 +20,30 @@ const VISUAL_ATTRS = [
['drawer/drawer-close.svelte', ['data-icon-only']],
['drawer/drawer-content.svelte', ['data-size']],
['editable/editable.svelte', ['data-size', 'data-variant', 'data-color']],
['field/field.svelte', ['data-size', 'data-variant', 'data-orientation', 'data-color']],
['file-upload/file-upload.svelte', ['data-size', 'data-variant', 'data-color']],
['file-upload/file-upload-item-progress.svelte', ['style']],
['meter/meter.svelte', ['data-size', 'data-shape']],
['number-field/number-field.svelte', ['data-size']],
['number-field/number-field.svelte', ['data-size', 'data-variant', 'data-color']],
['pagination/pagination.svelte', ['data-size']],
['popover/popover-close.svelte', ['data-icon-only']],
['popover/popover-content.svelte', ['data-size']],
['progress/progress.svelte', ['data-size', 'data-shape']],
['radio-group/radio-group.svelte', ['data-size', 'data-variant', 'data-color', 'data-columns']],
['rating-group/rating-group.svelte', ['data-size']],
['search-field/search-field.svelte', ['data-size']],
['search-field/search-field.svelte', ['data-size', 'data-variant', 'data-color']],
['select/select.svelte', ['data-size', 'data-variant', 'data-color']],
['select/select-trigger.svelte', ['data-size', 'data-variant', 'data-color']],
['select/select-content.svelte', ['data-size', 'data-variant', 'data-color']],
['select/select-indicator.svelte', ['data-select-indicator']],
['select/select-item-indicator.svelte', ['data-select-item-indicator']],
['combobox/combobox.svelte', ['data-size', 'data-variant', 'data-color']],
['combobox/combobox-control.svelte', ['data-combobox-control', 'data-size', 'data-variant', 'data-color']],
['combobox/combobox-input.svelte', ['data-size', 'data-variant', 'data-color']],
['combobox/combobox-trigger.svelte', ['data-size', 'data-variant', 'data-color']],
['combobox/combobox-content.svelte', ['data-size', 'data-variant', 'data-color']],
['combobox/combobox-indicator.svelte', ['data-combobox-indicator']],
['combobox/combobox-item-indicator.svelte', ['data-combobox-item-indicator']],
['slider/slider.svelte', ['data-size']],
['stepper/stepper.svelte', ['data-size', 'data-variant', 'data-color']],
['switch/switch.svelte', ['data-size']],

@ -40,7 +40,7 @@ accesibles como propiedades — `<Drawer.Trigger>`, `<Drawer.Content>`, etc.
`Object.assign` (que en Svelte 5 puede causar issues sutiles de
hidratación):
```ts
const Drawer = DrawerRoot as DrawerNamespace;
const Drawer = DrawerComponent as DrawerNamespace;
Drawer.Trigger = Trigger;
Drawer.Content = Content;
// ...
@ -61,7 +61,7 @@ drawer/
drawer-header.svelte ← eidos-only layout shell (cuando aplique)
drawer-footer.svelte ← idem
drawer.css ← recipe
types.ts ← extiende SomaXxxProviderProps
types.ts ← extiende las props públicas de Soma
index.ts ← compone Drawer + hijos
```
@ -79,7 +79,7 @@ contexto headless, acepta los bindables del estado (`open`, `value`,
* Eidos `<Drawer>` — root component. Wraps the Soma
* to set up the component context.
*/
import { Provider as SomaDrawerProvider } from '$soma/components/drawer';
import * as Drawer from '$soma/components/drawer';
import type { DrawerProps } from './types';
let {
@ -91,14 +91,43 @@ contexto headless, acepta los bindables del estado (`open`, `value`,
}: DrawerProps = $props();
</script>
<SomaDrawerProvider {...rest} bind:open bind:activeSnapPoint bind:isDragging>
<Drawer.Provider {...rest} bind:open bind:activeSnapPoint bind:isDragging>
{@render children?.()}
</SomaDrawerProvider>
</Drawer.Provider>
```
> **Patrón de import**: `import { Provider as SomaXxxProvider }`
> desde `$soma/components/{x}`. Eidos no crea una fachada headless propia
> por componente; envuelve las partes públicas de Soma directamente.
> **Patrón de import interno**: `import * as Drawer from '$soma/components/drawer'`.
> Eidos no crea una fachada headless propia por componente; envuelve las
> partes públicas de Soma directamente como `<Drawer.Provider>`,
> `<Drawer.Trigger>`, etc.
#### Prohibiciones de naming interno
El namespace interno que apunta a Soma se llama igual que el componente. No se
prefija ni se renombra:
```svelte
<!-- Correcto -->
import * as Collapsible from '$soma/components/collapsible';
<Collapsible.Provider>
<Collapsible.Trigger />
</Collapsible.Provider>
```
Formas prohibidas:
- `import * as Parts from '$soma/components/collapsible'`
- `import * as CollapsibleBase from '$soma/components/collapsible'`
- `import { Provider as SomaCollapsibleProvider } from '$soma/components/collapsible'`
- etiquetas sueltas `<Provider>`, `<Trigger>`, `<Content>` dentro de Eidos
- tipos o aliases internos `SomaXxxProvider`, `XxxBase`, `XxxRoot`
Motivo: Eidos ya declara la capa en el path. El import interno debe expresar la
entidad headless concreta de Soma, no una abstraccion inventada. Si un wrapper
Eidos necesita el provider de Soma, escribe `<Collapsible.Provider>`; si
necesita una parte, escribe `<Collapsible.Trigger>`, `<Collapsible.Content>`,
etc.
### `{name}-{part}.svelte` — hijos passthrough
@ -109,26 +138,26 @@ Content). Si es passthrough puro (Trigger, Close), trivial.
```svelte
<!-- drawer-trigger.svelte -->
<script lang="ts">
import { Trigger } from '$soma/components/drawer';
import * as Drawer from '$soma/components/drawer';
import type { DrawerTriggerProps } from './types';
let { children, ...rest }: DrawerTriggerProps = $props();
</script>
<Trigger {...rest}>
<Drawer.Trigger {...rest}>
{@render children?.()}
</Trigger>
</Drawer.Trigger>
```
### `index.ts` — compone el namespace
Asignación explícita per-property sobre el root component. **NO** usar
`Object.assign(DrawerRoot, { Trigger, ... })` — Svelte 5 puede manejar
`Object.assign(DrawerComponent, { Trigger, ... })` — Svelte 5 puede manejar
mal la mutación bulk del component constructor durante hidratación,
causando que los hijos aparezcan y desaparezcan después de mount.
```ts
import DrawerRoot from './drawer.svelte';
import DrawerComponent from './drawer.svelte';
import Trigger from './drawer-trigger.svelte';
import Overlay from './drawer-overlay.svelte';
import Content from './drawer-content.svelte';
@ -140,7 +169,7 @@ import Footer from './drawer-footer.svelte';
import Handle from './drawer-handle.svelte';
import { Portal } from '$soma/components/internal';
type DrawerNamespace = typeof DrawerRoot & {
type DrawerNamespace = typeof DrawerComponent & {
Trigger: typeof Trigger;
Portal: typeof Portal;
Overlay: typeof Overlay;
@ -153,7 +182,7 @@ type DrawerNamespace = typeof DrawerRoot & {
Footer: typeof Footer;
};
const Drawer = DrawerRoot as DrawerNamespace;
const Drawer = DrawerComponent as DrawerNamespace;
Drawer.Trigger = Trigger;
Drawer.Portal = Portal;
Drawer.Overlay = Overlay;
@ -181,17 +210,17 @@ export type {
```ts
import type {
ProviderProps as SomaDrawerProviderProps,
TriggerProps as SomaDrawerTriggerProps,
ContentProps as SomaDrawerContentProps
ProviderProps,
TriggerProps,
ContentProps
// …
} from '$soma/components/drawer';
export type DrawerProps = SomaDrawerProviderProps;
export type DrawerTriggerProps = SomaDrawerTriggerProps;
export type DrawerProps = ProviderProps;
export type DrawerTriggerProps = TriggerProps;
// Eidos añade props visuales que el recipe consume vía data-attrs
export type DrawerContentProps = SomaDrawerContentProps & {
export type DrawerContentProps = ContentProps & {
size?: ResponsiveProp<DrawerSize>;
};
```
@ -199,6 +228,34 @@ export type DrawerContentProps = SomaDrawerContentProps & {
> **Sin `XxxFlatProps`, sin `XxxProviderProps as ProviderProps` aliases.**
> Hay un solo `XxxProps` (el del root) y los `XxxPartProps` per hijo.
Cuando el tipo root de Soma se importa como `ProviderProps`, se usa solo como
tipo local dentro de `types.ts`. No se exporta al consumidor con nombre
`ProviderProps` desde Eidos ni se crea un alias `SomaXxxProviderProps`.
### Tipos visuales canonicos
No se redeclaran variantes, colores ni tamanos a mano en cada componente.
La unica fuente para las props visuales compartidas es
`src/uix/eidos/lib/types.ts`:
- `Size` y `ResponsiveProp<T>` para escalas responsivas.
- `ControlVariant`, `SelectionVariant`, `ChipVariant`, `MarkerVariant`,
`SurfaceVariant`, etc. para variantes canonicas por familia visual.
- `ColorRole` para la paleta de intents UIX (`primary`, `secondary`,
`neutral`, `affirm`, `fulfill`, `risk`, `threat`, `loss`).
Cuando un componente necesita restringir una familia compartida, usa
`Extract<>` sobre el tipo canonico. No se crean unions locales con literales
duplicados:
```ts
import type { ColorRole, ControlVariant, Size } from '$uix/eidos/lib/types';
export type DateFieldSize = Extract<Size, 'sm' | 'md' | 'lg'>;
export type DateFieldVariant = ControlVariant;
export type DateFieldColor = ColorRole;
```
### Partes Eidos-only
Algunas partes existen solo para componer la superficie visual: `Header`,
@ -361,12 +418,31 @@ implementar o revisar un componente:
tabla Morfo/Sema, gaps y decisiones. Cada `⚠️` / `❌` debe acabar en una
decision explicita: implementar ahora, diferir a Morfo/Soma, diferir a v2 o
descartar por no pertenecer a Eidos.
6. Solo despues tocar wrapper, recipe o tokens.
6. Si hay demo en `web/routes/uix/components/{name}`, sus snippets forman parte
de la arquitectura del componente: deben reproducir el mismo contrato visible
que el preview. En componentes schema-driven (`form`, `auto-fields`,
date/time con formatos, etc.) no se permite un schema reducido que omita
campos visibles, validators o defaults reales.
7. Solo despues tocar wrapper, recipe o tokens.
El objetivo no es copiar APIs, sino que Eidos no quede por debajo de Air ni de
los referentes en funcionalidades reales. Si una capacidad pertenece a Soma, la
tabla debe decirlo; si es visual, Eidos debe cubrirla o justificar el gap.
### Reglas especificas para demos de formularios
- La validacion en tiempo real se modela como
`validationBehaviour: 'onChange'`.
- Si una demo arranca en `onChange`, los defaults iniciales deben ser validos
salvo que el README y la UI digan explicitamente que se esta mostrando un
formulario inicialmente invalido.
- El snippet de Soma y el snippet de Eidos deben declarar los mismos campos
visibles que el preview: imports SIUM, schema, defaults, `Form + Field` y
`Form.AutoFields`. No se admiten snippets que omiten `age`, `role`,
campos de fecha, arrays u otros datos que el preview valida.
- `Form.AutoFields` es renderer reflectivo de SIUM. Si necesita comportamiento
nuevo, se cambia Soma / `$libs/forms` / Morfo antes que Eidos.
---
## Forma legacy (CSS-only, retired)
@ -394,6 +470,7 @@ estado, ARIA, traducciones y escritura headless en Soma/Morfo.
| dialog | ✅ multi-part | Trigger, Portal, Overlay, Content, Title, Description, Close, Header, Footer |
| drawer | ✅ multi-part | + Handle |
| field | ✅ multi-part | Label, RequiredIndicator, Control, Input, HelperText, ErrorText, Prefix, Suffix |
| form | ✅ multi-part | Submit, Reset, ErrorSummary, AutoFields |
| popover | ✅ multi-part | Arrow, Anchor, Title, Description |
| toast | ✅ multi-part | + Toaster separate |
| accordion | ✅ multi-part | Item, Header, Trigger, Content |
@ -406,6 +483,8 @@ estado, ARIA, traducciones y escritura headless en Soma/Morfo.
| progress | ✅ multi-part | Label, ValueText, Indicator |
| rating-group | ✅ multi-part | Item |
| search-field | ✅ multi-part | Input, ClearTrigger |
| select | ✅ multi-part | Trigger, Value, Indicator, Portal, Content, Viewport, Item, ItemIndicator, Group |
| combobox | ✅ multi-part | Control, Input, Trigger, Indicator, Portal, Content, Viewport, Item, ItemIndicator |
| slider | ✅ multi-part | Range, Thumb, Tick |
| tooltip | ✅ multi-part | + Group |
| tabs | ✅ multi-part | List, Trigger, Content, Indicator |
@ -415,11 +494,11 @@ estado, ARIA, traducciones y escritura headless en Soma/Morfo.
| tag-group | ✅ multi-part | Label, Item, Link, RemoveButton |
| tags-input | ✅ multi-part | Control, Input, Item, ItemText, ItemDeleteTrigger, ClearTrigger |
| file-upload | ✅ multi-part | Label, Dropzone, Trigger, HiddenInput, FileList, Item, preview/progress/actions |
| editable | ✅ multi-part | Area, Preview, Input, EditTrigger, SubmitTrigger, CancelTrigger |
| editable | ✅ multi-part | Area, Control, Preview, Input, EditTrigger, SubmitTrigger, CancelTrigger |
| stepper | ✅ multi-part | List, Item, Trigger, Indicator, Separator, Content, CompletedContent, Prev/Next |
### Orden recomendado para continuar
1. Componentes grandes sólo con tabla previa de migración: `select`,
`combobox`, `calendar`, `date-field`, `date-picker`,
`date-range-picker`, `time-field`, `time-picker`.
1. Componentes grandes sólo con tabla previa de migración:
`calendar`, `date-field`, `date-picker`, `date-range-picker`,
`time-field`, `time-picker`.

@ -1,6 +1,6 @@
<script lang="ts">
/**
* Eidos `<Accordion.Trigger>` — wraps Soma's Trigger and adds a
* Eidos `Accordion.Trigger` — wraps Soma's Trigger and adds a
* default chevron indicator (via `<SvgChevron>` from the eidos svg
* primitives module). The chevron rotates 180° on `data-state='open'`
* via the recipe — no JS rotation here.

@ -16,7 +16,7 @@
* </Accordion.Item>
* </Accordion>
*/
import * as SomaAccordion from '$soma/components/accordion';
import * as Accordion from '$soma/components/accordion';
import { ActiveEidos } from '$uix/eidos';
import type { AccordionProps } from './types';
import { setAccordionEidosContext } from './accordion-context';
@ -39,6 +39,6 @@
});
</script>
<SomaAccordion.Provider {...rest} bind:value data-size={resolvedSize} data-variant={variant}>
<Accordion.Provider {...rest} bind:value data-size={resolvedSize} data-variant={variant}>
{@render children?.()}
</SomaAccordion.Provider>
</Accordion.Provider>

@ -14,20 +14,20 @@
// `<Accordion>` IS the root (lives in accordion.svelte). Children
// are attached explicitly. No Provider exported publicly; no
// flat-with-snippets invention.
import AccordionRoot from './accordion.svelte';
import AccordionComponent from './accordion.svelte';
import Item from './accordion-item.svelte';
import Header from './accordion-header.svelte';
import Trigger from './accordion-trigger.svelte';
import Content from './accordion-content.svelte';
type AccordionNamespace = typeof AccordionRoot & {
type AccordionNamespace = typeof AccordionComponent & {
Item: typeof Item;
Header: typeof Header;
Trigger: typeof Trigger;
Content: typeof Content;
};
const Accordion = AccordionRoot as AccordionNamespace;
const Accordion = AccordionComponent as AccordionNamespace;
Accordion.Item = Item;
Accordion.Header = Header;
Accordion.Trigger = Trigger;

@ -1,18 +1,18 @@
import type { ResponsiveProp } from '$uix/eidos/lib/types';
import type { ControlVariant, ResponsiveProp } from '$uix/eidos/lib/types';
import type {
ProviderProps as SomaAccordionProviderProps,
ItemProps as SomaAccordionItemProps,
HeaderProps as SomaAccordionHeaderProps,
TriggerProps as SomaAccordionTriggerProps,
ContentProps as SomaAccordionContentProps,
ProviderProps,
ItemProps,
HeaderProps,
TriggerProps,
ContentProps,
AccordionType
} from '$soma/components/accordion';
export type AccordionSize = 'sm' | 'md' | 'lg' | 'full';
export type AccordionVariant = 'outline' | 'surface' | 'ghost';
export type AccordionItemProps = SomaAccordionItemProps;
export type AccordionHeaderProps = SomaAccordionHeaderProps;
export type AccordionContentProps = SomaAccordionContentProps;
export type AccordionVariant = ControlVariant;
export type AccordionItemProps = ItemProps;
export type AccordionHeaderProps = HeaderProps;
export type AccordionContentProps = ContentProps;
/**
* Root props in eidos accept `ResponsiveProp<AccordionSize>` for
@ -22,7 +22,7 @@ export type AccordionContentProps = SomaAccordionContentProps;
* vs surface vs ghost on a per-breakpoint basis is not a use case worth
* exposing.
*/
export type AccordionProps = SomaAccordionProviderProps & {
export type AccordionProps = ProviderProps & {
size?: ResponsiveProp<AccordionSize>;
variant?: AccordionVariant;
};
@ -31,7 +31,7 @@ export type AccordionProps = SomaAccordionProviderProps & {
* Trigger picks up two visual props on top of the headless surface — the
* indicator chevron is opt-out and its placement is configurable.
*/
export type AccordionTriggerProps = SomaAccordionTriggerProps & {
export type AccordionTriggerProps = TriggerProps & {
/**
* Hide the default chevron indicator. The morfo's
* `aria-expanded` + the recipe's `[data-state='open']` styling

@ -43,6 +43,13 @@ Partes publicas: `Image`, `Fallback`.
- El fallback se renderiza en `idle` y `error` sin demora; la demora solo aplica
al estado `loading`.
## Eventos Sema
Avatar declara 0 eventos semanticos de forma deliberada. La carga de imagen es
un ciclo de recurso (`idle` / `loading` / `loaded` / `error`), no una ocurrencia
perceptiva iniciada por el usuario. Si un Avatar se usa dentro de un boton,
menu o perfil interactivo, el evento pertenece a ese componente contenedor.
## Referencias
- Radix Avatar: https://www.radix-ui.com/primitives/docs/components/avatar

@ -10,16 +10,16 @@
// `<Avatar>` IS the root (lives in avatar.svelte). Eidos-native — no
// headless backing for avatar; the recipe + image-load fallback live
// entirely here.
import AvatarRoot from './avatar.svelte';
import AvatarComponent from './avatar.svelte';
import Image from './avatar-image.svelte';
import Fallback from './avatar-fallback.svelte';
type AvatarNamespace = typeof AvatarRoot & {
type AvatarNamespace = typeof AvatarComponent & {
Image: typeof Image;
Fallback: typeof Fallback;
};
const Avatar = AvatarRoot as AvatarNamespace;
const Avatar = AvatarComponent as AvatarNamespace;
Avatar.Image = Image;
Avatar.Fallback = Fallback;

@ -1,19 +1,11 @@
import type { Snippet } from 'svelte';
import type { HTMLAttributes, HTMLImgAttributes } from 'svelte/elements';
import type { Size } from '$uix/eidos/lib/types';
import type { ColorRole, MarkerVariant, Size } from '$uix/eidos/lib/types';
import type { ImageStatus } from '$soma/layers';
export type AvatarSize = Extract<Size, 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'>;
export type AvatarVariant = 'solid' | 'soft' | 'outline';
export type AvatarColor =
| 'primary'
| 'secondary'
| 'neutral'
| 'affirm'
| 'fulfill'
| 'risk'
| 'threat'
| 'loss';
export type AvatarVariant = MarkerVariant;
export type AvatarColor = ColorRole;
export type AvatarRadius = 'full' | 'md' | 'sm' | 'none';
export type AvatarImageStatus = ImageStatus;

@ -51,6 +51,13 @@ Partes publicas: `List`, `Item`, `Link`, `Separator`, `Ellipsis`.
- `size` es visual y se estampa solo en el root como `data-size`; el recipe usa
CSS logico para funcionar en LTR/RTL.
## Eventos Sema
Breadcrumb declara 0 eventos semanticos de forma deliberada. La activacion de
un crumb es navegacion nativa o routing de la aplicacion, no una ocurrencia
perceptiva propia del breadcrumb. Si un ellipsis se convierte en trigger de un
Popover/Dropdown, los eventos pertenecen a ese overlay.
## Referencias
- Air anterior: sin componente Breadcrumb en `glm-5:src/uix/air/components`.

@ -1,10 +1,10 @@
<script lang="ts">
import { Ellipsis as SomaBreadcrumbEllipsis } from '$soma/components/breadcrumb';
import * as Breadcrumb from '$soma/components/breadcrumb';
import type { BreadcrumbEllipsisProps } from './types';
let { children, ...rest }: BreadcrumbEllipsisProps = $props();
</script>
<SomaBreadcrumbEllipsis {...rest}>
<Breadcrumb.Ellipsis {...rest}>
{@render children?.()}
</SomaBreadcrumbEllipsis>
</Breadcrumb.Ellipsis>

@ -1,10 +1,10 @@
<script lang="ts">
import { Item as SomaBreadcrumbItem } from '$soma/components/breadcrumb';
import * as Breadcrumb from '$soma/components/breadcrumb';
import type { BreadcrumbItemProps } from './types';
let { children, ...rest }: BreadcrumbItemProps = $props();
</script>
<SomaBreadcrumbItem {...rest}>
<Breadcrumb.Item {...rest}>
{@render children?.()}
</SomaBreadcrumbItem>
</Breadcrumb.Item>

@ -1,10 +1,10 @@
<script lang="ts">
import { Link as SomaBreadcrumbLink } from '$soma/components/breadcrumb';
import * as Breadcrumb from '$soma/components/breadcrumb';
import type { BreadcrumbLinkProps } from './types';
let { children, ...rest }: BreadcrumbLinkProps = $props();
</script>
<SomaBreadcrumbLink {...rest}>
<Breadcrumb.Link {...rest}>
{@render children?.()}
</SomaBreadcrumbLink>
</Breadcrumb.Link>

@ -1,10 +1,10 @@
<script lang="ts">
import { List as SomaBreadcrumbList } from '$soma/components/breadcrumb';
import * as Breadcrumb from '$soma/components/breadcrumb';
import type { BreadcrumbListProps } from './types';
let { children, ...rest }: BreadcrumbListProps = $props();
</script>
<SomaBreadcrumbList {...rest}>
<Breadcrumb.List {...rest}>
{@render children?.()}
</SomaBreadcrumbList>
</Breadcrumb.List>

@ -1,10 +1,10 @@
<script lang="ts">
import { Separator as SomaBreadcrumbSeparator } from '$soma/components/breadcrumb';
import * as Breadcrumb from '$soma/components/breadcrumb';
import type { BreadcrumbSeparatorProps } from './types';
let { children, ...rest }: BreadcrumbSeparatorProps = $props();
</script>
<SomaBreadcrumbSeparator {...rest}>
<Breadcrumb.Separator {...rest}>
{@render children?.()}
</SomaBreadcrumbSeparator>
</Breadcrumb.Separator>

@ -1,10 +1,10 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
/**
* Eidos `<Breadcrumb>` — visual wrapper over Soma's breadcrumb primitive.
* Soma owns ARIA landmark/current semantics; Eidos adds `size`.
*/
import { ActiveEidos } from '$uix/eidos';
import { Provider as SomaBreadcrumbProvider } from '$soma/components/breadcrumb';
import * as Breadcrumb from '$soma/components/breadcrumb';
import type { BreadcrumbProps } from './types';
let { size = 'md', children, ...rest }: BreadcrumbProps = $props();
@ -13,6 +13,6 @@
const resolvedSize = $derived(eidos.resolve(size, 'md'));
</script>
<SomaBreadcrumbProvider {...rest} data-size={resolvedSize}>
<Breadcrumb.Provider {...rest} data-size={resolvedSize}>
{@render children?.()}
</SomaBreadcrumbProvider>
</Breadcrumb.Provider>

@ -9,14 +9,14 @@
// <Breadcrumb.Item><Breadcrumb.Link current>Docs</Breadcrumb.Link></Breadcrumb.Item>
// </Breadcrumb.List>
// </Breadcrumb>
import BreadcrumbRoot from './breadcrumb.svelte';
import BreadcrumbComponent from './breadcrumb.svelte';
import List from './breadcrumb-list.svelte';
import Item from './breadcrumb-item.svelte';
import Link from './breadcrumb-link.svelte';
import Separator from './breadcrumb-separator.svelte';
import Ellipsis from './breadcrumb-ellipsis.svelte';
type BreadcrumbNamespace = typeof BreadcrumbRoot & {
type BreadcrumbNamespace = typeof BreadcrumbComponent & {
List: typeof List;
Item: typeof Item;
Link: typeof Link;
@ -24,7 +24,7 @@ type BreadcrumbNamespace = typeof BreadcrumbRoot & {
Ellipsis: typeof Ellipsis;
};
const Breadcrumb = BreadcrumbRoot as BreadcrumbNamespace;
const Breadcrumb = BreadcrumbComponent as BreadcrumbNamespace;
Breadcrumb.List = List;
Breadcrumb.Item = Item;
Breadcrumb.Link = Link;

@ -1,11 +1,11 @@
import type {
BreadcrumbCurrent,
EllipsisProps as SomaBreadcrumbEllipsisProps,
ItemProps as SomaBreadcrumbItemProps,
LinkProps as SomaBreadcrumbLinkProps,
ListProps as SomaBreadcrumbListProps,
ProviderProps as SomaBreadcrumbProviderProps,
SeparatorProps as SomaBreadcrumbSeparatorProps
EllipsisProps,
ItemProps,
LinkProps,
ListProps,
ProviderProps,
SeparatorProps
} from '$soma/components/breadcrumb';
import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
@ -17,14 +17,14 @@ export type BreadcrumbSize = Extract<Size, 'sm' | 'md' | 'lg'>;
* Extends Soma's breadcrumb landmark with visual sizing only. Current crumb,
* ARIA label/current and interactive ellipsis semantics are owned by Soma.
*/
export type BreadcrumbProps = SomaBreadcrumbProviderProps & {
export type BreadcrumbProps = ProviderProps & {
/** Visual size. Accepts a responsive map resolved by ActiveEidos. @default 'md' */
size?: ResponsiveProp<BreadcrumbSize>;
};
export type BreadcrumbListProps = SomaBreadcrumbListProps;
export type BreadcrumbItemProps = SomaBreadcrumbItemProps;
export type BreadcrumbLinkProps = SomaBreadcrumbLinkProps;
export type BreadcrumbSeparatorProps = SomaBreadcrumbSeparatorProps;
export type BreadcrumbEllipsisProps = SomaBreadcrumbEllipsisProps;
export type BreadcrumbListProps = ListProps;
export type BreadcrumbItemProps = ItemProps;
export type BreadcrumbLinkProps = LinkProps;
export type BreadcrumbSeparatorProps = SeparatorProps;
export type BreadcrumbEllipsisProps = EllipsisProps;
export type { BreadcrumbCurrent };

@ -1,6 +1,6 @@
<script lang="ts">
/**
* Eidos `<Checkbox.Group>` — adds optional `columns` prop for grid
* Eidos `Checkbox.Group` — adds optional `columns` prop for grid
* layouts. Combined with `orientation`:
* - vertical + columns N → CSS grid with N columns (items wrap)
* - vertical (no columns) → flex column

@ -1,6 +1,6 @@
<script lang="ts">
/**
* Eidos `<Checkbox.HiddenInput>` — passthrough wrapper around the
* Eidos `Checkbox.HiddenInput` — passthrough wrapper around the
* headless part. Renders a visually-hidden `<input type="checkbox">` so
* the browser includes name+value in form submissions.
*/

@ -1,6 +1,6 @@
<script lang="ts">
/**
* Eidos `<Checkbox.Indicator>` — defaults to `<SvgCheck>` /
* Eidos `Checkbox.Indicator` — defaults to `<SvgCheck>` /
* `<SvgDash>` from the eidos svg primitives. Pass an `icon` snippet
* to override (receives `{ checked, indeterminate }` so a single
* snippet can branch on state).

@ -11,7 +11,7 @@
* </Checkbox>
*/
import { ActiveEidos } from '$uix/eidos';
import * as SomaCheckbox from '$soma/components/checkbox';
import * as Checkbox from '$soma/components/checkbox';
import type { CheckboxProps } from './types';
let {
@ -28,7 +28,7 @@
const resolvedSize = $derived(eidos.resolve(size, 'md'));
</script>
<SomaCheckbox.Provider
<Checkbox.Provider
{...rest}
bind:checked
bind:indeterminate
@ -39,4 +39,4 @@
{#snippet children(snippetProps)}
{@render bodyContent?.(snippetProps)}
{/snippet}
</SomaCheckbox.Provider>
</Checkbox.Provider>

@ -12,20 +12,20 @@
// <Checkbox value="email"><Checkbox.Indicator /></Checkbox>
// <Checkbox value="sms"><Checkbox.Indicator /></Checkbox>
// </Checkbox.Group>
import CheckboxRoot from './checkbox.svelte';
import CheckboxComponent from './checkbox.svelte';
import Indicator from './checkbox-indicator.svelte';
import HiddenInput from './checkbox-hidden-input.svelte';
import Group from './checkbox-group.svelte';
import GroupLabel from './checkbox-group-label.svelte';
type CheckboxNamespace = typeof CheckboxRoot & {
type CheckboxNamespace = typeof CheckboxComponent & {
Indicator: typeof Indicator;
HiddenInput: typeof HiddenInput;
Group: typeof Group;
GroupLabel: typeof GroupLabel;
};
const Checkbox = CheckboxRoot as CheckboxNamespace;
const Checkbox = CheckboxComponent as CheckboxNamespace;
Checkbox.Indicator = Indicator;
Checkbox.HiddenInput = HiddenInput;
Checkbox.Group = Group;

@ -1,11 +1,16 @@
import type { Snippet } from 'svelte';
import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type {
ProviderProps as SomaCheckboxProviderProps,
IndicatorProps as SomaCheckboxIndicatorProps,
HiddenInputProps as SomaCheckboxHiddenInputProps,
GroupProps as SomaCheckboxGroupProps,
GroupLabelProps as SomaCheckboxGroupLabelProps
AffirmativeColorRole,
ResponsiveProp,
SelectionVariant,
Size
} from '$uix/eidos/lib/types';
import type {
ProviderProps,
IndicatorProps,
HiddenInputProps,
GroupProps,
GroupLabelProps
} from '$soma/components/checkbox';
/**
@ -21,7 +26,7 @@ export type CheckboxSize = Extract<Size, 'sm' | 'md' | 'lg'>;
* - `outline` — transparent background, border carries the active colour.
* - `ghost` — minimal chrome, near-transparent off state.
*/
export type CheckboxVariant = 'solid' | 'outline' | 'ghost';
export type CheckboxVariant = SelectionVariant;
/**
* Checkbox color subset — `primary | secondary | neutral | affirm`.
@ -33,7 +38,7 @@ export type CheckboxVariant = 'solid' | 'outline' | 'ghost';
* which paints a threat-coloured border via `data-invalid` without
* touching the colour palette.
*/
export type CheckboxColor = 'primary' | 'secondary' | 'neutral' | 'affirm';
export type CheckboxColor = AffirmativeColorRole;
/**
* Snippet args passed to a custom indicator icon. Receives the current
@ -44,7 +49,7 @@ export type CheckboxIconSnippetArgs = {
indeterminate: boolean;
};
export type CheckboxProps = SomaCheckboxProviderProps & {
export type CheckboxProps = ProviderProps & {
/** Visual size preset. Resolved via `Eidos.resolve`. @default 'md' */
size?: ResponsiveProp<CheckboxSize>;
/** Visual variant. @default 'solid' */
@ -53,7 +58,7 @@ export type CheckboxProps = SomaCheckboxProviderProps & {
color?: CheckboxColor;
};
export type CheckboxIndicatorProps = SomaCheckboxIndicatorProps & {
export type CheckboxIndicatorProps = IndicatorProps & {
/**
* Override the default `<SvgCheck>` / `<SvgDash>` glyphs with a custom
* snippet. Receives `{ checked, indeterminate }` so a single snippet
@ -65,9 +70,9 @@ export type CheckboxIndicatorProps = SomaCheckboxIndicatorProps & {
icon?: Snippet<[CheckboxIconSnippetArgs]>;
};
export type CheckboxHiddenInputProps = SomaCheckboxHiddenInputProps;
export type CheckboxHiddenInputProps = HiddenInputProps;
export type CheckboxGroupProps = SomaCheckboxGroupProps & {
export type CheckboxGroupProps = GroupProps & {
/**
* Lay out children in a CSS grid with this many columns. Combined
* with `orientation`, controls the visual flow:
@ -81,4 +86,4 @@ export type CheckboxGroupProps = SomaCheckboxGroupProps & {
columns?: number;
};
export type CheckboxGroupLabelProps = SomaCheckboxGroupLabelProps;
export type CheckboxGroupLabelProps = GroupLabelProps;

@ -9,12 +9,12 @@
* <Collapsible.Content>...</Collapsible.Content>
* </Collapsible>
*/
import * as SomaCollapsible from '$soma/components/collapsible';
import * as Collapsible from '$soma/components/collapsible';
import type { CollapsibleProps } from './types';
let { open = $bindable(false), children, ...rest }: CollapsibleProps = $props();
</script>
<SomaCollapsible.Provider {...rest} bind:open>
<Collapsible.Provider {...rest} bind:open>
{@render children?.()}
</SomaCollapsible.Provider>
</Collapsible.Provider>

@ -6,16 +6,16 @@
// <Collapsible.Trigger>Toggle</Collapsible.Trigger>
// <Collapsible.Content>...</Collapsible.Content>
// </Collapsible>
import CollapsibleRoot from './collapsible.svelte';
import CollapsibleComponent from './collapsible.svelte';
import Trigger from './collapsible-trigger.svelte';
import Content from './collapsible-content.svelte';
type CollapsibleNamespace = typeof CollapsibleRoot & {
type CollapsibleNamespace = typeof CollapsibleComponent & {
Trigger: typeof Trigger;
Content: typeof Content;
};
const Collapsible = CollapsibleRoot as CollapsibleNamespace;
const Collapsible = CollapsibleComponent as CollapsibleNamespace;
Collapsible.Trigger = Trigger;
Collapsible.Content = Content;

@ -1,8 +1,8 @@
import type { Snippet } from 'svelte';
import type {
ProviderProps as SomaCollapsibleProviderProps,
TriggerProps as SomaCollapsibleTriggerProps,
ContentProps as SomaCollapsibleContentProps
ProviderProps,
TriggerProps,
ContentProps
} from '$soma/components/collapsible';
/**
@ -19,10 +19,10 @@ import type {
*/
/** Props for the `<Collapsible>` root. Pass-through from headless. */
export type CollapsibleProps = SomaCollapsibleProviderProps;
export type CollapsibleProps = ProviderProps;
/** Props for `<Collapsible.Trigger>`. Pass-through from headless. */
export type CollapsibleTriggerProps = SomaCollapsibleTriggerProps;
export type CollapsibleTriggerProps = TriggerProps;
/** Props for `<Collapsible.Content>`. Pass-through from headless. */
export type CollapsibleContentProps = SomaCollapsibleContentProps;
export type CollapsibleContentProps = ContentProps;

@ -0,0 +1,136 @@
# Combobox
Eidos Combobox wraps Soma Combobox and keeps the component compound:
`<Combobox>` + `Control`, `Input`, `Trigger`, `Indicator`, `Portal`,
`Content`, `Viewport`, `Item`, `ItemIndicator`, `Group`, `GroupHeading`,
`Separator` and `Arrow`.
## Comparison
| Feature | Air | Radix | Bits | Ark | shadcn-svelte | React Aria | Soma | Eidos | Decision |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Editable input + popup listbox | ✅ | ❌ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | Covered with `Input` + `Content`; Radix has no dedicated Combobox primitive. |
| Compound anatomy | ✅ | ❌ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | Eidos exposes one namespace and attached parts; the root is `<Combobox>` itself. |
| Input + trigger grouped visually | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | `Combobox.Control` is Eidos-only layout. |
| Portal + floating content + arrow | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Soma owns Floating; Eidos consumes `data-side` and floating CSS vars. |
| Groups, headings and separators | ✅ | ❌ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | Covered. |
| Selected item indicator | ⚠️ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | `ItemIndicator` is Eidos-only; item state comes from Soma/Morfo. |
| Single select | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Covered. |
| Multiple select | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | Covered by Soma `type="multiple"`; Eidos only styles selected items. |
| Typeahead / filtering input | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Soma owns input value and filtering surface. |
| Hidden form value | ✅ | ❌ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | Soma renders hidden inputs when `name` is passed. |
| Clear trigger | ❌ | ❌ | ⚠️ | ✅ | ❌ | ✅ | ❌ | ❌ | Defer to Soma; clearing changes value/input state. |
| Scroll up/down affordance parts | ✅ | ❌ | ✅ | ⚠️ | ❌ | ❌ | ❌ | ❌ | Defer to Soma; requires viewport scroll behavior. |
| Static/non-floating content | ✅ | ❌ | ⚠️ | ⚠️ | ❌ | ❌ | ❌ | ❌ | Defer to Soma if inline listbox topology is needed. |
| Menu trigger behavior | ❌ | ❌ | ⚠️ | ✅ | ❌ | ✅ | ❌ | ❌ | Defer to Soma; affects when open state changes. |
| Inline autocomplete / auto highlight | ❌ | ❌ | ⚠️ | ✅ | ❌ | ✅ | ❌ | ❌ | Defer to Soma; input behavior, not visual styling. |
| Custom value / freeform commit | ❌ | ❌ | ⚠️ | ⚠️ | ❌ | ✅ | ❌ | ❌ | Defer to Soma/Form; needs value semantics. |
| Validation state | ✅ | ❌ | ⚠️ | ✅ | ⚠️ | ✅ | ❌ | ❌ | Defer to Field/Soma; Eidos must not invent invalid state. |
| Async/virtualized collections | ❌ | ❌ | ⚠️ | ✅ | ❌ | ✅ | ❌ | ❌ | Belongs in collection/virtualization, not visual wrapper. |
References:
- Bits UI Combobox: https://bits-ui.com/docs/components/combobox
- Ark UI Combobox: https://ark-ui.com/docs/components/combobox
- shadcn-svelte Combobox: https://www.shadcn-svelte.com/docs/components/combobox
- React Aria ComboBox: https://react-spectrum.adobe.com/react-aria/ComboBox.html
- Radix Primitives overview: https://www.radix-ui.com/primitives/docs/overview/introduction
## Layer split
| Layer | Owns |
| --- | --- |
| Morfo | Parts, ARIA/data contract, `commit-select` and `commit-unselect` events. |
| Soma | Input text, open state, single/multiple values, hidden form inputs, groups, floating, dismissal and keyboard navigation. |
| Sema | Subtle form commit sounds + tap haptic for select/unselect. Typing, filtering, open/close and highlight are intentionally silent. |
| Eidos | Control layout, size, variant, intent color, trigger icon, selected item indicator, panel recipe and animation. |
## Morfo/Sema
| Event | Family | Verb | Intent | Target | Sequence | Reason |
| --- | --- | --- | --- | --- | --- | --- |
| `commit-select` | `commit` | `select` | `affirm` | `item` | `post` | User confirms an option. The concrete item is the perceived target. |
| `commit-unselect` | `commit` | `remove` | `neutral` | `item` | `post` | Multiple deselect or single clear via `allowDeselect`. |
Opening, closing, typing, filtering and highlighting are navigation mechanics,
not committed choices. They do not emit Sema events.
## Out of scope (v2 roadmap)
- `ClearTrigger`: Ark and React Aria expose clear behavior. Deferred to Soma
because it mutates `value` and `inputValue`. Estimated cost: 80-140 lines
across Morfo/Soma/Eidos.
- `ScrollUpButton` / `ScrollDownButton`: Air and Bits expose them. Deferred
to Soma because the part needs scroll behavior and viewport awareness.
Estimated cost: 120-180 lines.
- `ContentStatic`: Air exposes a non-floating content variant. Deferred to
Soma because it changes topology and dismissal behavior. Estimated cost:
80-140 lines.
- `menuTrigger`, `autoHighlight`, `inlineAutocomplete`: Ark and React Aria
cover these input behaviors. Deferred to Soma because they affect open,
highlighted and input value state. Estimated cost: 180-260 lines.
- Custom/freeform value: React Aria supports custom values. Deferred to
Soma/Form because it changes value semantics and submitted form value.
Estimated cost: 140-220 lines.
- Validation state: Ark and React Aria expose validation. Deferred to
Field/Soma so Combobox consumes real field state instead of visual-only
invalid selectors. Estimated cost: 120-200 lines.
- Async/virtualized collections: Ark and React Aria cover this class of
behavior. Deferred to the collection/virtualization layer, not Eidos.
## Usage
```svelte
<script lang="ts">
import { Combobox } from '$uix/eidos/components/combobox'
let value = $state<string[]>([])
let inputValue = $state('')
</script>
<Combobox bind:value bind:inputValue name="framework" size="md" variant="surface" color="primary">
<Combobox.Control>
<Combobox.Input placeholder="Search framework..." />
<Combobox.Trigger>
<Combobox.Indicator />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Portal>
<Combobox.Content sideOffset={6}>
<Combobox.Viewport>
<Combobox.Group>
<Combobox.GroupHeading>Frontend</Combobox.GroupHeading>
<Combobox.Item value="svelte" label="Svelte">
<span>Svelte</span>
<Combobox.ItemIndicator />
</Combobox.Item>
<Combobox.Item value="react" label="React">
<span>React</span>
<Combobox.ItemIndicator />
</Combobox.Item>
<Combobox.Item value="vue" label="Vue">
<span>Vue</span>
<Combobox.ItemIndicator />
</Combobox.Item>
<Combobox.Item value="solid" label="Solid">
<span>Solid</span>
<Combobox.ItemIndicator />
</Combobox.Item>
</Combobox.Group>
<Combobox.Separator />
<Combobox.Group>
<Combobox.GroupHeading>Runtime</Combobox.GroupHeading>
<Combobox.Item value="node" label="Node">
<span>Node</span>
<Combobox.ItemIndicator />
</Combobox.Item>
<Combobox.Item value="deno" label="Deno">
<span>Deno</span>
<Combobox.ItemIndicator />
</Combobox.Item>
</Combobox.Group>
</Combobox.Viewport>
<Combobox.Arrow />
</Combobox.Content>
</Combobox.Portal>
</Combobox>
```

@ -0,0 +1,10 @@
<script lang="ts">
import * as Combobox from '$soma/components/combobox';
import type { ComboboxArrowProps } from './types';
let { children, ...rest }: ComboboxArrowProps = $props();
</script>
<Combobox.Arrow {...rest}>
{@render children?.()}
</Combobox.Arrow>

@ -0,0 +1,44 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
import * as Combobox from '$soma/components/combobox';
import { composeInlineStyle } from '$uix/eidos/lib/style';
import { getComboboxVisualContext } from './context.svelte';
import type { ComboboxContentProps } from './types';
let {
size,
variant,
color,
matchAnchorWidth = true,
style,
children: bodyContent,
...rest
}: ComboboxContentProps = $props();
const eidos = ActiveEidos.require();
const visual = getComboboxVisualContext();
const resolvedSize = $derived(eidos.resolve(size ?? visual?.getSize(), 'md'));
const resolvedVariant = $derived(variant ?? visual?.getVariant() ?? 'surface');
const resolvedColor = $derived(color ?? visual?.getColor() ?? 'primary');
const resolvedMatchAnchorWidth = $derived(eidos.resolve(matchAnchorWidth, true));
const composedStyle = $derived(
composeInlineStyle(
resolvedMatchAnchorWidth
? '--_combobox-match-anchor-width: var(--floating-anchor-width);'
: undefined,
style
)
);
</script>
<Combobox.Content
{...rest}
style={composedStyle}
data-size={resolvedSize}
data-variant={resolvedVariant}
data-color={resolvedColor}
>
{#snippet children(snippetProps: { open: boolean })}
{@render bodyContent?.(snippetProps)}
{/snippet}
</Combobox.Content>

@ -0,0 +1,23 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
import { getComboboxVisualContext } from './context.svelte';
import type { ComboboxControlProps } from './types';
let { size, variant, color, children, ...rest }: ComboboxControlProps = $props();
const eidos = ActiveEidos.require();
const visual = getComboboxVisualContext();
const resolvedSize = $derived(eidos.resolve(size ?? visual?.getSize(), 'md'));
const resolvedVariant = $derived(variant ?? visual?.getVariant() ?? 'surface');
const resolvedColor = $derived(color ?? visual?.getColor() ?? 'primary');
</script>
<div
{...rest}
data-combobox-control=""
data-size={resolvedSize}
data-variant={resolvedVariant}
data-color={resolvedColor}
>
{@render children?.()}
</div>

@ -0,0 +1,10 @@
<script lang="ts">
import * as Combobox from '$soma/components/combobox';
import type { ComboboxGroupHeadingProps } from './types';
let { children, ...rest }: ComboboxGroupHeadingProps = $props();
</script>
<Combobox.GroupHeading {...rest}>
{@render children?.()}
</Combobox.GroupHeading>

@ -0,0 +1,10 @@
<script lang="ts">
import * as Combobox from '$soma/components/combobox';
import type { ComboboxGroupProps } from './types';
let { children, ...rest }: ComboboxGroupProps = $props();
</script>
<Combobox.Group {...rest}>
{@render children?.()}
</Combobox.Group>

@ -0,0 +1,14 @@
<script lang="ts">
import { SvgChevron } from '$uix/eidos/components/svg';
import type { ComboboxIndicatorProps } from './types';
let { children, ...rest }: ComboboxIndicatorProps = $props();
</script>
<span {...rest} data-combobox-indicator="" aria-hidden="true">
{#if children}
{@render children()}
{:else}
<SvgChevron />
{/if}
</span>

@ -0,0 +1,21 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
import * as Combobox from '$soma/components/combobox';
import { getComboboxVisualContext } from './context.svelte';
import type { ComboboxInputProps } from './types';
let { size, variant, color, ...rest }: ComboboxInputProps = $props();
const eidos = ActiveEidos.require();
const visual = getComboboxVisualContext();
const resolvedSize = $derived(eidos.resolve(size ?? visual?.getSize(), 'md'));
const resolvedVariant = $derived(variant ?? visual?.getVariant() ?? 'surface');
const resolvedColor = $derived(color ?? visual?.getColor() ?? 'primary');
</script>
<Combobox.Input
{...rest}
data-size={resolvedSize}
data-variant={resolvedVariant}
data-color={resolvedColor}
/>

@ -0,0 +1,14 @@
<script lang="ts">
import { SvgCheck } from '$uix/eidos/components/svg';
import type { ComboboxItemIndicatorProps } from './types';
let { children, ...rest }: ComboboxItemIndicatorProps = $props();
</script>
<span {...rest} data-combobox-item-indicator="" aria-hidden="true">
{#if children}
{@render children()}
{:else}
<SvgCheck />
{/if}
</span>

@ -0,0 +1,12 @@
<script lang="ts">
import * as Combobox from '$soma/components/combobox';
import type { ComboboxItemProps } from './types';
let { children, ...rest }: ComboboxItemProps = $props();
</script>
<Combobox.Item {...rest}>
{#snippet children(snippetProps)}
{@render children?.(snippetProps)}
{/snippet}
</Combobox.Item>

@ -0,0 +1,10 @@
<script lang="ts">
import * as Combobox from '$soma/components/combobox';
import type { ComboboxSeparatorProps } from './types';
let { children, ...rest }: ComboboxSeparatorProps = $props();
</script>
<Combobox.Separator {...rest}>
{@render children?.()}
</Combobox.Separator>

@ -0,0 +1,23 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
import * as Combobox from '$soma/components/combobox';
import { getComboboxVisualContext } from './context.svelte';
import type { ComboboxTriggerProps } from './types';
let { size, variant, color, children, ...rest }: ComboboxTriggerProps = $props();
const eidos = ActiveEidos.require();
const visual = getComboboxVisualContext();
const resolvedSize = $derived(eidos.resolve(size ?? visual?.getSize(), 'md'));
const resolvedVariant = $derived(variant ?? visual?.getVariant() ?? 'surface');
const resolvedColor = $derived(color ?? visual?.getColor() ?? 'primary');
</script>
<Combobox.Trigger
{...rest}
data-size={resolvedSize}
data-variant={resolvedVariant}
data-color={resolvedColor}
>
{@render children?.()}
</Combobox.Trigger>

@ -0,0 +1,10 @@
<script lang="ts">
import * as Combobox from '$soma/components/combobox';
import type { ComboboxViewportProps } from './types';
let { children, ...rest }: ComboboxViewportProps = $props();
</script>
<Combobox.Viewport {...rest}>
{@render children?.()}
</Combobox.Viewport>

@ -0,0 +1,420 @@
[data-combobox] {
display: inline-grid;
gap: var(--space-2);
inline-size: 100%;
min-inline-size: 0;
font-family: var(--font-ui);
color: var(--color-content-primary);
}
[data-combobox-control],
[data-combobox-input],
[data-combobox-trigger],
[data-combobox-content] {
--_combobox-accent-track: var(--color-primary-track);
--_combobox-accent-border: var(--color-primary-border);
--_combobox-accent-text: var(--color-primary-text);
}
[data-combobox-control][data-color='secondary'],
[data-combobox-input][data-color='secondary'],
[data-combobox-trigger][data-color='secondary'],
[data-combobox-content][data-color='secondary'] {
--_combobox-accent-track: var(--color-secondary-track);
--_combobox-accent-border: var(--color-secondary-border);
--_combobox-accent-text: var(--color-secondary-text);
}
[data-combobox-control][data-color='neutral'],
[data-combobox-input][data-color='neutral'],
[data-combobox-trigger][data-color='neutral'],
[data-combobox-content][data-color='neutral'] {
--_combobox-accent-track: var(--color-neutral-track);
--_combobox-accent-border: var(--color-neutral-border);
--_combobox-accent-text: var(--color-neutral-text);
}
[data-combobox-control][data-color='affirm'],
[data-combobox-input][data-color='affirm'],
[data-combobox-trigger][data-color='affirm'],
[data-combobox-content][data-color='affirm'] {
--_combobox-accent-track: var(--color-affirm-track);
--_combobox-accent-border: var(--color-affirm-border);
--_combobox-accent-text: var(--color-affirm-text);
}
[data-combobox-control][data-color='fulfill'],
[data-combobox-input][data-color='fulfill'],
[data-combobox-trigger][data-color='fulfill'],
[data-combobox-content][data-color='fulfill'] {
--_combobox-accent-track: var(--color-fulfill-track);
--_combobox-accent-border: var(--color-fulfill-border);
--_combobox-accent-text: var(--color-fulfill-text);
}
[data-combobox-control][data-color='risk'],
[data-combobox-input][data-color='risk'],
[data-combobox-trigger][data-color='risk'],
[data-combobox-content][data-color='risk'] {
--_combobox-accent-track: var(--color-risk-track);
--_combobox-accent-border: var(--color-risk-border);
--_combobox-accent-text: var(--color-risk-text);
}
[data-combobox-control][data-color='threat'],
[data-combobox-input][data-color='threat'],
[data-combobox-trigger][data-color='threat'],
[data-combobox-content][data-color='threat'] {
--_combobox-accent-track: var(--color-threat-track);
--_combobox-accent-border: var(--color-threat-border);
--_combobox-accent-text: var(--color-threat-text);
}
[data-combobox-control][data-color='loss'],
[data-combobox-input][data-color='loss'],
[data-combobox-trigger][data-color='loss'],
[data-combobox-content][data-color='loss'] {
--_combobox-accent-track: var(--color-loss-track);
--_combobox-accent-border: var(--color-loss-border);
--_combobox-accent-text: var(--color-loss-text);
}
[data-combobox-control] {
--_combobox-control-height: var(--control-height-md);
--_combobox-control-px: var(--space-3);
--_combobox-control-gap: var(--space-2);
--_combobox-font-size: var(--font-size-sm);
box-sizing: border-box;
display: flex;
align-items: center;
gap: var(--_combobox-control-gap);
inline-size: 100%;
min-inline-size: 0;
min-block-size: var(--_combobox-control-height);
padding-inline: var(--_combobox-control-px);
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--radius-md);
background: var(--color-surface-default);
box-shadow: var(--shadow-xs);
color: var(--color-content-primary);
font-size: var(--_combobox-font-size);
line-height: var(--leading-ui);
transition:
background var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default),
box-shadow var(--duration-fast) var(--ease-default);
}
[data-combobox-control][data-size='sm'] {
--_combobox-control-height: var(--control-height-sm);
--_combobox-control-px: var(--space-2-5);
--_combobox-control-gap: var(--space-1-5);
--_combobox-font-size: var(--font-size-xs);
}
[data-combobox-control][data-size='lg'] {
--_combobox-control-height: var(--control-height-lg);
--_combobox-control-px: var(--space-3-5);
--_combobox-control-gap: var(--space-2-5);
--_combobox-font-size: var(--font-size-md);
}
[data-combobox-control][data-variant='outline'] {
background: transparent;
box-shadow: none;
}
[data-combobox-control][data-variant='ghost'] {
border-color: transparent;
background: transparent;
box-shadow: none;
}
[data-combobox-control]:has([data-combobox-input]:focus-visible),
[data-combobox-control]:has([data-combobox-trigger]:focus-visible),
[data-combobox-control]:has([data-state='open']) {
border-color: var(--_combobox-accent-border);
box-shadow:
0 0 0 var(--focus-ring-offset) var(--color-surface-default),
0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
}
[data-combobox-control]:has([data-disabled]),
[data-combobox][data-disabled] [data-combobox-control] {
border-color: var(--color-border-subtle);
background: var(--color-surface-raised);
color: var(--color-content-disabled);
box-shadow: none;
}
[data-combobox-input] {
box-sizing: border-box;
flex: 1 1 auto;
inline-size: 100%;
min-inline-size: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
line-height: var(--leading-ui);
outline: none;
}
[data-combobox-input]::placeholder {
color: var(--color-content-muted);
}
[data-combobox-input]:disabled {
color: var(--color-content-disabled);
cursor: not-allowed;
}
[data-combobox-input]:not([data-combobox-control] [data-combobox-input]) {
min-block-size: var(--control-height-md);
padding-inline: var(--space-3);
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--radius-md);
background: var(--color-surface-default);
box-shadow: var(--shadow-xs);
}
[data-combobox-trigger] {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
inline-size: var(--control-height-xs);
block-size: var(--control-height-xs);
padding: 0;
border: 0;
border-radius: var(--radius-full);
background: transparent;
color: var(--color-content-secondary);
font: inherit;
cursor: pointer;
outline: none;
transition:
background var(--duration-fast) var(--ease-default),
color var(--duration-fast) var(--ease-default),
transform var(--duration-fast) var(--ease-default);
}
[data-combobox-trigger]:hover:not(:disabled):not([data-disabled]) {
background: var(--color-surface-raised);
color: var(--color-content-primary);
}
[data-combobox-trigger]:focus-visible {
box-shadow:
0 0 0 var(--focus-ring-offset) var(--color-surface-default),
0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
}
[data-combobox-trigger][data-disabled],
[data-combobox-trigger]:disabled {
color: var(--color-content-disabled);
cursor: not-allowed;
}
[data-combobox-indicator] {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: var(--icon-size-sm);
block-size: var(--icon-size-sm);
color: currentColor;
transition: transform var(--duration-fast) var(--ease-default);
}
[data-combobox-trigger][data-state='open'] [data-combobox-indicator] {
transform: rotate(180deg);
color: var(--_combobox-accent-text);
}
[data-floating-wrapper]:has(> [data-combobox-content]) {
z-index: 80;
}
[data-combobox-content] {
--_combobox-content-py: var(--space-1-5);
--_combobox-content-px: var(--space-1-5);
--_combobox-font-size: var(--font-size-sm);
--_combobox-content-width: var(--_combobox-match-anchor-width, 14rem);
box-sizing: border-box;
inline-size: min(var(--_combobox-content-width), calc(100vw - var(--space-4)));
min-inline-size: min(14rem, calc(100vw - var(--space-4)));
max-inline-size: min(28rem, calc(100vw - var(--space-4)));
max-block-size: min(22rem, var(--floating-available-height, 100dvh));
padding: var(--_combobox-content-py) var(--_combobox-content-px);
overflow: hidden;
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--radius-lg);
background: var(--color-surface-overlay);
box-shadow: var(--shadow-lg);
color: var(--color-content-primary);
font-family: var(--font-ui);
font-size: var(--_combobox-font-size);
line-height: var(--leading-ui);
outline: none;
transform-origin: var(--floating-transform-origin, center);
}
[data-combobox-content][data-size='sm'] {
--_combobox-content-py: var(--space-1);
--_combobox-content-px: var(--space-1);
--_combobox-font-size: var(--font-size-xs);
}
[data-combobox-content][data-size='lg'] {
--_combobox-content-py: var(--space-2);
--_combobox-content-px: var(--space-2);
--_combobox-font-size: var(--font-size-md);
}
[data-combobox-content][data-variant='outline'] {
box-shadow: var(--shadow-md);
}
[data-combobox-content][data-variant='ghost'] {
border-color: transparent;
box-shadow: var(--shadow-md);
}
[data-combobox-content][data-state='open'] {
animation: combobox-content-in var(--duration-normal) var(--ease-out) both;
}
[data-combobox-content][data-state='closed'] {
animation: combobox-content-out var(--duration-fast) var(--ease-default) both;
}
@keyframes combobox-content-in {
from {
opacity: 0;
transform: translateY(-2px) scale(0.98);
}
}
@keyframes combobox-content-out {
to {
opacity: 0;
transform: translateY(-2px) scale(0.98);
}
}
[data-combobox-viewport] {
display: flex;
flex-direction: column;
gap: var(--space-0-5);
max-block-size: calc(22rem - (var(--_combobox-content-py) * 2));
overflow: auto;
padding: var(--space-0-5);
}
[data-combobox-group] {
display: flex;
flex-direction: column;
gap: var(--space-0-5);
padding-block: var(--space-1);
}
[data-combobox-group-heading] {
padding: var(--space-1) var(--space-2);
color: var(--color-content-muted);
font-family: var(--font-ui);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
line-height: var(--leading-ui);
letter-spacing: 0.04em;
text-transform: uppercase;
}
[data-combobox-separator] {
block-size: var(--border-width);
margin-block: var(--space-1);
background: var(--color-border-subtle);
}
[data-combobox-item] {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
min-inline-size: 0;
min-block-size: var(--control-height-sm);
padding-inline: var(--space-2);
border-radius: var(--radius-md);
color: var(--color-content-primary);
cursor: pointer;
outline: none;
transition:
background var(--duration-fast) var(--ease-default),
color var(--duration-fast) var(--ease-default);
}
[data-combobox-item][data-highlighted] {
background: var(--color-surface-raised);
color: var(--color-content-primary);
}
[data-combobox-item][data-state='checked'] {
background: var(--_combobox-accent-track);
color: var(--_combobox-accent-text);
}
[data-combobox-item][data-disabled] {
color: var(--color-content-disabled);
cursor: not-allowed;
}
[data-combobox-item-indicator] {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
inline-size: var(--icon-size-sm);
block-size: var(--icon-size-sm);
color: currentColor;
opacity: 0;
transform: scale(0.75);
transition:
opacity var(--duration-fast) var(--ease-default),
transform var(--duration-fast) var(--ease-default);
}
[data-combobox-item][data-state='checked'] [data-combobox-item-indicator] {
opacity: 1;
transform: scale(1);
}
[data-combobox-arrow] svg {
display: block;
overflow: visible;
}
[data-combobox-arrow] polygon {
fill: var(--color-surface-overlay);
stroke: none;
}
[data-combobox-arrow] path {
stroke: var(--color-border-default);
stroke-width: var(--border-width);
stroke-linecap: round;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
}
@media (prefers-reduced-motion: reduce) {
[data-combobox-control],
[data-combobox-trigger],
[data-combobox-indicator],
[data-combobox-content],
[data-combobox-item],
[data-combobox-item-indicator] {
animation: none;
transition: none;
}
}

@ -0,0 +1,38 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
import * as Combobox from '$soma/components/combobox';
import { setComboboxVisualContext } from './context.svelte';
import type { ComboboxProps } from './types';
let {
size = 'md',
variant = 'surface',
color = 'primary',
value = $bindable([]),
inputValue = $bindable(''),
open = $bindable(false),
children,
...rest
}: ComboboxProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
setComboboxVisualContext({
getSize: () => resolvedSize,
getVariant: () => variant,
getColor: () => color
});
</script>
<Combobox.Provider
{...rest}
bind:value
bind:inputValue
bind:open
data-size={resolvedSize}
data-variant={variant}
data-color={color}
>
{@render children?.()}
</Combobox.Provider>

@ -0,0 +1,19 @@
import { getContext, setContext } from 'svelte'
import type { ComboboxColor, ComboboxSize, ComboboxVariant } from './types'
const COMBOBOX_VISUAL_CONTEXT = Symbol('uix.eidos.combobox.visual')
export interface ComboboxVisualContext {
getSize(): ComboboxSize
getVariant(): ComboboxVariant
getColor(): ComboboxColor
}
export function setComboboxVisualContext(ctx: ComboboxVisualContext): ComboboxVisualContext {
setContext(COMBOBOX_VISUAL_CONTEXT, ctx)
return ctx
}
export function getComboboxVisualContext(): ComboboxVisualContext | undefined {
return getContext<ComboboxVisualContext | undefined>(COMBOBOX_VISUAL_CONTEXT)
}

@ -0,0 +1,68 @@
import ComboboxComponent from './combobox.svelte'
import Control from './combobox-control.svelte'
import Input from './combobox-input.svelte'
import Trigger from './combobox-trigger.svelte'
import Indicator from './combobox-indicator.svelte'
import Content from './combobox-content.svelte'
import Viewport from './combobox-viewport.svelte'
import Item from './combobox-item.svelte'
import ItemIndicator from './combobox-item-indicator.svelte'
import Group from './combobox-group.svelte'
import GroupHeading from './combobox-group-heading.svelte'
import Separator from './combobox-separator.svelte'
import Arrow from './combobox-arrow.svelte'
import { Portal } from '$soma/components/internal'
type ComboboxNamespace = typeof ComboboxComponent & {
Control: typeof Control
Input: typeof Input
Trigger: typeof Trigger
Indicator: typeof Indicator
Portal: typeof Portal
Content: typeof Content
Viewport: typeof Viewport
Item: typeof Item
ItemIndicator: typeof ItemIndicator
Group: typeof Group
GroupHeading: typeof GroupHeading
Separator: typeof Separator
Arrow: typeof Arrow
}
const Combobox = ComboboxComponent as ComboboxNamespace
Combobox.Control = Control
Combobox.Input = Input
Combobox.Trigger = Trigger
Combobox.Indicator = Indicator
Combobox.Portal = Portal
Combobox.Content = Content
Combobox.Viewport = Viewport
Combobox.Item = Item
Combobox.ItemIndicator = ItemIndicator
Combobox.Group = Group
Combobox.GroupHeading = GroupHeading
Combobox.Separator = Separator
Combobox.Arrow = Arrow
export { Combobox }
export default Combobox
export type {
ComboboxProps,
ComboboxControlProps as ControlProps,
ComboboxInputProps as InputProps,
ComboboxTriggerProps as TriggerProps,
ComboboxIndicatorProps as IndicatorProps,
ComboboxContentProps as ContentProps,
ComboboxViewportProps as ViewportProps,
ComboboxItemProps as ItemProps,
ComboboxItemIndicatorProps as ItemIndicatorProps,
ComboboxGroupProps as GroupProps,
ComboboxGroupHeadingProps as GroupHeadingProps,
ComboboxSeparatorProps as SeparatorProps,
ComboboxArrowProps as ArrowProps,
ComboboxSize,
ComboboxVariant,
ComboboxColor
} from './types'

@ -0,0 +1,70 @@
import type {
ArrowProps,
ContentProps,
GroupHeadingProps,
GroupProps,
InputProps,
ItemProps,
ProviderProps,
SeparatorProps,
TriggerProps,
ViewportProps
} from '$soma/components/combobox'
import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types'
import type { Snippet } from 'svelte'
import type { HTMLAttributes } from 'svelte/elements'
export type ComboboxSize = Extract<Size, 'sm' | 'md' | 'lg'>
export type ComboboxVariant = ControlVariant
export type ComboboxColor = ColorRole
export type ComboboxProps = ProviderProps & {
/** Visual density for input, trigger, content and items. @default 'md' */
size?: ResponsiveProp<ComboboxSize>
/** Input/content treatment. @default 'surface' */
variant?: ComboboxVariant
/** Accent palette for focus, selected item and item indicator. @default 'primary' */
color?: ComboboxColor
}
export type ComboboxControlProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
children?: Snippet
size?: ResponsiveProp<ComboboxSize>
variant?: ComboboxVariant
color?: ComboboxColor
}
export type ComboboxInputProps = InputProps & {
size?: ResponsiveProp<ComboboxSize>
variant?: ComboboxVariant
color?: ComboboxColor
}
export type ComboboxTriggerProps = TriggerProps & {
size?: ResponsiveProp<ComboboxSize>
variant?: ComboboxVariant
color?: ComboboxColor
}
export type ComboboxIndicatorProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
children?: Snippet
}
export type ComboboxContentProps = ContentProps & {
size?: ResponsiveProp<ComboboxSize>
variant?: ComboboxVariant
color?: ComboboxColor
/**
* Match the floating panel's inline size to the input/control anchor.
* @default true
*/
matchAnchorWidth?: ResponsiveProp<boolean>
}
export type ComboboxViewportProps = ViewportProps
export type ComboboxItemProps = ItemProps
export type ComboboxItemIndicatorProps = ComboboxIndicatorProps
export type ComboboxGroupProps = GroupProps
export type ComboboxGroupHeadingProps = GroupHeadingProps
export type ComboboxSeparatorProps = SeparatorProps
export type ComboboxArrowProps = ArrowProps

@ -0,0 +1,74 @@
# Eidos DateField
Fecha de revision: 2026-05-19.
## Baseline
Air no tenia un `DateField` equivalente en la rama anterior. La referencia local
real es Soma `date-field`, que ya implementa la edicion segmentada, y la receta
Eidos solo envuelve esa superficie con tamanos, variante visual y paleta de
intents.
## Superficie
```svelte
<DateField bind:value placeholder={today} size="md" variant="surface" color="primary">
<DateField.Label>Arrival</DateField.Label>
<DateField.Input name="arrival">
{#snippet children({ segments })}
{#each segments as { part, value } (part + value)}
<DateField.Segment {part}>{value}</DateField.Segment>
{/each}
{/snippet}
</DateField.Input>
</DateField>
```
Partes publicas: `Label`, `Input`, `Segment`, `HiddenInput`.
## Comparativa
| Capacidad | Soma/Eidos | React Aria | Ark UI | Bits UI |
| --- | --- | --- | --- | --- |
| Input segmentado | Si | Si | Si | Si |
| Dia/mes/ano | Si | Si | Si | Si |
| Hora/minuto/segundo | Si | Si | Si | Si |
| AM/PM y zona horaria | Si | Si | Si | Si |
| `granularity` | Si | Si | Si | Si |
| `hideTimeZone` | Si | Si | Si | Si |
| `minValue` / `maxValue` | Si | Si | Si | Si |
| Validacion custom | Si | Si | Si | Si |
| Segmentos readonly | Si | No | No | Si |
| Input hidden para forms | Si | Via DateInput | Si | Si |
| Integracion con Field | Si | Via Form/TextField | Via Field | Manual |
| Variante/tamano/paleta visual | Si, Eidos | CSS externo | CSS externo | CSS externo |
## Decisiones
- No se exporta `Provider` ni `Root` publico. La forma Eidos es `<DateField>`
con hijos attached.
- Eidos no implementa logica de calendario ni popover. Eso pertenece a
`DatePicker`, no a `DateField`.
- `HiddenInput` se mantiene como parte publica por simetria con Soma, pero el
caso normal ya lo renderiza `DateField.Input` cuando recibe `name`.
- `size`, `variant` y `color` pertenecen a Eidos porque solo afectan receta
visual; `value`, `placeholder`, `granularity`, `locale`, `dir`, validacion y
navegacion de segmentos pertenecen a Soma.
- Los eventos declarados por Morfo son `commit-set` y `commit-clear`; la
edicion continua de segmentos no se modela como evento perceptivo por cada
pulsacion.
## Gaps
| Gap | Decision |
| --- | --- |
| Popover/calendario | Fuera de `DateField`; debe vivir en `DatePicker`. |
| Formatting visual extra | Usar `locale`, `hourCycle` y `granularity` de Soma. |
| Rango visual doble | Fuera de este componente; pertenece a range/date picker. |
## Referencias
- Soma DateField: `src/uix/soma/components/date-field/README.md`
- React Aria DateField: https://react-spectrum.adobe.com/react-aria/DateField.html
- Ark UI Date Picker / Field: https://ark-ui.com/docs/components/date-picker
- Bits UI Date Field: https://bits-ui.com/docs/components/date-field

@ -0,0 +1,5 @@
<script lang="ts">
import * as DateField from '$soma/components/date-field';
</script>
<DateField.HiddenInput />

@ -0,0 +1,12 @@
<script lang="ts">
import * as DateField from '$soma/components/date-field';
import type { DateFieldInputProps, DateFieldInputSnippetProps } from './types';
let { children: bodyContent, ...rest }: DateFieldInputProps = $props();
</script>
<DateField.Input {...rest}>
{#snippet children(args: DateFieldInputSnippetProps)}
{@render bodyContent?.(args)}
{/snippet}
</DateField.Input>

@ -0,0 +1,10 @@
<script lang="ts">
import * as DateField from '$soma/components/date-field';
import type { DateFieldLabelProps } from './types';
let { children, ...rest }: DateFieldLabelProps = $props();
</script>
<DateField.Label {...rest}>
{@render children?.()}
</DateField.Label>

@ -0,0 +1,10 @@
<script lang="ts">
import * as DateField from '$soma/components/date-field';
import type { DateFieldSegmentProps } from './types';
let { children, ...rest }: DateFieldSegmentProps = $props();
</script>
<DateField.Segment {...rest}>
{@render children?.()}
</DateField.Segment>

@ -0,0 +1,181 @@
[data-date-field] {
--_date-field-height: var(--date-field-height-md);
--_date-field-px: var(--date-field-px-md);
--_date-field-gap: var(--date-field-gap-md);
--_date-field-segment-px: var(--date-field-segment-px-md);
--_date-field-font-size: var(--date-field-font-size-md);
--_date-field-accent-border: var(--color-primary-border);
--_date-field-accent-track: var(--color-primary-track);
--_date-field-accent-text: var(--color-primary-text);
display: inline-flex;
flex-direction: column;
gap: var(--date-field-stack-gap);
inline-size: 100%;
min-inline-size: 0;
color: var(--date-field-color);
font-family: var(--date-field-font-family);
font-size: var(--_date-field-font-size);
line-height: var(--date-field-line-height);
}
[data-date-field][data-size='sm'] {
--_date-field-height: var(--date-field-height-sm);
--_date-field-px: var(--date-field-px-sm);
--_date-field-gap: var(--date-field-gap-sm);
--_date-field-segment-px: var(--date-field-segment-px-sm);
--_date-field-font-size: var(--date-field-font-size-sm);
}
[data-date-field][data-size='lg'] {
--_date-field-height: var(--date-field-height-lg);
--_date-field-px: var(--date-field-px-lg);
--_date-field-gap: var(--date-field-gap-lg);
--_date-field-segment-px: var(--date-field-segment-px-lg);
--_date-field-font-size: var(--date-field-font-size-lg);
}
[data-date-field][data-color='secondary'] {
--_date-field-accent-border: var(--color-secondary-border);
--_date-field-accent-track: var(--color-secondary-track);
--_date-field-accent-text: var(--color-secondary-text);
}
[data-date-field][data-color='neutral'] {
--_date-field-accent-border: var(--color-neutral-border);
--_date-field-accent-track: var(--color-neutral-track);
--_date-field-accent-text: var(--color-neutral-text);
}
[data-date-field][data-color='affirm'] {
--_date-field-accent-border: var(--color-affirm-border);
--_date-field-accent-track: var(--color-affirm-track);
--_date-field-accent-text: var(--color-affirm-text);
}
[data-date-field][data-color='fulfill'] {
--_date-field-accent-border: var(--color-fulfill-border);
--_date-field-accent-track: var(--color-fulfill-track);
--_date-field-accent-text: var(--color-fulfill-text);
}
[data-date-field][data-color='risk'] {
--_date-field-accent-border: var(--color-risk-border);
--_date-field-accent-track: var(--color-risk-track);
--_date-field-accent-text: var(--color-risk-text);
}
[data-date-field][data-color='threat'] {
--_date-field-accent-border: var(--color-threat-border);
--_date-field-accent-track: var(--color-threat-track);
--_date-field-accent-text: var(--color-threat-text);
}
[data-date-field][data-color='loss'] {
--_date-field-accent-border: var(--color-loss-border);
--_date-field-accent-track: var(--color-loss-track);
--_date-field-accent-text: var(--color-loss-text);
}
[data-date-field-label] {
color: var(--date-field-label-color);
font: inherit;
font-weight: var(--date-field-label-font-weight);
}
[data-date-field-input] {
display: inline-flex;
align-items: center;
gap: var(--_date-field-gap);
min-inline-size: 0;
inline-size: 100%;
min-block-size: var(--_date-field-height);
padding-inline: var(--_date-field-px);
border: var(--date-field-border-width) solid var(--date-field-border);
border-radius: var(--date-field-radius);
background: var(--date-field-bg);
color: inherit;
transition:
background var(--date-field-transition-duration) var(--date-field-transition-ease),
border-color var(--date-field-transition-duration) var(--date-field-transition-ease),
box-shadow var(--date-field-transition-duration) var(--date-field-transition-ease);
}
[data-date-field][data-variant='outline'] [data-date-field-input] {
background: transparent;
}
[data-date-field][data-variant='ghost'] [data-date-field-input] {
border-color: transparent;
background: transparent;
padding-inline: 0;
}
[data-date-field-input]:has([data-date-field-segment]:focus-visible) {
border-color: var(--_date-field-accent-border);
box-shadow: var(--date-field-focus-shadow);
}
[data-date-field][data-invalid] [data-date-field-input],
[data-date-field-input][data-invalid] {
border-color: var(--date-field-border-invalid);
}
[data-date-field][data-disabled],
[data-date-field-input][data-disabled] {
opacity: var(--date-field-disabled-opacity);
}
[data-date-field-segment] {
display: inline-flex;
align-items: center;
justify-content: center;
min-inline-size: var(--date-field-segment-min-width);
block-size: var(--date-field-segment-height);
padding-inline: var(--_date-field-segment-px);
border-radius: var(--date-field-segment-radius);
color: var(--date-field-segment-color);
font: inherit;
outline: none;
white-space: nowrap;
transition:
background var(--date-field-transition-duration) var(--date-field-transition-ease),
color var(--date-field-transition-duration) var(--date-field-transition-ease),
box-shadow var(--date-field-transition-duration) var(--date-field-transition-ease);
}
[data-date-field-segment][data-segment='literal'] {
min-inline-size: auto;
padding-inline: 0;
color: var(--date-field-literal-color);
}
[data-date-field-segment][data-placeholder] {
color: var(--date-field-placeholder-color);
}
[data-date-field-segment]:focus-visible {
background: var(--_date-field-accent-track);
color: var(--_date-field-accent-text);
box-shadow: var(--date-field-segment-focus-shadow);
}
[data-date-field-segment][data-invalid] {
color: var(--date-field-segment-invalid-color);
}
[data-date-field-segment][data-readonly] {
background: var(--date-field-segment-readonly-bg);
}
[data-date-field-segment][data-disabled] {
cursor: not-allowed;
opacity: var(--date-field-disabled-opacity);
}
@media (prefers-reduced-motion: reduce) {
[data-date-field-input],
[data-date-field-segment] {
transition: none;
}
}

@ -0,0 +1,29 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
import * as DateField from '$soma/components/date-field';
import type { DateFieldProps } from './types';
let {
size = 'md',
variant = 'surface',
color = 'primary',
value = $bindable(),
placeholder = $bindable(),
children: bodyContent,
...rest
}: DateFieldProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
</script>
<DateField.Provider
{...rest}
bind:value
bind:placeholder
data-size={resolvedSize}
data-variant={variant}
data-color={color}
>
{@render bodyContent?.()}
</DateField.Provider>

@ -0,0 +1,34 @@
import DateFieldComponent from './date-field.svelte'
import Label from './date-field-label.svelte'
import Input from './date-field-input.svelte'
import Segment from './date-field-segment.svelte'
import HiddenInput from './date-field-hidden-input.svelte'
type DateFieldNamespace = typeof DateFieldComponent & {
Label: typeof Label
Input: typeof Input
Segment: typeof Segment
HiddenInput: typeof HiddenInput
}
const DateField = DateFieldComponent as DateFieldNamespace
DateField.Label = Label
DateField.Input = Input
DateField.Segment = Segment
DateField.HiddenInput = HiddenInput
export { DateField }
export default DateField
export type {
DateFieldProps,
DateFieldLabelProps as LabelProps,
DateFieldInputProps as InputProps,
DateFieldSegmentProps as SegmentProps,
DateFieldHiddenInputProps as HiddenInputProps,
InputSnippetProps,
DateFieldSize,
DateFieldVariant,
DateFieldColor
} from './types'

@ -0,0 +1,28 @@
import type {
InputProps,
InputSnippetProps as DateFieldInputSnippetProps,
LabelProps,
ProviderProps,
SegmentProps
} from '$soma/components/date-field'
import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types'
export type DateFieldSize = Extract<Size, 'sm' | 'md' | 'lg'>
export type DateFieldVariant = ControlVariant
export type DateFieldColor = ColorRole
export type DateFieldProps = ProviderProps & {
/** Visual size for input height, segment padding and text. @default 'md' */
size?: ResponsiveProp<DateFieldSize>
/** Visual treatment of the segmented input. @default 'surface' */
variant?: DateFieldVariant
/** Intent palette for focus affordance. @default 'primary' */
color?: DateFieldColor
}
export type DateFieldLabelProps = LabelProps
export type DateFieldInputProps = InputProps
export type DateFieldSegmentProps = SegmentProps
export type DateFieldHiddenInputProps = Record<string, never>
export type { DateFieldInputSnippetProps, DateFieldInputSnippetProps as InputSnippetProps }

@ -1,6 +1,6 @@
<script lang="ts">
/**
* Eidos `<Dialog.Content>` — wraps the headless content and adds responsive
* Eidos `Dialog.Content` — wraps the headless content and adds responsive
* `size` resolution + auto-sheet behaviour on narrow viewports.
*
* Sheet rule: when the resolved size is anything other than `'full'`

@ -1,6 +1,6 @@
<script lang="ts">
/**
* Eidos `<Dialog.Overlay>` — wraps the headless overlay and adds the
* Eidos `Dialog.Overlay` — wraps the headless overlay and adds the
* `overlayOpacity` ergonomic prop. Accepts either a fraction (`0.4` →
* 40 %) or an explicit CSS value (`'80%'`); emits the result on the
* inline `--dialog-overlay-opacity` custom property so the recipe's

@ -19,12 +19,12 @@
* </Dialog.Portal>
* </Dialog>
*/
import * as SomaDialog from '$soma/components/dialog';
import * as Dialog from '$soma/components/dialog';
import type { DialogProps } from './types';
let { open = $bindable(false), children, ...rest }: DialogProps = $props();
</script>
<SomaDialog.Provider {...rest} bind:open>
<Dialog.Provider {...rest} bind:open>
{@render children?.()}
</SomaDialog.Provider>
</Dialog.Provider>

@ -16,7 +16,7 @@
// </Dialog.Content>
// </Dialog.Portal>
// </Dialog>
import DialogRoot from './dialog.svelte';
import DialogComponent from './dialog.svelte';
import Trigger from './dialog-trigger.svelte';
import Overlay from './dialog-overlay.svelte';
import Content from './dialog-content.svelte';
@ -27,7 +27,7 @@ import Header from './dialog-header.svelte';
import Footer from './dialog-footer.svelte';
import { Portal } from '$soma/components/internal';
type DialogNamespace = typeof DialogRoot & {
type DialogNamespace = typeof DialogComponent & {
Trigger: typeof Trigger;
Portal: typeof Portal;
Overlay: typeof Overlay;
@ -39,7 +39,7 @@ type DialogNamespace = typeof DialogRoot & {
Footer: typeof Footer;
};
const Dialog = DialogRoot as DialogNamespace;
const Dialog = DialogComponent as DialogNamespace;
Dialog.Trigger = Trigger;
Dialog.Portal = Portal;
Dialog.Overlay = Overlay;

@ -2,13 +2,13 @@ import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import type { Position, ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type {
ProviderProps as SomaDialogProviderProps,
TriggerProps as SomaDialogTriggerProps,
ContentProps as SomaDialogContentProps,
OverlayProps as SomaDialogOverlayProps,
TitleProps as SomaDialogTitleProps,
DescriptionProps as SomaDialogDescriptionProps,
CloseProps as SomaDialogCloseProps
ProviderProps,
TriggerProps,
ContentProps,
OverlayProps,
TitleProps,
DescriptionProps,
CloseProps
} from '$soma/components/dialog';
/**
@ -28,10 +28,10 @@ export type DialogSize = Extract<Size, 'sm' | 'md' | 'lg' | 'xl' | 'full'>;
export type DialogPosition = Position;
export type DialogCssLength = string | number;
export type DialogProps = SomaDialogProviderProps;
export type DialogTriggerProps = SomaDialogTriggerProps;
export type DialogProps = ProviderProps;
export type DialogTriggerProps = TriggerProps;
export type DialogContentProps = SomaDialogContentProps & {
export type DialogContentProps = ContentProps & {
/**
* Visual width preset. Responsive — pass either a single value or a
* breakpoint map (`{ base: 'full', md: 'md' }`). Resolved reactively via
@ -75,7 +75,7 @@ export type DialogContentProps = SomaDialogContentProps & {
maxHeight?: ResponsiveProp<DialogCssLength>;
};
export type DialogOverlayProps = SomaDialogOverlayProps & {
export type DialogOverlayProps = OverlayProps & {
/**
* Override the default overlay opacity. Accepts either a fraction
* (`0.4` → 40 %) or an explicit CSS value (`'80%'`, `'0.5'`). Drives
@ -84,8 +84,8 @@ export type DialogOverlayProps = SomaDialogOverlayProps & {
overlayOpacity?: number | string;
};
export type DialogTitleProps = SomaDialogTitleProps;
export type DialogDescriptionProps = SomaDialogDescriptionProps;
export type DialogTitleProps = TitleProps;
export type DialogDescriptionProps = DescriptionProps;
/**
* Where the Close button anchors inside Content. Subset of the canonical
@ -99,7 +99,7 @@ export type DialogClosePosition = Extract<
'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
>;
export type DialogCloseProps = SomaDialogCloseProps & {
export type DialogCloseProps = CloseProps & {
/**
* Close anchor inside Content. Responsive — pass a single value or a
* breakpoint map. Resolved reactively via `Eidos.resolve(position)`.

@ -1,6 +1,6 @@
<script lang="ts">
/**
* Eidos `<Drawer.Close>` — adds the auto-X icon when no children are
* Eidos `Drawer.Close` — adds the auto-X icon when no children are
* provided (matches the air baseline) and emits `data-icon-only` so the
* recipe can size the button as a square. The icon is inline SVG (no
* external Icon dependency) to keep the wrapper self-contained.

@ -1,6 +1,6 @@
<script lang="ts">
/**
* Eidos `<Drawer.Content>` — wraps the headless content and adds responsive
* Eidos `Drawer.Content` — wraps the headless content and adds responsive
* `size` resolution. The size attr maps to width (left/right drawers) or
* height (top/bottom drawers) via the recipe.
*

@ -1,6 +1,6 @@
<script lang="ts">
/**
* Eidos `<Drawer.Handle>` — passthrough wrapper for the headless Handle part.
* Eidos `Drawer.Handle` — passthrough wrapper for the headless Handle part.
* The recipe styles the handle as a small grab-bar oriented along the
* drawer's main axis (horizontal for top/bottom, vertical for left/right).
* Cycles through snap points on tap unless `preventCycle` is set.

@ -1,6 +1,6 @@
<script lang="ts">
/**
* Eidos `<Drawer.Overlay>` — wraps the headless overlay and adds the
* Eidos `Drawer.Overlay` — wraps the headless overlay and adds the
* `overlayOpacity` ergonomic prop. Mirrors Dialog's overlay shape:
* accepts a fraction (`0.4` → 40 %) or an explicit CSS value (`'80%'`)
* and emits the result on `--drawer-overlay-opacity` so the recipe's

@ -1,6 +1,6 @@
<script lang="ts">
/**
* Eidos `<Drawer>` — root component. Wraps Soma's `<Drawer.Provider>`
* Eidos `<Drawer>` — root component. Wraps Soma's `<Drawer.Drawer.Provider>`
* to set up the headless context; children compose via the named
* members attached in `index.ts`:
*
@ -12,7 +12,7 @@
* </Drawer.Portal>
* </Drawer>
*/
import { Provider as SomaDrawerProvider } from '$soma/components/drawer';
import * as Drawer from '$soma/components/drawer';
import type { DrawerProps } from './types';
let {
@ -24,6 +24,6 @@
}: DrawerProps = $props();
</script>
<SomaDrawerProvider {...rest} bind:open bind:activeSnapPoint bind:isDragging>
<Drawer.Provider {...rest} bind:open bind:activeSnapPoint bind:isDragging>
{@render children?.()}
</SomaDrawerProvider>
</Drawer.Provider>

@ -24,7 +24,7 @@
// follows air / headless: Trigger, Content, Overlay, Title, Description,
// Close, Portal (+ Header, Footer as eidos-only layout shells, and
// Handle inherited from Soma's drag affordance).
import DrawerRoot from './drawer.svelte';
import DrawerComponent from './drawer.svelte';
import Trigger from './drawer-trigger.svelte';
import Overlay from './drawer-overlay.svelte';
import Content from './drawer-content.svelte';
@ -40,7 +40,7 @@ import { Portal } from '$soma/components/internal';
// to avoid a Svelte 5 hydration quirk where mutating the component
// constructor in bulk caused `<Drawer.Trigger>` children
// to re-mount on hydration (button appeared, then disappeared).
type DrawerNamespace = typeof DrawerRoot & {
type DrawerNamespace = typeof DrawerComponent & {
Trigger: typeof Trigger;
Portal: typeof Portal;
Overlay: typeof Overlay;
@ -53,7 +53,7 @@ type DrawerNamespace = typeof DrawerRoot & {
Footer: typeof Footer;
};
const Drawer = DrawerRoot as DrawerNamespace;
const Drawer = DrawerComponent as DrawerNamespace;
Drawer.Trigger = Trigger;
Drawer.Portal = Portal;
Drawer.Overlay = Overlay;

@ -2,14 +2,14 @@ import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type {
ProviderProps as SomaDrawerProviderProps,
TriggerProps as SomaDrawerTriggerProps,
ContentProps as SomaDrawerContentProps,
OverlayProps as SomaDrawerOverlayProps,
HandleProps as SomaDrawerHandleProps,
TitleProps as SomaDrawerTitleProps,
DescriptionProps as SomaDrawerDescriptionProps,
CloseProps as SomaDrawerCloseProps,
ProviderProps,
TriggerProps,
ContentProps,
OverlayProps,
HandleProps,
TitleProps,
DescriptionProps,
CloseProps,
DrawerVariant,
DrawerDirection,
DrawerSide
@ -26,13 +26,13 @@ export type { DrawerVariant, DrawerDirection, DrawerSide };
export type DrawerCssLength = string | number;
export type DrawerProps = SomaDrawerProviderProps;
export type DrawerTriggerProps = SomaDrawerTriggerProps;
export type DrawerHandleProps = SomaDrawerHandleProps;
export type DrawerTitleProps = SomaDrawerTitleProps;
export type DrawerDescriptionProps = SomaDrawerDescriptionProps;
export type DrawerProps = ProviderProps;
export type DrawerTriggerProps = TriggerProps;
export type DrawerHandleProps = HandleProps;
export type DrawerTitleProps = TitleProps;
export type DrawerDescriptionProps = DescriptionProps;
export type DrawerContentProps = SomaDrawerContentProps & {
export type DrawerContentProps = ContentProps & {
/**
* Visual size preset on the drawer's main axis. Resolved reactively via
* `Eidos.resolve(size)`. Pass either a single value or a breakpoint map
@ -59,7 +59,7 @@ export type DrawerContentProps = SomaDrawerContentProps & {
maxHeight?: ResponsiveProp<DrawerCssLength>;
};
export type DrawerOverlayProps = SomaDrawerOverlayProps & {
export type DrawerOverlayProps = OverlayProps & {
/**
* Override the default overlay opacity. Accepts either a fraction
* (`0.4` → 40 %) or an explicit CSS value (`'80%'`, `'0.5'`). Drives
@ -68,7 +68,7 @@ export type DrawerOverlayProps = SomaDrawerOverlayProps & {
overlayOpacity?: number | string;
};
export type DrawerCloseProps = SomaDrawerCloseProps;
export type DrawerCloseProps = CloseProps;
/**
* Eidos-only layout shells for the typical "header / body / footer" drawer

@ -1,13 +1,15 @@
# Editable
Eidos `Editable` viste el primitive headless de Soma para edición inline. Air no
tenia equivalente en `glm-5`; la baseline local es Soma/Morfo.
tenia equivalente directo en `src/uix/air/components`, pero la rama anterior si
tenia baseline en `src/uix/terra/editable`.
## Baseline
| Fuente | Superficie | Estado Eidos |
| --- | --- | --- |
| Air `glm-5` | No existe `editable`. | Sin baseline Air. |
| Air `glm-5` | No existe `src/uix/air/components/editable`. | Sin baseline Air directa. |
| Terra `glm-5` | `Root`, `Area`, `Preview`, `Input`, `EditTrigger`, `SubmitTrigger`, `CancelTrigger`; preview inline, input solo en modo edición y acciones auxiliares. | Baseline visual recuperada: `Editable` no debe parecer un `Field/Input` permanente. |
| Soma actual | `Provider`, `Area`, `Preview`, `Input`, `EditTrigger`, `SubmitTrigger`, `CancelTrigger`; activation/submit modes, draft, auto-resize, max length, disabled/readonly. | Cubierto por passthrough. Eidos anade `size`, `variant`, `color` y receta. |
| Chakra / Ark Editable | Root, Area, Preview, Input, Control, Edit/Submit/Cancel triggers, activation modes, selectOnFocus, textarea. | Cubierto salvo `Control` dedicado; `Area` agrupa preview/input y triggers son hermanos. |
| Radix Vue Editable | Root, Area, Preview, Input, Edit/Submit/Cancel triggers. | Cubierto. |
@ -28,9 +30,11 @@ Referencias externas consultadas:
<Editable.Preview />
<Editable.Input />
</Editable.Area>
<Editable.EditTrigger>Edit</Editable.EditTrigger>
<Editable.SubmitTrigger>Save</Editable.SubmitTrigger>
<Editable.CancelTrigger>Cancel</Editable.CancelTrigger>
<Editable.Control>
<Editable.EditTrigger>Edit</Editable.EditTrigger>
<Editable.SubmitTrigger>Save</Editable.SubmitTrigger>
<Editable.CancelTrigger>Cancel</Editable.CancelTrigger>
</Editable.Control>
</Editable>
```
@ -38,8 +42,13 @@ Referencias externas consultadas:
- No se crea `Provider` publico: el root visual es `Editable`.
- No se anade API flat con snippets.
- `Editable.Control` agrupa acciones; Soma expone estado en el control y en
los triggers, y Eidos decide la visibilidad visual de Edit frente a
Save/Cancel.
- `size` es responsive y se resuelve con `ActiveEidos`.
- `variant` controla el tratamiento del preview/input.
- `variant` controla el tratamiento del preview/input, pero el preview se
mantiene como contenido inline: si se quiere una caja permanente se debe usar
`Field`/`Input`, no `Editable`.
- `color` usa paletas canonicas de Eidos para foco y submit.
- El soporte textarea se mantiene en Soma mediante `child`.

@ -1,10 +1,10 @@
<script lang="ts">
import { Area as SomaEditableArea } from '$soma/components/editable';
import * as Editable from '$soma/components/editable';
import type { EditableAreaProps } from './types';
let { children, ...rest }: EditableAreaProps = $props();
</script>
<SomaEditableArea {...rest}>
<Editable.Area {...rest}>
{@render children?.()}
</SomaEditableArea>
</Editable.Area>

@ -1,10 +1,10 @@
<script lang="ts">
import { CancelTrigger as SomaEditableCancelTrigger } from '$soma/components/editable';
import * as Editable from '$soma/components/editable';
import type { EditableCancelTriggerProps } from './types';
let { children, ...rest }: EditableCancelTriggerProps = $props();
</script>
<SomaEditableCancelTrigger {...rest}>
<Editable.CancelTrigger {...rest}>
{@render children?.()}
</SomaEditableCancelTrigger>
</Editable.CancelTrigger>

@ -0,0 +1,10 @@
<script lang="ts">
import * as Editable from '$soma/components/editable';
import type { EditableControlProps } from './types';
let { children, ...rest }: EditableControlProps = $props();
</script>
<Editable.Control {...rest}>
{@render children?.()}
</Editable.Control>

@ -1,10 +1,10 @@
<script lang="ts">
import { EditTrigger as SomaEditableEditTrigger } from '$soma/components/editable';
import * as Editable from '$soma/components/editable';
import type { EditableEditTriggerProps } from './types';
let { children, ...rest }: EditableEditTriggerProps = $props();
</script>
<SomaEditableEditTrigger {...rest}>
<Editable.EditTrigger {...rest}>
{@render children?.()}
</SomaEditableEditTrigger>
</Editable.EditTrigger>

@ -1,10 +1,10 @@
<script lang="ts">
import { Input as SomaEditableInput } from '$soma/components/editable';
import * as Editable from '$soma/components/editable';
import type { EditableInputProps } from './types';
let { children, ...rest }: EditableInputProps = $props();
</script>
<SomaEditableInput {...rest}>
<Editable.Input {...rest}>
{@render children?.()}
</SomaEditableInput>
</Editable.Input>

@ -1,10 +1,14 @@
<script lang="ts">
import { Preview as SomaEditablePreview } from '$soma/components/editable';
import * as Editable from '$soma/components/editable';
import type { EditablePreviewProps } from './types';
let { children, ...rest }: EditablePreviewProps = $props();
let { children: bodyContent, ...rest }: EditablePreviewProps = $props();
</script>
<SomaEditablePreview {...rest}>
{@render children?.()}
</SomaEditablePreview>
{#if bodyContent}
<Editable.Preview {...rest}>
{@render bodyContent()}
</Editable.Preview>
{:else}
<Editable.Preview {...rest} />
{/if}

@ -1,10 +1,10 @@
<script lang="ts">
import { SubmitTrigger as SomaEditableSubmitTrigger } from '$soma/components/editable';
import * as Editable from '$soma/components/editable';
import type { EditableSubmitTriggerProps } from './types';
let { children, ...rest }: EditableSubmitTriggerProps = $props();
</script>
<SomaEditableSubmitTrigger {...rest}>
<Editable.SubmitTrigger {...rest}>
{@render children?.()}
</SomaEditableSubmitTrigger>
</Editable.SubmitTrigger>

@ -8,10 +8,10 @@
--_editable-accent-border: var(--editable-color-primary-border);
--_editable-accent-text: var(--editable-color-primary-text);
display: inline-grid;
grid-template-columns: minmax(0, 1fr) auto auto auto;
display: inline-flex;
align-items: center;
gap: var(--_editable-gap);
max-inline-size: 100%;
min-inline-size: 0;
font-family: var(--editable-font-family);
font-size: var(--_editable-font-size);
@ -19,6 +19,10 @@
color: var(--editable-color);
}
[data-editable][data-editing] {
align-items: flex-start;
}
[data-editable][data-size='sm'] {
--_editable-gap: var(--editable-gap-sm);
--_editable-control-height: var(--editable-control-height-sm);
@ -70,19 +74,28 @@
[data-editable-area] {
display: grid;
align-items: center;
min-inline-size: var(--editable-area-min-inline-size);
max-inline-size: 100%;
}
[data-editable-control] {
display: inline-flex;
align-items: center;
gap: var(--editable-trigger-gap);
}
[data-editable-preview],
[data-editable-input] {
grid-area: 1 / 1;
box-sizing: border-box;
min-inline-size: 0;
min-block-size: var(--_editable-control-height);
padding-inline: var(--_editable-control-px);
border: var(--editable-control-border-width) solid var(--editable-control-border);
border: var(--editable-control-border-width) solid transparent;
border-radius: var(--editable-control-radius);
background: var(--editable-control-bg);
box-shadow: var(--editable-control-shadow);
background: transparent;
box-shadow: none;
color: inherit;
font: inherit;
outline: none;
@ -95,29 +108,45 @@
[data-editable-preview] {
display: inline-flex;
align-items: center;
inline-size: fit-content;
max-inline-size: 100%;
cursor: text;
}
[data-editable-input] {
inline-size: 100%;
border-color: var(--_editable-accent-border);
background: var(--editable-control-bg);
box-shadow: var(--editable-control-shadow), var(--editable-focus-shadow);
}
[data-editable][data-variant='outline'] [data-editable-preview],
[data-editable][data-variant='outline'] [data-editable-input] {
background: transparent;
box-shadow: none;
}
[data-editable][data-variant='ghost'] [data-editable-preview],
[data-editable][data-variant='ghost'] [data-editable-preview] {
padding-inline: 0;
border-color: transparent;
}
[data-editable][data-variant='ghost'] [data-editable-input] {
border-color: transparent;
background: transparent;
box-shadow: none;
}
[data-editable-preview]:hover:not([data-disabled]):not([data-readonly]),
[data-editable-input]:hover:not([data-disabled]):not([data-readonly]) {
[data-editable-preview]:hover:not([data-disabled]):not([data-readonly]) {
border-color: var(--editable-control-border-hover);
background: var(--editable-control-bg);
}
[data-editable][data-variant='ghost']
[data-editable-preview]:hover:not([data-disabled]):not([data-readonly]) {
background: transparent;
}
[data-editable-input]:hover:not([data-disabled]):not([data-readonly]) {
border-color: var(--_editable-accent-border);
}
[data-editable-preview]:focus-visible,
@ -177,9 +206,24 @@
box-shadow: var(--editable-focus-shadow);
}
[data-editable-preview][hidden],
[data-editable-input][hidden],
[data-editable-edit-trigger][hidden],
[data-editable-submit-trigger][hidden],
[data-editable-cancel-trigger][hidden] {
display: none;
}
[data-editable][data-editing] [data-editable-edit-trigger],
[data-editable]:not([data-editing]) [data-editable-submit-trigger],
[data-editable]:not([data-editing]) [data-editable-cancel-trigger] {
display: none;
}
[data-editable][data-disabled],
[data-editable-preview][data-disabled],
[data-editable-input][data-disabled],
[data-editable-control][data-disabled],
[data-editable-edit-trigger][data-disabled],
[data-editable-submit-trigger][data-disabled],
[data-editable-cancel-trigger][data-disabled] {

@ -1,13 +1,15 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
import { Provider as SomaEditableProvider } from '$soma/components/editable';
import * as Editable from '$soma/components/editable';
import type { EditableProps } from './types';
let {
size = 'md',
variant = 'surface',
color = 'primary',
children,
value = $bindable(''),
editing = $bindable(false),
children: bodyContent,
...rest
}: EditableProps = $props();
@ -15,11 +17,13 @@
const resolvedSize = $derived(eidos.resolve(size, 'md'));
</script>
<SomaEditableProvider
<Editable.Provider
{...rest}
bind:value
bind:editing
data-size={resolvedSize}
data-variant={variant}
data-color={color}
>
{@render children?.()}
</SomaEditableProvider>
{@render bodyContent?.()}
</Editable.Provider>

@ -1,13 +1,15 @@
import EditableRoot from './editable.svelte';
import EditableComponent from './editable.svelte';
import Area from './editable-area.svelte';
import Control from './editable-control.svelte';
import Preview from './editable-preview.svelte';
import Input from './editable-input.svelte';
import EditTrigger from './editable-edit-trigger.svelte';
import SubmitTrigger from './editable-submit-trigger.svelte';
import CancelTrigger from './editable-cancel-trigger.svelte';
type EditableNamespace = typeof EditableRoot & {
type EditableNamespace = typeof EditableComponent & {
Area: typeof Area;
Control: typeof Control;
Preview: typeof Preview;
Input: typeof Input;
EditTrigger: typeof EditTrigger;
@ -15,8 +17,9 @@ type EditableNamespace = typeof EditableRoot & {
CancelTrigger: typeof CancelTrigger;
};
const Editable = EditableRoot as EditableNamespace;
const Editable = EditableComponent as EditableNamespace;
Editable.Area = Area;
Editable.Control = Control;
Editable.Preview = Preview;
Editable.Input = Input;
Editable.EditTrigger = EditTrigger;
@ -30,6 +33,7 @@ export default Editable;
export type {
EditableProps,
EditableAreaProps as AreaProps,
EditableControlProps as ControlProps,
EditablePreviewProps as PreviewProps,
EditableInputProps as InputProps,
EditableEditTriggerProps as EditTriggerProps,

@ -1,19 +1,20 @@
import type {
AreaProps as SomaEditableAreaProps,
CancelTriggerProps as SomaEditableCancelTriggerProps,
EditTriggerProps as SomaEditableEditTriggerProps,
InputProps as SomaEditableInputProps,
PreviewProps as SomaEditablePreviewProps,
ProviderProps as SomaEditableProviderProps,
SubmitTriggerProps as SomaEditableSubmitTriggerProps
AreaProps,
CancelTriggerProps,
ControlProps,
EditTriggerProps,
InputProps,
PreviewProps,
ProviderProps,
SubmitTriggerProps
} from '$soma/components/editable';
import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type { EditableColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types';
export type EditableSize = Extract<Size, 'sm' | 'md' | 'lg'>;
export type EditableVariant = 'surface' | 'outline' | 'ghost';
export type EditableColor = 'primary' | 'secondary' | 'neutral' | 'affirm' | 'risk' | 'threat';
export type EditableVariant = ControlVariant;
export type EditableColor = EditableColorRole;
export type EditableProps = SomaEditableProviderProps & {
export type EditableProps = ProviderProps & {
/** Visual density for preview, input and action buttons. @default 'md' */
size?: ResponsiveProp<EditableSize>;
/** Inline edit treatment. @default 'surface' */
@ -22,9 +23,10 @@ export type EditableProps = SomaEditableProviderProps & {
color?: EditableColor;
};
export type EditableAreaProps = SomaEditableAreaProps;
export type EditablePreviewProps = SomaEditablePreviewProps;
export type EditableInputProps = SomaEditableInputProps;
export type EditableEditTriggerProps = SomaEditableEditTriggerProps;
export type EditableSubmitTriggerProps = SomaEditableSubmitTriggerProps;
export type EditableCancelTriggerProps = SomaEditableCancelTriggerProps;
export type EditableAreaProps = AreaProps;
export type EditableControlProps = ControlProps;
export type EditablePreviewProps = PreviewProps;
export type EditableInputProps = InputProps;
export type EditableEditTriggerProps = EditTriggerProps;
export type EditableSubmitTriggerProps = SubmitTriggerProps;
export type EditableCancelTriggerProps = CancelTriggerProps;

@ -5,13 +5,13 @@
`Field` es el wrapper visual de Eidos sobre el campo de formulario de Soma.
Soma posee el estado (`disabled`, `readonly`, `required`, `invalid`, `dirty`,
`touched`, `loading`), la integracion con `Form.Provider` y todo el cableado
ARIA entre label, input, helper text y error text. Eidos solo anade `size`,
`variant` y receta visual.
ARIA entre label, input, helper text y error text. Eidos anade `size`,
`variant`, `orientation`, `color` y receta visual.
API publica:
```svelte
<Field required invalid size="md" variant="outline">
<Field required invalid size="md" variant="outline" orientation="vertical" color="primary">
<Field.Label>
Email
<Field.RequiredIndicator />
@ -51,6 +51,8 @@ alinearse con el contrato Soma/Morfo.
| Required indicator | Morfo/Soma/Eidos | Parte publica contract-backed; Eidos renderiza `*` por defecto |
| Visual size | Eidos | `xs`, `sm`, `md`, `lg`, responsive |
| Visual variant | Eidos | `outline`, `ghost` |
| Visual orientation | Eidos | `vertical`, `horizontal`, `responsive` |
| Intent color | Eidos | Focus/required accent; invalid sigue usando estado de error |
## Comparativa externa
@ -60,7 +62,7 @@ alinearse con el contrato Soma/Morfo.
| Base UI Field | `Root`, `Label`, `Control`, `Description`, `Item`, `Error`, `Validity`; soporta touched/dirty/focused/filled y validacion. | UIX cubre root/control/error y dirty/touched desde `Form`. `Validity`, `Item`, focused/filled quedan para Soma si aparece un consumer. |
| Radix Form | `Form.Field`, `Label`, `Control`, `Message`, `ValidityState`; mensajes asociados automaticamente al control. | UIX cubre asociacion label/control/error. ValidityState nativo esta diferido al modulo `Form`, no a Eidos. |
| React Aria TextField | `TextField`, `Label`, `Input`/`TextArea`, description slot y `FieldError`; data attrs para disabled/invalid/readonly/required. | UIX cubre `Input`, description via `HelperText`, error via `ErrorText` y los mismos estados data. |
| shadcn-svelte Field | Campo visual de composicion con `Field`, `Label`, `Description`, `Error`, `Group`, `Set`, `Legend`, `Content`. | UIX cubre el campo individual. `Group`/`Set`/`Legend` son composicion/form layout y quedan como futuro componente o `Form`/`Fieldset`, no como parte de este wrapper. |
| shadcn-svelte Field | Campo visual de composicion con `Field`, `Label`, `Description`, `Error`, `Group`, `Set`, `Legend`, `Content`, orientacion responsive. | UIX cubre el campo individual y orientacion `vertical/horizontal/responsive`. `Group`/`Set`/`Legend` quedan como futuro `Fieldset`/`Form`. |
| Bits UI | No tiene `Field` completo; ofrece `Label` y componentes de formulario separados. | UIX tiene Field dedicado. |
Fuentes:
@ -107,6 +109,15 @@ fieldset que afecta a grupos de campos.
Decision: diferido a `Fieldset`/`Form`/layout de formulario, no a este wrapper.
### Orientation
La orientacion de un campo individual si pertenece a Eidos: cambia la relacion
visual entre label, control y mensajes sin crear estado ni comportamiento.
Decision: `Field` expone `orientation="vertical" | "horizontal" | "responsive"`.
El modo `responsive` usa container query para pasar a dos columnas cuando hay
ancho suficiente.
### ValidityState
Radix y Base UI exponen partes para inspeccionar `ValidityState`. UIX ya tiene
@ -115,6 +126,13 @@ comportamiento, no visual.
Decision: si se necesita, se implementa en Soma/Form.
## Eventos Sema
Field declara 0 eventos semanticos de forma deliberada. Es una envoltura de
composicion para label, control, mensajes y estado de formulario; las acciones
del usuario pertenecen al control que vive dentro (`Input`, `SearchField`,
`NumberField`, `Select`, etc.) o al modulo `Form`, no al shell visual del campo.
## Gaps
| Gap | Decision |

@ -1,10 +1,10 @@
<script lang="ts">
import { Control as SomaFieldControl } from '$soma/components/field';
import * as Field from '$soma/components/field';
import type { FieldControlProps } from './types';
let { children, ...rest }: FieldControlProps = $props();
</script>
<SomaFieldControl {...rest}>
<Field.Control {...rest}>
{@render children?.()}
</SomaFieldControl>
</Field.Control>

@ -1,10 +1,10 @@
<script lang="ts">
import { ErrorText as SomaFieldErrorText } from '$soma/components/field';
import * as Field from '$soma/components/field';
import type { FieldErrorTextProps } from './types';
let { children, ...rest }: FieldErrorTextProps = $props();
</script>
<SomaFieldErrorText {...rest}>
<Field.ErrorText {...rest}>
{@render children?.()}
</SomaFieldErrorText>
</Field.ErrorText>

@ -1,10 +1,10 @@
<script lang="ts">
import { HelperText as SomaFieldHelperText } from '$soma/components/field';
import * as Field from '$soma/components/field';
import type { FieldHelperTextProps } from './types';
let { children, ...rest }: FieldHelperTextProps = $props();
</script>
<SomaFieldHelperText {...rest}>
<Field.HelperText {...rest}>
{@render children?.()}
</SomaFieldHelperText>
</Field.HelperText>

@ -1,10 +1,10 @@
<script lang="ts">
import { Input as SomaFieldInput } from '$soma/components/field';
import * as Field from '$soma/components/field';
import type { FieldInputProps } from './types';
let { value = $bindable(''), children, ...rest }: FieldInputProps = $props();
</script>
<SomaFieldInput {...rest} bind:value>
<Field.Input {...rest} bind:value>
{@render children?.()}
</SomaFieldInput>
</Field.Input>

@ -1,10 +1,10 @@
<script lang="ts">
import { Label as SomaFieldLabel } from '$soma/components/field';
import * as Field from '$soma/components/field';
import type { FieldLabelProps } from './types';
let { children, ...rest }: FieldLabelProps = $props();
</script>
<SomaFieldLabel {...rest}>
<Field.Label {...rest}>
{@render children?.()}
</SomaFieldLabel>
</Field.Label>

@ -1,10 +1,10 @@
<script lang="ts">
import { Prefix as SomaFieldPrefix } from '$soma/components/field';
import * as Field from '$soma/components/field';
import type { FieldPrefixProps } from './types';
let { children, ...rest }: FieldPrefixProps = $props();
</script>
<SomaFieldPrefix {...rest}>
<Field.Prefix {...rest}>
{@render children?.()}
</SomaFieldPrefix>
</Field.Prefix>

@ -1,14 +1,14 @@
<script lang="ts">
import { RequiredIndicator as SomaFieldRequiredIndicator } from '$soma/components/field';
import * as Field from '$soma/components/field';
import type { FieldRequiredIndicatorProps } from './types';
let { children, ...rest }: FieldRequiredIndicatorProps = $props();
</script>
<SomaFieldRequiredIndicator {...rest}>
<Field.RequiredIndicator {...rest}>
{#if children}
{@render children()}
{:else}
*
{/if}
</SomaFieldRequiredIndicator>
</Field.RequiredIndicator>

@ -1,10 +1,10 @@
<script lang="ts">
import { Suffix as SomaFieldSuffix } from '$soma/components/field';
import * as Field from '$soma/components/field';
import type { FieldSuffixProps } from './types';
let { children, ...rest }: FieldSuffixProps = $props();
</script>
<SomaFieldSuffix {...rest}>
<Field.Suffix {...rest}>
{@render children?.()}
</SomaFieldSuffix>
</Field.Suffix>

@ -7,6 +7,8 @@
--_field-control-font-size: var(--field-control-font-size-md);
--_field-control-border: var(--field-control-border);
--_field-control-bg: var(--field-control-bg);
--_field-accent-border: var(--color-primary-border);
--_field-accent-text: var(--color-primary-text);
display: flex;
flex-direction: column;
@ -17,6 +19,41 @@
color: var(--field-control-color);
}
[data-field][data-color='secondary'] {
--_field-accent-border: var(--color-secondary-border);
--_field-accent-text: var(--color-secondary-text);
}
[data-field][data-color='neutral'] {
--_field-accent-border: var(--color-neutral-border);
--_field-accent-text: var(--color-neutral-text);
}
[data-field][data-color='affirm'] {
--_field-accent-border: var(--color-affirm-border);
--_field-accent-text: var(--color-affirm-text);
}
[data-field][data-color='fulfill'] {
--_field-accent-border: var(--color-fulfill-border);
--_field-accent-text: var(--color-fulfill-text);
}
[data-field][data-color='risk'] {
--_field-accent-border: var(--color-risk-border);
--_field-accent-text: var(--color-risk-text);
}
[data-field][data-color='threat'] {
--_field-accent-border: var(--color-threat-border);
--_field-accent-text: var(--color-threat-text);
}
[data-field][data-color='loss'] {
--_field-accent-border: var(--color-loss-border);
--_field-accent-text: var(--color-loss-text);
}
[data-field][data-size='xs'] {
--_field-gap: var(--field-gap-xs);
--_field-label-font-size: var(--field-label-font-size-xs);
@ -49,6 +86,48 @@
--_field-control-bg: var(--field-control-ghost-bg);
}
[data-field][data-orientation='horizontal'] {
display: grid;
grid-template-columns: minmax(8rem, 0.42fr) minmax(0, 1fr);
align-items: start;
column-gap: var(--field-layout-column-gap);
row-gap: var(--_field-gap);
}
[data-field][data-orientation='horizontal'] > [data-field-label] {
min-block-size: var(--_field-control-height);
}
[data-field][data-orientation='horizontal'] > [data-field-control],
[data-field][data-orientation='horizontal'] > [data-field-helper-text],
[data-field][data-orientation='horizontal'] > [data-field-error-text] {
grid-column: 2;
}
[data-field][data-orientation='responsive'] {
/* Mobile-first; desktop switches to the same two-column layout as horizontal. */
}
@media (min-width: 48rem) {
[data-field][data-orientation='responsive'] {
display: grid;
grid-template-columns: minmax(8rem, 0.42fr) minmax(0, 1fr);
align-items: start;
column-gap: var(--field-layout-column-gap);
row-gap: var(--_field-gap);
}
[data-field][data-orientation='responsive'] > [data-field-label] {
min-block-size: var(--_field-control-height);
}
[data-field][data-orientation='responsive'] > [data-field-control],
[data-field][data-orientation='responsive'] > [data-field-helper-text],
[data-field][data-orientation='responsive'] > [data-field-error-text] {
grid-column: 2;
}
}
[data-field-label] {
display: inline-flex;
align-items: center;

@ -1,20 +1,33 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
/**
* Eidos `<Field>` — visual wrapper over Soma's form field primitive.
* Soma owns state, form registration and ARIA; Eidos adds size/variant.
* Soma owns state, form registration and ARIA; Eidos adds visual attrs.
*/
import { ActiveEidos } from '$uix/eidos';
import { Provider as SomaFieldProvider } from '$soma/components/field';
import * as Field from '$soma/components/field';
import type { FieldProps } from './types';
let { size = 'md', variant = 'outline', children: bodyContent, ...rest }: FieldProps = $props();
let {
size = 'md',
variant = 'outline',
orientation = 'vertical',
color = 'primary',
children: bodyContent,
...rest
}: FieldProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
</script>
<SomaFieldProvider {...rest} data-size={resolvedSize} data-variant={variant}>
<Field.Provider
{...rest}
data-size={resolvedSize}
data-variant={variant}
data-orientation={orientation}
data-color={color}
>
{#snippet children(snippetProps)}
{@render bodyContent?.(snippetProps)}
{/snippet}
</SomaFieldProvider>
</Field.Provider>

@ -10,7 +10,7 @@
// <Field.HelperText>Use your work email.</Field.HelperText>
// <Field.ErrorText>Enter a valid email.</Field.ErrorText>
// </Field>
import FieldRoot from './field.svelte';
import FieldComponent from './field.svelte';
import Label from './field-label.svelte';
import RequiredIndicator from './field-required-indicator.svelte';
import Control from './field-control.svelte';
@ -20,7 +20,7 @@ import ErrorText from './field-error-text.svelte';
import Prefix from './field-prefix.svelte';
import Suffix from './field-suffix.svelte';
type FieldNamespace = typeof FieldRoot & {
type FieldNamespace = typeof FieldComponent & {
Label: typeof Label;
RequiredIndicator: typeof RequiredIndicator;
Control: typeof Control;
@ -31,7 +31,7 @@ type FieldNamespace = typeof FieldRoot & {
Suffix: typeof Suffix;
};
const Field = FieldRoot as FieldNamespace;
const Field = FieldComponent as FieldNamespace;
Field.Label = Label;
Field.RequiredIndicator = RequiredIndicator;
Field.Control = Control;
@ -57,5 +57,7 @@ export type {
FieldSuffixProps as SuffixProps,
FieldProviderSnippetProps as ProviderSnippetProps,
FieldSize,
FieldVariant
FieldVariant,
FieldOrientation,
FieldColor
} from './types';

@ -1,39 +1,45 @@
import type {
ControlProps as SomaFieldControlProps,
ErrorTextProps as SomaFieldErrorTextProps,
HelperTextProps as SomaFieldHelperTextProps,
InputProps as SomaFieldInputProps,
LabelProps as SomaFieldLabelProps,
PrefixProps as SomaFieldPrefixProps,
ProviderProps as SomaFieldProviderProps,
ControlProps,
ErrorTextProps,
HelperTextProps,
InputProps,
LabelProps,
PrefixProps,
ProviderProps,
FieldSnippetProps as FieldProviderSnippetProps,
RequiredIndicatorProps as SomaFieldRequiredIndicatorProps,
SuffixProps as SomaFieldSuffixProps
RequiredIndicatorProps,
SuffixProps
} from '$soma/components/field';
import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types';
export type FieldSize = Extract<Size, 'xs' | 'sm' | 'md' | 'lg'>;
export type FieldVariant = 'outline' | 'ghost';
export type FieldVariant = Extract<ControlVariant, 'outline' | 'ghost'>;
export type FieldOrientation = 'vertical' | 'horizontal' | 'responsive';
export type FieldColor = ColorRole;
/**
* Props for the eidos `<Field>`.
*
* Soma owns state, form registration and ARIA wiring. Eidos adds visual size
* and variant only.
* Soma owns state, form registration and ARIA wiring. Eidos adds visual
* size, variant, orientation and intent color.
*/
export type FieldProps = SomaFieldProviderProps & {
export type FieldProps = ProviderProps & {
/** Visual size. Accepts a responsive map resolved by ActiveEidos. @default 'md' */
size?: ResponsiveProp<FieldSize>;
/** Visual variant for the control shell. @default 'outline' */
variant?: FieldVariant;
/** Label/control layout. @default 'vertical' */
orientation?: FieldOrientation;
/** Accent palette for focus and required indicator. @default 'primary' */
color?: FieldColor;
};
export type FieldLabelProps = SomaFieldLabelProps;
export type FieldRequiredIndicatorProps = SomaFieldRequiredIndicatorProps;
export type FieldControlProps = SomaFieldControlProps;
export type FieldInputProps = SomaFieldInputProps;
export type FieldHelperTextProps = SomaFieldHelperTextProps;
export type FieldErrorTextProps = SomaFieldErrorTextProps;
export type FieldPrefixProps = SomaFieldPrefixProps;
export type FieldSuffixProps = SomaFieldSuffixProps;
export type FieldLabelProps = LabelProps;
export type FieldRequiredIndicatorProps = RequiredIndicatorProps;
export type FieldControlProps = ControlProps;
export type FieldInputProps = InputProps;
export type FieldHelperTextProps = HelperTextProps;
export type FieldErrorTextProps = ErrorTextProps;
export type FieldPrefixProps = PrefixProps;
export type FieldSuffixProps = SuffixProps;
export type { FieldProviderSnippetProps };

@ -1,10 +1,10 @@
<script lang="ts">
import { ClearTrigger as SomaFileUploadClearTrigger } from '$soma/components/file-upload';
import * as FileUpload from '$soma/components/file-upload';
import type { FileUploadClearTriggerProps } from './types';
let { children, ...rest }: FileUploadClearTriggerProps = $props();
</script>
<SomaFileUploadClearTrigger {...rest}>
<FileUpload.ClearTrigger {...rest}>
{@render children?.()}
</SomaFileUploadClearTrigger>
</FileUpload.ClearTrigger>

@ -1,10 +1,10 @@
<script lang="ts">
import { Dropzone as SomaFileUploadDropzone } from '$soma/components/file-upload';
import * as FileUpload from '$soma/components/file-upload';
import type { FileUploadDropzoneProps } from './types';
let { children, ...rest }: FileUploadDropzoneProps = $props();
</script>
<SomaFileUploadDropzone {...rest}>
<FileUpload.Dropzone {...rest}>
{@render children?.()}
</SomaFileUploadDropzone>
</FileUpload.Dropzone>

@ -1,10 +1,10 @@
<script lang="ts">
import { FileList as SomaFileUploadFileList } from '$soma/components/file-upload';
import * as FileUpload from '$soma/components/file-upload';
import type { FileUploadFileListProps } from './types';
let { children, ...rest }: FileUploadFileListProps = $props();
</script>
<SomaFileUploadFileList {...rest}>
<FileUpload.FileList {...rest}>
{@render children?.()}
</SomaFileUploadFileList>
</FileUpload.FileList>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save

Powered by TurnKey Linux.