diff --git a/continue.md b/continue.md index 4e46f2e2b..3d5be4875 100644 --- a/continue.md +++ b/continue.md @@ -58,9 +58,20 @@ 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. +- Cuarta tanda del mismo bloque: + - `RatingGroup`: root visual `rating-group.svelte`, parte `Item`, + `rating-group.css`, `types.ts` e `index.ts`. + - No se inventa icono/star por defecto ni API nueva; el wrapper conserva los + snippets de Soma (`items` en root y `state` en item) y la receta estiliza + `active`, `partial`, `inactive`, foco, vertical/horizontal y disabled. +- Validado despues de `RatingGroup`: + - `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 `rating-group` o `number-field`. + morfo/soma existentes son `number-field`, `field` o `search-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 68a82e093..80ade6e26 100644 --- a/src/uix/eidos/component-visual-attrs.test.ts +++ b/src/uix/eidos/component-visual-attrs.test.ts @@ -22,6 +22,7 @@ const VISUAL_ATTRS = [ ['popover/popover-content.svelte', ['data-size']], ['progress/progress.svelte', ['data-size']], ['radio-group/radio-group.svelte', ['data-size', 'data-variant', 'data-color', 'data-columns']], + ['rating-group/rating-group.svelte', ['data-size']], ['slider/slider.svelte', ['data-size']], ['switch/switch.svelte', ['data-size']], ['tabs/tabs.svelte', ['data-size', 'data-variant', 'data-fitted', 'data-collapsed', 'data-motion']], diff --git a/src/uix/eidos/components/README.md b/src/uix/eidos/components/README.md index 81f5a40b8..bee50f19e 100644 --- a/src/uix/eidos/components/README.md +++ b/src/uix/eidos/components/README.md @@ -357,6 +357,7 @@ su subdirectorio. | meter | ✅ multi-part | Indicator | | pagination | ✅ multi-part | PrevTrigger, NextTrigger, Item, Ellipsis | | progress | ✅ multi-part | Indicator | +| rating-group | ✅ multi-part | Item | | slider | ✅ multi-part | Range, Thumb, Tick | | tooltip | ✅ multi-part | + Group | | tabs | ✅ multi-part | List, Trigger, Content, Indicator | diff --git a/src/uix/eidos/components/rating-group/index.ts b/src/uix/eidos/components/rating-group/index.ts new file mode 100644 index 000000000..1a58f6d86 --- /dev/null +++ b/src/uix/eidos/components/rating-group/index.ts @@ -0,0 +1,32 @@ +// RatingGroup — eidos compound API, disciplined option C. +// +// import { RatingGroup } from '$uix/eidos/components/rating-group'; +// +// +// {#each items as item} +// ★ +// {/each} +// +import RatingGroupRoot from './rating-group.svelte'; +import Item from './rating-group-item.svelte'; + +type RatingGroupNamespace = typeof RatingGroupRoot & { + Item: typeof Item; +}; + +const RatingGroup = RatingGroupRoot as RatingGroupNamespace; +RatingGroup.Item = Item; + +export { RatingGroup }; + +export default RatingGroup; + +export type { + RatingGroupProps, + RatingGroupItemProps as ItemProps, + RatingGroupSize, + RatingGroupProviderSnippetProps as ProviderSnippetProps, + RatingGroupItemSnippetProps as ItemSnippetProps, + RatingItemDescriptor, + RatingItemState +} from './types'; diff --git a/src/uix/eidos/components/rating-group/rating-group-item.svelte b/src/uix/eidos/components/rating-group/rating-group-item.svelte new file mode 100644 index 000000000..4b45a0fea --- /dev/null +++ b/src/uix/eidos/components/rating-group/rating-group-item.svelte @@ -0,0 +1,12 @@ + + + + {#snippet children(snippetProps)} + {@render children?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/rating-group/rating-group.css b/src/uix/eidos/components/rating-group/rating-group.css new file mode 100644 index 000000000..e5318c08b --- /dev/null +++ b/src/uix/eidos/components/rating-group/rating-group.css @@ -0,0 +1,83 @@ +[data-rating-group] { + --_rating-group-gap: var(--rating-group-gap-md); + --_rating-group-item-size: var(--rating-group-item-size-md); + --_rating-group-font-size: var(--rating-group-font-size-md); + + display: inline-flex; + align-items: center; + gap: var(--_rating-group-gap); + color: var(--rating-group-item-color-inactive); + font-family: var(--rating-group-font-family); + font-size: var(--_rating-group-font-size); + line-height: var(--rating-group-line-height); + outline: none; +} + +[data-rating-group][data-size='sm'] { + --_rating-group-gap: var(--rating-group-gap-sm); + --_rating-group-item-size: var(--rating-group-item-size-sm); + --_rating-group-font-size: var(--rating-group-font-size-sm); +} + +[data-rating-group][data-size='lg'] { + --_rating-group-gap: var(--rating-group-gap-lg); + --_rating-group-item-size: var(--rating-group-item-size-lg); + --_rating-group-font-size: var(--rating-group-font-size-lg); +} + +[data-rating-group][data-orientation='vertical'] { + flex-direction: column; + align-items: flex-start; +} + +[data-rating-group]:focus-visible { + border-radius: var(--rating-group-focus-radius); + box-shadow: var(--rating-group-focus-shadow); +} + +[data-rating-group][data-disabled] { + opacity: var(--rating-group-disabled-opacity); +} + +[data-rating-group-item] { + display: inline-flex; + align-items: center; + justify-content: center; + inline-size: var(--_rating-group-item-size); + block-size: var(--_rating-group-item-size); + color: var(--rating-group-item-color-inactive); + cursor: pointer; + transition: + color var(--rating-group-transition-duration) var(--rating-group-transition-ease), + transform var(--rating-group-transition-duration) var(--rating-group-transition-ease); +} + +[data-rating-group-item][data-state='active'] { + color: var(--rating-group-item-color-active); +} + +[data-rating-group-item][data-state='partial'] { + color: var(--rating-group-item-color-partial); +} + +[data-rating-group-item]:hover:not([data-disabled]):not([data-readonly]) { + transform: translateY(var(--rating-group-item-hover-offset)); +} + +[data-rating-group-item][data-disabled], +[data-rating-group-item][data-readonly] { + cursor: default; +} + +[data-rating-group-item] > :where(svg) { + inline-size: 100%; + block-size: 100%; + fill: currentColor; + stroke: currentColor; +} + +@media (prefers-reduced-motion: reduce) { + [data-rating-group-item] { + transition: none; + } +} diff --git a/src/uix/eidos/components/rating-group/rating-group.svelte b/src/uix/eidos/components/rating-group/rating-group.svelte new file mode 100644 index 000000000..8afd80c89 --- /dev/null +++ b/src/uix/eidos/components/rating-group/rating-group.svelte @@ -0,0 +1,20 @@ + + + + {#snippet children(snippetProps)} + {@render children?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/rating-group/types.ts b/src/uix/eidos/components/rating-group/types.ts new file mode 100644 index 000000000..ef699af34 --- /dev/null +++ b/src/uix/eidos/components/rating-group/types.ts @@ -0,0 +1,30 @@ +import type { + ItemProps as SomaRatingGroupItemProps, + ProviderProps as SomaRatingGroupProviderProps, + ProviderSnippetProps as RatingGroupProviderSnippetProps, + RatingItemDescriptor, + RatingItemState, + ItemSnippetProps as RatingGroupItemSnippetProps +} from '$soma/components/rating-group'; +import type { ResponsiveProp, Size } from '$uix/eidos/lib/types'; + +export type RatingGroupSize = Extract; + +/** + * Props for the eidos ``. + * + * Extends Soma's rating slider with visual sizing only. Value, hover preview, + * active/partial/inactive item state and ARIA are owned by Soma. + */ +export type RatingGroupProps = SomaRatingGroupProviderProps & { + /** Visual size. Accepts a responsive map resolved by ActiveEidos. @default 'md' */ + size?: ResponsiveProp; +}; + +export type RatingGroupItemProps = SomaRatingGroupItemProps; +export type { + RatingGroupProviderSnippetProps, + RatingGroupItemSnippetProps, + RatingItemDescriptor, + RatingItemState +}; diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 2067ecf13..61dcfb8b2 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -735,6 +735,26 @@ --radio-group-label-font-weight: var(--font-weight-medium); --radio-group-label-line-height: var(--leading-ui); --radio-group-label-color: var(--color-content-primary); + --rating-group-gap-sm: var(--space-1); + --rating-group-gap-md: var(--space-1-5); + --rating-group-gap-lg: var(--space-2); + --rating-group-item-size-sm: 18px; + --rating-group-item-size-md: 22px; + --rating-group-item-size-lg: 28px; + --rating-group-font-family: var(--font-ui); + --rating-group-font-size-sm: var(--font-size-sm); + --rating-group-font-size-md: var(--font-size-md); + --rating-group-font-size-lg: var(--font-size-lg); + --rating-group-line-height: var(--leading-ui); + --rating-group-item-color-inactive: var(--color-neutral-border); + --rating-group-item-color-active: var(--color-fulfill-solid); + --rating-group-item-color-partial: var(--color-fulfill-solid); + --rating-group-item-hover-offset: -1px; + --rating-group-focus-radius: var(--radius-md); + --rating-group-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); + --rating-group-transition-duration: var(--duration-fast); + --rating-group-transition-ease: var(--ease-default); + --rating-group-disabled-opacity: 0.56; --slider-track-size-sm: 4px; --slider-track-size-md: 6px; --slider-track-size-lg: 8px; diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css index 6bdcfe5e1..34e2c0f8a 100644 --- a/src/uix/eidos/index.css +++ b/src/uix/eidos/index.css @@ -77,4 +77,5 @@ @import './components/tabs/tabs.css'; @import './components/checkbox/checkbox.css'; @import './components/radio-group/radio-group.css'; +@import './components/rating-group/rating-group.css'; @import './components/slider/slider.css'; diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 81df1c2c2..23ede292f 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -494,6 +494,29 @@ export const THEME_BASE_RECIPE_TOKENS = { 'label-line-height': 'var(--leading-ui)', 'label-color': 'var(--color-content-primary)' }, + 'rating-group': { + 'gap-sm': 'var(--space-1)', + 'gap-md': 'var(--space-1-5)', + 'gap-lg': 'var(--space-2)', + 'item-size-sm': '18px', + 'item-size-md': '22px', + 'item-size-lg': '28px', + 'font-family': 'var(--font-ui)', + 'font-size-sm': 'var(--font-size-sm)', + 'font-size-md': 'var(--font-size-md)', + 'font-size-lg': 'var(--font-size-lg)', + 'line-height': 'var(--leading-ui)', + 'item-color-inactive': 'var(--color-neutral-border)', + 'item-color-active': 'var(--color-fulfill-solid)', + 'item-color-partial': 'var(--color-fulfill-solid)', + 'item-hover-offset': '-1px', + 'focus-radius': 'var(--radius-md)', + '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' + }, slider: { 'track-size-sm': '4px', 'track-size-md': '6px',