From f62b32677c6088cae4aa024d7af285f1cfcbd061 Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 17 May 2026 02:04:37 +0200 Subject: [PATCH] Add Eidos pagination wrapper --- continue.md | 14 ++- src/uix/eidos/component-visual-attrs.test.ts | 1 + src/uix/eidos/components/README.md | 1 + src/uix/eidos/components/pagination/index.ts | 45 ++++++++ .../pagination/pagination-ellipsis.svelte | 10 ++ .../pagination/pagination-item.svelte | 10 ++ .../pagination/pagination-next-trigger.svelte | 10 ++ .../pagination/pagination-prev-trigger.svelte | 10 ++ .../components/pagination/pagination.css | 105 ++++++++++++++++++ .../components/pagination/pagination.svelte | 20 ++++ src/uix/eidos/components/pagination/types.ts | 30 +++++ src/uix/eidos/generated/base.css | 32 ++++++ src/uix/eidos/index.css | 1 + src/uix/eidos/lib/recipes/base.ts | 35 ++++++ 14 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 src/uix/eidos/components/pagination/index.ts create mode 100644 src/uix/eidos/components/pagination/pagination-ellipsis.svelte create mode 100644 src/uix/eidos/components/pagination/pagination-item.svelte create mode 100644 src/uix/eidos/components/pagination/pagination-next-trigger.svelte create mode 100644 src/uix/eidos/components/pagination/pagination-prev-trigger.svelte create mode 100644 src/uix/eidos/components/pagination/pagination.css create mode 100644 src/uix/eidos/components/pagination/pagination.svelte create mode 100644 src/uix/eidos/components/pagination/types.ts diff --git a/continue.md b/continue.md index fccbfa746..4e46f2e2b 100644 --- a/continue.md +++ b/continue.md @@ -46,9 +46,21 @@ 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. +- Tercera tanda del mismo bloque: + - `Pagination`: root visual `pagination.svelte`, partes `PrevTrigger`, + `NextTrigger`, `Item`, `Ellipsis`, `pagination.css`, `types.ts` e + `index.ts`. + - El root conserva el snippet de Soma con `{ pages, totalPages }` y solo + anade `size` visual resuelto por `ActiveEidos`. + - La receta cubre controles, estado selected, disabled, ellipsis y focus. +- Validado despues de `Pagination`: + - `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 `pagination`, `rating-group` o `number-field`. + morfo/soma existentes son `rating-group` o `number-field`. - 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/eidos/component-visual-attrs.test.ts b/src/uix/eidos/component-visual-attrs.test.ts index 2d953376d..68a82e093 100644 --- a/src/uix/eidos/component-visual-attrs.test.ts +++ b/src/uix/eidos/component-visual-attrs.test.ts @@ -17,6 +17,7 @@ const VISUAL_ATTRS = [ ['drawer/drawer-close.svelte', ['data-icon-only']], ['drawer/drawer-content.svelte', ['data-size']], ['meter/meter.svelte', ['data-size']], + ['pagination/pagination.svelte', ['data-size']], ['popover/popover-close.svelte', ['data-icon-only']], ['popover/popover-content.svelte', ['data-size']], ['progress/progress.svelte', ['data-size']], diff --git a/src/uix/eidos/components/README.md b/src/uix/eidos/components/README.md index 91aae5446..81f5a40b8 100644 --- a/src/uix/eidos/components/README.md +++ b/src/uix/eidos/components/README.md @@ -355,6 +355,7 @@ su subdirectorio. | avatar | ✅ multi-part | Image, Fallback (eidos-native) | | icon | ✅ single-part | + 1697 lucide glyphs | | meter | ✅ multi-part | Indicator | +| pagination | ✅ multi-part | PrevTrigger, NextTrigger, Item, Ellipsis | | progress | ✅ multi-part | Indicator | | slider | ✅ multi-part | Range, Thumb, Tick | | tooltip | ✅ multi-part | + Group | diff --git a/src/uix/eidos/components/pagination/index.ts b/src/uix/eidos/components/pagination/index.ts new file mode 100644 index 000000000..f4f905707 --- /dev/null +++ b/src/uix/eidos/components/pagination/index.ts @@ -0,0 +1,45 @@ +// Pagination — eidos compound API, disciplined option C. +// +// import { Pagination } from '$uix/eidos/components/pagination'; +// +// +// Previous +// {#each pages as page} +// ... +// {/each} +// Next +// +import PaginationRoot from './pagination.svelte'; +import PrevTrigger from './pagination-prev-trigger.svelte'; +import NextTrigger from './pagination-next-trigger.svelte'; +import Item from './pagination-item.svelte'; +import Ellipsis from './pagination-ellipsis.svelte'; + +type PaginationNamespace = typeof PaginationRoot & { + PrevTrigger: typeof PrevTrigger; + NextTrigger: typeof NextTrigger; + Item: typeof Item; + Ellipsis: typeof Ellipsis; +}; + +const Pagination = PaginationRoot as PaginationNamespace; +Pagination.PrevTrigger = PrevTrigger; +Pagination.NextTrigger = NextTrigger; +Pagination.Item = Item; +Pagination.Ellipsis = Ellipsis; + +export { Pagination }; + +export default Pagination; + +export type { + EllipsisItem, + PageItem, + PageRange, + PaginationProps, + PaginationPrevTriggerProps as PrevTriggerProps, + PaginationNextTriggerProps as NextTriggerProps, + PaginationItemProps as ItemProps, + PaginationEllipsisProps as EllipsisProps, + PaginationSize +} from './types'; diff --git a/src/uix/eidos/components/pagination/pagination-ellipsis.svelte b/src/uix/eidos/components/pagination/pagination-ellipsis.svelte new file mode 100644 index 000000000..63cfe09e6 --- /dev/null +++ b/src/uix/eidos/components/pagination/pagination-ellipsis.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/pagination/pagination-item.svelte b/src/uix/eidos/components/pagination/pagination-item.svelte new file mode 100644 index 000000000..65c5cf526 --- /dev/null +++ b/src/uix/eidos/components/pagination/pagination-item.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/pagination/pagination-next-trigger.svelte b/src/uix/eidos/components/pagination/pagination-next-trigger.svelte new file mode 100644 index 000000000..fa6b958e6 --- /dev/null +++ b/src/uix/eidos/components/pagination/pagination-next-trigger.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/pagination/pagination-prev-trigger.svelte b/src/uix/eidos/components/pagination/pagination-prev-trigger.svelte new file mode 100644 index 000000000..57bca3528 --- /dev/null +++ b/src/uix/eidos/components/pagination/pagination-prev-trigger.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/pagination/pagination.css b/src/uix/eidos/components/pagination/pagination.css new file mode 100644 index 000000000..e023ee538 --- /dev/null +++ b/src/uix/eidos/components/pagination/pagination.css @@ -0,0 +1,105 @@ +[data-pagination] { + --_pagination-control-size: var(--pagination-control-size-md); + --_pagination-control-px: var(--pagination-control-px-md); + --_pagination-font-size: var(--pagination-font-size-md); + --_pagination-gap: var(--pagination-gap-md); + + display: flex; + align-items: center; + gap: var(--_pagination-gap); + font-family: var(--pagination-font-family); + font-size: var(--_pagination-font-size); + line-height: var(--pagination-line-height); +} + +[data-pagination][data-size='sm'] { + --_pagination-control-size: var(--pagination-control-size-sm); + --_pagination-control-px: var(--pagination-control-px-sm); + --_pagination-font-size: var(--pagination-font-size-sm); + --_pagination-gap: var(--pagination-gap-sm); +} + +[data-pagination][data-size='lg'] { + --_pagination-control-size: var(--pagination-control-size-lg); + --_pagination-control-px: var(--pagination-control-px-lg); + --_pagination-font-size: var(--pagination-font-size-lg); + --_pagination-gap: var(--pagination-gap-lg); +} + +[data-pagination][data-disabled] { + opacity: var(--pagination-disabled-opacity); +} + +[data-pagination-prev-trigger], +[data-pagination-next-trigger], +[data-pagination-item] { + display: inline-flex; + align-items: center; + justify-content: center; + min-inline-size: var(--_pagination-control-size); + block-size: var(--_pagination-control-size); + padding-inline: var(--_pagination-control-px); + border: var(--pagination-control-border-width) solid var(--pagination-control-border); + border-radius: var(--pagination-control-radius); + background: var(--pagination-control-bg); + color: var(--pagination-control-color); + font: inherit; + cursor: pointer; + outline: none; + transition: + background var(--pagination-transition-duration) var(--pagination-transition-ease), + border-color var(--pagination-transition-duration) var(--pagination-transition-ease), + color var(--pagination-transition-duration) var(--pagination-transition-ease), + box-shadow var(--pagination-transition-duration) var(--pagination-transition-ease); +} + +[data-pagination-prev-trigger]:hover:not([data-disabled]), +[data-pagination-next-trigger]:hover:not([data-disabled]), +[data-pagination-item]:hover:not([data-disabled]) { + border-color: var(--pagination-control-border-hover); + background: var(--pagination-control-bg-hover); + color: var(--pagination-control-color-hover); +} + +[data-pagination-prev-trigger]:focus-visible, +[data-pagination-next-trigger]:focus-visible, +[data-pagination-item]:focus-visible { + box-shadow: var(--pagination-focus-shadow); +} + +[data-pagination-prev-trigger][data-disabled], +[data-pagination-next-trigger][data-disabled], +[data-pagination-item][data-disabled] { + cursor: not-allowed; + opacity: var(--pagination-disabled-opacity); +} + +[data-pagination-item][data-selected] { + border-color: var(--pagination-selected-border); + background: var(--pagination-selected-bg); + color: var(--pagination-selected-color); +} + +[data-pagination-item][data-selected]:hover { + border-color: var(--pagination-selected-border-hover); + background: var(--pagination-selected-bg-hover); + color: var(--pagination-selected-color); +} + +[data-pagination-ellipsis] { + display: inline-flex; + align-items: center; + justify-content: center; + min-inline-size: var(--_pagination-control-size); + block-size: var(--_pagination-control-size); + color: var(--pagination-ellipsis-color); + user-select: none; +} + +@media (prefers-reduced-motion: reduce) { + [data-pagination-prev-trigger], + [data-pagination-next-trigger], + [data-pagination-item] { + transition: none; + } +} diff --git a/src/uix/eidos/components/pagination/pagination.svelte b/src/uix/eidos/components/pagination/pagination.svelte new file mode 100644 index 000000000..c463b61c7 --- /dev/null +++ b/src/uix/eidos/components/pagination/pagination.svelte @@ -0,0 +1,20 @@ + + + + {#snippet children(snippetProps)} + {@render children?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/pagination/types.ts b/src/uix/eidos/components/pagination/types.ts new file mode 100644 index 000000000..b2e596297 --- /dev/null +++ b/src/uix/eidos/components/pagination/types.ts @@ -0,0 +1,30 @@ +import type { + EllipsisItem, + EllipsisProps as SomaPaginationEllipsisProps, + ItemProps as SomaPaginationItemProps, + NextTriggerProps as SomaPaginationNextTriggerProps, + PageItem, + PageRange, + PrevTriggerProps as SomaPaginationPrevTriggerProps, + ProviderProps as SomaPaginationProviderProps +} from '$soma/components/pagination'; +import type { ResponsiveProp, Size } from '$uix/eidos/lib/types'; + +export type PaginationSize = Extract; + +/** + * Props for the eidos ``. + * + * Extends Soma's pagination with visual sizing only. Page calculation, + * selected state and disabled state are owned by Soma. + */ +export type PaginationProps = SomaPaginationProviderProps & { + /** Visual size. Accepts a responsive map resolved by ActiveEidos. @default 'md' */ + size?: ResponsiveProp; +}; + +export type PaginationPrevTriggerProps = SomaPaginationPrevTriggerProps; +export type PaginationNextTriggerProps = SomaPaginationNextTriggerProps; +export type PaginationItemProps = SomaPaginationItemProps; +export type PaginationEllipsisProps = SomaPaginationEllipsisProps; +export type { EllipsisItem, PageItem, PageRange }; diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 0e01ce8ad..2067ecf13 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -602,6 +602,38 @@ --meter-indicator-bg-above: var(--color-threat-solid); --meter-transition-duration: var(--duration-normal); --meter-transition-ease: var(--ease-default); + --pagination-gap-sm: var(--space-1); + --pagination-gap-md: var(--space-1-5); + --pagination-gap-lg: var(--space-2); + --pagination-control-size-sm: var(--control-height-xs); + --pagination-control-size-md: var(--control-height-sm); + --pagination-control-size-lg: var(--control-height-md); + --pagination-control-px-sm: var(--space-2); + --pagination-control-px-md: var(--space-2-5); + --pagination-control-px-lg: var(--space-3); + --pagination-font-family: var(--font-ui); + --pagination-font-size-sm: var(--font-size-xs); + --pagination-font-size-md: var(--font-size-sm); + --pagination-font-size-lg: var(--font-size-sm); + --pagination-line-height: var(--leading-ui); + --pagination-control-radius: var(--radius-md); + --pagination-control-border-width: var(--border-width); + --pagination-control-border: transparent; + --pagination-control-border-hover: var(--color-border-subtle); + --pagination-control-bg: transparent; + --pagination-control-bg-hover: var(--color-surface-raised); + --pagination-control-color: var(--color-content-secondary); + --pagination-control-color-hover: var(--color-content-primary); + --pagination-selected-border: var(--color-primary-border); + --pagination-selected-border-hover: var(--color-primary-border); + --pagination-selected-bg: var(--color-primary-solid); + --pagination-selected-bg-hover: var(--color-primary-solid-hover); + --pagination-selected-color: var(--color-content-on-solid); + --pagination-ellipsis-color: var(--color-content-muted); + --pagination-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); + --pagination-transition-duration: var(--duration-fast); + --pagination-transition-ease: var(--ease-default); + --pagination-disabled-opacity: 0.56; --popover-content-z: 75; --popover-overlay-z: 60; --popover-overlay-bg: var(--color-overlay); diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css index bfcb43739..6bdcfe5e1 100644 --- a/src/uix/eidos/index.css +++ b/src/uix/eidos/index.css @@ -69,6 +69,7 @@ @import './components/tooltip/tooltip.css'; @import './components/progress/progress.css'; @import './components/meter/meter.css'; +@import './components/pagination/pagination.css'; @import './components/toggle/toggle.css'; @import './components/switch/switch.css'; @import './components/collapsible/collapsible.css'; diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 278aeabb8..81df1c2c2 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -347,6 +347,41 @@ export const THEME_BASE_RECIPE_TOKENS = { 'transition-duration': 'var(--duration-normal)', 'transition-ease': 'var(--ease-default)' }, + pagination: { + 'gap-sm': 'var(--space-1)', + 'gap-md': 'var(--space-1-5)', + 'gap-lg': 'var(--space-2)', + 'control-size-sm': 'var(--control-height-xs)', + 'control-size-md': 'var(--control-height-sm)', + 'control-size-lg': 'var(--control-height-md)', + 'control-px-sm': 'var(--space-2)', + 'control-px-md': 'var(--space-2-5)', + 'control-px-lg': 'var(--space-3)', + '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)', + 'control-radius': 'var(--radius-md)', + 'control-border-width': 'var(--border-width)', + 'control-border': 'transparent', + 'control-border-hover': 'var(--color-border-subtle)', + 'control-bg': 'transparent', + 'control-bg-hover': 'var(--color-surface-raised)', + 'control-color': 'var(--color-content-secondary)', + 'control-color-hover': 'var(--color-content-primary)', + 'selected-border': 'var(--color-primary-border)', + 'selected-border-hover': 'var(--color-primary-border)', + 'selected-bg': 'var(--color-primary-solid)', + 'selected-bg-hover': 'var(--color-primary-solid-hover)', + 'selected-color': 'var(--color-content-on-solid)', + 'ellipsis-color': 'var(--color-content-muted)', + '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)', + 'disabled-opacity': '0.56' + }, popover: { 'content-z': '75', 'overlay-z': '60',