< script lang = "ts" >
import {
SearchField,
type SearchFieldColor,
type SearchFieldSize,
type SearchFieldVariant
} from '$uix/eidos/components/search-field';
import { Popover } from '$uix/eidos/components/popover';
import { PickerShell } from '$uix/eidos/components/picker-shell';
import { Search , 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 loading = $state(false);
let debounceMs = $state(0);
let lastSubmit = $state('none');
let lastValueChangeAt = $state< number | null > (null);
let lastValueChange = $state('—');
let trace = $state< TraceEntry [ ] > ([]);
let stageRef = $state< HTMLElement | null > (null);
let resultsOpen = $state(false);
let inputRef = $state< HTMLInputElement | null > (null);
// Set briefly after committing a pick so the input refocus doesn't
// re-open the popover we just closed via `onfocusin` on the anchor.
let justCommitted = false;
function commitPick(name: string) {
value = name;
resultsOpen = false;
justCommitted = true;
inputRef?.focus();
queueMicrotask(() => {
justCommitted = false;
});
}
const compiled = compileMorfo(searchFieldMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
const isEmpty = $derived(value.length === 0);
// Demo dataset for the live search panel.
const components: { name : string ; group : string } [] = [
{ name : 'Accordion' , group : 'Disclosure' } ,
{ name : 'Avatar' , group : 'Media' } ,
{ name : 'Breadcrumb' , group : 'Navigation' } ,
{ name : 'Button' , group : 'Action' } ,
{ name : 'Calendar' , group : 'Forms' } ,
{ name : 'Checkbox' , group : 'Forms' } ,
{ name : 'Collapsible' , group : 'Disclosure' } ,
{ name : 'Color picker' , group : 'Forms' } ,
{ name : 'Combobox' , group : 'Forms' } ,
{ name : 'Date picker' , group : 'Forms' } ,
{ name : 'Dialog' , group : 'Overlay' } ,
{ name : 'Drawer' , group : 'Overlay' } ,
{ name : 'Editable' , group : 'Forms' } ,
{ name : 'Field' , group : 'Forms' } ,
{ name : 'File upload' , group : 'Forms' } ,
{ name : 'Icon' , group : 'Media' } ,
{ name : 'Meter' , group : 'Status' } ,
{ name : 'Number field' , group : 'Forms' } ,
{ name : 'Pagination' , group : 'Navigation' } ,
{ name : 'Popover' , group : 'Overlay' } ,
{ name : 'Progress' , group : 'Status' } ,
{ name : 'Radio group' , group : 'Forms' } ,
{ name : 'Rating group' , group : 'Forms' } ,
{ name : 'Search field' , group : 'Forms' } ,
{ name : 'Select' , group : 'Forms' } ,
{ name : 'Slider' , group : 'Forms' } ,
{ name : 'Switch' , group : 'Forms' } ,
{ name : 'Tabs' , group : 'Disclosure' } ,
{ name : 'Tag group' , group : 'Forms' } ,
{ name : 'Time picker' , group : 'Forms' } ,
{ name : 'Toast' , group : 'Overlay' } ,
{ name : 'Toggle' , group : 'Action' } ,
{ name : 'Toolbar' , group : 'Action' } ,
{ name : 'Tooltip' , group : 'Overlay' }
];
// Filter matches name OR group, case-insensitive.
const results = $derived.by(() => {
const q = value.trim().toLowerCase();
if (!q) return components;
return components.filter(
(c) => c.name.toLowerCase().includes(q) || c.group.toLowerCase().includes(q)
);
});
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 } "`,
loading & & ' loading',
debounceMs > 0 && ` debounceMs={ $ { debounceMs }} `,
'>',
' < SearchField.Icon > < Search size = "1em" decorative / > < / SearchField.Icon > ',
' < SearchField.Input / > ',
' < SearchField.LoadingIndicator / > ',
' < 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%);" >
< Popover bind:open = { resultsOpen } >
< Popover.Anchor
onfocusin={() => {
if (!disabled & & !readonly & & !justCommitted) resultsOpen = true;
}}
>
< SearchField
bind:value
{ placeholder }
{ clearOnEscape }
{ disabled }
{ readonly }
{ required }
{ invalid }
{ loading }
{ debounceMs }
{ size }
{ variant }
{ color }
onValueChange={( next ) => {
lastValueChange = next === '' ? 'empty' : next;
lastValueChangeAt = Date.now();
if (!disabled & & !readonly) resultsOpen = true;
}}
onSubmit={( next ) => {
lastSubmit = next || 'empty';
resultsOpen = false;
}}
onClear={() => {
lastSubmit = 'cleared';
resultsOpen = false;
}}
>
< SearchField.Icon >
< Search size = "1em" decorative / >
< / SearchField.Icon >
< SearchField.Input bind:ref = { inputRef } / >
< SearchField.LoadingIndicator / >
< SearchField.ClearTrigger
aria-label={ uix . langs . ts ( '#?components.search-field.clear|Clear' )}
>
< X size = "1em" decorative / >
< / SearchField.ClearTrigger >
< / SearchField >
< / Popover.Anchor >
< Popover.Portal >
< Popover.Content
align="start"
sideOffset={ 6 }
onOpenAutoFocus={( e ) => e . preventDefault ()}
onCloseAutoFocus={( e ) => e . preventDefault ()}
>
< PickerShell.Body >
<!-- Live result list — filters the demo dataset by name/group. -->
< ul
class="search-field-results"
role="listbox"
aria-label="Filtered components"
aria-live="polite"
>
{ #if results . length === 0 }
< li class = "search-field-results-empty" >
No matches for < strong > "{ value } "</ strong >
< / li >
{ : else }
{ #each results as item ( item . name )}
< li role = "option" aria-selected = { value === item . name } >
< button
type="button"
class="search-field-result"
onclick={() => commitPick ( item . name )}
>
< span class = "search-field-result-name" > { item . name } </ span >
< span class = "search-field-result-group" > { item . group } </ span >
< / button >
< / li >
{ /each }
{ /if }
< / ul >
< div class = "search-field-results-meta" >
< span data-uix-stage-trace-key > results< / span >
{ results . length } / { components . length }
< / div >
< / PickerShell.Body >
< / Popover.Content >
< / Popover.Portal >
< / Popover >
< / 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 > onChange< / span >
< span data-uix-stage-trace-event > { lastValueChange } </ span >
{ #if lastValueChangeAt !== null }
< span style = "color: var(--uix-text-faint)" > { fmtTime ( lastValueChangeAt )} </ span >
{ /if }
< 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' , 'loading' ] 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 if flag === 'invalid' }
< input type = "checkbox" bind:checked = { invalid } / >
< span data-uix-switch-label > { invalid ? 'on' : 'off' } </ span >
{ : else }
< input type = "checkbox" bind:checked = { loading } / >
< span data-uix-switch-label > { loading ? 'on' : 'off' } </ span >
{ /if }
< / span >
< / label >
{ /each }
< label data-uix-control >
< span data-uix-control-label > debounceMs< / span >
< input
type="number"
min="0"
step="50"
bind:value={ debounceMs }
style="inline-size: 6rem;"
/>
< / 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" >
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
{ #each [ 'xs' , 'sm' , 'md' , 'lg' , 'xl' ] 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 . find (( p ) => p . kebab === 'input' ) ? . 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 >
< style >
.search-field-results {
display: flex;
flex-direction: column;
gap: var(--space-1);
padding: 0;
margin: 0;
list-style: none;
min-inline-size: 14rem;
max-block-size: 16rem;
overflow-y: auto;
}
.search-field-result {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-3);
inline-size: 100%;
padding: var(--space-1-5) var(--space-2);
border: var(--border-width) solid transparent;
border-radius: var(--radius-md);
background: transparent;
color: inherit;
font: inherit;
text-align: start;
cursor: pointer;
transition: background var(--duration-fast) var(--ease-default);
}
.search-field-result:hover {
background: var(--color-surface-overlay);
}
.search-field-result:focus-visible {
outline: var(--focus-ring-width) solid var(--color-primary-border);
outline-offset: var(--focus-ring-offset);
}
.search-field-result-name {
color: var(--color-content-primary);
font-weight: var(--font-weight-medium);
font-size: var(--font-size-sm);
}
.search-field-result-group {
color: var(--color-content-muted);
font-size: var(--font-size-xs);
}
.search-field-results-empty {
padding: var(--space-3);
color: var(--color-content-muted);
text-align: center;
font-size: var(--font-size-sm);
}
.search-field-results-meta {
display: flex;
align-items: center;
gap: var(--space-2);
color: var(--color-content-muted);
font-family: var(--font-family-mono);
font-size: var(--font-size-xs);
}
< / style >