|
|
<script lang="ts">
|
|
|
import ImageAdjustments, {
|
|
|
type ImageAdjustmentKey,
|
|
|
type ImageAdjustmentValues,
|
|
|
type ImageAdjustmentsSize
|
|
|
} from '$uix/eidos/components/image-adjustments';
|
|
|
import { compileMorfo } from '$uix/morfo';
|
|
|
import { imageAdjustmentsMorfo } from '@/uix/morfo/components/image-adjustments';
|
|
|
import { getActiveUix } from '$active-uix';
|
|
|
|
|
|
const uix = getActiveUix();
|
|
|
|
|
|
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
|
|
|
let tab = $state<Tab>('live');
|
|
|
|
|
|
// ── Live state ────────────────────────────────────────────────────────
|
|
|
type Preset = 'core' | 'photo' | 'artistic' | 'all';
|
|
|
let preset = $state<Preset>('core');
|
|
|
let size = $state<ImageAdjustmentsSize>('md');
|
|
|
let showReset = $state(true);
|
|
|
let disabled = $state(false);
|
|
|
let value = $state<ImageAdjustmentValues>({});
|
|
|
let filter = $state('none');
|
|
|
|
|
|
const PRESETS: Record<Preset, ImageAdjustmentKey[]> = {
|
|
|
core: ['brightness', 'contrast', 'saturation', 'temperature', 'hue', 'blur'],
|
|
|
photo: ['brightness', 'contrast', 'saturation', 'temperature'],
|
|
|
artistic: ['grayscale', 'sepia', 'hue', 'blur'],
|
|
|
all: ['brightness', 'contrast', 'saturation', 'temperature', 'hue', 'blur', 'grayscale', 'sepia']
|
|
|
};
|
|
|
const adjustments = $derived(PRESETS[preset]);
|
|
|
|
|
|
// A synthetic "photo" (inline SVG) — works offline and exercises every
|
|
|
// filter (gradient sky, sun, layered hills, trees).
|
|
|
const photo =
|
|
|
'data:image/svg+xml;utf8,' +
|
|
|
encodeURIComponent(
|
|
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 320">
|
|
|
<defs>
|
|
|
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#4ea8ff"/><stop offset="1" stop-color="#bfe3ff"/></linearGradient>
|
|
|
<radialGradient id="sun" cx="0.5" cy="0.5" r="0.5"><stop offset="0" stop-color="#fff6c0"/><stop offset="1" stop-color="#ffd24a"/></radialGradient>
|
|
|
</defs>
|
|
|
<rect width="480" height="320" fill="url(#sky)"/>
|
|
|
<circle cx="370" cy="80" r="46" fill="url(#sun)"/>
|
|
|
<path d="M0 230 Q120 170 240 220 T480 200 V320 H0 Z" fill="#2f9e57"/>
|
|
|
<path d="M0 270 Q140 220 300 260 T480 250 V320 H0 Z" fill="#1f7a42"/>
|
|
|
<g fill="#5a3a1b"><rect x="90" y="208" width="10" height="40"/><rect x="360" y="226" width="9" height="34"/></g>
|
|
|
<g fill="#176b39"><circle cx="95" cy="200" r="26"/><circle cx="364" cy="220" r="20"/></g>
|
|
|
</svg>`
|
|
|
);
|
|
|
|
|
|
type TraceEntry = { event: string; family: string; intent?: string; at: number };
|
|
|
let trace = $state<TraceEntry[]>([]);
|
|
|
|
|
|
let stageRef = $state<HTMLElement | null>(null);
|
|
|
$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();
|
|
|
});
|
|
|
|
|
|
// ── Compiled morfo ────────────────────────────────────────────────────
|
|
|
const compiled = compileMorfo(imageAdjustmentsMorfo);
|
|
|
const partsList = $derived([...compiled.parts.byKebab.values()]);
|
|
|
const events = $derived([...compiled.actions.byName.values()]);
|
|
|
|
|
|
function fmtTime(at: number): string {
|
|
|
const d = new Date(at);
|
|
|
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
|
|
}
|
|
|
|
|
|
// Soma snippet — headless, ARIA + behavior only. Eidos renders the rows in a
|
|
|
// loop; this shows the composition shape for ONE row.
|
|
|
const somaSnippet = $derived(
|
|
|
[
|
|
|
"<script lang='ts'>",
|
|
|
" import * as ImageAdjustments from '$soma/components/image-adjustments';",
|
|
|
" import { Slider } from '$uix/eidos/components/slider';",
|
|
|
' let value = $state({});',
|
|
|
'</' + 'script>',
|
|
|
'',
|
|
|
'<ImageAdjustments.Provider',
|
|
|
' bind:value',
|
|
|
' onFilterChange={(f) => apply(f)}',
|
|
|
preset !== 'core' && ` adjustments={[${PRESETS[preset].map((k) => `'${k}'`).join(', ')}]}`,
|
|
|
disabled && ' disabled',
|
|
|
'>',
|
|
|
' <ImageAdjustments.Item adjustment="brightness">',
|
|
|
' {#snippet children({ label, value: v, formattedValue, def, setValue })}',
|
|
|
' <ImageAdjustments.ItemLabel>{label}</ImageAdjustments.ItemLabel>',
|
|
|
' <Slider value={[v]} min={def.min} max={def.max} step={def.step}',
|
|
|
' aria-label={label} onValueChange={(n) => setValue(n[0])}>',
|
|
|
' <Slider.Range /><Slider.Thumb />',
|
|
|
' </Slider>',
|
|
|
' <ImageAdjustments.ItemValue>{formattedValue}</ImageAdjustments.ItemValue>',
|
|
|
' {/snippet}',
|
|
|
' </ImageAdjustments.Item>',
|
|
|
' <!-- …one Item per adjustment… -->',
|
|
|
showReset && ' <ImageAdjustments.Reset />',
|
|
|
'</ImageAdjustments.Provider>'
|
|
|
]
|
|
|
.filter(Boolean)
|
|
|
.join('\n')
|
|
|
);
|
|
|
|
|
|
// Eidos snippet — configured component: renders the labelled Slider rows
|
|
|
// internally from `adjustments`. The consumer applies the streamed filter.
|
|
|
const eidosSnippet = $derived(
|
|
|
[
|
|
|
"<script lang='ts'>",
|
|
|
" import ImageAdjustments from '$uix/eidos/components/image-adjustments';",
|
|
|
' let value = $state({});',
|
|
|
" let filter = $state('none');",
|
|
|
'</' + 'script>',
|
|
|
'',
|
|
|
'<ImageAdjustments',
|
|
|
' bind:value',
|
|
|
' onFilterChange={(f) => (filter = f)}',
|
|
|
preset !== 'core' && ` adjustments={[${PRESETS[preset].map((k) => `'${k}'`).join(', ')}]}`,
|
|
|
size !== 'md' && ` size="${size}"`,
|
|
|
!showReset && ' showReset={false}',
|
|
|
disabled && ' disabled',
|
|
|
'/>',
|
|
|
'',
|
|
|
'<img src={src} style="filter: {filter}" />'
|
|
|
]
|
|
|
.filter(Boolean)
|
|
|
.join('\n')
|
|
|
);
|
|
|
</script>
|
|
|
|
|
|
<div data-uix-canvas-inner>
|
|
|
<header>
|
|
|
<div data-uix-eyebrow>Media · Image adjustments</div>
|
|
|
<h1 data-uix-page-title>ImageAdjustments</h1>
|
|
|
<p data-uix-page-lede>
|
|
|
A reusable panel of image-filter sliders. Soma owns the values and the value→CSS-<code
|
|
|
>filter</code
|
|
|
> math; it composes the framework's <code><Slider></code> per row and streams a live
|
|
|
<code>filter</code> string via <code>onFilterChange</code> — apply it to any surface (an
|
|
|
ImagePicker preview, an avatar, a Words image, a future cropper). Five parts, one event
|
|
|
(<code>commit-reset</code>).
|
|
|
</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>adjustments</span>8
|
|
|
</span>
|
|
|
<span data-uix-meta-pill>
|
|
|
<span data-uix-meta-key>sizes</span>3
|
|
|
</span>
|
|
|
<span data-uix-meta-pill>
|
|
|
<span data-uix-meta-key>output</span>CSS filter
|
|
|
</span>
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
<!-- Live preview always rendered (between header and tablist) -->
|
|
|
<div data-uix-stage>
|
|
|
<div data-uix-stage-area bind:this={stageRef}>
|
|
|
<div class="ia-stage-grid">
|
|
|
<div data-uix-surface class="ia-panel">
|
|
|
<ImageAdjustments
|
|
|
bind:value
|
|
|
{adjustments}
|
|
|
{size}
|
|
|
{showReset}
|
|
|
{disabled}
|
|
|
onFilterChange={(f) => (filter = f)}
|
|
|
aria-label="Photo adjustments"
|
|
|
/>
|
|
|
</div>
|
|
|
<div class="ia-preview">
|
|
|
<img
|
|
|
class="ia-photo"
|
|
|
src={photo}
|
|
|
alt="Adjustable sample landscape"
|
|
|
style="filter: {filter === 'none' ? 'none' : filter};"
|
|
|
/>
|
|
|
<button data-uix-chip onclick={() => (value = {})}>Clear values</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div data-uix-stage-trace>
|
|
|
<span data-uix-stage-trace-key>trace</span>
|
|
|
{#if trace.length === 0}
|
|
|
<span>drag a slider, then press Reset to see the commit-reset event</span>
|
|
|
{:else}
|
|
|
{#each trace.slice(0, 3) as entry}
|
|
|
<span
|
|
|
><span data-uix-stage-trace-event>{entry.event}</span> · {entry.family}{entry.intent
|
|
|
? ' · ' + entry.intent
|
|
|
: ''}</span
|
|
|
>
|
|
|
<span style="color: var(--uix-text-faint)">{fmtTime(entry.at)}</span>
|
|
|
{/each}
|
|
|
{/if}
|
|
|
<span style="margin-inline-start: auto;">
|
|
|
<span data-uix-stage-trace-key>filter</span>
|
|
|
{filter}
|
|
|
</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Tabs -->
|
|
|
<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>8</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>
|
|
|
Props grouped by the architectural layer that owns them: <span
|
|
|
data-uix-layer-badge="soma">soma</span
|
|
|
>
|
|
|
headless behavior, <span data-uix-layer-badge="eidos">eidos</span> visual treatment. Drag
|
|
|
any slider — the preview updates live. The one Sema event lives in the dedicated
|
|
|
<a href="#sema" onclick={(e) => { e.preventDefault(); tab = 'sema'; }}>Sema</a> tab.
|
|
|
</p>
|
|
|
|
|
|
<!-- ── Soma props ──────────────────────────────────────────────── -->
|
|
|
<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>adjustments <span data-uix-control-hint>subset + order</span></span>
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
{#each ['core', 'photo', 'artistic', 'all'] as const as p}
|
|
|
<button data-uix-chip data-active={preset === p} onclick={() => (preset = p)}>{p}</button>
|
|
|
{/each}
|
|
|
</span>
|
|
|
</label>
|
|
|
<div data-uix-control>
|
|
|
<span data-uix-control-label>value <span data-uix-control-hint>bindable · driven by the sliders</span></span>
|
|
|
<code style="font-size: var(--font-size-xs); color: var(--uix-text-muted);">{Object.keys(value).length ? JSON.stringify(value) : '{}'}</code>
|
|
|
</div>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>disabled</span>
|
|
|
<span data-uix-switch>
|
|
|
<input type="checkbox" bind:checked={disabled} />
|
|
|
<span data-uix-switch-label>{disabled ? 'on' : 'off'}</span>
|
|
|
</span>
|
|
|
</label>
|
|
|
</div>
|
|
|
|
|
|
<!-- ── Eidos props ─────────────────────────────────────────────── -->
|
|
|
<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>size <span data-uix-control-hint>slider + row scale</span></span>
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
{#each ['sm', 'md', 'lg'] as const as s}
|
|
|
<button data-uix-chip data-active={size === s} onclick={() => (size = s)}>{s}</button>
|
|
|
{/each}
|
|
|
</span>
|
|
|
</label>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>showReset <span data-uix-control-hint>composition</span></span>
|
|
|
<span data-uix-switch>
|
|
|
<input type="checkbox" bind:checked={showReset} />
|
|
|
<span data-uix-switch-label>{showReset ? 'on' : 'off'}</span>
|
|
|
</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 · composes Slider per Item</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 · configured rows, adds size + showReset</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>All public props. Eidos additions are tagged.</p>
|
|
|
|
|
|
<div data-uix-subsection-head>Provider</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">value <span data-uix-tag data-kind="bindable">bindable</span></td><td class="type">{`ImageAdjustmentValues`}</td><td class="default">{`{}`}</td><td>Per-adjustment values. Missing keys = neutral default.</td></tr>
|
|
|
<tr><td class="name">onValueChange</td><td class="type">{`(v: ImageAdjustmentValues) => void`}</td><td class="default empty">—</td><td>Full values record on any change.</td></tr>
|
|
|
<tr><td class="name">onFilterChange</td><td class="type">{`(filter: string) => void`}</td><td class="default empty">—</td><td>The recomputed CSS filter string. The component's real output.</td></tr>
|
|
|
<tr><td class="name">adjustments</td><td class="type">ImageAdjustmentKey[]</td><td class="default">core 6</td><td>Which adjustments + order. Subset of the 8 keys.</td></tr>
|
|
|
<tr><td class="name">disabled</td><td class="type">boolean</td><td class="default">false</td><td>Disable all sliders + reset.</td></tr>
|
|
|
<tr><td class="name">aria-label</td><td class="type">string</td><td class="default">"Image adjustments"</td><td>Names the role=group.</td></tr>
|
|
|
<tr><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">{`ResponsiveProp<'sm' | 'md' | 'lg'>`}</td><td class="default">'md'</td><td>Slider + row typography scale.</td></tr>
|
|
|
<tr><td class="name">showReset <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">boolean</td><td class="default">true</td><td>Render the reset control below the rows.</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-subsection-head>Adjustment catalog</div>
|
|
|
<div data-uix-table-wrap>
|
|
|
<table data-uix-table>
|
|
|
<thead><tr><th>Key</th><th>Range</th><th>CSS mapping</th></tr></thead>
|
|
|
<tbody>
|
|
|
<tr><td class="name">brightness</td><td class="default">−100…100</td><td class="type">{`brightness((100+v)/100)`}</td></tr>
|
|
|
<tr><td class="name">contrast</td><td class="default">−100…100</td><td class="type">{`contrast((100+v)/100)`}</td></tr>
|
|
|
<tr><td class="name">saturation</td><td class="default">−100…100</td><td class="type">{`saturate((100+v)/100)`}</td></tr>
|
|
|
<tr><td class="name">temperature</td><td class="default">−100…100</td><td class="type">approx: warm→sepia, cool→hue-rotate+saturate</td></tr>
|
|
|
<tr><td class="name">hue</td><td class="default">−180…180°</td><td class="type">{`hue-rotate(v deg)`}</td></tr>
|
|
|
<tr><td class="name">blur</td><td class="default">0…20px</td><td class="type">{`blur(v px)`}</td></tr>
|
|
|
<tr><td class="name">grayscale</td><td class="default">0…100%</td><td class="type">{`grayscale(v%)`}</td></tr>
|
|
|
<tr><td class="name">sepia</td><td class="default">0…100%</td><td class="type">{`sepia(v%)`}</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>Capability</th><th>Untitled</th><th>Chakra</th><th>Radix</th><th>MUI</th><th>UIX</th></tr></thead>
|
|
|
<tbody>
|
|
|
<tr><td class="name">Filter-slider panel primitive</td><td>✅</td><td>❌</td><td>❌</td><td>❌</td><td>✅</td></tr>
|
|
|
<tr><td class="name">Live CSS filter output</td><td>⚠️ canvas</td><td>—</td><td>—</td><td>—</td><td>✅ string</td></tr>
|
|
|
<tr><td class="name">Composes the lib's own Slider</td><td>✅</td><td>n/a</td><td>n/a</td><td>n/a</td><td>✅</td></tr>
|
|
|
<tr><td class="name">Reusable outside an image picker</td><td>❌</td><td>—</td><td>—</td><td>—</td><td>✅</td></tr>
|
|
|
<tr><td class="name">Sound/haptic on reset</td><td>❌</td><td>❌</td><td>❌</td><td>❌</td><td>✅</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
{/if}
|
|
|
|
|
|
{#if tab === 'morfo'}
|
|
|
<section data-uix-section>
|
|
|
<h2 data-uix-section-title>
|
|
|
<span data-uix-layer-badge="morfo">morfo</span>
|
|
|
· declarative contract
|
|
|
</h2>
|
|
|
<p data-uix-section-desc>
|
|
|
The morfo is the single source of truth: parts + the one event. Soma transcribes it to
|
|
|
runtime behavior; eidos targets the data-attrs it emits; sema dispatches the event
|
|
|
signature. Source: <code>src/uix/morfo/components/image-adjustments.ts</code>.
|
|
|
</p>
|
|
|
|
|
|
<!-- ── Component header ───────────────────────────────────────── -->
|
|
|
<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 class="type">"{imageAdjustmentsMorfo.name}"</td></tr>
|
|
|
<tr><td class="name">kebab</td><td class="type">"{imageAdjustmentsMorfo.kebab}"</td></tr>
|
|
|
<tr><td class="name">scope</td><td class="type">[{imageAdjustmentsMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td></tr>
|
|
|
<tr><td class="name">parts.length</td><td class="default">{imageAdjustmentsMorfo.parts.length}</td></tr>
|
|
|
<tr><td class="name">events.length</td><td class="default">{imageAdjustmentsMorfo.events?.length ?? 0}</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
<!-- ── Parts overview ────────────────────────────────────────── -->
|
|
|
<div data-uix-subsection-head>Parts</div>
|
|
|
<div data-uix-table-wrap>
|
|
|
<table data-uix-table>
|
|
|
<thead><tr><th>Part</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="default">{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>
|
|
|
|
|
|
<!-- ── Per-part contracts (data + aria) ──────────────────────── -->
|
|
|
{#each imageAdjustmentsMorfo.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>Condition</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.condition ? `when ${a.condition.when}${a.condition.prop ? ` (${a.condition.prop})` : ''}` : 'always'}</td>
|
|
|
<td class="default">{a.severity ?? 'required'}</td>
|
|
|
</tr>
|
|
|
{/each}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
{/if}
|
|
|
{/if}
|
|
|
{/each}
|
|
|
|
|
|
<!-- ── Events declaration ────────────────────────────────────── -->
|
|
|
<div data-uix-subsection-head>Events declaration</div>
|
|
|
<div data-uix-table-wrap>
|
|
|
<table data-uix-table>
|
|
|
<thead><tr><th>name</th><th>family</th><th>verb</th><th>sequence</th><th>intent</th><th>target</th></tr></thead>
|
|
|
<tbody>
|
|
|
{#each events as action}
|
|
|
{@const sem = action.semantic}
|
|
|
{@const intentDecl = 'intent' in sem ? sem.intent : undefined}
|
|
|
<tr>
|
|
|
<td class="name">{action.name}</td>
|
|
|
<td class="type">{sem.family}</td>
|
|
|
<td>{sem.verb ?? '—'}</td>
|
|
|
<td>{sem.sequence ?? 'pre'}</td>
|
|
|
<td class="default">{typeof intentDecl === 'string' ? intentDecl : '—'}</td>
|
|
|
<td>{action.target}</td>
|
|
|
</tr>
|
|
|
{/each}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
{/if}
|
|
|
|
|
|
{#if tab === 'sema'}
|
|
|
<section data-uix-section id="sema">
|
|
|
<h2 data-uix-section-title>
|
|
|
<span data-uix-layer-badge="sema">sema</span>
|
|
|
· events + perceptual signature
|
|
|
</h2>
|
|
|
<p data-uix-section-desc>
|
|
|
One event: <code>commit-reset</code> (family <code>commit</code>, verb <code>reset</code>,
|
|
|
neutral, <code>sequence: post</code>) — the deliberate wipe back to neutral, riding the
|
|
|
soft form-commit tuning + a tap. Per-slider drag feedback is owned by the composed
|
|
|
<code><Slider></code> pack, not here. Click <strong>play</strong> to fire the signal
|
|
|
on the live group.
|
|
|
</p>
|
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
<table data-uix-table>
|
|
|
<thead><tr><th>Name</th><th>Family</th><th>Verb</th><th>Sequence</th><th>Intent</th><th>Play</th></tr></thead>
|
|
|
<tbody>
|
|
|
{#each events as action}
|
|
|
{@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined}
|
|
|
{@const effectiveIntent = typeof intentDecl === 'string' ? intentDecl : undefined}
|
|
|
<tr>
|
|
|
<td class="name">{action.name}</td>
|
|
|
<td class="type">{action.semantic.family}</td>
|
|
|
<td>{action.semantic.verb ?? '—'}</td>
|
|
|
<td>{action.semantic.sequence ?? 'pre'}</td>
|
|
|
<td class="default">{effectiveIntent ?? '—'}</td>
|
|
|
<td>
|
|
|
<button
|
|
|
data-uix-play
|
|
|
onclick={() => {
|
|
|
const target = (stageRef?.querySelector('[data-image-adjustments]') ?? stageRef) as HTMLElement | null;
|
|
|
if (!target) return;
|
|
|
void uix.events?.emit({
|
|
|
name: action.name,
|
|
|
family: action.semantic.family,
|
|
|
target,
|
|
|
...(effectiveIntent ? { intent: effectiveIntent } : {})
|
|
|
});
|
|
|
}}
|
|
|
>▶ play</button>
|
|
|
</td>
|
|
|
</tr>
|
|
|
{/each}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
{/if}
|
|
|
|
|
|
{#if tab === 'recipe'}
|
|
|
<section data-uix-section>
|
|
|
<h2 data-uix-section-title>Eidos recipe</h2>
|
|
|
<p data-uix-section-desc>
|
|
|
Selectors at <code>src/uix/eidos/components/image-adjustments/image-adjustments.css</code>.
|
|
|
Tokens bare-prefixed (<code>--image-adjustments-*</code>). The only slider override is the
|
|
|
<code>hue</code> row's rainbow track, keyed off the morfo's <code>data-adjustment</code>.
|
|
|
</p>
|
|
|
<div data-uix-table-wrap>
|
|
|
<table data-uix-table>
|
|
|
<thead><tr><th>Selector</th><th>Source</th></tr></thead>
|
|
|
<tbody>
|
|
|
<tr><td class="name">[data-image-adjustments]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
|
|
|
<tr><td class="name">[data-image-adjustments][data-disabled]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
|
|
|
<tr><td class="name">[data-image-adjustments-item]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
|
|
|
<tr><td class="name">[data-image-adjustments-item-label]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
|
|
|
<tr><td class="name">[data-image-adjustments-item-value]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
|
|
|
<tr><td class="name">[data-image-adjustments-reset]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
|
|
|
<tr><td class="name">[data-adjustment='hue'] [data-slider]</td><td><span data-uix-tag data-kind="eidos">eidos</span></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 group is a labelled <code>role="group"</code>; each row is an APG-pattern slider via
|
|
|
the composed <code><Slider></code> (full keyboard + <code>aria-valuenow</code>). The
|
|
|
value read-out is a plain <code><span></code> (not a live region) — the slider thumb
|
|
|
already announces, so a live region would double-announce.
|
|
|
</p>
|
|
|
|
|
|
<div data-uix-subsection-head>Keyboard (per slider row)</div>
|
|
|
<div data-uix-table-wrap>
|
|
|
<table data-uix-table>
|
|
|
<thead><tr><th>Key</th><th>Action</th></tr></thead>
|
|
|
<tbody>
|
|
|
<tr><td><span data-uix-kbd>Tab</span></td><td>Move between sliders and the reset control.</td></tr>
|
|
|
<tr><td><span data-uix-kbd>←</span> / <span data-uix-kbd>→</span></td><td>Decrease / increase by one step.</td></tr>
|
|
|
<tr><td><span data-uix-kbd>↑</span> / <span data-uix-kbd>↓</span></td><td>Increase / decrease by one step.</td></tr>
|
|
|
<tr><td><span data-uix-kbd>PageUp</span> / <span data-uix-kbd>PageDown</span></td><td>Larger step (×10).</td></tr>
|
|
|
<tr><td><span data-uix-kbd>Home</span> / <span data-uix-kbd>End</span></td><td>Jump to min / max.</td></tr>
|
|
|
<tr><td><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span></td><td>Activate the reset control.</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
<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">provider</td><td>role</td><td class="type">"group"</td></tr>
|
|
|
<tr><td class="name">provider</td><td>aria-label</td><td class="type">"Image adjustments" (or prop)</td></tr>
|
|
|
<tr><td class="name">provider</td><td>data-disabled</td><td class="type">when disabled</td></tr>
|
|
|
<tr><td class="name">thumb (slider)</td><td>role</td><td class="type">"slider"</td></tr>
|
|
|
<tr><td class="name">thumb (slider)</td><td>aria-valuemin / max / now</td><td class="type">the adjustment range + value</td></tr>
|
|
|
<tr><td class="name">thumb (slider)</td><td>aria-label</td><td class="type">the adjustment label</td></tr>
|
|
|
<tr><td class="name">reset</td><td>aria-label</td><td class="type">"Reset"</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
{/if}
|
|
|
</div>
|
|
|
|
|
|
<style>
|
|
|
.ia-stage-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: minmax(220px, 320px) 1fr;
|
|
|
gap: var(--uix-space-3);
|
|
|
align-items: start;
|
|
|
inline-size: 100%;
|
|
|
}
|
|
|
@media (max-width: 720px) {
|
|
|
.ia-stage-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
}
|
|
|
.ia-panel {
|
|
|
padding: var(--uix-space-3);
|
|
|
border-radius: var(--radius-lg);
|
|
|
}
|
|
|
.ia-preview {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--uix-space-2);
|
|
|
align-items: flex-start;
|
|
|
}
|
|
|
.ia-photo {
|
|
|
inline-size: 100%;
|
|
|
block-size: auto;
|
|
|
aspect-ratio: 3 / 2;
|
|
|
object-fit: cover;
|
|
|
border-radius: var(--radius-lg);
|
|
|
border: 1px solid var(--color-border-default);
|
|
|
background: var(--color-surface-muted);
|
|
|
}
|
|
|
</style>
|