The combined `/test/soma/forms` page covered three separate components
(Switch, Checkbox, RadioGroup) in one route. Each deserves its own demo
page — matching the convention of every other component having a
dedicated `/test/soma/{kebab}` route, and letting the permutation runner
actually exercise them (the runner navigates per morfo-kebab, not per
merged page, so the combined demo left the 3 morfos SKIPPED).
## New demos
- `/test/soma/switch` — basic + disabled variants. `data-perm-step="0"`
toggles the basic switch. Axes: `dir, disabled`.
- `/test/soma/checkbox` — basic + indeterminate + group variants. Steps
0-2 toggle the first checkbox + first two group items. Seeds
`?perm.checked=true` / `?perm.value=a,b`. Axes: `dir`.
- `/test/soma/radio-group` — active + disabled variants. Steps 0-2 select
each option. Seeds `?perm.value=option-2` / `?perm.orientation`.
Axes: `dir, orientation`.
Each demo reuses the CSS from the original combined page (via `:global`
selectors for `data-switch`, `data-checkbox`, `data-radio-group-item`).
All three have `aria-label` on the Provider — the DEV-mode A4 warning
added in batch 10 of audit 2026-04-21 requires it.
The combined `/test/soma/forms` page is kept in place (pre-existing
integration demo showing composition); future batches may decide
whether to drop it or repurpose it.
## Coverage delta
- Before: 63/66 demos · 131 variants · 202 steps.
- After: 66/66 demos · 139 variants · 228 steps. (smoke: 68 routes)
Coverage complete — every morfo now has at least one instrumented demo
under the permutation runner.
## Verification
- `npm run check`: 0 errors in new demo files.
- `npm run smoke`: 68/68 routes (was 65).
- `npm run morfo:check`: 66/66 morfos.
- `npm run perm:check`: 66/66 demos · 228 steps · 139 variants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
semantuix
parent
db3ad3497f
commit
0d8de2869f
@ -0,0 +1,146 @@
|
||||
<script lang="ts">
|
||||
import * as RadioGroup from '$soma/components/radio-group';
|
||||
import { Perms } from '../_perms.svelte';
|
||||
|
||||
const perms = Perms.getOr({} as Record<string, string | number | boolean>);
|
||||
|
||||
let radioValue = $state((perms.value as string) ?? 'option-1');
|
||||
let orientation = $state<'horizontal' | 'vertical'>(
|
||||
(perms.orientation as 'horizontal' | 'vertical') ?? 'vertical'
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>RadioGroup · Soma</title>
|
||||
<meta name="perm-axes" content="dir,orientation" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="page">
|
||||
<h1>Soma RadioGroup</h1>
|
||||
<p>Single-select group of radio items with roving tabindex and WAI-ARIA support.</p>
|
||||
|
||||
<section>
|
||||
<h2>1. Basic</h2>
|
||||
<p class="state">value: {radioValue}</p>
|
||||
|
||||
<RadioGroup.Provider bind:value={radioValue} {orientation} aria-label="Subscription plan">
|
||||
{#each ['option-1', 'option-2', 'option-3'] as opt, i}
|
||||
<div class="form-row">
|
||||
<RadioGroup.Item
|
||||
value={opt}
|
||||
data-perm-step={i < 3 ? String(i) : undefined}
|
||||
data-perm-label={i === 0
|
||||
? 'select option-1'
|
||||
: i === 1
|
||||
? 'select option-2'
|
||||
: i === 2
|
||||
? 'select option-3'
|
||||
: undefined}
|
||||
>
|
||||
{#snippet children({ checked })}
|
||||
<span class="radio-indicator">{checked ? '●' : '○'}</span>
|
||||
{/snippet}
|
||||
</RadioGroup.Item>
|
||||
<span>{opt}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</RadioGroup.Provider>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. Disabled + loop</h2>
|
||||
|
||||
<RadioGroup.Provider value="b" disabled loop aria-label="Read-only selection">
|
||||
{#each ['a', 'b', 'c'] as opt}
|
||||
<div class="form-row">
|
||||
<RadioGroup.Item value={opt}>
|
||||
{#snippet children({ checked })}
|
||||
<span class="radio-indicator">{checked ? '●' : '○'}</span>
|
||||
{/snippet}
|
||||
</RadioGroup.Item>
|
||||
<span>Item {opt}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</RadioGroup.Provider>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Data attributes</h2>
|
||||
<pre>Root: [data-radio-group] [role=radiogroup] [data-orientation]
|
||||
Item: [data-radio-group-item] [role=radio] [data-state=checked|unchecked] [data-value]</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
max-width: 640px;
|
||||
margin: 2rem auto;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
p {
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
}
|
||||
section {
|
||||
padding: 1.25rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.state {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
}
|
||||
pre {
|
||||
font-size: 0.75rem;
|
||||
background: #f8fafc;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.4rem 0;
|
||||
}
|
||||
:global([data-radio-group-item]) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #cbd5e1;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
font-size: 10px;
|
||||
transition: border-color 150ms;
|
||||
}
|
||||
:global([data-radio-group-item][data-state='checked']) {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
:global([data-radio-group-item]:disabled) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
:global([data-radio-group-item]:focus-visible) {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.radio-indicator {
|
||||
color: #3b82f6;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
import * as Switch from '$soma/components/switch';
|
||||
import { Perms } from '../_perms.svelte';
|
||||
|
||||
const perms = Perms.getOr({} as Record<string, string | number | boolean>);
|
||||
|
||||
let checked = $state((perms.checked as boolean) ?? false);
|
||||
let disabled = $state((perms.disabled as boolean) ?? false);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Switch · Soma</title>
|
||||
<meta name="perm-axes" content="dir,disabled" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="page">
|
||||
<h1>Soma Switch</h1>
|
||||
<p>Binary on/off control with keyboard toggle and `role="switch"`.</p>
|
||||
|
||||
<section>
|
||||
<h2>1. Basic</h2>
|
||||
<p class="state">checked: {checked ? 'on' : 'off'}</p>
|
||||
|
||||
<Switch.Provider
|
||||
bind:checked
|
||||
{disabled}
|
||||
aria-label="Notifications"
|
||||
data-perm-step="0"
|
||||
data-perm-label="toggle switch"
|
||||
>
|
||||
<Switch.Thumb />
|
||||
</Switch.Provider>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. Disabled (forced checked)</h2>
|
||||
|
||||
<Switch.Provider disabled checked={true} aria-label="Permanently on">
|
||||
<Switch.Thumb />
|
||||
</Switch.Provider>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Data attributes</h2>
|
||||
<pre>Root: [data-switch] [role=switch] [data-state=checked|unchecked] [aria-checked]
|
||||
Thumb: [data-switch-thumb] [data-state]</pre>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
max-width: 640px;
|
||||
margin: 2rem auto;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
p {
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
}
|
||||
section {
|
||||
padding: 1.25rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.state {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
}
|
||||
pre {
|
||||
font-size: 0.75rem;
|
||||
background: #f8fafc;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
:global([data-switch]) {
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #cbd5e1;
|
||||
background: #e2e8f0;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: background 150ms;
|
||||
}
|
||||
:global([data-switch][data-state='checked']) {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
:global([data-switch]:disabled) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
:global([data-switch-thumb]) {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
transition: transform 150ms;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
:global([data-switch][data-state='checked'] [data-switch-thumb]) {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in new issue