feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build:
- New <Button> compound (morfo + soma + sema + eidos): 6 variants
(solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional
icon/endIcon snippets, loading state with built-in Spinner, asChild
Slot pattern, intent-driven sema (commit-action with fromProp:intent).
Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE):
- context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group:
full per-part Morfo contracts, reference-parity tables, ARIA contract
tables, keyboard tables, composition controls.
- color-field, time-field, time-picker, time-range-picker, color-picker,
month-grid, year-grid: same canonical treatment; per-part data/aria/
keyboard iteration of raw morfo, reference parity vs react-aria/
ark-ui/Chakra/bits-ui/Mantine.
Picker recipe polish:
- PickerShell.Clear/Cancel/Close refactored to use <Button size="xs">
(affects 5 pickers — date / date-range / time / time-range / color).
- TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14
/ 32 -> 28), per-channel accent (hour=primary, minute=secondary,
second=neutral; end variant muted), ruler off by default, hour ruler
cleaned to 0/6/12/18/23.
- ColorPicker: typography token --_color-picker-trigger-font-size that
scales with data-size (10/12/14px); autoFormat default true; Area
recipe override for [data-x-channel='hue'] with rainbow gradient +
saturation/brightness Y overlays.
- ValueText eidos wrapper: fix children snippet bug that suppressed
soma's displayValue fallback.
- date-field.css: segment editing affordance (cursor + hover + active
ring) via new --_date-field-accent-element token across 8 color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< script lang = "ts" >
import {
Button,
type ButtonVariant,
type ButtonSize,
type ButtonRounded
} from '$uix/eidos/components/button';
import { SHAPE_FAMILIES , type ShapeFamily } from '$uix/eidos/lib/types';
feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build:
- New <Button> compound (morfo + soma + sema + eidos): 6 variants
(solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional
icon/endIcon snippets, loading state with built-in Spinner, asChild
Slot pattern, intent-driven sema (commit-action with fromProp:intent).
Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE):
- context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group:
full per-part Morfo contracts, reference-parity tables, ARIA contract
tables, keyboard tables, composition controls.
- color-field, time-field, time-picker, time-range-picker, color-picker,
month-grid, year-grid: same canonical treatment; per-part data/aria/
keyboard iteration of raw morfo, reference parity vs react-aria/
ark-ui/Chakra/bits-ui/Mantine.
Picker recipe polish:
- PickerShell.Clear/Cancel/Close refactored to use <Button size="xs">
(affects 5 pickers — date / date-range / time / time-range / color).
- TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14
/ 32 -> 28), per-channel accent (hour=primary, minute=secondary,
second=neutral; end variant muted), ruler off by default, hour ruler
cleaned to 0/6/12/18/23.
- ColorPicker: typography token --_color-picker-trigger-font-size that
scales with data-size (10/12/14px); autoFormat default true; Area
recipe override for [data-x-channel='hue'] with rainbow gradient +
saturation/brightness Y overlays.
- ValueText eidos wrapper: fix children snippet bug that suppressed
soma's displayValue fallback.
- date-field.css: segment editing affordance (cursor + hover + active
ring) via new --_date-field-accent-element token across 8 color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
import type { ButtonIntent } from '$soma/components/button';
import { compileMorfo } from '$uix/morfo';
import { buttonMorfo } from '@/uix/morfo/components/button';
import { getActiveUix } from '$active-uix';
import * as Icon from '$uix/eidos/components/icon';
const uix = getActiveUix();
type IconKey = 'none' | 'save' | 'send' | 'trash' | 'settings' | 'heart' | 'plus' | 'download' | 'pencil' | 'check' | 'x';
const iconKeys: IconKey[] = ['none', 'save', 'send', 'trash', 'settings', 'heart', 'plus', 'download', 'pencil', 'check', 'x'];
const iconMap = {
none: null,
save: Icon.Save,
send: Icon.Send,
trash: Icon.Trash,
settings: Icon.Settings,
heart: Icon.Heart,
plus: Icon.Plus,
download: Icon.Download,
pencil: Icon.Pencil,
check: Icon.Check,
x: Icon.X
} as const;
type Tab = 'live' | 'gallery' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build:
- New <Button> compound (morfo + soma + sema + eidos): 6 variants
(solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional
icon/endIcon snippets, loading state with built-in Spinner, asChild
Slot pattern, intent-driven sema (commit-action with fromProp:intent).
Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE):
- context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group:
full per-part Morfo contracts, reference-parity tables, ARIA contract
tables, keyboard tables, composition controls.
- color-field, time-field, time-picker, time-range-picker, color-picker,
month-grid, year-grid: same canonical treatment; per-part data/aria/
keyboard iteration of raw morfo, reference parity vs react-aria/
ark-ui/Chakra/bits-ui/Mantine.
Picker recipe polish:
- PickerShell.Clear/Cancel/Close refactored to use <Button size="xs">
(affects 5 pickers — date / date-range / time / time-range / color).
- TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14
/ 32 -> 28), per-channel accent (hour=primary, minute=secondary,
second=neutral; end variant muted), ruler off by default, hour ruler
cleaned to 0/6/12/18/23.
- ColorPicker: typography token --_color-picker-trigger-font-size that
scales with data-size (10/12/14px); autoFormat default true; Area
recipe override for [data-x-channel='hue'] with rainbow gradient +
saturation/brightness Y overlays.
- ValueText eidos wrapper: fix children snippet bug that suppressed
soma's displayValue fallback.
- date-field.css: segment editing affordance (cursor + hover + active
ring) via new --_date-field-accent-element token across 8 color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
let tab = $state< Tab > ('live');
const variants: ButtonVariant[] = ['solid', 'soft', 'surface', 'outline', 'ghost', 'plain'];
const sizes: ButtonSize[] = ['xs', 'sm', 'md', 'lg', 'xl'];
const roundeds: ButtonRounded[] = ['sm', 'md', 'lg', 'xl', 'full'];
const intents: ButtonIntent[] = ['neutral', 'affirm', 'fulfill', 'risk', 'threat', 'loss'];
const colors: Array< 'primary' | 'secondary' | 'neutral'> = ['primary', 'secondary', 'neutral'];
const types: Array< 'button' | 'submit' | 'reset'> = ['button', 'submit', 'reset'];
// Gallery: all 8 color roles. Hierarchy (primary/secondary/neutral) rides the
// `color` prop; the evaluative palette (affirm/fulfill/risk/threat/loss) rides
// `intent`, which wins on data-color.
const gallery: Array< { name : string ; color ?: 'primary' | 'secondary' | 'neutral' ; intent : ButtonIntent } > = [
{ name : 'primary' , color : 'primary' , intent : 'neutral' } ,
{ name : 'secondary' , color : 'secondary' , intent : 'neutral' } ,
{ name : 'neutral' , color : 'neutral' , intent : 'neutral' } ,
{ name : 'affirm' , intent : 'affirm' } ,
{ name : 'fulfill' , intent : 'fulfill' } ,
{ name : 'risk' , intent : 'risk' } ,
{ name : 'threat' , intent : 'threat' } ,
{ name : 'loss' , intent : 'loss' }
];
feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build:
- New <Button> compound (morfo + soma + sema + eidos): 6 variants
(solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional
icon/endIcon snippets, loading state with built-in Spinner, asChild
Slot pattern, intent-driven sema (commit-action with fromProp:intent).
Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE):
- context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group:
full per-part Morfo contracts, reference-parity tables, ARIA contract
tables, keyboard tables, composition controls.
- color-field, time-field, time-picker, time-range-picker, color-picker,
month-grid, year-grid: same canonical treatment; per-part data/aria/
keyboard iteration of raw morfo, reference parity vs react-aria/
ark-ui/Chakra/bits-ui/Mantine.
Picker recipe polish:
- PickerShell.Clear/Cancel/Close refactored to use <Button size="xs">
(affects 5 pickers — date / date-range / time / time-range / color).
- TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14
/ 32 -> 28), per-channel accent (hour=primary, minute=secondary,
second=neutral; end variant muted), ruler off by default, hour ruler
cleaned to 0/6/12/18/23.
- ColorPicker: typography token --_color-picker-trigger-font-size that
scales with data-size (10/12/14px); autoFormat default true; Area
recipe override for [data-x-channel='hue'] with rainbow gradient +
saturation/brightness Y overlays.
- ValueText eidos wrapper: fix children snippet bug that suppressed
soma's displayValue fallback.
- date-field.css: segment editing affordance (cursor + hover + active
ring) via new --_date-field-accent-element token across 8 color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
// ── Live state ────────────────────────────────────────────────────────
// Soma
let intent = $state< ButtonIntent > ('neutral');
let color = $state< 'primary' | 'secondary' | 'neutral' | undefined>(undefined);
let buttonType = $state< 'button' | 'submit' | 'reset'>('button');
let disabled = $state(false);
let loading = $state(false);
let loadingText = $state('');
let ariaLabel = $state('');
// Eidos
let variant = $state< ButtonVariant > ('solid');
let size = $state< ButtonSize > ('md');
let rounded = $state< ButtonRounded | undefined > (undefined);
let shape = $state< ShapeFamily > ('rounded');
feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build:
- New <Button> compound (morfo + soma + sema + eidos): 6 variants
(solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional
icon/endIcon snippets, loading state with built-in Spinner, asChild
Slot pattern, intent-driven sema (commit-action with fromProp:intent).
Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE):
- context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group:
full per-part Morfo contracts, reference-parity tables, ARIA contract
tables, keyboard tables, composition controls.
- color-field, time-field, time-picker, time-range-picker, color-picker,
month-grid, year-grid: same canonical treatment; per-part data/aria/
keyboard iteration of raw morfo, reference parity vs react-aria/
ark-ui/Chakra/bits-ui/Mantine.
Picker recipe polish:
- PickerShell.Clear/Cancel/Close refactored to use <Button size="xs">
(affects 5 pickers — date / date-range / time / time-range / color).
- TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14
/ 32 -> 28), per-channel accent (hour=primary, minute=secondary,
second=neutral; end variant muted), ruler off by default, hour ruler
cleaned to 0/6/12/18/23.
- ColorPicker: typography token --_color-picker-trigger-font-size that
scales with data-size (10/12/14px); autoFormat default true; Area
recipe override for [data-x-channel='hue'] with rainbow gradient +
saturation/brightness Y overlays.
- ValueText eidos wrapper: fix children snippet bug that suppressed
soma's displayValue fallback.
- date-field.css: segment editing affordance (cursor + hover + active
ring) via new --_date-field-accent-element token across 8 color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
let block = $state(false);
let iconOnly = $state(false);
let spinnerPlacement = $state< 'start' | 'end'>('start');
let startIcon = $state< IconKey > ('save');
let endIcon = $state< IconKey > ('none');
// Demo content
let label = $state('Save changes');
let pressCount = $state(0);
const StartIconComp = $derived(iconMap[startIcon]);
const EndIconComp = $derived(iconMap[endIcon]);
function handlePress() {
pressCount++;
}
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(buttonMorfo);
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' )} `;
}
const somaSnippet = $derived(
[
"< script lang = 'ts' > " ,
" import * as Button from '$soma/components/button';",
'< /' + 'script>',
'',
'< Button.Provider ' ,
intent !== 'neutral' && ` intent="${ intent } "`,
color && ` color="${ color } "`,
buttonType !== 'button' && ` type="${ buttonType } "`,
disabled & & ' disabled',
loading & & ' loading',
loadingText && ` loadingText="${ loadingText } "`,
ariaLabel && ` aria-label="${ ariaLabel } "`,
`>${ label } </ Button.Provider > `
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
[
"< script lang = 'ts' > " ,
" import { Button } from '$uix/eidos/components/button';",
'< /' + 'script>',
'',
'< Button ' ,
intent !== 'neutral' && ` intent="${ intent } "`,
color && ` color="${ color } "`,
variant !== 'solid' && ` variant="${ variant } "`,
size !== 'md' && ` size="${ size } "`,
rounded && ` rounded="${ rounded } "`,
shape !== 'rounded' && ` shape="${ shape } "`,
feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build:
- New <Button> compound (morfo + soma + sema + eidos): 6 variants
(solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional
icon/endIcon snippets, loading state with built-in Spinner, asChild
Slot pattern, intent-driven sema (commit-action with fromProp:intent).
Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE):
- context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group:
full per-part Morfo contracts, reference-parity tables, ARIA contract
tables, keyboard tables, composition controls.
- color-field, time-field, time-picker, time-range-picker, color-picker,
month-grid, year-grid: same canonical treatment; per-part data/aria/
keyboard iteration of raw morfo, reference parity vs react-aria/
ark-ui/Chakra/bits-ui/Mantine.
Picker recipe polish:
- PickerShell.Clear/Cancel/Close refactored to use <Button size="xs">
(affects 5 pickers — date / date-range / time / time-range / color).
- TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14
/ 32 -> 28), per-channel accent (hour=primary, minute=secondary,
second=neutral; end variant muted), ruler off by default, hour ruler
cleaned to 0/6/12/18/23.
- ColorPicker: typography token --_color-picker-trigger-font-size that
scales with data-size (10/12/14px); autoFormat default true; Area
recipe override for [data-x-channel='hue'] with rainbow gradient +
saturation/brightness Y overlays.
- ValueText eidos wrapper: fix children snippet bug that suppressed
soma's displayValue fallback.
- date-field.css: segment editing affordance (cursor + hover + active
ring) via new --_date-field-accent-element token across 8 color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
block & & ' block',
iconOnly & & ' iconOnly',
loading & & ' loading',
loadingText && ` loadingText="${ loadingText } "`,
spinnerPlacement !== 'start' && ` spinnerPlacement="${ spinnerPlacement } "`,
disabled & & ' disabled',
ariaLabel && ` aria-label="${ ariaLabel } "`,
`>${ label } </ Button > `
]
.filter(Boolean)
.join('\n')
);
< / script >
< div data-uix-canvas-inner >
< header >
< div data-uix-eyebrow > Form · Button< / div >
< h1 data-uix-page-title > Button< / h1 >
< p data-uix-page-lede >
Single-press action surface. One doctrinal event
(< code > commit-action< / code > ) with intent bound to the consumer's
prop — drives the perceptual signature (sound + haptic) per the
canon. 6 visual variants, 8 colors (3 hierarchy + 5 evaluative
intents auto-cascading), 5 sizes, optional < code > loading< / code >
state with a built-in spinner part, < code > iconOnly< / code > square
aspect, < code > block< / code > stretch. Native button keyboard
(Enter / Space). Field-aware: < code > disabled< / code > OR-merges
with the enclosing < code > Field< / code > . Supports Radix-style
< code > asChild< / code > composition via the < code > child< / code >
snippet.
< / 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 > variants</ span > { variants . length }
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > sizes</ span > { sizes . length }
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > apg< / span >
< a href = { buttonMorfo . apg } target="_blank" rel = "noreferrer" > button ↗</ a >
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > presses</ span > { pressCount }
< / span >
< / div >
< / header >
<!-- Live preview always rendered -->
< div data-uix-stage >
< div data-uix-stage-area bind:this = { stageRef } >
< Button
{ intent }
{ color }
type={ buttonType }
{ variant }
{ size }
{ rounded }
{ shape }
feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build:
- New <Button> compound (morfo + soma + sema + eidos): 6 variants
(solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional
icon/endIcon snippets, loading state with built-in Spinner, asChild
Slot pattern, intent-driven sema (commit-action with fromProp:intent).
Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE):
- context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group:
full per-part Morfo contracts, reference-parity tables, ARIA contract
tables, keyboard tables, composition controls.
- color-field, time-field, time-picker, time-range-picker, color-picker,
month-grid, year-grid: same canonical treatment; per-part data/aria/
keyboard iteration of raw morfo, reference parity vs react-aria/
ark-ui/Chakra/bits-ui/Mantine.
Picker recipe polish:
- PickerShell.Clear/Cancel/Close refactored to use <Button size="xs">
(affects 5 pickers — date / date-range / time / time-range / color).
- TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14
/ 32 -> 28), per-channel accent (hour=primary, minute=secondary,
second=neutral; end variant muted), ruler off by default, hour ruler
cleaned to 0/6/12/18/23.
- ColorPicker: typography token --_color-picker-trigger-font-size that
scales with data-size (10/12/14px); autoFormat default true; Area
recipe override for [data-x-channel='hue'] with rainbow gradient +
saturation/brightness Y overlays.
- ValueText eidos wrapper: fix children snippet bug that suppressed
soma's displayValue fallback.
- date-field.css: segment editing affordance (cursor + hover + active
ring) via new --_date-field-accent-element token across 8 color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ block }
{ iconOnly }
{ loading }
loadingText={ loadingText || undefined }
{ spinnerPlacement }
{ disabled }
aria-label={ ariaLabel || ( iconOnly ? label : undefined )}
onPress={ handlePress }
onclick={ handlePress }
>
{ # snippet icon ()}
{ #if StartIconComp }{ @const C = StartIconComp } < C /> { /if }
{ /snippet }
{ # snippet endIcon ()}
{ #if EndIconComp }{ @const C = EndIconComp } < C /> { /if }
{ /snippet }
{ label }
< / Button >
< / div >
< div data-uix-stage-trace >
< span data-uix-stage-trace-key > trace< / span >
{ #if trace . length === 0 }
< span > click the button to fire commit-action< / span >
{ : else }
fix(demos): eliminate each_key_duplicate crash in 22 component demos
The event-trace inspector in each demo keyed its `{#each}` by a
`Date.now()` timestamp (`entry.at` / `h.at`). A single interaction can
stamp `data-event` on several elements within the same millisecond, so
two trace entries share the key and Svelte throws `each_key_duplicate`
(reported on dropdown-menu).
The trace is an ephemeral, 3-item, text-only log with no transitions or
stateful children, so positional reconciliation is correct: drop the
timestamp key (the each becomes unkeyed). The `fmtTime(...at)` display is
left intact.
Swept all 22 demos carrying the pattern (21 keyed by `entry.at` + announce
by `h.at`): dropdown-menu, context-menu, menubar, navigation-menu, listbox,
grid-list, table, tree-view, tree-grid, feed, command, carousel, drag-drop,
clipboard, announce, alert-dialog, button, color-field, link-preview,
range-calendar, time-field, time-range-field.
Verified: `as \w+ (\w+.at)` -> 0 occurrences site-wide; `npm run check`
0 errors; browser repro on dropdown-menu + table (burst of events, no
each_key_duplicate, console clean).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
{ #each trace . slice ( 0 , 3 ) as entry }
feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build:
- New <Button> compound (morfo + soma + sema + eidos): 6 variants
(solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional
icon/endIcon snippets, loading state with built-in Spinner, asChild
Slot pattern, intent-driven sema (commit-action with fromProp:intent).
Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE):
- context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group:
full per-part Morfo contracts, reference-parity tables, ARIA contract
tables, keyboard tables, composition controls.
- color-field, time-field, time-picker, time-range-picker, color-picker,
month-grid, year-grid: same canonical treatment; per-part data/aria/
keyboard iteration of raw morfo, reference parity vs react-aria/
ark-ui/Chakra/bits-ui/Mantine.
Picker recipe polish:
- PickerShell.Clear/Cancel/Close refactored to use <Button size="xs">
(affects 5 pickers — date / date-range / time / time-range / color).
- TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14
/ 32 -> 28), per-channel accent (hour=primary, minute=secondary,
second=neutral; end variant muted), ruler off by default, hour ruler
cleaned to 0/6/12/18/23.
- ColorPicker: typography token --_color-picker-trigger-font-size that
scales with data-size (10/12/14px); autoFormat default true; Area
recipe override for [data-x-channel='hue'] with rainbow gradient +
saturation/brightness Y overlays.
- ValueText eidos wrapper: fix children snippet bug that suppressed
soma's displayValue fallback.
- date-field.css: segment editing affordance (cursor + hover + active
ring) via new --_date-field-accent-element token across 8 color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< span >< span data-uix-stage-trace-event > { entry . event } </ span > · { entry . family }{ entry . intent ? ' · ' + entry . intent : '' } </ span >
< span style = "color: var(--uix-text-faint)" > { fmtTime ( entry . at )} </ span >
{ /each }
{ /if }
< span style = "margin-inline-start: auto;" >
< span data-uix-stage-trace-key > intent</ span > { intent }
< span data-uix-stage-trace-key style = "margin-inline-start: 0.5rem;" > presses</ span > { pressCount }
< / span >
< / div >
< / div >
<!-- Tabs -->
< 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 === 'gallery' } onclick= {() => ( tab = 'gallery' )} >
Gallery < span data-uix-tab-count > { variants . length } × { gallery . length } </ span >
< / button >
feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build:
- New <Button> compound (morfo + soma + sema + eidos): 6 variants
(solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional
icon/endIcon snippets, loading state with built-in Spinner, asChild
Slot pattern, intent-driven sema (commit-action with fromProp:intent).
Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE):
- context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group:
full per-part Morfo contracts, reference-parity tables, ARIA contract
tables, keyboard tables, composition controls.
- color-field, time-field, time-picker, time-range-picker, color-picker,
month-grid, year-grid: same canonical treatment; per-part data/aria/
keyboard iteration of raw morfo, reference parity vs react-aria/
ark-ui/Chakra/bits-ui/Mantine.
Picker recipe polish:
- PickerShell.Clear/Cancel/Close refactored to use <Button size="xs">
(affects 5 pickers — date / date-range / time / time-range / color).
- TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14
/ 32 -> 28), per-channel accent (hour=primary, minute=secondary,
second=neutral; end variant muted), ruler off by default, hour ruler
cleaned to 0/6/12/18/23.
- ColorPicker: typography token --_color-picker-trigger-font-size that
scales with data-size (10/12/14px); autoFormat default true; Area
recipe override for [data-x-channel='hue'] with rainbow gradient +
saturation/brightness Y overlays.
- ValueText eidos wrapper: fix children snippet bug that suppressed
soma's displayValue fallback.
- date-field.css: segment editing affordance (cursor + hover + active
ring) via new --_date-field-accent-element token across 8 color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< 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 >
Props grouped by the layer that owns them:
< span data-uix-layer-badge = "soma" > soma< / span > for intent /
disabled / loading / native button attrs;
< span data-uix-layer-badge = "eidos" > eidos< / span > for variant /
size / rounded / block / iconOnly. Semantic events live in the
< a href = "#sema" onclick = {( e ) => { e . preventDefault (); tab = 'sema' ; }} > Sema</a > tab.
< / p >
<!-- ── Soma · semantics ─────────────────────────────────────────── -->
< div data-uix-subsection-head >
< span data-uix-layer-badge = "soma" > soma< / span > Semantics
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > intent < span data-uix-control-hint > doctrinal · drives sema< / span > < / span >
< span data-uix-chips role = "radiogroup" >
{ #each intents as opt ( opt )}
< button data-uix-chip data-active = { intent === opt } onclick= {() => ( intent = opt )} > { opt } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > color < span data-uix-control-hint > only applies when intent=neutral< / span > < / span >
< span data-uix-chips role = "radiogroup" >
< button data-uix-chip data-active = { color === undefined } onclick= {() => ( color = undefined )} > default</ button >
{ #each colors as opt ( opt )}
< button data-uix-chip data-active = { color === opt } onclick= {() => ( color = opt )} disabled = { intent !== 'neutral' } > { opt } </button >
{ /each }
< / span >
< / label >
< / div >
<!-- ── Soma · behavior ──────────────────────────────────────────── -->
< div data-uix-subsection-head >
< span data-uix-layer-badge = "soma" > soma< / span > Behavior + native
< / 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 ( opt )}
< button data-uix-chip data-active = { buttonType === opt } onclick= {() => ( buttonType = opt )} > { opt } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > disabled < span data-uix-control-hint > Field-OR< / span > < / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { disabled } / >
< span data-uix-switch-label > { disabled ? 'on' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > loading < span data-uix-control-hint > renders Spinner part< / span > < / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { loading } / >
< span data-uix-switch-label > { loading ? 'on' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > loadingText < span data-uix-control-hint > replace label while loading< / span > < / span >
< input type = "text" bind:value = { loadingText } placeholder="(empty = keep children )" />
< / label >
< label data-uix-control >
< span data-uix-control-label > aria-label < span data-uix-control-hint > required when iconOnly< / span > < / span >
< input type = "text" bind:value = { ariaLabel } placeholder="(empty unless iconOnly )" />
< / label >
< / div >
<!-- ── Eidos · visual ───────────────────────────────────────────── -->
< div data-uix-subsection-head >
< span data-uix-layer-badge = "eidos" > eidos< / span > Visual treatment
< / 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 ( 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 > size< / span >
< span data-uix-chips role = "radiogroup" >
{ #each sizes as opt ( opt )}
< button data-uix-chip data-active = { size === opt } onclick= {() => ( size = opt )} > { opt } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > rounded< / span >
< span data-uix-chips role = "radiogroup" >
< button data-uix-chip data-active = { rounded === undefined } onclick= {() => ( rounded = undefined )} > default</ button >
{ #each roundeds as opt ( opt )}
< button data-uix-chip data-active = { rounded === opt } onclick= {() => ( rounded = opt )} > { opt } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > shape< / span >
< span data-uix-chips role = "radiogroup" >
{ #each SHAPE_FAMILIES as sh ( sh )}
< button data-uix-chip data-active = { shape === sh } onclick= {() => ( shape = sh )} > { sh } </ button >
{ /each }
< / span >
< / label >
feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build:
- New <Button> compound (morfo + soma + sema + eidos): 6 variants
(solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional
icon/endIcon snippets, loading state with built-in Spinner, asChild
Slot pattern, intent-driven sema (commit-action with fromProp:intent).
Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE):
- context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group:
full per-part Morfo contracts, reference-parity tables, ARIA contract
tables, keyboard tables, composition controls.
- color-field, time-field, time-picker, time-range-picker, color-picker,
month-grid, year-grid: same canonical treatment; per-part data/aria/
keyboard iteration of raw morfo, reference parity vs react-aria/
ark-ui/Chakra/bits-ui/Mantine.
Picker recipe polish:
- PickerShell.Clear/Cancel/Close refactored to use <Button size="xs">
(affects 5 pickers — date / date-range / time / time-range / color).
- TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14
/ 32 -> 28), per-channel accent (hour=primary, minute=secondary,
second=neutral; end variant muted), ruler off by default, hour ruler
cleaned to 0/6/12/18/23.
- ColorPicker: typography token --_color-picker-trigger-font-size that
scales with data-size (10/12/14px); autoFormat default true; Area
recipe override for [data-x-channel='hue'] with rainbow gradient +
saturation/brightness Y overlays.
- ValueText eidos wrapper: fix children snippet bug that suppressed
soma's displayValue fallback.
- date-field.css: segment editing affordance (cursor + hover + active
ring) via new --_date-field-accent-element token across 8 color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< label data-uix-control >
< span data-uix-control-label > block < span data-uix-control-hint > stretch to fill< / span > < / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { block } / >
< span data-uix-switch-label > { block ? 'on' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > iconOnly < span data-uix-control-hint > square aspect< / span > < / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { iconOnly } / >
< span data-uix-switch-label > { iconOnly ? 'on' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > spinnerPlacement< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 'start' , 'end' ] as opt ( opt )}
< button data-uix-chip data-active = { spinnerPlacement === opt } onclick= {() => ( spinnerPlacement = opt as 'start' | 'end' )} > { opt } </ button >
{ /each }
< / span >
< / label >
< / div >
<!-- ── Icon slots ──────────────────────────────────────────────── -->
< div data-uix-subsection-head >
< span data-uix-layer-badge = "eidos" > eidos< / span > Icon slots
< span style = "font-weight: normal; color: var(--uix-text-muted);" >
· when iconOnly is on, the start icon is the visible glyph and the label becomes sr-only
< / span >
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > icon < span data-uix-control-hint > start / sole glyph when iconOnly< / span > < / span >
< span data-uix-chips role = "radiogroup" >
{ #each iconKeys as key ( key )}
< button data-uix-chip data-active = { startIcon === key } onclick= {() => ( startIcon = key )} > { key } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > endIcon < span data-uix-control-hint > collapsed when iconOnly< / span > < / span >
< span data-uix-chips role = "radiogroup" >
{ #each iconKeys as key ( key )}
< button data-uix-chip data-active = { endIcon === key } onclick= {() => ( endIcon = key )} disabled = { iconOnly } > { key } </button >
{ /each }
< / span >
< / label >
< / div >
<!-- ── Demo content ─────────────────────────────────────────────── -->
< div data-uix-subsection-head >
Demo content
< span style = "font-weight: normal; color: var(--uix-text-muted);" >
· children literal · stays sr-only when iconOnly
< / span >
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > label< / span >
< input type = "text" bind:value = { label } / >
< / label >
< / div >
<!-- ── Code snippets ───────────────────────────────────────────── -->
< div data-uix-code >
< div data-uix-code-head >
< span data-uix-layer-badge = "soma" > soma< / span >
< span > headless · ARIA + commit-action + loading state< / 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 · adds variant, size, rounded, block, iconOnly, spinnerPlacement< / span >
< span data-uix-code-lang > svelte< / span >
< / div >
< pre >< code > { eidosSnippet } </ code ></ pre >
< / div >
< / section >
{ /if }
{ #if tab === 'gallery' }
< section data-uix-section >
< h2 data-uix-section-title > Variants × colors< / h2 >
< p data-uix-section-desc >
Every < span data-uix-layer-badge = "eidos" > eidos< / span > variant across all 8 color roles.
Hierarchy (< code > primary< / code > / < code > secondary< / code > / < code > neutral< / code > ) rides the
< code > color< / code > prop; the evaluative palette (< code > affirm< / code > / < code > fulfill< / code >
/ < code > risk< / code > / < code > threat< / code > / < code > loss< / code > ) rides < code > intent< / code > ,
which wins on < code > data-color< / code > .
< / p >
< div style = "display: flex; flex-direction: column; gap: 0.85rem;" >
{ #each variants as v }
< div style = "display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;" >
< span
style="inline-size: 60px; flex-shrink: 0; font-family: monospace; font-size: 12px; color: var(--uix-text-faint);"
>{ v } < /span
>
{ #each gallery as c }
< Button variant = { v } color= { c . color } intent = { c . intent } size="sm" > { c . name } </ Button >
{ /each }
< / div >
{ /each }
< / div >
< / section >
{ /if }
feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build:
- New <Button> compound (morfo + soma + sema + eidos): 6 variants
(solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional
icon/endIcon snippets, loading state with built-in Spinner, asChild
Slot pattern, intent-driven sema (commit-action with fromProp:intent).
Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE):
- context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group:
full per-part Morfo contracts, reference-parity tables, ARIA contract
tables, keyboard tables, composition controls.
- color-field, time-field, time-picker, time-range-picker, color-picker,
month-grid, year-grid: same canonical treatment; per-part data/aria/
keyboard iteration of raw morfo, reference parity vs react-aria/
ark-ui/Chakra/bits-ui/Mantine.
Picker recipe polish:
- PickerShell.Clear/Cancel/Close refactored to use <Button size="xs">
(affects 5 pickers — date / date-range / time / time-range / color).
- TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14
/ 32 -> 28), per-channel accent (hour=primary, minute=secondary,
second=neutral; end variant muted), ruler off by default, hour ruler
cleaned to 0/6/12/18/23.
- ColorPicker: typography token --_color-picker-trigger-font-size that
scales with data-size (10/12/14px); autoFormat default true; Area
recipe override for [data-x-channel='hue'] with rainbow gradient +
saturation/brightness Y overlays.
- ValueText eidos wrapper: fix children snippet bug that suppressed
soma's displayValue fallback.
- date-field.css: segment editing affordance (cursor + hover + active
ring) via new --_date-field-accent-element token across 8 color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ #if tab === 'api' }
< section data-uix-section >
< h2 data-uix-section-title > API reference< / h2 >
< p data-uix-section-desc >
Single-part component: the root IS the < code > < button> < / code > .
The Spinner is rendered automatically when < code > loading=true< / code >
— consumers can override the glyph via the < code > spinner< / code >
snippet prop on the root, or compose < code > Button.Spinner< / code >
explicitly inside a < code > child< / code > snippet for total control.
< / p >
< div data-uix-subsection-head > Button (root)< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Prop< / th > < th > Type< / th > < th > Default< / th > < th > Description< / th > < / tr > < / thead >
< tbody >
< tr > < td class = "name" > id< / td > < td class = "type" > string< / td > < td class = "default empty" > auto< / td > < td > Stable identifier.< / td > < / tr >
< tr > < td class = "name" > intent< / td > < td class = "type" > 'neutral' | 'affirm' | 'fulfill' | 'risk' | 'threat' | 'loss'< / td > < td class = "default" > 'neutral'< / td > < td > Doctrinal consequence intent. Drives sema perceptual signature; non-neutral wins over < code > color< / code > .< / td > < / tr >
< tr > < td class = "name" > color< / td > < td class = "type" > 'primary' | 'secondary' | 'neutral'< / td > < td class = "default empty" > — (resolves to 'primary' when intent='neutral')< / td > < td > Hierarchical override. Only applies when < code > intent='neutral'< / code > .< / td > < / tr >
< tr > < td class = "name" > type< / td > < td class = "type" > 'button' | 'submit' | 'reset'< / td > < td class = "default" > 'button'< / td > < td > Native button type.< / td > < / tr >
< tr > < td class = "name" > disabled < span data-uix-tag data-kind = "field" > field-OR< / span > < / td > < td class = "type" > boolean< / td > < td class = "default" > false< / td > < td > OR-merges with Field.Provider.< / td > < / tr >
< tr > < td class = "name" > loading< / td > < td class = "type" > boolean< / td > < td class = "default" > false< / td > < td > While true, gates click handler, sets < code > aria-busy< / code > , renders Spinner part.< / td > < / tr >
< tr > < td class = "name" > loadingText< / td > < td class = "type" > string< / td > < td class = "default empty" > —< / td > < td > Replace children visibly while loading (children remain sr-only).< / td > < / tr >
< tr > < td class = "name" > spinnerPlacement < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > 'start' | 'end'< / td > < td class = "default" > 'start'< / td > < td > Where to render the spinner.< / td > < / tr >
< tr > < td class = "name" > aria-label< / td > < td class = "type" > string< / td > < td class = "default empty" > —< / td > < td > Accessible name. < strong > Required< / strong > when < code > iconOnly< / code > .< / td > < / tr >
< tr >< td class = "name" > onPress</ td >< td class = "type" > { `({ intent }) => void` } </ td >< td class = "default empty" > —</ td >< td > Read-only feedback fired alongside < code > commit-action</ code > . For business logic use native < code > onclick</ code > .</ td ></ tr >
< tr > < td class = "name" > variant < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > 'solid' | 'soft' | 'surface' | 'outline' | 'ghost' | 'plain'< / td > < td class = "default" > 'solid'< / td > < td > Visual treatment.< / td > < / tr >
< tr > < td class = "name" > size < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > ResponsiveProp< 'xs' | 'sm' | 'md' | 'lg' | 'xl'> < / td > < td class = "default" > 'md'< / td > < td > T-shirt scale.< / td > < / tr >
< tr > < td class = "name" > rounded < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > ResponsiveProp< 'sm' | 'md' | 'lg' | 'xl' | 'full'> < / td > < td class = "default empty" > inherits from size< / td > < td > Corner radius override.< / 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 to fill parent inline-size.< / 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, padding-inline collapsed.< / td > < / tr >
< tr > < td class = "name" > spinner < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > Snippet< / td > < td class = "default empty" > —< / td > < td > Override the default spinner glyph.< / td > < / tr >
< tr >< td class = "name" > child</ td >< td class = "type" > { `Snippet<[{ loading, disabled, props }]>` } </ td >< td class = "default empty" > —</ td >< td > Slot composition (Radix asChild pattern). Spread < code > props</ code > into your own element.</ td ></ tr >
< tr > < td class = "name" > …native button attrs< / td > < td class = "type" > PrimitiveButtonAttributes< / td > < td class = "default empty" > —< / td > < td > onclick, form, formaction, autofocus, etc.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Button.Spinner< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Prop< / th > < th > Type< / th > < th > Default< / th > < th > Description< / th > < / tr > < / thead >
< tbody >
< tr > < td class = "name" > id< / td > < td class = "type" > string< / td > < td class = "default empty" > auto< / td > < td > —< / td > < / tr >
< tr > < td class = "name" > children< / td > < td class = "type" > Snippet< / td > < td class = "default empty" > default ring SVG< / td > < td > Override the spinner glyph.< / td > < / tr >
< tr > < td class = "name" > …native span attrs< / td > < td class = "type" > PrimitiveSpanAttributes< / td > < td class = "default empty" > —< / td > < td > —< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Reference parity< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Feature< / th > < th > Radix< / th > < th > bits-ui< / th > < th > shadcn< / th > < th > Chakra v3< / th > < th > Mantine< / th > < th > MUI< / th > < th > Eidos (this)< / th > < / tr > < / thead >
< tbody >
< tr > < td > Component exists< / td > < td > —< / td > < td > ✓ (thin)< / td > < td > ✓< / td > < td > ✓< / td > < td > ✓< / td > < td > ✓< / td > < td > ✓< / td > < / tr >
< tr > < td > variant count< / td > < td > —< / td > < td > —< / td > < td > 6< / td > < td > 7< / td > < td > 8< / td > < td > 3< / td > < td > 6< / td > < / tr >
< tr > < td > color palette< / td > < td > —< / td > < td > —< / td > < td > implicit< / td > < td > palette token< / td > < td > color< / td > < td > color< / td > < td > 8 (3 hierarchy + 5 intents)< / td > < / tr >
< tr > < td > sizes< / td > < td > —< / td > < td > —< / td > < td > 4< / td > < td > 6< / td > < td > 5< / td > < td > 3< / td > < td > 5< / td > < / tr >
< tr > < td > loading state< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > ✓< / td > < td > ✓< / td > < td > ✓< / td > < td > ✓< / td > < / tr >
< tr > < td > loadingText< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > ✓< / td > < td > —< / td > < td > —< / td > < td > ✓< / td > < / tr >
< tr > < td > spinnerPlacement< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > ✓< / td > < td > —< / td > < td > —< / td > < td > ✓< / td > < / tr >
< tr > < td > iconOnly square< / td > < td > —< / td > < td > —< / td > < td > 'icon' size< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > ✓< / td > < / tr >
< tr > < td > block / fullWidth< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > ✓< / td > < td > ✓< / td > < td > ✓< / td > < / tr >
< tr > < td > rounded scale< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > radius< / td > < td > —< / td > < td > ✓< / td > < / tr >
< tr > < td > asChild / Slot< / td > < td > ✓< / td > < td > ✓< / td > < td > ✓< / td > < td > ✓< / td > < td > —< / td > < td > —< / td > < td > ✓< / td > < / tr >
< tr > < td > commit-action semantic event< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > ✓< / td > < / tr >
< tr > < td > per-intent perceptual signature< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > —< / td > < td > ✓< / td > < / tr >
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'morfo' }
< section data-uix-section >
< h2 data-uix-section-title >
< span data-uix-layer-badge = "morfo" > morfo< / span >
· declarative contract
< / h2 >
< p data-uix-section-desc >
2 parts (Provider + optional Spinner), 1 event
(< code > commit-action< / code > with intent fromProp). Sequence
< em > pre< / em > — perceptual signal lands at click, before the
consumer's onclick. Source:
< code > src/uix/morfo/components/button.ts< / code > .
< / p >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Field< / th > < th > Value< / th > < / tr > < / thead >
< tbody >
< tr >< td class = "name" > name</ td >< td class = "type" > "{ buttonMorfo . name } "</ td ></ tr >
< tr >< td class = "name" > kebab</ td >< td class = "type" > "{ buttonMorfo . kebab } "</ td ></ tr >
< tr >< td class = "name" > scope</ td >< td class = "type" > [{ buttonMorfo . scope . map (( s ) => `"$ { s } " `).join(', ') } ]</ td ></ tr >
< tr >< td class = "name" > apg</ td >< td >< a href = { buttonMorfo . apg } target="_blank" rel = "noreferrer" > { buttonMorfo . apg } </ a ></ td ></ tr >
< tr >< td class = "name" > parts.length</ td >< td class = "default" > { buttonMorfo . parts . length } </ td ></ tr >
< tr >< td class = "name" > events.length</ td >< td class = "default" > { buttonMorfo . events ? . length ?? 0 } </ td ></ tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Parts< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Part< / th > < th > Marker< / th > < th > Element< / th > < th > Role< / th > < th > Archetype< / th > < th > States< / th > < th > Optional< / th > < / tr > < / thead >
< tbody >
{ #each partsList as part ( part . kebab )}
< tr >
< td class = "name" > { part . kebab } </ td >
< td >< code data-uix-part-marker > [{ part . marker } ]</ code ></ td >
< td class = "type" > < { part . defaultElement } > </ td >
< td class = "type" > { part . role ?? '—' } </ td >
< td class = "default" > { part . archetype ?? '—' } </ td >
< td class = "default" > { part . states . length ? part . states . join ( ' | ' ) : '—' } </ td >
< td class = "default" > { part . optional ? 'yes' : 'no' } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
{ #each buttonMorfo . parts as rawPart ( rawPart . kebab )}
{ @const partAny = rawPart as unknown as { kebab : string ; aria? : ReadonlyArray < { attr : string ; value : { kind : string }; condition ?: { when : string ; prop? : string ; part? : string }; severity? : string } > ; keyboard? : ReadonlyArray < { key : string ; action : string } > }}
{ @const dataAttrs = compiled . contracts . dataAttrsByPart . get ( partAny . kebab ) ?? []}
{ @const ariaAttrs = partAny . aria ?? []}
{ @const keyboard = partAny . keyboard ?? []}
{ #if dataAttrs . length || ariaAttrs . length || keyboard . length }
< div data-uix-subsection-head > { partAny . kebab } </ div >
{ #if dataAttrs . length }
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > data-attr< / th > < th > Values< / th > < th > Source kind< / th > < / tr > < / thead >
< tbody >
{ #each dataAttrs as attr ( attr . attr )}
< tr >
< td class = "name" > { attr . attr } </ td >
< td class = "type" > { attr . values ? attr . values . join ( ' | ' ) : '—' } </ td >
< td class = "default" > { 'value' in attr && attr . value ? ( attr as { value : { kind : string } }). value . kind : '—' } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
{ /if }
{ #if ariaAttrs . length }
< div data-uix-table-wrap style = "margin-top: var(--uix-space-2);" >
< table data-uix-table >
< thead > < tr > < th > aria-attr< / th > < th > Source kind< / th > < th > Condition< / th > < th > Severity< / th > < / tr > < / thead >
< tbody >
{ #each ariaAttrs as a ( a . attr )}
< tr >
< td class = "name" > { a . attr } </ td >
< td class = "type" > { a . value . kind } </ td >
< td class = "default" > { a . condition ? `when $ { a . condition . when } $ { a . condition . prop ? ` ($ { a . condition . prop }) ` : '' } $ { a . condition . part ? ` ($ { a . condition . part }) ` : '' } ` : 'always' } </ td >
< td class = "default" > { a . severity ?? 'required' } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
{ /if }
{ #if keyboard . length }
< div data-uix-table-wrap style = "margin-top: var(--uix-space-2);" >
< table data-uix-table >
< thead > < tr > < th > key< / th > < th > action< / th > < / tr > < / thead >
< tbody >
{ #each keyboard as kb ( kb . key )}
< tr >
< td >< span data-uix-kbd > { kb . key === ' ' ? 'Space' : kb . key } </ span ></ td >
< td class = "default" > { kb . action } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
{ /if }
{ /if }
{ /each }
< div data-uix-subsection-head > Events declaration< / div >
< p data-uix-section-desc style = "margin-top: 0.25rem;" >
< code > commit-action< / code > fires with sequence < em > pre< / em > so
the perceptual signal lands at the moment of the click; the
consumer's onclick may then run async work without blocking.
Intent is bound to the < code > intent< / code > prop; supported
intents are the full commit-friendly set (neutral / affirm /
fulfill / risk / threat / loss).
< / 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 > target< / th > < th > prewrite< / th > < th > commit< / th > < / tr > < / thead >
< tbody >
{ #each events as action ( action . name )}
{ @const sem = action . semantic }
{ @const intentDecl = 'intent' in sem ? sem.intent : undefined }
{ @const intentStr = typeof intentDecl === 'string' ? intentDecl : intentDecl ? `fromProp:$ {( intentDecl as { fromProp? : string }). fromProp ?? '?' } ( default $ {( intentDecl as { default ?: string }). default ?? '?' }) ` : '—' }
< 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" > { intentStr } </ td >
< td > { action . target } </ td >
< td class = "default" > { action . prewrite . length ? action . prewrite . map (( p ) => `$ { p . attr } = $ { p . value } `).join(', ') : '—' } </ td >
< td class = "default" > { action . commit ? `$ { action . commit . attr } = $ { action . commit . value } ` : '—' } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'sema' }
< section data-uix-section id = "sema" >
< 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-action< / code > from the
< code > commit< / code > family. Each press fires it with sequence
< em > pre< / em > — the perceptual signal lands at the click moment,
before any async work in the consumer's onclick. Intent is per-
button via < code > fromProp:intent< / code > and modulates the
non-visual signature (sound timbre + haptic curve) via
< code > SEMA_MAP.intents< / 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 ( action . name )}
{ @const sem = action . semantic }
{ @const intentDecl = 'intent' in sem ? sem.intent : undefined }
{ @const effectiveIntent =
typeof intentDecl === 'string'
? intentDecl
: intentDecl
? intent
: 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 ?? '—' } < span style = "color: var(--uix-text-faint)" > (live)</ span ></ td >
< td >
< button
data-uix-play
onclick={() => {
const target = stageRef?.querySelector('[data-button]') as HTMLElement | null;
if (!target) return;
void uix.events?.emit({
name: action.name,
family: sem.family,
target,
...(effectiveIntent ? { intent : effectiveIntent } : {} )
});
}}
>▶ 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 >
Source: < code > src/uix/eidos/components/button/button.css< / code > .
Recipe tokens declared in < code > src/uix/eidos/lib/recipes/base.ts< / code >
under the < code > button< / code > key. Six variant slices map the
resolved palette to < code > bg / fg / border / hover-*< / code >
private vars; the size cascade overrides height / padding /
gap / font-size / icon-size; the per-color cascade re-binds
the palette block (8 system tokens).
< / p >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Selector< / th > < th > Source< / th > < th > Concern< / th > < / tr > < / thead >
< tbody >
< tr > < td class = "name" > [data-button]< / td > < td > < span data-uix-tag data-kind = "soma" > morfo< / span > < / td > < td > Base layout: inline-flex, gap, height, padding, border, radius, transitions.< / td > < / tr >
< tr > < td class = "name" > [data-button][data-size='xs' | 'sm' | 'md' | 'lg' | 'xl']< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Size token cascade.< / td > < / tr >
< tr > < td class = "name" > [data-button][data-color='X']< / td > < td > < span data-uix-tag data-kind = "soma" > morfo< / span > < / td > < td > Per-color palette cascade (8 system tokens).< / td > < / tr >
< tr > < td class = "name" > [data-button][data-variant='solid' | 'soft' | 'surface' | 'outline' | 'ghost' | 'plain']< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Variant slice maps palette → bg/fg/border/hover.< / td > < / tr >
< tr > < td class = "name" > [data-button][data-rounded='X']< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Corner radius override.< / td > < / tr >
< tr > < td class = "name" > [data-button][data-block]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > display: flex + inline-size: 100%.< / td > < / tr >
< tr > < td class = "name" > [data-button][data-icon-only]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Square aspect, padding-inline: 0.< / td > < / tr >
< tr > < td class = "name" > [data-button][data-loading]< / td > < td > < span data-uix-tag data-kind = "soma" > morfo< / span > < / td > < td > Cursor progress + pointer-events lock.< / td > < / tr >
< tr > < td class = "name" > [data-button][data-disabled]< / td > < td > < span data-uix-tag data-kind = "soma" > morfo< / span > < / td > < td > Opacity + not-allowed cursor.< / td > < / tr >
< tr > < td class = "name" > [data-button]:hover / :active / :focus-visible< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Interaction states.< / td > < / tr >
< tr > < td class = "name" > [data-button-spinner]< / td > < td > < span data-uix-tag data-kind = "soma" > morfo< / span > < / td > < td > Rotating ring SVG container.< / td > < / tr >
< tr > < td class = "name" > .eidos-button-spinner-ring< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > CSS @keyframes rotate animation.< / td > < / tr >
< tr > < td class = "name" > .eidos-button-body< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Inline-flex wrapper for children + gap.< / td > < / tr >
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'a11y' }
< section data-uix-section >
< h2 data-uix-section-title > Accessibility< / h2 >
< p data-uix-section-desc >
Follows the APG < a href = { buttonMorfo . apg } target="_blank" rel = "noreferrer" > button pattern</ a > .
Renders a native < code > < button> < / code > — gets the full
native keyboard + focus model. < code > iconOnly< / code > consumers
MUST set < code > aria-label< / code > . While loading, the button
exposes < code > aria-busy='true'< / code > so screen readers
announce the deferred state.
< / p >
< div data-uix-subsection-head > Keyboard< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Key< / th > < th > Action< / th > < / tr > < / thead >
< tbody >
< tr > < td > < span data-uix-kbd > Enter< / span > < / td > < td > Activate (native).< / td > < / tr >
< tr > < td > < span data-uix-kbd > Space< / span > < / td > < td > Activate (native).< / td > < / tr >
< tr > < td > < span data-uix-kbd > Tab< / span > / < span data-uix-kbd > Shift+Tab< / span > < / td > < td > Move focus to next / previous focusable.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > ARIA contract< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Part< / th > < th > Attribute< / th > < th > Value< / th > < / tr > < / thead >
< tbody >
< tr >< td class = "name" > provider</ td >< td > type</ td >< td class = "type" > { `{type}` } (default "button" — explicit to avoid implicit submit in forms)</ td ></ tr >
< tr > < td class = "name" > provider< / td > < td > data-state< / td > < td class = "type" > "idle" | "loading"< / td > < / tr >
< tr > < td class = "name" > provider< / td > < td > data-color< / td > < td class = "type" > resolved color (intent wins when evaluative)< / td > < / tr >
< tr > < td class = "name" > provider< / td > < td > data-loading< / td > < td class = "type" > "true" when loading< / td > < / tr >
< tr > < td class = "name" > provider< / td > < td > data-disabled< / td > < td class = "type" > "true" when disabled< / td > < / tr >
< tr > < td class = "name" > provider< / td > < td > aria-busy< / td > < td class = "type" > "true" when loading< / td > < / tr >
< tr > < td class = "name" > provider< / td > < td > aria-disabled< / td > < td class = "type" > "true" when disabled< / td > < / tr >
< tr >< td class = "name" > provider</ td >< td > aria-label</ td >< td class = "type" > { `{aria-label}` } when set (required for iconOnly)</ td ></ tr >
< tr > < td class = "name" > spinner< / td > < td > aria-hidden< / td > < td class = "type" > "true" (decorative — aria-busy on provider conveys state to AT)< / td > < / tr >
< / tbody >
< / table >
< / div >
< / section >
{ /if }
< / div >