diff --git a/src/uix/eidos/components/radio-cards/README.md b/src/uix/eidos/components/radio-cards/README.md index 52f68af50..530893ac7 100644 --- a/src/uix/eidos/components/radio-cards/README.md +++ b/src/uix/eidos/components/radio-cards/README.md @@ -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 + + + … + +``` + ## Recipe / cascade The `radio-cards` recipe (`lib/recipes/base.ts`) owns the card chrome; spacing is diff --git a/src/uix/eidos/components/radio-cards/radio-cards.svelte b/src/uix/eidos/components/radio-cards/radio-cards.svelte index 566e8905c..24401081b 100644 --- a/src/uix/eidos/components/radio-cards/radio-cards.svelte +++ b/src/uix/eidos/components/radio-cards/radio-cards.svelte @@ -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 @@ & { /** Visual size preset. Resolved via `Eidos.resolve`. @default 'md' */ size?: ResponsiveProp; /** 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; }; /** Card item props — the reused RadioGroup.Item with card chrome. */ diff --git a/web/routes/uix/components/radio-cards/+page.svelte b/web/routes/uix/components/radio-cards/+page.svelte index 26464f5ad..100a86985 100644 --- a/web/routes/uix/components/radio-cards/+page.svelte +++ b/web/routes/uix/components/radio-cards/+page.svelte @@ -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('md'); let color = $state('primary'); - let orientation = $state('vertical'); + let orientation = $state>('vertical'); let columns = $state(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 @@ {proItemDisabled ? 'on' : 'off'} + {#if typeof orientation === 'object'} +

+ {'{ base: "vertical", md: "horizontal" }'} — resize the window: vertical below md (768px), horizontal above. +

+ {/if}