Add Eidos breadcrumb wrapper

active-uix
dev 5 months ago
parent e359cabd4b
commit 458996882e

@ -21,11 +21,11 @@ Fecha de corte: 2026-05-17. Rama: `active-uix`.
- `rating-group`
- `search-field`
- `number-field`
- `breadcrumb`
- Siguiente decision antes de seguir migrando:
- Continuar con componentes de riesgo bajo/medio (`field`, `toolbar`,
`breadcrumb`, `tag-group`) o parar a definir una tabla de migracion por
componente para los grandes (`select`, `combobox`, `calendar`,
`date-field`).
`tag-group`) o parar a definir una tabla de migracion por componente para
los grandes (`select`, `combobox`, `calendar`, `date-field`).
- Para cada componente nuevo conviene escribir primero: partes Soma,
props visuales Eidos, `data-*` que estampa el wrapper, tokens de recipe y
attrs morfo-backed que consumira el CSS.
@ -122,9 +122,22 @@ Actualizacion Eidos component migration 2026-05-17:
- `npm run check` -> 0 errores, 0 warnings.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> 0 invalid, sin drift
hotspots.
- Septima tanda del mismo bloque:
- `Breadcrumb`: root visual `breadcrumb.svelte`, partes `List`, `Item`,
`Link`, `Separator`, `Ellipsis`, `breadcrumb.css`, `types.ts` e `index.ts`.
- El wrapper conserva semantica, current item/link, ellipsis interactivo y
ARIA de Soma; Eidos solo anade `size`.
- La receta consume `data-current` y `data-interactive` declarados por Morfo
y define tokens para gaps, tipografia, links, separador, ellipsis, foco y
transiciones.
- Validado despues de `Breadcrumb`:
- `npx vitest run src/uix/eidos` -> 7 archivos, 84 tests OK.
- `npm run check` -> 0 errores, 0 warnings.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> 0 invalid, sin drift
hotspots.
- Siguiente bloque sugerido:
- Migrar otro grupo de bajo riesgo desde Soma a Eidos: candidatos reales por
morfo/soma existentes son `field`, `toolbar`, `breadcrumb` o `tag-group`.
morfo/soma existentes son `field`, `toolbar` o `tag-group`.
- Antes de tocar componentes grandes (`select`, `combobox`, `calendar`,
`date-field`) conviene fijar una tabla por componente: partes Soma,
props visuales Eidos, recipe tokens y attrs esperados.

@ -162,8 +162,8 @@ leer CSS.
La migración de componentes Soma -> Eidos se retoma por tandas pequeñas. A fecha
2026-05-17, además de los pilotos previos, ya están migrados `meter`,
`progress`, `slider`, `pagination`, `rating-group`, `search-field` y
`number-field`; cada uno envuelve partes públicas de Soma y añade únicamente
superficie visual.
`number-field`, y `breadcrumb`; cada uno envuelve partes públicas de Soma y
añade únicamente superficie visual.
Eidos lee del DOM lo que las otras capas escriben (parts, data-attrs, ARIA,
event signals) — nunca importa internals de soma ni de sema.

@ -881,7 +881,7 @@ Para evitar mission creep, conviene fijar lo que UIX **no quiere ser**:
La tanda de wrappers migrados desde Soma queda documentada en
`eidos/components/README.md`; a fecha 2026-05-17 incluye primitives de estado
y formulario como `progress`, `meter`, `slider`, `pagination`,
`rating-group`, `search-field` y `number-field`.
`rating-group`, `search-field`, `number-field` y `breadcrumb`.
- `adom` — `dom.apply(change)`, `dom.remove(target, names)`. Toda la
superficie reactiva (viewport, breakpoints, listeners, observers,
BodyScrollLock, DOMContext, RovingFocusGroup) ya estable.

@ -695,8 +695,8 @@ podrá retirarse.
`toggle`, `switch`, `collapsible`, `dialog`, `drawer`, `popover`, `toast`,
`accordion`, `avatar`, `tooltip`, `tabs`, `checkbox`, `radio-group`,
`meter`, `progress`, `slider`, `pagination`, `rating-group`,
`search-field` y `number-field`. Todos usan root visual + partes attached,
sin `Provider` público ni API flat.
`search-field`, `number-field` y `breadcrumb`. Todos usan root visual +
partes attached, sin `Provider` público ni API flat.
- **Color**: basado en roles canonicos de jerarquia e intents
(`primary`, `secondary`, `tertiary`, `neutral`, `affirm`, `fulfill`,
`risk`, `threat`, `loss`) y escalas de 12 pasos. Por cada escala genera

