< script lang = "ts" >
import {
SearchField,
type SearchFieldColor,
type SearchFieldSize,
type SearchFieldVariant
} from '$uix/eidos/components/search-field';
import { X } from '$uix/eidos/components/icon';
import { compileMorfo } from '$uix/morfo';
import { searchFieldMorfo } from '@/uix/morfo/components/search-field';
import { getActiveUix } from '$active-uix';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event : string ; family : string ; intent? : string ; at : number } ;
const uix = getActiveUix();
let tab = $state< Tab > ('live');
let value = $state('drawer');
let placeholder = $state('Search components');
let clearOnEscape = $state(true);
let disabled = $state(false);
let readonly = $state(false);
let required = $state(false);
let invalid = $state(false);
let size = $state< SearchFieldSize > ('md');
let variant = $state< SearchFieldVariant > ('surface');
let color = $state< SearchFieldColor > ('primary');
let lastSubmit = $state('none');
let trace = $state< TraceEntry [ ] > ([]);
let stageRef = $state< HTMLElement | null > (null);
const compiled = compileMorfo(searchFieldMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
const isEmpty = $derived(value.length === 0);
const variants: SearchFieldVariant[] = ['surface', 'outline', 'ghost'];
const colors: SearchFieldColor[] = [
'primary',
'secondary',
'neutral',
'affirm',
'fulfill',
'risk',
'threat',
'loss'
];
$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' )} `;
}
const somaSnippet = $derived(
[
"< script lang = 'ts' > " ,
" import * as SearchField from '$soma/components/search-field';",
` let value = $state('${ value } ');`,
'< /' + 'script>',
'',
'< SearchField.Provider bind:value placeholder = "Search components" > ',
' < SearchField.Input / > ',
' < SearchField.ClearTrigger aria-label = "Clear" > < X size = "1em" decorative / > < / SearchField.ClearTrigger > ',
'< / SearchField.Provider > '
].join('\n')
);
const eidosSnippet = $derived(
[
"< script lang = 'ts' > " ,
" import { SearchField } from '$uix/eidos/components/search-field';",
` let value = $state('${ value } ');`,
'< /' + 'script>',
'',
`< SearchField bind:value placeholder = "Search components" size = "$ { size } " `,
variant !== 'surface' && ` variant="${ variant } "`,
color !== 'primary' && ` color="${ color } "`,
'>',
' < SearchField.Input / > ',
' < SearchField.ClearTrigger aria-label = "Clear" > < X size = "1em" decorative / > < / SearchField.ClearTrigger > ',
'< / SearchField > '
]
.filter(Boolean)
.join('\n')
);
< / script >
< div data-uix-canvas-inner >
< header >
< div data-uix-eyebrow > Forms · Search field< / div >
< h1 data-uix-page-title > SearchField< / h1 >
< p data-uix-page-lede >
Searchbox primitive with clear and submit behavior. Soma owns value, focus, clear, keyboard
and ARIA; Eidos adds size, variant and intent color.
< / 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 > keyboard< / span > 2< / span >
< span data-uix-meta-pill > < span data-uix-meta-key > sizes< / span > 3< / span >
< span data-uix-meta-pill > < span data-uix-meta-key > variants< / span > 3< / span >
< / div >
< / header >
< div data-uix-stage >
< div data-uix-stage-area bind:this = { stageRef } >
< div style = "inline-size: min(26rem, 100%);" >
< SearchField
bind:value
{ placeholder }
{ clearOnEscape }
{ disabled }
{ readonly }
{ required }
{ invalid }
{ size }
{ variant }
{ color }
onSubmit={( next ) => ( lastSubmit = next || 'empty' )}
onClear={() => ( lastSubmit = 'cleared' )}
>
< SearchField.Input / >
< SearchField.ClearTrigger
aria-label={ uix . langs . ts ( '#?components.search-field.clear|Clear' )}
>
< X size = "1em" decorative / >
< / SearchField.ClearTrigger >
< / SearchField >
< / div >
< / div >
< div data-uix-stage-trace >
< span data-uix-stage-trace-key > trace< / span >
{ #if trace . length === 0 }
< span > press Enter or clear the field to see 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 >
< span data-uix-stage-trace-event > { isEmpty ? 'empty' : value } </ span >
< span style = "color: var(--uix-text-faint)" > ·< / span >
< span data-uix-stage-trace-key > last< / span >
{ lastSubmit }
< / 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 > 10< / 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 >
< span data-uix-layer-badge = "soma" > soma< / span > owns value, clear and submit.
< span data-uix-layer-badge = "eidos" > eidos< / span > owns visual treatment.
< / p >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "soma" > soma< / span > props · search behavior
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > value< / span >
< input type = "search" bind:value / >
< / label >
< label data-uix-control >
< span data-uix-control-label > placeholder< / span >
< input type = "text" bind:value = { placeholder } / >
< / label >
{ #each [ 'clearOnEscape' , 'disabled' , 'readonly' , 'required' , 'invalid' ] as flag }
< label data-uix-control >
< span data-uix-control-label > { flag } </ span >
< span data-uix-switch >
{ #if flag === 'clearOnEscape' }
< input type = "checkbox" bind:checked = { clearOnEscape } / >
< span data-uix-switch-label > { clearOnEscape ? 'on' : 'off' } </ span >
{ :else if flag === 'disabled' }
< input type = "checkbox" bind:checked = { disabled } / >
< span data-uix-switch-label > { disabled ? 'on' : 'off' } </ span >
{ :else if flag === 'readonly' }
< input type = "checkbox" bind:checked = { readonly } / >
< span data-uix-switch-label > { readonly ? 'on' : 'off' } </ span >
{ :else if flag === 'required' }
< input type = "checkbox" bind:checked = { required } / >
< span data-uix-switch-label > { required ? 'on' : 'off' } </ span >
{ : else }
< input type = "checkbox" bind:checked = { invalid } / >
< span data-uix-switch-label > { invalid ? 'on' : 'off' } </ span >
{ /if }
< / span >
< / label >
{ /each }
< / 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 [ 'sm' , 'md' , 'lg' ] as s }
< button
data-uix-chip
data-active={ size === s }
onclick={() => ( size = s as SearchFieldSize )} >{ s } < /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 >
< / div >
< div data-uix-code >
< div data-uix-code-head >
< span data-uix-layer-badge = "soma" > soma< / span >
< span > headless · search behavior< / 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 · size, variant and intent color< / 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" > Native search input< / td > < td > ❌< / td > < td > ❌< / td > < td > ❌< / td > < td > ⚠️< / td > < td > ✅< / td > < td > ✅< / td > < td > Soma owns `type="search"` and `role="searchbox"`.< / td > < / tr >
< tr > < td class = "name" > Clear trigger< / td > < td > ❌< / td > < td > ❌< / td > < td > ❌< / td > < td > ⚠️< / td > < td > ✅< / td > < td > ✅< / td > < td > Covered with translated label and disabled state.< / td > < / tr >
< tr > < td class = "name" > Escape clears< / td > < td > ❌< / td > < td > ❌< / td > < td > ❌< / td > < td > ⚠️< / td > < td > ✅< / td > < td > ✅< / td > < td > Covered via `clearOnEscape`.< / td > < / tr >
< tr > < td class = "name" > Submit on Enter< / td > < td > ❌< / td > < td > ❌< / td > < td > ❌< / td > < td > ⚠️< / td > < td > ✅< / td > < td > ✅< / td > < td > Covered with `commit-submit` event.< / td > < / tr >
< tr > < td class = "name" > Field state integration< / td > < td > ⚠️< / td > < td > ✅< / td > < td > ⚠️< / td > < td > ✅< / td > < td > ✅< / td > < td > ✅< / td > < td > UIX delegates label/error/description to Field.< / 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 search text< /td
>< /tr
>
< tr
>< td class = "name" > onValueChange< / td > < td > soma< / td > < td class = "default" > noop< / td > < td
>Live filtering path< /td
>< /tr
>
< tr
>< td class = "name" > onSubmit< / td > < td > soma< / td > < td class = "default" > noop< / td > < td
>Enter key path< /td
>< /tr
>
< tr
>< td class = "name" > onClear< / td > < td > soma< / td > < td class = "default" > noop< / td > < td
>Clear trigger / Escape path< /td
>< /tr
>
< tr
>< td class = "name" > clearOnEscape< / td > < td > soma< / td > < td class = "default" > true< / td > < td
>Escape clears the input< /td
>< /tr
>
< tr
>< td class = "name" > disabled / readonly / invalid< / td > < td > soma< / td > < td class = "default"
>false< /td
>< td > Field integration flags< / td > < /tr
>
< tr
>< td class = "name" > size< / td > < td > eidos< / td > < td class = "default" > md< / td > < td
>sm | md | lg< /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 and clear trigger hover.< / td > < / tr >
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'morfo' }
< section data-uix-section >
< h2 data-uix-section-title > Morfo contract< / h2 >
< div data-uix-table-wrap >
< table data-uix-table >
< thead
>< tr > < th > Part< / th > < th > Marker< / th > < th > Element< / th > < th > Role< / 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 . optional ? 'yes' : 'no' } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
< 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 >
{ #each searchFieldMorfo . parts [ 1 ]. keyboard ?? [] as key }
< tr >< td class = "name" > { key . key } </ td >< td > { key . action } </ td ></ tr >
{ /each }
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'sema' }
< section data-uix-section >
< h2 data-uix-section-title >
< span data-uix-layer-badge = "sema" > sema< / span > · events + perceptual signature
< / h2 >
< p data-uix-section-desc >
SearchField splits submit and reset so Enter and Clear/Escape can carry different perceptual
hooks while the value mutation remains owned by Soma.
< / 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 > 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 . target } </ td >
< td >
< button
data-uix-play
onclick={() => {
const target = stageRef?.querySelector(
'[data-search-field]'
) as HTMLElement | null;
if (!target) return;
void uix.events?.emit({
name: action.name,
family: action.semantic.family,
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/search-field/search-field.css< / code >
and consume Soma empty/focus state plus Eidos < code > data-size< / code > ,
< code > data-variant< / code > and < code > data-color< / code > .
< / p >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Selector< / th > < th > Owner< / th > < th > Purpose< / th > < / tr > < / thead >
< tbody >
< tr >
< td class = "name" > < code > [data-search-field]< / code > < / td >
< td > < span data-uix-tag data-kind = "morfo" > morfo< / span > < / td >
< td > Root search composition shell.< / td >
< / tr >
< tr >
< td class = "name"
>< code > [data-search-field][data-size][data-variant][data-color]< / code > < /td
>
< td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td >
< td > Visual scale, input treatment and intent palette.< / td >
< / tr >
< tr >
< td class = "name"
>< code > [data-search-field-input][data-empty][data-focus][data-invalid]< / code > < /td
>
< td > < span data-uix-tag data-kind = "morfo" > morfo< / span > < / td >
< td > Search input state surfaced by Soma.< / td >
< / tr >
< tr >
< td class = "name" > < code > [data-search-field-clear-trigger]< / code > < / td >
< td > < span data-uix-tag data-kind = "morfo" > morfo< / span > < / td >
< td > Clear action affordance and disabled state.< / td >
< / tr >
< tr >
< td class = "name" > < code > [data-search-field][data-event]< / code > < / td >
< td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td >
< td > Transient perceptual event styling from Sema projection.< / 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" > Role< / td > < td
>Input uses < code > role="searchbox"< / code > and < code > type="search"< / code > .< /td
>< /tr
>
< tr
>< td class = "name" > Label< / td > < td
>Input receives translated label unless a Field label wins.< /td
>< /tr
>
< tr
>< td class = "name" > Keyboard< / td > < td > Enter submits; Escape clears when enabled.< / td > < /tr
>
< tr > < td class = "name" > Clear< / td > < td > Clear trigger restores focus to the input.< / td > < / tr >
< / tbody >
< / table >
< / div >
< / section >
{ /if }
< / div >