Normalize Eidos component surface

active-uix
dev 5 months ago
parent 9bde5b212e
commit 13cc14f971

@ -49,6 +49,39 @@ Arranque rapido para mañana:
6. Mantener fuera de commits los logs sueltos (`.codex-vite-*`, 6. Mantener fuera de commits los logs sueltos (`.codex-vite-*`,
`debug.log`) salvo que el usuario pida conservarlos. `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: Actualizacion 2026-05-16:
- Barrido de wrappers publicos Soma frente a providers cerrado: - Barrido de wrappers publicos Soma frente a providers cerrado:

@ -80,7 +80,7 @@ contexto headless, acepta los bindables del estado (`open`, `value`,
* to set up the component context. * to set up the component context.
*/ */
import { Provider as SomaDrawerProvider } from '$soma/components/drawer'; import { Provider as SomaDrawerProvider } from '$soma/components/drawer';
import type { DrawerProviderProps } from './types'; import type { DrawerProps } from './types';
let { let {
open = $bindable(false), open = $bindable(false),
@ -88,7 +88,7 @@ contexto headless, acepta los bindables del estado (`open`, `value`,
isDragging = $bindable(false), isDragging = $bindable(false),
children, children,
...rest ...rest
}: DrawerProviderProps = $props(); }: DrawerProps = $props();
</script> </script>
<SomaDrawerProvider {...rest} bind:open bind:activeSnapPoint bind:isDragging> <SomaDrawerProvider {...rest} bind:open bind:activeSnapPoint bind:isDragging>
@ -169,7 +169,7 @@ export { Drawer };
export default Drawer; export default Drawer;
export type { export type {
DrawerProviderProps as DrawerProps, DrawerProps,
DrawerTriggerProps as TriggerProps, DrawerTriggerProps as TriggerProps,
DrawerOverlayProps as OverlayProps, DrawerOverlayProps as OverlayProps,
DrawerContentProps as ContentProps DrawerContentProps as ContentProps
@ -187,7 +187,7 @@ import type {
// … // …
} from '$soma/components/drawer'; } from '$soma/components/drawer';
export type DrawerProviderProps = SomaDrawerProviderProps; export type DrawerProps = SomaDrawerProviderProps;
export type DrawerTriggerProps = SomaDrawerTriggerProps; export type DrawerTriggerProps = SomaDrawerTriggerProps;
// Eidos añade props visuales que el recipe consume vía data-attrs // Eidos añade props visuales que el recipe consume vía data-attrs

@ -18,7 +18,7 @@
*/ */
import * as SomaAccordion from '$soma/components/accordion'; import * as SomaAccordion from '$soma/components/accordion';
import { ActiveEidos } from '$uix/eidos'; import { ActiveEidos } from '$uix/eidos';
import type { AccordionProviderProps } from './types'; import type { AccordionProps } from './types';
import { setAccordionEidosContext } from './accordion-context'; import { setAccordionEidosContext } from './accordion-context';
let { let {
@ -27,7 +27,7 @@
variant = 'outline', variant = 'outline',
children, children,
...rest ...rest
}: AccordionProviderProps = $props(); }: AccordionProps = $props();
const eidos = ActiveEidos.require(); const eidos = ActiveEidos.require();
const resolvedSize = $derived(typeof size === 'string' ? size : eidos.resolve(size, 'md')); const resolvedSize = $derived(typeof size === 'string' ? size : eidos.resolve(size, 'md'));

@ -12,25 +12,33 @@
// </Accordion> // </Accordion>
// //
// `<Accordion>` IS the root (lives in accordion.svelte). Children // `<Accordion>` IS the root (lives in accordion.svelte). Children
// attached via Object.assign — bits-ui / shadcn-svelte pattern. No // are attached explicitly. No Provider exported publicly; no
// Provider exported publicly; no flat-with-snippets invention. // flat-with-snippets invention.
import AccordionRoot from './accordion.svelte'; import AccordionRoot from './accordion.svelte';
import Item from './accordion-item.svelte'; import Item from './accordion-item.svelte';
import Header from './accordion-header.svelte'; import Header from './accordion-header.svelte';
import Trigger from './accordion-trigger.svelte'; import Trigger from './accordion-trigger.svelte';
import Content from './accordion-content.svelte'; import Content from './accordion-content.svelte';
export const Accordion = Object.assign(AccordionRoot, { type AccordionNamespace = typeof AccordionRoot & {
Item, Item: typeof Item;
Header, Header: typeof Header;
Trigger, Trigger: typeof Trigger;
Content 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 default Accordion;
export type { export type {
AccordionProviderProps as AccordionProps, AccordionProps,
AccordionItemProps as ItemProps, AccordionItemProps as ItemProps,
AccordionHeaderProps as HeaderProps, AccordionHeaderProps as HeaderProps,
AccordionTriggerProps as TriggerProps, AccordionTriggerProps as TriggerProps,

@ -15,14 +15,14 @@ export type AccordionHeaderProps = SomaAccordionHeaderProps;
export type AccordionContentProps = SomaAccordionContentProps; export type AccordionContentProps = SomaAccordionContentProps;
/** /**
* Provider props in eidos accept `ResponsiveProp<AccordionSize>` for * Root props in eidos accept `ResponsiveProp<AccordionSize>` for
* the `size` axis. The wrapper resolves it via `Eidos.resolve` and * the `size` axis. The wrapper resolves it via `Eidos.resolve` and
* stamps `data-size`; headless never receives size/variant because those * stamps `data-size`; headless never receives size/variant because those
* axes are visual-only. Variant stays single-valued — picking outline * 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 * vs surface vs ghost on a per-breakpoint basis is not a use case worth
* exposing. * exposing.
*/ */
export type AccordionProviderProps = SomaAccordionProviderProps & { export type AccordionProps = SomaAccordionProviderProps & {
size?: ResponsiveProp<AccordionSize>; size?: ResponsiveProp<AccordionSize>;
variant?: AccordionVariant; variant?: AccordionVariant;
}; };

@ -9,7 +9,7 @@
* <Avatar.Fallback>JD</Avatar.Fallback> * <Avatar.Fallback>JD</Avatar.Fallback>
* </Avatar> * </Avatar>
*/ */
import type { AvatarProviderProps } from './types'; import type { AvatarProps } from './types';
let { let {
size = 'md', size = 'md',
@ -18,7 +18,7 @@
radius = 'full', radius = 'full',
children, children,
...restProps ...restProps
}: AvatarProviderProps = $props(); }: AvatarProps = $props();
</script> </script>
<span <span

@ -14,15 +14,21 @@ import AvatarRoot from './avatar.svelte';
import Image from './avatar-image.svelte'; import Image from './avatar-image.svelte';
import Fallback from './avatar-fallback.svelte'; import Fallback from './avatar-fallback.svelte';
export const Avatar = Object.assign(AvatarRoot, { type AvatarNamespace = typeof AvatarRoot & {
Image, Image: typeof Image;
Fallback Fallback: typeof Fallback;
}); };
const Avatar = AvatarRoot as AvatarNamespace;
Avatar.Image = Image;
Avatar.Fallback = Fallback;
export { Avatar };
export default Avatar; export default Avatar;
export type { export type {
AvatarProviderProps as AvatarProps, AvatarProps,
AvatarImageProps as ImageProps, AvatarImageProps as ImageProps,
AvatarFallbackProps as FallbackProps, AvatarFallbackProps as FallbackProps,
AvatarSize, AvatarSize,

@ -15,7 +15,7 @@ export type AvatarColor =
| 'loss'; | 'loss';
export type AvatarRadius = 'full' | 'md' | 'sm' | 'none'; export type AvatarRadius = 'full' | 'md' | 'sm' | 'none';
export type AvatarProviderProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & { export type AvatarProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
/** Visual size token. @default 'md' */ /** Visual size token. @default 'md' */
size?: AvatarSize; size?: AvatarSize;
/** Visual variant. @default 'soft' */ /** Visual variant. @default 'soft' */

@ -7,16 +7,22 @@
* - horizontal → flex row (or grid with auto cols if columns set) * - horizontal → flex row (or grid with auto cols if columns set)
*/ */
import * as Checkbox from '$soma/components/checkbox'; import * as Checkbox from '$soma/components/checkbox';
import { composeInlineStyle } from '$uix/eidos/lib/style';
import type { CheckboxGroupProps } from './types'; 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));
</script> </script>
<Checkbox.Group <Checkbox.Group
{...rest} {...rest}
bind:value bind:value
data-columns={columns ?? undefined} data-columns={columns ?? undefined}
style={columns ? `--_checkbox-group-columns: ${columns};` : undefined} style={mergedStyle}
> >
{@render children?.()} {@render children?.()}
</Checkbox.Group> </Checkbox.Group>

@ -12,7 +12,7 @@
*/ */
import { ActiveEidos } from '$uix/eidos'; import { ActiveEidos } from '$uix/eidos';
import * as SomaCheckbox from '$soma/components/checkbox'; import * as SomaCheckbox from '$soma/components/checkbox';
import type { CheckboxProviderProps } from './types'; import type { CheckboxProps } from './types';
let { let {
size = 'md', size = 'md',
@ -22,7 +22,7 @@
indeterminate = $bindable(false), indeterminate = $bindable(false),
children: bodyContent, children: bodyContent,
...rest ...rest
}: CheckboxProviderProps = $props(); }: CheckboxProps = $props();
const eidos = ActiveEidos.require(); const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md')); const resolvedSize = $derived(eidos.resolve(size, 'md'));

@ -18,17 +18,25 @@ import HiddenInput from './checkbox-hidden-input.svelte';
import Group from './checkbox-group.svelte'; import Group from './checkbox-group.svelte';
import GroupLabel from './checkbox-group-label.svelte'; import GroupLabel from './checkbox-group-label.svelte';
export const Checkbox = Object.assign(CheckboxRoot, { type CheckboxNamespace = typeof CheckboxRoot & {
Indicator, Indicator: typeof Indicator;
HiddenInput, HiddenInput: typeof HiddenInput;
Group, Group: typeof Group;
GroupLabel 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 default Checkbox;
export type { export type {
CheckboxProviderProps as CheckboxProps, CheckboxProps,
CheckboxIndicatorProps as IndicatorProps, CheckboxIndicatorProps as IndicatorProps,
CheckboxHiddenInputProps as HiddenInputProps, CheckboxHiddenInputProps as HiddenInputProps,
CheckboxGroupProps as GroupProps, CheckboxGroupProps as GroupProps,

@ -44,7 +44,7 @@ export type CheckboxIconSnippetArgs = {
indeterminate: boolean; indeterminate: boolean;
}; };
export type CheckboxProviderProps = SomaCheckboxProviderProps & { export type CheckboxProps = SomaCheckboxProviderProps & {
/** Visual size preset. Resolved via `Eidos.resolve`. @default 'md' */ /** Visual size preset. Resolved via `Eidos.resolve`. @default 'md' */
size?: ResponsiveProp<CheckboxSize>; size?: ResponsiveProp<CheckboxSize>;
/** Visual variant. @default 'solid' */ /** Visual variant. @default 'solid' */

@ -10,10 +10,16 @@ import CollapsibleRoot from './collapsible.svelte';
import Trigger from './collapsible-trigger.svelte'; import Trigger from './collapsible-trigger.svelte';
import Content from './collapsible-content.svelte'; import Content from './collapsible-content.svelte';
export const Collapsible = Object.assign(CollapsibleRoot, { type CollapsibleNamespace = typeof CollapsibleRoot & {
Trigger, Trigger: typeof Trigger;
Content Content: typeof Content;
}); };
const Collapsible = CollapsibleRoot as CollapsibleNamespace;
Collapsible.Trigger = Trigger;
Collapsible.Content = Content;
export { Collapsible };
export default Collapsible; export default Collapsible;

@ -6,6 +6,7 @@
* inline `--dialog-overlay-opacity` custom property so the recipe's * inline `--dialog-overlay-opacity` custom property so the recipe's
* `color-mix` picks it up without a class swap. * `color-mix` picks it up without a class swap.
*/ */
import { composeInlineStyle } from '$uix/eidos/lib/style';
import * as Dialog from '$soma/components/dialog'; import * as Dialog from '$soma/components/dialog';
import type { DialogOverlayProps } from './types'; import type { DialogOverlayProps } from './types';
@ -23,12 +24,10 @@
const resolvedOpacity = $derived(serializeOpacity(overlayOpacity)); const resolvedOpacity = $derived(serializeOpacity(overlayOpacity));
const composedStyle = $derived( const composedStyle = $derived(
[style, resolvedOpacity ? `--dialog-overlay-opacity:${resolvedOpacity}` : ''] composeInlineStyle(style, resolvedOpacity ? `--dialog-overlay-opacity: ${resolvedOpacity};` : undefined)
.filter(Boolean)
.join(';')
); );
</script> </script>
<Dialog.Overlay {...rest} style={composedStyle || undefined}> <Dialog.Overlay {...rest} style={composedStyle}>
{@render children?.()} {@render children?.()}
</Dialog.Overlay> </Dialog.Overlay>

@ -20,9 +20,9 @@
* </Dialog> * </Dialog>
*/ */
import * as SomaDialog from '$soma/components/dialog'; 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();
</script> </script>
<SomaDialog.Provider {...rest} bind:open> <SomaDialog.Provider {...rest} bind:open>

@ -27,22 +27,35 @@ import Header from './dialog-header.svelte';
import Footer from './dialog-footer.svelte'; import Footer from './dialog-footer.svelte';
import { Portal } from '$soma/components/internal'; import { Portal } from '$soma/components/internal';
export const Dialog = Object.assign(DialogRoot, { type DialogNamespace = typeof DialogRoot & {
Trigger, Trigger: typeof Trigger;
Portal, Portal: typeof Portal;
Overlay, Overlay: typeof Overlay;
Content, Content: typeof Content;
Title, Title: typeof Title;
Description, Description: typeof Description;
Close, Close: typeof Close;
Header, Header: typeof Header;
Footer 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 default Dialog;
export type { export type {
DialogProviderProps as DialogProps, DialogProps,
DialogTriggerProps as TriggerProps, DialogTriggerProps as TriggerProps,
DialogOverlayProps as OverlayProps, DialogOverlayProps as OverlayProps,
DialogContentProps as ContentProps, DialogContentProps as ContentProps,

@ -27,7 +27,7 @@ export type DialogSize = Extract<Size, 'sm' | 'md' | 'lg' | 'xl' | 'full'>;
*/ */
export type DialogPosition = Position; export type DialogPosition = Position;
export type DialogProviderProps = SomaDialogProviderProps; export type DialogProps = SomaDialogProviderProps;
export type DialogTriggerProps = SomaDialogTriggerProps; export type DialogTriggerProps = SomaDialogTriggerProps;
export type DialogContentProps = SomaDialogContentProps & { export type DialogContentProps = SomaDialogContentProps & {

@ -11,6 +11,7 @@
* wrapper still mounts the headless part for consistency, but it has no * wrapper still mounts the headless part for consistency, but it has no
* visual effect. * visual effect.
*/ */
import { composeInlineStyle } from '$uix/eidos/lib/style';
import * as Drawer from '$soma/components/drawer'; import * as Drawer from '$soma/components/drawer';
import type { DrawerOverlayProps } from './types'; import type { DrawerOverlayProps } from './types';
@ -28,12 +29,10 @@
const resolvedOpacity = $derived(serializeOpacity(overlayOpacity)); const resolvedOpacity = $derived(serializeOpacity(overlayOpacity));
const composedStyle = $derived( const composedStyle = $derived(
[style, resolvedOpacity ? `--drawer-overlay-opacity:${resolvedOpacity}` : ''] composeInlineStyle(style, resolvedOpacity ? `--drawer-overlay-opacity: ${resolvedOpacity};` : undefined)
.filter(Boolean)
.join(';')
); );
</script> </script>
<Drawer.Overlay {...rest} style={composedStyle || undefined}> <Drawer.Overlay {...rest} style={composedStyle}>
{@render children?.()} {@render children?.()}
</Drawer.Overlay> </Drawer.Overlay>

@ -13,7 +13,7 @@
* </Drawer> * </Drawer>
*/ */
import { Provider as SomaDrawerProvider } from '$soma/components/drawer'; import { Provider as SomaDrawerProvider } from '$soma/components/drawer';
import type { DrawerProviderProps } from './types'; import type { DrawerProps } from './types';
let { let {
open = $bindable(false), open = $bindable(false),
@ -21,7 +21,7 @@
isDragging = $bindable(false), isDragging = $bindable(false),
children, children,
...rest ...rest
}: DrawerProviderProps = $props(); }: DrawerProps = $props();
</script> </script>
<SomaDrawerProvider {...rest} bind:open bind:activeSnapPoint bind:isDragging> <SomaDrawerProvider {...rest} bind:open bind:activeSnapPoint bind:isDragging>

@ -18,9 +18,9 @@
// </Drawer> // </Drawer>
// //
// `<Drawer>` IS the root (lives in drawer.svelte, wraps Soma's // `<Drawer>` IS the root (lives in drawer.svelte, wraps Soma's
// Drawer.Provider). All child parts are attached as named members via // Drawer.Provider). All child parts are attached as named members
// Object.assign — bits-ui / shadcn-svelte pattern. No Provider exported // explicitly. No Provider exported publicly; no flat-with-snippet-slots
// publicly; no flat-with-snippet-slots invention. Children naming // invention. Children naming
// follows air / headless: Trigger, Content, Overlay, Title, Description, // follows air / headless: Trigger, Content, Overlay, Title, Description,
// Close, Portal (+ Header, Footer as eidos-only layout shells, and // Close, Portal (+ Header, Footer as eidos-only layout shells, and
// Handle inherited from Soma's drag affordance). // Handle inherited from Soma's drag affordance).
@ -36,9 +36,9 @@ import Header from './drawer-header.svelte';
import Footer from './drawer-footer.svelte'; import Footer from './drawer-footer.svelte';
import { Portal } from '$soma/components/internal'; 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 // to avoid a Svelte 5 hydration quirk where mutating the component
// constructor with bulk Object.assign caused `<Drawer.Trigger>` children // constructor in bulk caused `<Drawer.Trigger>` children
// to re-mount on hydration (button appeared, then disappeared). // to re-mount on hydration (button appeared, then disappeared).
type DrawerNamespace = typeof DrawerRoot & { type DrawerNamespace = typeof DrawerRoot & {
Trigger: typeof Trigger; Trigger: typeof Trigger;
@ -69,7 +69,7 @@ export { Drawer };
export default Drawer; export default Drawer;
export type { export type {
DrawerProviderProps as DrawerProps, DrawerProps,
DrawerTriggerProps as TriggerProps, DrawerTriggerProps as TriggerProps,
DrawerOverlayProps as OverlayProps, DrawerOverlayProps as OverlayProps,
DrawerContentProps as ContentProps, DrawerContentProps as ContentProps,

@ -24,7 +24,7 @@ export type DrawerSize = Extract<Size, 'sm' | 'md' | 'lg' | 'full'>;
export type { DrawerVariant, DrawerDirection, DrawerSide }; export type { DrawerVariant, DrawerDirection, DrawerSide };
export type DrawerProviderProps = SomaDrawerProviderProps; export type DrawerProps = SomaDrawerProviderProps;
export type DrawerTriggerProps = SomaDrawerTriggerProps; export type DrawerTriggerProps = SomaDrawerTriggerProps;
export type DrawerHandleProps = SomaDrawerHandleProps; export type DrawerHandleProps = SomaDrawerHandleProps;
export type DrawerTitleProps = SomaDrawerTitleProps; export type DrawerTitleProps = SomaDrawerTitleProps;

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { composeInlineStyle } from '$uix/eidos/lib/style';
import type { IconProps } from './types'; import type { IconProps } from './types';
import { resolveIconSize, resolveIconStrokeWidth } from './create-icon'; import { resolveIconSize, resolveIconStrokeWidth } from './create-icon';
@ -16,10 +17,9 @@
const resolvedSize = $derived(resolveIconSize(size)); const resolvedSize = $derived(resolveIconSize(size));
const resolvedStrokeWidth = $derived(resolveIconStrokeWidth(strokeWidth, size)); const resolvedStrokeWidth = $derived(resolveIconStrokeWidth(strokeWidth, size));
const mergedStyle = $derived.by(() => { const mergedStyle = $derived(
const baseStyle = `width:${resolvedSize};height:${resolvedSize};`; composeInlineStyle(`width: ${resolvedSize}; height: ${resolvedSize};`, style)
return style ? `${baseStyle}${style}` : baseStyle; );
});
</script> </script>
<svg <svg

@ -21,20 +21,31 @@ import Anchor from './popover-anchor.svelte';
import Overlay from './popover-overlay.svelte'; import Overlay from './popover-overlay.svelte';
import { Portal } from '$soma/components/internal'; import { Portal } from '$soma/components/internal';
export const Popover = Object.assign(PopoverRoot, { type PopoverNamespace = typeof PopoverRoot & {
Trigger, Trigger: typeof Trigger;
Portal, Portal: typeof Portal;
Anchor, Anchor: typeof Anchor;
Overlay, Overlay: typeof Overlay;
Content, Content: typeof Content;
Arrow, Arrow: typeof Arrow;
Close Close: typeof Close;
}); };
const Popover = PopoverRoot as PopoverNamespace;
Popover.Trigger = Trigger;
Popover.Portal = Portal;
Popover.Anchor = Anchor;
Popover.Overlay = Overlay;
Popover.Content = Content;
Popover.Arrow = Arrow;
Popover.Close = Close;
export { Popover };
export default Popover; export default Popover;
export type { export type {
PopoverProviderProps as PopoverProps, PopoverProps,
PopoverTriggerProps as TriggerProps, PopoverTriggerProps as TriggerProps,
PopoverContentProps as ContentProps, PopoverContentProps as ContentProps,
PopoverArrowProps as ArrowProps, PopoverArrowProps as ArrowProps,

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
/** /**
* Eidos `<Popover.Overlay>` — passthrough wrapper for the headless * Eidos `<Popover.Overlay>` — passthrough wrapper for the headless
* Overlay part. Only meaningful for MODAL popovers (`<Popover.Provider * Overlay part. Only meaningful for modal `<Popover modal>` roots; the
* modal>`); the recipe handles backdrop styling. * recipe handles backdrop styling.
*/ */
import * as Popover from '$soma/components/popover'; import * as Popover from '$soma/components/popover';
import type { PopoverOverlayProps } from './types'; import type { PopoverOverlayProps } from './types';

@ -16,9 +16,9 @@
* </Popover> * </Popover>
*/ */
import * as SomaPopover from '$soma/components/popover'; 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();
</script> </script>
<SomaPopover.Provider {...rest} bind:open> <SomaPopover.Provider {...rest} bind:open>

@ -16,7 +16,7 @@ import type {
*/ */
export type PopoverSize = Extract<Size, 'sm' | 'md' | 'lg' | 'xl'>; export type PopoverSize = Extract<Size, 'sm' | 'md' | 'lg' | 'xl'>;
export type PopoverProviderProps = SomaPopoverProviderProps; export type PopoverProps = SomaPopoverProviderProps;
export type PopoverTriggerProps = SomaPopoverTriggerProps; export type PopoverTriggerProps = SomaPopoverTriggerProps;
export type PopoverArrowProps = SomaPopoverArrowProps; export type PopoverArrowProps = SomaPopoverArrowProps;
export type PopoverCloseProps = SomaPopoverCloseProps; export type PopoverCloseProps = SomaPopoverCloseProps;

@ -19,17 +19,25 @@ import Indicator from './radio-group-indicator.svelte';
import HiddenInput from './radio-group-hidden-input.svelte'; import HiddenInput from './radio-group-hidden-input.svelte';
import Label from './radio-group-label.svelte'; import Label from './radio-group-label.svelte';
export const RadioGroup = Object.assign(RadioGroupRoot, { type RadioGroupNamespace = typeof RadioGroupRoot & {
Item, Item: typeof Item;
Indicator, Indicator: typeof Indicator;
HiddenInput, HiddenInput: typeof HiddenInput;
Label 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 default RadioGroup;
export type { export type {
RadioGroupProviderProps as RadioGroupProps, RadioGroupProps,
RadioGroupItemProps as ItemProps, RadioGroupItemProps as ItemProps,
RadioGroupIndicatorProps as IndicatorProps, RadioGroupIndicatorProps as IndicatorProps,
RadioGroupHiddenInputProps as HiddenInputProps, RadioGroupHiddenInputProps as HiddenInputProps,

@ -14,8 +14,9 @@
* </RadioGroup> * </RadioGroup>
*/ */
import { ActiveEidos } from '$uix/eidos'; import { ActiveEidos } from '$uix/eidos';
import { composeInlineStyle } from '$uix/eidos/lib/style';
import * as SomaRadioGroup from '$soma/components/radio-group'; import * as SomaRadioGroup from '$soma/components/radio-group';
import type { RadioGroupProviderProps } from './types'; import type { RadioGroupProps } from './types';
let { let {
size = 'md', size = 'md',
@ -23,12 +24,17 @@
color = 'primary', color = 'primary',
columns, columns,
value = $bindable(''), value = $bindable(''),
style,
children, children,
...rest ...rest
}: RadioGroupProviderProps = $props(); }: RadioGroupProps = $props();
const eidos = ActiveEidos.require(); const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md')); const resolvedSize = $derived(eidos.resolve(size, 'md'));
const columnsStyle = $derived(
columns ? `--_radio-group-columns: ${columns};` : undefined
);
const mergedStyle = $derived(composeInlineStyle(style, columnsStyle));
</script> </script>
<SomaRadioGroup.Provider <SomaRadioGroup.Provider
@ -38,7 +44,7 @@
data-variant={variant} data-variant={variant}
data-color={color} data-color={color}
data-columns={columns ?? undefined} data-columns={columns ?? undefined}
style={columns ? `--_radio-group-columns: ${columns};` : undefined} style={mergedStyle}
> >
{@render children?.()} {@render children?.()}
</SomaRadioGroup.Provider> </SomaRadioGroup.Provider>

@ -41,7 +41,7 @@ export type RadioGroupIconSnippetArgs = {
checked: boolean; checked: boolean;
}; };
export type RadioGroupProviderProps = SomaRadioGroupProviderProps & { export type RadioGroupProps = SomaRadioGroupProviderProps & {
/** Visual size preset. Resolved via `Eidos.resolve`. @default 'md' */ /** Visual size preset. Resolved via `Eidos.resolve`. @default 'md' */
size?: ResponsiveProp<RadioGroupSize>; size?: ResponsiveProp<RadioGroupSize>;
/** Visual variant. @default 'solid' */ /** Visual variant. @default 'solid' */

@ -10,6 +10,7 @@
* `pathLength="100"` so the dasharray maths stays consumer-readable * `pathLength="100"` so the dasharray maths stays consumer-readable
* regardless of the path's actual length in user units. * regardless of the path's actual length in user units.
*/ */
import { composeInlineStyle } from '$uix/eidos/lib/style';
import type { SvgPrimitiveProps } from './types'; import type { SvgPrimitiveProps } from './types';
let { let {
@ -20,15 +21,7 @@
}: SvgPrimitiveProps = $props(); }: SvgPrimitiveProps = $props();
const dim = $derived(typeof size === 'number' ? `${size}px` : size); const dim = $derived(typeof size === 'number' ? `${size}px` : size);
const mergedStyle = $derived( const mergedStyle = $derived(composeInlineStyle(`width: ${dim}; height: ${dim};`, style));
[
`width: ${dim}`,
`height: ${dim}`,
typeof style === 'string' ? style : ''
]
.filter(Boolean)
.join('; ')
);
</script> </script>
<svg <svg

@ -12,6 +12,7 @@
* prop, because the prop sets the BASELINE direction and the recipe * prop, because the prop sets the BASELINE direction and the recipe
* sets the dynamic one. * sets the dynamic one.
*/ */
import { composeInlineStyle } from '$uix/eidos/lib/style';
import type { SvgPrimitiveProps, SvgChevronDirection } from './types'; import type { SvgPrimitiveProps, SvgChevronDirection } from './types';
type Props = SvgPrimitiveProps & { type Props = SvgPrimitiveProps & {
@ -37,14 +38,11 @@
const dim = $derived(typeof size === 'number' ? `${size}px` : size); const dim = $derived(typeof size === 'number' ? `${size}px` : size);
const rotation = $derived(ROTATION[direction]); const rotation = $derived(ROTATION[direction]);
const mergedStyle = $derived( const mergedStyle = $derived(
[ composeInlineStyle(
`width: ${dim}`, `width: ${dim}; height: ${dim};`,
`height: ${dim}`, rotation !== 0 ? `transform: rotate(${rotation}deg);` : undefined,
rotation !== 0 ? `transform: rotate(${rotation}deg)` : '', style
typeof style === 'string' ? style : '' )
]
.filter(Boolean)
.join('; ')
); );
</script> </script>

@ -8,6 +8,7 @@
* recipes can animate via `stroke-dashoffset` without measuring the * recipes can animate via `stroke-dashoffset` without measuring the
* actual path length. * actual path length.
*/ */
import { composeInlineStyle } from '$uix/eidos/lib/style';
import type { SvgPrimitiveProps } from './types'; import type { SvgPrimitiveProps } from './types';
let { let {
@ -18,15 +19,7 @@
}: SvgPrimitiveProps = $props(); }: SvgPrimitiveProps = $props();
const dim = $derived(typeof size === 'number' ? `${size}px` : size); const dim = $derived(typeof size === 'number' ? `${size}px` : size);
const mergedStyle = $derived( const mergedStyle = $derived(composeInlineStyle(`width: ${dim}; height: ${dim};`, style));
[
`width: ${dim}`,
`height: ${dim}`,
typeof style === 'string' ? style : ''
]
.filter(Boolean)
.join('; ')
);
</script> </script>
<svg <svg

@ -8,6 +8,7 @@
* viewBox so the recipe can scale it via `transform: scale(...)` from * viewBox so the recipe can scale it via `transform: scale(...)` from
* a small radius to full to animate the "select" moment. * a small radius to full to animate the "select" moment.
*/ */
import { composeInlineStyle } from '$uix/eidos/lib/style';
import type { SvgPrimitiveProps } from './types'; import type { SvgPrimitiveProps } from './types';
let { let {
@ -18,15 +19,7 @@
}: SvgPrimitiveProps = $props(); }: SvgPrimitiveProps = $props();
const dim = $derived(typeof size === 'number' ? `${size}px` : size); const dim = $derived(typeof size === 'number' ? `${size}px` : size);
const mergedStyle = $derived( const mergedStyle = $derived(composeInlineStyle(`width: ${dim}; height: ${dim};`, style));
[
`width: ${dim}`,
`height: ${dim}`,
typeof style === 'string' ? style : ''
]
.filter(Boolean)
.join('; ')
);
</script> </script>
<svg <svg

@ -17,17 +17,25 @@ import Trigger from './tabs-trigger.svelte';
import Content from './tabs-content.svelte'; import Content from './tabs-content.svelte';
import Indicator from './tabs-indicator.svelte'; import Indicator from './tabs-indicator.svelte';
export const Tabs = Object.assign(TabsRoot, { type TabsNamespace = typeof TabsRoot & {
List, List: typeof List;
Trigger, Trigger: typeof Trigger;
Content, Content: typeof Content;
Indicator Indicator: typeof Indicator;
}); };
const Tabs = TabsRoot as TabsNamespace;
Tabs.List = List;
Tabs.Trigger = Trigger;
Tabs.Content = Content;
Tabs.Indicator = Indicator;
export { Tabs };
export default Tabs; export default Tabs;
export type { export type {
TabsProviderProps as TabsProps, TabsProps,
TabsListProps as ListProps, TabsListProps as ListProps,
TabsTriggerProps as TriggerProps, TabsTriggerProps as TriggerProps,
TabsContentProps as ContentProps, TabsContentProps as ContentProps,

@ -16,10 +16,11 @@
* snaps to the active trigger). * snaps to the active trigger).
*/ */
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { composeInlineStyle } from '$uix/eidos/lib/style';
import * as Tabs from '$soma/components/tabs'; import * as Tabs from '$soma/components/tabs';
import type { TabsIndicatorProps } from './types'; import type { TabsIndicatorProps } from './types';
let { ref = $bindable<HTMLElement | null>(null), ...rest }: TabsIndicatorProps = $props(); let { ref = $bindable<HTMLElement | null>(null), style, ...rest }: TabsIndicatorProps = $props();
let geom = $state({ x: 0, y: 0, w: 0, h: 0, ready: false }); let geom = $state({ x: 0, y: 0, w: 0, h: 0, ready: false });
let rafId: number | null = null; 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(() => { onMount(() => {
const el = ref; const el = ref;
if (!el) return; if (!el) return;
@ -78,8 +86,6 @@
<Tabs.Indicator <Tabs.Indicator
{...rest} {...rest}
bind:ref bind:ref
style={geom.ready style={mergedStyle}
? `transform: translate3d(${geom.x}px, ${geom.y}px, 0); width: ${geom.w}px; height: ${geom.h}px;`
: 'opacity: 0;'}
data-ready={geom.ready ? '' : undefined} data-ready={geom.ready ? '' : undefined}
/> />

@ -22,7 +22,7 @@
*/ */
import { ActiveEidos } from '$uix/eidos'; import { ActiveEidos } from '$uix/eidos';
import * as SomaTabs from '$soma/components/tabs'; import * as SomaTabs from '$soma/components/tabs';
import type { TabsProviderProps } from './types'; import type { TabsProps } from './types';
import { setTabsEidosContext } from './tabs-context'; import { setTabsEidosContext } from './tabs-context';
let { let {
@ -36,7 +36,7 @@
ref = $bindable<HTMLElement | null>(null), ref = $bindable<HTMLElement | null>(null),
children, children,
...rest ...rest
}: TabsProviderProps = $props(); }: TabsProps = $props();
const eidos = ActiveEidos.require(); const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md')); const resolvedSize = $derived(eidos.resolve(size, 'md'));

@ -46,7 +46,7 @@ export type TabsMotionPreset = 'none' | 'fade' | 'slide';
*/ */
export type TabsCollapseAt = 'never' | Breakpoint | number; 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 * Visual size preset. Resolved via `Eidos.resolve`. Pass either a single
* value or a breakpoint map (`{ base: 'sm', md: 'md' }`). * value or a breakpoint map (`{ base: 'sm', md: 'md' }`).

@ -3,8 +3,8 @@
// Two distinct roots, NOT one wrapping the other: // Two distinct roots, NOT one wrapping the other:
// //
// - `<Toast>` — manual compound. Wraps Soma's Provider; consumer // - `<Toast>` — manual compound. Wraps Soma's Provider; consumer
// iterates `toaster.toasts` themselves. Children attached via // iterates `toaster.toasts` themselves. Children attached explicitly:
// Object.assign: Viewport, Item, Status, Main, Title, Description, // Viewport, Item, Status, Main, Title, Description,
// Action, Close. // Action, Close.
// //
// - `<Toaster />` — imperative auto-mount. Renders the entire // - `<Toaster />` — imperative auto-mount. Renders the entire
@ -45,16 +45,28 @@ import Description from './toast-description.svelte';
import Action from './toast-action.svelte'; import Action from './toast-action.svelte';
import Close from './toast-close.svelte'; import Close from './toast-close.svelte';
export const Toast = Object.assign(ToastRoot, { type ToastNamespace = typeof ToastRoot & {
Viewport, Viewport: typeof Viewport;
Item, Item: typeof Item;
Status, Status: typeof Status;
Main, Main: typeof Main;
Title, Title: typeof Title;
Description, Description: typeof Description;
Action, Action: typeof Action;
Close 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; export default Toast;
@ -73,7 +85,7 @@ export {
export type { export type {
ToasterProps, ToasterProps,
ToastProviderProps as ToastProps, ToastProps,
ToastViewportProps as ViewportProps, ToastViewportProps as ViewportProps,
ToastItemProps as ItemProps, ToastItemProps as ItemProps,
ToastStatusProps as StatusProps, ToastStatusProps as StatusProps,

@ -5,38 +5,19 @@
* `--toast-toaster-gap` token inline. Forwards everything else to the * `--toast-toaster-gap` token inline. Forwards everything else to the
* headless Viewport. * headless Viewport.
*/ */
import { composeInlineStyle } from '$uix/eidos/lib/style';
import * as Toast from '$soma/components/toast'; import * as Toast from '$soma/components/toast';
import type { ToastViewportProps } from './types'; import type { ToastViewportProps } from './types';
let { position = 'bottom-right', gap, style, children, ...rest }: ToastViewportProps = $props(); let { position = 'bottom-right', gap, style, children, ...rest }: ToastViewportProps = $props();
function styleObjectToString(obj: Record<string, string | number | undefined>): string {
return Object.entries(obj)
.filter(([, v]) => v !== undefined && v !== null)
.map(([k, v]) => `${k}: ${v};`)
.join(' ');
}
const gapStyle = $derived.by(() => { const gapStyle = $derived.by(() => {
if (gap === undefined) return ''; if (gap === undefined) return '';
const value = typeof gap === 'number' ? `${gap}px` : gap; const value = typeof gap === 'number' ? `${gap}px` : gap;
return `--toast-toaster-gap: ${value};`; return `--toast-toaster-gap: ${value};`;
}); });
// Preserve consumer-supplied style whether they pass a string or an const mergedStyle = $derived(composeInlineStyle(style, gapStyle));
// 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<string, string | number | undefined>)
: '';
if (!gapStyle && !userStyle) return undefined;
if (!gapStyle) return userStyle;
return userStyle ? `${gapStyle} ${userStyle}` : gapStyle;
});
</script> </script>
<Toast.Viewport data-position={position} style={mergedStyle} {...rest}> <Toast.Viewport data-position={position} style={mergedStyle} {...rest}>

@ -20,9 +20,9 @@
* not a child). * not a child).
*/ */
import * as SomaToast from '$soma/components/toast'; 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();
</script> </script>
<SomaToast.Provider {toaster} {label} {...rest}> <SomaToast.Provider {toaster} {label} {...rest}>

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
/** /**
* Eidos flat `<Toaster>` — the ergonomic 90 % case. * Eidos auto-mount `<Toaster>` — the ergonomic 90 % case.
* *
* <Toaster {toaster} position="top-right" /> * <Toaster {toaster} position="top-right" />
* *
@ -8,14 +8,14 @@
* (status disc fed by `<IntentIcon>`, title, description, action, * (status disc fed by `<IntentIcon>`, title, description, action,
* close). For exotic shapes — custom card layout, multiple toasters, * close). For exotic shapes — custom card layout, multiple toasters,
* compound trees in different DOM subtrees — the consumer drops down * compound trees in different DOM subtrees — the consumer drops down
* to `<Toast.Provider> / <Toast.Viewport> / <Toast.Item>` directly. * to `<Toast> / <Toast.Viewport> / <Toast.Item>` directly.
* *
* The intent → glyph mapping lives in * The intent → glyph mapping lives in
* `$uix/eidos/components/icon/intent.ts` so any other surface that * `$uix/eidos/components/icon/intent.ts` so any other surface that
* needs intent icons (Dialog, Field, Banner) reads from the same * needs intent icons (Dialog, Field, Banner) reads from the same
* doctrinal source. * doctrinal source.
*/ */
import Provider from './toast.svelte'; import ToastRoot from './toast.svelte';
import Viewport from './toast-viewport.svelte'; import Viewport from './toast-viewport.svelte';
import Item from './toast-item.svelte'; import Item from './toast-item.svelte';
import Status from './toast-status.svelte'; import Status from './toast-status.svelte';
@ -36,7 +36,7 @@
}: ToasterProps = $props(); }: ToasterProps = $props();
</script> </script>
<Provider {toaster} {label}> <ToastRoot {toaster} {label}>
<Viewport {position} {gap}> <Viewport {position} {gap}>
{#each toaster.toasts as item (item.id)} {#each toaster.toasts as item (item.id)}
<Item toast={item}> <Item toast={item}>
@ -70,4 +70,4 @@
</Item> </Item>
{/each} {/each}
</Viewport> </Viewport>
</Provider> </ToastRoot>

@ -26,7 +26,7 @@ export type ToastPosition =
| 'bottom-center' | 'bottom-center'
| 'bottom-right'; | 'bottom-right';
export type ToastProviderProps = SomaToastProviderProps; export type ToastProps = SomaToastProviderProps;
export type ToastTitleProps = SomaToastTitleProps; export type ToastTitleProps = SomaToastTitleProps;
export type ToastDescriptionProps = SomaToastDescriptionProps; export type ToastDescriptionProps = SomaToastDescriptionProps;
export type ToastActionProps = SomaToastActionProps; export type ToastActionProps = SomaToastActionProps;
@ -56,7 +56,7 @@ export type ToastViewportProps = SomaToastViewportProps & {
* (typically in the root layout), receives the `toaster` instance, and * (typically in the root layout), receives the `toaster` instance, and
* renders Provider + Viewport + Item-loop with default content layout * renders Provider + Viewport + Item-loop with default content layout
* (status icon, title, description, action, close). Use the compound * (status icon, title, description, action, close). Use the compound
* `<Toast.Provider>` / `<Toast.Viewport>` / `<Toast.Item>` shape when * `<Toast>` / `<Toast.Viewport>` / `<Toast.Item>` shape when
* you need a non-default item template. * you need a non-default item template.
*/ */
export type ToasterProps = { export type ToasterProps = {

@ -20,18 +20,27 @@ import Content from './tooltip-content.svelte';
import Arrow from './tooltip-arrow.svelte'; import Arrow from './tooltip-arrow.svelte';
import { Portal } from '$soma/components/internal'; import { Portal } from '$soma/components/internal';
export const Tooltip = Object.assign(TooltipRoot, { type TooltipNamespace = typeof TooltipRoot & {
Group, Group: typeof Group;
Trigger, Trigger: typeof Trigger;
Portal, Portal: typeof Portal;
Content, Content: typeof Content;
Arrow 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 default Tooltip;
export type { export type {
TooltipProviderProps as TooltipProps, TooltipProps,
TooltipGroupProps as GroupProps, TooltipGroupProps as GroupProps,
TooltipTriggerProps as TriggerProps, TooltipTriggerProps as TriggerProps,
TooltipContentProps as ContentProps, TooltipContentProps as ContentProps,

@ -19,9 +19,9 @@
* tooltip to a sibling is instantaneous within the grace window. * tooltip to a sibling is instantaneous within the grace window.
*/ */
import * as SomaTooltip from '$soma/components/tooltip'; 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();
</script> </script>
<SomaTooltip.Provider {...rest} bind:open> <SomaTooltip.Provider {...rest} bind:open>

@ -24,7 +24,7 @@ export type TooltipSize = Extract<Size, 'sm' | 'md' | 'lg'>;
export type TooltipVariant = 'solid' | 'outline' | 'ghost'; export type TooltipVariant = 'solid' | 'outline' | 'ghost';
export type TooltipGroupProps = SomaTooltipGroupProps; export type TooltipGroupProps = SomaTooltipGroupProps;
export type TooltipProviderProps = SomaTooltipProviderProps; export type TooltipProps = SomaTooltipProviderProps;
export type TooltipTriggerProps = SomaTooltipTriggerProps; export type TooltipTriggerProps = SomaTooltipTriggerProps;
export type TooltipArrowProps = SomaTooltipArrowProps; export type TooltipArrowProps = SomaTooltipArrowProps;

@ -0,0 +1,17 @@
type StyleRecord = Record<string, string | number | null | undefined>;
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;
}
Loading…
Cancel
Save

Powered by TurnKey Linux.