From 9452bc25453f8ba40bcf6d63c94be510265930f0 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 9 Jun 2026 19:47:47 +0200 Subject: [PATCH] =?UTF-8?q?docs(uix):=20close=20css-field=20audit=20gaps?= =?UTF-8?q?=20=E2=80=94=20eidos=20README=20Comparativa=20+=20Gaps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds external comparison table (WAI-ARIA / React Aria / Ark UI / Base UI / Bits) with sources and a Gaps table with disposition markers, bringing the css-field audit scorecard to parity with number-field (only the systemic E-2.2 css-import rule + R-1.3 readonly warning remain, both shared with the reference component). Smoke PASS for /uix/components/css-field. Co-Authored-By: Claude Opus 4.8 --- src/uix/eidos/components/css-field/README.md | 32 ++- .../components/palabras/palabras-anchor.ts | 87 ++++++ .../palabras/palabras-appearance.svelte | 183 ++++++++----- .../components/palabras/palabras-panel.svelte | 257 ++++++++++-------- .../palabras/palabras-segmented.svelte | 56 ++++ .../eidos/components/palabras/palabras.css | 103 ++++--- .../eidos/components/palabras/palabras.svelte | 45 ++- .../eidos/components/palabras/panel-schema.ts | 30 +- src/uix/eidos/components/palabras/types.ts | 25 +- 9 files changed, 534 insertions(+), 284 deletions(-) create mode 100644 src/uix/eidos/components/palabras/palabras-anchor.ts create mode 100644 src/uix/eidos/components/palabras/palabras-segmented.svelte diff --git a/src/uix/eidos/components/css-field/README.md b/src/uix/eidos/components/css-field/README.md index 51c043972..f31d9dfdb 100644 --- a/src/uix/eidos/components/css-field/README.md +++ b/src/uix/eidos/components/css-field/README.md @@ -35,11 +35,28 @@ Tokens live under the `css-field` recipe key in `src/uix/eidos/lib/recipes/base. No Air baseline — new component. Recipe cloned from `number-field`'s with token renames. -## Comparativa +## Comparativa externa -See `src/uix/soma/components/css-field/README.md` for the cross-library feature table. -Visually it is a single-row control matching the input family (DateField / NumberField / -ColorField): border + control buttons + optional scrubber, 5 sizes × 3 variants × 8 colors. +Ningún sistema de referencia trae un campo de valor CSS con whitelist de unidades; los +análogos más cercanos son los spinbuttons numéricos. La superficie visual coincide con la +familia de inputs (DateField / NumberField / ColorField): borde + botones de control + +scrubber opcional, 5 tamaños × 3 variantes × 8 colores. + +| Plataforma | Modelo | Eidos/UIX | +| --- | --- | --- | +| WAI-ARIA APG Spinbutton | Campo de texto enfocable + botones increment/decrement, flechas, Home/End, PageUp/Down. | Cumple el patrón; los triggers son `tabindex=-1` para mantener el foco en el input. | +| React Aria NumberField | Label, Group, Input, Buttons, `formatOptions` Intl, wheel. Numérico puro. | Soma cubre teclado/wheel/scrubber/Field; añade valor+unidad (`allowedUnits`) que React Aria no modela. | +| Ark UI Number Input | Root, Control, Input, triggers, Scrubber, clamp/overflow, invalid callbacks. Numérico puro. | Soma cubre scrubber + clamp + invalid; opera sobre dimensión CSS, no número. | +| Base UI Number Field | Root, ScrubArea, Group, Decrement, Input, Increment. Numérico puro. | Soma cubre scrubber + triggers; el valor es una cadena CSS con unidad. | +| Radix / Bits / shadcn | Sin campo numérico ni de valor CSS dedicado. | UIX tiene componente dedicado con validación ambivalente (funciones puras o schema sium). | + +Fuentes: + +- [WAI-ARIA APG Spinbutton](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/) +- [React Aria NumberField](https://react-aria.adobe.com/NumberField) +- [Ark UI Number Input](https://ark-ui.com/docs/components/number-input) +- [Base UI Number Field](https://base-ui.com/react/components/number-field) +- [Bits UI](https://bits-ui.com/docs/introduction) ## Decisiones @@ -49,5 +66,8 @@ ColorField): border + control buttons + optional scrubber, 5 sizes × 3 variants ## Gaps -- A units-suffix affordance (showing the unit separately) is not provided — the unit lives - inside the input text. _diferir_. +| Gap | Disposición | Detalle | +| --- | --- | --- | +| Afijo de unidad separado (mostrar la unidad fuera del texto del input) | **diferir** | Hoy la unidad vive dentro del texto del input; un sufijo visual requiere markup + medición. Sólo si llega un caso real. | +| `calc(...)` y valores de color | **descartar** | Fuera del dominio de dimensiones; color ya lo cubre `color-field`. | +| Tests browser-level del flujo completo (Playwright) | **implementar** | Cobertura visual + interacciones, en la pasada conjunta de tests del catálogo. | diff --git a/src/uix/eidos/components/palabras/palabras-anchor.ts b/src/uix/eidos/components/palabras/palabras-anchor.ts new file mode 100644 index 000000000..4c7e6789e --- /dev/null +++ b/src/uix/eidos/components/palabras/palabras-anchor.ts @@ -0,0 +1,87 @@ +/** + * Clamped virtual anchor for the properties Popover. + * + * The panel is anchored to the ACTIVE block but must always sit on a chosen + * `side` (the main panel to the right, the appearance popover to the left). A + * full-width block fills the content column, so in a narrow window the gutter on + * that side is smaller than the panel — a plain block anchor then makes the + * Popover FLIP to the opposite side (the panel jumps over the doc sidebar). The + * user wants it to stay put ("que apareciera a la derecha"). + * + * Fix: hand the Popover a VIRTUAL anchor (a floating-ui `Measurable`) whose + * rect is CLAMPED on the main axis so the panel always fits the viewport on + * `side`. When the gutter is too small the anchor slides inward, so the panel + * overlaps the content's edge instead of flipping — `flip()` never triggers + * because the placement always fits. A zero-width strip spanning the block's + * vertical extent keeps `align='center'` centred on the block's mid-height. + * + * floating-ui calls `getBoundingClientRect` on every autoUpdate cycle, so scroll + * and resize re-clamp automatically (`contextElement` = the block, the element + * autoUpdate observes for scroll parents). The rect depends only on the block + + * viewport — never on the panel — so moving it can't feed back into a resize loop. + * + * NOTE on the type: the soma floating layer accepts `Measurable` as its anchor + * (`FloatingContent.customAnchor: string | HTMLElement | null | Measurable`), + * but the public `` prop narrows that to + * `string | HTMLElement | null`. The capability is real at runtime; consumers + * cast the result to `HTMLElement` at the call site (we don't modify soma). + */ + +/** Minimal floating-ui virtual-element shape (avoids a runtime floating-ui dep). */ +export interface Measurable { + getBoundingClientRect(): DOMRect; + contextElement?: Element; +} + +export interface ClampedAnchorOptions { + /** Which side the panel opens toward — the axis edge that gets clamped. */ + side: 'left' | 'right'; + /** The panel's width (must match ``). */ + panelWidth: number; + /** Gap between block and panel (must match `sideOffset`). */ + gap: number; + /** Viewport edge padding to keep (match `collisionPadding`). */ + padding: number; +} + +/** + * Build a clamped virtual anchor over `block`. `win` resolves the viewport width + * (pass the block's own window so it stays iframe / popup correct). + */ +export function clampedBlockAnchor( + block: HTMLElement, + win: Window, + opts: ClampedAnchorOptions +): Measurable { + return { + contextElement: block, + getBoundingClientRect() { + const b = block.getBoundingClientRect(); + const vw = win.innerWidth; + let x: number; + if (opts.side === 'right') { + // panel.left = x + gap → keep panel.left + panelWidth <= vw - padding. + const maxRight = vw - opts.panelWidth - opts.gap - opts.padding; + x = Math.min(b.right, maxRight); + } else { + // panel.right = x - gap → keep panel.right - panelWidth >= padding. + const minLeft = opts.padding + opts.panelWidth + opts.gap; + x = Math.max(b.left, minLeft); + } + // Zero-width strip at the clamped x, spanning the block's height. + return { + x, + y: b.top, + width: 0, + height: b.height, + top: b.top, + right: x, + bottom: b.bottom, + left: x, + toJSON() { + return this; + } + } as DOMRect; + } + }; +} diff --git a/src/uix/eidos/components/palabras/palabras-appearance.svelte b/src/uix/eidos/components/palabras/palabras-appearance.svelte index b4aba31ac..3cf0be5dc 100644 --- a/src/uix/eidos/components/palabras/palabras-appearance.svelte +++ b/src/uix/eidos/components/palabras/palabras-appearance.svelte @@ -2,8 +2,10 @@ /** * Appearance popover — opened by the panel's "Editar" button. Edits THIS * block's own visual override (`block.visual.*`) via `setBlockVisualAtPath`. - * Draggable floating window. Colours use our ``, numbers our - * ``, alignment our ``. + * A framework `` anchored to the active block on its LEFT (opposite + * the main panel), `align='center'` → centred on the block's mid-height; the + * Popover owns positioning + portal + dismissal. Colours use our ``, + * numbers our ``, alignment our ``. */ import type { ProviderSnippetProps } from '$soma/components/words'; import { Button } from '$uix/eidos/components/button'; @@ -11,17 +13,48 @@ import { NumberField } from '$uix/eidos/components/number-field'; import { X } from '$uix/eidos/components/icon'; import PalabrasColorRow from './palabras-color-row.svelte'; - import { createDrag } from './palabras-drag.svelte'; + import { Popover } from '$uix/eidos/components/popover'; + import { clampedBlockAnchor } from './palabras-anchor'; + + // Panel width — kept in sync with `` AND the anchor clamp. + const PANEL_WIDTH = 256; let { api, content, + open, onClose - }: { api: ProviderSnippetProps; content: HTMLElement | null; onClose: () => void } = $props(); + }: { + api: ProviderSnippetProps; + content: HTMLElement | null; + open: boolean; + onClose: () => void; + } = $props(); - const drag = createDrag(); const loc = $derived(api.activeBlockLocation); + // The active block element drives the Popover's `customAnchor` (placed on the + // LEFT of the block, opposite the main panel). The Popover owns positioning + // (flip / shift / fit) + portal + dismissal. + let anchorEl = $state(null); + $effect(() => { + void api.activeBlockId; + void open; + anchorEl = (content?.querySelector('[data-words-active]') as HTMLElement | null) ?? null; + }); + // Clamped virtual anchor (side LEFT): keeps the appearance popover from + // flipping across the block when the left gutter is narrow. See palabras-anchor.ts. + const anchor = $derived.by(() => { + const win = content?.ownerDocument?.defaultView; + if (!anchorEl || !win) return null; + return clampedBlockAnchor(anchorEl, win, { + side: 'left', + panelWidth: PANEL_WIDTH, + gap: 8, + padding: 12 + }); + }); + type Align = 'left' | 'center' | 'right' | 'justify'; type VisualPatch = { color?: string; @@ -76,69 +109,79 @@ ]; - +
+ setVisual({ color: hex })} + /> + setVisual({ background: hex })} + /> +
+ Tamaño (px) + + setVisual({ fontSize: typeof v === 'number' && v > 0 ? v : undefined })} + > + + +
+
+ Interlineado + + setVisual({ lineHeight: typeof v === 'number' && v > 0 ? v : undefined })} + > + + +
+
+ Alineación + { + const a = v[0] as Align; + if (a) setVisual({ align: a }); + }} + > + {#each ALIGNS as a (a.v)} + {a.label} + {/each} + +
+
+
+ +
diff --git a/src/uix/eidos/components/palabras/palabras-panel.svelte b/src/uix/eidos/components/palabras/palabras-panel.svelte index 095a4219e..4c8989c79 100644 --- a/src/uix/eidos/components/palabras/palabras-panel.svelte +++ b/src/uix/eidos/components/palabras/palabras-panel.svelte @@ -1,19 +1,22 @@ + + onValueChange(v[0])} +> + {#each options as opt (opt.value)} + {@const Icon = opt.icon} + + {#if Icon}{:else}{opt.label}{/if} + + {/each} + diff --git a/src/uix/eidos/components/palabras/palabras.css b/src/uix/eidos/components/palabras/palabras.css index dd88cac01..deedfac50 100644 --- a/src/uix/eidos/components/palabras/palabras.css +++ b/src/uix/eidos/components/palabras/palabras.css @@ -46,6 +46,13 @@ [data-palabras-rail] { flex: 0 0 var(--palabras-rail, 32px); position: relative; + /* Bullet-proof containing block for the absolutely-positioned grip slot. If + `position: relative` alone ever fails to register the rail as the slot's + offsetParent, the slot jumps past DOC + CARD (both static — they can't be CBs + without breaking the Words drop-line seam's frame anchor) straight to the outer + frame, landing the grip ~25px LEFT at the card edge. A transform establishes + the containing block via a second, independent mechanism so it can't fall through. */ + transform: translateZ(0); } /* Canvas — the editing column. `min-inline-size: 0` lets it shrink within the flex row instead of overflowing. NOT positioned: the drop-line seam painted by @@ -260,6 +267,11 @@ align-items: center; margin-inline: 0; margin-block: var(--space-4) 0; + /* The image is an ATOMIC block (the figure is contenteditable=false). Without + this, clicking it inside the contenteditable SELECTS the image and the browser + paints its native SOLID selection outline over our dashed active marker. + `user-select` inherits, so this covers the img / tile / placeholder / caption. */ + user-select: none; } [data-palabras-doc] figure[data-words-block='image'] img { display: block; @@ -353,24 +365,19 @@ } /* ── Floating inspector panel ─────────────────────────────────────────────── */ -[data-palabras-panel-card] { - position: absolute; - inset-block-start: var(--space-4); - inset-inline-end: var(--space-4); - inline-size: 17rem; - z-index: 10; - /* Flex column: a pinned header, a SCROLLABLE body (owns its overflow), and a - pinned footer. The card itself never scrolls (overflow hidden), so its width - is constant and the footer always spans edge-to-edge. */ +/* The floating surface IS a framework `` — positioning (side / + align / flip / shift / fit-to-viewport), the portal, dismissal AND the surface + (border / bg / shadow / max-height) all come from the popover recipe. Here we + only RE-SHAPE the content's inner layout into a flex column with a pinned + header, a scrollable body and a pinned footer — overriding the popover recipe's + `display: grid` / padding / gap / `overflow: auto`. Doubled attribute (0,2,0) + beats `[data-popover-content]` (0,1,0) regardless of stylesheet load order. */ +[data-palabras-panel-card][data-palabras-panel-card] { display: flex; flex-direction: column; - max-block-size: calc(100vh - 2 * var(--space-4)); + gap: 0; + padding: 0; overflow: hidden; - border: 1px solid var(--color-neutral-border); - border-radius: var(--radius-lg); - background: var(--color-surface-overlay); - /* Elevation = it reads as a floating window, not a docked sidebar. */ - box-shadow: var(--shadow-lg, 0 16px 40px -12px rgb(0 0 0 / 0.6)); font-family: var(--font-ui); } /* Force the UI SANS across the whole panel + every descendant. The framework @@ -403,7 +410,9 @@ [data-palabras-panel-body] { flex: 1 1 auto; min-block-size: 0; - overflow: hidden; + /* Scrolls within the floating-ui height cap (`--_palabras-float-max-h`) when + the panel can't fit fully on screen — header + footer stay pinned. */ + overflow-y: auto; padding: 0 var(--space-4) var(--space-4); } /* The block's defining icon, tinted by the schema's dotToken (via currentColor). */ @@ -469,9 +478,13 @@ [data-palabras-src-group] [data-palabras-text] { flex: 1 1 auto; } -/* Slider field — label + value ABOVE, the slider full-width BELOW (photo-app). */ +/* Slider field — label + value ABOVE, the slider full-width BELOW (photo-app). + Compact (Apariencia stacks 7 in a row): tight label line-height + reduced gaps. */ [data-palabras-field][data-slider] { - gap: var(--space-1); + gap: var(--space-0-5); +} +[data-palabras-field][data-slider] + [data-palabras-field][data-slider] { + margin-block-start: var(--space-2); } [data-palabras-field][data-slider] [data-palabras-slider-head] { display: flex; @@ -479,6 +492,10 @@ justify-content: space-between; gap: var(--space-2); } +[data-palabras-field][data-slider] [data-palabras-field-label], +[data-palabras-field][data-slider] [data-palabras-slider-value] { + line-height: 1.1; +} [data-palabras-field][data-slider] [data-palabras-slider-value] { font-size: var(--font-size-xs); color: var(--color-content-muted); @@ -551,10 +568,10 @@ [data-palabras-text][data-invalid] { border-color: var(--color-threat-border, var(--color-danger-border, crimson)); } -/* Length input (px / rem / %) — compact, right-aligned like a number. */ -[data-palabras-text][data-length] { - inline-size: 5.5rem; - font-family: inherit; +/* CSS-length field (Ancho / Alto) — the framework (its own border / + bg / invalid ring / xs size) with −/+ steppers, kept compact in the inline row. */ +[data-palabras-field][data-inline] [data-css-field] { + inline-size: 9rem; } /* Number field (width / height) — compact input + unit suffix, right-aligned. */ [data-palabras-number] { @@ -626,18 +643,11 @@ } /* ── Appearance popover (2nd window, opened by "Editar") ───────────────────── */ -[data-palabras-appearance] { - position: absolute; - inset-block-start: var(--space-4); - /* Sits just left of the main panel (17rem wide). */ - inset-inline-end: calc(17rem + var(--space-3)); - inline-size: 16rem; - z-index: 11; - padding: var(--space-4); - border: 1px solid var(--color-neutral-border); - border-radius: var(--radius-lg); - background: var(--color-surface-overlay); - box-shadow: var(--shadow-lg, 0 16px 40px -12px rgb(0 0 0 / 0.6)); +/* Also a framework `` (positioning + surface from the recipe); + we only force the UI sans across it + its descendants (the popover's content + archetype would otherwise leak the reading serif into the field labels). */ +[data-palabras-appearance][data-palabras-appearance], +[data-palabras-appearance][data-palabras-appearance] * { font-family: var(--font-ui); } /* NumberField in the appearance popover — compact, fixed width so the grid's @@ -660,25 +670,6 @@ background: var(--color-surface-muted); } -/* ── Mobile: the floating panels become bottom drawers ────────────────────── */ -@media (max-width: 40rem) { - /* The rail + grip stay identical to desktop (option A). Only the floating - panels change to bottom drawers below. */ - [data-palabras-panel-card], - [data-palabras-appearance] { - position: fixed; - inset: auto 0 0 0; - inline-size: auto; - max-block-size: 70vh; - overflow-y: auto; - border-inline: none; - border-block-end: none; - border-radius: var(--radius-lg) var(--radius-lg) 0 0; - /* Anchored bottom drawer — ignore any drag offset from the desktop float. */ - transform: none !important; - } - /* Appearance drawer sits above the main one. */ - [data-palabras-appearance] { - z-index: 12; - } -} +/* Mobile: the panels stay framework ``s — the popover recipe already + keeps them within `calc(100vw - …)` and floating-ui shifts them on screen, so + no bespoke bottom-drawer layer is needed. */ diff --git a/src/uix/eidos/components/palabras/palabras.svelte b/src/uix/eidos/components/palabras/palabras.svelte index 29da76f3b..121fcd82f 100644 --- a/src/uix/eidos/components/palabras/palabras.svelte +++ b/src/uix/eidos/components/palabras/palabras.svelte @@ -23,7 +23,6 @@ import PalabrasBreadcrumb from './palabras-breadcrumb.svelte'; import PalabrasColumnInserter from './palabras-column-inserter.svelte'; import PalabrasHandle from './palabras-handle.svelte'; - import PalabrasNav from './palabras-nav.svelte'; import PalabrasPanel from './palabras-panel.svelte'; import type { PalabrasProps } from './types'; @@ -128,31 +127,25 @@ - {#if panelOpen} - (appearanceOpen = true)} - onClose={() => { - panelOpen = false; - appearanceOpen = false; - }} - /> - {#if appearanceOpen} - (appearanceOpen = false)} - /> - {/if} - { - panelOpen = false; - appearanceOpen = false; - }} - /> - {/if} + + (appearanceOpen = true)} + onClose={() => { + panelOpen = false; + appearanceOpen = false; + }} + /> + (appearanceOpen = false)} + />

haz clic en un bloque y pulsa su tirador ⋮ para ver sus propiedades · diff --git a/src/uix/eidos/components/palabras/panel-schema.ts b/src/uix/eidos/components/palabras/panel-schema.ts index 48b525e0d..6e24b497e 100644 --- a/src/uix/eidos/components/palabras/panel-schema.ts +++ b/src/uix/eidos/components/palabras/panel-schema.ts @@ -10,6 +10,7 @@ * only). The rest land once the architecture is verified end-to-end. */ +import { TextAlignStart, TextAlignCenter, TextAlignEnd } from '$uix/eidos/components/icon'; import type { PalabrasBlockSchema, PalabrasPanelRegistry } from './types'; const heading: PalabrasBlockSchema = { @@ -84,6 +85,18 @@ const image: PalabrasBlockSchema = { title: 'Tamaño y posición', defaultCollapsed: true, fields: [ + { + key: 'align', + type: 'pills', + label: 'Alineación', + gatedBy: 'fullWidth', + default: 'center', + options: [ + { value: 'left', label: 'Izquierda', icon: TextAlignStart }, + { value: 'center', label: 'Centro', icon: TextAlignCenter }, + { value: 'right', label: 'Derecha', icon: TextAlignEnd } + ] + }, { key: 'fit', type: 'pills', @@ -96,24 +109,13 @@ const image: PalabrasBlockSchema = { ] }, { key: 'fullWidth', type: 'toggle', label: 'Ancho completo', default: false }, - { - key: 'align', - type: 'pills', - label: 'Alineación', - gatedBy: 'fullWidth', - default: 'center', - options: [ - { value: 'left', label: 'Izq.' }, - { value: 'center', label: 'Centro' }, - { value: 'right', label: 'Der.' } - ] - }, { key: 'width', type: 'length', label: 'Ancho', placeholder: 'auto', unit: 'px', + units: ['px', '%', 'rem', 'em', 'vw', 'vh'], gatedBy: 'fullWidth' }, { @@ -122,6 +124,10 @@ const image: PalabrasBlockSchema = { label: 'Alto', placeholder: 'auto', unit: 'px', + // Same units as width, `%` included (per request). NOTE: a percentage + // height only takes effect with a definite-height ancestor — otherwise + // the browser ignores it — but we accept it rather than block the input. + units: ['px', 'rem', 'em', 'vh', 'vw', '%'], gatedBy: 'fullWidth' } ] diff --git a/src/uix/eidos/components/palabras/types.ts b/src/uix/eidos/components/palabras/types.ts index 31a759ba2..35e93bf09 100644 --- a/src/uix/eidos/components/palabras/types.ts +++ b/src/uix/eidos/components/palabras/types.ts @@ -1,4 +1,6 @@ +import type { Component } from 'svelte'; import type { ProviderProps } from '$soma/components/words'; +import type { CssUnit } from '$sium'; /** Output / persistence format for `saveas`. The engine already serializes to all three. */ export type PalabrasSaveAs = 'md' | 'html' | 'json'; @@ -41,6 +43,17 @@ export type PalabrasFieldType = | 'slider' | 'length'; +/** One choice in a `pills` / `select` field. */ +export interface PalabrasFieldOption { + /** Value written raw to the block prop when picked. */ + readonly value: string | number | boolean; + /** Visible (or accessible, when `icon` is set) label. */ + readonly label: string; + /** Optional eidos icon component. When set, the option renders icon-only and + * `label` becomes its accessible name + tooltip (e.g. alignment controls). */ + readonly icon?: Component; +} + /** One editable property of a block, shown as a control in the panel. */ export interface PalabrasFieldDef { /** @@ -60,6 +73,13 @@ export interface PalabrasFieldDef { readonly max?: number; readonly step?: number; readonly unit?: string; + /** `length` field: unit whitelist passed to `` (e.g. + * `['px','rem']`). The first entry is the default unit for a bare number. + * Absent → every CSS unit allowed. Alto (height) lists every absolute-resolving + * unit (px/rem/em/vh/vw) but omits `%`: a percentage height is inert without a + * definite-height ancestor, whereas px/rem/em/vh resolve to a real height. + * Width keeps `%` (a percentage width resolves against the content column). */ + readonly units?: readonly CssUnit[]; /** Disable this control when the active block's `node[gatedBy]` is truthy — e.g. * width / height / align gated by `fullWidth` (the engine ignores them then). */ readonly gatedBy?: string; @@ -74,10 +94,7 @@ export interface PalabrasFieldDef { * height where 0 is meaningless. Filters leave this off (0 is a real value). */ readonly clearWhenZero?: boolean; /** Choices for `pills` / `select`. The `value` is written raw to the prop. */ - readonly options?: readonly { - readonly value: string | number | boolean; - readonly label: string; - }[]; + readonly options?: readonly PalabrasFieldOption[]; /** Read-only display (e.g. an `'Auto'` chip). */ readonly readOnly?: boolean; }