Add Eidos progress and meter wrappers

active-uix
dev 5 months ago
parent d3efdb5c30
commit 5895f9d8b8

@ -1,6 +1,45 @@
# Continue # Continue
Fecha de corte: 2026-05-16. Rama: `active-uix`. Fecha de corte: 2026-05-17. Rama: `active-uix`.
Actualizacion Eidos component migration 2026-05-17:
- Inicio de la migracion Soma -> Eidos siguiendo `soma/COMPONENT_GUIDE.md`
y `eidos/components/README.md`.
- Componentes migrados:
- `Progress`: root visual `progress.svelte`, `Progress.Indicator`,
`progress.css`, `types.ts` e `index.ts` con disciplined option C.
- `Meter`: root visual `meter.svelte`, `Meter.Indicator`, `meter.css`,
`types.ts` e `index.ts` con disciplined option C.
- Decisiones aplicadas:
- Eidos no crea comportamiento: ambos wrappers envuelven `Provider` e
`Indicator` publicos de Soma.
- La unica prop visual anadida es `size?: ResponsiveProp<'sm' | 'md' | 'lg'>`,
resuelta con `ActiveEidos.resolve(...)` y proyectada como `data-size`.
- `Progress` usa el `data-state` de Soma (`indeterminate`, `loading`,
`loaded`) para cargar/terminar/indeterminado.
- `Meter` usa el `data-state` de Soma (`below`, `optimum`, `above`) para
zonas visuales; no se anade intent ni color en Eidos.
- Los tokens base viven en `THEME_BASE_RECIPE_TOKENS` y se regenero
`src/uix/eidos/generated/base.css`.
- Guardias actualizadas:
- `component-visual-attrs.test.ts` protege `data-size` en `Progress` y
`Meter`.
- `scripts/eidos-lint-all.ts` reconoce `data-floating-wrapper` como marcador
publico actual y retira el sufijo legacy `flat`.
- `components/README.md` refleja `meter` y `progress` como migrados.
- Validado:
- `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: `separator` no existe
en Soma; candidatos reales por morfo/soma existentes son `slider`,
`number-field`, `pagination` o `rating-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.
Actualizacion Eidos components audit 2026-05-17: Actualizacion Eidos components audit 2026-05-17:

