Add Eidos search field wrapper

active-uix
dev 5 months ago
parent 5a6193d365
commit 3c60d33a6c

@ -69,9 +69,21 @@ Actualizacion Eidos component migration 2026-05-17:
- `npm run check` -> 0 errores, 0 warnings. - `npm run check` -> 0 errores, 0 warnings.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> 0 invalid, sin drift - `node --import tsx/esm scripts/eidos-lint-all.ts` -> 0 invalid, sin drift
hotspots. hotspots.
- Quinta tanda del mismo bloque:
- `SearchField`: root visual `search-field.svelte`, partes `Input`,
`ClearTrigger`, `search-field.css`, `types.ts` e `index.ts`.
- El wrapper conserva el snippet de Soma (`value`, `isEmpty`, `isFocused`,
`clear`) y solo anade `size`.
- La receta cubre contenedor, input, clear trigger, focused, invalid, empty
y disabled.
- Validado despues de `SearchField`:
- `npx vitest run src/uix/eidos` -> 7 archivos, 84 tests OK.
- `npm run check` -> 0 errores, 0 warnings.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> 0 invalid, sin drift
hotspots.
- Siguiente bloque sugerido: - Siguiente bloque sugerido:
- Migrar otro grupo de bajo riesgo desde Soma a Eidos: candidatos reales por - Migrar otro grupo de bajo riesgo desde Soma a Eidos: candidatos reales por
morfo/soma existentes son `number-field`, `field` o `search-field`. morfo/soma existentes son `number-field`, `field` o `toolbar`.
- Antes de tocar componentes grandes (`select`, `combobox`, `calendar`, - Antes de tocar componentes grandes (`select`, `combobox`, `calendar`,
`date-field`) conviene fijar una tabla por componente: partes Soma, `date-field`) conviene fijar una tabla por componente: partes Soma,
props visuales Eidos, recipe tokens y attrs esperados. props visuales Eidos, recipe tokens y attrs esperados.

