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

650 lines
28 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>&lt;Slider&gt;</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">&lt;{part.defaultElement}&gt;</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>&lt;Slider&gt;</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>&lt;Slider&gt;</code> (full keyboard + <code>aria-valuenow</code>). The
value read-out is a plain <code>&lt;span&gt;</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>

Powered by TurnKey Linux.