@ -36,7 +36,7 @@ const EIDOS_ONLY_ATTRS = new Set([
'data-scrollable', 'data-scrollable',
'data-sheet', 'data-sheet',
'data-size', 'data-size',
'data-soma-floating-wrapper', 'data-floating-wrapper',
'data-svg', 'data-svg',
'data-svg-direction', 'data-svg-direction',
'data-variant' 'data-variant'
@ -53,7 +53,6 @@ const RUNTIME_SIGNAL_ATTRS = new Set([
]) ])
const EIDOS_ONLY_PART_SUFFIXES = [ const EIDOS_ONLY_PART_SUFFIXES = [
'flat',
'footer', 'footer',
'handle', 'handle',
'header', 'header',

@ -16,8 +16,10 @@ const VISUAL_ATTRS = [
['dialog/dialog-content.svelte', ['data-size', 'data-position', 'data-sheet']], ['dialog/dialog-content.svelte', ['data-size', 'data-position', 'data-sheet']],
['drawer/drawer-close.svelte', ['data-icon-only']], ['drawer/drawer-close.svelte', ['data-icon-only']],
['drawer/drawer-content.svelte', ['data-size']], ['drawer/drawer-content.svelte', ['data-size']],
['meter/meter.svelte', ['data-size']],
['popover/popover-close.svelte', ['data-icon-only']], ['popover/popover-close.svelte', ['data-icon-only']],
['popover/popover-content.svelte', ['data-size']], ['popover/popover-content.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']],
['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']],

@ -340,7 +340,7 @@ su subdirectorio.
--- ---
## Estado actual de la migración (2026-05-14) ## Estado actual de la migración (2026-05-17)
| Componente | Forma canónica | Notas | | Componente | Forma canónica | Notas |
| ----------- | -------------- | ---------------------------------------------------------------------------- | | ----------- | -------------- | ---------------------------------------------------------------------------- |
@ -354,6 +354,8 @@ su subdirectorio.
| accordion | ✅ multi-part | Item, Header, Trigger, Content | | accordion | ✅ multi-part | Item, Header, Trigger, Content |
| avatar | ✅ multi-part | Image, Fallback (eidos-native) | | avatar | ✅ multi-part | Image, Fallback (eidos-native) |
| icon | ✅ single-part | + 1697 lucide glyphs | | icon | ✅ single-part | + 1697 lucide glyphs |
| meter | ✅ multi-part | Indicator |
| progress | ✅ multi-part | Indicator |
| tooltip | ✅ multi-part | + Group | | tooltip | ✅ multi-part | + Group |
| tabs | ✅ multi-part | List, Trigger, Content, Indicator | | tabs | ✅ multi-part | List, Trigger, Content, Indicator |
| checkbox | ✅ multi-part | Indicator, HiddenInput, Group, GroupLabel | | checkbox | ✅ multi-part | Indicator, HiddenInput, Group, GroupLabel |

@ -0,0 +1,27 @@
// Meter — eidos compound API, disciplined option C.
//
// import { Meter } from '$uix/eidos/components/meter';
//
// <Meter value={72} low={30} high={90}>
// <Meter.Indicator />
// </Meter>
import MeterRoot from './meter.svelte';
import Indicator from './meter-indicator.svelte';
type MeterNamespace = typeof MeterRoot & {
Indicator: typeof Indicator;
};
const Meter = MeterRoot as MeterNamespace;
Meter.Indicator = Indicator;
export { Meter };
export default Meter;
export type {
MeterProps,
MeterIndicatorProps as IndicatorProps,
MeterSize,
MeterZone
} from './types';

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

@ -0,0 +1,47 @@
[data-meter] {
--_meter-height: var(--meter-height-md);
--_meter-radius: var(--meter-radius-md);
--_meter-indicator-bg: var(--meter-indicator-bg-optimum);
position: relative;
overflow: hidden;
inline-size: 100%;
block-size: var(--_meter-height);
border-radius: var(--_meter-radius);
background: var(--meter-track-bg);
}
[data-meter][data-size='sm'] {
--_meter-height: var(--meter-height-sm);
--_meter-radius: var(--meter-radius-sm);
}
[data-meter][data-size='lg'] {
--_meter-height: var(--meter-height-lg);
--_meter-radius: var(--meter-radius-lg);
}
[data-meter][data-state='below'] {
--_meter-indicator-bg: var(--meter-indicator-bg-below);
}
[data-meter][data-state='above'] {
--_meter-indicator-bg: var(--meter-indicator-bg-above);
}
[data-meter-indicator] {
display: block;
inline-size: calc(var(--soma-meter-value-pct, 0) * 1%);
block-size: 100%;
border-radius: inherit;
background: var(--_meter-indicator-bg);
transition:
inline-size var(--meter-transition-duration) var(--meter-transition-ease),
background var(--meter-transition-duration) var(--meter-transition-ease);
}
@media (prefers-reduced-motion: reduce) {
[data-meter-indicator] {
transition: none;
}
}

@ -0,0 +1,18 @@
<script lang="ts">
/**
* Eidos `<Meter>` — visual wrapper over Soma's meter primitive.
* Soma owns ARIA value state and zone classification; Eidos adds `size`.
*/
import { ActiveEidos } from '$uix/eidos';
import { Provider as SomaMeterProvider } from '$soma/components/meter';
import type { MeterProps } from './types';
let { size = 'md', children, ...rest }: MeterProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
</script>
<SomaMeterProvider {...rest} data-size={resolvedSize}>
{@render children?.()}
</SomaMeterProvider>

@ -0,0 +1,22 @@
import type {
IndicatorProps as SomaMeterIndicatorProps,
MeterZone,
ProviderProps as SomaMeterProviderProps
} from '$soma/components/meter';
import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
export type MeterSize = Extract<Size, 'sm' | 'md' | 'lg'>;
/**
* Props for the eidos `<Meter>`.
*
* Extends Soma's meter with visual sizing only. Zone state (`below`,
* `optimum`, `above`) is owned by Soma and emitted through `data-state`.
*/
export type MeterProps = SomaMeterProviderProps & {
/** Visual size. Accepts a responsive map resolved by ActiveEidos. @default 'md' */
size?: ResponsiveProp<MeterSize>;
};
export type MeterIndicatorProps = SomaMeterIndicatorProps;
export type { MeterZone };

@ -0,0 +1,27 @@
// Progress — eidos compound API, disciplined option C.
//
// import { Progress } from '$uix/eidos/components/progress';
//
// <Progress value={64}>
// <Progress.Indicator />
// </Progress>
import ProgressRoot from './progress.svelte';
import Indicator from './progress-indicator.svelte';
type ProgressNamespace = typeof ProgressRoot & {
Indicator: typeof Indicator;
};
const Progress = ProgressRoot as ProgressNamespace;
Progress.Indicator = Indicator;
export { Progress };
export default Progress;
export type {
ProgressProps,
ProgressIndicatorProps as IndicatorProps,
ProgressSize,
ProgressState
} from './types';

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

@ -0,0 +1,63 @@
[data-progress] {
--_progress-height: var(--progress-height-md);
--_progress-radius: var(--progress-radius-md);
position: relative;
overflow: hidden;
inline-size: 100%;
block-size: var(--_progress-height);
border-radius: var(--_progress-radius);
background: var(--progress-track-bg);
}
[data-progress][data-size='sm'] {
--_progress-height: var(--progress-height-sm);
--_progress-radius: var(--progress-radius-sm);
}
[data-progress][data-size='lg'] {
--_progress-height: var(--progress-height-lg);
--_progress-radius: var(--progress-radius-lg);
}
[data-progress-indicator] {
display: block;
inline-size: calc(var(--soma-progress-value-pct, 0) * 1%);
block-size: 100%;
border-radius: inherit;
background: var(--progress-indicator-bg);
transition:
inline-size var(--progress-transition-duration) var(--progress-transition-ease),
background var(--progress-transition-duration) var(--progress-transition-ease);
}
[data-progress][data-state='loaded'] [data-progress-indicator] {
background: var(--progress-indicator-bg-loaded);
}
[data-progress][data-state='indeterminate'] [data-progress-indicator] {
inline-size: var(--progress-indeterminate-width);
animation: progress-indeterminate var(--progress-indeterminate-duration)
var(--progress-indeterminate-ease) infinite;
}
@keyframes progress-indeterminate {
from {
transform: translateX(-100%);
}
to {
transform: translateX(300%);
}
}
@media (prefers-reduced-motion: reduce) {
[data-progress-indicator] {
transition: none;
}
[data-progress][data-state='indeterminate'] [data-progress-indicator] {
animation: none;
transform: none;
}
}

@ -0,0 +1,18 @@
<script lang="ts">
/**
* Eidos `<Progress>` — visual wrapper over Soma's progressbar.
* Soma owns ARIA value state and `data-state`; Eidos adds `size`.
*/
import { ActiveEidos } from '$uix/eidos';
import { Provider as SomaProgressProvider } from '$soma/components/progress';
import type { ProgressProps } from './types';
let { size = 'md', children, ...rest }: ProgressProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
</script>
<SomaProgressProvider {...rest} data-size={resolvedSize}>
{@render children?.()}
</SomaProgressProvider>

@ -0,0 +1,23 @@
import type {
IndicatorProps as SomaProgressIndicatorProps,
ProgressState,
ProviderProps as SomaProgressProviderProps
} from '$soma/components/progress';
import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
export type ProgressSize = Extract<Size, 'sm' | 'md' | 'lg'>;
/**
* Props for the eidos `<Progress>`.
*
* Extends Soma's progressbar with visual sizing only. Completion state
* (`indeterminate`, `loading`, `loaded`) is owned by Soma and emitted through
* `data-state`.
*/
export type ProgressProps = SomaProgressProviderProps & {
/** Visual size. Accepts a responsive map resolved by ActiveEidos. @default 'md' */
size?: ResponsiveProp<ProgressSize>;
};
export type ProgressIndicatorProps = SomaProgressIndicatorProps;
export type { ProgressState };

@ -590,6 +590,18 @@
--drawer-close-disabled-opacity: 0.5; --drawer-close-disabled-opacity: 0.5;
--icon-size: var(--icon-size-md); --icon-size: var(--icon-size-md);
--icon-stroke-width: var(--icon-stroke-width-md); --icon-stroke-width: var(--icon-stroke-width-md);
--meter-height-sm: 4px;
--meter-height-md: 6px;
--meter-height-lg: 8px;
--meter-radius-sm: var(--radius-full);
--meter-radius-md: var(--radius-full);
--meter-radius-lg: var(--radius-full);
--meter-track-bg: var(--color-neutral-track);
--meter-indicator-bg-below: var(--color-risk-solid);
--meter-indicator-bg-optimum: var(--color-fulfill-solid);
--meter-indicator-bg-above: var(--color-threat-solid);
--meter-transition-duration: var(--duration-normal);
--meter-transition-ease: var(--ease-default);
--popover-content-z: 75; --popover-content-z: 75;
--popover-overlay-z: 60; --popover-overlay-z: 60;
--popover-overlay-bg: var(--color-overlay); --popover-overlay-bg: var(--color-overlay);
@ -629,6 +641,20 @@
--popover-close-color: var(--color-content-secondary); --popover-close-color: var(--color-content-secondary);
--popover-close-border: color-mix(in srgb, var(--color-border-default) 72%, transparent); --popover-close-border: color-mix(in srgb, var(--color-border-default) 72%, transparent);
--popover-close-border-hover: var(--color-border-strong); --popover-close-border-hover: var(--color-border-strong);
--progress-height-sm: 4px;
--progress-height-md: 6px;
--progress-height-lg: 8px;
--progress-radius-sm: var(--radius-full);
--progress-radius-md: var(--radius-full);
--progress-radius-lg: var(--radius-full);
--progress-track-bg: var(--color-neutral-track);
--progress-indicator-bg: var(--color-primary-solid);
--progress-indicator-bg-loaded: var(--color-fulfill-solid);
--progress-transition-duration: var(--duration-normal);
--progress-transition-ease: var(--ease-default);
--progress-indeterminate-width: 35%;
--progress-indeterminate-duration: 1.1s;
--progress-indeterminate-ease: var(--ease-default);
--radio-group-gap-vertical: var(--space-3); --radio-group-gap-vertical: var(--space-3);
--radio-group-gap-horizontal: var(--space-4); --radio-group-gap-horizontal: var(--space-4);
--radio-group-item-size-sm: 16px; --radio-group-item-size-sm: 16px;

@ -67,6 +67,8 @@
@import './components/drawer/drawer.css'; @import './components/drawer/drawer.css';
@import './components/popover/popover.css'; @import './components/popover/popover.css';
@import './components/tooltip/tooltip.css'; @import './components/tooltip/tooltip.css';
@import './components/progress/progress.css';
@import './components/meter/meter.css';
@import './components/toggle/toggle.css'; @import './components/toggle/toggle.css';
@import './components/switch/switch.css'; @import './components/switch/switch.css';
@import './components/collapsible/collapsible.css'; @import './components/collapsible/collapsible.css';

@ -333,6 +333,20 @@ export const THEME_BASE_RECIPE_TOKENS = {
size: 'var(--icon-size-md)', size: 'var(--icon-size-md)',
'stroke-width': 'var(--icon-stroke-width-md)' 'stroke-width': 'var(--icon-stroke-width-md)'
}, },
meter: {
'height-sm': '4px',
'height-md': '6px',
'height-lg': '8px',
'radius-sm': 'var(--radius-full)',
'radius-md': 'var(--radius-full)',
'radius-lg': 'var(--radius-full)',
'track-bg': 'var(--color-neutral-track)',
'indicator-bg-below': 'var(--color-risk-solid)',
'indicator-bg-optimum': 'var(--color-fulfill-solid)',
'indicator-bg-above': 'var(--color-threat-solid)',
'transition-duration': 'var(--duration-normal)',
'transition-ease': 'var(--ease-default)'
},
popover: { popover: {
'content-z': '75', 'content-z': '75',
'overlay-z': '60', 'overlay-z': '60',
@ -374,6 +388,22 @@ export const THEME_BASE_RECIPE_TOKENS = {
'close-border': 'color-mix(in srgb, var(--color-border-default) 72%, transparent)', 'close-border': 'color-mix(in srgb, var(--color-border-default) 72%, transparent)',
'close-border-hover': 'var(--color-border-strong)' 'close-border-hover': 'var(--color-border-strong)'
}, },
progress: {
'height-sm': '4px',
'height-md': '6px',
'height-lg': '8px',
'radius-sm': 'var(--radius-full)',
'radius-md': 'var(--radius-full)',
'radius-lg': 'var(--radius-full)',
'track-bg': 'var(--color-neutral-track)',
'indicator-bg': 'var(--color-primary-solid)',
'indicator-bg-loaded': 'var(--color-fulfill-solid)',
'transition-duration': 'var(--duration-normal)',
'transition-ease': 'var(--ease-default)',
'indeterminate-width': '35%',
'indeterminate-duration': '1.1s',
'indeterminate-ease': 'var(--ease-default)'
},
'radio-group': { 'radio-group': {
'gap-vertical': 'var(--space-3)', 'gap-vertical': 'var(--space-3)',
'gap-horizontal': 'var(--space-4)', 'gap-horizontal': 'var(--space-4)',

Loading…
Cancel
Save

Powered by TurnKey Linux.