@ -10,6 +10,7 @@ const VISUAL_ATTRS = [
['accordion/accordion-header.svelte', ['data-size']],
['accordion/accordion-trigger.svelte', ['data-size', 'data-indicator-placement']],
['avatar/avatar.svelte', ['data-size', 'data-variant', 'data-color']],
['breadcrumb/breadcrumb.svelte', ['data-size']],
['checkbox/checkbox.svelte', ['data-size', 'data-variant', 'data-color']],
['checkbox/checkbox-group.svelte', ['data-columns']],
['dialog/dialog-close.svelte', ['data-position', 'data-icon-only']],

@ -358,6 +358,7 @@ estado, ARIA, traducciones y escritura headless en Soma/Morfo.
| toast | ✅ multi-part | + Toaster separate |
| accordion | ✅ multi-part | Item, Header, Trigger, Content |
| avatar | ✅ multi-part | Image, Fallback (eidos-native) |
| breadcrumb | ✅ multi-part | List, Item, Link, Separator, Ellipsis |
| icon | ✅ single-part | + 1697 lucide glyphs |
| meter | ✅ multi-part | Indicator |
| number-field | ✅ multi-part | Input, IncrementTrigger, DecrementTrigger, Scrubber |
@ -374,7 +375,7 @@ estado, ARIA, traducciones y escritura headless en Soma/Morfo.
### Orden recomendado para continuar
1. Componentes simples de composición/formulario: `field`, `toolbar`,
`breadcrumb`, `tag-group`.
`tag-group`.
2. Componentes medianos con más partes o layout interno: `file-upload`,
`tags-input`, `editable`, `stepper`.
3. Componentes grandes sólo con tabla previa de migración: `select`,

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

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

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

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

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

@ -0,0 +1,110 @@
[data-breadcrumb] {
--_breadcrumb-gap: var(--breadcrumb-gap-md);
--_breadcrumb-font-size: var(--breadcrumb-font-size-md);
color: var(--breadcrumb-color);
font-family: var(--breadcrumb-font-family);
font-size: var(--_breadcrumb-font-size);
line-height: var(--breadcrumb-line-height);
}
[data-breadcrumb][data-size='sm'] {
--_breadcrumb-gap: var(--breadcrumb-gap-sm);
--_breadcrumb-font-size: var(--breadcrumb-font-size-sm);
}
[data-breadcrumb][data-size='lg'] {
--_breadcrumb-gap: var(--breadcrumb-gap-lg);
--_breadcrumb-font-size: var(--breadcrumb-font-size-lg);
}
[data-breadcrumb-list] {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--_breadcrumb-gap);
margin: 0;
padding: 0;
list-style: none;
}
[data-breadcrumb-item],
[data-breadcrumb-separator] {
display: inline-flex;
align-items: center;
min-inline-size: 0;
}
[data-breadcrumb-link] {
display: inline-flex;
align-items: center;
min-inline-size: 0;
border-radius: var(--breadcrumb-link-radius);
color: var(--breadcrumb-link-color);
font-weight: var(--breadcrumb-link-font-weight);
text-decoration: none;
outline: none;
transition:
color var(--breadcrumb-transition-duration) var(--breadcrumb-transition-ease),
box-shadow var(--breadcrumb-transition-duration) var(--breadcrumb-transition-ease);
}
[data-breadcrumb-link]:hover:not([data-current]) {
color: var(--breadcrumb-link-color-hover);
}
[data-breadcrumb-link]:focus-visible {
box-shadow: var(--breadcrumb-focus-shadow);
}
[data-breadcrumb-link][data-current] {
color: var(--breadcrumb-link-color-current);
font-weight: var(--breadcrumb-link-font-weight-current);
cursor: default;
}
[data-breadcrumb-separator] {
color: var(--breadcrumb-separator-color);
user-select: none;
}
[data-breadcrumb-ellipsis] {
display: inline-flex;
align-items: center;
justify-content: center;
min-inline-size: var(--breadcrumb-ellipsis-size);
block-size: var(--breadcrumb-ellipsis-size);
border: var(--breadcrumb-ellipsis-border-width) solid var(--breadcrumb-ellipsis-border);
border-radius: var(--breadcrumb-ellipsis-radius);
background: var(--breadcrumb-ellipsis-bg);
color: var(--breadcrumb-ellipsis-color);
font: inherit;
outline: none;
user-select: none;
}
[data-breadcrumb-ellipsis][data-interactive] {
cursor: pointer;
transition:
background var(--breadcrumb-transition-duration) var(--breadcrumb-transition-ease),
border-color var(--breadcrumb-transition-duration) var(--breadcrumb-transition-ease),
color var(--breadcrumb-transition-duration) var(--breadcrumb-transition-ease),
box-shadow var(--breadcrumb-transition-duration) var(--breadcrumb-transition-ease);
}
[data-breadcrumb-ellipsis][data-interactive]:hover {
border-color: var(--breadcrumb-ellipsis-border-hover);
background: var(--breadcrumb-ellipsis-bg-hover);
color: var(--breadcrumb-ellipsis-color-hover);
}
[data-breadcrumb-ellipsis][data-interactive]:focus-visible {
box-shadow: var(--breadcrumb-focus-shadow);
}
@media (prefers-reduced-motion: reduce) {
[data-breadcrumb-link],
[data-breadcrumb-ellipsis][data-interactive] {
transition: none;
}
}

