< script lang = "ts" >
import {
Combobox,
type ComboboxColor,
type ComboboxSize,
type ComboboxVariant
} from '$uix/eidos/components/combobox';
import { compileMorfo } from '$uix/morfo';
import { comboboxMorfo } from '@/uix/morfo/components/combobox';
import type { Intent } from '$uix/intent';
import { getActiveUix } from '$active-uix';
import { page } from '$app/state';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type ComboboxMode = 'single' | 'multiple';
type Option = { value : string ; label : string ; group : string ; disabled? : boolean } ;
type TraceEntry = { event : string ; family : string ; intent? : string ; at : number } ;
const uix = getActiveUix();
const params = page.url.searchParams;
const options: readonly Option[] = [
{ value : 'svelte' , label : 'Svelte' , group : 'Frontend' } ,
{ value : 'react' , label : 'React' , group : 'Frontend' } ,
{ value : 'vue' , label : 'Vue' , group : 'Frontend' } ,
{ value : 'solid' , label : 'Solid' , group : 'Frontend' } ,
{ value : 'qwik' , label : 'Qwik' , group : 'Frontend' } ,
{ value : 'angular' , label : 'Angular' , group : 'Frontend' } ,
{ value : 'node' , label : 'Node' , group : 'Runtime' } ,
{ value : 'deno' , label : 'Deno' , group : 'Runtime' } ,
{ value : 'bun' , label : 'Bun' , group : 'Runtime' , disabled : true } ,
{ value : 'vite' , label : 'Vite' , group : 'Build' } ,
{ value : 'astro' , label : 'Astro' , group : 'Build' } ,
{ value : 'next' , label : 'Next.js' , group : 'Build' } ,
{ value : 'nuxt' , label : 'Nuxt' , group : 'Build' } ,
{ value : 'tanstack-query' , label : 'TanStack Query' , group : 'Data' } ,
{ value : 'trpc' , label : 'tRPC' , group : 'Data' } ,
{ value : 'zod' , label : 'Zod' , group : 'Data' } ,
{ value : 'vitest' , label : 'Vitest' , group : 'Testing' } ,
{ value : 'playwright' , label : 'Playwright' , group : 'Testing' } ,
{ value : 'storybook' , label : 'Storybook' , group : 'Testing' }
];
const groups = [...new Set(options.map((option) => option.group))];
eidos: theming fixes + size/variant parity batch + docs
- theme: add surface.muted + content.muted to color contract (neutral-3
+ neutral-10). Plugs 17 broken --color-content-muted and 3 broken
--color-surface-muted references in recipes/components.
- form.css: fix --color-neutral-element-hover typo → --color-neutral-hover.
- archetypes.css + events.css: replace raw hsl/rgba indigo with
color-mix(var(--color-primary-solid) …) — no raw colors left in eidos.
- combobox dark scrollbar: unscope ::-webkit-scrollbar rules in uix.css
and duplicate --uix-line on :root + :root[data-mode='dark'] so portaled
overlays (Combobox listbox, Popover, Dialog, Drawer) inherit the theme.
- sizes: 20 components expand from sm/md/lg to xs..xl (form controls,
text inputs, progress/meter, field/form) or xs..lg (nav controls:
breadcrumb, pagination, tag-group, toolbar). Composite panels keep
sm/md/lg deliberately.
- variants: field + toolbar drop arbitrary ControlVariant narrowings;
both expose all 3 (surface | outline | ghost) with new outline CSS.
- pagination demo: disambiguate siblingCount/boundaryCount as "per side"
in label + API table (Radix/MUI convention).
- docs: CHECKLIST §C-2.6 (contract token presence) + §D-7.4 (chip parity)
+ §D-7.5 (size category) added. DEMO_AUTHORING §12.7 (chip parity) +
§12.8 (size category cheatsheet) added. eidos/README +
active_architecture.md sync texts/migration nomenclature.
- PENDIENTES.md: normas N-1..N-5 implantadas en esta sesión.
Verification: 88/88 eidos tests, 0 type errors, 67/67 component:audit PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
const sizes: ComboboxSize[] = ['xs', 'sm', 'md', 'lg', 'xl'];
const variants: ComboboxVariant[] = ['surface', 'outline', 'ghost'];
const colors: ComboboxColor[] = [
'primary',
'secondary',
'neutral',
'affirm',
'fulfill',
'risk',
'threat',
'loss'
];
let tab = $state< Tab > ('live');
let value = $state< string [ ] > (['svelte']);
let inputValue = $state('');
let open = $state(true);
let mode = $state< ComboboxMode > ('single');
let disabled = $state(params.get('perm.disabled') === 'true');
let required = $state(false);
let allowDeselect = $state(true);
let allowsCustomValue = $state(false);
let loop = $state(false);
let size = $state< ComboboxSize > ('md');
let variant = $state< ComboboxVariant > ('surface');
let color = $state< ComboboxColor > ('primary');
let matchAnchorWidth = $state(true);
let trace = $state< TraceEntry [ ] > ([]);
let stageRef = $state< HTMLElement | null > (null);
const compiled = compileMorfo(comboboxMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
const filteredOptions = $derived.by(() => {
const q = inputValue.trim().toLocaleLowerCase();
if (!q) return [...options];
return options.filter((option) => option.label.toLocaleLowerCase().includes(q));
});
const selectedLabels = $derived(
options.filter((option) => value.includes(option.value)).map((option) => option.label)
);
$effect(() => {
if (mode === 'single' & & value.length > 1) value = value.slice(0, 1);
});
$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();
});
function fmtTime(at: number): string {
const d = new Date(at);
return `${ String ( d . getSeconds ()). padStart ( 2 , '0' )} .${ String ( d . getMilliseconds ()). padStart ( 3 , '0' )} `;
}
function intentLiteral(value: unknown): Intent | undefined {
return typeof value === 'string' ? (value as Intent) : undefined;
}
const somaSnippet = $derived(
[
"< script lang = 'ts' > " ,
" import * as Combobox from '$soma/components/combobox';",
" let value = $state(['svelte']);",
" let inputValue = $state('');",
" let open = $state(false);",
'< /' + 'script>',
'',
'< Combobox.Provider ' ,
' bind:value',
' bind:inputValue',
' bind:open',
` type="${ mode } "`,
allowDeselect & & ' allowDeselect',
loop & & ' loop',
required & & ' required',
' name="framework"',
'>',
' < Combobox.Input placeholder = "Search framework..." / > ',
' < Combobox.Trigger > Open< / Combobox.Trigger > ',
' < Combobox.Content sideOffset = { 6 } > ',
' < Combobox.Viewport > ',
' < Combobox.Group > ',
' < Combobox.GroupHeading > Frontend< / Combobox.GroupHeading > ',
' < Combobox.Item value = "svelte" label = "Svelte" > Svelte< / Combobox.Item > ',
' < Combobox.Item value = "react" label = "React" > React< / Combobox.Item > ',
' < Combobox.Item value = "vue" label = "Vue" > Vue< / Combobox.Item > ',
' < / Combobox.Group > ',
' < Combobox.Group > ',
' < Combobox.GroupHeading > Runtime< / Combobox.GroupHeading > ',
' < Combobox.Item value = "node" label = "Node" > Node< / Combobox.Item > ',
' < Combobox.Item value = "deno" label = "Deno" > Deno< / Combobox.Item > ',
' < / Combobox.Group > ',
' < / Combobox.Viewport > ',
' < / Combobox.Content > ',
'< / Combobox.Provider > '
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
[
"< script lang = 'ts' > " ,
" import { Combobox } from '$uix/eidos/components/combobox';",
" let value = $state(['svelte']);",
" let inputValue = $state('');",
'< /' + 'script>',
'',
'< Combobox ' ,
' bind:value',
' bind:inputValue',
` type="${ mode } "`,
size !== 'md' && ` size="${ size } "`,
variant !== 'surface' && ` variant="${ variant } "`,
color !== 'primary' && ` color="${ color } "`,
' name="framework"',
'>',
' < Combobox.Control > ',
' < Combobox.Input placeholder = "Search framework..." / > ',
' < Combobox.Trigger > < Combobox.Indicator / > < / Combobox.Trigger > ',
' < / Combobox.Control > ',
' < Combobox.Portal > ',
' < Combobox.Content sideOffset = { 6 } > ',
' < Combobox.Viewport > ',
' < Combobox.Group > ',
' < Combobox.GroupHeading > Frontend< / Combobox.GroupHeading > ',
' < Combobox.Item value = "svelte" label = "Svelte" > ',
' < span > Svelte< / span > ',
' < Combobox.ItemIndicator / > ',
' < / Combobox.Item > ',
' < Combobox.Item value = "react" label = "React" > ',
' < span > React< / span > ',
' < Combobox.ItemIndicator / > ',
' < / Combobox.Item > ',
' < Combobox.Item value = "vue" label = "Vue" > ',
' < span > Vue< / span > ',
' < Combobox.ItemIndicator / > ',
' < / Combobox.Item > ',
' < / Combobox.Group > ',
' < Combobox.Group > ',
' < Combobox.GroupHeading > Runtime< / Combobox.GroupHeading > ',
' < Combobox.Item value = "node" label = "Node" > ',
' < span > Node< / span > ',
' < Combobox.ItemIndicator / > ',
' < / Combobox.Item > ',
' < Combobox.Item value = "deno" label = "Deno" > ',
' < span > Deno< / span > ',
' < Combobox.ItemIndicator / > ',
' < / Combobox.Item > ',
' < / Combobox.Group > ',
' < / Combobox.Viewport > ',
' < Combobox.Arrow / > ',
' < / Combobox.Content > ',
' < / Combobox.Portal > ',
'< / Combobox > '
]
.filter(Boolean)
.join('\n')
);
< / script >
< svelte:head >
< meta name = "perm-axes" content = "disabled" / >
< / svelte:head >
< div data-uix-canvas-inner >
< header >
< div data-uix-eyebrow > Forms · Combobox< / div >
< h1 data-uix-page-title > Combobox< / h1 >
< p data-uix-page-lede >
Editable input with a floating listbox. Soma owns input text, open state, selection,
keyboard navigation, groups and form values; Eidos owns the control layout, trigger icon,
selected item indicator and panel recipe.
< / p >
< div data-uix-page-meta >
< span data-uix-meta-pill >< span data-uix-meta-key > parts</ span > { partsList . 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 > mode</ span > { mode } </ span >
< span data-uix-meta-pill >< span data-uix-meta-key > scope</ span > { comboboxMorfo . scope . join ( ' · ' )} </ span >
< / div >
< / header >
< div data-uix-stage >
< div
data-uix-stage-area
bind:this={ stageRef }
style="display: grid; gap: 1rem; align-items: start; inline-size: min(26rem, 100%);"
>
< Combobox
bind:value
bind:inputValue
bind:open
type={ mode }
{ disabled }
{ required }
{ allowDeselect }
{ allowsCustomValue }
{ loop }
{ size }
{ variant }
{ color }
name="framework"
>
< Combobox.Control >
< Combobox.Input
placeholder="Search framework..."
data-perm-step={ ! disabled ? '0' : undefined }
data-perm-mode={ ! disabled ? 'type="re"' : undefined }
data-perm-label={ ! disabled ? 'type query' : undefined }
/>
< Combobox.ClearButton / >
< Combobox.Trigger >
< Combobox.Indicator / >
< / Combobox.Trigger >
< / Combobox.Control >
< Combobox.Portal >
< Combobox.Content sideOffset = { 6 } { matchAnchorWidth } >
< Combobox.Viewport >
{ #each groups as group }
{ @const groupOptions = filteredOptions . filter (( option ) => option . group === group )}
{ #if groupOptions . length }
< Combobox.Group >
< Combobox.GroupHeading > { group } </ Combobox.GroupHeading >
{ #each groupOptions as option }
< Combobox.Item
value={ option . value }
label={ option . label }
disabled={ option . disabled }
data-perm-step={ ! disabled && option . value === 'react' ? '1' : undefined }
data-perm-label={ ! disabled && option . value === 'react' ? 'select react' : undefined }
>
< span > { option . label } </ span >
< Combobox.ItemIndicator / >
< / Combobox.Item >
{ /each }
< / Combobox.Group >
{ /if }
{ /each }
< / Combobox.Viewport >
< Combobox.Arrow / >
< / Combobox.Content >
< / Combobox.Portal >
< / Combobox >
< / div >
< div data-uix-stage-trace >
< span data-uix-stage-trace-key > trace< / span >
{ #if trace . length === 0 }
< span > type and select an item to see Sema events< / span >
{ : else }
{ #each trace . slice ( 0 , 3 ) as entry }
< 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 > value< / span >
{ selectedLabels . length ? selectedLabels . join ( ', ' ) : 'none' }
< / 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 > 9< / 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 >
Controls are grouped by owner. The demo filters rendered items from
< code > inputValue< / code > ; Soma owns the input state, but the consumer owns its
collection query.
< / p >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "soma" > soma< / span > props · headless behavior
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > type< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 'single' , 'multiple' ] as item }
< button
data-uix-chip
data-active={ mode === item }
onclick={() => ( mode = item as ComboboxMode )}
>
{ item }
< / button >
{ /each }
< / span >
< / label >
< label data-uix-switch >
< input type = "checkbox" bind:checked = { disabled } / >
< span data-uix-switch-label > disabled< / span >
< / label >
< label data-uix-switch >
< input type = "checkbox" bind:checked = { required } / >
< span data-uix-switch-label > required< / span >
< / label >
< label data-uix-switch >
< input type = "checkbox" bind:checked = { allowDeselect } / >
< span data-uix-switch-label > allowDeselect< / span >
< / label >
< label data-uix-switch >
< input type = "checkbox" bind:checked = { allowsCustomValue } / >
< span data-uix-switch-label > allowsCustomValue< / span >
< / label >
< label data-uix-switch >
< input type = "checkbox" bind:checked = { loop } / >
< span data-uix-switch-label > loop< / span >
< / label >
< / div >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "eidos" > eidos< / span > props · visual treatment
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > size< / span >
< span data-uix-chips role = "radiogroup" >
{ #each sizes as item }
< button data-uix-chip data-active = { size === item } onclick= {() => ( size = item )} >
{ item }
< / button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > variant< / span >
< span data-uix-chips role = "radiogroup" >
{ #each variants as item }
< button data-uix-chip data-active = { variant === item } onclick= {() => ( variant = item )} >
{ item }
< / button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > color< / span >
< span data-uix-chips role = "radiogroup" >
{ #each colors as item }
< button data-uix-chip data-active = { color === item } onclick= {() => ( color = item )} >
{ item }
< / button >
{ /each }
< / span >
< / label >
< label data-uix-switch >
< input type = "checkbox" bind:checked = { matchAnchorWidth } / >
< span data-uix-switch-label > matchAnchorWidth< / span >
< / label >
< / div >
< div data-uix-code >
< div data-uix-code-head >
< span data-uix-layer-badge = "soma" > soma< / span >
< span > headless · input + listbox behavior< / span >
< span data-uix-code-lang > svelte< / span >
< / div >
< pre >< code > { somaSnippet } </ code ></ pre >
< / div >
< div data-uix-code >
< div data-uix-code-head >
< span data-uix-layer-badge = "eidos" > eidos< / span >
< span > visual · control/content recipe< / 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 comparison< / h2 >
< div data-uix-table-wrap >
< table data-uix-table >
< thead >
< tr > < th > Feature< / th > < th > Air< / th > < th > Radix< / th > < th > Bits< / th > < th > Ark< / th > < th > React Aria< / th > < th > UIX< / th > < th > Decision< / th > < / tr >
< / thead >
< tbody >
< tr > < td class = "name" > Editable input + listbox< / td > < td > ✅< / td > < td > ❌< / td > < td > ✅< / td > < td > ✅< / td > < td > ✅< / td > < td > ✅< / td > < td > Covered.< / td > < / tr >
< tr > < td class = "name" > Control wrapper< / td > < td > ✅< / td > < td > ❌< / td > < td > ✅< / td > < td > ✅< / td > < td > ✅< / td > < td > ✅< / td > < td > Eidos-only visual layout.< / td > < / tr >
< tr > < td class = "name" > Portal + floating + arrow< / td > < td > ✅< / td > < td > ❌< / td > < td > ✅< / td > < td > ✅< / td > < td > ✅< / td > < td > ✅< / td > < td > Soma owns floating.< / td > < / tr >
< tr > < td class = "name" > Groups + separator< / td > < td > ✅< / td > < td > ❌< / td > < td > ✅< / td > < td > ✅< / td > < td > ✅< / td > < td > ✅< / td > < td > Covered.< / td > < / tr >
< tr > < td class = "name" > Multiple select< / td > < td > ✅< / td > < td > ❌< / td > < td > ✅< / td > < td > ✅< / td > < td > ❌< / td > < td > ✅< / td > < td > Soma type="multiple".< / td > < / tr >
< tr > < td class = "name" > Clear trigger< / td > < td > ❌< / td > < td > ❌< / td > < td > ⚠️< / td > < td > ✅< / td > < td > ✅< / td > < td > ❌< / td > < td > Defer to Soma.< / td > < / tr >
< tr > < td class = "name" > Scroll buttons< / td > < td > ✅< / td > < td > ❌< / td > < td > ✅< / td > < td > ⚠️< / td > < td > ❌< / td > < td > ❌< / td > < td > Defer to Soma.< / td > < / tr >
< tr > < td class = "name" > Menu trigger behavior< / td > < td > ❌< / td > < td > ❌< / td > < td > ⚠️< / td > < td > ✅< / td > < td > ✅< / td > < td > ❌< / td > < td > Defer to Soma.< / td > < / tr >
< tr > < td class = "name" > Validation / async collections< / td > < td > ✅< / td > < td > ❌< / td > < td > ⚠️< / td > < td > ✅< / td > < td > ✅< / td > < td > ❌< / td > < td > Defer to Field/Soma/collections.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Prop< / th > < th > Owner< / th > < th > Default< / th > < th > Notes< / th > < / tr > < / thead >
< tbody >
< tr > < td class = "name" > value< / td > < td > soma< / td > < td class = "default" > []< / td > < td > Bindable selected values.< / td > < / tr >
< tr > < td class = "name" > inputValue< / td > < td > soma< / td > < td class = "default" > ''< / td > < td > Bindable text query; consumer filters items.< / td > < / tr >
< tr > < td class = "name" > open< / td > < td > soma< / td > < td class = "default" > false< / td > < td > Bindable popup state.< / td > < / tr >
< tr > < td class = "name" > type< / td > < td > soma< / td > < td class = "default" > single< / td > < td > single | multiple.< / td > < / tr >
< tr > < td class = "name" > allowDeselect< / td > < td > soma< / td > < td class = "default" > false< / td > < td > Single mode can clear by reselecting.< / td > < / tr >
< tr > < td class = "name" > side / align< / td > < td > soma< / td > < td class = "default" > bottom / start< / td > < td > Floating placement. No < code > auto< / code > side.< / td > < / tr >
< tr > < td class = "name" > size< / td > < td > eidos< / td > < td class = "default" > md< / td > < td > Responsive visual density.< / td > < / tr >
< tr > < td class = "name" > variant< / td > < td > eidos< / td > < td class = "default" > surface< / td > < td > surface | outline | ghost.< / td > < / tr >
< tr > < td class = "name" > color< / td > < td > eidos< / td > < td class = "default" > primary< / td > < td > Intent palette for focus/selected state.< / 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 > · contract< / h2 >
< p data-uix-section-desc >
Source: < code > src/uix/morfo/components/combobox.ts< / code > . Morfo is the source for
markers, ARIA, keyboard and semantic event targets.
< / 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" > "{ comboboxMorfo . name } "</ td ></ tr >
< tr >< td class = "name" > kebab</ td >< td class = "type" > "{ comboboxMorfo . kebab } "</ td ></ tr >
< tr >< td class = "name" > scope</ td >< td class = "type" > [{ comboboxMorfo . scope . map (( s ) => `"$ { s } " `).join(', ') } ]</ td ></ tr >
< tr >< td class = "name" > apg</ td >< td >< a href = { comboboxMorfo . apg } target="_blank" rel = "noreferrer" > { comboboxMorfo . apg } </ a ></ td ></ tr >
< tr >< td class = "name" > parts.length</ td >< td class = "default" > { comboboxMorfo . parts . length } </ td ></ tr >
< tr >< td class = "name" > events.length</ td >< td class = "default" > { comboboxMorfo . events . length } </ 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 }
< 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 comboboxMorfo . parts as rawPart }
{ @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 }
< 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 }
< 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 }
< 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 >
< 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 }
{ @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 ?? '?' } ` : '-' }
< 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 >
Combobox has two semantic commits: selecting is affirmative; unselecting is
neutral. Typing, filtering, opening and highlighting are intentionally silent.
< / 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 > Play< / th > < / tr > < / thead >
< tbody >
{ #each events as action }
< tr >
< td class = "name" > { action . name } </ td >
< td class = "type" > { action . semantic . family } </ td >
< td > { action . semantic . verb ?? '-' } </ td >
< td > { action . semantic . sequence ?? 'pre' } </ td >
< td > { action . semantic . intent ?? '-' } </ td >
< td > { action . target } </ td >
< td >
< button
data-uix-play
onclick={() => {
const target = stageRef?.querySelector('[data-combobox-item]') as HTMLElement | null;
if (!target) return;
void uix.events?.emit({
name: action.name,
family: action.semantic.family,
intent: intentLiteral(action.semantic.intent),
target
});
}}
>
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 >
Selectors live in < code > src/uix/eidos/components/combobox/combobox.css< / code > .
Morfo provides structure/state; Eidos-only attrs provide visual treatment.
< / p >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Selector< / th > < th > Source< / th > < / tr > < / thead >
< tbody >
< tr > < td class = "name" > [data-combobox]< / td > < td > < span data-uix-tag data-kind = "soma" > morfo< / span > < / td > < / tr >
< tr > < td class = "name" > [data-combobox-control]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos-only< / span > < / td > < / tr >
< tr > < td class = "name" > [data-combobox-input][data-state]< / td > < td > < span data-uix-tag data-kind = "soma" > morfo< / span > < / td > < / tr >
< tr > < td class = "name" > [data-combobox-content][data-side]< / td > < td > < span data-uix-tag data-kind = "soma" > runtime< / span > < / td > < / tr >
< tr > < td class = "name" > [data-combobox-item][data-state='checked']< / td > < td > < span data-uix-tag data-kind = "soma" > morfo< / span > < / td > < / tr >
< tr > < td class = "name" > [data-combobox-input][data-size]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < / tr >
< tr > < td class = "name" > [data-combobox-indicator]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos-only< / span > < / td > < / tr >
< tr > < td class = "name" > [data-combobox-item-indicator]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos-only< / span > < / td > < / tr >
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'a11y' }
< section data-uix-section >
< h2 data-uix-section-title > Accessibility< / h2 >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Concern< / th > < th > Contract< / th > < / tr > < / thead >
< tbody >
< tr > < td class = "name" > Pattern< / td > < td > Input is < code > role="combobox"< / code > ; content is < code > role="listbox"< / code > ; items are < code > role="option"< / code > .< / td > < / tr >
< tr > < td class = "name" > Keyboard< / td > < td > Arrows navigate, Home/End jump, Enter selects, Escape closes.< / td > < / tr >
< tr > < td class = "name" > Focus< / td > < td > Typing keeps focus in the input; highlighted item id is exposed via < code > aria-activedescendant< / code > .< / td > < / tr >
< tr > < td class = "name" > Form< / td > < td > < code > name< / code > renders hidden inputs for submission.< / td > < / tr >
< tr > < td class = "name" > Disabled< / td > < td > Disabled input, trigger and items are removed from interaction.< / td > < / tr >
< / tbody >
< / table >
< / div >
< / section >
{ /if }
< / div >