First component in the soma→eidos porting sprint. Two-part compound (Provider + Item), modeled on radio-group's structure. Eidos surface: - variant: 'solid' | 'outline' | 'ghost' — mirrors <Toggle> - color: 7 tones, propagated to items via data-color cascade - size: xs/sm/md/lg/xl (responsive) - attached: boolean — segmented control affordance (shared borders, active item z-indexed above neighbors) - block: boolean — items grow to fill the inline-size - iconOnly: per-item flag — square aspect, sr-only label Items consume the Toggle palette token vocabulary (--toggle-*) so a standalone <Toggle> and a <ToggleGroup.Item> at the same variant × color × size are visually identical — single source of truth for the "toggle button" recipe. Morfo scope fix: ['soma'] → ['soma', 'sema', 'eidos']. Adds apg URL (WAI-ARIA Button pattern) so the audit's A-1.4 rule passes. The commit-toggle event's sound comes from the `commit` family base — no per-component cascade needed. Demo: canonical 6-tab template (Live / API / Morfo / Sema / Recipe / A11y) with every prop exposed as a chip/switch, soma + eidos snippets update live, sema "▶ play" button on the commit-toggle event. Reference comparison vs Radix Primitives / Bits UI / Ark UI / Chakra v3 / Mantine / Radix Themes / shadcn documented in README §Comparativa. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>active-uix
parent
4324cb489d
commit
61ffbade46
@ -0,0 +1,32 @@
|
||||
// ToggleGroup — eidos compound API, disciplined option C.
|
||||
//
|
||||
// import { ToggleGroup } from '$uix/eidos/components/toggle-group';
|
||||
//
|
||||
// <ToggleGroup type="single" variant="outline" size="md" attached
|
||||
// bind:value aria-label="Text format">
|
||||
// <ToggleGroup.Item value="bold">B</ToggleGroup.Item>
|
||||
// <ToggleGroup.Item value="italic">I</ToggleGroup.Item>
|
||||
// <ToggleGroup.Item value="underline">U</ToggleGroup.Item>
|
||||
// </ToggleGroup>
|
||||
import ToggleGroupComponent from './toggle-group.svelte';
|
||||
import Item from './toggle-group-item.svelte';
|
||||
|
||||
type ToggleGroupNamespace = typeof ToggleGroupComponent & {
|
||||
Item: typeof Item;
|
||||
};
|
||||
|
||||
const ToggleGroup = ToggleGroupComponent as ToggleGroupNamespace;
|
||||
ToggleGroup.Item = Item;
|
||||
|
||||
export { ToggleGroup };
|
||||
|
||||
export default ToggleGroup;
|
||||
|
||||
export type {
|
||||
ToggleGroupProps,
|
||||
ToggleGroupItemProps as ItemProps,
|
||||
ToggleGroupType,
|
||||
ToggleGroupVariant,
|
||||
ToggleGroupColor,
|
||||
ToggleGroupSize
|
||||
} from './types';
|
||||
@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Eidos `<ToggleGroup.Item>`. Visual styling for the item lives in
|
||||
* `toggle-group.css` and reads `data-variant` / `data-color` /
|
||||
* `data-size` from the ancestor root (set by `<ToggleGroup>`).
|
||||
*
|
||||
* The morfo emits `data-state="on"|"off"`, `data-disabled` and
|
||||
* `data-value` on the item — the recipe consumes those for the
|
||||
* pressed-state visuals.
|
||||
*/
|
||||
import * as ToggleGroup from '$soma/components/toggle-group';
|
||||
import type { ToggleGroupItemProps } from './types';
|
||||
|
||||
let { iconOnly = false, children, ...rest }: ToggleGroupItemProps = $props();
|
||||
</script>
|
||||
|
||||
<ToggleGroup.Item {...rest} data-icon-only={iconOnly ? '' : undefined}>
|
||||
{@render children?.()}
|
||||
</ToggleGroup.Item>
|
||||
@ -0,0 +1,296 @@
|
||||
/*
|
||||
* ToggleGroup recipe.
|
||||
*
|
||||
* Visually each item IS a Toggle — same height / padding / radius
|
||||
* tokens, same variant + palette vocabulary. The group adds:
|
||||
*
|
||||
* - Layout: flex container, orientation-aware direction, gap
|
||||
* - `data-attached`: items collapse spacing + share borders so the
|
||||
* group reads as a single segmented control
|
||||
* - `data-block`: items grow equally to fill the inline-size
|
||||
* - Disabled cascade: a single `disabled` on the group disables all
|
||||
* items (the morfo already projects `data-disabled` on each item).
|
||||
*
|
||||
* [data-toggle-group] → flex container
|
||||
* [data-toggle-group-item] → button (Toggle-shaped)
|
||||
*/
|
||||
|
||||
[data-toggle-group] {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
gap: var(--toggle-group-gap, var(--space-1));
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
[data-toggle-group][data-orientation='vertical'] {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
[data-toggle-group][data-block] {
|
||||
display: flex;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
[data-toggle-group][data-block] [data-toggle-group-item] {
|
||||
flex: 1 1 auto;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
/* Attached mode — segmented control look. Items share edges, only the
|
||||
* outermost corners stay rounded. */
|
||||
[data-toggle-group][data-attached] {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
[data-toggle-group][data-attached] [data-toggle-group-item] {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
[data-toggle-group][data-attached][data-orientation='horizontal']
|
||||
[data-toggle-group-item]:first-of-type {
|
||||
border-start-start-radius: var(--_toggle-radius);
|
||||
border-end-start-radius: var(--_toggle-radius);
|
||||
}
|
||||
|
||||
[data-toggle-group][data-attached][data-orientation='horizontal']
|
||||
[data-toggle-group-item]:last-of-type {
|
||||
border-start-end-radius: var(--_toggle-radius);
|
||||
border-end-end-radius: var(--_toggle-radius);
|
||||
}
|
||||
|
||||
[data-toggle-group][data-attached][data-orientation='horizontal']
|
||||
[data-toggle-group-item]:not(:first-of-type) {
|
||||
margin-inline-start: calc(var(--toggle-border-width) * -1);
|
||||
}
|
||||
|
||||
[data-toggle-group][data-attached][data-orientation='vertical']
|
||||
[data-toggle-group-item]:first-of-type {
|
||||
border-start-start-radius: var(--_toggle-radius);
|
||||
border-start-end-radius: var(--_toggle-radius);
|
||||
}
|
||||
|
||||
[data-toggle-group][data-attached][data-orientation='vertical']
|
||||
[data-toggle-group-item]:last-of-type {
|
||||
border-end-start-radius: var(--_toggle-radius);
|
||||
border-end-end-radius: var(--_toggle-radius);
|
||||
}
|
||||
|
||||
[data-toggle-group][data-attached][data-orientation='vertical']
|
||||
[data-toggle-group-item]:not(:first-of-type) {
|
||||
margin-block-start: calc(var(--toggle-border-width) * -1);
|
||||
}
|
||||
|
||||
/* Pressed item floats above its neighbors so the active border isn't
|
||||
* clipped by the attached overlap. */
|
||||
[data-toggle-group][data-attached] [data-toggle-group-item][data-state='on'] {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ── Item ────────────────────────────────────────────────────────────
|
||||
*
|
||||
* Items consume the Toggle palette tokens (one source of truth for the
|
||||
* "toggle button" visual vocabulary). `data-variant` / `data-color` /
|
||||
* `data-size` are read from the ancestor group via descendant
|
||||
* selectors below.
|
||||
*/
|
||||
|
||||
[data-toggle-group-item] {
|
||||
--_toggle-height: var(--toggle-height-md);
|
||||
--_toggle-padding-inline: var(--toggle-px-md);
|
||||
--_toggle-gap: var(--toggle-gap-md);
|
||||
--_toggle-font-size: var(--toggle-font-size-md);
|
||||
--_toggle-font-weight: var(--toggle-font-weight-md);
|
||||
--_toggle-radius: var(--toggle-radius-md);
|
||||
--toggle-palette-track: var(--toggle-color-neutral-track);
|
||||
--toggle-palette-element: var(--toggle-color-neutral-element);
|
||||
--toggle-palette-hover: var(--toggle-color-neutral-hover);
|
||||
--toggle-palette-border: var(--toggle-color-neutral-border);
|
||||
--toggle-palette-solid: var(--toggle-color-neutral-solid);
|
||||
--toggle-palette-solid-hover: var(--toggle-color-neutral-solid-hover);
|
||||
--toggle-palette-text: var(--toggle-color-neutral-text);
|
||||
--toggle-palette-contrast: var(--toggle-color-neutral-contrast);
|
||||
--_toggle-bg: var(--toggle-solid-bg);
|
||||
--_toggle-fg: var(--toggle-solid-fg);
|
||||
--_toggle-border: var(--toggle-solid-border);
|
||||
--_toggle-hover-bg: var(--toggle-solid-hover-bg);
|
||||
--_toggle-hover-border: var(--toggle-solid-hover-border);
|
||||
--_toggle-on-bg: var(--toggle-solid-on-bg);
|
||||
--_toggle-on-fg: var(--toggle-solid-on-fg);
|
||||
--_toggle-on-border: var(--toggle-solid-on-border);
|
||||
--_toggle-on-hover-bg: var(--toggle-solid-on-hover-bg);
|
||||
--_toggle-on-hover-border: var(--toggle-solid-on-hover-border);
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--_toggle-gap);
|
||||
min-height: var(--_toggle-height);
|
||||
padding-inline: var(--_toggle-padding-inline);
|
||||
border: var(--toggle-border-width) solid var(--_toggle-border);
|
||||
border-radius: var(--_toggle-radius);
|
||||
background: var(--_toggle-bg);
|
||||
color: var(--_toggle-fg);
|
||||
box-shadow: var(--toggle-shadow);
|
||||
font-family: var(--toggle-font-family);
|
||||
font-size: var(--_toggle-font-size);
|
||||
font-weight: var(--_toggle-font-weight);
|
||||
line-height: var(--toggle-line-height);
|
||||
letter-spacing: var(--toggle-letter-spacing);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--toggle-transition-duration) var(--toggle-transition-ease),
|
||||
border-color var(--toggle-transition-duration) var(--toggle-transition-ease),
|
||||
color var(--toggle-transition-duration) var(--toggle-transition-ease),
|
||||
box-shadow var(--toggle-transition-duration) var(--toggle-transition-ease);
|
||||
}
|
||||
|
||||
[data-toggle-group-item]:hover:not([data-disabled]) {
|
||||
background: var(--_toggle-hover-bg);
|
||||
border-color: var(--_toggle-hover-border);
|
||||
}
|
||||
|
||||
[data-toggle-group-item][data-state='on'] {
|
||||
background: var(--_toggle-on-bg);
|
||||
color: var(--_toggle-on-fg);
|
||||
border-color: var(--_toggle-on-border);
|
||||
}
|
||||
|
||||
[data-toggle-group-item][data-state='on']:hover:not([data-disabled]) {
|
||||
background: var(--_toggle-on-hover-bg);
|
||||
border-color: var(--_toggle-on-hover-border);
|
||||
}
|
||||
|
||||
[data-toggle-group-item]:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
[data-toggle-group-item][data-disabled] {
|
||||
cursor: default;
|
||||
opacity: var(--toggle-disabled-opacity, 0.55);
|
||||
}
|
||||
|
||||
[data-toggle-group-item][data-icon-only] {
|
||||
padding-inline: 0;
|
||||
inline-size: var(--_toggle-height);
|
||||
}
|
||||
|
||||
/* ── Size cascade — items inherit data-size from the group ───────── */
|
||||
[data-toggle-group][data-size='xs'] [data-toggle-group-item] {
|
||||
--_toggle-height: var(--toggle-height-xs);
|
||||
--_toggle-padding-inline: var(--toggle-px-xs);
|
||||
--_toggle-gap: var(--toggle-gap-xs);
|
||||
--_toggle-font-size: var(--toggle-font-size-xs);
|
||||
--_toggle-font-weight: var(--toggle-font-weight-xs);
|
||||
--_toggle-radius: var(--toggle-radius-xs);
|
||||
}
|
||||
|
||||
[data-toggle-group][data-size='sm'] [data-toggle-group-item] {
|
||||
--_toggle-height: var(--toggle-height-sm);
|
||||
--_toggle-padding-inline: var(--toggle-px-sm);
|
||||
--_toggle-gap: var(--toggle-gap-sm);
|
||||
--_toggle-font-size: var(--toggle-font-size-sm);
|
||||
--_toggle-font-weight: var(--toggle-font-weight-sm);
|
||||
--_toggle-radius: var(--toggle-radius-sm);
|
||||
}
|
||||
|
||||
[data-toggle-group][data-size='lg'] [data-toggle-group-item] {
|
||||
--_toggle-height: var(--toggle-height-lg);
|
||||
--_toggle-padding-inline: var(--toggle-px-lg);
|
||||
--_toggle-gap: var(--toggle-gap-lg);
|
||||
--_toggle-font-size: var(--toggle-font-size-lg);
|
||||
--_toggle-font-weight: var(--toggle-font-weight-lg);
|
||||
--_toggle-radius: var(--toggle-radius-lg);
|
||||
}
|
||||
|
||||
[data-toggle-group][data-size='xl'] [data-toggle-group-item] {
|
||||
--_toggle-height: var(--toggle-height-xl);
|
||||
--_toggle-padding-inline: var(--toggle-px-xl);
|
||||
--_toggle-gap: var(--toggle-gap-xl);
|
||||
--_toggle-font-size: var(--toggle-font-size-xl);
|
||||
--_toggle-font-weight: var(--toggle-font-weight-xl);
|
||||
--_toggle-radius: var(--toggle-radius-xl);
|
||||
}
|
||||
|
||||
/* ── Variant cascade ─────────────────────────────────────────────── */
|
||||
|
||||
[data-toggle-group][data-variant='outline'] [data-toggle-group-item] {
|
||||
--_toggle-bg: var(--toggle-outline-bg);
|
||||
--_toggle-fg: var(--toggle-outline-fg);
|
||||
--_toggle-border: var(--toggle-outline-border);
|
||||
--_toggle-hover-bg: var(--toggle-outline-hover-bg);
|
||||
--_toggle-hover-border: var(--toggle-outline-hover-border);
|
||||
--_toggle-on-bg: var(--toggle-outline-on-bg);
|
||||
--_toggle-on-fg: var(--toggle-outline-on-fg);
|
||||
--_toggle-on-border: var(--toggle-outline-on-border);
|
||||
--_toggle-on-hover-bg: var(--toggle-outline-on-hover-bg);
|
||||
--_toggle-on-hover-border: var(--toggle-outline-on-hover-border);
|
||||
}
|
||||
|
||||
[data-toggle-group][data-variant='ghost'] [data-toggle-group-item] {
|
||||
--_toggle-bg: var(--toggle-ghost-bg);
|
||||
--_toggle-fg: var(--toggle-ghost-fg);
|
||||
--_toggle-border: var(--toggle-ghost-border);
|
||||
--_toggle-hover-bg: var(--toggle-ghost-hover-bg);
|
||||
--_toggle-hover-border: var(--toggle-ghost-hover-border);
|
||||
--_toggle-on-bg: var(--toggle-ghost-on-bg);
|
||||
--_toggle-on-fg: var(--toggle-ghost-on-fg);
|
||||
--_toggle-on-border: var(--toggle-ghost-on-border);
|
||||
--_toggle-on-hover-bg: var(--toggle-ghost-on-hover-bg);
|
||||
--_toggle-on-hover-border: var(--toggle-ghost-on-hover-border);
|
||||
}
|
||||
|
||||
/* ── Color cascade — switches the palette tokens ───────────────── */
|
||||
|
||||
[data-toggle-group][data-color='primary'] [data-toggle-group-item] {
|
||||
--toggle-palette-track: var(--toggle-color-primary-track);
|
||||
--toggle-palette-element: var(--toggle-color-primary-element);
|
||||
--toggle-palette-hover: var(--toggle-color-primary-hover);
|
||||
--toggle-palette-border: var(--toggle-color-primary-border);
|
||||
--toggle-palette-solid: var(--toggle-color-primary-solid);
|
||||
--toggle-palette-solid-hover: var(--toggle-color-primary-solid-hover);
|
||||
--toggle-palette-text: var(--toggle-color-primary-text);
|
||||
--toggle-palette-contrast: var(--toggle-color-primary-contrast);
|
||||
}
|
||||
|
||||
[data-toggle-group][data-color='affirm'] [data-toggle-group-item] {
|
||||
--toggle-palette-track: var(--toggle-color-affirm-track);
|
||||
--toggle-palette-element: var(--toggle-color-affirm-element);
|
||||
--toggle-palette-hover: var(--toggle-color-affirm-hover);
|
||||
--toggle-palette-border: var(--toggle-color-affirm-border);
|
||||
--toggle-palette-solid: var(--toggle-color-affirm-solid);
|
||||
--toggle-palette-solid-hover: var(--toggle-color-affirm-solid-hover);
|
||||
--toggle-palette-text: var(--toggle-color-affirm-text);
|
||||
--toggle-palette-contrast: var(--toggle-color-affirm-contrast);
|
||||
}
|
||||
|
||||
[data-toggle-group][data-color='risk'] [data-toggle-group-item] {
|
||||
--toggle-palette-track: var(--toggle-color-risk-track);
|
||||
--toggle-palette-element: var(--toggle-color-risk-element);
|
||||
--toggle-palette-hover: var(--toggle-color-risk-hover);
|
||||
--toggle-palette-border: var(--toggle-color-risk-border);
|
||||
--toggle-palette-solid: var(--toggle-color-risk-solid);
|
||||
--toggle-palette-solid-hover: var(--toggle-color-risk-solid-hover);
|
||||
--toggle-palette-text: var(--toggle-color-risk-text);
|
||||
--toggle-palette-contrast: var(--toggle-color-risk-contrast);
|
||||
}
|
||||
|
||||
[data-toggle-group][data-color='threat'] [data-toggle-group-item] {
|
||||
--toggle-palette-track: var(--toggle-color-threat-track);
|
||||
--toggle-palette-element: var(--toggle-color-threat-element);
|
||||
--toggle-palette-hover: var(--toggle-color-threat-hover);
|
||||
--toggle-palette-border: var(--toggle-color-threat-border);
|
||||
--toggle-palette-solid: var(--toggle-color-threat-solid);
|
||||
--toggle-palette-solid-hover: var(--toggle-color-threat-solid-hover);
|
||||
--toggle-palette-text: var(--toggle-color-threat-text);
|
||||
--toggle-palette-contrast: var(--toggle-color-threat-contrast);
|
||||
}
|
||||
|
||||
[data-toggle-group][data-disabled] {
|
||||
opacity: var(--toggle-disabled-opacity, 0.55);
|
||||
pointer-events: none;
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Eidos `<ToggleGroup>` — root.
|
||||
*
|
||||
* <ToggleGroup type="single" variant="outline" size="md" attached
|
||||
* bind:value aria-label="Text format">
|
||||
* <ToggleGroup.Item value="bold">B</ToggleGroup.Item>
|
||||
* <ToggleGroup.Item value="italic">I</ToggleGroup.Item>
|
||||
* </ToggleGroup>
|
||||
*
|
||||
* Soma owns: selection type, value, keyboard nav, disabled cascade,
|
||||
* `commit-toggle` perceptual event. Eidos adds: variant / color /
|
||||
* size / attached / block recipe knobs as `data-*` on the root, which
|
||||
* the CSS recipe consumes to style the children via attribute
|
||||
* descendant selectors.
|
||||
*/
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import * as ToggleGroup from '$soma/components/toggle-group';
|
||||
import type { ToggleGroupProps } from './types';
|
||||
|
||||
let {
|
||||
variant = 'solid',
|
||||
color = 'neutral',
|
||||
size = 'md',
|
||||
attached = false,
|
||||
block = false,
|
||||
value = $bindable([]),
|
||||
children,
|
||||
...rest
|
||||
}: ToggleGroupProps = $props();
|
||||
|
||||
const eidos = ActiveEidos.require();
|
||||
const resolvedSize = $derived(eidos.resolve(size, 'md'));
|
||||
</script>
|
||||
|
||||
<ToggleGroup.Provider
|
||||
{...rest}
|
||||
bind:value
|
||||
data-variant={variant}
|
||||
data-color={color}
|
||||
data-size={resolvedSize}
|
||||
data-attached={attached ? '' : undefined}
|
||||
data-block={block ? '' : undefined}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ToggleGroup.Provider>
|
||||
@ -0,0 +1,64 @@
|
||||
import type { ProviderProps, ItemProps, ToggleGroupType } from '$soma/components/toggle-group';
|
||||
import type { ResponsiveProp, SelectionVariant, Size } from '$uix/eidos/lib/types';
|
||||
|
||||
/**
|
||||
* Eidos `<ToggleGroup>` — visual wrapper over the headless soma
|
||||
* primitive. Compound shape mirrors Radix `ToggleGroup`:
|
||||
*
|
||||
* <ToggleGroup type="single" bind:value aria-label="Text format">
|
||||
* <ToggleGroup.Item value="bold">B</ToggleGroup.Item>
|
||||
* <ToggleGroup.Item value="italic">I</ToggleGroup.Item>
|
||||
* <ToggleGroup.Item value="underline">U</ToggleGroup.Item>
|
||||
* </ToggleGroup>
|
||||
*
|
||||
* Visual concerns: `variant` (solid / outline / ghost), `size`, `color`,
|
||||
* `attached` (shared borders à la `Group`). The single-vs-multiple
|
||||
* selection logic, keyboard navigation, and `commit-toggle` semantic
|
||||
* event live in soma.
|
||||
*/
|
||||
|
||||
/** Toggle group visual variants. Mirrors {@link ToggleVariant}. */
|
||||
export type ToggleGroupVariant = SelectionVariant;
|
||||
|
||||
/** Color tone the group's recipe propagates to its items via `data-color`. */
|
||||
export type ToggleGroupColor =
|
||||
| 'primary'
|
||||
| 'neutral'
|
||||
| 'affirm'
|
||||
| 'risk'
|
||||
| 'threat'
|
||||
| 'absent'
|
||||
| 'unknown';
|
||||
|
||||
/**
|
||||
* Sizing subset toggle-group exposes. Matches {@link ToggleSize}, narrowed
|
||||
* to the t-shirt steps the recipe maps to tokens.
|
||||
*/
|
||||
export type ToggleGroupSize = Extract<Size, 'xs' | 'sm' | 'md' | 'lg' | 'xl'>;
|
||||
|
||||
/** Selection mode — single (radio-like, deselectable) vs multiple (checkbox-like). */
|
||||
export type { ToggleGroupType };
|
||||
|
||||
export type ToggleGroupProps = ProviderProps & {
|
||||
/** Visual variant. @default 'solid' */
|
||||
variant?: ToggleGroupVariant;
|
||||
/** Color tone — propagates to items as `data-color`. @default 'neutral' */
|
||||
color?: ToggleGroupColor;
|
||||
/** Sizing scale. @default 'md' */
|
||||
size?: ResponsiveProp<ToggleGroupSize>;
|
||||
/**
|
||||
* Items share borders / collapse spacing, presenting as a single
|
||||
* segmented control. @default false
|
||||
*/
|
||||
attached?: boolean;
|
||||
/** Stretch to fill the container's inline-size. @default false */
|
||||
block?: boolean;
|
||||
};
|
||||
|
||||
export type ToggleGroupItemProps = ItemProps & {
|
||||
/**
|
||||
* Render as an icon-only button (square aspect, sr-only label).
|
||||
* @default false
|
||||
*/
|
||||
iconOnly?: boolean;
|
||||
};
|
||||
@ -0,0 +1,622 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
ToggleGroup,
|
||||
type ToggleGroupSize,
|
||||
type ToggleGroupVariant,
|
||||
type ToggleGroupColor,
|
||||
type ToggleGroupType
|
||||
} from '$uix/eidos/components/toggle-group';
|
||||
import type { Orientation } from '$soma/types';
|
||||
import { compileMorfo } from '$uix/morfo';
|
||||
import { toggleGroupMorfo } from '@/uix/morfo/components/toggle-group';
|
||||
import { getActiveUix } from '$active-uix';
|
||||
import { page } from '$app/state';
|
||||
|
||||
const uix = getActiveUix();
|
||||
const params = page.url.searchParams;
|
||||
|
||||
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
|
||||
let tab = $state<Tab>('live');
|
||||
|
||||
// ── Live state ──────────────────────────────────────────────────
|
||||
const types: ToggleGroupType[] = ['single', 'multiple'];
|
||||
const variants: ToggleGroupVariant[] = ['solid', 'outline', 'ghost'];
|
||||
const colors: ToggleGroupColor[] = [
|
||||
'primary',
|
||||
'neutral',
|
||||
'affirm',
|
||||
'risk',
|
||||
'threat',
|
||||
'absent',
|
||||
'unknown'
|
||||
];
|
||||
const sizes: ToggleGroupSize[] = ['xs', 'sm', 'md', 'lg', 'xl'];
|
||||
const orientations: Orientation[] = ['horizontal', 'vertical'];
|
||||
|
||||
let type = $state<ToggleGroupType>('single');
|
||||
let value = $state<string[]>(['bold']);
|
||||
let variant = $state<ToggleGroupVariant>('outline');
|
||||
let color = $state<ToggleGroupColor>('neutral');
|
||||
let size = $state<ToggleGroupSize>('md');
|
||||
let orientation = $state<Orientation>('horizontal');
|
||||
let attached = $state(true);
|
||||
let block = $state(false);
|
||||
let disabled = $state(params.get('perm.disabled') === 'true');
|
||||
let loop = $state(true);
|
||||
let iconOnly = $state(false);
|
||||
let italicDisabled = $state(false);
|
||||
let ariaLabel = $state('Text format');
|
||||
|
||||
const items = $derived([
|
||||
{ value: 'bold', label: 'Bold', glyph: 'B' },
|
||||
{ value: 'italic', label: 'Italic', glyph: 'I', disabled: italicDisabled },
|
||||
{ value: 'underline', label: 'Underline', glyph: 'U' },
|
||||
{ value: 'strike', label: 'Strikethrough', glyph: 'S' }
|
||||
]);
|
||||
|
||||
type TraceEntry = { event: string; family: string; intent?: string; at: number };
|
||||
let trace = $state<TraceEntry[]>([]);
|
||||
let stageRef = $state<HTMLElement | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
const el = stageRef;
|
||||
if (!el) return;
|
||||
const obs = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.attributeName !== 'data-event') continue;
|
||||
const target = m.target as Element;
|
||||
const ev = target.getAttribute('data-event');
|
||||
if (!ev) continue;
|
||||
trace = [
|
||||
{
|
||||
event: ev,
|
||||
family: target.getAttribute('data-event-family') ?? '—',
|
||||
intent: target.getAttribute('data-event-intent') ?? undefined,
|
||||
at: Date.now()
|
||||
},
|
||||
...trace
|
||||
].slice(0, 6);
|
||||
}
|
||||
});
|
||||
obs.observe(el, { attributes: true, subtree: true, attributeFilter: ['data-event'] });
|
||||
return () => obs.disconnect();
|
||||
});
|
||||
|
||||
const compiled = compileMorfo(toggleGroupMorfo);
|
||||
const partsList = $derived([...compiled.parts.byKebab.values()]);
|
||||
const events = $derived([...compiled.actions.byName.values()]);
|
||||
|
||||
function fmtTime(at: number): string {
|
||||
const d = new Date(at);
|
||||
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
||||
}
|
||||
|
||||
// ── Snippets ────────────────────────────────────────────────────
|
||||
const somaSnippet = $derived(
|
||||
[
|
||||
"<script lang='ts'>",
|
||||
" import * as ToggleGroup from '$soma/components/toggle-group';",
|
||||
' let value = $state<string[]>([\'bold\']);',
|
||||
'</' + 'script>',
|
||||
'',
|
||||
'<ToggleGroup.Provider',
|
||||
type !== 'single' && ` type="${type}"`,
|
||||
' bind:value',
|
||||
disabled && ' disabled',
|
||||
!loop && ' loop={false}',
|
||||
orientation !== 'horizontal' && ` orientation="${orientation}"`,
|
||||
` aria-label="${ariaLabel}"`,
|
||||
'>',
|
||||
...items.map(
|
||||
(it) =>
|
||||
` <ToggleGroup.Item value="${it.value}"${it.disabled ? ' disabled' : ''}>${it.glyph}</ToggleGroup.Item>`
|
||||
),
|
||||
'</ToggleGroup.Provider>'
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
);
|
||||
|
||||
const eidosSnippet = $derived(
|
||||
[
|
||||
"<script lang='ts'>",
|
||||
" import { ToggleGroup } from '$uix/eidos/components/toggle-group';",
|
||||
' let value = $state<string[]>([\'bold\']);',
|
||||
'</' + 'script>',
|
||||
'',
|
||||
'<ToggleGroup',
|
||||
type !== 'single' && ` type="${type}"`,
|
||||
' bind:value',
|
||||
variant !== 'solid' && ` variant="${variant}"`,
|
||||
color !== 'neutral' && ` color="${color}"`,
|
||||
size !== 'md' && ` size="${size}"`,
|
||||
orientation !== 'horizontal' && ` orientation="${orientation}"`,
|
||||
attached && ' attached',
|
||||
block && ' block',
|
||||
disabled && ' disabled',
|
||||
!loop && ' loop={false}',
|
||||
` aria-label="${ariaLabel}"`,
|
||||
'>',
|
||||
...items.map(
|
||||
(it) =>
|
||||
` <ToggleGroup.Item value="${it.value}"${it.disabled ? ' disabled' : ''}${iconOnly ? ' iconOnly' : ''}>${it.glyph}</ToggleGroup.Item>`
|
||||
),
|
||||
'</ToggleGroup>'
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
);
|
||||
</script>
|
||||
|
||||
<div data-uix-canvas-inner>
|
||||
<header>
|
||||
<div data-uix-eyebrow>Form · ToggleGroup</div>
|
||||
<h1 data-uix-page-title>ToggleGroup</h1>
|
||||
<p data-uix-page-lede>
|
||||
Multiple toggle buttons sharing one selection model.
|
||||
<code>type="single"</code> behaves radio-like (deselectable);
|
||||
<code>type="multiple"</code> behaves checkbox-like. Headless
|
||||
<span data-uix-layer-badge="soma">soma</span>
|
||||
owns selection state, keyboard navigation, and the
|
||||
<code>commit-toggle</code> perceptual event; Eidos paints chrome —
|
||||
<code>variant</code>, <code>color</code>, <code>size</code>,
|
||||
<code>attached</code> (segmented control affordance) and
|
||||
<code>block</code> (stretch to fill).
|
||||
</p>
|
||||
<div data-uix-page-meta>
|
||||
<span data-uix-meta-pill>
|
||||
<span data-uix-meta-key>parts</span>{compiled.parts.order.length}
|
||||
</span>
|
||||
<span data-uix-meta-pill>
|
||||
<span data-uix-meta-key>events</span>{events.length}
|
||||
</span>
|
||||
<span data-uix-meta-pill>
|
||||
<span data-uix-meta-key>type</span>{type}
|
||||
</span>
|
||||
<span data-uix-meta-pill>
|
||||
<span data-uix-meta-key>selected</span>{value.length}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div data-uix-stage>
|
||||
<div data-uix-stage-area bind:this={stageRef}>
|
||||
<ToggleGroup
|
||||
{type}
|
||||
bind:value
|
||||
{variant}
|
||||
{color}
|
||||
{size}
|
||||
{orientation}
|
||||
{attached}
|
||||
{block}
|
||||
{disabled}
|
||||
{loop}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{#each items as item (item.value)}
|
||||
<ToggleGroup.Item value={item.value} disabled={item.disabled} {iconOnly}>
|
||||
{item.glyph}
|
||||
</ToggleGroup.Item>
|
||||
{/each}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<div data-uix-stage-trace>
|
||||
<span data-uix-stage-trace-key>trace</span>
|
||||
<span>{trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`}</span>
|
||||
{#if trace[0]}
|
||||
<span style="color: var(--uix-text-faint)">·</span>
|
||||
<span data-uix-stage-trace-key>last</span>
|
||||
<span>{trace[0].event} ({trace[0].family}) @{fmtTime(trace[0].at)}</span>
|
||||
{/if}
|
||||
<span style="margin-inline-start: auto;">
|
||||
<span data-uix-stage-trace-key>value</span>
|
||||
[{value.map((v) => `"${v}"`).join(', ')}]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-uix-tabs role="tablist">
|
||||
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
|
||||
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>
|
||||
API <span data-uix-tab-count>2</span>
|
||||
</button>
|
||||
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
|
||||
<span data-uix-layer-badge="morfo">morfo</span>
|
||||
<span data-uix-tab-count>{partsList.length}p · {events.length}e</span>
|
||||
</button>
|
||||
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
|
||||
<span data-uix-layer-badge="sema">sema</span>
|
||||
<span data-uix-tab-count>{events.length}</span>
|
||||
</button>
|
||||
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button>
|
||||
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
|
||||
</div>
|
||||
|
||||
{#if tab === 'live'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>Controls</h2>
|
||||
<p data-uix-section-desc>
|
||||
Knobs split by layer — <span data-uix-layer-badge="soma">soma</span> owns
|
||||
selection/keyboard/disabled, <span data-uix-layer-badge="eidos">eidos</span> owns chrome
|
||||
(variant/color/size/attached/block/iconOnly).
|
||||
</p>
|
||||
|
||||
<div data-uix-subsection-head>
|
||||
<span data-uix-layer-badge="soma">soma</span> props · selection + a11y
|
||||
</div>
|
||||
<div data-uix-controls>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>type</span>
|
||||
<span data-uix-chips role="radiogroup">
|
||||
{#each types as opt}
|
||||
<button data-uix-chip data-active={type === opt} onclick={() => (type = opt)}>{opt}</button>
|
||||
{/each}
|
||||
</span>
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>orientation</span>
|
||||
<span data-uix-chips role="radiogroup">
|
||||
{#each orientations as opt}
|
||||
<button data-uix-chip data-active={orientation === opt} onclick={() => (orientation = opt)}>{opt}</button>
|
||||
{/each}
|
||||
</span>
|
||||
</label>
|
||||
<label data-uix-control data-control-kind="switch">
|
||||
<input type="checkbox" bind:checked={disabled} /> disabled
|
||||
</label>
|
||||
<label data-uix-control data-control-kind="switch">
|
||||
<input type="checkbox" bind:checked={loop} /> loop arrow keys
|
||||
</label>
|
||||
<label data-uix-control data-control-kind="switch">
|
||||
<input type="checkbox" bind:checked={italicDisabled} /> disable "Italic" item
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>aria-label</span>
|
||||
<input type="text" bind:value={ariaLabel} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div data-uix-subsection-head>
|
||||
<span data-uix-layer-badge="eidos">eidos</span> props · chrome
|
||||
</div>
|
||||
<div data-uix-controls>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>variant</span>
|
||||
<span data-uix-chips role="radiogroup">
|
||||
{#each variants as opt}
|
||||
<button data-uix-chip data-active={variant === opt} onclick={() => (variant = opt)}>{opt}</button>
|
||||
{/each}
|
||||
</span>
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>color</span>
|
||||
<span data-uix-chips role="radiogroup">
|
||||
{#each colors as opt}
|
||||
<button data-uix-chip data-active={color === opt} onclick={() => (color = opt)}>{opt}</button>
|
||||
{/each}
|
||||
</span>
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>size</span>
|
||||
<span data-uix-chips role="radiogroup">
|
||||
{#each sizes as opt}
|
||||
<button data-uix-chip data-active={size === opt} onclick={() => (size = opt)}>{opt}</button>
|
||||
{/each}
|
||||
</span>
|
||||
</label>
|
||||
<label data-uix-control data-control-kind="switch">
|
||||
<input type="checkbox" bind:checked={attached} /> attached (segmented)
|
||||
</label>
|
||||
<label data-uix-control data-control-kind="switch">
|
||||
<input type="checkbox" bind:checked={block} /> block (stretch)
|
||||
</label>
|
||||
<label data-uix-control data-control-kind="switch">
|
||||
<input type="checkbox" bind:checked={iconOnly} /> iconOnly (square)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div data-uix-code>
|
||||
<div data-uix-code-head>
|
||||
<span data-uix-layer-badge="soma">soma</span>
|
||||
<span>headless · selection + keyboard</span>
|
||||
<span data-uix-code-lang>svelte</span>
|
||||
</div>
|
||||
<pre><code>{somaSnippet}</code></pre>
|
||||
</div>
|
||||
|
||||
<div data-uix-code style="margin-top: var(--uix-space-3);">
|
||||
<div data-uix-code-head>
|
||||
<span data-uix-layer-badge="eidos">eidos</span>
|
||||
<span>visual · variant + size + attached + block</span>
|
||||
<span data-uix-code-lang>svelte</span>
|
||||
</div>
|
||||
<pre><code>{eidosSnippet}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if tab === 'api'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>API reference</h2>
|
||||
<p data-uix-section-desc>
|
||||
Two parts — <code>ToggleGroup</code> (provider/container) and
|
||||
<code>ToggleGroup.Item</code> (button). Soma drives selection,
|
||||
eidos drives visual treatment.
|
||||
</p>
|
||||
<div data-uix-subsection-head>ToggleGroup (root)</div>
|
||||
<div data-uix-table-wrap>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>Name</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="name">type <span data-uix-tag data-kind="soma">soma</span></td>
|
||||
<td class="type">'single' | 'multiple'</td>
|
||||
<td class="default">'single'</td>
|
||||
<td>Selection mode.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name">value <span data-uix-tag data-kind="soma">soma</span></td>
|
||||
<td class="type">string[]</td>
|
||||
<td class="default">[]</td>
|
||||
<td>Bindable array of selected item values.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name">disabled <span data-uix-tag data-kind="soma">soma</span></td>
|
||||
<td class="type">boolean</td>
|
||||
<td class="default">false</td>
|
||||
<td>Disables all items.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name">orientation <span data-uix-tag data-kind="soma">soma</span></td>
|
||||
<td class="type">'horizontal' | 'vertical'</td>
|
||||
<td class="default">'horizontal'</td>
|
||||
<td>Arrow-key direction.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name">loop <span data-uix-tag data-kind="soma">soma</span></td>
|
||||
<td class="type">boolean</td>
|
||||
<td class="default">true</td>
|
||||
<td>Wrap arrow-key navigation at boundaries.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name">aria-label <span data-uix-tag data-kind="soma">soma</span></td>
|
||||
<td class="type">string</td>
|
||||
<td class="default empty">—</td>
|
||||
<td>
|
||||
<strong>Required</strong> unless <code>aria-labelledby</code> is set.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name">variant <span data-uix-tag data-kind="eidos">eidos</span></td>
|
||||
<td class="type">'solid' | 'outline' | 'ghost'</td>
|
||||
<td class="default">'solid'</td>
|
||||
<td>Visual treatment. Mirrors Toggle.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name">color <span data-uix-tag data-kind="eidos">eidos</span></td>
|
||||
<td class="type">primary | neutral | affirm | risk | threat | absent | unknown</td>
|
||||
<td class="default">'neutral'</td>
|
||||
<td>Propagates to items via <code>data-color</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td>
|
||||
<td class="type">'xs' | 'sm' | 'md' | 'lg' | 'xl'</td>
|
||||
<td class="default">'md'</td>
|
||||
<td>T-shirt scale, responsive.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name">attached <span data-uix-tag data-kind="eidos">eidos</span></td>
|
||||
<td class="type">boolean</td>
|
||||
<td class="default">false</td>
|
||||
<td>Segmented control — shared borders.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name">block <span data-uix-tag data-kind="eidos">eidos</span></td>
|
||||
<td class="type">boolean</td>
|
||||
<td class="default">false</td>
|
||||
<td>Stretch items to fill the container.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div data-uix-subsection-head>ToggleGroup.Item</div>
|
||||
<div data-uix-table-wrap>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>Name</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="name">value <span data-uix-tag data-kind="soma">soma</span></td>
|
||||
<td class="type">string</td>
|
||||
<td class="default empty">—</td>
|
||||
<td><strong>Required</strong>. Item identifier.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name">disabled <span data-uix-tag data-kind="soma">soma</span></td>
|
||||
<td class="type">boolean</td>
|
||||
<td class="default">false</td>
|
||||
<td>Disables this item only.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name">iconOnly <span data-uix-tag data-kind="eidos">eidos</span></td>
|
||||
<td class="type">boolean</td>
|
||||
<td class="default">false</td>
|
||||
<td>Square aspect, label sr-only.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if tab === 'morfo'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>Morfo contract</h2>
|
||||
<p data-uix-section-desc>
|
||||
Compiled from <code>src/uix/morfo/components/toggle-group.ts</code>.
|
||||
</p>
|
||||
<div data-uix-subsection-head>Parts ({partsList.length})</div>
|
||||
<div data-uix-table-wrap>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>Kebab</th><th>Role</th><th>Marker</th><th>States</th></tr></thead>
|
||||
<tbody>
|
||||
{#each partsList as part}
|
||||
{@const partAny = part as unknown as Record<string, unknown>}
|
||||
<tr>
|
||||
<td class="name">{partAny.kebab}</td>
|
||||
<td>{partAny.role ?? '—'}</td>
|
||||
<td><code>{partAny.marker ?? '—'}</code></td>
|
||||
<td>
|
||||
{#if Array.isArray(partAny.states) && partAny.states.length}
|
||||
{(partAny.states as string[]).join(', ')}
|
||||
{:else}—{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if tab === 'sema'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>
|
||||
<span data-uix-layer-badge="sema">sema</span>
|
||||
· events + perceptual signature
|
||||
</h2>
|
||||
<p data-uix-section-desc>
|
||||
One event: <code>commit-toggle</code> from the
|
||||
<code>commit</code> family. Each item press fires it with
|
||||
sequence <em>post</em> — the perceptual signal lands after
|
||||
the state has flipped. Intent is per-item via
|
||||
<code>fromProp:intent</code>.
|
||||
</p>
|
||||
<div data-uix-table-wrap>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>Name</th><th>Family</th><th>Verb</th><th>Sequence</th><th>Intent</th><th>Play</th></tr></thead>
|
||||
<tbody>
|
||||
{#each events as action}
|
||||
{@const sem = action.semantic}
|
||||
{@const intentDecl = 'intent' in sem ? sem.intent : undefined}
|
||||
{@const effectiveIntent =
|
||||
typeof intentDecl === 'string'
|
||||
? intentDecl
|
||||
: intentDecl && typeof intentDecl === 'object' && 'default' in intentDecl
|
||||
? (intentDecl as { default: string }).default
|
||||
: undefined}
|
||||
<tr>
|
||||
<td class="name">{action.name}</td>
|
||||
<td class="type">{sem.family}</td>
|
||||
<td>{sem.verb ?? '—'}</td>
|
||||
<td>{sem.sequence ?? 'pre'}</td>
|
||||
<td class="default">{effectiveIntent ?? '—'}</td>
|
||||
<td>
|
||||
<button
|
||||
data-uix-play
|
||||
onclick={() => {
|
||||
const target = stageRef?.querySelector(
|
||||
'[data-toggle-group-item]'
|
||||
) as HTMLElement | null;
|
||||
if (!target) return;
|
||||
void uix.events?.emit({
|
||||
name: action.name,
|
||||
family: sem.family,
|
||||
target,
|
||||
...(effectiveIntent
|
||||
? { intent: effectiveIntent as 'neutral' | 'affirm' | 'risk' | 'threat' }
|
||||
: {})
|
||||
});
|
||||
}}>▶ play</button
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if tab === 'recipe'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>Eidos recipe</h2>
|
||||
<p data-uix-section-desc>
|
||||
Recipe at <code>src/uix/eidos/components/toggle-group/toggle-group.css</code>.
|
||||
Items consume the Toggle palette tokens so visuals stay
|
||||
coherent across <code><Toggle></code> and
|
||||
<code><ToggleGroup.Item></code>.
|
||||
</p>
|
||||
<div data-uix-table-wrap>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>Selector</th><th>Owner</th><th>Purpose</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="name"><code>[data-toggle-group]</code></td>
|
||||
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
|
||||
<td>Flex container, orientation-aware direction.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name"><code>[data-toggle-group][data-attached]</code></td>
|
||||
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
|
||||
<td>Collapsed gap + shared borders (segmented control).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name"><code>[data-toggle-group][data-block]</code></td>
|
||||
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
|
||||
<td>Items <code>flex: 1</code>; group <code>inline-size: 100%</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name"><code>[data-toggle-group-item]</code></td>
|
||||
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
|
||||
<td>Button — inherits Toggle's height/padding/radius/font tokens.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name"><code>[data-toggle-group-item][data-state='on']</code></td>
|
||||
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
|
||||
<td>Pressed visual — switches to <code>--_toggle-on-*</code> palette slice.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="name"
|
||||
><code>[data-toggle-group][data-variant='outline'] [data-toggle-group-item]</code></td
|
||||
>
|
||||
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
|
||||
<td>Cascades the variant slice from root to items.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if tab === 'a11y'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>A11y contract</h2>
|
||||
<p data-uix-section-desc>
|
||||
Group exposes <code>role="group"</code> with the required
|
||||
accessible name (warns in dev when missing). Items are
|
||||
<code><button></code> with
|
||||
<code>aria-pressed</code> and arrow-key navigation.
|
||||
Pattern: WAI-ARIA <a
|
||||
href="https://www.w3.org/WAI/ARIA/apg/patterns/button/"
|
||||
target="_blank"
|
||||
rel="noopener">Button</a
|
||||
>.
|
||||
</p>
|
||||
<div data-uix-table-wrap>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>Key</th><th>Effect</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><kbd>ArrowRight</kbd> / <kbd>ArrowDown</kbd></td><td>Move focus to next item.</td></tr>
|
||||
<tr><td><kbd>ArrowLeft</kbd> / <kbd>ArrowUp</kbd></td><td>Move focus to previous item.</td></tr>
|
||||
<tr><td><kbd>Home</kbd></td><td>Move focus to first item.</td></tr>
|
||||
<tr><td><kbd>End</kbd></td><td>Move focus to last item.</td></tr>
|
||||
<tr><td><kbd>Space</kbd> / <kbd>Enter</kbd></td><td>Toggle the focused item.</td></tr>
|
||||
<tr><td><kbd>Tab</kbd></td><td>Move focus out of the group.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
Loading…
Reference in new issue