@ -0,0 +1,18 @@
<script lang="ts">
/**
* 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 type { BreadcrumbProps } from './types';
let { size = 'md', children, ...rest }: BreadcrumbProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
</script>
<SomaBreadcrumbProvider {...rest} data-size={resolvedSize}>
{@render children?.()}
</SomaBreadcrumbProvider>

@ -0,0 +1,47 @@
// Breadcrumb — eidos compound API, disciplined option C.
//
// import { Breadcrumb } from '$uix/eidos/components/breadcrumb';
//
// <Breadcrumb>
// <Breadcrumb.List>
// <Breadcrumb.Item><Breadcrumb.Link href="/">Home</Breadcrumb.Link></Breadcrumb.Item>
// <Breadcrumb.Separator />
// <Breadcrumb.Item><Breadcrumb.Link current>Docs</Breadcrumb.Link></Breadcrumb.Item>
// </Breadcrumb.List>
// </Breadcrumb>
import BreadcrumbRoot 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 & {
List: typeof List;
Item: typeof Item;
Link: typeof Link;
Separator: typeof Separator;
Ellipsis: typeof Ellipsis;
};
const Breadcrumb = BreadcrumbRoot as BreadcrumbNamespace;
Breadcrumb.List = List;
Breadcrumb.Item = Item;
Breadcrumb.Link = Link;
Breadcrumb.Separator = Separator;
Breadcrumb.Ellipsis = Ellipsis;
export { Breadcrumb };
export default Breadcrumb;
export type {
BreadcrumbCurrent,
BreadcrumbProps,
BreadcrumbListProps as ListProps,
BreadcrumbItemProps as ItemProps,
BreadcrumbLinkProps as LinkProps,
BreadcrumbSeparatorProps as SeparatorProps,
BreadcrumbEllipsisProps as EllipsisProps,
BreadcrumbSize
} from './types';

@ -0,0 +1,30 @@
import type {
BreadcrumbCurrent,
EllipsisProps as SomaBreadcrumbEllipsisProps,
ItemProps as SomaBreadcrumbItemProps,
LinkProps as SomaBreadcrumbLinkProps,
ListProps as SomaBreadcrumbListProps,
ProviderProps as SomaBreadcrumbProviderProps,
SeparatorProps as SomaBreadcrumbSeparatorProps
} from '$soma/components/breadcrumb';
import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
export type BreadcrumbSize = Extract<Size, 'sm' | 'md' | 'lg'>;
/**
* Props for the eidos `<Breadcrumb>`.
*
* 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 & {
/** 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 { BreadcrumbCurrent };

@ -415,6 +415,34 @@
--avatar-color-loss-soft-fg: var(--color-loss-text);
--avatar-color-loss-outline-border: var(--color-loss-border);
--avatar-color-loss-outline-fg: var(--color-loss-text);
--breadcrumb-gap-sm: var(--space-1);
--breadcrumb-gap-md: var(--space-1-5);
--breadcrumb-gap-lg: var(--space-2);
--breadcrumb-font-family: var(--font-ui);
--breadcrumb-font-size-sm: var(--font-size-xs);
--breadcrumb-font-size-md: var(--font-size-sm);
--breadcrumb-font-size-lg: var(--font-size-sm);
--breadcrumb-line-height: var(--leading-ui);
--breadcrumb-color: var(--color-content-secondary);
--breadcrumb-link-radius: var(--radius-sm);
--breadcrumb-link-color: var(--color-content-secondary);
--breadcrumb-link-color-hover: var(--color-content-primary);
--breadcrumb-link-color-current: var(--color-content-primary);
--breadcrumb-link-font-weight: var(--font-weight-medium);
--breadcrumb-link-font-weight-current: var(--font-weight-semibold);
--breadcrumb-separator-color: var(--color-content-muted);
--breadcrumb-ellipsis-size: var(--control-height-xs);
--breadcrumb-ellipsis-radius: var(--radius-md);
--breadcrumb-ellipsis-border-width: var(--border-width);
--breadcrumb-ellipsis-border: transparent;
--breadcrumb-ellipsis-border-hover: var(--color-border-subtle);
--breadcrumb-ellipsis-bg: transparent;
--breadcrumb-ellipsis-bg-hover: var(--color-surface-raised);
--breadcrumb-ellipsis-color: var(--color-content-muted);
--breadcrumb-ellipsis-color-hover: var(--color-content-primary);
--breadcrumb-focus-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);
--breadcrumb-transition-duration: var(--duration-fast);
--breadcrumb-transition-ease: var(--ease-default);
--checkbox-size-sm-box-size: 16px;
--checkbox-size-sm-indicator-size: 10px;
--checkbox-size-sm-indicator-stroke: 2px;

@ -62,6 +62,7 @@
/* ── Component recipes ──────────────────────────────────────────────────── */
@import './components/icon/icon.css';
@import './components/avatar/avatar.css';
@import './components/breadcrumb/breadcrumb.css';
@import './components/toast/toast.css';
@import './components/dialog/dialog.css';
@import './components/drawer/drawer.css';

