Skip to content

refactor(studio): give Toggle the phone glyph rule and use its ghost variant in the composer - #2381

Open
peppescg wants to merge 1 commit into
mainfrom
fix/2380-toggle-ghost-phone-icons
Open

peppescg wants to merge 1 commit into
mainfrom
fix/2380-toggle-ghost-phone-icons

Conversation

@peppescg

Copy link
Copy Markdown
Contributor

Summary

Follow-up to Daniel's non-blocking review notes on #2362 and #2368.

  1. Toggle gets the phone glyph rule. Toggle now renders data-toggle="" after its props spread, the same shape fix(studio): keep the phone icon size for Buttons inside asChild triggers #2368 gave Button with data-button. data-slot="toggle" keeps naming the outermost part, so an asChild trigger can still replace it. The phone rule in styles.css adds [data-toggle] svg.lucide:not([class*="size-"]):not([class*="h-"]):not([class*="w-"]), so a Toggle's bare icon draws at 20px on phones. The hand-written size-4 on the mic icons in chat-composer.tsx and side-thread-panel.tsx is gone.
  2. The composer uses the ghost Toggle variant (refactor(studio): use the Tabs and Toggle primitives in two hand-rolled controls #2334). The dictation Toggles, the phone sheet's Mode rows (toggleRowClass), Model rows (model-effort-menu.tsx) and Effort pills (effortPillClass) no longer carry tokens that only undid the default variant's pressed fill and ink (data-[state=on]:bg-transparent, data-[state=on]:text-current, data-[state=on]:hover:bg-muted/50 on the rows, rounded-full on the pills). The dictation Toggles also stop borrowing buttonVariants along with the border-0 bg-transparent shadow-none overrides it needed.
  3. One dictation class. features/chat/dictation-toggle.ts exports dictationToggleClass, used by both the composer and the side thread. The two copies had drifted: only the composer's had border-0 bg-transparent shadow-none, which had no visible effect.
  4. button.tsx documents the data-button boundary. An element that only borrows buttonVariants classes has no data-button, so the phone glyph rule doesn't size its icons.

The dark pressed-hover on the model rows (Daniel's question on #2362)

The comment in #2362 was right, and the token was a no-op. Before #2362, a chosen row had bg-zinc-100 dark:bg-zinc-800. The dark variant (&:is(.dark *)) has the same specificity as hover: and sorts after it, so the dark pressed-hover fill was already zinc-800, and the light one was muted/50. dark:data-[state=on]:hover:bg-zinc-800 never changed anything: removing it changes nothing in the browser (computed-style diff below), and the new browser pin passes with or without it. This PR removes it and rewrites the comment to say why the dark fill stays. Light pressed-hover stays muted/50 and dark pressed-hover stays zinc-800, and both are now pinned.

Visible changes

The comparison covers every state, not only screenshots. A throwaway Playwright probe read the computed style of every changed control on origin/main (9f08f63) and on this branch: background, color, border, radius, box-shadow, outline, padding, size, font weight and glyph size. It covered rest, hover and keyboard focus, pressed and not pressed, light and dark, at 1280px and 390px. 150 states were compared. Only two things differ:

  • Keyboard focus on the sheet rows and Effort pills. These now show the ghost variant's ring-2 ring-offset-2 focus ring, the same ring as the dictation Toggle and every Button, instead of the browser's auto outline. On the full-width rows, focus-visible:ring-inset keeps the ring inside the row, because the sheet's scroll container would otherwise clip it on the left and right. The plain-button rows in the same sheet (Mode, Model, Back, Add a file) keep the browser outline.
  • The canvas Preview/Edit switch on phones. Its bare icons are inside a Toggle, so they now draw at 20px instead of 18px, as fix(studio): keep the phone icon size for Buttons inside asChild triggers #2368 did for bare icons in Buttons. Each segment is 2px wider. On desktop they stay 16px.

Everything else is identical. That includes both dictation Toggles in every state (focus too), the rows and pills at rest, hover, pressed and pressed-hover, and the desktop Model/Effort menu, which has no Toggles.

Before | After

Before is origin/main 9f08f63; After is 914645b. Red boxes mark real pixel differences (a channel delta of 8 or more). Amber boxes mark the Toggles that were reviewed and are unchanged. The few 1 to 3 level antialias pixels on unrelated rounded borders (the starter pill and the side thread's model pill) are deterministic rasterisation noise and are not boxed.

Composer: dictation Toggle, idle and listening

Desktop 390px
composer desktop composer phone
composer listening desktop composer listening phone

Side thread: dictation Toggle, idle and listening

Desktop 390px
side thread desktop side thread phone
side thread listening desktop side thread listening phone

Model and effort menu (desktop dropdown, phone sheet), and the phone Mode sheet

Desktop 390px
model menu desktop model sheet phone
mode sheet phone

Keyboard focus in the phone sheet (red: the ghost focus ring)

Row (GPT-5) Effort pill (Low)
focus row focus pill

Canvas Preview/Edit switch (red: 18px to 20px glyphs on phones)

Desktop 390px
canvas desktop canvas phone

Tests

New pins were written first and passed on origin/main. They pass on this branch and fail under mutation.

  • e2e/composer-toggles.spec.ts (real browser):
    • Glyph size: the composer's dictation glyph is 20px on phones and 16px on desktop, idle and listening, and so is the canvas switch's. On main the canvas pin fails on phones (18px), which is the intended change.
    • Look relations, light and dark, measured at rest, hover, pressed and pressed-hover:
      • Dictation: no fill at rest, a hover fill and ink, a brand tint while listening, and the same hover ink while listening.
      • Mode rows: no pressed fill, and the same hover fill whether pressed or not.
      • Model rows: zinc while chosen; on hover, muted/50 in light and the zinc fill kept in dark.
      • Effort pills: no hover fill, and a brand border, fill and ink while pressed.
      • Shapes: square rows and round pills and mic.
  • toggle.test.tsx: data-toggle is present and survives a TooltipTrigger asChild whose data-slot wins.
  • composer-toggles.pinning.test.tsx: the composer's and the side thread's dictation Toggles render the same class, carry data-toggle, and their glyph has no size class of its own.
  • fonts.test.ts: the 20px phone rule lists [data-toggle] svg.lucide:not([class*="size-"]).

Mutation results. Each mutation was applied, the pins were run, and the code was restored:

Mutation Result
Remove the [data-toggle] line from the phone rule fonts.test.ts fails; the e2e glyph pin fails on phones
Remove data-toggle from Toggle toggle.test.tsx and the pinning test fail
Mode rows back to the default variant e2e looks fail (light, dark)
Model rows back to the default variant e2e looks fail
Effort pills back to the default variant e2e looks fail (light, dark)
Drop the dictation pressed-hover ink e2e looks fail (light, dark)
Composer dictation without variant="ghost" pinning test fails (classes differ)
Force the dark pressed-hover to muted/50 e2e looks fail (dark)
Drop the light pressed-hover muted/50 e2e looks fail (light)

Gates

  • task studio:check: lint, typecheck, test (web 1656, server 252) and generated-check, all green.
  • task studio:browser: 150 passed, 40 skipped (the project-specific skips), 0 failed.
  • No Markdown changes, so task docs was not needed.

Closes #2380

🤖 Generated with Claude Code

…variant in the composer

Toggle now renders data-toggle after its props spread, as Button renders
data-button, and the phone glyph rule in styles.css keys on it, so a
Toggle's bare icon draws at 20px on phones without a hand-written size-4.
The composer and side-thread dictation Toggles drop theirs and share one
class on the ghost variant. The phone sheet's Mode and Model rows and
Effort pills use the ghost variant instead of strings that undid the
default variant's pressed fill; the rows inset the ghost focus ring so
the scrolling sheet doesn't clip it. The model rows drop a dark
pressed-hover token that never applied: the dark fill already sorts
after hover. button.tsx notes that borrowing buttonVariants doesn't
bring data-button.

Closes #2380

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@peppescg peppescg self-assigned this Oct 16, 2026
@kantord

kantord commented Oct 19, 2026

Copy link
Copy Markdown
Member

Hi, a heads-up from the review: GitHub reports this PR as conflicting with main (22 commits behind). The files changed on both sides are chat-composer.tsx and composer-options-sheet.tsx. Most likely the cause is #2382 ("keep conversation controls usable on phones"), though I haven't confirmed that.

Could you rebase and push when you have a moment? I've read this PR and expect to approve it, with only non-blocking notes (mostly that ToggleGroupItem never gets data-toggle). I'd rather approve the rebased commit than the current one, because the last few stacked PRs moved after approval.

Fully or partially written by an AI agent.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio: give Toggle the phone icon rule and use its ghost variant in the composer

2 participants