Skip to content

feat(web): Appearance settings tab with font-size slider + live preview#2174

Open
akarabach wants to merge 16 commits intopingdotgg:mainfrom
akarabach:feat/appearance-settings-tab
Open

feat(web): Appearance settings tab with font-size slider + live preview#2174
akarabach wants to merge 16 commits intopingdotgg:mainfrom
akarabach:feat/appearance-settings-tab

Conversation

@akarabach
Copy link
Copy Markdown
Contributor

@akarabach akarabach commented Apr 18, 2026

Re-open of #2014 (closed, couldn't reopen)

Summary

Introduces a dedicated Appearance tab under settings and adds a new
Chat font size control with a slider and a live preview that mirrors
the real chat timeline.

  • Extract Theme + Time format from the General panel into a new
    /settings/appearance route (the original scope of feat(web): extract appearance settings to a separate tab #2014).
  • Add chatFontSize client setting (12–24 px, default 14) that cascades
    through a single --chat-font-size CSS variable into every
    text-chat-* tier and .chat-markdown rule.
  • Replace the original +/- stepper with a continuous slider
    (@base-ui/react/slider).
  • Refactor chat bubble outer shells into
    apps/web/src/components/chat/chatBubbleShells.tsx so the live
    timeline and the settings preview render identical bubbles.
  • New ChatFontSizePreview component renders a compact mock turn:
    work-log header (text-chat-mini), Bash/Edit tool rows
    (text-chat-xs / text-chat-xxs), a font-mono changed-file chip
    (text-chat-xxxs), a user bubble (text-chat-body with a
    text-chat-xs timestamp), and an assistant bubble rendering real
    ChatMarkdown (paragraph, list, inline code, fenced code block) with
    a text-chat-xxxs meta line — so every tier scales visibly as the
    slider moves.
  • Row order in the panel: Theme → Time format → Chat font size.

Test plan

  • bun fmt && bun lint && bun typecheck — all green.
  • Open Settings → Appearance.
    • Theme select still switches between System / Light / Dark.
    • Time format select still switches locale / 12h / 24h.
    • Drag the font-size slider from 12 → 24. Confirm the work-log
      header, tool heading, tool preview, changed-file chip, user
      bubble body, assistant markdown (paragraph + list + inline
      code + fenced ts code block), and meta line all scale.
    • Open a real chat thread; sizes in the preview match the live
      rendering at the same slider value.
    • Click Restore defaults → font size snaps to 14, slider
      follows, preview re-renders at default size.

Screenshots

Screenshot 2026-04-18 at 19 05 43 Screenshot 2026-04-18 at 19 05 56

Closes #2014 (re-opened as this PR).


Note

Medium Risk
Touches shared settings schema/persistence and applies a new CSS-variable-driven typography scale across the chat timeline, so regressions could affect rendering or settings decoding across web/desktop.

Overview
Adds a new Appearance settings section at /settings/appearance (with sidebar nav + route generation) and moves Theme and Time format controls out of General.

Introduces a persisted chatFontSize client setting (12–24px, default 14) in packages/contracts and wires it through web/desktop persistence and local API tests.

Applies chat font scaling by passing chatFontSize from ChatView into MessagesTimeline, setting --chat-font-size on the timeline wrapper, updating timeline text classes to text-chat-*, and adding [data-timeline-root] CSS rules (including markdown/code scaling). Includes a new Slider component, a live ChatAppearancePreview, and refactors work-log row rendering into reusable SimpleWorkEntryRow with updated tests.

Reviewed by Cursor Bugbot for commit 24cacb3. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add Appearance settings tab with chat font-size slider and live preview

  • Adds a new /settings/appearance route and AppearanceSettingsPanel with controls for theme, time format, and chat font size (range 12–24px, default 14).
  • Moves theme and time format controls out of the General settings panel into the new Appearance panel.
  • Font size is stored in ClientSettingsSchema as chatFontSize and applied via a --chat-font-size CSS variable scoped to [data-timeline-root] in index.css, scaling all chat text tiers uniformly.
  • Adds a ChatAppearancePreview component that renders a live preview of the chat timeline as the slider is adjusted.
  • Extracts work entry row rendering into a dedicated SimpleWorkEntryRow component and replaces hard-coded text size classes throughout MessagesTimeline with scalable text-chat-* utilities.

Macroscope summarized 24cacb3.

Move Theme and Time format settings from the General tab into a new
dedicated Appearance tab, improving settings discoverability.
Adds a new "Chat font size" appearance setting that proportionally
scales every text tier in the chat timeline — message bodies, work
log rows, metadata, code blocks, and inline markdown — via a single
CSS variable on the timeline wrapper. The control is a numeric
stepper (12–24 px, default 14) and participates in Restore defaults.
Pull the presentational outer shells for user and assistant chat bubbles
into a shared module (plus a `ChatFontSizeScope` helper that sets the
`--chat-font-size` CSS var and the `data-timeline-root` attribute). The
live timeline now renders through these shells, and the appearance
settings preview will too — so both stay pixel-identical as the styling
evolves.

No behavior change for the live chat; this is a lift-and-reuse.
Replaces the +/- stepper with a continuous slider (base-ui) and swaps
the two-bubble preview for a compact chat mock-up that exercises every
`text-chat-*` tier plus `ChatMarkdown`:

  - work-log section header (text-chat-mini)
  - Bash / Edit tool rows (text-chat-xs heading, text-chat-xxs preview)
  - changed-file chip (font-mono text-chat-xxxs)
  - user bubble body (text-chat-body) with text-chat-xs timestamp
  - assistant bubble rendering real ChatMarkdown (paragraph, bulleted
    list, inline code, fenced ts code block) with a text-chat-xxxs meta
    line

So every scale point the live chat uses visibly responds as the slider
moves. Also moves the Chat font size row to the bottom of the Appearance
tab so simpler rows (Theme, Time format) stay above the fold.

Preview lives in its own `ChatFontSizePreview` component to keep
`AppearanceSettingsPanel.tsx` focused on wiring.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 18, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cc1b6f0b-38c8-4564-abd8-f0eecc2db9e8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Apr 18, 2026
Comment thread apps/web/src/index.css
Comment thread apps/web/src/index.css
Comment thread apps/web/src/index.css
Comment thread apps/web/src/index.css
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented Apr 18, 2026

Approvability

Verdict: Needs human review

This PR introduces a new user-facing feature (Appearance settings tab with font-size customization and live preview), which warrants human review. Additionally, there are unresolved review comments identifying potential bugs: missing copy-button padding in code blocks, and the Restore defaults button not appearing on the new Appearance page.

You can customize Macroscope's approvability policy. Learn more.

The `chatBubbleShells` module introduced by the earlier refactor added
indirection without meaningful payoff:

- `UserMessageBubbleShell` / `AssistantMessageBubbleShell` are
  one-to-two-element wrappers over trivial Tailwind class strings;
  inlining them at the two call sites is cheaper than maintaining a
  shared module.
- `ChatFontSizeScope` was only used by the settings preview. The live
  timeline sets `--chat-font-size` on the LegendList wrapper and
  `data-timeline-root` on each row, so the single-element helper never
  fit there.
- The preview is decorative — pixel-perfect parity with the real
  bubble JSX isn't required, so cross-file coupling isn't earning us
  anything.

Delete the module, restore inline bubble JSX in MessagesTimeline, and
inline the wrapper + font-size scope in ChatFontSizePreview.
Comment thread apps/web/src/index.css
Comment thread apps/web/src/index.css

.markdown-file-link-tooltip-scroll {
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--border) 78%, transparent) transparent;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Codeblock copy-button padding-right accidentally removed

