Add configurable studio sidebar navigation - #407
Conversation
|
Warning Review limit reached
More reviews will be available in 28 minutes and 56 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Summary by QodoMake StudioFrame sidebar navigation configurable via accessible vertical tabs Description
Diagram
High-Level Assessment
Files changed (9)
|
CI Feedback 🧐A test triggered by this PR failed. Here is an AI-generated analysis of the failure:
|
Code Review by Qodo
Context used✅ Compliance rules (platform):
5 rules 1. Tabs reference missing panels
|
| 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)} | ||
| > | ||
| <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> |
There was a problem hiding this comment.
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
| 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)} |
There was a problem hiding this comment.
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
Motivation
Description
nav,activePanel,onSelectPanelandsidebarprops and exposes an internal active state; added slug/id generation for stableariatargets and tab ids (file:configurator/src/components/editors/StudioFrame.svelte).StudioFrameusingrole="tablist"/role="tab"/aria-selected/aria-controlsandrole="tabpanel"on the surface, and vertical tab styling for the left column (file:configurator/src/components/editors/StudioFrame.svelte).navinstead of rendering their own workflow:ColorStudio,SpacingStudio,LayoutStudio,MotionStudio,ShapeStudio,EffectsStudio,ShadowStudio, andTypographyStudionow providenav(files underconfigurator/src/components/editors/*Studio.svelte).activePanel/onSelectPanelwithTYPOGRAPHY_PANELSand removed its local tab toolbar, and added realistic left-column sections for Color Studio (Main colors,Semantic colors,Gradients,Shade curve,Contrast,Assignments) (files:TypographyStudio.svelte,ColorStudio.svelte).StudioWorkflow.sveltecomponent and trimmed per-studio workflow CSS that is now handled byStudioFrame(file removed:configurator/src/components/editors/StudioWorkflow.svelte).Testing
npm run check(Svelte diagnostics) which completed with 0 errors and only unrelated warnings.npm run build(Vite) which completed successfully; build emitted non-blocking warnings about chunk sizes and unrelated deprecated usage.git diff --checkto ensure no whitespace/patch issues (no problems reported).Codex Task