Add Eidos rating group wrapper

active-uix
dev 5 months ago
parent f62b32677c
commit 5a6193d365

@ -58,9 +58,20 @@ Actualizacion Eidos component migration 2026-05-17:
- `npm run check` -> 0 errores, 0 warnings. - `npm run check` -> 0 errores, 0 warnings.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> 0 invalid, sin drift - `node --import tsx/esm scripts/eidos-lint-all.ts` -> 0 invalid, sin drift
hotspots. 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: - Siguiente bloque sugerido:
- Migrar otro grupo de bajo riesgo desde Soma a Eidos: candidatos reales por - 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`, - Antes de tocar componentes grandes (`select`, `combobox`, `calendar`,
`date-field`) conviene fijar una tabla por componente: partes Soma, `date-field`) conviene fijar una tabla por componente: partes Soma,
props visuales Eidos, recipe tokens y attrs esperados. props visuales Eidos, recipe tokens y attrs esperados.

@ -22,6 +22,7 @@ const VISUAL_ATTRS = [
['popover/popover-content.svelte', ['data-size']], ['popover/popover-content.svelte', ['data-size']],
['progress/progress.svelte', ['data-size']], ['progress/progress.svelte', ['data-size']],
['radio-group/radio-group.svelte', ['data-size', 'data-variant', 'data-color', 'data-columns']], ['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']], ['slider/slider.svelte', ['data-size']],
['switch/switch.svelte', ['data-size']], ['switch/switch.svelte', ['data-size']],
['tabs/tabs.svelte', ['data-size', 'data-variant', 'data-fitted', 'data-collapsed', 'data-motion']], ['tabs/tabs.svelte', ['data-size', 'data-variant', 'data-fitted', 'data-collapsed', 'data-motion']],

@ -357,6 +357,7 @@ su subdirectorio.
| meter | ✅ multi-part | Indicator | | meter | ✅ multi-part | Indicator |
| pagination | ✅ multi-part | PrevTrigger, NextTrigger, Item, Ellipsis | | pagination | ✅ multi-part | PrevTrigger, NextTrigger, Item, Ellipsis |
| progress | ✅ multi-part | Indicator | | progress | ✅ multi-part | Indicator |
| rating-group | ✅ multi-part | Item |
| slider | ✅ multi-part | Range, Thumb, Tick | | slider | ✅ multi-part | Range, Thumb, Tick |
| tooltip | ✅ multi-part | + Group | | tooltip | ✅ multi-part | + Group |
| tabs | ✅ multi-part | List, Trigger, Content, Indicator | | tabs | ✅ multi-part | List, Trigger, Content, Indicator |

@ -0,0 +1,32 @@
// RatingGroup — eidos compound API, disciplined option C.
//
// import { RatingGroup } from '$uix/eidos/components/rating-group';
//
// <RatingGroup bind:value let:items>
// {#each items as item}
// <RatingGroup.Item index={item.index}>★</RatingGroup.Item>
// {/each}
// </RatingGroup>
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';

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

@ -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;
}
}

@ -0,0 +1,20 @@
<script lang="ts">
/**
* Eidos `<RatingGroup>` — visual wrapper over Soma's rating primitive.
* Soma owns slider semantics, pointer preview and item state; Eidos adds `size`.
*/
import { ActiveEidos } from '$uix/eidos';
import { Provider as SomaRatingGroupProvider } from '$soma/components/rating-group';
import type { RatingGroupProps } from './types';
let { size = 'md', value = $bindable(0), children, ...rest }: RatingGroupProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
</script>
<SomaRatingGroupProvider {...rest} bind:value data-size={resolvedSize}>
{#snippet children(snippetProps)}
{@render children?.(snippetProps)}
{/snippet}
</SomaRatingGroupProvider>

@ -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<Size, 'sm' | 'md' | 'lg'>;
/**
* Props for the eidos `<RatingGroup>`.
*
* 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<RatingGroupSize>;
};
export type RatingGroupItemProps = SomaRatingGroupItemProps;
export type {
RatingGroupProviderSnippetProps,
RatingGroupItemSnippetProps,
RatingItemDescriptor,
RatingItemState
};

@ -735,6 +735,26 @@
--radio-group-label-font-weight: var(--font-weight-medium); --radio-group-label-font-weight: var(--font-weight-medium);
--radio-group-label-line-height: var(--leading-ui); --radio-group-label-line-height: var(--leading-ui);
--radio-group-label-color: var(--color-content-primary); --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-sm: 4px;
--slider-track-size-md: 6px; --slider-track-size-md: 6px;
--slider-track-size-lg: 8px; --slider-track-size-lg: 8px;

@ -77,4 +77,5 @@
@import './components/tabs/tabs.css'; @import './components/tabs/tabs.css';
@import './components/checkbox/checkbox.css'; @import './components/checkbox/checkbox.css';
@import './components/radio-group/radio-group.css'; @import './components/radio-group/radio-group.css';
@import './components/rating-group/rating-group.css';
@import './components/slider/slider.css'; @import './components/slider/slider.css';

@ -494,6 +494,29 @@ export const THEME_BASE_RECIPE_TOKENS = {
'label-line-height': 'var(--leading-ui)', 'label-line-height': 'var(--leading-ui)',
'label-color': 'var(--color-content-primary)' '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: { slider: {
'track-size-sm': '4px', 'track-size-sm': '4px',
'track-size-md': '6px', 'track-size-md': '6px',

Loading…
Cancel
Save

Powered by TurnKey Linux.