@ -23,6 +23,7 @@ const VISUAL_ATTRS = [
['progress/progress.svelte', ['data-size']], ['progress/progress.svelte', ['data-size']],
['radio-group/radio-group.svelte', ['data-size', 'data-variant', 'data-color', 'data-columns']], ['radio-group/radio-group.svelte', ['data-size', 'data-variant', 'data-color', 'data-columns']],
['rating-group/rating-group.svelte', ['data-size']], ['rating-group/rating-group.svelte', ['data-size']],
['search-field/search-field.svelte', ['data-size']],
['slider/slider.svelte', ['data-size']], ['slider/slider.svelte', ['data-size']],
['switch/switch.svelte', ['data-size']], ['switch/switch.svelte', ['data-size']],
['tabs/tabs.svelte', ['data-size', 'data-variant', 'data-fitted', 'data-collapsed', 'data-motion']], ['tabs/tabs.svelte', ['data-size', 'data-variant', 'data-fitted', 'data-collapsed', 'data-motion']],

@ -358,6 +358,7 @@ su subdirectorio.
| pagination | ✅ multi-part | PrevTrigger, NextTrigger, Item, Ellipsis | | pagination | ✅ multi-part | PrevTrigger, NextTrigger, Item, Ellipsis |
| progress | ✅ multi-part | Indicator | | progress | ✅ multi-part | Indicator |
| rating-group | ✅ multi-part | Item | | rating-group | ✅ multi-part | Item |
| search-field | ✅ multi-part | Input, ClearTrigger |
| slider | ✅ multi-part | Range, Thumb, Tick | | slider | ✅ multi-part | Range, Thumb, Tick |
| tooltip | ✅ multi-part | + Group | | tooltip | ✅ multi-part | + Group |
| tabs | ✅ multi-part | List, Trigger, Content, Indicator | | tabs | ✅ multi-part | List, Trigger, Content, Indicator |

@ -0,0 +1,32 @@
// SearchField — eidos compound API, disciplined option C.
//
// import { SearchField } from '$uix/eidos/components/search-field';
//
// <SearchField bind:value>
// <SearchField.Input />
// <SearchField.ClearTrigger>Clear</SearchField.ClearTrigger>
// </SearchField>
import SearchFieldRoot from './search-field.svelte';
import Input from './search-field-input.svelte';
import ClearTrigger from './search-field-clear-trigger.svelte';
type SearchFieldNamespace = typeof SearchFieldRoot & {
Input: typeof Input;
ClearTrigger: typeof ClearTrigger;
};
const SearchField = SearchFieldRoot as SearchFieldNamespace;
SearchField.Input = Input;
SearchField.ClearTrigger = ClearTrigger;
export { SearchField };
export default SearchField;
export type {
SearchFieldProps,
SearchFieldInputProps as InputProps,
SearchFieldClearTriggerProps as ClearTriggerProps,
SearchFieldProviderSnippetProps as ProviderSnippetProps,
SearchFieldSize
} from './types';

@ -0,0 +1,10 @@
<script lang="ts">
import { ClearTrigger as SomaSearchFieldClearTrigger } from '$soma/components/search-field';
import type { SearchFieldClearTriggerProps } from './types';
let { children, ...rest }: SearchFieldClearTriggerProps = $props();
</script>
<SomaSearchFieldClearTrigger {...rest}>
{@render children?.()}
</SomaSearchFieldClearTrigger>

@ -0,0 +1,10 @@
<script lang="ts">
import { Input as SomaSearchFieldInput } from '$soma/components/search-field';
import type { SearchFieldInputProps } from './types';
let { children, ...rest }: SearchFieldInputProps = $props();
</script>
<SomaSearchFieldInput {...rest}>
{@render children?.()}
</SomaSearchFieldInput>

@ -0,0 +1,114 @@
[data-search-field] {
--_search-field-height: var(--search-field-height-md);
--_search-field-px: var(--search-field-px-md);
--_search-field-gap: var(--search-field-gap-md);
--_search-field-font-size: var(--search-field-font-size-md);
display: inline-flex;
align-items: center;
gap: var(--_search-field-gap);
inline-size: 100%;
min-inline-size: 0;
block-size: var(--_search-field-height);
padding-inline: var(--_search-field-px);
border: var(--search-field-border-width) solid var(--search-field-border);
border-radius: var(--search-field-radius);
background: var(--search-field-bg);
color: var(--search-field-color);
font-family: var(--search-field-font-family);
font-size: var(--_search-field-font-size);
line-height: var(--search-field-line-height);
transition:
background var(--search-field-transition-duration) var(--search-field-transition-ease),
border-color var(--search-field-transition-duration) var(--search-field-transition-ease),
box-shadow var(--search-field-transition-duration) var(--search-field-transition-ease);
}
[data-search-field][data-size='sm'] {
--_search-field-height: var(--search-field-height-sm);
--_search-field-px: var(--search-field-px-sm);
--_search-field-gap: var(--search-field-gap-sm);
--_search-field-font-size: var(--search-field-font-size-sm);
}
[data-search-field][data-size='lg'] {
--_search-field-height: var(--search-field-height-lg);
--_search-field-px: var(--search-field-px-lg);
--_search-field-gap: var(--search-field-gap-lg);
--_search-field-font-size: var(--search-field-font-size-lg);
}
[data-search-field][data-focused] {
border-color: var(--search-field-border-focus);
box-shadow: var(--search-field-focus-shadow);
}
[data-search-field][data-invalid] {
border-color: var(--search-field-border-invalid);
}
[data-search-field][data-disabled] {
opacity: var(--search-field-disabled-opacity);
}
[data-search-field-input] {
flex: 1 1 auto;
min-inline-size: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
outline: none;
}
[data-search-field-input]::placeholder {
color: var(--search-field-placeholder-color);
}
[data-search-field-input][data-disabled] {
cursor: not-allowed;
}
[data-search-field-clear-trigger] {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
inline-size: var(--search-field-clear-size);
block-size: var(--search-field-clear-size);
border: var(--search-field-clear-border-width) solid var(--search-field-clear-border);
border-radius: var(--search-field-clear-radius);
background: var(--search-field-clear-bg);
color: var(--search-field-clear-color);
font: inherit;
cursor: pointer;
outline: none;
transition:
background var(--search-field-transition-duration) var(--search-field-transition-ease),
border-color var(--search-field-transition-duration) var(--search-field-transition-ease),
color var(--search-field-transition-duration) var(--search-field-transition-ease);
}
[data-search-field-clear-trigger]:hover:not([data-disabled]) {
border-color: var(--search-field-clear-border-hover);
background: var(--search-field-clear-bg-hover);
color: var(--search-field-clear-color-hover);
}
[data-search-field-clear-trigger]:focus-visible {
box-shadow: var(--search-field-clear-focus-shadow);
}
[data-search-field-clear-trigger][data-disabled],
[data-search-field][data-empty] [data-search-field-clear-trigger] {
cursor: default;
opacity: var(--search-field-clear-hidden-opacity);
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
[data-search-field],
[data-search-field-clear-trigger] {
transition: none;
}
}

@ -0,0 +1,20 @@
<script lang="ts">
/**
* Eidos `<SearchField>` — visual wrapper over Soma's search primitive.
* Soma owns value, clear and submit behavior; Eidos adds `size`.
*/
import { ActiveEidos } from '$uix/eidos';
import { Provider as SomaSearchFieldProvider } from '$soma/components/search-field';
import type { SearchFieldProps } from './types';
let { size = 'md', value = $bindable(''), children, ...rest }: SearchFieldProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
</script>
<SomaSearchFieldProvider {...rest} bind:value data-size={resolvedSize}>
{#snippet children(snippetProps)}
{@render children?.(snippetProps)}
{/snippet}
</SomaSearchFieldProvider>

@ -0,0 +1,24 @@
import type {
ClearTriggerProps as SomaSearchFieldClearTriggerProps,
InputProps as SomaSearchFieldInputProps,
ProviderProps as SomaSearchFieldProviderProps,
ProviderSnippetProps as SearchFieldProviderSnippetProps
} from '$soma/components/search-field';
import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
export type SearchFieldSize = Extract<Size, 'sm' | 'md' | 'lg'>;
/**
* Props for the eidos `<SearchField>`.
*
* Extends Soma's search field with visual sizing only. Value, focus, clear,
* submit and integrated Field state are owned by Soma.
*/
export type SearchFieldProps = SomaSearchFieldProviderProps & {
/** Visual size. Accepts a responsive map resolved by ActiveEidos. @default 'md' */
size?: ResponsiveProp<SearchFieldSize>;
};
export type SearchFieldInputProps = SomaSearchFieldInputProps;
export type SearchFieldClearTriggerProps = SomaSearchFieldClearTriggerProps;
export type { SearchFieldProviderSnippetProps };

@ -755,6 +755,43 @@
--rating-group-transition-duration: var(--duration-fast); --rating-group-transition-duration: var(--duration-fast);
--rating-group-transition-ease: var(--ease-default); --rating-group-transition-ease: var(--ease-default);
--rating-group-disabled-opacity: 0.56; --rating-group-disabled-opacity: 0.56;
--search-field-height-sm: var(--control-height-sm);
--search-field-height-md: var(--control-height-md);
--search-field-height-lg: var(--control-height-lg);
--search-field-px-sm: var(--space-2-5);
--search-field-px-md: var(--space-3);
--search-field-px-lg: var(--space-3-5);
--search-field-gap-sm: var(--space-1-5);
--search-field-gap-md: var(--space-2);
--search-field-gap-lg: var(--space-2-5);
--search-field-font-family: var(--font-ui);
--search-field-font-size-sm: var(--font-size-xs);
--search-field-font-size-md: var(--font-size-sm);
--search-field-font-size-lg: var(--font-size-sm);
--search-field-line-height: var(--leading-ui);
--search-field-radius: var(--radius-md);
--search-field-border-width: var(--border-width);
--search-field-border: var(--color-border-default);
--search-field-border-focus: var(--color-focus-ring);
--search-field-border-invalid: var(--color-risk-border);
--search-field-bg: var(--color-surface-default);
--search-field-color: var(--color-content-primary);
--search-field-placeholder-color: var(--color-content-muted);
--search-field-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--search-field-clear-size: var(--control-height-xs);
--search-field-clear-radius: var(--radius-full);
--search-field-clear-border-width: var(--border-width);
--search-field-clear-border: transparent;
--search-field-clear-border-hover: var(--color-border-subtle);
--search-field-clear-bg: transparent;
--search-field-clear-bg-hover: var(--color-surface-raised);
--search-field-clear-color: var(--color-content-secondary);
--search-field-clear-color-hover: var(--color-content-primary);
--search-field-clear-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--search-field-clear-hidden-opacity: 0;
--search-field-transition-duration: var(--duration-fast);
--search-field-transition-ease: var(--ease-default);
--search-field-disabled-opacity: 0.56;
--slider-track-size-sm: 4px; --slider-track-size-sm: 4px;
--slider-track-size-md: 6px; --slider-track-size-md: 6px;
--slider-track-size-lg: 8px; --slider-track-size-lg: 8px;

@ -78,4 +78,5 @@
@import './components/checkbox/checkbox.css'; @import './components/checkbox/checkbox.css';
@import './components/radio-group/radio-group.css'; @import './components/radio-group/radio-group.css';
@import './components/rating-group/rating-group.css'; @import './components/rating-group/rating-group.css';
@import './components/search-field/search-field.css';
@import './components/slider/slider.css'; @import './components/slider/slider.css';

@ -517,6 +517,47 @@ export const THEME_BASE_RECIPE_TOKENS = {
'transition-ease': 'var(--ease-default)', 'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56' 'disabled-opacity': '0.56'
}, },
'search-field': {
'height-sm': 'var(--control-height-sm)',
'height-md': 'var(--control-height-md)',
'height-lg': 'var(--control-height-lg)',
'px-sm': 'var(--space-2-5)',
'px-md': 'var(--space-3)',
'px-lg': 'var(--space-3-5)',
'gap-sm': 'var(--space-1-5)',
'gap-md': 'var(--space-2)',
'gap-lg': 'var(--space-2-5)',
'font-family': 'var(--font-ui)',
'font-size-sm': 'var(--font-size-xs)',
'font-size-md': 'var(--font-size-sm)',
'font-size-lg': 'var(--font-size-sm)',
'line-height': 'var(--leading-ui)',
'radius': 'var(--radius-md)',
'border-width': 'var(--border-width)',
'border': 'var(--color-border-default)',
'border-focus': 'var(--color-focus-ring)',
'border-invalid': 'var(--color-risk-border)',
bg: 'var(--color-surface-default)',
color: 'var(--color-content-primary)',
'placeholder-color': 'var(--color-content-muted)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'clear-size': 'var(--control-height-xs)',
'clear-radius': 'var(--radius-full)',
'clear-border-width': 'var(--border-width)',
'clear-border': 'transparent',
'clear-border-hover': 'var(--color-border-subtle)',
'clear-bg': 'transparent',
'clear-bg-hover': 'var(--color-surface-raised)',
'clear-color': 'var(--color-content-secondary)',
'clear-color-hover': 'var(--color-content-primary)',
'clear-focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'clear-hidden-opacity': '0',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
},
slider: { slider: {
'track-size-sm': '4px', 'track-size-sm': '4px',
'track-size-md': '6px', 'track-size-md': '6px',

Loading…
Cancel
Save

Powered by TurnKey Linux.