diff --git a/continue.md b/continue.md index 3d5be4875..627612904 100644 --- a/continue.md +++ b/continue.md @@ -69,9 +69,21 @@ Actualizacion Eidos component migration 2026-05-17: - `npm run check` -> 0 errores, 0 warnings. - `node --import tsx/esm scripts/eidos-lint-all.ts` -> 0 invalid, sin drift 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: - 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`, `date-field`) conviene fijar una tabla por componente: partes Soma, props visuales Eidos, recipe tokens y attrs esperados. diff --git a/src/uix/eidos/component-visual-attrs.test.ts b/src/uix/eidos/component-visual-attrs.test.ts index 80ade6e26..982d48c74 100644 --- a/src/uix/eidos/component-visual-attrs.test.ts +++ b/src/uix/eidos/component-visual-attrs.test.ts @@ -23,6 +23,7 @@ const VISUAL_ATTRS = [ ['progress/progress.svelte', ['data-size']], ['radio-group/radio-group.svelte', ['data-size', 'data-variant', 'data-color', 'data-columns']], ['rating-group/rating-group.svelte', ['data-size']], + ['search-field/search-field.svelte', ['data-size']], ['slider/slider.svelte', ['data-size']], ['switch/switch.svelte', ['data-size']], ['tabs/tabs.svelte', ['data-size', 'data-variant', 'data-fitted', 'data-collapsed', 'data-motion']], diff --git a/src/uix/eidos/components/README.md b/src/uix/eidos/components/README.md index bee50f19e..00c4c05a1 100644 --- a/src/uix/eidos/components/README.md +++ b/src/uix/eidos/components/README.md @@ -358,6 +358,7 @@ su subdirectorio. | pagination | ✅ multi-part | PrevTrigger, NextTrigger, Item, Ellipsis | | progress | ✅ multi-part | Indicator | | rating-group | ✅ multi-part | Item | +| search-field | ✅ multi-part | Input, ClearTrigger | | slider | ✅ multi-part | Range, Thumb, Tick | | tooltip | ✅ multi-part | + Group | | tabs | ✅ multi-part | List, Trigger, Content, Indicator | diff --git a/src/uix/eidos/components/search-field/index.ts b/src/uix/eidos/components/search-field/index.ts new file mode 100644 index 000000000..62283c323 --- /dev/null +++ b/src/uix/eidos/components/search-field/index.ts @@ -0,0 +1,32 @@ +// SearchField — eidos compound API, disciplined option C. +// +// import { SearchField } from '$uix/eidos/components/search-field'; +// +// +// +// Clear +// +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'; diff --git a/src/uix/eidos/components/search-field/search-field-clear-trigger.svelte b/src/uix/eidos/components/search-field/search-field-clear-trigger.svelte new file mode 100644 index 000000000..8fce8aab0 --- /dev/null +++ b/src/uix/eidos/components/search-field/search-field-clear-trigger.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/search-field/search-field-input.svelte b/src/uix/eidos/components/search-field/search-field-input.svelte new file mode 100644 index 000000000..a3ed22900 --- /dev/null +++ b/src/uix/eidos/components/search-field/search-field-input.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/search-field/search-field.css b/src/uix/eidos/components/search-field/search-field.css new file mode 100644 index 000000000..33997b398 --- /dev/null +++ b/src/uix/eidos/components/search-field/search-field.css @@ -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; + } +} diff --git a/src/uix/eidos/components/search-field/search-field.svelte b/src/uix/eidos/components/search-field/search-field.svelte new file mode 100644 index 000000000..12b0f175b --- /dev/null +++ b/src/uix/eidos/components/search-field/search-field.svelte @@ -0,0 +1,20 @@ + + + + {#snippet children(snippetProps)} + {@render children?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/search-field/types.ts b/src/uix/eidos/components/search-field/types.ts new file mode 100644 index 000000000..3337c0b7d --- /dev/null +++ b/src/uix/eidos/components/search-field/types.ts @@ -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; + +/** + * Props for the eidos ``. + * + * 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; +}; + +export type SearchFieldInputProps = SomaSearchFieldInputProps; +export type SearchFieldClearTriggerProps = SomaSearchFieldClearTriggerProps; +export type { SearchFieldProviderSnippetProps }; diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 61dcfb8b2..e80cb2df8 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -755,6 +755,43 @@ --rating-group-transition-duration: var(--duration-fast); --rating-group-transition-ease: var(--ease-default); --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-md: 6px; --slider-track-size-lg: 8px; diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css index 34e2c0f8a..fc05f061a 100644 --- a/src/uix/eidos/index.css +++ b/src/uix/eidos/index.css @@ -78,4 +78,5 @@ @import './components/checkbox/checkbox.css'; @import './components/radio-group/radio-group.css'; @import './components/rating-group/rating-group.css'; +@import './components/search-field/search-field.css'; @import './components/slider/slider.css'; diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 23ede292f..8ef4b0fe2 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -517,6 +517,47 @@ export const THEME_BASE_RECIPE_TOKENS = { 'transition-ease': 'var(--ease-default)', '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: { 'track-size-sm': '4px', 'track-size-md': '6px',