You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/web/routes/uix/components/scroll-area/+page.svelte

703 lines
25 KiB

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
<script lang="ts">
import {
ScrollArea,
type ScrollAreaProps,
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
type ScrollAreaType,
type ScrollAreaSize,
type ScrollAreaRadius,
type ScrollAreaScrollbars
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
} from '$uix/eidos/components/scroll-area';
import { compileMorfo } from '$uix/morfo';
import { scrollAreaMorfo } from '@/uix/morfo/components/scroll-area';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
let tab = $state<Tab>('live');
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
// ── Live state ───────────────────────────────────────────────────────
const types: ScrollAreaType[] = ['hover', 'scroll', 'auto', 'always'];
let type = $state<ScrollAreaType>('hover');
let scrollHideDelay = $state<number>(600);
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
const sizes: ScrollAreaSize[] = ['xs', 'sm', 'md', 'lg'];
let size = $state<ScrollAreaSize>('md');
const radii: ScrollAreaRadius[] = ['none', 'sm', 'md', 'lg', 'full'];
let radius = $state<ScrollAreaRadius>('full');
type Axis = ScrollAreaScrollbars;
const axes: Axis[] = ['vertical', 'horizontal', 'both'];
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
let axis = $state<Axis>('vertical');
let itemCount = $state<number>(40);
const areaProps = $derived<Partial<ScrollAreaProps>>({
type,
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
scrollHideDelay,
size,
radius
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
});
// ── Compiled morfo ───────────────────────────────────────────────────
const compiled = compileMorfo(scrollAreaMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
$effect(() => {
const el = stageRef;
if (!el) return;
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.attributeName !== 'data-event') continue;
const target = m.target as Element;
const ev = target.getAttribute('data-event');
if (!ev) continue;
trace = [
{
event: ev,
family: target.getAttribute('data-event-family') ?? '—',
intent: target.getAttribute('data-event-intent') ?? undefined,
at: Date.now()
},
...trace
].slice(0, 6);
}
});
obs.observe(el, { attributes: true, subtree: true, attributeFilter: ['data-event'] });
return () => obs.disconnect();
});
const sampleItems = $derived(
Array.from({ length: itemCount }, (_, i) => `Item ${String(i + 1).padStart(2, '0')}`)
);
// ── Snippets ─────────────────────────────────────────────────────────
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
// Soma is headless — no `size` / `radius` / `scrollbars` sugar; the user
// composes Scrollbar / Thumb / Corner parts directly.
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
const somaSnippet = $derived(
[
"<script lang='ts'>",
" import * as ScrollArea from '$soma/components/scroll-area';",
'</' + 'script>',
'',
'<ScrollArea.Provider',
type !== 'hover' && ` type="${type}"`,
scrollHideDelay !== 600 && ` scrollHideDelay={${scrollHideDelay}}`,
'>',
' <ScrollArea.Viewport>',
' <!-- scrollable content -->',
' </ScrollArea.Viewport>',
(axis === 'vertical' || axis === 'both') && ' <ScrollArea.Scrollbar orientation="vertical">',
(axis === 'vertical' || axis === 'both') && ' <ScrollArea.Thumb />',
(axis === 'vertical' || axis === 'both') && ' </ScrollArea.Scrollbar>',
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
(axis === 'horizontal' || axis === 'both') &&
' <ScrollArea.Scrollbar orientation="horizontal">',
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
(axis === 'horizontal' || axis === 'both') && ' <ScrollArea.Thumb />',
(axis === 'horizontal' || axis === 'both') && ' </ScrollArea.Scrollbar>',
axis === 'both' && ' <ScrollArea.Corner />',
'</ScrollArea.Provider>'
]
.filter(Boolean)
.join('\n')
);
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
// Eidos exposes `size` / `radius` / `scrollbars` sugar. With
// `scrollbars` set, the recipe auto-mounts Scrollbar / Thumb /
// Corner parts — children carry only the Viewport.
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
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { ScrollArea } from '$uix/eidos/components/scroll-area';",
'</' + 'script>',
'',
'<ScrollArea',
type !== 'hover' && ` type="${type}"`,
scrollHideDelay !== 600 && ` scrollHideDelay={${scrollHideDelay}}`,
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
size !== 'md' && ` size="${size}"`,
radius !== 'full' && ` radius="${radius}"`,
` scrollbars="${axis}"`,
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
'>',
' <ScrollArea.Viewport>',
' <!-- scrollable content -->',
' </ScrollArea.Viewport>',
'</ScrollArea>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · ScrollArea</div>
<h1 data-uix-page-title>ScrollArea</h1>
<p data-uix-page-lede>
Custom-scrollbar container. Hides the native scrollbar and renders a custom-painted track +
thumb on top of a scrollable viewport. The headless <span data-uix-layer-badge="soma">soma</span>
layer owns the scroll math, the visibility timer (<code>type</code>) and the thumb
position; Eidos paints chrome only. Compound API mirrors Radix Primitives: explicit
<code>Viewport</code> / <code>Scrollbar</code> / <code>Thumb</code> / <code>Corner</code>
composition.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>
<span data-uix-meta-key>parts</span>{compiled.parts.order.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>events</span>{events.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>type</span>{type}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>scope</span>{scrollAreaMorfo.scope.join(', ')}
</span>
</div>
</header>
<!-- Live preview always rendered -->
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<div
style="inline-size: 100%; max-inline-size: 32rem; block-size: 16rem; border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-default); overflow: hidden;"
>
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
<ScrollArea {...areaProps} scrollbars={axis} style="inline-size: 100%; block-size: 100%;">
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
<ScrollArea.Viewport style="inline-size: 100%; block-size: 100%;">
<div
style={`padding: var(--space-4); ${axis === 'horizontal' || axis === 'both' ? 'inline-size: 48rem;' : ''}`}
>
<h3
style="margin: 0 0 var(--space-2) 0; color: var(--color-content-primary); font-weight: 600;"
>
Sample list — {itemCount} items
</h3>
<p
style="margin: 0 0 var(--space-3) 0; color: var(--color-content-secondary); font-size: var(--font-size-sm);"
>
Scroll inside the framed box to observe the custom scrollbar painted by Eidos.
The thumb follows the soma-tracked scroll position; the track hover state is
driven by the morfo's <code>data-hover</code> marker.
</p>
<ul style="margin: 0; padding-inline-start: var(--space-5); color: var(--color-content-primary);">
{#each sampleItems as item}
<li style="padding: var(--space-1) 0;">{item} — body text continues across the line for horizontal scroll demos when the axis is wide enough.</li>
{/each}
</ul>
</div>
</ScrollArea.Viewport>
</ScrollArea>
</div>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
<span>{trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`}</span>
<span style="color: var(--uix-text-faint)">·</span>
<span data-uix-stage-trace-key>type</span>
<span>{type}</span>
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>axis</span> {axis} ·
<span data-uix-stage-trace-key>hideDelay</span> {scrollHideDelay}ms
</span>
</div>
</div>
<div data-uix-tabs role="tablist">
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>
API <span data-uix-tab-count>3</span>
</button>
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
<span data-uix-layer-badge="morfo">morfo</span>
<span data-uix-tab-count>{partsList.length}p · {events.length}e</span>
</button>
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
<span data-uix-layer-badge="sema">sema</span>
<span data-uix-tab-count>{events.length}</span>
</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
{#if tab === 'live'}
<section data-uix-section>
<h2 data-uix-section-title>Controls</h2>
<p data-uix-section-desc>
All knobs are <span data-uix-layer-badge="soma">soma</span> props — the headless layer
owns visibility timing and scroll math. The <span data-uix-layer-badge="eidos">eidos</span>
layer paints chrome with no additional props; tweak the recipe via the
<code>--scroll-area-*</code> custom properties.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> props · headless behavior
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>type</span>
<span data-uix-chips role="radiogroup">
{#each types as opt}
<button data-uix-chip data-active={type === opt} onclick={() => (type = opt)}
>{opt}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label
>scrollHideDelay
<span data-uix-control-hint>ms before hiding (hover / scroll only)</span></span
>
<input type="range" min="100" max="2000" step="100" bind:value={scrollHideDelay} />
<span data-uix-control-value>{scrollHideDelay}ms</span>
</label>
</div>
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
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · recipe sugar
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label
>size <span data-uix-control-hint>scrollbar thickness (Chakra-style)</span></span
>
<span data-uix-chips role="radiogroup">
{#each sizes as opt}
<button data-uix-chip data-active={size === opt} onclick={() => (size = opt)}
>{opt}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label
>radius <span data-uix-control-hint>thumb curvature</span></span
>
<span data-uix-chips role="radiogroup">
{#each radii as opt}
<button data-uix-chip data-active={radius === opt} onclick={() => (radius = opt)}
>{opt}</button
>
{/each}
</span>
</label>
</div>
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
<div data-uix-subsection-head>Composition · which scrollbars to mount</div>
<div data-uix-controls>
<label data-uix-control>
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
<span data-uix-control-label
>scrollbars <span data-uix-control-hint
>axes auto-mounted by the recipe (Radix Themes-style sugar)</span
></span
>
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
<span data-uix-chips role="radiogroup">
{#each axes as opt}
<button data-uix-chip data-active={axis === opt} onclick={() => (axis = opt)}
>{opt}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label
>itemCount <span data-uix-control-hint>list length — drives overflow</span></span
>
<input type="range" min="5" max="120" step="5" bind:value={itemCount} />
<span data-uix-control-value>{itemCount}</span>
</label>
</div>
<!-- ── Code snippets per layer ─────────────────────────────────── -->
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>headless · scroll math + visibility timer</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{somaSnippet}</code></pre>
</div>
<div data-uix-code style="margin-top: var(--uix-space-3);">
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>visual · pure pass-through of soma; recipe paints chrome</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{eidosSnippet}</code></pre>
</div>
</section>
{/if}
{#if tab === 'api'}
<section data-uix-section>
<h2 data-uix-section-title>API reference</h2>
<p data-uix-section-desc>
Pass-through from soma — no evaluative additions on the eidos layer. Visual tuning lives
in the recipe via <code>--scroll-area-*</code> custom properties.
</p>
<div data-uix-subsection-head>ScrollArea (root)</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr>
<td class="name">type</td>
<td class="type">{types.map((t) => `'${t}'`).join(' | ')}</td>
<td class="default">'hover'</td>
<td>Visibility behavior of the scrollbars.</td>
</tr>
<tr>
<td class="name">scrollHideDelay</td>
<td class="type">number</td>
<td class="default">600</td>
<td>ms before hiding (only for 'hover' and 'scroll').</td>
</tr>
<tr>
<td class="name">dir</td>
<td class="type">'ltr' | 'rtl'</td>
<td class="default">from Soma</td>
<td>Text direction. Affects horizontal scrollbar behavior.</td>
</tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>ScrollArea.Scrollbar</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr>
<td class="name">orientation</td>
<td class="type">'vertical' | 'horizontal'</td>
<td class="default empty">—</td>
<td>Required. Drives the recipe's <code>data-orientation</code> selector.</td>
</tr>
<tr>
<td class="name">forceMount</td>
<td class="type">boolean</td>
<td class="default">false</td>
<td>Keep in DOM when hidden. Enables CSS transitions.</td>
</tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>CSS tokens</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Token</th><th>Default</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td class="name">--scroll-area-scrollbar-size</td>
<td class="type">var(--space-2-5)</td>
<td>Inline-axis size of the track (and block-axis for horizontal).</td>
</tr>
<tr>
<td class="name">--scroll-area-track-bg</td>
<td class="type">color-mix(--color-neutral-track)</td>
<td>Track background, also used by the Corner.</td>
</tr>
<tr>
<td class="name">--scroll-area-thumb-bg</td>
<td class="type">var(--color-content-muted)</td>
<td>Thumb fill; hover state lifts to <code>--color-content-secondary</code>.</td>
</tr>
<tr>
<td class="name">--scroll-area-thumb-min-size</td>
<td class="type">var(--space-3)</td>
<td>Floor on the thumb so it stays grabable even on long content.</td>
</tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Reference comparison</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead>
<tr><th>Library</th><th>Closest equivalent</th><th>Difference</th></tr>
</thead>
<tbody>
<tr>
<td class="name">radix-primitives</td>
<td><code>&lt;ScrollArea&gt;</code></td>
<td>Identical compound shape. UIX adds bare token names + soma layer.</td>
</tr>
<tr>
<td class="name">bits-ui</td>
<td><code>&lt;ScrollArea&gt;</code></td>
<td>Same — UIX matches the Svelte-native shape.</td>
</tr>
<tr>
<td class="name">react-aria-components</td>
<td>n/a (uses native scrolling + virtualization)</td>
<td>UIX paints chrome; React Aria leaves it to the OS / browser.</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title>Morfo contract</h2>
<p data-uix-section-desc>
Source: <code>src/uix/morfo/components/scroll-area.ts</code>. Scope is
<code>['soma']</code> — the headless layer is the source of state. Five parts; no
events (see Sema tab).
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Field</th><th>Value</th></tr></thead>
<tbody>
<tr><td class="name">name</td><td>{scrollAreaMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{scrollAreaMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{scrollAreaMorfo.scope.join(', ')}</td></tr>
<tr><td class="name">parts</td><td>{partsList.length}</td></tr>
<tr><td class="name">events</td><td>0</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Parts</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead>
<tr>
<th>kebab</th>
<th>marker</th>
<th>element</th>
<th>role</th>
<th>archetype</th>
<th>states</th>
<th>optional</th>
</tr>
</thead>
<tbody>
{#each partsList as part}
<tr>
<td class="name">{part.kebab}</td>
<td><code data-uix-part-marker>[{part.marker}]</code></td>
<td class="type">&lt;{part.defaultElement}&gt;</td>
<td class="type">{part.role ?? '—'}</td>
<td class="type">{part.archetype ?? '—'}</td>
<td class="default">{part.states.length ? part.states.join(' | ') : '—'}</td>
<td class="default">{part.optional ? 'yes' : 'no'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#each scrollAreaMorfo.parts as rawPart}
{@const partAny = rawPart as unknown as {
kebab: string;
aria?: ReadonlyArray<{
attr: string;
value: { kind: string };
condition?: { when: string; prop?: string; part?: string };
severity?: string;
}>;
}}
{@const dataAttrs = compiled.contracts.dataAttrsByPart.get(partAny.kebab) ?? []}
{@const ariaAttrs = partAny.aria ?? []}
{#if dataAttrs.length || ariaAttrs.length}
<div data-uix-subsection-head>{partAny.kebab}</div>
{#if dataAttrs.length}
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>data-attr</th><th>Values</th><th>Source kind</th></tr></thead>
<tbody>
{#each dataAttrs as attr}
<tr>
<td class="name">{attr.attr}</td>
<td class="type">{attr.values ? attr.values.join(' | ') : '—'}</td>
<td class="default"
>{'value' in attr && attr.value
? (attr as { value: { kind: string } }).value.kind
: '—'}</td
>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{#if ariaAttrs.length}
<div data-uix-table-wrap style="margin-top: var(--uix-space-2);">
<table data-uix-table>
<thead><tr><th>aria-attr</th><th>Source kind</th><th>Severity</th></tr></thead>
<tbody>
{#each ariaAttrs as a}
<tr>
<td class="name">{a.attr}</td>
<td class="type">{a.value.kind}</td>
<td class="default">{a.severity ?? 'required'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{/if}
{/each}
</section>
{/if}
{#if tab === 'sema'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="sema">sema</span> · events
</h2>
<p data-uix-section-desc>
ScrollArea declares no semantic events. Scrolling is continuous and non-evaluative —
there is no commit / emerge / contact verb to project onto a perceptual channel. The
Scrollbar's <code>data-state</code> visibility transition is a CSS concern, not a sema
event. Consumers that need to react to scroll position read <code>data-at-top</code> /
<code>data-at-bottom</code> / <code>data-at-left</code> / <code>data-at-right</code> on
the Viewport — see the morfo declaration above.
</p>
</section>
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>
<p data-uix-section-desc>
Recipe lives in <code>src/uix/eidos/components/scroll-area/scroll-area.css</code>. The
<code>data-orientation</code> attribute drives the geometry of the scrollbar; the
visibility states (<code>data-state='visible'</code> / <code>'hidden'</code>) come from
the soma timer.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Owner</th><th>Purpose</th></tr></thead>
<tbody>
<tr>
<td class="name"><code>[data-scroll-area]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Provider marker. Sets up the relative-positioned shell.</td>
</tr>
<tr>
<td class="name"><code>[data-scroll-area-viewport]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Viewport marker. Receives <code>overflow: auto</code> from soma.</td>
</tr>
<tr>
<td class="name"
><code>[data-scroll-area-scrollbar][data-orientation='vertical']</code></td
>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Vertical track sizing.</td>
</tr>
<tr>
<td class="name"
><code>[data-scroll-area-scrollbar][data-orientation='horizontal']</code></td
>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Horizontal track sizing.</td>
</tr>
<tr>
<td class="name"
><code>[data-scroll-area-scrollbar][data-state='hidden']</code></td
>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Visibility fade for the auto-hide timer.</td>
</tr>
<tr>
<td class="name"><code>[data-scroll-area-thumb]</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Painted thumb; position is set inline by soma.</td>
</tr>
<tr>
<td class="name"><code>[data-scroll-area-corner]</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Square between the two scrollbars when both axes scroll.</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<p data-uix-section-desc>
The custom scrollbar must remain reachable to keyboard and screen-reader users. The morfo
stamps the WAI-ARIA <code>scrollbar</code> role on every scrollbar with
<code>aria-valuenow</code> / <code>aria-valuemin</code> / <code>aria-valuemax</code>
bound to the soma-tracked scroll percentage.
</p>
<div data-uix-subsection-head>ARIA contract</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>Attribute</th><th>Value</th></tr></thead>
<tbody>
<tr><td class="name">scrollbar</td><td>role</td><td class="type">'scrollbar'</td></tr>
<tr>
<td class="name">scrollbar</td><td>aria-controls</td>
<td class="type">id of viewport</td>
</tr>
<tr>
<td class="name">scrollbar</td><td>aria-orientation</td>
<td class="type">'vertical' | 'horizontal'</td>
</tr>
<tr>
<td class="name">scrollbar</td><td>aria-valuenow / min / max</td>
<td class="type">scroll percent (0–100)</td>
</tr>
<tr>
<td class="name">scrollbar</td><td>aria-label</td>
<td class="type">'Vertical scrollbar' / 'Horizontal scrollbar'</td>
</tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Other concerns</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Concern</th><th>Contract</th></tr></thead>
<tbody>
<tr>
<td class="name">Keyboard</td>
<td>
The viewport accepts native scroll keys (Arrow / Page / Home / End) when
focused. Browser keyboard handling is preserved; UIX does not steal arrow keys.
</td>
</tr>
<tr>
<td class="name">Focus visible</td>
<td>
<code>:focus-visible</code> on the scrollbar exposes the canonical UIX focus
ring.
</td>
</tr>
<tr>
<td class="name">Touch</td>
<td>
The Scrollbar declares <code>touch-action: none</code> so touch drags on the
custom thumb don't conflict with browser-native gestures.
</td>
</tr>
<tr>
<td class="name">RTL</td>
<td>
Soma's <code>dir</code> prop is honoured: in RTL, the vertical scrollbar moves
to the inline-start side, matching native scrollbar positioning.
</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.