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,
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
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 ─────────────────────────────────────────────────────────
|
|
|
|
|
|
// 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>',
|
|
|
|
|
|
(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')
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 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}}`,
|
|
|
|
|
|
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;"
|
|
|
|
|
|
>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<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><ScrollArea></code></td>
|
|
|
|
|
|
<td>Identical compound shape. UIX adds bare token names + soma layer.</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td class="name">bits-ui</td>
|
|
|
|
|
|
<td><code><ScrollArea></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"><{part.defaultElement}></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>
|