Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions configurator/src/components/editors/ColorStudio.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,11 @@
'--sf-focus-ring-width',
'--sf-focus-ring-offset',
].map((name) => tokenByName.get(name)).filter(Boolean);
const workflow = ['Source pairs', 'Role map', 'Shade ramp', 'Usage check'];
const nav = ['Main colors', 'Semantic colors', 'Gradients', 'Shade curve', 'Contrast', 'Assignments'];
</script>

<StudioFrame title="Color Studio" description="Set brand sources first, then verify semantic roles, status colors, shade ramp, contrast, and real usage." tone="color">
<StudioFrame title="Color Studio" description="Set brand sources first, then verify semantic roles, status colors, shade ramp, contrast, and real usage." tone="color" {nav}>
<div class="color-studio">
<nav class="workflow" aria-label="Color workflow">
{#each workflow as step, index (step)}<span><b>{index + 1}</b>{step}</span>{/each}
</nav>

<section class="usage" aria-label="Usage preview">
<button>Primary button</button>
Expand Down Expand Up @@ -68,9 +65,6 @@

<style>
.color-studio { display: grid; gap: 12px; }
.workflow { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.workflow span { display: flex; align-items: center; gap: 8px; padding: 9px 10px; border: 1px solid var(--cfg-border); border-radius: var(--cfg-radius-s); background: var(--cfg-bg-2); color: var(--cfg-text-muted); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; }
.workflow b { display: grid; place-items: center; inline-size: 20px; block-size: 20px; border-radius: 999px; background: var(--cfg-accent-strong); color: white; font-size: 10px; }
.usage { display: grid; grid-template-columns: auto 1fr auto; gap: 12px; align-items: center; padding: 14px; border-radius: 14px; background: var(--sf-color-raised); border: 1px solid var(--sf-color-border); }
.usage button { border: 0; border-radius: 999px; background: var(--sf-color-primary); color: var(--sf-color-primary-text, white); padding: 10px 14px; }
.usage p { margin: 2px 0 0; color: var(--sf-color-text--muted); }
Expand Down Expand Up @@ -100,6 +94,6 @@
details { border: 1px solid var(--cfg-border); border-radius: var(--cfg-radius); background: var(--cfg-surface); overflow: clip; }
summary { padding: 12px 14px; cursor: pointer; font-weight: 800; text-transform: uppercase; font-size: 12px; letter-spacing: .06em; background: var(--cfg-surface-2); }
.rows { display: grid; }
@media (max-width: 800px) { .workflow, .theme-pair, .role-map__grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 640px) { .usage, .workflow, .theme-pair, .role-map__grid { grid-template-columns: 1fr; } .swatches { grid-template-columns: repeat(5, 1fr); } }
@media (max-width: 800px) { .theme-pair, .role-map__grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 640px) { .usage, .theme-pair, .role-map__grid { grid-template-columns: 1fr; } .swatches { grid-template-columns: repeat(5, 1fr); } }
</style>
6 changes: 2 additions & 4 deletions configurator/src/components/editors/EffectsStudio.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
import { STUDIO_GROUPS, resolveStudioGroups } from '../../lib/studioSchema.js';
import StudioFrame from './StudioFrame.svelte';
import StudioControls from './StudioControls.svelte';
import StudioWorkflow from './StudioWorkflow.svelte';

const groups = resolveStudioGroups(STUDIO_GROUPS.effects);
const workflow = ['Glass', 'Opacity', 'Scrim', 'Masks'];
const nav = ['Glass', 'Opacity', 'Scrim', 'Masks'];
const states = ['normal', 'muted', 'disabled', 'pending'];
</script>

<StudioFrame title="Effects Studio" description="Blur, opacity, scrim and mask fades shown as ready UI patterns: media cards, disabled states and scroll fade overlays.">
<StudioFrame title="Effects Studio" description="Blur, opacity, scrim and mask fades shown as ready UI patterns: media cards, disabled states and scroll fade overlays." {nav}>
<div class="effects-studio">
<StudioWorkflow steps={workflow} ariaLabel="Effects workflow" />

<section class="glass-lab" aria-label="Glass and blur preview">
<div class="glass-card"><small>Glass & blur</small><h4>Readable media card</h4><p>Scrim, blur and muted states preview.</p><button>Primary action</button></div>
Expand Down
16 changes: 3 additions & 13 deletions configurator/src/components/editors/LayoutStudio.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,12 @@
import ContainerBars from '../ContainerBars.svelte';

const groups = resolveStudioGroups(STUDIO_GROUPS.layout);
const workflow = ['Containers', 'Grid', 'Measure', 'Anchors'];
const nav = ['Containers', 'Grid', 'Measure', 'Anchors'];
const devices = ['Mobile', 'Tablet', 'Desktop'];
</script>

<StudioFrame title="Layout Studio" description="Container widths, reading measure, grid and global anchors shown as a live layout system — from viewport down to sections, sidebar and sticky offsets.">
<StudioFrame title="Layout Studio" description="Container widths, reading measure, grid and global anchors shown as a live layout system — from viewport down to sections, sidebar and sticky offsets." {nav}>
<div class="layout-studio">
<nav class="workflow" aria-label="Layout workflow">
<ol>
{#each workflow as step, index (step)}<li><b>{index + 1}</b>{step}</li>{/each}
</ol>
</nav>

<section class="viewport-lab" aria-label="Viewport and container preview">
<div class="viewport-lab__header">
Expand Down Expand Up @@ -58,10 +53,6 @@

<style>
.layout-studio { display: grid; gap: 12px; }
.workflow { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.workflow ol { display: contents; list-style: none; margin: 0; padding: 0; }
.workflow li { display: flex; align-items: center; gap: 8px; padding: 9px 10px; border: 1px solid var(--cfg-border); border-radius: var(--cfg-radius-s); background: var(--cfg-bg-2); color: var(--cfg-text-muted); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; }
.workflow b { display: grid; place-items: center; inline-size: 20px; block-size: 20px; border-radius: 999px; background: var(--cfg-accent-strong); color: white; font-size: 10px; }
.viewport-lab { display: grid; gap: 12px; padding: 16px; border: 1px dashed var(--cfg-border-strong); border-radius: 16px; background: var(--cfg-bg-2); }
.viewport-lab__header { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; align-items: baseline; }
.viewport-lab__header strong { font-size: 13px; text-transform: uppercase; letter-spacing: .06em; }
Expand All @@ -87,6 +78,5 @@
.legend { display: flex; gap: 10px; flex-wrap: wrap; }
.legend b { font-size: 11px; color: var(--cfg-text-muted); text-transform: uppercase; }
@media (max-width: 840px) { .composition, .measure-lab { grid-template-columns: 1fr; } }
@media (max-width: 640px) { .workflow, .device-strip { grid-template-columns: 1fr 1fr; } }
@media (max-width: 480px) { .workflow { grid-template-columns: repeat(4, minmax(110px, 1fr)); overflow-x: auto; padding-bottom: 4px; } }
@media (max-width: 640px) { .device-strip { grid-template-columns: 1fr 1fr; } }
</style>
6 changes: 2 additions & 4 deletions configurator/src/components/editors/MotionStudio.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@
import { STUDIO_GROUPS, resolveStudioGroups } from '../../lib/studioSchema.js';
import StudioFrame from './StudioFrame.svelte';
import StudioControls from './StudioControls.svelte';
import StudioWorkflow from './StudioWorkflow.svelte';

const groups = resolveStudioGroups(STUDIO_GROUPS.motion);
const workflow = ['Duration', 'Easing', 'Presets', 'Reduced'];
const nav = ['Duration', 'Easing', 'Presets', 'Reduced'];
const durations = ['instant', 'fast', 'normal', 'slow'];
const easings = ['linear', 'in', 'out', 'spring'];
</script>

<StudioFrame title="Motion Studio" description="Durations, easing and animation presets shown as live interactions: see timing, curves and UI states without guessing from token names.">
<StudioFrame title="Motion Studio" description="Durations, easing and animation presets shown as live interactions: see timing, curves and UI states without guessing from token names." {nav}>
<div class="motion-studio">
<StudioWorkflow steps={workflow} ariaLabel="Motion workflow" />

<section class="duration-lab" aria-label="Duration scale preview">
<div class="lab-copy"><strong>Speed scale</strong><span>Motion scale controls the feel of every duration without editing each preset by hand.</span></div>
Expand Down
6 changes: 2 additions & 4 deletions configurator/src/components/editors/ShadowStudio.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
import { STUDIO_GROUPS, resolveStudioGroups } from '../../lib/studioSchema.js';
import StudioFrame from './StudioFrame.svelte';
import StudioControls from './StudioControls.svelte';
import StudioWorkflow from './StudioWorkflow.svelte';

const levels = ['xs', 's', 'm', 'l', 'xl', '2xl'];
const workflow = ['Elevation', 'Surfaces', 'Overlays', 'Media'];
const nav = ['Elevation', 'Surfaces', 'Overlays', 'Media'];
const groups = resolveStudioGroups(STUDIO_GROUPS.shadows);
</script>

<StudioFrame title="Shadow Studio" description="Elevation stack shows strength, colour and dark-mode character across cards, popovers, dialogs and media.">
<StudioFrame title="Shadow Studio" description="Elevation stack shows strength, colour and dark-mode character across cards, popovers, dialogs and media." {nav}>
<div class="shadow-studio">
<StudioWorkflow steps={workflow} ariaLabel="Shadow workflow" />

<section class="elevation-lab" aria-label="Elevation scale preview">
<div class="lab-copy"><strong>Elevation stack</strong><span>Compare every shadow level on the same surface and tune global strength/lightness once.</span></div>
Expand Down
6 changes: 2 additions & 4 deletions configurator/src/components/editors/ShapeStudio.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@
import { STUDIO_GROUPS, resolveStudioGroups } from '../../lib/studioSchema.js';
import StudioFrame from './StudioFrame.svelte';
import StudioControls from './StudioControls.svelte';
import StudioWorkflow from './StudioWorkflow.svelte';

const groups = resolveStudioGroups(STUDIO_GROUPS.borders);
const workflow = ['Radius', 'Borders', 'Dividers', 'Focus'];
const nav = ['Radius', 'Borders', 'Dividers', 'Focus'];
const radii = ['xs', 's', 'm', 'l', 'xl', 'full'];
const components = ['Button', 'Input', 'Card', 'Badge'];
</script>

<StudioFrame title="Shape Studio" description="Radius, border, divider and focus ring shown as a complete shape system — from the corner scale down to accessible focus outlines.">
<StudioFrame title="Shape Studio" description="Radius, border, divider and focus ring shown as a complete shape system — from the corner scale down to accessible focus outlines." {nav}>
<div class="shape-studio">
<StudioWorkflow steps={workflow} ariaLabel="Shape workflow" />

<section class="radius-lab" aria-label="Radius scale preview">
<div class="lab-copy"><strong>Radius system</strong><span>One scale controls components from sharp utility chips to fully pill-shaped actions.</span></div>
Expand Down
11 changes: 2 additions & 9 deletions configurator/src/components/editors/SpacingStudio.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
import StudioControls from './StudioControls.svelte';

const steps = ['2xs', 'xs', 's', 'm', 'l', 'xl', '2xl', '3xl'];
const workflow = ['Scale', 'Rhythm', 'Components', 'Sections'];
const nav = ['Scale', 'Rhythm', 'Components', 'Sections'];
const groups = resolveStudioGroups(STUDIO_GROUPS.spacing);
</script>

<StudioFrame title="Spacing Studio" description="Scale ruler, rhythm and layout preview — from the global space multiplier down to real component and section gaps.">
<StudioFrame title="Spacing Studio" description="Scale ruler, rhythm and layout preview — from the global space multiplier down to real component and section gaps." {nav}>
<div class="spacing-studio">
<nav class="workflow" aria-label="Spacing workflow">
{#each workflow as step, index (step)}<span><b>{index + 1}</b>{step}</span>{/each}
</nav>

<section class="scale-lab" aria-label="Space map">
<div class="scale-lab__intro">
Expand Down Expand Up @@ -55,9 +52,6 @@

<style>
.spacing-studio { display: grid; gap: 12px; }
.workflow { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.workflow span { display: flex; align-items: center; gap: 8px; padding: 9px 10px; border: 1px solid var(--cfg-border); border-radius: var(--cfg-radius-s); background: var(--cfg-bg-2); color: var(--cfg-text-muted); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; }
.workflow b { display: grid; place-items: center; inline-size: 20px; block-size: 20px; border-radius: 999px; background: var(--cfg-accent-strong); color: white; font-size: 10px; }
.scale-lab { display: grid; grid-template-columns: minmax(190px, .5fr) 1fr; gap: 14px; padding: 14px; border: 1px solid var(--cfg-border); border-radius: var(--cfg-radius); background: var(--cfg-bg-2); }
.scale-lab__intro { display: grid; align-content: center; gap: 6px; }
.scale-lab__intro strong { font-size: 13px; text-transform: uppercase; letter-spacing: .06em; }
Expand All @@ -80,5 +74,4 @@
.component-grid div { display: flex; flex-wrap: wrap; gap: var(--sf-gap, .75rem); }
.component-grid span { display: grid; place-items: center; inline-size: 34px; block-size: 34px; border-radius: 999px; background: var(--cfg-accent-soft); }
@media (max-width: 820px) { .scale-lab, .rhythm-preview, .component-grid { grid-template-columns: 1fr; } }
@media (max-width: 640px) { .workflow { grid-template-columns: 1fr 1fr; } }
</style>
66 changes: 58 additions & 8 deletions configurator/src/components/editors/StudioFrame.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,66 @@
<script>
import { overrides, ui } from '../../lib/store.svelte.js';
import { buildPreviewDeclarations } from '../../lib/preview.js';
let { title, eyebrow = 'Studio', description = '', tone = 'primary', children } = $props();
let {
title,
eyebrow = 'Studio',
description = '',
tone = 'primary',
nav = [],
activePanel,
onSelectPanel,
sidebar,
children,
} = $props();
let internalActivePanel = $state();
const panelNav = $derived(nav.map((item) => (typeof item === 'string' ? { id: item, label: item } : item)));
const frameSlug = $derived(String(title).replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase());
const firstPanel = $derived(panelNav[0]?.id);
const selectedPanel = $derived(activePanel ?? internalActivePanel ?? firstPanel);
const selectedPanelSlug = $derived(selectedPanel ? String(selectedPanel).replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase() : undefined);
const panelId = $derived(selectedPanelSlug ? `studio-panel-${frameSlug}-${selectedPanelSlug}` : undefined);
const stageStyle = $derived(buildPreviewDeclarations(overrides, ui.previewTheme));

function selectPanel(panel) {
internalActivePanel = panel.id;
onSelectPanel?.(panel.id, panel);
}

function getPanelSlug(panel) {
return String(panel.id).replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
}
</script>

<section class="studio studio--{tone}" style={stageStyle}>
<div class="studio__copy">
<p>{eyebrow}</p>
<h3>{title}</h3>
{#if description}<span>{description}</span>{/if}
<ol class="studio__steps" aria-label="Studio workflow">
<li>01 Preview</li><li>02 Tune</li><li>03 Verify</li>
</ol>
{#if sidebar}
{@render sidebar?.({ activePanel: selectedPanel, panelId })}
{:else if panelNav.length}
<nav class="studio__nav" aria-label={`${title} sections`}>
<div class="studio__tabs" role="tablist" aria-orientation="vertical" aria-label={`${title} sections`}>
{#each panelNav as panel, index (panel.id)}
<button
type="button"
role="tab"
class:active={selectedPanel === panel.id}
aria-selected={selectedPanel === panel.id}
aria-controls={panel.panelId ?? `studio-panel-${frameSlug}-${getPanelSlug(panel)}`}
id={`studio-tab-${frameSlug}-${getPanelSlug(panel)}`}
tabindex={selectedPanel === panel.id ? 0 : -1}
onclick={() => selectPanel(panel)}
Comment on lines +18 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Invalid activepanel breaks focus 🐞 Bug ☼ Reliability

If a parent supplies an activePanel value that isn’t present in nav, selectedPanel will not match
any tab, leaving every tab with tabindex=-1 and aria-selected=false, so the tablist has no
focusable/selected tab. This produces a broken keyboard/a11y state for a controlled StudioFrame.
Agent Prompt
## Issue description
`selectedPanel` is derived as `activePanel ?? internalActivePanel ?? firstPanel` without validating that `activePanel` exists in `panelNav`. When it does not exist, none of the rendered tabs become selected or tabbable (`tabindex=-1` for all).

## Issue Context
This is easy to hit when callers treat `activePanel` as controlled state and `nav` changes (or values drift), and it breaks keyboard accessibility.

## Fix Focus Areas
- configurator/src/components/editors/StudioFrame.svelte[16-53]

### Suggested approach
- Compute `validSelectedPanel` by checking membership in `panelNav`:
  - If `activePanel` is defined but not found in `panelNav`, fall back to `firstPanel` (or the first valid panel).
- Ensure at least one tab always has `tabindex=0` (typically the selected tab).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

>
<b>{String(index + 1).padStart(2, '0')}</b>
<span>{panel.label}</span>
</button>
{/each}
</div>
</nav>
{/if}
</div>
<div class="studio__surface">{@render children?.()}</div>
<div class="studio__surface" role="tabpanel" id={panelId} aria-labelledby={selectedPanelSlug ? `studio-tab-${frameSlug}-${selectedPanelSlug}` : undefined}>{@render children?.()}</div>
Comment on lines +16 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Tabs reference missing panels 🐞 Bug ≡ Correctness

StudioFrame sets each tab’s aria-controls to a panel-specific id, but only renders a single tabpanel
whose id changes with the currently selected panel, so non-selected tabs point to elements that
don’t exist in the DOM. If a nav item provides panelId, aria-controls can also diverge because the
rendered tabpanel id is not derived from panel.panelId.
Agent Prompt
## Issue description
`StudioFrame` uses `role="tablist"/"tab"` with `aria-controls` pointing to IDs like `studio-panel-${frameSlug}-${slug(panel.id)}`, but only one element with `role="tabpanel"` exists and its `id` is derived from the *selected* panel. This means only the active tab’s `aria-controls` can match at any given time; all other tabs reference a non-existent element. Also, when a nav item provides `panel.panelId`, that value is used in `aria-controls` but the tabpanel `id` is still computed from `selectedPanel`, so the wiring can never match.

## Issue Context
Current DOM structure implies a full ARIA Tabs pattern but doesn’t provide a stable 1:1 mapping between each tab and its associated tabpanel element.

## Fix Focus Areas
- configurator/src/components/editors/StudioFrame.svelte[15-63]

### Suggested approach
- Introduce a single source of truth for IDs, e.g. `panelDomId(panel)`.
- Either:
  - Render **one tabpanel per nav item** (with stable `id`s), and toggle visibility via `hidden`/CSS based on `selectedPanel`, ensuring each tab’s `aria-controls` always points at an existing element; **or**
  - If the surface is not actually per-tab content, drop `role="tablist"/"tab"/"tabpanel"` semantics and use a navigation pattern instead (e.g., `nav` + buttons/links + `aria-current`).
- If supporting `panel.panelId`, ensure the tabpanel’s `id` uses that same value.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

</section>

<style>
Expand All @@ -23,14 +69,18 @@
.studio__copy p { margin: 0; color: var(--cfg-accent); font-size: 11px; font-weight: 900; letter-spacing: .1em; text-transform: uppercase; }
.studio__copy h3 { margin: 0; font-size: clamp(22px, 3vw, 30px); line-height: 1.02; letter-spacing: -.03em; }
.studio__copy span { display: block; color: var(--cfg-text-muted); font-size: 13px; line-height: 1.45; }
.studio__steps { display: grid; gap: 6px; margin-top: 6px; list-style: none; padding: 0; }
.studio__steps li { padding: 7px 9px; border: 1px solid var(--cfg-border); border-radius: 999px; background: var(--cfg-bg-2); color: var(--cfg-text-muted); font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; }
.studio__nav { margin-top: 6px; }
.studio__tabs { display: grid; gap: 6px; }
.studio__tabs button { display: grid; grid-template-columns: auto 1fr; gap: 8px; align-items: center; width: 100%; padding: 8px 10px; border: 1px solid var(--cfg-border); border-radius: 999px; background: var(--cfg-bg-2); color: var(--cfg-text-muted); text-align: start; font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; cursor: pointer; }
.studio__tabs button.active { border-color: color-mix(in oklab, var(--cfg-accent) 50%, var(--cfg-border)); background: var(--cfg-accent-soft); color: var(--cfg-text); }
.studio__tabs b { color: var(--cfg-accent); }
.studio__surface { min-width: 0; }
@media (max-width: 820px) {
.studio { grid-template-columns: 1fr; padding: 12px; }
.studio__copy { position: static; }
.studio__copy h3 { font-size: clamp(18px, 4vw, 26px); }
.studio__copy span { display: none; }
.studio__steps { grid-template-columns: repeat(3, 1fr); }
.studio__tabs { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); }
.studio__tabs { overflow-x: auto; padding-bottom: 4px; }
}
</style>
19 changes: 0 additions & 19 deletions configurator/src/components/editors/StudioWorkflow.svelte

This file was deleted.

Loading
Loading