diff --git a/continue.md b/continue.md index c1b94bf3a..b45385391 100644 --- a/continue.md +++ b/continue.md @@ -49,6 +49,39 @@ Arranque rapido para mañana: 6. Mantener fuera de commits los logs sueltos (`.codex-vite-*`, `debug.log`) salvo que el usuario pida conservarlos. +Actualizacion Eidos 2026-05-16: + +- Primera pasada de componentes Option C ejecutada: + - Barrels multi-part normalizados a asignacion explicita per-property + (`const Drawer = DrawerRoot as DrawerNamespace; Drawer.Trigger = Trigger;`) + y sin `Object.assign`. + - Tipos root visuales renombrados a `AccordionProps`, `DrawerProps`, + `DialogProps`, `TabsProps`, etc. Eidos ya no define `XxxProviderProps`; + `ProviderProps` solo aparece como alias importado desde Soma cuando la + superficie headless lo llama asi. + - `src/uix/eidos/components/README.md` actualizado con la convencion real. + - `Toaster` deja de usar un alias interno `Provider` para no reintroducir el + modelo mental retirado. + - Aniadido `src/uix/eidos/lib/style.ts` con `composeInlineStyle(...)` y + aplicado a wrappers que mezclan estilo del consumidor con custom properties + de receta: overlay de Dialog/Drawer, Toast.Viewport, Checkbox.Group, + RadioGroup, Tabs.Indicator, Icon y primitives SVG. +- Validacion de esta pasada: + - `npm run check` -> 0 errores, 0 warnings. + - `npx vitest run src/uix/eidos` -> 4 archivos, 56 tests OK. + - `npm run test` -> 233 archivos, 2270 tests OK. + - SSR smoke de `web/routes/uix/components/{accordion,avatar,checkbox, + collapsible,dialog,drawer,popover,radio-group,switch,tabs,toast,toggle, + tooltip}` -> HTTP 200. +- Siguiente foco recomendado en Eidos: + 1. Auditar recipes/CSS contra tokens generados por `ActiveEidos` y decidir + que CSS legacy queda como recipe por componente y que debe generarse. + 2. Revisar componentes visuales que anaden partes eidos-only (`Header`, + `Footer`, `Status`, `Main`, SVG primitives) para documentar si necesitan + morfo propio o si son recipe primitives privadas. + 3. Revisar tests de componentes Eidos: hoy hay cobertura del runtime Eidos, + pero no tests unitarios especificos para wrappers visuales. + Actualizacion 2026-05-16: - Barrido de wrappers publicos Soma frente a providers cerrado: diff --git a/src/uix/eidos/components/README.md b/src/uix/eidos/components/README.md index d9616c9f0..3d90947cc 100644 --- a/src/uix/eidos/components/README.md +++ b/src/uix/eidos/components/README.md @@ -78,9 +78,9 @@ contexto headless, acepta los bindables del estado (`open`, `value`, /** * Eidos `` — root component. Wraps the Soma * to set up the component context. - */ + */ import { Provider as SomaDrawerProvider } from '$soma/components/drawer'; - import type { DrawerProviderProps } from './types'; + import type { DrawerProps } from './types'; let { open = $bindable(false), @@ -88,7 +88,7 @@ contexto headless, acepta los bindables del estado (`open`, `value`, isDragging = $bindable(false), children, ...rest - }: DrawerProviderProps = $props(); + }: DrawerProps = $props(); @@ -169,7 +169,7 @@ export { Drawer }; export default Drawer; export type { - DrawerProviderProps as DrawerProps, + DrawerProps, DrawerTriggerProps as TriggerProps, DrawerOverlayProps as OverlayProps, DrawerContentProps as ContentProps @@ -187,7 +187,7 @@ import type { // … } from '$soma/components/drawer'; -export type DrawerProviderProps = SomaDrawerProviderProps; +export type DrawerProps = SomaDrawerProviderProps; export type DrawerTriggerProps = SomaDrawerTriggerProps; // Eidos añade props visuales que el recipe consume vía data-attrs diff --git a/src/uix/eidos/components/accordion/accordion.svelte b/src/uix/eidos/components/accordion/accordion.svelte index 06258fa2e..e9732e54d 100644 --- a/src/uix/eidos/components/accordion/accordion.svelte +++ b/src/uix/eidos/components/accordion/accordion.svelte @@ -18,7 +18,7 @@ */ import * as SomaAccordion from '$soma/components/accordion'; import { ActiveEidos } from '$uix/eidos'; - import type { AccordionProviderProps } from './types'; + import type { AccordionProps } from './types'; import { setAccordionEidosContext } from './accordion-context'; let { @@ -27,7 +27,7 @@ variant = 'outline', children, ...rest - }: AccordionProviderProps = $props(); + }: AccordionProps = $props(); const eidos = ActiveEidos.require(); const resolvedSize = $derived(typeof size === 'string' ? size : eidos.resolve(size, 'md')); diff --git a/src/uix/eidos/components/accordion/index.ts b/src/uix/eidos/components/accordion/index.ts index 461142386..eb5aef7b7 100644 --- a/src/uix/eidos/components/accordion/index.ts +++ b/src/uix/eidos/components/accordion/index.ts @@ -12,25 +12,33 @@ // // // `` IS the root (lives in accordion.svelte). Children -// attached via Object.assign — bits-ui / shadcn-svelte pattern. No -// Provider exported publicly; no flat-with-snippets invention. +// are attached explicitly. No Provider exported publicly; no +// flat-with-snippets invention. import AccordionRoot from './accordion.svelte'; import Item from './accordion-item.svelte'; import Header from './accordion-header.svelte'; import Trigger from './accordion-trigger.svelte'; import Content from './accordion-content.svelte'; -export const Accordion = Object.assign(AccordionRoot, { - Item, - Header, - Trigger, - Content -}); +type AccordionNamespace = typeof AccordionRoot & { + Item: typeof Item; + Header: typeof Header; + Trigger: typeof Trigger; + Content: typeof Content; +}; + +const Accordion = AccordionRoot as AccordionNamespace; +Accordion.Item = Item; +Accordion.Header = Header; +Accordion.Trigger = Trigger; +Accordion.Content = Content; + +export { Accordion }; export default Accordion; export type { - AccordionProviderProps as AccordionProps, + AccordionProps, AccordionItemProps as ItemProps, AccordionHeaderProps as HeaderProps, AccordionTriggerProps as TriggerProps, diff --git a/src/uix/eidos/components/accordion/types.ts b/src/uix/eidos/components/accordion/types.ts index c2043b666..263064459 100644 --- a/src/uix/eidos/components/accordion/types.ts +++ b/src/uix/eidos/components/accordion/types.ts @@ -15,14 +15,14 @@ export type AccordionHeaderProps = SomaAccordionHeaderProps; export type AccordionContentProps = SomaAccordionContentProps; /** - * Provider props in eidos accept `ResponsiveProp` for + * Root props in eidos accept `ResponsiveProp` for * the `size` axis. The wrapper resolves it via `Eidos.resolve` and * stamps `data-size`; headless never receives size/variant because those * axes are visual-only. Variant stays single-valued — picking outline * vs surface vs ghost on a per-breakpoint basis is not a use case worth * exposing. */ -export type AccordionProviderProps = SomaAccordionProviderProps & { +export type AccordionProps = SomaAccordionProviderProps & { size?: ResponsiveProp; variant?: AccordionVariant; }; diff --git a/src/uix/eidos/components/avatar/avatar.svelte b/src/uix/eidos/components/avatar/avatar.svelte index 6d138bb9d..2ce3cbe2a 100644 --- a/src/uix/eidos/components/avatar/avatar.svelte +++ b/src/uix/eidos/components/avatar/avatar.svelte @@ -9,7 +9,7 @@ * JD * */ - import type { AvatarProviderProps } from './types'; + import type { AvatarProps } from './types'; let { size = 'md', @@ -18,7 +18,7 @@ radius = 'full', children, ...restProps - }: AvatarProviderProps = $props(); + }: AvatarProps = $props(); , 'children'> & { +export type AvatarProps = Omit, 'children'> & { /** Visual size token. @default 'md' */ size?: AvatarSize; /** Visual variant. @default 'soft' */ diff --git a/src/uix/eidos/components/checkbox/checkbox-group.svelte b/src/uix/eidos/components/checkbox/checkbox-group.svelte index 9e6f1fe5e..b9b76ad69 100644 --- a/src/uix/eidos/components/checkbox/checkbox-group.svelte +++ b/src/uix/eidos/components/checkbox/checkbox-group.svelte @@ -5,18 +5,24 @@ * - vertical + columns N → CSS grid with N columns (items wrap) * - vertical (no columns) → flex column * - horizontal → flex row (or grid with auto cols if columns set) - */ + */ import * as Checkbox from '$soma/components/checkbox'; + import { composeInlineStyle } from '$uix/eidos/lib/style'; import type { CheckboxGroupProps } from './types'; - let { columns, value = $bindable([]), children, ...rest }: CheckboxGroupProps = $props(); + let { columns, value = $bindable([]), style, children, ...rest }: CheckboxGroupProps = $props(); + + const columnsStyle = $derived( + columns ? `--_checkbox-group-columns: ${columns};` : undefined + ); + const mergedStyle = $derived(composeInlineStyle(style, columnsStyle)); {@render children?.()} diff --git a/src/uix/eidos/components/checkbox/checkbox.svelte b/src/uix/eidos/components/checkbox/checkbox.svelte index 19f69d5aa..b1d7ae2ce 100644 --- a/src/uix/eidos/components/checkbox/checkbox.svelte +++ b/src/uix/eidos/components/checkbox/checkbox.svelte @@ -12,7 +12,7 @@ */ import { ActiveEidos } from '$uix/eidos'; import * as SomaCheckbox from '$soma/components/checkbox'; - import type { CheckboxProviderProps } from './types'; + import type { CheckboxProps } from './types'; let { size = 'md', @@ -22,7 +22,7 @@ indeterminate = $bindable(false), children: bodyContent, ...rest - }: CheckboxProviderProps = $props(); + }: CheckboxProps = $props(); const eidos = ActiveEidos.require(); const resolvedSize = $derived(eidos.resolve(size, 'md')); diff --git a/src/uix/eidos/components/checkbox/index.ts b/src/uix/eidos/components/checkbox/index.ts index eeaeb3c0e..6a9d055e2 100644 --- a/src/uix/eidos/components/checkbox/index.ts +++ b/src/uix/eidos/components/checkbox/index.ts @@ -18,17 +18,25 @@ import HiddenInput from './checkbox-hidden-input.svelte'; import Group from './checkbox-group.svelte'; import GroupLabel from './checkbox-group-label.svelte'; -export const Checkbox = Object.assign(CheckboxRoot, { - Indicator, - HiddenInput, - Group, - GroupLabel -}); +type CheckboxNamespace = typeof CheckboxRoot & { + Indicator: typeof Indicator; + HiddenInput: typeof HiddenInput; + Group: typeof Group; + GroupLabel: typeof GroupLabel; +}; + +const Checkbox = CheckboxRoot as CheckboxNamespace; +Checkbox.Indicator = Indicator; +Checkbox.HiddenInput = HiddenInput; +Checkbox.Group = Group; +Checkbox.GroupLabel = GroupLabel; + +export { Checkbox }; export default Checkbox; export type { - CheckboxProviderProps as CheckboxProps, + CheckboxProps, CheckboxIndicatorProps as IndicatorProps, CheckboxHiddenInputProps as HiddenInputProps, CheckboxGroupProps as GroupProps, diff --git a/src/uix/eidos/components/checkbox/types.ts b/src/uix/eidos/components/checkbox/types.ts index ea37929ee..536789aa9 100644 --- a/src/uix/eidos/components/checkbox/types.ts +++ b/src/uix/eidos/components/checkbox/types.ts @@ -44,7 +44,7 @@ export type CheckboxIconSnippetArgs = { indeterminate: boolean; }; -export type CheckboxProviderProps = SomaCheckboxProviderProps & { +export type CheckboxProps = SomaCheckboxProviderProps & { /** Visual size preset. Resolved via `Eidos.resolve`. @default 'md' */ size?: ResponsiveProp; /** Visual variant. @default 'solid' */ diff --git a/src/uix/eidos/components/collapsible/index.ts b/src/uix/eidos/components/collapsible/index.ts index 34ec7ff51..6f9f5a547 100644 --- a/src/uix/eidos/components/collapsible/index.ts +++ b/src/uix/eidos/components/collapsible/index.ts @@ -10,10 +10,16 @@ import CollapsibleRoot from './collapsible.svelte'; import Trigger from './collapsible-trigger.svelte'; import Content from './collapsible-content.svelte'; -export const Collapsible = Object.assign(CollapsibleRoot, { - Trigger, - Content -}); +type CollapsibleNamespace = typeof CollapsibleRoot & { + Trigger: typeof Trigger; + Content: typeof Content; +}; + +const Collapsible = CollapsibleRoot as CollapsibleNamespace; +Collapsible.Trigger = Trigger; +Collapsible.Content = Content; + +export { Collapsible }; export default Collapsible; diff --git a/src/uix/eidos/components/dialog/dialog-overlay.svelte b/src/uix/eidos/components/dialog/dialog-overlay.svelte index d6d20ed65..2deba8e7c 100644 --- a/src/uix/eidos/components/dialog/dialog-overlay.svelte +++ b/src/uix/eidos/components/dialog/dialog-overlay.svelte @@ -6,6 +6,7 @@ * inline `--dialog-overlay-opacity` custom property so the recipe's * `color-mix` picks it up without a class swap. */ + import { composeInlineStyle } from '$uix/eidos/lib/style'; import * as Dialog from '$soma/components/dialog'; import type { DialogOverlayProps } from './types'; @@ -23,12 +24,10 @@ const resolvedOpacity = $derived(serializeOpacity(overlayOpacity)); const composedStyle = $derived( - [style, resolvedOpacity ? `--dialog-overlay-opacity:${resolvedOpacity}` : ''] - .filter(Boolean) - .join(';') + composeInlineStyle(style, resolvedOpacity ? `--dialog-overlay-opacity: ${resolvedOpacity};` : undefined) ); - + {@render children?.()} diff --git a/src/uix/eidos/components/dialog/dialog.svelte b/src/uix/eidos/components/dialog/dialog.svelte index 6bc8f04f9..688240471 100644 --- a/src/uix/eidos/components/dialog/dialog.svelte +++ b/src/uix/eidos/components/dialog/dialog.svelte @@ -20,9 +20,9 @@ * */ import * as SomaDialog from '$soma/components/dialog'; - import type { DialogProviderProps } from './types'; + import type { DialogProps } from './types'; - let { open = $bindable(false), children, ...rest }: DialogProviderProps = $props(); + let { open = $bindable(false), children, ...rest }: DialogProps = $props(); diff --git a/src/uix/eidos/components/dialog/index.ts b/src/uix/eidos/components/dialog/index.ts index 0e5854e29..df9ac43ea 100644 --- a/src/uix/eidos/components/dialog/index.ts +++ b/src/uix/eidos/components/dialog/index.ts @@ -27,22 +27,35 @@ import Header from './dialog-header.svelte'; import Footer from './dialog-footer.svelte'; import { Portal } from '$soma/components/internal'; -export const Dialog = Object.assign(DialogRoot, { - Trigger, - Portal, - Overlay, - Content, - Title, - Description, - Close, - Header, - Footer -}); +type DialogNamespace = typeof DialogRoot & { + Trigger: typeof Trigger; + Portal: typeof Portal; + Overlay: typeof Overlay; + Content: typeof Content; + Title: typeof Title; + Description: typeof Description; + Close: typeof Close; + Header: typeof Header; + Footer: typeof Footer; +}; + +const Dialog = DialogRoot as DialogNamespace; +Dialog.Trigger = Trigger; +Dialog.Portal = Portal; +Dialog.Overlay = Overlay; +Dialog.Content = Content; +Dialog.Title = Title; +Dialog.Description = Description; +Dialog.Close = Close; +Dialog.Header = Header; +Dialog.Footer = Footer; + +export { Dialog }; export default Dialog; export type { - DialogProviderProps as DialogProps, + DialogProps, DialogTriggerProps as TriggerProps, DialogOverlayProps as OverlayProps, DialogContentProps as ContentProps, diff --git a/src/uix/eidos/components/dialog/types.ts b/src/uix/eidos/components/dialog/types.ts index 06c4d707d..c55772d60 100644 --- a/src/uix/eidos/components/dialog/types.ts +++ b/src/uix/eidos/components/dialog/types.ts @@ -27,7 +27,7 @@ export type DialogSize = Extract; */ export type DialogPosition = Position; -export type DialogProviderProps = SomaDialogProviderProps; +export type DialogProps = SomaDialogProviderProps; export type DialogTriggerProps = SomaDialogTriggerProps; export type DialogContentProps = SomaDialogContentProps & { diff --git a/src/uix/eidos/components/drawer/drawer-overlay.svelte b/src/uix/eidos/components/drawer/drawer-overlay.svelte index 7b99d4f67..a1facd6f1 100644 --- a/src/uix/eidos/components/drawer/drawer-overlay.svelte +++ b/src/uix/eidos/components/drawer/drawer-overlay.svelte @@ -11,6 +11,7 @@ * wrapper still mounts the headless part for consistency, but it has no * visual effect. */ + import { composeInlineStyle } from '$uix/eidos/lib/style'; import * as Drawer from '$soma/components/drawer'; import type { DrawerOverlayProps } from './types'; @@ -28,12 +29,10 @@ const resolvedOpacity = $derived(serializeOpacity(overlayOpacity)); const composedStyle = $derived( - [style, resolvedOpacity ? `--drawer-overlay-opacity:${resolvedOpacity}` : ''] - .filter(Boolean) - .join(';') + composeInlineStyle(style, resolvedOpacity ? `--drawer-overlay-opacity: ${resolvedOpacity};` : undefined) ); - + {@render children?.()} diff --git a/src/uix/eidos/components/drawer/drawer.svelte b/src/uix/eidos/components/drawer/drawer.svelte index 2bd8a2531..2d5b22389 100644 --- a/src/uix/eidos/components/drawer/drawer.svelte +++ b/src/uix/eidos/components/drawer/drawer.svelte @@ -13,7 +13,7 @@ * */ import { Provider as SomaDrawerProvider } from '$soma/components/drawer'; - import type { DrawerProviderProps } from './types'; + import type { DrawerProps } from './types'; let { open = $bindable(false), @@ -21,7 +21,7 @@ isDragging = $bindable(false), children, ...rest - }: DrawerProviderProps = $props(); + }: DrawerProps = $props(); diff --git a/src/uix/eidos/components/drawer/index.ts b/src/uix/eidos/components/drawer/index.ts index cbd3ceb24..537af94f4 100644 --- a/src/uix/eidos/components/drawer/index.ts +++ b/src/uix/eidos/components/drawer/index.ts @@ -18,9 +18,9 @@ // // // `` IS the root (lives in drawer.svelte, wraps Soma's -// Drawer.Provider). All child parts are attached as named members via -// Object.assign — bits-ui / shadcn-svelte pattern. No Provider exported -// publicly; no flat-with-snippet-slots invention. Children naming +// Drawer.Provider). All child parts are attached as named members +// explicitly. No Provider exported publicly; no flat-with-snippet-slots +// invention. Children naming // follows air / headless: Trigger, Content, Overlay, Title, Description, // Close, Portal (+ Header, Footer as eidos-only layout shells, and // Handle inherited from Soma's drag affordance). @@ -36,9 +36,9 @@ import Header from './drawer-header.svelte'; import Footer from './drawer-footer.svelte'; import { Portal } from '$soma/components/internal'; -// Explicit per-property assignment instead of `Object.assign(DrawerRoot, ...)` +// Explicit per-property assignment instead of bulk assignment // to avoid a Svelte 5 hydration quirk where mutating the component -// constructor with bulk Object.assign caused `` children +// constructor in bulk caused `` children // to re-mount on hydration (button appeared, then disappeared). type DrawerNamespace = typeof DrawerRoot & { Trigger: typeof Trigger; @@ -69,7 +69,7 @@ export { Drawer }; export default Drawer; export type { - DrawerProviderProps as DrawerProps, + DrawerProps, DrawerTriggerProps as TriggerProps, DrawerOverlayProps as OverlayProps, DrawerContentProps as ContentProps, diff --git a/src/uix/eidos/components/drawer/types.ts b/src/uix/eidos/components/drawer/types.ts index d40d0dde0..647e4f0d9 100644 --- a/src/uix/eidos/components/drawer/types.ts +++ b/src/uix/eidos/components/drawer/types.ts @@ -24,7 +24,7 @@ export type DrawerSize = Extract; export type { DrawerVariant, DrawerDirection, DrawerSide }; -export type DrawerProviderProps = SomaDrawerProviderProps; +export type DrawerProps = SomaDrawerProviderProps; export type DrawerTriggerProps = SomaDrawerTriggerProps; export type DrawerHandleProps = SomaDrawerHandleProps; export type DrawerTitleProps = SomaDrawerTitleProps; diff --git a/src/uix/eidos/components/icon/icon.svelte b/src/uix/eidos/components/icon/icon.svelte index 91df8e16b..f024b06a3 100644 --- a/src/uix/eidos/components/icon/icon.svelte +++ b/src/uix/eidos/components/icon/icon.svelte @@ -1,4 +1,5 @@ /** * Eidos `` — passthrough wrapper for the headless - * Overlay part. Only meaningful for MODAL popovers (``); the recipe handles backdrop styling. + * Overlay part. Only meaningful for modal `` roots; the + * recipe handles backdrop styling. */ import * as Popover from '$soma/components/popover'; import type { PopoverOverlayProps } from './types'; diff --git a/src/uix/eidos/components/popover/popover.svelte b/src/uix/eidos/components/popover/popover.svelte index 03af146d0..df076bb98 100644 --- a/src/uix/eidos/components/popover/popover.svelte +++ b/src/uix/eidos/components/popover/popover.svelte @@ -16,9 +16,9 @@ * */ import * as SomaPopover from '$soma/components/popover'; - import type { PopoverProviderProps } from './types'; + import type { PopoverProps } from './types'; - let { open = $bindable(false), children, ...rest }: PopoverProviderProps = $props(); + let { open = $bindable(false), children, ...rest }: PopoverProps = $props(); diff --git a/src/uix/eidos/components/popover/types.ts b/src/uix/eidos/components/popover/types.ts index 28c7348f0..cc350322a 100644 --- a/src/uix/eidos/components/popover/types.ts +++ b/src/uix/eidos/components/popover/types.ts @@ -16,7 +16,7 @@ import type { */ export type PopoverSize = Extract; -export type PopoverProviderProps = SomaPopoverProviderProps; +export type PopoverProps = SomaPopoverProviderProps; export type PopoverTriggerProps = SomaPopoverTriggerProps; export type PopoverArrowProps = SomaPopoverArrowProps; export type PopoverCloseProps = SomaPopoverCloseProps; diff --git a/src/uix/eidos/components/radio-group/index.ts b/src/uix/eidos/components/radio-group/index.ts index 38ba7d036..fa08337a7 100644 --- a/src/uix/eidos/components/radio-group/index.ts +++ b/src/uix/eidos/components/radio-group/index.ts @@ -19,17 +19,25 @@ import Indicator from './radio-group-indicator.svelte'; import HiddenInput from './radio-group-hidden-input.svelte'; import Label from './radio-group-label.svelte'; -export const RadioGroup = Object.assign(RadioGroupRoot, { - Item, - Indicator, - HiddenInput, - Label -}); +type RadioGroupNamespace = typeof RadioGroupRoot & { + Item: typeof Item; + Indicator: typeof Indicator; + HiddenInput: typeof HiddenInput; + Label: typeof Label; +}; + +const RadioGroup = RadioGroupRoot as RadioGroupNamespace; +RadioGroup.Item = Item; +RadioGroup.Indicator = Indicator; +RadioGroup.HiddenInput = HiddenInput; +RadioGroup.Label = Label; + +export { RadioGroup }; export default RadioGroup; export type { - RadioGroupProviderProps as RadioGroupProps, + RadioGroupProps, RadioGroupItemProps as ItemProps, RadioGroupIndicatorProps as IndicatorProps, RadioGroupHiddenInputProps as HiddenInputProps, diff --git a/src/uix/eidos/components/radio-group/radio-group.svelte b/src/uix/eidos/components/radio-group/radio-group.svelte index 241f3646d..e7b7accfc 100644 --- a/src/uix/eidos/components/radio-group/radio-group.svelte +++ b/src/uix/eidos/components/radio-group/radio-group.svelte @@ -12,10 +12,11 @@ * * * - */ + */ import { ActiveEidos } from '$uix/eidos'; + import { composeInlineStyle } from '$uix/eidos/lib/style'; import * as SomaRadioGroup from '$soma/components/radio-group'; - import type { RadioGroupProviderProps } from './types'; + import type { RadioGroupProps } from './types'; let { size = 'md', @@ -23,12 +24,17 @@ color = 'primary', columns, value = $bindable(''), + style, children, ...rest - }: RadioGroupProviderProps = $props(); + }: RadioGroupProps = $props(); const eidos = ActiveEidos.require(); const resolvedSize = $derived(eidos.resolve(size, 'md')); + const columnsStyle = $derived( + columns ? `--_radio-group-columns: ${columns};` : undefined + ); + const mergedStyle = $derived(composeInlineStyle(style, columnsStyle)); {@render children?.()} diff --git a/src/uix/eidos/components/radio-group/types.ts b/src/uix/eidos/components/radio-group/types.ts index 76aef931f..0ac2146b8 100644 --- a/src/uix/eidos/components/radio-group/types.ts +++ b/src/uix/eidos/components/radio-group/types.ts @@ -41,7 +41,7 @@ export type RadioGroupIconSnippetArgs = { checked: boolean; }; -export type RadioGroupProviderProps = SomaRadioGroupProviderProps & { +export type RadioGroupProps = SomaRadioGroupProviderProps & { /** Visual size preset. Resolved via `Eidos.resolve`. @default 'md' */ size?: ResponsiveProp; /** Visual variant. @default 'solid' */ diff --git a/src/uix/eidos/components/svg/svg-check.svelte b/src/uix/eidos/components/svg/svg-check.svelte index 39bb87ee4..8ea758a92 100644 --- a/src/uix/eidos/components/svg/svg-check.svelte +++ b/src/uix/eidos/components/svg/svg-check.svelte @@ -10,6 +10,7 @@ * `pathLength="100"` so the dasharray maths stays consumer-readable * regardless of the path's actual length in user units. */ + import { composeInlineStyle } from '$uix/eidos/lib/style'; import type { SvgPrimitiveProps } from './types'; let { @@ -20,15 +21,7 @@ }: SvgPrimitiveProps = $props(); const dim = $derived(typeof size === 'number' ? `${size}px` : size); - const mergedStyle = $derived( - [ - `width: ${dim}`, - `height: ${dim}`, - typeof style === 'string' ? style : '' - ] - .filter(Boolean) - .join('; ') - ); + const mergedStyle = $derived(composeInlineStyle(`width: ${dim}; height: ${dim};`, style)); diff --git a/src/uix/eidos/components/svg/svg-dash.svelte b/src/uix/eidos/components/svg/svg-dash.svelte index edcdfc6c4..7ca65dff7 100644 --- a/src/uix/eidos/components/svg/svg-dash.svelte +++ b/src/uix/eidos/components/svg/svg-dash.svelte @@ -8,6 +8,7 @@ * recipes can animate via `stroke-dashoffset` without measuring the * actual path length. */ + import { composeInlineStyle } from '$uix/eidos/lib/style'; import type { SvgPrimitiveProps } from './types'; let { @@ -18,15 +19,7 @@ }: SvgPrimitiveProps = $props(); const dim = $derived(typeof size === 'number' ? `${size}px` : size); - const mergedStyle = $derived( - [ - `width: ${dim}`, - `height: ${dim}`, - typeof style === 'string' ? style : '' - ] - .filter(Boolean) - .join('; ') - ); + const mergedStyle = $derived(composeInlineStyle(`width: ${dim}; height: ${dim};`, style)); (null), ...rest }: TabsIndicatorProps = $props(); + let { ref = $bindable(null), style, ...rest }: TabsIndicatorProps = $props(); let geom = $state({ x: 0, y: 0, w: 0, h: 0, ready: false }); let rafId: number | null = null; @@ -46,6 +47,13 @@ }); } + const geometryStyle = $derived( + geom.ready + ? `transform: translate3d(${geom.x}px, ${geom.y}px, 0); width: ${geom.w}px; height: ${geom.h}px;` + : 'opacity: 0;' + ); + const mergedStyle = $derived(composeInlineStyle(style, geometryStyle)); + onMount(() => { const el = ref; if (!el) return; @@ -78,8 +86,6 @@ diff --git a/src/uix/eidos/components/tabs/tabs.svelte b/src/uix/eidos/components/tabs/tabs.svelte index cdded1179..c7b366fef 100644 --- a/src/uix/eidos/components/tabs/tabs.svelte +++ b/src/uix/eidos/components/tabs/tabs.svelte @@ -22,7 +22,7 @@ */ import { ActiveEidos } from '$uix/eidos'; import * as SomaTabs from '$soma/components/tabs'; - import type { TabsProviderProps } from './types'; + import type { TabsProps } from './types'; import { setTabsEidosContext } from './tabs-context'; let { @@ -36,7 +36,7 @@ ref = $bindable(null), children, ...rest - }: TabsProviderProps = $props(); + }: TabsProps = $props(); const eidos = ActiveEidos.require(); const resolvedSize = $derived(eidos.resolve(size, 'md')); diff --git a/src/uix/eidos/components/tabs/types.ts b/src/uix/eidos/components/tabs/types.ts index 2b59568ae..ee055acd7 100644 --- a/src/uix/eidos/components/tabs/types.ts +++ b/src/uix/eidos/components/tabs/types.ts @@ -46,7 +46,7 @@ export type TabsMotionPreset = 'none' | 'fade' | 'slide'; */ export type TabsCollapseAt = 'never' | Breakpoint | number; -export type TabsProviderProps = SomaTabsProviderProps & { +export type TabsProps = SomaTabsProviderProps & { /** * Visual size preset. Resolved via `Eidos.resolve`. Pass either a single * value or a breakpoint map (`{ base: 'sm', md: 'md' }`). diff --git a/src/uix/eidos/components/toast/index.ts b/src/uix/eidos/components/toast/index.ts index d6e5a1c70..ffe0087fb 100644 --- a/src/uix/eidos/components/toast/index.ts +++ b/src/uix/eidos/components/toast/index.ts @@ -3,8 +3,8 @@ // Two distinct roots, NOT one wrapping the other: // // - `` — manual compound. Wraps Soma's Provider; consumer -// iterates `toaster.toasts` themselves. Children attached via -// Object.assign: Viewport, Item, Status, Main, Title, Description, +// iterates `toaster.toasts` themselves. Children attached explicitly: +// Viewport, Item, Status, Main, Title, Description, // Action, Close. // // - `` — imperative auto-mount. Renders the entire @@ -45,16 +45,28 @@ import Description from './toast-description.svelte'; import Action from './toast-action.svelte'; import Close from './toast-close.svelte'; -export const Toast = Object.assign(ToastRoot, { - Viewport, - Item, - Status, - Main, - Title, - Description, - Action, - Close -}); +type ToastNamespace = typeof ToastRoot & { + Viewport: typeof Viewport; + Item: typeof Item; + Status: typeof Status; + Main: typeof Main; + Title: typeof Title; + Description: typeof Description; + Action: typeof Action; + Close: typeof Close; +}; + +const Toast = ToastRoot as ToastNamespace; +Toast.Viewport = Viewport; +Toast.Item = Item; +Toast.Status = Status; +Toast.Main = Main; +Toast.Title = Title; +Toast.Description = Description; +Toast.Action = Action; +Toast.Close = Close; + +export { Toast }; export default Toast; @@ -73,7 +85,7 @@ export { export type { ToasterProps, - ToastProviderProps as ToastProps, + ToastProps, ToastViewportProps as ViewportProps, ToastItemProps as ItemProps, ToastStatusProps as StatusProps, diff --git a/src/uix/eidos/components/toast/toast-viewport.svelte b/src/uix/eidos/components/toast/toast-viewport.svelte index 4e7716037..753436645 100644 --- a/src/uix/eidos/components/toast/toast-viewport.svelte +++ b/src/uix/eidos/components/toast/toast-viewport.svelte @@ -5,38 +5,19 @@ * `--toast-toaster-gap` token inline. Forwards everything else to the * headless Viewport. */ + import { composeInlineStyle } from '$uix/eidos/lib/style'; import * as Toast from '$soma/components/toast'; import type { ToastViewportProps } from './types'; let { position = 'bottom-right', gap, style, children, ...rest }: ToastViewportProps = $props(); - function styleObjectToString(obj: Record): string { - return Object.entries(obj) - .filter(([, v]) => v !== undefined && v !== null) - .map(([k, v]) => `${k}: ${v};`) - .join(' '); - } - const gapStyle = $derived.by(() => { if (gap === undefined) return ''; const value = typeof gap === 'number' ? `${gap}px` : gap; return `--toast-toaster-gap: ${value};`; }); - // Preserve consumer-supplied style whether they pass a string or an - // object. Previously the object branch was silently dropped when `gap` - // was set, because we only read `typeof style === 'string'`. - const mergedStyle = $derived.by(() => { - const userStyle = - typeof style === 'string' - ? style - : style && typeof style === 'object' - ? styleObjectToString(style as Record) - : ''; - if (!gapStyle && !userStyle) return undefined; - if (!gapStyle) return userStyle; - return userStyle ? `${gapStyle} ${userStyle}` : gapStyle; - }); + const mergedStyle = $derived(composeInlineStyle(style, gapStyle)); diff --git a/src/uix/eidos/components/toast/toast.svelte b/src/uix/eidos/components/toast/toast.svelte index fccddc18a..017729bb7 100644 --- a/src/uix/eidos/components/toast/toast.svelte +++ b/src/uix/eidos/components/toast/toast.svelte @@ -20,9 +20,9 @@ * not a child). */ import * as SomaToast from '$soma/components/toast'; - import type { ToastProviderProps } from './types'; + import type { ToastProps } from './types'; - let { toaster, label, children, ...rest }: ToastProviderProps = $props(); + let { toaster, label, children, ...rest }: ToastProps = $props(); diff --git a/src/uix/eidos/components/toast/toaster.svelte b/src/uix/eidos/components/toast/toaster.svelte index f1e78a776..66bff9b12 100644 --- a/src/uix/eidos/components/toast/toaster.svelte +++ b/src/uix/eidos/components/toast/toaster.svelte @@ -1,6 +1,6 @@ - + {#each toaster.toasts as item (item.id)} @@ -70,4 +70,4 @@ {/each} - + diff --git a/src/uix/eidos/components/toast/types.ts b/src/uix/eidos/components/toast/types.ts index 86a102f14..bdc0fb71b 100644 --- a/src/uix/eidos/components/toast/types.ts +++ b/src/uix/eidos/components/toast/types.ts @@ -26,7 +26,7 @@ export type ToastPosition = | 'bottom-center' | 'bottom-right'; -export type ToastProviderProps = SomaToastProviderProps; +export type ToastProps = SomaToastProviderProps; export type ToastTitleProps = SomaToastTitleProps; export type ToastDescriptionProps = SomaToastDescriptionProps; export type ToastActionProps = SomaToastActionProps; @@ -56,7 +56,7 @@ export type ToastViewportProps = SomaToastViewportProps & { * (typically in the root layout), receives the `toaster` instance, and * renders Provider + Viewport + Item-loop with default content layout * (status icon, title, description, action, close). Use the compound - * `` / `` / `` shape when + * `` / `` / `` shape when * you need a non-default item template. */ export type ToasterProps = { diff --git a/src/uix/eidos/components/tooltip/index.ts b/src/uix/eidos/components/tooltip/index.ts index 5a77d59bc..78b4aeb76 100644 --- a/src/uix/eidos/components/tooltip/index.ts +++ b/src/uix/eidos/components/tooltip/index.ts @@ -20,18 +20,27 @@ import Content from './tooltip-content.svelte'; import Arrow from './tooltip-arrow.svelte'; import { Portal } from '$soma/components/internal'; -export const Tooltip = Object.assign(TooltipRoot, { - Group, - Trigger, - Portal, - Content, - Arrow -}); +type TooltipNamespace = typeof TooltipRoot & { + Group: typeof Group; + Trigger: typeof Trigger; + Portal: typeof Portal; + Content: typeof Content; + Arrow: typeof Arrow; +}; + +const Tooltip = TooltipRoot as TooltipNamespace; +Tooltip.Group = Group; +Tooltip.Trigger = Trigger; +Tooltip.Portal = Portal; +Tooltip.Content = Content; +Tooltip.Arrow = Arrow; + +export { Tooltip }; export default Tooltip; export type { - TooltipProviderProps as TooltipProps, + TooltipProps, TooltipGroupProps as GroupProps, TooltipTriggerProps as TriggerProps, TooltipContentProps as ContentProps, diff --git a/src/uix/eidos/components/tooltip/tooltip.svelte b/src/uix/eidos/components/tooltip/tooltip.svelte index b0283ae05..2c6477624 100644 --- a/src/uix/eidos/components/tooltip/tooltip.svelte +++ b/src/uix/eidos/components/tooltip/tooltip.svelte @@ -19,9 +19,9 @@ * tooltip to a sibling is instantaneous within the grace window. */ import * as SomaTooltip from '$soma/components/tooltip'; - import type { TooltipProviderProps } from './types'; + import type { TooltipProps } from './types'; - let { open = $bindable(false), children, ...rest }: TooltipProviderProps = $props(); + let { open = $bindable(false), children, ...rest }: TooltipProps = $props(); diff --git a/src/uix/eidos/components/tooltip/types.ts b/src/uix/eidos/components/tooltip/types.ts index 1182cdfd1..199a75756 100644 --- a/src/uix/eidos/components/tooltip/types.ts +++ b/src/uix/eidos/components/tooltip/types.ts @@ -24,7 +24,7 @@ export type TooltipSize = Extract; export type TooltipVariant = 'solid' | 'outline' | 'ghost'; export type TooltipGroupProps = SomaTooltipGroupProps; -export type TooltipProviderProps = SomaTooltipProviderProps; +export type TooltipProps = SomaTooltipProviderProps; export type TooltipTriggerProps = SomaTooltipTriggerProps; export type TooltipArrowProps = SomaTooltipArrowProps; diff --git a/src/uix/eidos/lib/style.ts b/src/uix/eidos/lib/style.ts new file mode 100644 index 000000000..a42a632d7 --- /dev/null +++ b/src/uix/eidos/lib/style.ts @@ -0,0 +1,17 @@ +type StyleRecord = Record; + +function serializeStyle(style: unknown): string { + if (!style) return ''; + if (typeof style === 'string') return style.trim(); + if (typeof style !== 'object') return ''; + + return Object.entries(style as StyleRecord) + .filter(([, value]) => value !== undefined && value !== null) + .map(([name, value]) => `${name}: ${value};`) + .join(' '); +} + +export function composeInlineStyle(...styles: unknown[]): string | undefined { + const parts = styles.map(serializeStyle).filter(Boolean); + return parts.length ? parts.join(' ') : undefined; +}