feat: redesign TanStack Devtools workbench - #492
Conversation
📝 WalkthroughWalkthroughThis PR introduces a branded, compact TanStack Devtools Workbench. It adds a shared semantic theme system with light/dark tokens and bundled fonts, replaces the Tabs/draw-context layout with a WorkbenchHeader, PluginsStrip, and secondary-tab primitives, integrates accessible theming into the a11y plugin, and adds extensive tests, documentation, and a font/design specification. ChangesSemantic theming, accessibility, and Workbench redesign
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Devtools
participant WorkbenchHeader
participant PluginsStrip
participant PluginsTab
participant Plugin
Devtools->>WorkbenchHeader: render destinations and actions
WorkbenchHeader->>Devtools: select Plugins or Marketplace
Devtools->>PluginsStrip: render plugin tabs
PluginsStrip->>PluginsTab: activate plugin
PluginsTab->>Plugin: render active plugin pane
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 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 |
|
View your CI Pipeline Execution ↗ for commit feee9cd
☁️ Nx Cloud last updated this comment at |
|
| Command | Status | Duration | Result |
|---|---|---|---|
nx affected --targets=test:eslint,test:sherif,t... |
❌ Failed | 36m 35s | View ↗ |
nx run-many --target=test:e2e --parallel=1 --pr... |
❌ Failed | 13m 17s | View ↗ |
nx run-many --targets=build --exclude=examples/... |
✅ Succeeded | 39s | View ↗ |
☁️ Nx Cloud last updated this comment at 2026-08-04 14:20:45 UTC
More templates
@tanstack/angular-devtools
@tanstack/devtools
@tanstack/devtools-a11y
@tanstack/devtools-bundler-core
@tanstack/devtools-client
@tanstack/devtools-rspack
@tanstack/devtools-ui
@tanstack/devtools-utils
@tanstack/devtools-vite
@tanstack/devtools-event-bus
@tanstack/devtools-event-client
@tanstack/preact-devtools
@tanstack/react-devtools
@tanstack/solid-devtools
@tanstack/svelte-devtools
@tanstack/vue-devtools
commit: |
There was a problem hiding this comment.
Actionable comments posted: 7
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/devtools-ui/src/components/tree.tsx (1)
407-453: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRemove the copy button’s unnecessary
createTheme()dependency.
CopyButtoncallscreateTheme()only to passtheme()toCopiedCopier, soJsonTreewithcopyablenow requiresThemeContextProvidereven though the value is not used. RemovecreateTheme()and the unusedthemeprop unless this provider becomes part of the publicJsonTreecontract.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-ui/src/components/tree.tsx` around lines 407 - 453, Remove the unnecessary `createTheme()` call and `theme` variable declaration at the start of the button component, since the theme is only passed to `CopiedCopier` and this dependency is not required for the component's public contract. Update the `CopiedCopier` component invocation within the Match block to remove the `theme={theme()}` prop so the component no longer depends on ThemeContextProvider.
🟡 Minor comments (14)
docs/superpowers/specs/2026-07-31-tanstack-devtools-branding-design.md-250-250 (1)
250-250: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winEscape the pipe so the table row keeps its third cell.
The literal pipe inside
light | darkstarts a fourth cell. The "Rendered result and lifecycle" content is dropped when the table renders. markdownlint reports this as MD056.📝 Proposed fix
-| Theme change | Existing `theme` value changes | Active plugin `render` and custom name callbacks receive the new `light | dark` value | +| Theme change | Existing `theme` value changes | Active plugin `render` and custom name callbacks receive the new `light \| dark` value |🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/superpowers/specs/2026-07-31-tanstack-devtools-branding-design.md` at line 250, Escape the literal pipe in the “Theme change” table row so “light | dark” remains within the third cell and the existing “Rendered result and lifecycle” content is preserved. Update only that row’s Markdown formatting.Source: Linters/SAST tools
docs/superpowers/specs/2026-07-31-tanstack-devtools-branding-design.md-210-219 (1)
210-219: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUpdate the strip geometry sections to the shipped fixed-height strip.
The specification describes a 32px idle strip that expands to 48px on hover,
:focus-within, and Marketplace-open, with a 400ms leave grace period. The tests added in this PR assert different behavior:
packages/devtools/tests/workbench.test.tsxline 203 asserts a 44px strip height, and lines 205-210 assert that hover and focus events change neither the height nor the timer count.packages/devtools/tests/workbench.test.tsxline 871 assertsgrid-template-rows: 36px 44px minmax(0, 1fr).packages/devtools/tests/workbench.test.tsxlines 658-674 assert that the Marketplace control renders in the header destinations and not inside the strip, which contradicts line 246.Update line 210 (grid row), line 214 (32/48px expansion), line 217 (32→48px growth and grace period), line 242 (forced 48px on Marketplace), line 246 (Marketplace inside the strip), and the acceptance criteria on lines 372 and 376 so the specification matches the implemented fixed-height strip.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/superpowers/specs/2026-07-31-tanstack-devtools-branding-design.md` around lines 210 - 219, Update the strip geometry and interaction behavior in the specification to match the implemented fixed-height strip rather than the dynamic expansion behavior currently described. Replace the grid-template-rows value on line 210 from the dynamic minmax expression to reflect the fixed 44px strip height, update line 214 to remove the description of 32px idle and 48px expansion states with hover and focus triggers, remove the 32→48px growth and 400ms grace period logic from line 217, correct line 242 to remove the forced 48px expansion on Marketplace-open, update line 246 to remove Marketplace as a strip interior element, and update the acceptance criteria on lines 372 and 376 to reflect a static strip configuration that does not change height based on interaction state.packages/devtools/tests/workbench-secondary-tabs.test.tsx-155-160 (1)
155-160: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winProve the bar background is opaque before measuring contrast.
contrastRatiodrops the alpha channel on line 33. IfgetComputedStyle(bar).backgroundColorresolves torgba(0, 0, 0, 0), the helper treats the background as opaque black and reports a high ratio. The 4.5 threshold then passes without measuring the real pairing. Line 151 already applies this check to the selected tab.💚 Proposed fix
+ expect(getComputedStyle(bar).backgroundColor).not.toBe('rgba(0, 0, 0, 0)') expect( contrastRatio( getComputedStyle(two).color, getComputedStyle(bar).backgroundColor, ), ).toBeGreaterThanOrEqual(4.5)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/tests/workbench-secondary-tabs.test.tsx` around lines 155 - 160, The contrast ratio test for the bar element does not validate that the background color is fully opaque before measuring contrast. Add an assertion before the contrastRatio call (similar to the check on line 151 for the selected tab) to verify that getComputedStyle(bar).backgroundColor has an alpha channel of 1, ensuring the background is opaque before the contrast ratio calculation. This prevents the helper from incorrectly treating transparent colors as opaque when computing the ratio.packages/devtools/tests/semantic-color-usage.test.ts-89-92 (1)
89-92: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAssert the Marketplace section marker exists before slicing.
If
// Plugin Marketplace Stylesis renamed or removed,indexOfreturns-1.slice(-1)then yields the last character of the file and thenot.toMatch(/\bt\(/)assertion passes trivially. The guard stops protecting the Marketplace styles at the moment it is most likely to regress.💚 Proposed fix
- const marketplaceSource = cssSource.slice( - cssSource.indexOf('// Plugin Marketplace Styles'), - ) + const marketplaceStart = cssSource.indexOf('// Plugin Marketplace Styles') + expect(marketplaceStart).toBeGreaterThanOrEqual(0) + const marketplaceSource = cssSource.slice(marketplaceStart) expect(marketplaceSource).not.toMatch(/\bt\(/)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/tests/semantic-color-usage.test.ts` around lines 89 - 92, Validate that the `// Plugin Marketplace Styles` marker exists before slicing `cssSource` in the Marketplace assertion. Store its index, assert it is non-negative, then slice from that validated index so the `not.toMatch(/\bt\(/)` check cannot pass trivially when the marker is missing.packages/devtools/src/styles/use-styles.ts-1785-1791 (1)
1785-1791: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winKeep the inverted banner hover on a solid white hover state.
semantic.color.state.hoveris#1111110fin light mode over a#ffffffbackground and#ffffff14in dark mode over a#1f1f1fbackground; this is workspace hover style, not a solid elevated hover surface for this inverted button. Use a light elevated hover token here socolors.blue[600]maintains contrast at the banner edge.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/src/styles/use-styles.ts` around lines 1785 - 1791, Update the inverted banner hover rule near the existing transform and box-shadow to use the light elevated hover token instead of semantic.color.state.hover, ensuring the hover background remains solid white and preserves colors.blue[600] contrast.packages/devtools/src/tabs/settings-tab.tsx-89-106 (1)
89-106: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMatch the configured URL parameter exactly.
Line 103 describes
urlFlagas a URL parameter name.DevToolschecks it withwindow.location.search.includes(...), sodebugalso matches?not-debug=1, and an empty value matches every URL. ParseURLSearchParamswithhas()and reject an empty parameter name.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/src/tabs/settings-tab.tsx` around lines 89 - 106, Update the DevTools URL-flag validation and matching flow to reject an empty urlFlag and use URLSearchParams.has(urlFlag) instead of window.location.search.includes(...). Preserve the configured parameter-name behavior so “debug” matches only URLs containing the exact debug parameter, not similarly named parameters.packages/devtools/src/devtools.tsx-85-92 (1)
85-92: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMove focus out of the drawer when it closes.
createDisableTabbing()only setstabIndex="-1"on the closed container. The Escape handler and header close button calltoggleOpen(), which changes state without returning focus to the trigger. If focus remains inside the drawer after collapse, move it to the trigger before hiding the drawer and add coverage for the Escape and header close-button paths.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/src/devtools.tsx` around lines 85 - 92, In the toggleOpen function, when the drawer is closing (newState is false), restore focus to the trigger element before the drawer becomes hidden. This ensures that keyboard focus returns to the trigger button when the drawer closes via any path, including the Escape handler and header close button, so focus doesn't remain trapped inside the drawer while it's hidden.packages/devtools/src/components/tab-content.tsx-25-31 (1)
25-31: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winKeep the marketplace test selector unique.
When
showMarketplaceis true, Line 26 andpackages/devtools/src/tabs/plugin-marketplace.tsxLine 327 create nested elements with the samedata-testid. AgetByTestId('plugin-marketplace')query then matches two elements and fails. Remove the wrapper test ID or give it a distinct value.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/src/components/tab-content.tsx` around lines 25 - 31, Update the wrapper around PluginMarketplace in the tab-content component to remove its duplicate data-testid="plugin-marketplace" or replace it with a distinct selector, preserving the existing plugin-marketplace test ID in the PluginMarketplace implementation.packages/devtools-ui/src/components/checkbox.tsx-26-28 (1)
26-28: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winSynchronize
data-tsd-selectedwith the effective checkbox state.The native
checkedbinding usesprops.checked ?? isChecked(), but the marker reads onlyisChecked(). If a controlled parent changescheckedwithout an input event, the marker remains stale.
packages/devtools-ui/src/components/checkbox.tsx#L26-L28: derivedata-tsd-selectedfromprops.checked ?? isChecked().packages/devtools-ui/tests/index.test.ts#L124-L127: update a controlledcheckedprop after mount and assert that the marker changes with the native checkbox.Proposed fix
- data-tsd-selected={isChecked() ? 'true' : undefined} + data-tsd-selected={ + (props.checked ?? isChecked()) ? 'true' : undefined + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-ui/src/components/checkbox.tsx` around lines 26 - 28, The data-tsd-selected marker in packages/devtools-ui/src/components/checkbox.tsx (lines 26-28) is derived only from isChecked(), but the native checked binding uses props.checked ?? isChecked(). When a controlled parent changes the checked prop without triggering an input event, the marker becomes stale. Update data-tsd-selected to use the same fallback logic as the native checkbox binding (props.checked ?? isChecked()) so the marker stays synchronized with the effective checkbox state. Additionally, in packages/devtools-ui/tests/index.test.ts (lines 124-127), add or update a test case that sets a controlled checked prop after the component mounts and verifies that the data-tsd-selected marker updates along with the native checkbox element.packages/devtools-ui/tests/tree.tsx-70-73 (1)
70-73: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRestore the original
navigator.clipboarddescriptor.This test replaces
navigator.clipboarddirectly, andvi.restoreAllMocks()only restores tracked spies, not manualObject.definePropertyassignments. Capture the original descriptor before overriding and restore it inafterEachor afinallyblock.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-ui/tests/tree.tsx` around lines 70 - 73, Update the test setup around the manual navigator.clipboard override to capture its original property descriptor before Object.defineProperty replaces it, then restore that descriptor in afterEach or a finally block. Keep vi.restoreAllMocks() for tracked mocks, but ensure each test restores the original navigator.clipboard state.packages/devtools-ui/src/styles/semantic-theme.ts-1-2 (1)
1-2: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winKeep the Inter font as a variable font or expand the import list for non-inlined assets.
packages/devtools-ui/src/styles/semantic-theme.ts:26-32usesfont-weight: 100 900, butpackages/devtools-ui/src/assets/fonts/Inter-latin.woff2is WOFF2 without anfvaraxis, so it is a static instance. That can cause browser synthesis outside the declared weight range or different typography from the variable design source. The Vite version supports?url&no-inline, so the asset query itself does not need change.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-ui/src/styles/semantic-theme.ts` around lines 1 - 2, Update the Inter font asset used by semantic-theme.ts so it is a true variable font supporting the declared 100–900 weight range, or expand the imports and font-face declarations to cover each required static Inter weight. Keep the existing ?url&no-inline query unchanged and ensure the font-weight declarations match the imported assets.packages/devtools-a11y/src/core/styles/styles.ts-257-265 (1)
257-265: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winRemove
cursor: pointerfrom the issue card.
A11yIssueCardnow renders an<article>root and moves selection into a dedicatedissueSelectButton. The card root is no longer interactive, but it still shows a pointer cursor across its whole area, including the tag row and the aside. That signals a click target that does not exist.🛠 Proposed fix
issueCard: css` padding: ${space[3]}; margin-bottom: ${space[2]}; border: 1px solid ${color.border.decorative}; border-radius: ${radius.group}; - cursor: pointer; background: ${color.surface.elevated}; box-shadow: ${shadow.xs}; `,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-a11y/src/core/styles/styles.ts` around lines 257 - 265, Remove the cursor: pointer declaration from the issueCard style in the styles configuration, leaving the dedicated issueSelectButton responsible for interactive cursor behavior.packages/devtools-a11y/src/core/utils/ui.utils.ts-92-98 (1)
92-98: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick winRebuild the highlight stylesheet only when the theme changes.
injectStylesnow removes the existing style element and builds a new one on every call.highlightElementcallsinjectStylesonce per call, andIssueList.handleIssueClickcallshighlightElementonce per node of the selected issue. An issue with many nodes therefore removes and re-creates the same stylesheet many times in one click. During each gap the highlighted elements lose their outline, and the browser recomputes styles for the whole document.Track the injected theme and skip the rebuild when it is unchanged.
🛠 Proposed fix
+let injectedTheme: TanStackDevtoolsTheme | null = null + function injectStyles(theme: TanStackDevtoolsTheme): void { - document.getElementById(HIGHLIGHT_STYLE_ID)?.remove() + const existing = document.getElementById(HIGHLIGHT_STYLE_ID) + if (existing && injectedTheme === theme) return + existing?.remove() + injectedTheme = theme🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-a11y/src/core/utils/ui.utils.ts` around lines 92 - 98, Update injectStyles to track the theme used for the currently injected highlight stylesheet and return early when the incoming theme is unchanged. Only remove the existing style element and rebuild the stylesheet when the theme differs, preserving highlightElement behavior for repeated calls across multiple nodes.packages/devtools-a11y/src/core/components/IssueCard.tsx-35-60 (1)
35-60: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe select button contains flow content, which is invalid inside
<button>.The HTML content model for
<button>allows phrasing content only. Lines 57 and 59 place a<p>and a<div>inside the button. Browsers recover from this, but validators report it, and this is an accessibility plugin whose own markup should pass an audit. Nested block elements inside a button also produce inconsistent layout across browsers.Replace the two elements with
<span>and setdisplay: blockinissueSelectButtondescendants.🛠 Proposed fix
- <p class={styles().issueMessage}>{props.issue.message}</p> + <span class={styles().issueMessage}>{props.issue.message}</span> - <div class={styles().selector}>{selector()}</div> + <span class={styles().selector}>{selector()}</span>In
packages/devtools-a11y/src/core/styles/styles.ts, adddisplay: block;toissueMessageandselectorso the spans keep the current stacked layout.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-a11y/src/core/components/IssueCard.tsx` around lines 35 - 60, The button element contains block-level elements (p and div) which violates HTML button content rules and causes accessibility/validation issues. In IssueCard.tsx, replace the p element wrapping issueMessage and the div element wrapping selector with span elements. Then in the styles file (packages/devtools-a11y/src/core/styles/styles.ts), add display: block styling to both the issueMessage and selector style definitions to preserve the current stacked layout while using inline elements.
🧹 Nitpick comments (20)
packages/devtools/tests/index.test.ts (2)
116-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove the
elementFromPointstub after the test.
Object.definePropertyis not a Vitest mock, sovi.restoreAllMocks()inafterEachleaves this stub installed. Later tests in this file keep receiving the detachedtargetbutton fromdocument.elementFromPoint. Usevi.spyOnso the existingafterEachrestores it.♻️ Proposed change
- Object.defineProperty(document, 'elementFromPoint', { - configurable: true, - value: vi.fn().mockReturnValue(target), - }) + vi.spyOn(document, 'elementFromPoint').mockReturnValue(target)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/tests/index.test.ts` around lines 116 - 119, Update the document.elementFromPoint setup in the affected test to use vi.spyOn instead of Object.defineProperty, preserving the target return value so the existing vi.restoreAllMocks cleanup restores the original implementation.
113-113: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winStub only
navigator.clipboardinstead of replacingnavigator.Object spread copies own enumerable properties. In jsdom,
navigatorexposesuserAgent,platform,language, andclipboardthrough prototype accessors, so the spread produces an almost empty object. Code under test that reads any othernavigatorproperty then receivesundefined. Hotkey and platform checks are common consumers ofnavigator.platformandnavigator.userAgent.♻️ Proposed change
- vi.stubGlobal('navigator', { ...navigator, clipboard: { writeText } }) + vi.spyOn(navigator, 'clipboard', 'get').mockReturnValue({ + writeText, + } as unknown as Clipboard)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/tests/index.test.ts` at line 113, The current stub in the vi.stubGlobal call uses object spread on navigator which copies only own enumerable properties, causing prototype accessor properties like userAgent and platform to be lost. Instead of spreading the entire navigator object, stub only the clipboard property on the existing navigator global to preserve all other navigator properties and their accessor behaviors.packages/devtools/tests/workbench-secondary-tabs.test.tsx (1)
22-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExtract
resolvedCssColorinto a shared test helper.
packages/devtools/tests/seo-workbench.test.tsxlines 17-23 define the identical helper. Move it to one module underpackages/devtools/tests/and import it in both files.contrastRatiois a second candidate if further tests need it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/tests/workbench-secondary-tabs.test.tsx` around lines 22 - 28, Extract the resolvedCssColor function from workbench-secondary-tabs.test.tsx into a new shared test helper module under packages/devtools/tests/. Remove the local resolvedCssColor definition from both workbench-secondary-tabs.test.tsx and seo-workbench.test.tsx, then import the shared function in both files. This eliminates the duplicate helper definitions while preserving the existing implementation.packages/devtools/src/styles/use-styles.ts (6)
494-498: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDo not key layout on a
data-testidvalue.The grid row template depends on
[data-testid='plugins-strip']. A test-hook rename then silently breaks the workbench layout, and the coupling is invisible fromplugins-strip.tsx.Add a dedicated presentation attribute to the strip and select on it. Do not reuse
data-workbench-secondary-tabs, becauseSeoTabalso rendersWorkbenchSecondaryTabsinside the content row and:has()matches at any depth.♻️ Proposed change
- &:has([data-testid='plugins-strip']) { + &:has([data-tsd-plugins-strip]) { grid-template-rows: ${WORKBENCH_HEADER_HEIGHT}px ${PLUGINS_STRIP_HEIGHT}px minmax(0, 1fr); }Then set the attribute on the strip container in
packages/devtools/src/components/plugins-strip.tsx, for example by adding apluginsStripflag toWorkbenchSecondaryTabsthat rendersdata-tsd-plugins-strip.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/src/styles/use-styles.ts` around lines 494 - 498, Replace the [data-testid='plugins-strip'] layout selector in the styles with a dedicated presentation attribute. Add a pluginsStrip flag to WorkbenchSecondaryTabs and set the resulting data-tsd-plugins-strip attribute on the PluginsStrip container, then select that attribute so nested SeoTab WorkbenchSecondaryTabs instances cannot match.
1653-1660: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd reduced-motion handling to the remaining animated rules.
pluginMarketplace(Line 1232) andpluginMarketplaceGrid(Line 1441) suppress their animation underprefers-reduced-motion: reduce.pluginMarketplaceEmptyrunsfadeInwith no such guard.pluginMarketplaceCardStatus(Lines 1628 and 1633) has the same gap. These elements do not carry thetsd-motion-safeclass, so the global rule inensureWorkbenchGeometryStylesdoes not reach them.♻️ Proposed change
pluginMarketplaceEmpty: css` padding: 3rem 2rem; text-align: center; background: ${semantic.color.surface.elevated}; border: 2px dashed ${semantic.color.border.control}; border-radius: 0.75rem; animation: ${fadeIn} 0.3s ease; + `@media` (prefers-reduced-motion: reduce) { + animation: none; + } `,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/src/styles/use-styles.ts` around lines 1653 - 1660, Update the animated style rules pluginMarketplaceEmpty and pluginMarketplaceCardStatus to disable or suppress their fadeIn animations under prefers-reduced-motion: reduce, matching the existing handling in pluginMarketplace and pluginMarketplaceGrid. Keep the normal animation behavior unchanged when reduced motion is not requested.
28-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueTwo breakpoints hide the wordmark.
This global rule hides
.tsd-workbench-wordmarkbelow 360px. TheworkbenchWordmarkclass already hides the same element below 430px (Lines 619-621), so this rule never changes the outcome. Remove it, or keep one source of truth for the wordmark breakpoint.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/src/styles/use-styles.ts` around lines 28 - 30, Remove the redundant global `.tsd-workbench-wordmark` rule from the `@media (max-width: 360px)` block, keeping the existing `workbenchWordmark` breakpoint as the single source of truth for hiding the wordmark.
713-716: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake the resize handle focus indicator distinct from hover.
The handle is now focusable and drives keyboard resizing.
:focus-visible::afterrenders the same 4px bar as:hover::after. A keyboard user cannot tell focus from a pointer hover. Use a stronger treatment for focus, for example thesemantic.color.border.focustoken and a larger bar.♻️ Proposed change
&:hover::after { background-color: ${semantic.color.border.control}; } + &:focus-visible::after { + top: 8px; + height: 8px; + background-color: ${semantic.color.border.focus}; + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/src/styles/use-styles.ts` around lines 713 - 716, The resize handle's `:focus-visible::after` pseudo-element currently applies the same styling as `:hover::after`, using `semantic.color.border.control` for the background, making keyboard focus indistinguishable from mouse hover. Separate the `:focus-visible::after` rule from the `:hover::after` rule and update it to use `semantic.color.border.focus` for the background color and increase the size or height of the bar to create a visually distinct focus indicator for keyboard users.
1594-1601: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winLiteral
whiteforegrounds on semantic solid fills. Both rules paint text with a hard-codedwhiteover a theme-dependentsolidFill, so the contrast outcome is not guaranteed in light and dark themes.pluginMarketplaceFeatureBannerTextat Line 1766 already shows the intended pattern withsemantic.color.status.info.onFill.
packages/devtools/src/styles/use-styles.ts#L1594-L1601: replacecolor: whitewithsemantic.color.status.success.onFillonpluginMarketplaceNewBanner.packages/devtools/src/styles/use-styles.ts#L1750-L1753: replacecolor: whitewithsemantic.color.status.info.onFillonpluginMarketplaceFeatureBannerTitle.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/src/styles/use-styles.ts` around lines 1594 - 1601, The pluginMarketplaceNewBanner rule at packages/devtools/src/styles/use-styles.ts#L1594-L1601 and the pluginMarketplaceFeatureBannerTitle rule at packages/devtools/src/styles/use-styles.ts#L1750-L1753 both use hard-coded white text color over theme-dependent semantic solid fills, which does not guarantee sufficient contrast in light and dark themes. Replace the color: white declaration in pluginMarketplaceNewBanner with semantic.color.status.success.onFill and replace the color: white declaration in pluginMarketplaceFeatureBannerTitle with semantic.color.status.info.onFill to follow the established pattern already demonstrated in the pluginMarketplaceFeatureBannerText rule.
125-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe ramp casts declare steps that do not exist.
SemanticRamprequires all ten steps.blue,green, andredare built from partial step lists and then cast withas SemanticRamp. For exampleredhas no300or900. A latercolors.red[300]compiles, resolves toundefined, and emitscolor: undefinedinto the generated CSS. The current call sites happen to use only present steps, so nothing is broken today.Populate every step in the source arrays so the cast matches reality.
♻️ Example for the `red` ramp
red: Object.fromEntries( - [100, 400, 500, 600, 700].map((step) => [ + [50, 100, 200, 300, 400, 500, 600, 700, 800, 900].map((step) => [ step, - step === 100 + step <= 100 ? semantic.color.status.error.subtleFill : semantic.color.status.error.text, ]), ) as SemanticRamp,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/src/styles/use-styles.ts` around lines 125 - 154, The blue, green, red, and purple color ramp definitions are missing required steps to satisfy the SemanticRamp type contract, causing later access to undefined properties. Add all ten required steps (100, 200, 300, 400, 500, 600, 700, 800, 900) to each ramp's step array in the Object.fromEntries calls and the purple object literal. For blue, green, and red, continue applying the existing conditional logic to all steps so that step 100 uses subtleFill, higher steps use text or border as defined, and all intermediate steps are represented.packages/devtools/src/tabs/seo-tab/index.tsx (1)
30-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse one capitalization style for the two tab labels.
Line 30 uses sentence case ("Social previews"). Line 37 uses title case ("SERP Preview"). Pick one, for example "SERP preview".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/src/tabs/seo-tab/index.tsx` around lines 30 - 37, Use consistent sentence-case capitalization for the tab labels: update the SERP Preview label in the WorkbenchSecondaryTab with activeView() === 'serp-preview' to “SERP preview,” matching “Social previews.”packages/devtools/src/components/workbench-header.tsx (1)
53-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove the duplicate accessible names around the logo.
Three elements now expose the same name. The
headerhasaria-label="TanStack Devtools", the wrapperspanhas the samearia-label, and theimghasalt="TanStack Devtools".aria-labelon a plainspanhas no role to attach to, so support is inconsistent. Theimgname also repeats the visible wordmark on Line 60.Mark the logo as decorative and keep the name on the
headeronly.♻️ Proposed change
- <span aria-label="TanStack Devtools" class={styles().workbenchLogo}> - <img src={TanStackLogo} alt="TanStack Devtools" /> + <span class={styles().workbenchLogo}> + <img src={TanStackLogo} alt="" /> </span>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools/src/components/workbench-header.tsx` around lines 53 - 61, Remove the aria-label="TanStack Devtools" from the span wrapper with class workbenchLogo since it is not a semantic element and creates redundant accessible names. Change the img alt attribute from "TanStack Devtools" to an empty string to mark it as decorative, since the visible wordmark text is already provided by the strong element on line 60 with the same label. Keep the aria-label on the header element as the single accessible name source for the entire component.packages/devtools-a11y/src/core/utils/ui.utils.ts (1)
288-293: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueTwo maps now define severity labels.
getSeverityStylealready returns alabelfor each severity, andseverity-theme.tsowns those strings. This code uses a separateSEVERITY_LABELSmap. The two maps can drift, and the tooltip text would then disagree with the panel badge text. Consider reading the label fromgetSeverityStylehere, or derivingSEVERITY_LABELSfrom theseveritydefinition.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-a11y/src/core/utils/ui.utils.ts` around lines 288 - 293, Update the tooltip label construction in the surrounding single- and multiple-issue branches to reuse the label provided by getSeverityStyle, or derive SEVERITY_LABELS from the shared severity definition in severity-theme.ts. Remove the independent label source so tooltip text stays consistent with panel badge text.packages/devtools-ui/tests/contrast.test.ts (1)
69-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSelect the syntax keys by name instead of by position.
Object.values(theme.color.syntax).slice(0, 6)depends on the key insertion order insemanticThemes. If someone reorders thesyntaxkeys, this loop silently checksselectionFillandselectionTextagainst a surface color and skips a real syntax color. Name the keys to make the intent explicit and order-independent.♻️ Proposed change
- for (const color of Object.values(theme.color.syntax).slice(0, 6)) - expect(ratio(color, syntaxBackground)).toBeGreaterThanOrEqual(4.5) + for (const key of [ + 'keyword', + 'string', + 'number', + 'comment', + 'property', + 'punctuation', + ] as const) + expect( + ratio(theme.color.syntax[key], syntaxBackground), + ).toBeGreaterThanOrEqual(4.5)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-ui/tests/contrast.test.ts` around lines 69 - 70, Update the contrast test loop around theme.color.syntax to select the intended syntax colors by their explicit key names rather than relying on Object.values(...).slice(0, 6). Keep the existing ratio check against syntaxBackground and ensure selectionFill and selectionText are not included.packages/devtools-ui/tests/fonts.test.ts (2)
46-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueByte-level license pinning can fail on line-ending normalization.
The test pins both a sha256 digest and an exact byte size for each license file. If a contributor checks out the repository with
core.autocrlf=true, the files gain CRLF line endings and both assertions fail with an opaque message. Thesizeassertion also adds nothing beyond the digest.Consider dropping the
sizeassertions and hashing the content with newlines normalized. The existingtoContainassertions already cover copyright and license provenance.♻️ Proposed change
-async function sha256(path: string): Promise<string> { - return createHash('sha256') - .update(await readFile(path)) - .digest('hex') -} +async function sha256(path: string): Promise<string> { + const text = (await readFile(path, 'utf8')).replace(/\r\n/g, '\n') + return createHash('sha256').update(text, 'utf8').digest('hex') +}Recompute both expected digests after this change, and remove the two
toMatchObject({ size: ... })assertions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-ui/tests/fonts.test.ts` around lines 46 - 57, Update the license verification in fonts.test.ts by removing the size assertions for bricolageLicensePath and interLicensePath, and normalize line endings before hashing each file so checkout-specific CRLF changes do not affect the digest. Recompute and replace both expected SHA-256 values using the normalized content while preserving the existing provenance checks.
11-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBoth new test files resolve repository paths from
process.cwd(). The working directory depends on how the test runner is invoked. If Vitest runs from the workspace root instead of a package root,readdirandreaddirSyncthrowENOENT. Resolve the paths from the module location withnew URL(..., import.meta.url)andfileURLToPath.
packages/devtools-ui/tests/fonts.test.ts#L11-L12: replacejoin(process.cwd(), 'src', 'assets', 'fonts')with a path derived fromimport.meta.url.packages/devtools-a11y/tests/theme.test.ts#L17-L18: derivepackageRootandcoreRootfromimport.meta.urlso the source scan and therelative()paths in the violation messages stay stable.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-ui/tests/fonts.test.ts` around lines 11 - 12, Both tests must resolve repository paths independently of the test runner’s working directory. In packages/devtools-ui/tests/fonts.test.ts lines 11-12, update the fontDirectory setup near expectedFontNames to derive the path from import.meta.url using fileURLToPath and new URL; in packages/devtools-a11y/tests/theme.test.ts lines 17-18, derive packageRoot and coreRoot the same way so source scanning and relative() violation paths remain stable.packages/devtools-a11y/tests/theme.test.ts (1)
124-126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThis assertion does not discriminate the selected styling.
document.head.textContentholds every class goober has generated so far in the process. The colors#ffffffand#111111appear in many unrelated rules, and the sheet accumulates across bothdescribe.eachiterations. The assertion therefore passes even if the selected-state styles are removed.Assert on the resolved class name of the selected element instead, so the check binds to the styling under test.
♻️ Proposed change
- expect(document.head.textContent).toContain( - resolveSemanticTheme(theme).color.state.selectionText, - ) + const selectedClass = createStyles()().issueCardSelected + expect(host.querySelector('[data-tsd-surface]')?.className).toContain( + selectedClass, + )
createStylesrequires a theme context, so read it inside the rendered tree, or export the selected class name from the style factory for assertions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-a11y/tests/theme.test.ts` around lines 124 - 126, Replace the broad document.head.textContent assertion in the selected-styling test with an assertion against the selected element’s resolved class name. Read the theme context inside the rendered tree when calling createStyles, or expose the selected class name from the style factory, and verify that class is applied to the selected element rather than matching unrelated accumulated stylesheet colors.packages/devtools-a11y/src/core/styles/styles.ts (2)
227-237: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueResolve the severity style once per style function.
getSeverityStyleruns two or three times inside a single template. Each call allocates a new object through the spread inseverity-theme.ts. Call it once and reuse the result.♻️ Proposed change for `severityLabel`
- severityLabel: (impact: SeverityThreshold) => css` - font-size: ${type.labelSm.size}; - line-height: ${type.labelSm.lineHeight}; - color: ${getSeverityStyle(impact, themeName).colors.text}; - background: ${getSeverityStyle(impact, themeName).colors.subtleFill}; - border: 1px solid ${getSeverityStyle(impact, themeName).colors.border}; - padding: 2px ${space[1]}; - border-radius: ${radius.control}; - `, + severityLabel: (impact: SeverityThreshold) => { + const severity = getSeverityStyle(impact, themeName).colors + return css` + font-size: ${type.labelSm.size}; + line-height: ${type.labelSm.lineHeight}; + color: ${severity.text}; + background: ${severity.subtleFill}; + border: 1px solid ${severity.border}; + padding: 2px ${space[1]}; + border-radius: ${radius.control}; + ` + },Apply the same pattern to
summaryButtonActive,summaryCount,sectionTitle, anddot.Also applies to: 249-256, 311-326
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-a11y/src/core/styles/styles.ts` around lines 227 - 237, Update the style functions summaryButtonActive, summaryCount, severityLabel, sectionTitle, and dot to call getSeverityStyle once per invocation, store the returned style, and reuse it for all color properties within the template.
100-123: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove unused style keys from
styles.ts.
primaryButton,primaryButtonDisabled,button,toggleOverlay,toggleOverlayOn,smallLinkButton,disableRule,doneButton,select,issueMain, andsearchhave nostyles().<key>consumers inpackages/devtools-a11y. Remove the unused definitions while keeping keys that components still apply.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-a11y/src/core/styles/styles.ts` around lines 100 - 123, In the styles definition, remove the unused keys primaryButton, primaryButtonDisabled, button, toggleOverlay, toggleOverlayOn, smallLinkButton, disableRule, doneButton, select, issueMain, and search. Preserve all style keys that still have styles().<key> consumers in the devtools-a11y components.packages/devtools-ui/src/styles/use-styles.ts (1)
25-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSolid buttons lose hover and pressed color feedback.
solidHoverusesstatus.borderandsolidActiveusesstatus.solidFill. In the light theme,border,text, andsolidFillare the same value for every status role. For example,successuses#1d4226for all three. In the dark theme,borderandsolidFillare also equal forsuccess,warning,error, andinfo.The result is that a solid button keeps the same background on hover and on press. Only the box-shadow changes. That is weak feedback for pointer and keyboard users.
Consider composing the hover and pressed layers from
semantic.color.state.hoverandsemantic.color.state.pressed, which already encode translucent overlays.♻️ Proposed change to restore state feedback
solid: css` background: ${solidBg}; color: ${solidText}; border-color: ${solidBorder}; &:hover { - background: ${solidHover}; - border-color: ${solidHover}; + background: linear-gradient( + ${semantic.color.state.hover}, + ${semantic.color.state.hover} + ), + ${solidHover}; + border-color: ${solidHover}; box-shadow: ${semantic.shadow.xs}; } &:active { - background: ${solidActive}; - border-color: ${solidActive}; + background: linear-gradient( + ${semantic.color.state.pressed}, + ${semantic.color.state.pressed} + ), + ${solidActive}; + border-color: ${solidActive}; box-shadow: ${semantic.shadow.sm}; } `,Also applies to: 60-74
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-ui/src/styles/use-styles.ts` around lines 25 - 29, The solidHover and solidActive variable assignments use status color properties that are identical to solidFill in light theme and dark theme, preventing visual feedback on hover and pressed states. Replace the solidHover assignment with a reference to semantic.color.state.hover and the solidActive assignment with semantic.color.state.pressed, which provide translucent overlay states. Apply the same pattern fixes to the analogous hover and active state assignments in the outlineHover, outlineActive, ghostHover, and ghostActive variable definitions to ensure consistent state feedback across all button variants.packages/devtools-a11y/tests/index.test.ts (1)
3-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a development-mode case for
a11yDevtoolsPlugin.The mock tuple order matches
createReactPlugin, but the current test only covers the production no-op import path. Add a companion case that stubsNODE_ENVtodevelopmentand asserts the activeA11yDevtoolsPanel/a11yDevtoolsPluginbranch.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/devtools-a11y/tests/index.test.ts` around lines 3 - 9, Add a development-mode test case in the existing test suite that stubs NODE_ENV to development, imports or invokes the active a11yDevtoolsPlugin branch, and asserts it uses the mocked A11yDevtoolsPanel/createReactPlugin path rather than the production no-op path. Preserve the existing production coverage and restore the environment or module state between cases.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2a5ebdd3-1aaa-4775-914b-dac45d834f11
⛔ Files ignored due to path filters (4)
packages/devtools-ui/src/assets/fonts/BricolageGrotesque-Bold.ttfis excluded by!**/*.ttfpackages/devtools-ui/src/assets/fonts/Inter-latin.woff2is excluded by!**/*.woff2packages/devtools/src/components/tanstack-logo.pngis excluded by!**/*.pngpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (87)
.changeset/tanstack-devtools-branding.mddocs/architecture.mddocs/overview.mddocs/superpowers/specs/2026-07-31-tanstack-devtools-branding-design.mdexamples/react/basic/README.mdexamples/react/basic/package.jsonexamples/react/basic/scripts/check-a11y-fixture.mjsexamples/react/basic/src/a11y-audit-fixture.tsxexamples/react/basic/src/example.cssexamples/react/basic/src/index.tsxexamples/react/basic/src/setup.tsxexamples/react/basic/src/vite-env.d.tspackages/devtools-a11y/src/core/components/IssueCard.tsxpackages/devtools-a11y/src/core/components/IssueList.tsxpackages/devtools-a11y/src/core/components/Settings.tsxpackages/devtools-a11y/src/core/components/Shell.tsxpackages/devtools-a11y/src/core/contexts/allyContext.tsxpackages/devtools-a11y/src/core/styles/severity-theme.tspackages/devtools-a11y/src/core/styles/styles.tspackages/devtools-a11y/src/core/utils/ui.utils.tspackages/devtools-a11y/tests/index.test.tspackages/devtools-a11y/tests/theme.test.tspackages/devtools-a11y/vite.config.tspackages/devtools-ui/package.jsonpackages/devtools-ui/scripts/check-font-assets.mjspackages/devtools-ui/src/assets/fonts/OFL-Bricolage-Grotesque.txtpackages/devtools-ui/src/assets/fonts/OFL-Inter.txtpackages/devtools-ui/src/components/button.tsxpackages/devtools-ui/src/components/checkbox.tsxpackages/devtools-ui/src/components/header.tsxpackages/devtools-ui/src/components/icons.tsxpackages/devtools-ui/src/components/input.tsxpackages/devtools-ui/src/components/logo.tsxpackages/devtools-ui/src/components/main-panel.tsxpackages/devtools-ui/src/components/section.tsxpackages/devtools-ui/src/components/select.tsxpackages/devtools-ui/src/components/tag.tsxpackages/devtools-ui/src/components/theme.tsxpackages/devtools-ui/src/components/tree.tsxpackages/devtools-ui/src/internal.tspackages/devtools-ui/src/styles/semantic-theme.tspackages/devtools-ui/src/styles/use-styles.tspackages/devtools-ui/tests/contrast.test.tspackages/devtools-ui/tests/fonts.test.tspackages/devtools-ui/tests/index.test.tspackages/devtools-ui/tests/semantic-theme.test.tspackages/devtools-ui/tests/tree.tsxpackages/devtools-ui/tsconfig.jsonpackages/devtools-ui/vite.config.tspackages/devtools/src/components/content-panel.tsxpackages/devtools/src/components/main-panel.tsxpackages/devtools/src/components/plugins-strip.tsxpackages/devtools/src/components/source-inspector.tsxpackages/devtools/src/components/tab-content.tsxpackages/devtools/src/components/tabs.test.tsxpackages/devtools/src/components/tabs.tsxpackages/devtools/src/components/trigger.tsxpackages/devtools/src/components/workbench-header.tsxpackages/devtools/src/components/workbench-secondary-tabs.tsxpackages/devtools/src/context/devtools-context.test.tspackages/devtools/src/context/devtools-context.tsxpackages/devtools/src/context/draw-context.tsxpackages/devtools/src/context/pip-context.tsxpackages/devtools/src/context/use-devtools-context.tspackages/devtools/src/devtools.tsxpackages/devtools/src/styles/tokens.tspackages/devtools/src/styles/use-styles.tspackages/devtools/src/tabs/index.tsxpackages/devtools/src/tabs/marketplace/marketplace-header.tsxpackages/devtools/src/tabs/marketplace/plugin-card.tsxpackages/devtools/src/tabs/marketplace/plugin-section.tsxpackages/devtools/src/tabs/marketplace/settings-panel.tsxpackages/devtools/src/tabs/marketplace/tag-filters.tsxpackages/devtools/src/tabs/plugin-marketplace.tsxpackages/devtools/src/tabs/plugins-tab.tsxpackages/devtools/src/tabs/seo-tab/index.tsxpackages/devtools/src/tabs/seo-tab/serp-preview.tsxpackages/devtools/src/tabs/seo-tab/social-previews.tsxpackages/devtools/src/tabs/settings-tab.test.tsxpackages/devtools/src/tabs/settings-tab.tsxpackages/devtools/src/utils/constants.tspackages/devtools/src/utils/storage.tspackages/devtools/tests/index.test.tspackages/devtools/tests/semantic-color-usage.test.tspackages/devtools/tests/seo-workbench.test.tsxpackages/devtools/tests/workbench-secondary-tabs.test.tsxpackages/devtools/tests/workbench.test.tsx
💤 Files with no reviewable changes (5)
- packages/devtools/src/components/tabs.test.tsx
- packages/devtools/src/components/tabs.tsx
- packages/devtools/src/tabs/index.tsx
- packages/devtools/src/styles/tokens.ts
- packages/devtools/src/context/draw-context.tsx
| let nextHeight: number | undefined | ||
| if (grows) nextHeight = clampedHeight() + step | ||
| if (shrinks) nextHeight = clampedHeight() - step | ||
| if (event.key === 'Home') nextHeight = PANEL_CLOSE_THRESHOLD | ||
| if (event.key === 'End') nextHeight = maxHeight() | ||
| if (nextHeight === undefined) return | ||
| event.preventDefault() | ||
| props.handleHeightChange?.(Math.min(maxHeight(), nextHeight)) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Clamp the keyboard resize to PANEL_CLOSE_THRESHOLD.
Line 45 clamps only the upper bound. When clampedHeight() is already at PANEL_CLOSE_THRESHOLD, ArrowDown (bottom panel) reports 20 px. updateHeight in packages/devtools/src/devtools.tsx then sets isOpen to false, so the panel collapses and the separator is unmounted. A keyboard user cannot restore the panel from that control.
This also contradicts aria-valuemin={PANEL_CLOSE_THRESHOLD} on Line 66 and the Home behavior on Line 41, which both declare 70 as the floor.
🐛 Proposed fix to clamp both bounds
if (nextHeight === undefined) return
event.preventDefault()
- props.handleHeightChange?.(Math.min(maxHeight(), nextHeight))
+ props.handleHeightChange?.(
+ Math.min(maxHeight(), Math.max(PANEL_CLOSE_THRESHOLD, nextHeight)),
+ )
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let nextHeight: number | undefined | |
| if (grows) nextHeight = clampedHeight() + step | |
| if (shrinks) nextHeight = clampedHeight() - step | |
| if (event.key === 'Home') nextHeight = PANEL_CLOSE_THRESHOLD | |
| if (event.key === 'End') nextHeight = maxHeight() | |
| if (nextHeight === undefined) return | |
| event.preventDefault() | |
| props.handleHeightChange?.(Math.min(maxHeight(), nextHeight)) | |
| let nextHeight: number | undefined | |
| if (grows) nextHeight = clampedHeight() + step | |
| if (shrinks) nextHeight = clampedHeight() - step | |
| if (event.key === 'Home') nextHeight = PANEL_CLOSE_THRESHOLD | |
| if (event.key === 'End') nextHeight = maxHeight() | |
| if (nextHeight === undefined) return | |
| event.preventDefault() | |
| props.handleHeightChange?.( | |
| Math.min(maxHeight(), Math.max(PANEL_CLOSE_THRESHOLD, nextHeight)), | |
| ) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/devtools/src/components/content-panel.tsx` around lines 38 - 45,
Update the keyboard resize handler around nextHeight and
props.handleHeightChange so the reported height is clamped to both
PANEL_CLOSE_THRESHOLD and maxHeight(). Preserve the existing key-specific height
calculations and ensure ArrowDown cannot produce a value below the declared
minimum.
| <div | ||
| id={TANSTACK_DEVTOOLS} | ||
| data-testid="tsd-main-panel" | ||
| data-open={props.isOpen() ? 'true' : 'false'} | ||
| data-testid="tanstack-devtools-panel" | ||
| data-open={String(props.isOpen())} | ||
| data-collapsed={String(isAttachedCollapsed())} | ||
| data-tsd-surface |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Use an instance-specific panel target.
Line 86 assigns every MainPanel the same tanstack_devtools ID. createDisableTabbing resolves that ID with document.getElementById, so multiple DevTools owners update tab stops only in the first panel. A later closed panel can remain keyboard reachable, and the document contains duplicate IDs. Pass the panel element to createDisableTabbing or generate a per-instance ID.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/devtools/src/components/main-panel.tsx` around lines 85 - 90, Update
MainPanel and createDisableTabbing so tab handling targets the current panel
element rather than the shared TANSTACK_DEVTOOLS ID. Pass each panel’s element
directly to createDisableTabbing (or generate and consistently use a
per-instance ID), ensuring multiple MainPanel instances have no duplicate IDs
and closed panels are correctly removed from keyboard navigation.
| it('generates stable unique IDs while preserving explicit duplicate IDs', () => { | ||
| const generated = getExistingStateFromStorage(undefined, [ | ||
| { name: 'Same Name', render: () => {} }, | ||
| { name: 'Same Name', render: () => {} }, | ||
| { name: () => {}, render: () => {} }, | ||
| ]).plugins!.map((entry) => entry.id) | ||
| expect(generated).toEqual(['same-name-0', 'same-name-1', '2']) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Add the missing expected plugin ID.
Lines 359-363 pass four plugin definitions, and getExistingStateFromStorage maps every definition into plugins. generated therefore has four entries. Line 364 expects three entries, so this test fails before it checks uniqueness. Add the fourth expected ID and assert that the generated ID set has four entries.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/devtools/src/context/devtools-context.test.ts` around lines 358 -
364, The test case passes four plugin definitions to getExistingStateFromStorage
in the array, but the expect().toEqual() assertion on line 364 only includes
three expected IDs. Add the fourth expected ID to the assertion array to match
the number of plugins provided, so the test validates all four generated IDs
including the unnamed plugin that should generate an auto-incremented numeric
ID.
| it('reactively replaces plugins through the existing onSetPlugins callback', async () => { | ||
| let replace!: (plugins: Array<TanStackDevtoolsPlugin>) => void | ||
| let ids: Array<string> = [] | ||
| const dispose = createRoot((disposeRoot) => { | ||
| createComponent(DevtoolsProvider, { | ||
| plugins: [{ name: 'Old', render: () => {} }], | ||
| onSetPlugins: (setter) => { | ||
| replace = setter | ||
| }, | ||
| get children() { | ||
| const context = useContext(DevtoolsContext)! | ||
| createEffect(() => { | ||
| ids = context.store.plugins?.map((entry) => entry.id!) ?? [] | ||
| }) | ||
| return null | ||
| }, | ||
| }) | ||
| return disposeRoot | ||
| }) | ||
| await Promise.resolve() | ||
| expect(ids).toEqual(['old-0']) | ||
| replace([{ name: 'New', render: () => {} }]) | ||
| expect(ids).toEqual(['new-0']) | ||
| dispose() |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Reconcile active plugin IDs during replacement.
Line 393 replaces store.plugins with new-0. DevtoolsProvider.updatePlugins only updates store.plugins, so the initial one-plugin state retains activePlugins: ['old-0']. PluginsTab then creates a pane for old-0 and finds no plugin to render. Reconcile active IDs with the replacement list and apply the normal default-selection behavior when no valid active ID remains. Extend this test to assert the active state and rendered replacement plugin.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/devtools/src/context/devtools-context.test.ts` around lines 372 -
395, Update DevtoolsProvider.updatePlugins to reconcile activePlugins against
the replacement plugin IDs, removing stale IDs and applying the existing
default-selection behavior when none remain valid. Extend the reactive
replacement test around onSetPlugins to assert the active plugin state and
verify that the replacement plugin is rendered instead of the removed plugin.
| const updateHeight = (nextHeight: number) => { | ||
| setHeight(nextHeight) | ||
| setIsOpen(nextHeight >= PANEL_CLOSE_THRESHOLD) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve a usable height after a drag-close.
Line 81 stores the raw drag height, and Line 82 closes the drawer below PANEL_CLOSE_THRESHOLD. After a user drags to 50px, toggleOpen reopens the drawer without restoring its height. The drawer then opens at 50px. Preserve the last valid expanded height or clamp the stored height before closing. Apply the same maximum bound used by keyboard resizing.
Also applies to: 117-123
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/devtools/src/devtools.tsx` around lines 80 - 83, Update updateHeight
and the keyboard-resizing logic to clamp stored heights using the same maximum
bound, while preserving the last valid expanded height when a drag closes below
PANEL_CLOSE_THRESHOLD. Ensure toggleOpen reopens the drawer at a usable expanded
height rather than the raw drag height.
| <div | ||
| class={styles().pluginMarketplaceSettingsPanel} | ||
| data-tsd-surface | ||
| role="dialog" | ||
| aria-label="Marketplace settings" | ||
| > | ||
| <div class={styles().pluginMarketplaceSettingsPanelHeader}> | ||
| <h3 class={styles().pluginMarketplaceSettingsPanelTitle}> | ||
| Marketplace Settings | ||
| </h3> | ||
| <button | ||
| type="button" | ||
| aria-label="Close marketplace settings" | ||
| data-tsd-control | ||
| class={styles().pluginMarketplaceSettingsPanelClose} | ||
| onClick={props.onClose} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Move focus into the settings dialog.
When the settings button opens this dialog, no code moves focus to the panel. SettingsPanel is inserted before MarketplaceHeader, so focus stays on the trigger and forward Tab moves past the close button and checkbox. Focus the close button or first control on open. Restore focus to the trigger on close.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/devtools/src/tabs/marketplace/settings-panel.tsx` around lines 18 -
33, Update SettingsPanel to move focus to its close button or first control when
the dialog opens, ensuring keyboard navigation begins inside the panel. On
close, restore focus to the settings trigger that opened it, using the existing
open/close lifecycle and refs or focusable element symbols rather than leaving
focus on the removed trigger.
| export const getStorageItem = (key: string) => { | ||
| return localStorage.getItem(key) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Handle unavailable local storage.
Line 2 can throw when storage is blocked or the document has an opaque origin. Devtools context initialization reads settings and state through this helper, so the exception prevents Devtools from mounting. Return null on storage failure and add a test for a throwing getItem.
Proposed fix
export const getStorageItem = (key: string) => {
- return localStorage.getItem(key)
+ try {
+ return localStorage.getItem(key)
+ } catch {
+ return null
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const getStorageItem = (key: string) => { | |
| return localStorage.getItem(key) | |
| } | |
| export const getStorageItem = (key: string) => { | |
| try { | |
| return localStorage.getItem(key) | |
| } catch { | |
| return null | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/devtools/src/utils/storage.ts` around lines 1 - 3, Update
getStorageItem to catch failures from localStorage.getItem and return null when
storage is unavailable or access throws, while preserving normal retrieved
values. Add a test covering a throwing getItem call and asserting the helper
returns null.

Summary
Test plan
git diff --check.Summary by CodeRabbit