ScrollFrames (eidos) rebuilt as a first-class component -- it was draft WIP using
bare native APIs.
- No native globals: window scroll/resize -> dom.listen + dom.observeResize;
requestAnimationFrame -> dom.raf (disposer); window.innerHeight / devicePixelRatio
-> dom.getWindow(node); new Image() -> dom.getDocument().createElement('img').
- lang="ts" + typed props (types.ts) + index.ts + data-attrs + recipe CSS (no
scoped <style>), registered in index.css.
- New `root` prop: drive progress from a scroll container instead of the window
(makes it embeddable + demoable).
- Morfo (morfo/components/scroll-frames.ts): scope ['eidos'], 4 parts, 0 events.
- Demo (uix/components/scroll-frames): locked 6-tab template, self-contained
generated frames (canvas -> data URL), scrollable stage via `root`. + README.
- Fixes the 12 errors the earlier draft pushed (branch back to the 1 pre-existing
grafito error).
Motion demo (temas/animations): "genie y cia" section -- 4 expressive JS presets
played via eidos.motion.enter: genie (waapi warp), flip-3d (rotateY), swing
(pendulum), elastic (overshoot spring).
Verify: npm run check -> 1 pre-existing error (grafito), 0 new; recipe-css +
component-api contracts clean (3 pre-existing words failures); morfo:check
scroll-frames PASS (1/108 fail = pre-existing words).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
parent
0b7757d027
commit
ba74b6b416
@ -0,0 +1,70 @@
|
||||
# ScrollFrames
|
||||
|
||||
Eidos-only primitive for **scroll-driven media scrubbing**. The frame shown is a
|
||||
pure function of scroll progress (0 → 1) over a tall track with a sticky viewport.
|
||||
|
||||
Two mutually-exclusive modes:
|
||||
|
||||
- **`frames`** — an ordered array of image URLs drawn onto a `<canvas>` (cover
|
||||
fit, HiDPI-aware).
|
||||
- **`video`** — a clip's `currentTime` eased toward the scroll target (`smooth`).
|
||||
|
||||
```svelte
|
||||
<ScrollFrames frames={urls} height="400vh" />
|
||||
<ScrollFrames video="/clip.mp4" smooth={0.1} />
|
||||
<!-- inside a fixed-height scroller instead of the page: -->
|
||||
<div bind:this={box} style="height: 320px; overflow-y: auto;
|
||||
--_scroll-frames-viewport-block-size: 320px;">
|
||||
<ScrollFrames {frames} root={box} height="300%" />
|
||||
</div>
|
||||
```
|
||||
|
||||
## Adapted to the framework
|
||||
|
||||
Every DOM + timing touchpoint goes through **`ActiveDom`** (`eidos.dom`), so the
|
||||
component is iframe / popup / happy-dom safe and its lifecycle is tracked — no bare
|
||||
globals:
|
||||
|
||||
| Native (draft) | Framework |
|
||||
| --- | --- |
|
||||
| `window.addEventListener('scroll'/'resize')` | `dom.listen` + `dom.observeResize` |
|
||||
| `requestAnimationFrame` / `cancelAnimationFrame` | `dom.raf` (disposer) |
|
||||
| `window.innerHeight` / `devicePixelRatio` | `dom.getWindow(node)` |
|
||||
| `new Image()` | `dom.getDocument().createElement('img')` |
|
||||
|
||||
The `root` prop lets the scroll come from a container instead of the window;
|
||||
without it the window drives progress (the default).
|
||||
|
||||
## Morfo
|
||||
|
||||
`scope: ['eidos']`, **0 events**. Four structural parts:
|
||||
|
||||
| part | marker | role |
|
||||
| --- | --- | --- |
|
||||
| `provider` | `data-scroll-frames` | the tall track (scroll travel) |
|
||||
| `viewport` | `data-scroll-frames-viewport` | sticky child that pins the media |
|
||||
| `media` | `data-scroll-frames-media` | `<canvas>` / `<video>` — `aria-hidden` |
|
||||
| `loader` | `data-scroll-frames-loader` | optional load-progress readout |
|
||||
|
||||
## Sema events
|
||||
|
||||
**None — by design.** ScrollFrames is passive: the displayed frame is a function
|
||||
of scroll position, not of a discrete user act, so there is no semantic occurrence
|
||||
to emit. The component carries no `sema` participation (`scope: ['eidos']`). The
|
||||
demo's Sema tab states this explicitly per the demo template.
|
||||
|
||||
## Recipe
|
||||
|
||||
Layout only (`scroll-frames.css`): `[data-scroll-frames]` (relative track),
|
||||
`[data-scroll-frames-viewport]` (sticky, height via
|
||||
`--_scroll-frames-viewport-block-size`, default `100vh`),
|
||||
`[data-scroll-frames-media]` (full-bleed `object-fit: cover`),
|
||||
`[data-scroll-frames-loader]` (`--font-size-xs` / `--color-neutral-text`). Every
|
||||
selector is morfo-backed; no eidos-only, intent or variant selectors.
|
||||
|
||||
## A11y
|
||||
|
||||
- The media is **decorative** (`aria-hidden="true"`) — meaning lives in the
|
||||
surrounding page, not the canvas/video.
|
||||
- Scroll-linked: the user controls pace directly, so it never auto-animates. For a
|
||||
reduced-motion path, render a single representative frame.
|
||||
@ -0,0 +1,8 @@
|
||||
// ScrollFrames — scroll-driven media scrubbing (video scrub / canvas frame
|
||||
// sequence). See `./scroll-frames.svelte` + `./types.ts`.
|
||||
//
|
||||
// import ScrollFrames from '$uix/eidos/components/scroll-frames';
|
||||
// <ScrollFrames frames={urls} height="400vh" />
|
||||
// <ScrollFrames video="/clip.mp4" smooth={0.1} />
|
||||
export { default } from './scroll-frames.svelte';
|
||||
export type { ScrollFramesProps } from './types';
|
||||
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* ScrollFrames recipe — scroll-driven media scrubbing.
|
||||
*
|
||||
* Layout only: a tall track (`[data-scroll-frames]`) whose sticky viewport
|
||||
* (`[data-scroll-frames-viewport]`) pins the media (`[data-scroll-frames-media]`)
|
||||
* full-bleed while the track scrolls. The only token is the loader's text.
|
||||
*/
|
||||
[data-scroll-frames] {
|
||||
position: relative;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
[data-scroll-frames-viewport] {
|
||||
position: sticky;
|
||||
inset-block-start: 0;
|
||||
/* Window scroll → full viewport. Inside a `root` container, override the
|
||||
internal token to the container height (e.g. `100%`). */
|
||||
block-size: var(--_scroll-frames-viewport-block-size, 100vh);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-scroll-frames-media] {
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
object-fit: cover; /* affects <video>; the <canvas> does its own cover draw */
|
||||
display: block;
|
||||
}
|
||||
|
||||
[data-scroll-frames-loader] {
|
||||
position: absolute;
|
||||
inset-block-end: 1.5rem;
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--color-neutral-text);
|
||||
opacity: 0.5;
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
/**
|
||||
* `ScrollFrames` — scroll-driven media scrubbing. Two mutually-exclusive modes
|
||||
* (pick one by which prop you pass):
|
||||
*
|
||||
* - `video` → scrubs a `<video>`'s `currentTime` by scroll progress.
|
||||
* - `frames` → draws an ordered image sequence onto a `<canvas>` by progress.
|
||||
*
|
||||
* A tall track holds a `position: sticky` child that stays pinned while the
|
||||
* track scrolls; progress within the track (0 → 1) drives the animation.
|
||||
*
|
||||
* All DOM + timing goes through `ActiveDom` (scroll/resize listeners, rAF,
|
||||
* window/document resolution, image creation) so the component is iframe /
|
||||
* popup / happy-dom safe — no bare `window` / `requestAnimationFrame` / `new
|
||||
* Image()`.
|
||||
*/
|
||||
export interface ScrollFramesProps extends Omit<HTMLAttributes<HTMLDivElement>, 'class'> {
|
||||
/** Video URL — scrub mode. Mutually exclusive with `frames`. */
|
||||
video?: string | null;
|
||||
/** Ordered frame image URLs — canvas sequence mode. */
|
||||
frames?: readonly string[] | null;
|
||||
/**
|
||||
* Scroll container that drives progress. Default: the window. Pass an
|
||||
* overflow-scrolling element to scrub from ITS scroll instead — this lets the
|
||||
* component live inside a fixed-height region (a demo stage, a panel) rather
|
||||
* than the page. When set, size the sticky viewport with the internal token
|
||||
* `--_scroll-frames-viewport-block-size` (defaults to `100vh`).
|
||||
*/
|
||||
root?: HTMLElement | null;
|
||||
/** Track height; taller = more scroll distance = slower scrub. Default `'300vh'`. */
|
||||
height?: string;
|
||||
/** Video scrub smoothing 0..1 (`0` = exact to scroll, `0.12` ≈ soft inertia). Default `0.12`. */
|
||||
smooth?: number;
|
||||
/** Canvas / video background. Default `'transparent'`. */
|
||||
background?: string;
|
||||
/** Extra classes for the track element. */
|
||||
class?: string;
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import type { Morfo } from '../types';
|
||||
import { v } from '../types';
|
||||
|
||||
/**
|
||||
* ScrollFrames — scroll-driven media scrubbing (eidos-only, passive).
|
||||
*
|
||||
* No events: the component reacts to scroll position, it does not emit semantic
|
||||
* occurrences. Four structural parts:
|
||||
* - `provider` — the tall track that defines the scroll travel.
|
||||
* - `viewport` — the sticky child pinned while the track scrolls.
|
||||
* - `media` — the `<canvas>` / `<video>` (decorative → `aria-hidden`).
|
||||
* - `loader` — optional load-progress readout.
|
||||
*/
|
||||
export const scrollFramesMorfo = {
|
||||
name: 'ScrollFrames',
|
||||
kebab: 'scroll-frames',
|
||||
scope: ['eidos'],
|
||||
parts: [
|
||||
{
|
||||
name: 'Provider',
|
||||
kebab: 'provider',
|
||||
archetype: 'provider',
|
||||
kind: 'public',
|
||||
defaultElement: 'div',
|
||||
optional: false,
|
||||
data: [],
|
||||
aria: []
|
||||
},
|
||||
{
|
||||
name: 'Viewport',
|
||||
kebab: 'viewport',
|
||||
archetype: 'viewport',
|
||||
kind: 'public',
|
||||
defaultElement: 'div',
|
||||
optional: false,
|
||||
data: [],
|
||||
aria: []
|
||||
},
|
||||
{
|
||||
name: 'Media',
|
||||
kebab: 'media',
|
||||
archetype: 'image',
|
||||
kind: 'public',
|
||||
defaultElement: 'img',
|
||||
optional: false,
|
||||
data: [],
|
||||
// The media is purely decorative — the canvas/video carries no
|
||||
// accessible content of its own; the page around it provides meaning.
|
||||
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
|
||||
},
|
||||
{
|
||||
name: 'Loader',
|
||||
kebab: 'loader',
|
||||
archetype: 'indicator',
|
||||
kind: 'public',
|
||||
defaultElement: 'div',
|
||||
optional: true,
|
||||
data: [],
|
||||
aria: []
|
||||
}
|
||||
]
|
||||
} as const satisfies Morfo;
|
||||
@ -0,0 +1,369 @@
|
||||
<script lang="ts">
|
||||
import ScrollFrames from '$uix/eidos/components/scroll-frames';
|
||||
import type { ScrollFramesProps } from '$uix/eidos/components/scroll-frames';
|
||||
import { compileMorfo } from '$uix/morfo';
|
||||
import { scrollFramesMorfo } from '@/uix/morfo/components/scroll-frames';
|
||||
import { getActiveUix } from '$active-uix';
|
||||
|
||||
const uix = getActiveUix();
|
||||
|
||||
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 ────────────────────────────────────────────────────────
|
||||
type Mode = 'frames' | 'video';
|
||||
let mode = $state<Mode>('frames');
|
||||
let frameCount = $state(40);
|
||||
let trackHeight = $state(300); // % of the scroll container
|
||||
let smooth = $state(0.12);
|
||||
let videoUrl = $state('');
|
||||
|
||||
// The scroll container that drives progress (the component's `root`). Demoed
|
||||
// inside a fixed-height region so it scrubs without scrolling the whole page.
|
||||
let scrollRoot = $state<HTMLElement | null>(null);
|
||||
const STAGE_H = 320;
|
||||
|
||||
// ── Self-contained frames: draw a progress dial + index per frame to a
|
||||
// canvas and snapshot it. No external assets — the sequence IS the demo.
|
||||
function generateFrames(n: number): string[] {
|
||||
const doc = uix.dom.getDocument();
|
||||
const out: string[] = [];
|
||||
const W = 480;
|
||||
const H = 270;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const c = doc.createElement('canvas');
|
||||
c.width = W;
|
||||
c.height = H;
|
||||
const ctx = c.getContext('2d');
|
||||
if (!ctx) continue;
|
||||
const t = n > 1 ? i / (n - 1) : 0;
|
||||
const hue = Math.round(t * 300);
|
||||
ctx.fillStyle = `hsl(${hue} 55% 13%)`;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
ctx.strokeStyle = `hsl(${hue} 80% 62%)`;
|
||||
ctx.lineWidth = 16;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.arc(W / 2, H / 2, 74, -Math.PI / 2, -Math.PI / 2 + Math.max(0.0001, t) * Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = '600 44px system-ui, sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(String(i + 1), W / 2, H / 2);
|
||||
out.push(c.toDataURL());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
let generatedFrames = $state<string[]>([]);
|
||||
$effect(() => {
|
||||
generatedFrames = generateFrames(frameCount);
|
||||
});
|
||||
|
||||
// ── Compiled morfo ────────────────────────────────────────────────────
|
||||
const compiled = compileMorfo(scrollFramesMorfo);
|
||||
const partsList = $derived([...compiled.parts.byKebab.values()]);
|
||||
const events = $derived([...compiled.actions.byName.values()]);
|
||||
|
||||
// Trace strip (0 events — kept per the demo template; stays empty).
|
||||
$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();
|
||||
});
|
||||
|
||||
// ── Snippets ──────────────────────────────────────────────────────────
|
||||
const somaSnippet = [
|
||||
'<!-- ScrollFrames is an eidos-only primitive — no soma layer. -->',
|
||||
"<!-- The morfo declares scope:['eidos'] + 0 events (purely scroll-driven). -->"
|
||||
].join('\n');
|
||||
|
||||
const eidosSnippet = $derived(
|
||||
[
|
||||
"<script lang='ts'>",
|
||||
" import ScrollFrames from '$uix/eidos/components/scroll-frames';",
|
||||
mode === 'frames' ? ' const frames = [/* ordered image URLs */];' : '',
|
||||
'</' + 'script>',
|
||||
'',
|
||||
'<ScrollFrames',
|
||||
mode === 'frames' ? ' {frames}' : ` video="${videoUrl || '/clip.mp4'}"`,
|
||||
trackHeight !== 300 && ` height="${trackHeight}vh"`,
|
||||
mode === 'video' && smooth !== 0.12 && ` smooth={${smooth}}`,
|
||||
'/>'
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
);
|
||||
</script>
|
||||
|
||||
<div data-uix-canvas-inner>
|
||||
<header>
|
||||
<div data-uix-eyebrow>Media · ScrollFrames</div>
|
||||
<h1 data-uix-page-title>ScrollFrames</h1>
|
||||
<p data-uix-page-lede>
|
||||
Eidos-only primitive for scroll-driven media scrubbing. Two modes —
|
||||
<code>frames</code> (an ordered image sequence drawn onto a <code>canvas</code>) and
|
||||
<code>video</code> (a clip's <code>currentTime</code> eased by scroll). A tall track with a
|
||||
sticky viewport pins the media while progress (0 → 1) drives the frame. All DOM + timing go
|
||||
through <code>ActiveDom</code> — no bare <code>window</code> / <code>requestAnimationFrame</code
|
||||
>. Four morfo parts, zero events.
|
||||
</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>0
|
||||
</span>
|
||||
<span data-uix-meta-pill>
|
||||
<span data-uix-meta-key>modes</span>2
|
||||
</span>
|
||||
<span data-uix-meta-pill>
|
||||
<span data-uix-meta-key>scope</span>eidos
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Live preview always rendered. The component is driven by `root` (this
|
||||
fixed-height scroller) so it scrubs without scrolling the whole page. -->
|
||||
<div data-uix-stage>
|
||||
<div data-uix-stage-area bind:this={stageRef}>
|
||||
<div
|
||||
bind:this={scrollRoot}
|
||||
style="inline-size: 100%; block-size: {STAGE_H}px; overflow-y: auto; border-radius: var(--uix-radius-3, 12px); --_scroll-frames-viewport-block-size: {STAGE_H}px;"
|
||||
>
|
||||
{#if scrollRoot}
|
||||
{#if mode === 'frames'}
|
||||
<ScrollFrames
|
||||
frames={generatedFrames}
|
||||
root={scrollRoot}
|
||||
height="{trackHeight}%"
|
||||
background="#0b0b10"
|
||||
/>
|
||||
{:else}
|
||||
<ScrollFrames
|
||||
video={videoUrl || null}
|
||||
root={scrollRoot}
|
||||
{smooth}
|
||||
height="{trackHeight}%"
|
||||
background="#0b0b10"
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div data-uix-stage-trace>
|
||||
<span data-uix-stage-trace-key>trace</span>
|
||||
<span>{trace.length === 0 ? 'no semantic events (scroll-driven)' : `${trace.length} event(s)`}</span>
|
||||
<span style="margin-inline-start: auto;">
|
||||
<span data-uix-stage-trace-key>mode</span>
|
||||
{mode}{mode === 'frames' ? ` · ${frameCount} frames` : ''}
|
||||
</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>6</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 · 0e</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>
|
||||
ScrollFrames is eidos-only — there is no <span data-uix-layer-badge="soma">soma</span> layer.
|
||||
Scroll the preview above to scrub; the dial + index are auto-generated frames (no external
|
||||
assets). All knobs are visual / behavioral treatment.
|
||||
</p>
|
||||
|
||||
<div data-uix-subsection-head>
|
||||
<span data-uix-layer-badge="eidos">eidos</span> props · visual treatment
|
||||
</div>
|
||||
<div data-uix-controls>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>mode</span>
|
||||
<span data-uix-chips role="radiogroup">
|
||||
{#each ['frames', 'video'] as m}
|
||||
<button data-uix-chip data-active={mode === m} onclick={() => (mode = m as Mode)}
|
||||
>{m}</button
|
||||
>
|
||||
{/each}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{#if mode === 'frames'}
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>frames <span data-uix-control-hint>{frameCount}</span></span>
|
||||
<input type="range" min="6" max="80" step="1" bind:value={frameCount} />
|
||||
</label>
|
||||
{:else}
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label
|
||||
>video URL <span data-uix-control-hint>paste a muted clip</span></span
|
||||
>
|
||||
<input type="text" bind:value={videoUrl} placeholder="/clip.mp4" />
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>smooth <span data-uix-control-hint>{smooth}</span></span>
|
||||
<input type="range" min="0" max="0.4" step="0.02" bind:value={smooth} />
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label
|
||||
>height <span data-uix-control-hint>{trackHeight}% · taller = slower</span></span
|
||||
>
|
||||
<input type="range" min="150" max="600" step="10" bind:value={trackHeight} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div data-uix-code>
|
||||
<div data-uix-code-head>
|
||||
<span data-uix-layer-badge="soma">soma</span>
|
||||
<span>headless · none — eidos-only primitive</span>
|
||||
<span data-uix-code-lang>svelte</span>
|
||||
</div>
|
||||
<pre><code>{somaSnippet}</code></pre>
|
||||
</div>
|
||||
<div data-uix-code>
|
||||
<div data-uix-code-head>
|
||||
<span data-uix-layer-badge="eidos">eidos</span>
|
||||
<span>visual · scroll-driven media</span>
|
||||
<span data-uix-code-lang>svelte</span>
|
||||
</div>
|
||||
<pre><code>{eidosSnippet}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
{:else if tab === 'api'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>Props</h2>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>prop</th><th>type</th><th>default</th><th>notes</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="name">video</td><td class="type">{`string | null`}</td><td>null</td><td>Clip URL — scrub mode (mutually exclusive with frames).</td></tr>
|
||||
<tr><td class="name">frames</td><td class="type">{`readonly string[] | null`}</td><td>null</td><td>Ordered image URLs — canvas sequence mode.</td></tr>
|
||||
<tr><td class="name">root</td><td class="type">{`HTMLElement | null`}</td><td>null</td><td>Scroll container. Default: the window.</td></tr>
|
||||
<tr><td class="name">height</td><td class="type">string</td><td>'300vh'</td><td>Track height; taller = more travel = slower scrub.</td></tr>
|
||||
<tr><td class="name">smooth</td><td class="type">number</td><td>0.12</td><td>Video scrub easing 0..1 (0 = exact, 0.12 ≈ inertia).</td></tr>
|
||||
<tr><td class="name">background</td><td class="type">string</td><td>'transparent'</td><td>Canvas / video background.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{:else if tab === 'morfo'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>Declarative contract</h2>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>field</th><th>value</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="name">name</td><td class="type">"{scrollFramesMorfo.name}"</td></tr>
|
||||
<tr><td class="name">kebab</td><td class="type">"{scrollFramesMorfo.kebab}"</td></tr>
|
||||
<tr><td class="name">scope</td><td class="type">[{scrollFramesMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td></tr>
|
||||
<tr><td class="name">parts</td><td class="default">{scrollFramesMorfo.parts.length}</td></tr>
|
||||
<tr><td class="name">events</td><td class="default">0</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 data-uix-section-title>Parts</h2>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>kebab</th><th>marker</th><th>element</th><th>archetype</th><th>optional</th></tr></thead>
|
||||
<tbody>
|
||||
{#each scrollFramesMorfo.parts as part}
|
||||
<tr>
|
||||
<td class="name">{part.kebab}</td>
|
||||
<td class="type">data-{part.kebab === 'provider' ? 'scroll-frames' : `scroll-frames-${part.kebab}`}</td>
|
||||
<td>{part.defaultElement}</td>
|
||||
<td>{part.archetype}</td>
|
||||
<td>{part.optional ? 'yes' : 'no'}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 data-uix-section-title>media · ARIA</h2>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>attr</th><th>value</th><th>note</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="name">aria-hidden</td><td class="type">"true"</td><td>Media is decorative — meaning lives in the surrounding page.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 data-uix-section-title>Events</h2>
|
||||
<p data-uix-section-desc>None. ScrollFrames is passive — it reacts to scroll position and emits no semantic occurrences.</p>
|
||||
</section>
|
||||
{:else 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>
|
||||
ScrollFrames declares <strong>0 events</strong>. It is a passive, scroll-driven visual: the
|
||||
frame shown is a pure function of scroll progress, not of a discrete user act, so there is
|
||||
no semantic occurrence to emit. Justified in the component README — the component carries no
|
||||
<span data-uix-layer-badge="sema">sema</span> participation (scope is <code>['eidos']</code>).
|
||||
</p>
|
||||
</section>
|
||||
{:else if tab === 'recipe'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>Recipe selectors</h2>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>selector</th><th>source</th><th>role</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="type">[data-scroll-frames]</td><td>morfo</td><td>track — relative, full width</td></tr>
|
||||
<tr><td class="type">[data-scroll-frames-viewport]</td><td>morfo</td><td>sticky, pins the media; height via <code>--_scroll-frames-viewport-block-size</code></td></tr>
|
||||
<tr><td class="type">[data-scroll-frames-media]</td><td>morfo</td><td>full-bleed <code>object-fit: cover</code></td></tr>
|
||||
<tr><td class="type">[data-scroll-frames-loader]</td><td>morfo</td><td>load-progress readout</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p data-uix-section-desc>
|
||||
Every selector is morfo-backed (a part's <code>data-*</code> marker). No eidos-only or
|
||||
intent/variant selectors — the component is structural.
|
||||
</p>
|
||||
</section>
|
||||
{:else if tab === 'a11y'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>Accessibility</h2>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>concern</th><th>treatment</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="name">decorative media</td><td>The canvas / video carries <code>aria-hidden="true"</code> — it conveys no content of its own; the surrounding page provides meaning.</td></tr>
|
||||
<tr><td class="name">reduced motion</td><td>Scroll-linked; the user controls pace directly, so it does not auto-animate. For a reduced-motion path, render a single representative frame.</td></tr>
|
||||
<tr><td class="name">keyboard</td><td>Driven by the scroll container — standard scroll keys (Space, PageDown, arrows) advance it.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
Loading…
Reference in new issue