From 458996882e834ee850344ee24901a839f5b47e5e Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 17 May 2026 17:02:13 +0200 Subject: [PATCH] Add Eidos breadcrumb wrapper --- continue.md | 21 +++- src/uix/README.md | 4 +- src/uix/active_architecture.md | 2 +- src/uix/eidos/README.md | 4 +- src/uix/eidos/component-visual-attrs.test.ts | 1 + src/uix/eidos/components/README.md | 3 +- .../breadcrumb/breadcrumb-ellipsis.svelte | 10 ++ .../breadcrumb/breadcrumb-item.svelte | 10 ++ .../breadcrumb/breadcrumb-link.svelte | 10 ++ .../breadcrumb/breadcrumb-list.svelte | 10 ++ .../breadcrumb/breadcrumb-separator.svelte | 10 ++ .../components/breadcrumb/breadcrumb.css | 110 ++++++++++++++++++ .../components/breadcrumb/breadcrumb.svelte | 18 +++ src/uix/eidos/components/breadcrumb/index.ts | 47 ++++++++ src/uix/eidos/components/breadcrumb/types.ts | 30 +++++ src/uix/eidos/generated/base.css | 28 +++++ src/uix/eidos/index.css | 1 + src/uix/eidos/lib/recipes/base.ts | 31 +++++ 18 files changed, 340 insertions(+), 10 deletions(-) create mode 100644 src/uix/eidos/components/breadcrumb/breadcrumb-ellipsis.svelte create mode 100644 src/uix/eidos/components/breadcrumb/breadcrumb-item.svelte create mode 100644 src/uix/eidos/components/breadcrumb/breadcrumb-link.svelte create mode 100644 src/uix/eidos/components/breadcrumb/breadcrumb-list.svelte create mode 100644 src/uix/eidos/components/breadcrumb/breadcrumb-separator.svelte create mode 100644 src/uix/eidos/components/breadcrumb/breadcrumb.css create mode 100644 src/uix/eidos/components/breadcrumb/breadcrumb.svelte create mode 100644 src/uix/eidos/components/breadcrumb/index.ts create mode 100644 src/uix/eidos/components/breadcrumb/types.ts diff --git a/continue.md b/continue.md index 852193934..18b56fce0 100644 --- a/continue.md +++ b/continue.md @@ -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. diff --git a/src/uix/README.md b/src/uix/README.md index 6541fbd9c..bc40fc17f 100644 --- a/src/uix/README.md +++ b/src/uix/README.md @@ -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. diff --git a/src/uix/active_architecture.md b/src/uix/active_architecture.md index d01925fd2..d7f1c938b 100644 --- a/src/uix/active_architecture.md +++ b/src/uix/active_architecture.md @@ -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. diff --git a/src/uix/eidos/README.md b/src/uix/eidos/README.md index 5d41233c8..83d3948f4 100644 --- a/src/uix/eidos/README.md +++ b/src/uix/eidos/README.md @@ -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 diff --git a/src/uix/eidos/component-visual-attrs.test.ts b/src/uix/eidos/component-visual-attrs.test.ts index 7605aae4e..8bf300887 100644 --- a/src/uix/eidos/component-visual-attrs.test.ts +++ b/src/uix/eidos/component-visual-attrs.test.ts @@ -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']], diff --git a/src/uix/eidos/components/README.md b/src/uix/eidos/components/README.md index 3d66ab447..81b097f44 100644 --- a/src/uix/eidos/components/README.md +++ b/src/uix/eidos/components/README.md @@ -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`, diff --git a/src/uix/eidos/components/breadcrumb/breadcrumb-ellipsis.svelte b/src/uix/eidos/components/breadcrumb/breadcrumb-ellipsis.svelte new file mode 100644 index 000000000..f2b319ddc --- /dev/null +++ b/src/uix/eidos/components/breadcrumb/breadcrumb-ellipsis.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/breadcrumb/breadcrumb-item.svelte b/src/uix/eidos/components/breadcrumb/breadcrumb-item.svelte new file mode 100644 index 000000000..fded90597 --- /dev/null +++ b/src/uix/eidos/components/breadcrumb/breadcrumb-item.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/breadcrumb/breadcrumb-link.svelte b/src/uix/eidos/components/breadcrumb/breadcrumb-link.svelte new file mode 100644 index 000000000..25a9ba772 --- /dev/null +++ b/src/uix/eidos/components/breadcrumb/breadcrumb-link.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/breadcrumb/breadcrumb-list.svelte b/src/uix/eidos/components/breadcrumb/breadcrumb-list.svelte new file mode 100644 index 000000000..8f34633a6 --- /dev/null +++ b/src/uix/eidos/components/breadcrumb/breadcrumb-list.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/breadcrumb/breadcrumb-separator.svelte b/src/uix/eidos/components/breadcrumb/breadcrumb-separator.svelte new file mode 100644 index 000000000..8c55574cb --- /dev/null +++ b/src/uix/eidos/components/breadcrumb/breadcrumb-separator.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/breadcrumb/breadcrumb.css b/src/uix/eidos/components/breadcrumb/breadcrumb.css new file mode 100644 index 000000000..6425c7ba4 --- /dev/null +++ b/src/uix/eidos/components/breadcrumb/breadcrumb.css @@ -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; + } +} diff --git a/src/uix/eidos/components/breadcrumb/breadcrumb.svelte b/src/uix/eidos/components/breadcrumb/breadcrumb.svelte new file mode 100644 index 000000000..5a27e0258 --- /dev/null +++ b/src/uix/eidos/components/breadcrumb/breadcrumb.svelte @@ -0,0 +1,18 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/breadcrumb/index.ts b/src/uix/eidos/components/breadcrumb/index.ts new file mode 100644 index 000000000..fde5fae4d --- /dev/null +++ b/src/uix/eidos/components/breadcrumb/index.ts @@ -0,0 +1,47 @@ +// Breadcrumb — eidos compound API, disciplined option C. +// +// import { Breadcrumb } from '$uix/eidos/components/breadcrumb'; +// +// +// +// Home +// +// Docs +// +// +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'; diff --git a/src/uix/eidos/components/breadcrumb/types.ts b/src/uix/eidos/components/breadcrumb/types.ts new file mode 100644 index 000000000..12a148427 --- /dev/null +++ b/src/uix/eidos/components/breadcrumb/types.ts @@ -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; + +/** + * Props for the eidos ``. + * + * 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; +}; + +export type BreadcrumbListProps = SomaBreadcrumbListProps; +export type BreadcrumbItemProps = SomaBreadcrumbItemProps; +export type BreadcrumbLinkProps = SomaBreadcrumbLinkProps; +export type BreadcrumbSeparatorProps = SomaBreadcrumbSeparatorProps; +export type BreadcrumbEllipsisProps = SomaBreadcrumbEllipsisProps; +export type { BreadcrumbCurrent }; diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 8d49d9fab..4390dc8df 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -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; diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css index 49fb518ac..ebe769663 100644 --- a/src/uix/eidos/index.css +++ b/src/uix/eidos/index.css @@ -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'; diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 8880a8c6e..311157d3c 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -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',