Medium Severity

The --chat-markdown-codeblock-copy-button-space variable and the padding-right rule on .chat-markdown .chat-markdown-codeblock pre were removed. This padding reserved space so code content wouldn't flow under the absolutely-positioned copy button. Without it, the copy button (which appears on hover) will overlap the rightmost code text in fenced code blocks.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 47d87c5. Configure here.

@akarabach akarabach closed this Apr 18, 2026
…ngs-tab

# Conflicts:
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	apps/web/src/localApi.test.ts
#	packages/contracts/src/settings.ts
@akarabach akarabach reopened this Apr 18, 2026
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Apr 18, 2026
Revert the identifier rename from the extraction — the component was
called `SimpleWorkEntryRow` in MessagesTimeline and should keep that
name in its new file too. Only the module path changes.
Today's merge from main silently dropped several CSS additions that
landed on main while our branch was in flight:

- `.chat-markdown-file-link` / `:hover` / `:focus-visible` /
  `-icon` / `-label` (from pingdotgg#1956 — the file-link UX redesign)
- `.markdown-file-link-tooltip-scroll` + its `::-webkit-scrollbar*`
  variants (same PR)
- `--chat-markdown-codeblock-copy-button-space` var, the padding-right
  adjustment that uses it, and the `cursor: pointer` on the copy
  button (from pingdotgg#1985)
- `@custom-variant wco` (from pingdotgg#1969)

`ChatMarkdown.tsx` still references all four file-link class names, so
their absence silently un-styled every inline file link the assistant
renders. Fix by rebasing our index.css additions on top of origin/main
so the only delta versus main is the additive Chat-font-size tier
block (`text-chat-*` utilities + `[data-timeline-root]` scaling).
Two focused additions guarding the chat font-size feature:

- `packages/contracts/src/settings.test.ts` — new file, 11 tests.
  Covers `ChatFontSize` schema bounds (min 12 / max 24, rejects below,
  above, non-integer, non-numeric), `ClientSettingsSchema` decoding
  default behaviour for `chatFontSize`, and confirms both
  `DEFAULT_CLIENT_SETTINGS` and `DEFAULT_UNIFIED_SETTINGS` carry the
  default value 14.
- `apps/web/src/components/chat/MessagesTimeline.test.tsx` — one new
  SSR-render test asserting that a `chatFontSize={20}` prop surfaces as
  `--chat-font-size:20px` on the list wrapper, pinning the contract
  between the prop and the CSS variable that drives every `text-chat-*`
  tier plus `.chat-markdown` scaling.
Comment thread apps/web/src/components/ui/slider.tsx
Replace our hand-rolled slider.tsx with the exact upstream
`@shadcn` `base-mira` component via:

  bunx --bun shadcn@latest add slider --overwrite

Differences vs the previous local copy are purely cosmetic and match
the current shadcn default:

  - Track: `rounded-md bg-muted` with `h-1` (was `rounded-full
    bg-muted-foreground/30 h-1.5`)
  - Thumb: `size-3 rounded-md` (was `size-4 rounded-full`)
  - Data-attr variants: `data-horizontal:` / `data-vertical:` short
    form (was `data-[orientation=horizontal]:` / `-vertical:`)

Keeps us aligned with upstream so future `shadcn@latest add slider`
diffs stay empty. The `_values` fallback for the edge case of a
scalar-number `value` / `defaultValue` is preserved verbatim from
upstream — we do not locally patch shadcn components.
Comment on lines +5 to +17
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: SliderPrimitive.Root.Props) {
const _values = Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Low ui/slider.tsx:5

When value is passed as a single number (e.g., value={50}), Array.isArray(value) returns false so the code falls through to check defaultValue or use [min, max]. This renders 2 thumbs when only 1 value exists, creating a mismatch between the UI and the actual slider state. Consider normalizing single values to an array before the length check.

-  const _values = Array.isArray(value)
-    ? value
-    : Array.isArray(defaultValue)
-      ? defaultValue
-      : [min, max];
+  const _values =
+    value !== undefined
+      ? Array.isArray(value)
+        ? value
+        : [value]
+      : defaultValue !== undefined
+        ? Array.isArray(defaultValue)
+          ? defaultValue
+          : [defaultValue]
+        : [min, max];
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/components/ui/slider.tsx around lines 5-17:

When `value` is passed as a single number (e.g., `value={50}`), `Array.isArray(value)` returns false so the code falls through to check `defaultValue` or use `[min, max]`. This renders 2 thumbs when only 1 value exists, creating a mismatch between the UI and the actual slider state. Consider normalizing single values to an array before the length check.

Evidence trail:
apps/web/src/components/ui/slider.tsx lines 12-16 show the _values logic. Base UI documentation at https://base-ui.com/react/components/slider confirms `value` prop type is `number | number[] | undefined`. When `value={50}` is passed, `Array.isArray(50)` is false, so `_values` falls through to `[min, max]` (length 2), but `SliderPrimitive.Root` receives `value={50}` (single number) creating a UI/state mismatch.

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit fbab76c. Configure here.

[
areProviderSettingsDirty,
isGitWritingModelDirty,
settings.chatFontSize,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restore defaults button missing on Appearance settings page

Medium Severity

The showRestoreDefaults guard in settings.tsx only checks for "/settings/general", so the "Restore defaults" button never appears on the new /settings/appearance route. Theme, Time format, and Chat font size were moved to Appearance, but users on that page have no way to trigger a bulk restore. Additionally, useSettingsRestore still lists those Appearance-only settings (Theme, Time format, Chat font size) in changedSettingLabels, so the General page's "Restore defaults" dialog can mention and reset settings that aren't even visible there.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fbab76c. Configure here.

@akarabach
Copy link
Copy Markdown
Contributor Author

@juliusmarminge Should be straightforward to review. The only thing I'm a bit unsure about is the custom chat font size classes like text-chat-xs , text-chat-3xs, etc - they feel a bit like clothing sizes

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

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant