astra
alpha-0.1-background
alpha-0.1-dir-prefs
alpha-0.1-sec-dom
menubar-v4-safe
active-uix
morfo-runtime
morfo-driven-soma
semantuix
glm-5
main
sium-v1.0
${ noResults }
581 Commits (207774714ad048e17e944ee464017cefa853b5d6)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
207774714a |
feat(pin-input): port from soma to eidos (Tier 1 sprint, 5/5 — DONE)
Multi-cell OTP / verification code input. Two parts (Provider, Cell) with a snippet-driven cell iteration. Architecture: - A transparent absolutely-positioned <input> captures every keystroke, paste and IME event (soma). - Visible bordered cells follow the hidden input's caret via data-active / data-filled emitted by the morfo. - Single-input approach (shadcn / Bits family) — preserves paste distribution, autocomplete='one-time-code' for SMS autofill on iOS/Android, and IME quality. Eidos surface: - size: 'sm' | 'md' | 'lg' | 'xl' (responsive), cascades cell dimension + glyph size tokens to all cells - Recipe paints square bordered cells with focus ring tracking data-active, subtle bg lift on data-filled, threat-toned border when [data-invalid] cascades from the provider Morfo: - scope: ['soma'] → ['soma', 'sema', 'eidos'] - apg URL added (Textbox pattern — OTP has no formal ARIA pattern) - Passive at the morfo layer (no events array; perceptual feedback via the commit family base when onComplete fires) - README documents the passive classification Comparativa: aligned with shadcn / Bits UI (single hidden input). Ark and Chakra use one input per cell which loses paste / IME quality. Sidebar nav: 'Pin input' added under Forms (next to Tags input). === Sprint Tier 1 — DONE === 1. toggle-group |
5 months ago |
|
|
21eb8a6264 |
feat(context-menu): port from soma to eidos (Tier 1 sprint, 4/5)
14-part compound — DropdownMenu surface minus CheckboxGroup. Same visual recipe vocabulary; the only differences vs DropdownMenu are the trigger surface (a `<span>` wrapping right-clickable content rather than a `<button>` toggle) and cursor-anchored positioning (soma's floating layer handles that). Parts: Provider · Trigger · Content · Arrow · Item · Group · GroupHeading · Separator · CheckboxItem · RadioGroup · RadioItem · Sub · SubTrigger · SubContent. Recipe (context-menu.css): - Panel chrome + item rows identical to dropdown-menu - Trigger gets a subtle `data-state='open'` dashed outline as a "this region summoned the menu" affordance - Recipe duplication over aliasing — each component evolves freely without CSS cascade coupling Sidebar nav: 'Context menu' added under Overlays. Morfo scope: ['soma'] → ['soma', 'sema', 'eidos']. Sprint progress: 4/5 (toggle-group, alert-dialog, dropdown-menu, context-menu). Last is pin-input. Checks: svelte-check 0 errors, component:audit 91/91 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
f19b5c7270 |
docs(uix): add Toggle group · Alert dialog · Dropdown menu to sidebar nav
Wires the three Tier 1 sprint components into the IA: - 'Toggle group' under Controls (next to Toggle) - 'Alert dialog' under Overlays (next to Dialog) - 'Dropdown menu' under Overlays (next to Popover) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
4e477e637b |
feat(dropdown-menu): port from soma to eidos (Tier 1 sprint, 3/5)
Full 15-part compound — pass-through wrappers over soma with the canonical visual recipe. Parts: Provider · Trigger · Content · Arrow · Item · Group · GroupHeading · Separator · CheckboxItem · CheckboxGroup · RadioGroup · RadioItem · Sub · SubTrigger · SubContent. API parity with Bits UI (richest reference): CheckboxGroup + RadioGroup binding arrays / scalars, nested Sub-menus, Arrow pointer. Radix Primitives + Ark UI lack CheckboxGroup; Chakra v3 + Radix Themes lack Arrow. Recipe (dropdown-menu.css): - Uniform chrome across Content + SubContent (panel shadow, border, padding, max-height) - Items share one set of tokens (hover/focus highlight, disabled opacity, indicator slot via `::before` for Checkbox/Radio data-state='checked') - SubTrigger gets a chevron via `::after` - GroupHeading: muted uppercase, micro-tracked - Separator: 1px subtle divider - Enter animation on `data-state='open'` No `variant` / `color` / `size` knobs at the eidos layer — menus are themselves the visual variant of "list of actions" within an app. Per-item destructive coloring deferred to next pass (cascade via item's `data-color` attr). Morfo scope: ['soma'] → ['soma', 'sema', 'eidos']. Sound comes from the family bases (`emerge` for open/close, `commit` for select) — no per-component cascade needed. Demo: 6-tab template, items hooked to `lastSelected` tracker, all three groupings demoed (Items / CheckboxGroup / RadioGroup / Sub). Checks: svelte-check 0 errors, component:audit 91/91 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
d5630a528f |
feat(alert-dialog): port from soma to eidos (Tier 1 sprint, 2/5)
Modal Dialog variant with role=alertdialog, forced modal=true, and click-outside disabled. Soma delegates to the Dialog runtime (variant='alertdialog') so DOM markers are Dialog's (data-dialog-*) — eidos reuses Dialog's recipe for chrome and adds two alert-dialog-specific buttons: Action + Cancel. Eidos shape: - Provider, Content, Action, Cancel — alert-dialog-specific wrappers - Trigger, Overlay, Title, Description, Header, Footer, Portal — re-exported from Dialog's eidos barrel (zero duplication) Recipe (alert-dialog.css): - Only styles Action + Cancel - Buttons consume Toggle palette tokens (same "interactive button" vocabulary used by <Toggle> and <ToggleGroup.Item>) - Auto-color cascade: Action picks up the dialog's data-color so a risk/threat-flagged dialog gets a red/amber Action button without manual recipe code - Cancel stays neutral so the safe-choice contrast is preserved Morfo: - scope: ['soma'] → ['soma', 'sema', 'eidos'] - The morfo declares Action + Cancel parts only (Provider is virtual, other parts inherited via Dialog runtime delegation) - Classified passive at this contract level (Dialog owns the events); README documents this in §"Passive justification" Demo: 6-tab canonical template with intent / size / position chips, soma + eidos snippets, references comparison table vs Radix Primitives / Bits UI / Ark UI / Chakra v3 / Radix Themes / shadcn. Checks: svelte-check 0 errors, component:audit 91/91 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
61ffbade46 |
feat(toggle-group): port from soma to eidos (Tier 1 sprint, 1/5)
First component in the soma→eidos porting sprint. Two-part compound (Provider + Item), modeled on radio-group's structure. Eidos surface: - variant: 'solid' | 'outline' | 'ghost' — mirrors <Toggle> - color: 7 tones, propagated to items via data-color cascade - size: xs/sm/md/lg/xl (responsive) - attached: boolean — segmented control affordance (shared borders, active item z-indexed above neighbors) - block: boolean — items grow to fill the inline-size - iconOnly: per-item flag — square aspect, sr-only label Items consume the Toggle palette token vocabulary (--toggle-*) so a standalone <Toggle> and a <ToggleGroup.Item> at the same variant × color × size are visually identical — single source of truth for the "toggle button" recipe. Morfo scope fix: ['soma'] → ['soma', 'sema', 'eidos']. Adds apg URL (WAI-ARIA Button pattern) so the audit's A-1.4 rule passes. The commit-toggle event's sound comes from the `commit` family base — no per-component cascade needed. Demo: canonical 6-tab template (Live / API / Morfo / Sema / Recipe / A11y) with every prop exposed as a chip/switch, soma + eidos snippets update live, sema "▶ play" button on the commit-toggle event. Reference comparison vs Radix Primitives / Bits UI / Ark UI / Chakra v3 / Mantine / Radix Themes / shadcn documented in README §Comparativa. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
4324cb489d |
fix(splitter): register sema scope + cascade so handle-pick/drag actually sound
Two missing registrations meant the splitter sema cascade from the
previous commit never reached the engine — handle-pick and handle-drag
were silent, only commit-set sounded (because the `commit` family has
a sound payload baked into its SEMA_MAP base, while `handle` only has
haptic and depends on the cascade for sound).
Fixes:
1. morfo scope: ['soma'] → ['soma', 'sema', 'eidos']. Without 'sema'
the per-component cascade rules in src/uix/sema/components/splitter.ts
are never honored.
2. web/routes/uix/+layout@.svelte: add splitterSema to the
`components: [...]` array passed to defineEngineSemantic. The
sema engine only loads cascade packs it's explicitly given, so
forgetting this line silently drops all splitter cascade rules.
Verified in browser at /uix/components/splitter via Live tab (real
pointer interaction, AudioContext.createOscillator counter):
handle-pick → 2 oscillators (handle.pickup.air, gain 0.10)
handle-drag → 16 oscillators across 8 throttled emits — the
dynamic resolveSplitterDragSound curve is playing
per-frame as expected
commit-set → 2 oscillators (handle.snap.chime, gain 0.14)
Note: the Sema tab's static "▶ play" button still shows 0 oscillators
for handle-drag — same as Slider's demo. By design: that button calls
engine.emit() directly without the per-emit `signal.overrides` that
soma injects during real drag. The dynamic curve IS the payload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
59505e6da2 |
feat(splitter): sema + dynamic drag-signal sound infrastructure
Splitter previously had no sema bindings — handle-pick / handle-drag /
commit-set fired silently. Closes the asymmetry with Slider and Drawer.
Sema (src/uix/sema/components/splitter.ts):
- handle-pick → sound('handle.pickup.air', gain 0.10) + tick haptic
- handle-drag → channels: ['sound', 'haptic'] (channels-only; per-emit
signal.overrides from soma own the dynamic curve so cascade doesn't
clobber them)
- commit-set → sound('handle.snap.chime', gain 0.14) + tap haptic
Sound resolver (src/uix/sema/sounds.ts):
- resolveSplitterDragSound — heavier sonic register than slider since
splitters move whole panels, not a thumb:
pitch 480..820 Hz (slider: 760..1280)
centroid 2400..4800 (slider: 3600..6800) — less brilliant
roughness 0.006 (slider: 0.003) — woody texture, not metallic
envelope attack 16 / decay 72 / duration 76 — perceptual mass
gain 0.025 + velocity*0.03 — restrained ceiling, long swipes ok
Soma drag-signal infrastructure (splitter-provider.svelte.ts):
- Mirrors slider's frame-throttled signal pipeline:
dragSignalFrame / pendingDragPercent / lastDragSignalAt / lastDragPercent
DRAG_SIGNAL_MS = 72ms throttle (≈14 Hz emit cadence)
- dragSignalOverrides(percent01) computes velocity01 from frame delta,
builds SemaSignatureOverride with sound (via resolveSplitterDragSound)
and haptic (tick scaled by velocity).
- queueHandleDrag / cancelQueuedHandleDrag wrap requestFrame so each
pointermove only schedules one trigger per throttle window.
- onpointerdown calls resetDragSignal(startSizeA/100) to anchor the
velocity baseline at grab time.
- onpointerup / onlostpointercapture / unmount $effect all cancel the
queued frame so we don't fire post-release.
Verified in browser at /uix/components/splitter:
handle-pick → 1 event (family: handle)
handle-drag → frame-throttled across 6 pointermoves
commit-set → 1 event (family: commit, intent: neutral)
Checks: svelte-check 0 errors, 133/133 vitest (splitter + sema),
component:audit 91/91 PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
b504c1c4b7 |
feat(eidos foundation): vertebrate typography via named-style aliases + R-2.7
Single source of truth for typography values that the recipe layer
consumes. The foundation aliases `--font-ui` and `--leading-ui` (read
by ~30 recipe tokens in `lib/recipes/base.ts`) now derive from the
canonical `label` named style instead of carrying duplicate literals:
--style-label-font-family: var(--font-family-primary);
--style-label-line-height: 1.25;
--font-ui: var(--style-label-font-family, var(--font-family-primary));
--leading-ui: var(--style-label-line-height, 1.25);
Chain: typography.ts styles → --style-{name}-* → --leading-ui / --font-ui
→ recipe tokens → component CSS. Editing
`STATIC_TYPOGRAPHY.styles.label.lineHeight` now propagates to every
recipe in one go.
Why not push recipes to consume `--style-{name}-*` directly:
- t-shirt sizes (xs/sm/md/lg/xl) don't map to four semantic buckets
- per-component matices (description/caption/hint) need their own
color / weight / letter-spacing
- ref libraries (Radix Themes, Mantine, MUI, Chakra) all keep
numerical scale for component internals; semantic layer is only for
user-facing typography primitives (`<Text variant="body2">`)
Audit rule R-2.7 (warn): detects literal font-size / font-weight /
line-height / letter-spacing in eidos component CSS. Escape valves:
var(...), numeric identities (0/0px/1), keywords (inherit/initial/
unset), or trailing `/* literal: <reason> */` comment. Current run
flags 6 components with letter-spacing/font-size literals (all
intentional micro-tracking and em-relative; can be annotated case by
case).
Documentation:
- src/uix/eidos/README.md § "Vertebración tipográfica" — two-layer
architecture rationale, alias chain diagram, comparison vs Radix
Themes / Chakra / Mantine / MUI, escape valves
- web/routes/uix/lib/COMPONENT_AUDIT_GUIDE.md § 4.11 — pointer to
R-2.7 + cross-link to the foundation doc
Verified end-to-end in browser at /uix/components/field:
--font-ui → 'Instrument Sans', system-ui, sans-serif
--style-label-font-family → 'Instrument Sans', system-ui, sans-serif
--leading-ui → 1.25
--style-label-line-height → 1.25
computed [data-field-label].line-height → 17.5px (= 14 × 1.25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
c703e2e7ff |
fix(scroll-area): split mount-vs-visibility — the real type='scroll' / 'hover' bug
Root cause: soma/scroll-area-scrollbar.svelte gated the entire <div> on
`state.shouldShow`. For type='hover' / 'scroll', `shouldShow` starts
false, so the bar never reached the DOM. With no DOM node:
- bind:this never fired → scrollbar's `ref` stayed null
- `requestFrame(cb, ref.current)` ran with a null element, so the
`mounted` flag never flipped
- the hover / drag listeners had no element to attach to
- the recipe's `data-state` opacity transition had no element to
animate
- `shouldShow` could therefore never become true → deadlock
Fix — split the concern:
- `shouldMount` (new derived) — true whenever the axis overflows OR
type='always'. Drives the `{#if}` in the part component.
- `shouldShow` — drives `data-state="visible|hidden"`. The recipe
transitions opacity.
The bar now lives in the DOM whenever it could ever be needed, fades
in/out via CSS, and stays a stable hit target for hover / click /
drag. Dropped the `mounted` flag and the requestFrame mount-frame
scheduling — they only existed to suppress a flash that the opacity
transition handles cleanly.
Also dropped the inline `border-radius: inherit` on the thumb so the
recipe's `--scroll-area-thumb-radius` (the new `radius` prop) wins.
Without this the prop was silently overridden to `0px`.
Test updates:
- removed `cancelFrame` assertion (no mount-frame to cancel)
- added `shouldMount` assertion
Verified in browser at /uix/components/scroll-area:
- type='hover' + vertical: bar mounts hidden, fades in on root hover,
fades out after delay
- type='scroll' + horizontal: bar mounts hidden, fades in on scroll,
thumb tracks scrollLeft, fades out after 600ms
- type='always' + both: both bars + corner visible at all times
- size=lg, radius=full: 12px-wide bar, fully rounded thumb
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
e11d3539ef |
feat(scroll-area): add size/radius/scrollbars props + fix scroll-type race
Reference-library parity (Radix Themes, Chakra v3): - size: 'xs' | 'sm' | 'md' | 'lg' (Chakra t-shirt scale) → thickness via --scroll-area-scrollbar-size. - radius: 'none' | 'sm' | 'md' | 'lg' | 'full' (Radix Themes) → thumb curvature via --scroll-area-thumb-radius. - scrollbars: 'vertical' | 'horizontal' | 'both' (Radix Themes) → recipe-level sugar that auto-mounts Scrollbar / Thumb / Corner from the root. Explicit composition still supported (omit the prop). Bug fix — type='scroll' + horizontal sometimes stuck hidden: - `mounted` on ScrollAreaScrollbarProvider is now $state. Previously a plain field, so the `shouldShow` derived didn't invalidate when the requestFrame mount-flag flipped during the first frame. - The reveal-on-scroll $effect now skips its first run via a `firstRun` flag. That first run was the subscription pass with scrollTop/scrollLeft still 0 — it was eagerly calling show() and setting `visible=true` before `mounted` flipped. Subsequent real scrolls then assigned visible=true again (no-op), so shouldShow didn't re-run and the bar stayed hidden until the hide timer expired. Now the bar only reveals on genuine scroll deltas. Demo: chip controls for size + radius, switched to scrollbars prop on the root (drops the explicit Scrollbar/Thumb children), eidos snippet shows the recipe-sugar form, soma snippet still shows the explicit Radix-style composition. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
560ce8f66c |
fix(scroll-area, drawer): horizontal scrollbar visibility + drawer drag affordance
- scroll-area soma: defer the mount-frame request to `$effect` so SSR doesn't hit `dom::window_required` during constructor evaluation. - scroll-area recipe: scrollbars now `position: absolute` (overlay style) so the horizontal track lands at the viewport's block-end instead of flowing below it. `:has` rules reserve corresponding edges when both axes mount; corner switched to absolute as well. - drawer demo: drop the ↑↓←→ glyph pop-outs and `overflow: visible` during dragging. The handle's own pill grip is the affordance — the same shape the splitter uses, no extra chrome. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
bd2e40366a |
feat(layout): Layout Batch 3 — scroll-area, separator, splitter + splitter sema rewire
3 new layout primitives close the air port set (15 / 15 done).
`npm run component:audit`: 91 / 91 PASS, all 3 new PASS with 0 errors.
**scroll-area** — eidos compound wrapper over the existing soma:
`<ScrollArea><ScrollArea.Viewport>{content}</ScrollArea.Viewport>
<ScrollArea.Scrollbar orientation="vertical"><ScrollArea.Thumb />
</ScrollArea.Scrollbar><ScrollArea.Corner /></ScrollArea>`. Mirrors
Radix Primitives / Bits UI shape. Demo 667 lines.
**separator** — visual primitive (`scope: ['eidos']`) with conditional
ARIA: `role="separator"` + `aria-orientation` when not decorative,
`aria-hidden="true"` when decorative. New morfo
`src/uix/morfo/components/separator.ts`. Demo 645 lines.
**splitter** — eidos compound over existing soma:
`<Splitter orientation="horizontal"><Splitter.Panel>{a}</Splitter.Panel>
<Splitter.ResizeTrigger /><Splitter.Panel>{b}</Splitter.Panel>
</Splitter>`. Demo 824 lines.
**Splitter sema rewire — match Slider's handle/drag/commit shape:**
The morfo previously declared only `commit-resize`. Direct-manipulation
primitives (Slider, Splitter) should share the same event vocabulary so
audio/haptic projections feel coherent across the family. Renamed +
expanded to mirror Slider:
- `handle-pick` (family: handle, verb: pick, sequence: coincident,
target: resize-trigger) — fires on pointerdown when user grabs the
trigger.
- `handle-drag` (family: handle, verb: drag, sequence: coincident,
target: resize-trigger) — fires on each pointermove frame.
- `commit-set` (family: commit, verb: set, intent: neutral,
sequence: post, target: provider) — fires on pointerup. The runtime
`commit-set` handler is wired to call the consumer's `onResizeEnd`
callback, so sema and JS callback land at the same moment (parity
with Slider's `onValueCommit`).
Soma provider triggers these via `this.runtime.trigger(name)` in
`onpointerdown` / `onpointermove` / `notifyResizeEnd` (the latter
unchanged externally — internally now routes through the runtime
trigger).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
a08a0a9890 |
fix(aspect-ratio): fill inline axis by default + panel demo uses solid colors
Two bugs surfaced when verifying the panel demo visually: 1. **AspectRatio shrunk to content width inside grid/flex containers.** Grid items default to `width: max-content`, so AspectRatio without an explicit width was sizing itself to its child's intrinsic content instead of filling the slot. The ratio still applied but against the collapsed width, producing a tiny box. Fix: recipe sets `inline-size: var(--box-width, 100%)` so the default is "fill the slot". `width=…` from Box still wins; `maxWidth=…` constrains the upper bound exactly as before. Block-level containers are unaffected (100% of the line box matches their natural width). 2. **Panel demo gradient invisible in dark mode.** The demo used `--color-primary-track` + `--color-affirm-track` which are surface tokens (near-black in dark theme). Swapped to `*-solid` tokens with `--color-content-on-solid` text for high contrast in both themes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
d421f1fb5d |
feat(typography): 8 primitives — Heading, Display, Code, CodeBlock, Kbd, Mark, Highlight, Link
Full canon for the rest of the typography family, following Text
(commit `03531709`) as template. Each primitive ships morfo (passive,
scope eidos, 0 events) + full eidos set (svelte/css/types/index +
README with required sections) + canon 6-tab demo (≥400 lines each).
**Heading** — semantic h1..h6 via `level` prop, visual style decoupled
via `style` prop (defaults to match level but `<Heading level={3}
style="h1">` allowed). Demo 594 lines.
**Display** — always-h1 page-title primitive. Always uses
`style="hero"`. Tracking via literal em values until foundation emits
hero-scale tracking tokens. Demo 546 lines.
**Code** — inline code via `<code>`. `variant: 'plain' | 'soft' |
'outline'` for subtle background chrome. Demo 423 lines.
**CodeBlock** — multi-line `<pre><code>` with optional title +
copy button (button utility, NOT a sema verb — kept out of morfo per
2-of-3 rule). Demo 402 lines.
**Kbd** — keyboard shortcut chrome over `style="code"`. Demo 418 lines.
**Mark** — text highlight via `<mark>`. Palette uses canonical
`ColorRole` intents (default `fulfill` ≈ legacy yellow). Demo 402 lines.
**Highlight** — substring highlighter inside Text. Scans children for
`query` and wraps matches. Same intent palette as Mark. Demo 407 lines.
**Link** — text link via `<a>`. `variant: 'default' | 'subtle' |
'plain'` for underline style, `intent: ColorRole` for color. Passive
on the morfo — hover/active is CSS-only, navigation belongs to the
consumer (router). Demo 448 lines.
**Architectural deviations from air documented per README's
Decisiones section:**
- Heading: semantic level vs visual style decoupling
- Code split into inline `<Code>` + block `<CodeBlock>` (was one
component with `variant='block'` in air)
- Mark/Highlight palette swapped from literal hue names to canonical
ColorRole intents (theme contract)
- Link palette opened to full ColorRole; `quiet`/`emphasized` air
variants dropped in favour of composable patterns
`npm run component:audit`: **90 / 90 PASS**, 0 NEEDS-WORK.
svelte-check: 0 errors, 0 new warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
0353170982 |
feat(typography): Text — POC consuming new --style-* foundation tokens
First typography primitive consuming the named-style tokens added in
`015bd761`. Sets the canon for the remaining typography ports
(Heading, Display, Code, CodeBlock, Kbd, Mark, Highlight, Link).
API:
- `style: 'body' | 'prose' | 'label' | 'caption'` (default `body`).
Selects which `--style-{name}-*` token set the recipe consumes.
- Per-axis overrides via `size`, `family`, `weight`, `color`, `align`.
Each writes a local `--_text-{axis}` CSS variable that wins over
the named style's default via the cascade.
- `italic`, `underline` shorthand booleans.
- `truncate` (single-line ellipsis) + `clamp={N}` (multi-line via
CSS `-webkit-line-clamp`).
- `as` prop switches the tag (default `'span'` — inline composable).
- `tagStyle` escape hatch for one-off inline CSS that the prop
surface doesn't cover (separate from the `style` named-style prop).
Architectural choices documented in README:
- Sizes map to eidos `xxs..xxxl` (not air's `1..12`).
- Families use the eidos slot names (`primary/secondary/mono/display`)
not air's domain aliases (`ui/prose/code`) — coherence with the
foundation contract.
- Colors are restricted to the content-role slots
(`--color-content-*`) to preserve contrast guarantees. Free colors
via `tagStyle`.
- Canvas line-count measurement (air's `text` + bindable `lineCount`)
deferred; CSS line-clamp covers the visual-truncate use case.
Sidebar nav gains a `Typography` group with Text as its first entry.
`npm run component:audit`: 82 / 82 PASS, 0 NEEDS-WORK. svelte-check 0
errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
015bd761bd |
feat(eidos foundation): emit --style-{name}-* tokens for named typography styles
`STATIC_TYPOGRAPHY.styles` already defined 11 named typography styles
(hero, h1..h6, body, prose, label, caption, code) but the renderer
only emitted families / sizes / weights — the style entries lived
as data with no CSS reflection.
Extend `appendTypographyDeclarations` so each named style emits:
- `--style-{name}-font-family` → `var(--font-family-{family})`
- `--style-{name}-font-size` → `var(--font-size-{size})`
- `--style-{name}-line-height` → literal or `var(--font-line-height-{size})`
- `--style-{name}-letter-spacing` → literal or `var(--font-letter-spacing-{size})`
- `--style-{name}-font-weight` → `var(--font-weight-{weight})` (or numeric)
- `--style-{name}-color` → `var(--color-{dot.path → dash-path})`
Responsive sizes (e.g. `hero.size = { base: 'xxl', md: 'xxxl' }`)
emit the base value into `:root` and per-breakpoint
`@media (min-width: …)` blocks that override the same variables.
Breakpoint thresholds match `$libs/dom/responsive` (sm: 480, md: 768,
lg: 1024, xl: 1280, xxl: 1536).
`renderStaticCss` now iterates `STYLE_BREAKPOINT_ORDER` after the
main `:root` block and appends one media-query block per breakpoint
that has at least one responsive override. The `indentBlock` helper
preserves indentation inside the wrapper.
`base.css` regenerated. svelte-check 0 errors, `npm run component:audit`
81 / 81 PASS unchanged. This is foundation-only; no components consume
the new tokens yet — that lands in the typography port (Text + Heading
+ Display + Code + CodeBlock + Kbd + Mark + Highlight + Link).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
376f9e33eb |
feat(layout): Layout Batch 2 — aspect-ratio, auto-grid, banner, float
4 new layout primitives, full canon (morfo + eidos full set + canon
demo + README). `npm run component:audit`: 81 / 81 PASS, 0 NEEDS-WORK.
**AspectRatio** — composes through Box; emits single `--aspect-ratio`
CSS var (uses modern CSS `aspect-ratio` property, drops legacy
padding-bottom hack). Accepts numeric (16/9 → 1.777…) or string
("16/9") values.
**AutoGrid** — composes through Grid. Resolves `templateColumns` from
`minItemWidth` to `repeat(auto-fill, minmax(MIN, 1fr))` in JS so the
recipe stays declarative. Uses `auto-fill` (not auto-fit) to preserve
empty tracks when item count is low.
**Banner** — `<header role="banner">` announcement strip with intent
(full 8-role UIX `ColorRole`), variant (`ChipVariant` soft/solid/
outline/ghost), size (sm/md/lg). Dismissal is composition-driven:
consumer wraps in {#if show} and adds `<Banner.Close>`. No
`dismissible` boolean.
**Float** — CSS `float` primitive with logical `inline-start` /
`inline-end` sides. Redefined from air's 9-zone overlay primitive
(which becomes a future `<Positioned>` component if real demand
surfaces). Useful for inline images / pull-quotes / drop caps.
Each component ships morfo (`scope: ['eidos']`, 1 part, 0 events,
justified) + eidos set + README (Baseline / Comparativa /
Decisiones / Eventos Sema / Gaps with disposition markers /
Referencias / Passive justification) + canon demo with 6 tabs.
Sidebar nav appends the 4 entries to the existing Layout group.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
265a3946b5 |
feat(layout): plug Batch 1 gaps — Flex.alignContent + Grid.columns/rows/inline/alignContent
Addresses the "should" features documented as gaps in the layout
READMEs from `fdb0e680`. Audit script still reports 77 / 77 PASS,
svelte-check 0 errors.
**Flex**
- `alignContent` prop (multi-line cross-axis alignment). Maps to
`--flex-align-content`; defaults to `normal` in the recipe so it
has no effect when wrap is off. Closes the Radix / Chakra parity
gap.
**Grid**
- `inline` boolean — renders as `display: inline-grid` (paridad con
Flex.inline).
- `columns` / `rows` numeric shorthands — Radix Themes pattern.
Internally resolved to `repeat(N, minmax(0, 1fr))`. `templateColumns`
/ `templateRows` string still wins if both are passed.
- `alignContent` prop — same shape as Flex.
**Shared**
- New `LayoutAlignContent` type added to `_layout/shared.ts`
(covers start, end, center, stretch, space-between, space-around,
space-evenly, flex-start, flex-end, normal, baseline).
**Group**
- Verified `grow` boolean already implemented in `0ec2a57a` /
`591b0885` — no change needed; the README now correctly reflects it.
**Docs**
- Flex + Grid READMEs updated: comparison tables mark previously
missing features as Sí; Gaps section marks them ✅ implementado.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
ec66999b6c |
docs+style: close last 3 NEEDS-WORK → 77/77 PASS audit
`npm run component:audit` now reports 77 / 77 PASS, 0 NEEDS-WORK, 0 BROKEN. **month-grid + year-grid READMEs** Add Baseline / Comparativa / Decisiones / Gaps sections (F-1.1 / F-1.2 / F-1.3 / F-1.4). Both grids are standalone primitives extracted from DatePicker's `kind: 'month' | 'year'` views (task `#70`); they have no air counterpart. Documented Sema event design (single `commit-set` for value mutation, `nav-step` for paginated header navigation), the 4×3 grid choice, configurable `decadeSize` for YearGrid, and known gaps (multi-year range, quick-jump, localized month names — all deferred). **Audit script `focusMoveActions` extension** `A-3.7` was firing on month-grid/year-grid because the script didn't recognise `next-row`, `prev-row`, `first-month`, `last-month`, `first-year`, `last-year`, `next-page`, `prev-page` as focus moves. They ARE focus moves inside their respective grids (same semantic as `next-day` / `first-day-of-week` inside Calendar). Added to the focusMoveActions set in `scripts/component-audit.ts` so the event-vs-keyboard ratio heuristic no longer over-counts. **time-range-picker** - README Gaps section rewritten as a table with explicit disposition markers (`implementar` / `diferir` / `descartar`) per F-1.4. - Recipe `time-range-picker.css` now styles `[data-readonly]` (lower opacity + pointer-events: none on trigger/clear) and `[data-invalid]` (threat-colour border + focus ring) — closes R-1.3 / R-1.4. Both attrs were declared in morfo but unstyled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
fdb0e68028 |
docs(layout): READMEs for 8 layout primitives (audit PASS)
Closes `E-2.3` audit error for the 8 Layout Batch 1 primitives. Each README follows the canonical structure required by the `component-audit` script: - Baseline — origin (air branch), adaptations applied (drop air- prefix, rename --air-space-N → --space-N, ActiveEidos.resolve) - Superficie — minimal usage snippet - Comparativa — feature parity table vs Radix Themes / Chakra UI / Mantine, with explicit "No — gap conocido" markers for missing features - Decisiones — architectural rationale (Radix item/container split, composition over inheritance, etc.) - Eventos Sema — 0-event justification - Gaps — known feature gaps with disposition markers (implementar / diferir / descartar) and reference attribution - Referencias — links to canonical reference docs - Passive justification — why scope is `eidos` only Notable gaps documented for backlog: - Flex.alignContent (Radix/Chakra) - Grid.columns/rows numeric shorthand (Radix) - Grid.inline boolean (parity with Flex) - Grid.alignContent (Radix/Chakra) - Stack.divider slot (Chakra) - Stack/Flex HStack/VStack helpers (Chakra ergonomics) - Group.grow boolean (Mantine — children fill equally) - Group.preventGrowOverflow (Mantine) - Wrap.shouldWrapChildren (Chakra) - Section.as prop for semantic <section> render `npm run component:audit`: 77 components, 74 PASS, 3 NEEDS-WORK (month-grid, year-grid, time-range-picker — pre-existing, unrelated to layout work). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
60a50f1c27 |
docs: COMPONENT_AUDIT_GUIDE.md — inviolable pre-flight + audit script
Creates the binding pre-flight checklist every agent / contributor must read before touching any UIX component. AGENTS.md gets a top-banner linking the three required reads (this guide, DEMO_AUTHORING_GUIDE, eidos components README). Sections: 0. Inviolable rule — always read this + the demo guide + components README before coding 1. Reference library matrix (radix-themes, radix-primitives, chakra, mantine, mui, react-aria, ark, bits, shadcn, WAI-ARIA APG) 2. 4-layer ownership recap + 2-of-3 rule for morfo extensions 3. Pre-flight audit template (feature parity matrix, architectural choices, reference comparison, decision log, user sign-off line) 4. Project-wide architectural rules — Radix item/container split, composition over visibility props, chip parity, size category cheatsheet, per-event intent, no re-export facades, persistent label registries, floating layer defaults, combobox keyboard, Chakra band-above-control chips 4.11 `npm run component:audit` is the canonical verification tool — walks every component, parses morfo, cross-checks eidos + demo, emits report. A component is "done" only when its scoreboard row reads PASS with 0 errors. 5. Demo template is locked — points at DEMO_AUTHORING_GUIDE 6. Anti-pattern catalogue — every failed approach from recent sessions with WHY it failed (shallow demos, matchAnchorWidth, visibility booleans, inline chips, onpointerdown picks, flex 100% wrap, unregistering labels on unmount, auto-rendering wrappers, refocus without guard, agent git reset, skipped audit) 7. Canonical canaries per domain 8. Audit log — running table with commit hashes + known gaps from Layout Batch 1 9. Pre-port checklist (now includes README writing + the audit script run + npm run check) 10. "When in doubt, ask the user" closer Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
4d45cfcc82 |
docs: COMPONENT_AUDIT_GUIDE.md — inviolable pre-flight for component work
Creates the binding pre-flight checklist every agent / contributor
must read before touching any UIX component (`src/uix/{morfo, soma,
sema, eidos}/components/{name}` or `web/routes/uix/components/{name}/`).
Sections:
0. Inviolable rule — always read this + DEMO_AUTHORING_GUIDE +
components/README before coding
1. Reference library matrix (radix-themes, chakra, mantine, mui,
react-aria, ark, bits, shadcn, WAI-ARIA APG) with what each is for
2. 4-layer ownership recap (morfo / soma / sema / eidos) + the
2-of-3 rule for morfo extensions
3. Pre-flight audit template — feature parity matrix, architectural
choices, reference comparison, decision log, user sign-off line
4. Project-wide architectural rules (Radix item/container split,
composition over visibility props, chip parity, size category
cheatsheet, per-event intent, no re-export facades, persistent
label registries, floating layer defaults, combobox keyboard,
Chakra band-above-control chips)
5. Demo template lock — points at DEMO_AUTHORING_GUIDE
6. Anti-pattern catalogue — every failed approach from recent
sessions with WHY it failed (shallow demos, matchAnchorWidth,
visibility booleans, inline chips, onpointerdown picks, flex
100% wrap, unregistering labels on unmount, auto-rendering
wrappers, refocus without guard, agent git reset, skipped audit)
7. Canonical canaries per domain (drawer, search-field, box, flex,
date-picker, avatar)
8. Audit log — running table of completed audits with commit hashes
+ the known gaps from Layout Batch 1 to address before the next
round (alignContent on Flex/Grid, columns/rows shorthand on Grid,
grow boolean on Group, fluid on Container, HStack/VStack helpers)
9. Pre-port checklist consumers can copy into task plans
10. "When in doubt, ask the user" closer
AGENTS.md updated with a top-banner ⚠ block linking the three
required reads (this guide, the demo guide, the eidos components
README) so any new agent picks them up before touching code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
591b08853a |
feat(layout): canon demos + morfos for the remaining 6 primitives
Replicates the box / flex canonical pattern across grid, stack, group,
wrap, container and section. Each gets:
- `src/uix/morfo/components/{name}.ts` — scope `eidos`, single Provider
part, no events. Header comment justifies the 0-event surface as a
passive container composing through Box / Flex.
- Rewritten demo following DEMO_AUTHORING_GUIDE: 6 tabs (Live · API ·
Morfo · Sema · Recipe · A11y), `Layout · {Name}` eyebrow, 4 meta
pills, permanent stage between header and tablist, trace strip,
MutationObserver wired to data-event, reactive soma + eidos snippets,
API tables grouped by concern, reference comparison vs radix-themes
/ chakra-ui / mantine.
Cross-references in every lede + API tab:
- Grid → Box (item placement) + Flex (flex containers)
- Stack → Flex (full API) + Group (row cluster)
- Group → Stack / Wrap / Flex
- Wrap → Flex (`wrap="wrap"`) + Group (nowrap)
- Container → Box (arbitrary maxWidth) + Section (block padding)
- Section → Box (non-semantic padding) + Container (max-width)
Demo sizes: grid 479L, stack 426L, group 458L, wrap 460L, container
415L, section 386L (vs 80-95L before — matches drawer/avatar canon
mass).
svelte-check: 0 errors, 0 new warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
0dc9c523d1 |
feat(layout): canon Flex demo + morfo (mirrors Box pattern)
Second POC for redoing Layout Batch 1 demos. Mirrors `box`'s canonical shape adapted for a flex container: - 7 container-side controls (direction, align, justify, wrap, gap, padding, inline) — chips for unions, number for space tokens, switch for the boolean. - API tab separates "Container props (Flex-only)" from "Inherited from Box" with a forward-link. - Cross-references: lede + API call out that item-side props (alignSelf, gridColumn, …) live on the child `<Box>`, and that Stack / Group are direction-specific helpers. - Reference comparison row reflects the Radix Themes split (item vs container) and notes Chakra's `HStack`/`VStack` map to UIX `Stack`. Adds `src/uix/morfo/components/flex.ts` (scope `eidos`, 1 part, 0 events, mirrors box.ts justification). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
66c6897c61 |
feat(layout): canon Box demo + relocate grid item placement props to Box
POC for redoing Layout Batch 1 demos to follow DEMO_AUTHORING_GUIDE. Box becomes the reference template for the remaining 7 primitives. Demo follows the canon shape: - Header with eyebrow + 4 meta pills (parts, events, props, scope). - Stage permanently rendered with 3 nested boxes + trace strip showing live display/padding/gap state. - 6 tabs in canonical order (Live · API · Morfo · Sema · Recipe · A11y). - Live tab: 7 controls + reactive soma/eidos snippets. - API tab: 6 subsections grouped by concern (sizing / spacing / display+position / flex/grid item / escape hatch / reference comparison vs radix-themes / chakra / mantine). - Morfo / Sema / Recipe / A11y tabs each populated with the contract appropriate to a passive eidos-native primitive. Architectural fixes (heredados de air, ahora correctos): - `gridColumn`, `gridRow`, `gridArea` move from `<Grid>` to `<Box>` — these are item placement props, they belong on the CHILD that declares its slot, not the container. - Add `placeSelf` to Box (grid item shorthand for align-self + justify-self). - Grid recipe + props updated accordingly; comment clarifies that item placement lives on Box. Add `src/uix/morfo/components/box.ts` (scope `eidos`, 1 part, 0 events with justification comment). Box demo updated lede to point at Flex / Grid for container-side props (Radix Themes split, not Chakra everything-on-Box). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
9ab6746c63 |
chore(uix nav): add Layout group to sidebar for the 8 new primitives
Surfaces Box / Flex / Grid / Stack / Group / Wrap / Container / Section
in the docs sidebar. Pages already existed at `/uix/components/{name}/`
from `9ec2a57a` but weren't navigable from the chrome.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
9ec2a57a85 |
feat(eidos): port 8 layout primitives from air (box, flex, grid, stack, group, wrap, container, section)
Visual-only primitives translated from the legacy `air/components/layout`
on the morfo-runtime branch. Architectural adjustments for the eidos
target: drop the `air-` class prefix, target `[data-{name}]` directly,
rename CSS vars to `--{name}-*`, replace `var(--air-space-N)` with the
eidos foundation `var(--space-N)` token, resolve responsive props via
`ActiveEidos.resolve(prop)`.
- `box` — universal box-model utility (size, padding, margin, gap,
position, overflow, flex/grid item props). Every prop maps to a
CSS custom property; unset props fall through via `revert-layer`.
- `flex` / `grid` / `stack` / `group` / `wrap` — layout containers
that render through Box (single `<div>` per primitive, no
semantic-tag prop in this batch). Markers stack as
`[data-box][data-{name}]`.
- `container` — max-width centered (sm/md/lg/xl/full).
- `section` — semantic-padding block with background tone variants.
Shared helpers at `_layout/shared.ts` (formatLayoutSpace /
formatLayoutLength / formatLayoutRaw / pushStyleVar / composeStyle)
plus an optional `Layout.*` namespace barrel for consumers who prefer
that style; the individual `import { Box }` from each component dir
also works.
Each primitive ships a demo at `/uix/components/{name}/` with a live
example, a props table, and a reference comparison row (radix-themes /
chakra-ui / mantine).
svelte-check: 0 errors / 0 new warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
20709caf9c |
refactor(combobox): SelectedTags as band above the Control (Chakra pattern)
Switched the multi-mode chips from MUI-style "inline inside the input" to the Chakra v3 multi-select pattern — chips render as a separated band ABOVE the Control. Two wins: - The dropdown popover (opens downward) never covers the selected chips, which sit comfortably above the input. - The Control stays a normal-height single-line input. No layout shifts, no absolute-positioned affordances, no `display: contents` gymnastics, no input flex tuning. Reverts the inside-the-Control CSS gymnastics from `30e9517a` / `64bbe9b1` / `87e3d68f` (flex-wrap, padding-right reservation, absolute clear+trigger, input flex basis). `<Combobox.SelectedTags>` now renders as a plain `flex-wrap` row with chip gap and a margin- block-end separating it from the Control. Demo composes `<Combobox.SelectedTags />` BEFORE `<Combobox.Control>` when `mode === 'multiple'`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
91b0efc461 |
fix(combobox): inline chips + input flow as siblings via display:contents
`<Combobox.SelectedTags>` was a nested flex container so the input always wrapped to its own row below the entire chip block — wasting the empty space on the last chip row. Use `display: contents` so each chip becomes a direct flex child of `<Combobox.Control>`. Chips + input now share the same wrap context: the input flows on the same row as the last chip when there's room. `[data-empty]` still toggles to `display: none` so empty-state still collapses correctly (and the `:has(...:not([data-empty]))` selector that pins clear/trigger absolutely keeps working). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
87e3d68f86 |
fix(combobox): pin clear+trigger to first row, not vertical center
With many chips wrapping into 3+ rows, the previous `top: 50%` + translateY centered the clear/trigger pair at the midpoint of the whole stack — visually below the first row of chips. Pin to `top: var(--space-1)` instead so they sit flush with the first chip row no matter how many additional rows of chips wrap below. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
64bbe9b158 |
fix(combobox): pin clear+trigger absolute when chips present (MUI pattern)
When `<Combobox.SelectedTags>` has at least one chip, anchor the ClearButton + Trigger absolutely at the right edge of the Control (MUI Autocomplete pattern). Chips + input can now wrap freely without ever pushing the right-side affordances to a new row. `--_combobox-affordances-w` reserves the right-padding the absolutely-positioned buttons consume, scaled to the control's current height token. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
30e9517a9a |
feat(combobox): SelectedTags inline chips for multi mode
New parts `<Combobox.SelectedTags>`, `<Combobox.SelectedTag>` and `<Combobox.SelectedTagRemove>` render the current selection as removable chips inside `<Combobox.Control>` when `type='multiple'` (Material UI Autocomplete pattern): - Morfo: 3 new public parts with kebab names + texts entries. - Langs: es/en entries `selected-tags` / `selected-tag-remove`. - Soma: `ComboboxSelectedTagsProvider` exposes `items` derived from the provider's label registry; `ComboboxSelectedTagProvider` surfaces `data-value` / `data-label`; `ComboboxSelectedTagRemoveProvider` resolves its parent tag via context and calls `provider.removeValue` on click. Provider gains `getSelectedItems()` and `removeValue()` helpers; `selectedTagsEmpty` runtime prop drives `[data-empty]`. - Soma item registry: stop unregistering labels on Item cleanup so chips keep proper labels after the listbox closes/lazy-unmounts. - Eidos: default snippet loops items and renders a chip + X icon per entry; consumer can override the snippet for custom rendering. - CSS: chip styling, wrap-friendly Control with breathing padding when chips are present, ClearButton pinned right via auto margin, input flex basis reduced to 4rem so it shares a row with chips. - Demo: renders `<Combobox.SelectedTags />` inside Control when `mode === 'multiple'`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
26daa9e22e |
feat(search-field demo): full keyboard nav for results popover
Follows the WAI-ARIA combobox pattern via aria-activedescendant — the input keeps DOM focus, highlight moves virtually: - ArrowDown / ArrowUp: open popover if closed, cycle highlight (wrapping at edges). - Home / End: jump to first / last (when open). - Enter: commit the highlighted item, close popover. - Escape: close popover without clearing value; Soma's clearOnEscape fires only when popover is already closed. - pointermove on a result moves the highlight to it (mouse mirrors keyboard). - highlighted item auto-scrolls into view; index resets when result set shrinks past it or after commit/clear. Wires aria-controls / aria-expanded / aria-activedescendant / aria-autocomplete on `<SearchField.Input>` and ids + data-highlighted on each option. Header meta pill updated to 7 keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
99a1f05cd7 |
fix(search-field demo): close popover after pick without reopening on refocus
`commitPick()` set `value`, closed the popover, then refocused the input — but the `onfocusin` on `<Popover.Anchor>` reopened it. Guard the anchor's open handler with a `justCommitted` flag cleared on the next microtask so the refocus that follows a pick is ignored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
82349f15b4 |
feat(search-field): floating results popover in demo + hide native clear
- Demo `/uix/components/search-field` wraps the SearchField in `<Popover.Anchor>`; results render in `<Popover.Content>` (intrinsic width, align="start") wrapped in `<PickerShell.Body>` so the panel inherits the canonical picker chrome. Items are real `<button>`s with `onclick` — committing a selection sets `value`, closes the popover, and restores focus to the input. - Forward `bind:ref` through the eidos `<SearchField.Input>` wrapper so consumers can refocus the input from the demo's pick handler. - Hide the browser's native `::-webkit-search-cancel-button` / `::-webkit-search-decoration` / `::-ms-clear` so only the canonical `<SearchField.ClearTrigger>` X is visible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
afb09c2254 |
feat(picker-shell): extend to Root + Header + Body + Footer layout
Promote PickerShell from action-row-only to a canonical 4-zone shell (Root → Header / Body / Footer) shared by every composite picker so date/time/color popovers get the same visual contract for free. - Add `<PickerShell.Root>` flex-column wrapper (emits data-picker-shell, data-mode) plus `<PickerShell.Header>` and `<PickerShell.Body>` zones matching the existing Footer's spacing + border tokens. - Auto-wrap all 5 *-content.svelte popovers (date / date-range / time / time-range / color) in `<PickerShell.Root>` so consumers only compose Body + Footer. - Refresh all 5 demos to use `<PickerShell.Body>` around picker content; date-picker demo gains a `showShellHeader` toggle showcasing the new Header zone. - CSS scales section gap + header spacing across xs/sm/md/lg picker sizes via [data-popover-content][data-picker-size] scopes, in sync with the existing Footer scale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
da10548a68 |
revert(popover): drop auto-anchor min-width default; keep content-driven sizing
Yesterday's checkpoint normalized popover min-width to the floating anchor
width across all components, then re-anchored pickers to their input
wrapper so the rule would line popover ≥ input. Tested at /uix/components/
date-picker the result was a popover stretched to 458 px (input width)
while the calendar inside is only ~268 px — leaving an empty band to the
right of every row.
After re-checking the industry research:
- Radix / shadcn / React Aria popovers default to INTRINSIC content width
and expose `--*-trigger-width` as opt-in.
- Ark UI only auto-matches via `positioning.sameWidth` (opt-in).
- bits-ui Select is the only one that bakes width matching by default.
Conclusion: forcing min-width = anchor-width across all popovers is wrong;
each picker's content has its own intrinsic width that should drive the
popover. Reverting the normalization to the previous (Radix-style)
content-driven default. The Select + Combobox `matchAnchorWidth = true`
default is preserved (those are listbox-style popovers where the trigger
width is a sensible reference) and the sideOffset=6 normalization stays.
Files reverted:
- src/uix/eidos/components/popover/popover.css: removed
`--_popover-min-width: var(--floating-anchor-width)` default.
- src/uix/eidos/components/date-picker/date-picker-content.svelte:
restored `--_popover-content-min-width-override: max-content` default
and dropped the `customAnchor={picker.inputRef.current}` wiring.
- src/uix/eidos/components/date-picker/date-picker.css: restored
`min-inline-size: max-content` on the calendar/grid popover rules.
- src/uix/soma/components/date-picker/date-picker-provider.svelte.ts:
removed `inputRef` + `setInputRef`.
- Removed src/uix/{soma,eidos}/components/date-picker/date-picker-input.svelte.
- src/uix/{soma,eidos}/components/date-picker/{exports,index}: revert
`Input` to the DateField re-export.
Kept from yesterday:
- sideOffset = 6 default in popover-content + 5 pickers (genuine
normalization, no width tension).
- Select/Combobox matchAnchorWidth = true (listbox convention).
- floating.svelte.ts `opts.style.current` bug fix (was preventing
any consumer style from reaching the popover wrapper).
- Combobox single-mode auto-sync of inputValue to selected label.
Verified at /uix/components/date-picker: popover renders at 268 px
(calendar content width), aligned end of the 458 px input — matches the
Radix-style behaviour we now consider canonical.
svelte-check --threshold error → 0 errors / 0 warnings / 4584 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
79c94d85d5 |
wip(popover): unified min-width = anchor + sideOffset 6 across pickers
Day-end checkpoint for the floating-layer normalization work. Documented in CONTINUE.md. Done: - popover.css: new `--_popover-min-width` defaults to `var(--floating-anchor-width, auto)` so popover layer is NEVER narrower than its anchor (Radix / React Aria convention). Consumers that should opt out (tooltip, dialog) override with `auto`. - soma popover-content default sideOffset 0 → 6. - 5 picker eidos contents: sideOffset 10 → 6 (matches the new default; per-component override removed visually but kept as explicit prop). - combobox provider: reverted the multi-on-close stamp-labels patch. Multi-mode selections will be rendered as inline tags via a future `Combobox.SelectedTags` part (Material UI Autocomplete pattern, decided with user). Pending (see CONTINUE.md): - Re-anchor pickers to their Input wrapper (currently anchored to the chevron trigger ~14px). Required for the min-width normalization to actually show — research vs Ark UI / Radix / React Aria confirmed composite-input pickers anchor to the control wrapper, not the trigger button. - Combobox `SelectedTags` part for multi-mode inline tags. - SearchField floating Results part (user opted to make it Combobox-like rather than reuse Combobox). svelte-check --threshold error → 0 errors / 0 warnings / 4584 files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
df948aa35a |
fix(combobox): input mirrors selected label in single mode; multi preserves filter
Two related bugs reported in the demo: 1. After selecting an item in single mode, the input stayed at the previously typed query (or empty), so the user couldn't tell which value was committed unless they peeked at the Trigger area. 2. In multi mode, selecting an item used to clear the input, which destroyed any active filter. cmdk-style usage (filter → pick → filter → pick) was impossible — the user had to retype the query for every selection. Provider behaviour now: - `selectItem()` in SINGLE mode sets `inputValue` to the selected label (resolved via the existing `labelRegistry`). - `selectItem()` in MULTI mode leaves `inputValue` untouched, so the filter survives the click and the user can keep picking matches. - A new $effect keeps `inputValue` in sync with `value` in SINGLE mode for initial mount and external `bind:value` updates. - A second $effect clears `inputValue` whenever the picker switches between 'single' and 'multiple' — the two modes use the input for different purposes, and stale text from one would silently filter the other. Also: - Added `components.combobox.clear` to the langs dictionary so the ClearButton stops emitting `[langs] Translation key not found` warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
8aa6086976 |
docs(search-field demo): add live filterable results panel
The demo only had the SearchField input by itself with a trace strip; nothing actually demonstrated the search outcome. Added a 34-item dataset of UIX components (name + group) and a live results grid that filters by name or group substring as the user types. - Empty input renders 34/34 results. - Typing "form" filters to 17 components from the Forms group. - "drawer" filters to 1 (Drawer). - Empty results state renders a graceful "No matches for ..." message. - Counter shows `results N / total` so the demo doubles as a quick feedback loop for the debounceMs control (count + meta animate together when the debounced onValueChange finally fires). CSS lives inline in the demo (`.search-field-results*` selectors) since this is demo content rather than reusable component visuals. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
9fbf8e48b5 |
feat(number-field, search-field, combobox): audit-driven improvements
Three components closing the 6-component audit batch.
number-field
- N1 RTL scrubber: invert horizontal delta sign when dir='rtl' so
dragging right decrements (visually backward) in RTL layouts. Vertical
unaffected. Added 2 RTL scrub tests; existing scrub test pinned to ltr.
- N2 stepper height: Increment/Decrement now use --_number-field-height
for inline-size/block-size/min-inline-size so they always match the
input row at every size token (xs/sm/md/lg/xl).
- N3 input focus ring: explicit [data-number-field-input]:focus-visible
with --_number-field-accent-border box-shadow, alongside the existing
Provider-level ring.
search-field
- S1 icon tokens: added --search-field-icon-size-{xs..xl} + icon-color +
icon-margin-inline-end + a Morfo Icon part + soma/eidos wrappers.
Demo uses <SearchField.Icon><Search /></SearchField.Icon>.
- S2 debounceMs prop: soma wraps onValueChange in a timers.schedule with
the provided delay. clear() cancels, submit() flushes synchronously.
- S3 loading slot: new LoadingIndicator part (morfo + soma + eidos) +
data-loading on Provider + CSS spinner via @keyframes search-field-spin
honoring prefers-reduced-motion.
combobox
- C1 ClearButton part: new morfo trigger + soma provider + eidos wrapper
with X icon. Hidden via [data-empty] (opacity + pointer-events:none).
Click clears value + inputValue and refocuses input.
- C2 allowsCustomValue: when true and no highlighted item, Enter commits
the current inputValue as the value. Single mode closes after commit;
multi appends + clears input + stays open.
- C4 Backspace removes last tag in multi mode when input is empty.
- C5 Sticky group headers: CSS position:sticky with content-overlay bg.
Plus: fixed search-field demo's brittle parts[1] index reference to a
named lookup (parts.find(p => p.kebab === 'input')).
Verified: svelte-check --threshold error → 0 errors / 0 warnings /
4584 files. All component tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
7bc17485dc |
feat(editable): invalid + required + form + Control + better hover/disabled + defaultValue
Audit-driven improvements (7 items) to Editable:
- E1 invalid + required props
- morfo: Provider gains data-required/data-invalid + aria-required/aria-invalid;
Input emits data-invalid + aria-invalid.
- soma: opts include `invalid` and `required`; runtime registration + provider
props derivation emits the data-attrs.
- eidos css: risk-border on Input + Control when [data-invalid].
- E2 APG: added `apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/textbox/'` to
morfo — Editable is essentially a textbox swap.
- E3 form integration: new HiddenInput part (morfo + soma + eidos). The Provider
wrapper auto-renders `<EditableHiddenInput>` when `name` is set, so consumers
don't have to compose it manually. Submits value as native form data.
- E4 Control re-export: Eidos namespace now exposes `Editable.Control`,
matching the soma surface.
- E7 submit hover: replaced `filter: brightness(1.04)` with
`--_editable-accent-solid-hover` per color variant. Consistent with the
other triggers' bg+border transition pattern.
- E9 disabled state: added `cursor: not-allowed` + `color: content-disabled`
on Preview/Input/triggers, alongside the existing opacity. Clearer
feedback than opacity alone.
- E10 defaultValue: new prop seeds `value` once via untrack when `value`
is uncontrolled. Same pattern as the recent select.defaultValue.
Demo at /uix/components/editable: switches for invalid/required/disabled,
a Form integration section with native form submit + FormData panel,
and a defaultValue uncontrolled example.
Verified: svelte-check --threshold error → 0 errors / 0 warnings / 4578
files. Soma tests: 3/3 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
75136e6de5 |
fix(select) + feat(field): match-anchor width, label persistence, OptionalIndicator, auto-label
select bugs
- The popover content didn't match the trigger width even though
matchAnchorWidth was already implemented in the eidos layer. Root
cause was in soma/layers/floating/floating.svelte.ts: `transformedStyle`
read `this.opts.style` directly without `.current`, so consumer styles
never merged into the floating wrapper. Fixed by reading
`this.opts.style.current` and also returning the value when it's an
object (previously fell through to undefined). All popover-based
components (select, combobox, date/time pickers, color picker) now get
their `style` prop applied.
- The trigger value text reverted from the rendered label (e.g. "Apple")
to the raw value string ("apple") whenever the popover closed. Items
unmount when Content's Presence collapses, and they were unregistering
their label on cleanup — leaving the label registry empty between
opens. Drop the unregister on cleanup; keep registrations alive across
popover open/close cycles. Re-mounts overwrite the entry.
field improvements (F1 + F2 + F3)
- F1: add an explicit `[data-field][data-variant='surface']` CSS block
so the surface variant (declared in the type union) has its own rule
instead of silently falling back to the default style.
- F2: new `OptionalIndicator` part (morfo + soma wrapper + eidos
wrapper). Renders only when `required === false`. Default text comes
from `texts['optional-indicator']` ("(optional)"). Styled muted + 0.85em.
- F3: `<Field.Label />` without children now auto-renders the
localised label from `texts.label`. Reduces boilerplate when consumers
rely on the field name for its label.
- Demo at /uix/components/field gained composition examples for the new
parts.
Verified: svelte-check --threshold error → 0 errors / 0 warnings / 4576
files. In-browser: select trigger and popover widths both 336 px;
closed and open values both show labels (e.g. "Apple, Pear, Orange").
Field demo shows 2 required `*`, 1 `(optional)`, and an empty
`<Field.Label />` falling back to the literal "Field".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
ffdb480403 |
feat(select): invalid state + ItemDescription + defaultValue + visual fixes
Audit-driven improvements to Select: - morfo APG URL fixed (combobox → listbox; Select is a collapsible-listbox per ARIA APG, not a text-input combobox). - `invalid` prop added to soma + morfo data-invalid/aria-invalid on Provider and Trigger. Eidos applies risk-border + tinted focus shadow when invalid. - `defaultValue` prop added (string | string[]) for uncontrolled init. Normalized to string[] inside the soma wrapper via untrack so the reference is intentionally non-reactive. - `ItemDescription` part: new morfo part (kebab item-description, archetype description), new soma wrapper + new eidos wrapper. CSS gives muted color and 0.85em font-size; when the parent item is checked, the description inherits accent color. - Value truncation: ensure `min-inline-size: 0` + `max-inline-size: 100%` on the trigger value so long labels ellipsis instead of overflowing. - Highlighted vs checked distinction: the highlighted-only state now uses a lighter hover background, and an item that is both highlighted and checked gets an additional inset focus ring on top of the accent fill. Demo at /uix/components/select adds an `invalid` switch, and three of the six items now ship with a description (Apple / Pear / Carrot). Verified in browser: - triggerInvalid + aria-invalid = true after toggling switch. - Description on non-checked item: rgb(123,123,123) (content-muted). - Description on checked item: accent purple (inherits). - svelte-check --threshold error → 0 errors / 0 warnings / 4574 files. Deferred: SL3 readonly (rare use case), SL4 virtual scrolling (own task). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
826481f1d5 |
fix(picker-shell): footer buttons scale with picker size (xs/sm/md/lg)
The footer buttons (Clear / Cancel / Done) were hardcoded to control-height-sm + font-size-sm + space-3 padding regardless of the host picker's size. At xs the buttons looked oversized next to a compact picker; at lg they looked undersized. - picker-shell.css: refactored button rules to consume four `--_picker-shell-*` CSS vars (control-h, padding-x, font, gap, row-spacing). Defaults match the previous sm sizing. Added explicit xs/md/lg variant blocks keyed off `[data-popover-content][data-picker-size='X']`. - date-picker-content / date-range-picker-content / time-picker-content / time-range-picker-content / color-picker-content: forward the picker's resolvedSize to the popover via a separate `data-picker-size` attr, independent from the popover chrome's clamped `data-size`. This decouples the popover's own chrome (which still clamps xs → sm because the popover layer has no xs variant) from the picker-shell button scaling, which now uses the picker's true size. Verified at /uix/components/date-picker: - xs → 22 / 12px (control-height-xxs, font-size-xs) - sm → 30 / 14px (default) - md → 36 / 16px - lg → 44 / 16px Also fixes the horizontal scroll at xs that surfaced the issue. The date-picker.css `[data-popover-content]:has(...)` rule lets the popover grow to max-content when it hosts a calendar / grid. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
b11ef88bca |
fix(month-grid, year-grid): stretch to popover content width inside picker
When a MonthGrid / YearGrid is rendered inside a DatePicker.Content popover (via DatePicker.MonthView / YearView), the grid was using inline-size:max-content while the picker-shell Footer below it stretched to the popover's full content width — leaving an empty band to the right of each row of cells. Scope the override to `[data-popover-content]` so the standalone grids on their own demos keep their natural max-content sizing. Inside the popover, the grid root and the inner [data-*-grid] both stretch to 100% so the 3 columns (minmax(0, 1fr)) distribute across the same inline size as the footer. Also added box-sizing: border-box to both grid roots so the padding + border don't push them past the popover content area. Verified at /uix/components/date-picker with kind='year' size='md': popover content width = 288px, year-grid width = 262px (= footer width), inner grid = 228px (= grid container minus padding). 3 columns align flush to the footer row above. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
187d7d3751 |
refactor(date-picker): MonthView / YearView delegate to MonthGrid / YearGrid
date-picker-month-view.svelte and date-picker-year-view.svelte previously
rolled their own 3×4 grids with custom data-date-picker-{month,year}-view-*
markers and ~90 lines of selection / nav / formatter logic. They now
render the standalone MonthGrid / YearGrid primitives (introduced in #70)
and bridge a thin DatePickerProvider context adapter:
- value: CalendarDate ↔ {year, month?} / {year}
- placeholder: CalendarDate ↔ {year}
- onValueChange → new CalendarDate(year, month, 1) + handleDateSelect
- onPlaceholderChange → mutate provider.placeholder.year
- forwards locale / disabled / readonly straight from the provider
CSS: removed the data-date-picker-{month,year}-view-* selector block
(~90 lines of dead code). Visuals now come from month-grid.css /
year-grid.css, which means the views also inherit the size='xs' work,
roving tabindex, RTL keyboard flip and sema events for free.
Verified in browser at /uix/components/date-picker:
- kind='month' → MonthGrid renders 12 cells (Jan..Dec) inside the popover
- click Mar → value becomes 2026-03-01, input segments update to 03/2026
- kind='year' → YearGrid renders the 2016–2027 page
- svelte-check --threshold error → 0 errors / 0 warnings / 4572 files
date-range-picker MonthView / YearView left untouched — their range
state machine (start/end/in-range markers, complete-vs-pending swap) is
not modelled by the standalone grids.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
8b885e4448 |
chore(toast): add APG alert pattern URL to morfo
Closes the cosmetic gap surfaced by the cross-component audit (#83). Toast renders via role='alert' / live-region; the matching APG pattern is the "alert" widget. Avatar audited too — its empty morfo (scope: ['eidos'], no texts/events/ apg) is the canonical shape for purely visual primitives; Icon follows the same template. No change needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
fd3a0b00c9 |
feat(calendar, date-picker, date-range-picker): add xs size + fix duplicate footer
xs propagation - Add [data-size='xs'] blocks to calendar.css, date-picker.css, date-range-picker.css (root + inline range calendar). - Extend CalendarSize union with 'xs'; DatePicker / DateRangePicker inherit it through their type re-exports. - date-picker-content + date-range-picker-content clamp xs to sm when forwarding to PopoverContent (PopoverSize starts at sm; the calendar inside still receives xs via data-size). - size chip groups in calendar / date-picker / date-range-picker demos now list xs / sm / md / lg. date-picker demo: fix duplicate footer - The `calendarBody` snippet had a hardcoded <DatePicker.Footer> that rendered alongside the controllable `pickerFooter` snippet, producing two footer rows when showFooter=true. Removed the hardcoded one. Verified in browser: - date-picker size='xs' -> data-date-field-input height 26px (control-height-xs), padding 8px (space-2), font-size 12px. - date-picker popover now shows a single footer. - svelte-check --threshold error -> 0 errors / 0 warnings / 4572 files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |