feat(uix): RadioCards orientation as ResponsiveProp + first responsive-prop example

orientation is now breakpoint-aware like size: `orientation={{ base:'vertical',
md:'horizontal' }}` resolves via Eidos.resolve and forwards a concrete value to
the soma (keyboard nav handles both axes). Documented in the README (new
"Responsive orientation" section + example) and the demo (a "responsive" chip +
note + API row) — the first consumer-facing example of the ResponsiveProp object
syntax in the repo. Browser-verified: flips at md (768px). check 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 651f31aed0
commit 4add3ed58f

@ -54,7 +54,7 @@ plus the card visual knobs:
| `size` | `ResponsiveProp<'xs'…'xl'>` | `'md'` | padding · title/desc font · circle size |
| `color` | `'primary' \| 'secondary' \| 'neutral' \| 'affirm'` | `'primary'` | selected border + tint |
| `columns` | `number` | — | CSS grid with N columns |
| `orientation` | `'vertical' \| 'horizontal'` | `'vertical'` | stack vs wrapping row |
| `orientation` | `ResponsiveProp<'vertical' \| 'horizontal'>` | `'vertical'` | stack vs wrapping row · breakpoint-aware (see Layout) |
Like RadioGroup, the palette excludes risk/threat/loss (per book §3.1 radio is
not evaluative). Use `invalid` for validation — it paints a threat border
@ -67,6 +67,21 @@ without touching the colour.
(each `flex: 1 1 var(--radio-cards-horizontal-basis)`).
- **Grid** — `columns={N}` for an N-column CSS grid.
### Responsive orientation
`orientation` is a `ResponsiveProp` (same mechanism as `size`): pass a plain
value, or an object keyed by breakpoint (`base · sm · md · lg · xl · xxl`,
matching `BREAKPOINTS_DEFAULT` in `$libs/dom`). It's resolved via `Eidos.resolve`
against the active viewport and forwarded to the soma as a concrete value, so it
flips cleanly at the breakpoint (keyboard nav handles both axes).
```svelte
<!-- vertical on mobile, horizontal from the md breakpoint (768px) up -->
<RadioCards orientation={{ base: 'vertical', md: 'horizontal' }}>
…
</RadioCards>
```
## Recipe / cascade
The `radio-cards` recipe (`lib/recipes/base.ts`) owns the card chrome; spacing is

@ -25,6 +25,7 @@
size = 'md',
color = 'primary',
columns,
orientation = 'vertical',
value = $bindable(''),
style,
children,
@ -33,6 +34,9 @@
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
// Breakpoint-aware like size — resolve to a concrete value before handing it
// to the soma (which needs a plain Orientation for layout + keyboard nav).
const resolvedOrientation = $derived(eidos.resolve(orientation, 'vertical'));
// radio-group.css is not loaded on a RadioCards page, so the root owns its
// own flex/grid layout in radio-cards.css, reading this column count.
const columnsStyle = $derived(columns ? `--_radio-cards-columns: ${columns};` : undefined);
@ -42,6 +46,7 @@
<RadioGroup.Provider
{...rest}
bind:value
orientation={resolvedOrientation}
data-radio-cards=""
data-size={resolvedSize}
data-color={color}

@ -1,4 +1,5 @@
import type { Snippet } from 'svelte';
import type { Orientation } from '$soma/types';
import type { AffirmativeColorRole, ResponsiveProp, Size } from '$uix/eidos/lib/types';
import type {
ProviderProps,
@ -28,7 +29,7 @@ export type RadioCardsColor = AffirmativeColorRole;
* shares the same soma provider, selection state, roving focus and form
* integration, adding only the card chrome.
*/
export type RadioCardsProps = ProviderProps & {
export type RadioCardsProps = Omit<ProviderProps, 'orientation'> & {
/** Visual size preset. Resolved via `Eidos.resolve`. @default 'md' */
size?: ResponsiveProp<RadioCardsSize>;
/** Colour palette for the selected card. @default 'primary' */
@ -39,6 +40,16 @@ export type RadioCardsProps = ProviderProps & {
* @default undefined
*/
columns?: number;
/**
* Stack (`vertical`) vs wrapping row (`horizontal`). Breakpoint-aware like
* `size`: pass an object to flip responsively, e.g.
* `orientation={{ base: 'vertical', md: 'horizontal' }}` (vertical on
* mobile, horizontal from the `md` breakpoint up). Resolved via
* `Eidos.resolve` and forwarded as a concrete value to the soma — keyboard
* nav handles both axes, so flipping per breakpoint is safe.
* @default 'vertical'
*/
orientation?: ResponsiveProp<Orientation>;
};
/** Card item props — the reused RadioGroup.Item with card chrome. */

@ -5,6 +5,7 @@
type RadioCardsColor
} from '$uix/eidos/components/radio-cards';
import type { Orientation } from '$soma/types';
import type { ResponsiveProp } from '$uix/eidos/lib/types';
import { compileMorfo } from '$uix/morfo';
import { radioCardsMorfo } from '@/uix/morfo/components/radio-cards';
import { page } from '$app/state';
@ -17,7 +18,7 @@
let value = $state('pro');
let size = $state<RadioCardsSize>('md');
let color = $state<RadioCardsColor>('primary');
let orientation = $state<Orientation>('vertical');
let orientation = $state<ResponsiveProp<Orientation>>('vertical');
let columns = $state<number | undefined>(undefined);
let disabled = $state(params.get('perm.disabled') === 'true');
let invalid = $state(params.get('perm.invalid') === 'true');
@ -74,7 +75,9 @@
' bind:value',
size !== 'md' && ` size="${size}"`,
color !== 'primary' && ` color="${color}"`,
orientation !== 'vertical' && ` orientation="${orientation}"`,
typeof orientation === 'object'
? " orientation={{ base: 'vertical', md: 'horizontal' }}"
: orientation !== 'vertical' && ` orientation="${orientation}"`,
columns !== undefined && ` columns={${columns}}`,
disabled && ' disabled',
invalid && ' invalid',
@ -206,13 +209,18 @@
<span data-uix-switch><input type="checkbox" bind:checked={proItemDisabled} /><span data-uix-switch-label>{proItemDisabled ? 'on' : 'off'}</span></span>
</label>
<label data-uix-control>
<span data-uix-control-label>orientation</span>
<span data-uix-control-label>orientation <span data-uix-control-hint>ResponsiveProp</span></span>
<span data-uix-chips role="radiogroup">
{#each ['vertical', 'horizontal'] as o}
<button data-uix-chip data-active={orientation === o} onclick={() => (orientation = o as Orientation)}>{o}</button>
{/each}
<button data-uix-chip data-active={orientation === 'vertical'} onclick={() => (orientation = 'vertical')}>vertical</button>
<button data-uix-chip data-active={orientation === 'horizontal'} onclick={() => (orientation = 'horizontal')}>horizontal</button>
<button data-uix-chip data-active={typeof orientation === 'object'} onclick={() => (orientation = { base: 'vertical', md: 'horizontal' })}>responsive</button>
</span>
</label>
{#if typeof orientation === 'object'}
<p style="margin: 0.4rem 0 0; font-size: 0.8rem; color: var(--color-content-secondary);">
<code>{'{ base: "vertical", md: "horizontal" }'}</code> — resize the window: vertical below <strong>md (768px)</strong>, horizontal above.
</p>
{/if}
<label data-uix-control>
<span data-uix-control-label>name</span>
<input type="text" bind:value={nameAttr} placeholder="(no HiddenInput)" />
@ -328,7 +336,7 @@
<tbody>
<tr><td class="name">value <span data-uix-tag data-kind="bindable">bindable</span></td><td class="type">string</td><td class="default">''</td><td>Selected value (reused from RadioGroup).</td></tr>
<tr><td class="name">disabled / readonly / required / invalid</td><td class="type">boolean</td><td class="default">false</td><td>Field-integration flags.</td></tr>
<tr><td class="name">orientation</td><td class="type">'vertical'|'horizontal'</td><td class="default">'vertical'</td><td>Stack vs wrapping row.</td></tr>
<tr><td class="name">orientation</td><td class="type">ResponsiveProp&lt;'vertical'|'horizontal'&gt;</td><td class="default">'vertical'</td><td>Stack vs wrapping row. Breakpoint-aware: <code>{'{ base, md }'}</code>.</td></tr>
<tr><td class="name">name</td><td class="type">string</td><td class="default empty">—</td><td>Form field name (auto-renders HiddenInput per card).</td></tr>
<tr><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ResponsiveProp&lt;'xs'…'xl'&gt;</td><td class="default">'md'</td><td>Padding · title/desc font · circle size.</td></tr>
<tr><td class="name">color <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'primary'|'secondary'|'neutral'|'affirm'</td><td class="default">'primary'</td><td>Selected border + tint.</td></tr>

Loading…
Cancel
Save

Powered by TurnKey Linux.