@ -142,6 +142,37 @@ export const THEME_BASE_RECIPE_TOKENS = {
'color-loss-outline-border': 'var(--color-loss-border)',
'color-loss-outline-fg': 'var(--color-loss-text)'
},
breadcrumb: {
'gap-sm': 'var(--space-1)',
'gap-md': 'var(--space-1-5)',
'gap-lg': 'var(--space-2)',
'font-family': 'var(--font-ui)',
'font-size-sm': 'var(--font-size-xs)',
'font-size-md': 'var(--font-size-sm)',
'font-size-lg': 'var(--font-size-sm)',
'line-height': 'var(--leading-ui)',
color: 'var(--color-content-secondary)',
'link-radius': 'var(--radius-sm)',
'link-color': 'var(--color-content-secondary)',
'link-color-hover': 'var(--color-content-primary)',
'link-color-current': 'var(--color-content-primary)',
'link-font-weight': 'var(--font-weight-medium)',
'link-font-weight-current': 'var(--font-weight-semibold)',
'separator-color': 'var(--color-content-muted)',
'ellipsis-size': 'var(--control-height-xs)',
'ellipsis-radius': 'var(--radius-md)',
'ellipsis-border-width': 'var(--border-width)',
'ellipsis-border': 'transparent',
'ellipsis-border-hover': 'var(--color-border-subtle)',
'ellipsis-bg': 'transparent',
'ellipsis-bg-hover': 'var(--color-surface-raised)',
'ellipsis-color': 'var(--color-content-muted)',
'ellipsis-color-hover': 'var(--color-content-primary)',
'focus-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)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)'
},
checkbox: {
'size-sm-box-size': '16px',
'size-sm-indicator-size': '10px',

Loading…
Cancel
Save

Powered by TurnKey Linux.