Skip to content

feat(settings): dedicated Appearance tab + global font selection - #170

Merged
debpalash merged 2 commits into
mainfrom
feat/appearance-tab-font
May 30, 2026
Merged

feat(settings): dedicated Appearance tab + global font selection#170
debpalash merged 2 commits into
mainfrom
feat/appearance-tab-font

Conversation

@debpalash

@debpalash debpalash commented May 30, 2026

Copy link
Copy Markdown
Owner

Two requested Settings changes:

  1. Appearance is its own tab — moved the UI-scale + color-theme panel out of the Credentials tab into a dedicated Appearance tab (Palette icon).
  2. Global font selection — a new persisted preference in the Appearance panel. Picks from system-safe stacks (Inter default / System / Serif / Monospace / Rounded / Readable) and applies app-wide by overriding the --font-sans CSS variable on the root (the same var the whole UI's font-family already uses — mirrors how setTheme sets data-theme). Persisted via zustand + re-applied on launch in App.jsx's rehydrate effect.

No web-font downloads (all stacks fall back to system fonts). Verified: typecheck:ci ✓, test:legacy 36 ✓, vitest 94/94 (incl. new AppearancePanel test) ✓, build ✓, CJK guard ✓.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Introduced a dedicated Appearance tab in Settings for visual customization.
    • Added font selection feature allowing users to choose their preferred font from available options.
    • Font preferences are automatically saved and restored on app reload.
  • Tests

    • Added test suite for font selection functionality.

Review Change Stack

debpalash and others added 2 commits May 30, 2026 17:10
Add a persisted, system-safe global font selection. A new `font` pref in
the prefs slice overrides the root `--font-sans` CSS variable (the whole
UI uses `font-family: var(--font-sans)`), so the choice applies app-wide;
`default` removes the override and falls back to the :root Inter stack.

- prefsSlice: FontId type, FONT_OPTIONS/FONT_STACKS tables, font + setFont
- store/index: persist `font` via partialize; re-export font tables/type
- App.jsx: re-apply persisted font on launch in the rehydrate effect
- AppearancePanel: Font row (Select) next to UI-scale and color-theme
- AppearancePanel.test: covers render, selection, and default reset

All stacks are system fonts (no web-font downloads) so behavior is
identical offline across macOS/Windows/Linux.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Promote the Appearance panel out of the Credentials tab into a dedicated
top-level tab (with the Palette icon), placed before Credentials. Add the
English "appearance" label so the tab renders via t(`settings.${id}`).
Remove the AppearancePanel render (and its stale comment) from
CredentialsTab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR adds a global font preference feature allowing users to select and persist a font choice across sessions. It defines font types and CSS stacks in the store, applies the selected font via CSS variable during app bootstrap, surfaces font selection in a new Appearance settings tab, and includes tests validating the selection and CSS override behavior.

Changes

Global Font Preference Feature

Layer / File(s) Summary
Font preference store definition
frontend/src/store/prefsSlice.ts, frontend/src/store/index.ts
Defines FontId type, FONT_OPTIONS list, and FONT_STACKS lookup mapping fonts to CSS font-family values; extends PrefsSlice with font state initialized to 'default' and setFont setter that updates the --font-sans CSS custom property on the document root; configures localStorage persistence for the font preference.
App-level font application
frontend/src/App.jsx
App imports FONT_STACKS and reads the persisted font value during store rehydration; extends the rehydration effect to apply the selected font stack as a CSS variable override, or remove the override for the default font, with font included in the effect dependency array.
Settings UI and font selection
frontend/src/pages/Settings.jsx, frontend/src/components/settings/AppearancePanel.jsx, frontend/src/i18n/locales/en.json
Registers a new Appearance settings tab with the Palette icon; moves AppearancePanel from the Credentials tab to render conditionally when the Appearance tab is active; AppearancePanel imports FONT_OPTIONS and store hooks, renders a Select dropdown bound to the current font value, updates it via setFont on change, and includes test ID for testing; adds i18n translation for the Appearance label.
Font selection test coverage
frontend/src/components/settings/AppearancePanel.test.jsx
Vitest suite resets font state and clears CSS overrides before each test for determinism; verifies the font Select renders all options from FONT_OPTIONS, defaults to 'default', and properly updates both store state and CSS variable when selecting a non-default font or reverting to default.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant SettingsUI as Settings / AppearancePanel
    participant Store as Zustand Store
    participant DOM as DOM CSS
    
    User->>SettingsUI: Select font from dropdown
    SettingsUI->>Store: Call setFont(newFont)
    Store->>DOM: Update --font-sans CSS variable
    Store->>Store: Persist font to localStorage
    SettingsUI->>SettingsUI: Re-render with updated font value
    
    Note over Store,DOM: On app rehydration
    Store->>Store: Load persisted font from localStorage
    DOM->>DOM: Apply --font-sans override at bootstrap
Loading

Estimated Code Review Effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly Related PRs

  • debpalash/OmniVoice-Studio#108: Modifies the Settings > Appearance UI components (AppearancePanel, Settings.jsx), which this PR extends with a new global font selector and dedicated Appearance tab.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the key changes and provides implementation details, but is missing several required template sections like Type, Testing, Checklist, and Screenshots. Complete the missing template sections: select a Type checkbox, describe testing approach, verify checklist items, and add screenshots if applicable.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the two main changes: moving Appearance to its own tab and adding global font selection.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/appearance-tab-font

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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.

@greptile-apps

greptile-apps Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR promotes the Appearance panel out of the Credentials tab into its own dedicated Settings tab, and adds a global font-selection preference backed by Zustand persist and applied via the --font-sans CSS custom property on the document root.

  • The font hydration in App.jsx correctly mirrors the existing theme/locale rehydration pattern, and the store's partialize object is updated to persist font alongside other user prefs.
  • Three font-related issues are flagged: the \"Font\" label (and existing sibling labels) are hardcoded English strings that skip the project's i18n t() layer; the rounded stack silently falls back to system-ui, sans-serif on Windows/Linux where neither SF Pro Rounded nor Nunito/Quicksand are typically installed; and re-export statements in store/index.ts are interleaved between import declarations.

Confidence Score: 4/5

Safe to merge; all three findings are non-blocking style or UX-clarity concerns with no runtime failures or data loss paths.

The font hydration logic is correct and mirrors the existing theme/locale pattern. The "rounded" stack degrades silently on Windows/Linux but doesn't crash or corrupt anything. The hardcoded "Font" label violates the project's i18n convention but has no runtime impact. The interleaved export in index.ts is cosmetic.

frontend/src/store/prefsSlice.ts (rounded font stack cross-platform behaviour) and frontend/src/components/settings/AppearancePanel.jsx (hardcoded i18n strings).

Important Files Changed

Filename Overview
frontend/src/store/prefsSlice.ts Adds FontId type, FONT_OPTIONS, FONT_STACKS constants, and font/setFont to the prefs slice; the "rounded" stack silently degrades to system-ui on Windows/Linux where SF Pro Rounded, Nunito, and Quicksand are all absent.
frontend/src/store/index.ts Re-exports FontId, FONT_OPTIONS, FONT_STACKS and adds font to the persist partialize list; export statements are interleaved with imports (style issue only).
frontend/src/components/settings/AppearancePanel.jsx Adds the font Select row wired to the store; the new "Font" label is a hardcoded string, violating the project's i18n hard rule (existing labels in this panel share the same pre-existing gap).
frontend/src/App.jsx Subscribes to the font preference and re-applies --font-sans on rehydration; mirrors the existing theme/locale hydration pattern correctly.
frontend/src/pages/Settings.jsx Adds the Appearance tab to TAB_DEFS and renders AppearancePanel for it; removes AppearancePanel from inside CredentialsTab cleanly.
frontend/src/components/settings/AppearancePanel.test.jsx New test file with three focused cases: options render, non-default font sets --font-sans, reverting to default removes it. Coverage looks solid.
frontend/src/i18n/locales/en.json Adds the "appearance" tab label key; omits i18n keys for the panel's internal labels ("Font", etc.) which are hardcoded in the component.

Sequence Diagram

sequenceDiagram
    participant LS as localStorage
    participant ZS as Zustand Store
    participant AJ as App.jsx (useEffect)
    participant DOM as document.documentElement
    participant AP as AppearancePanel

    Note over LS,DOM: App launch / rehydration
    LS->>ZS: persist middleware rehydrates font (or default)
    ZS->>AJ: font state change triggers effect
    AJ->>DOM: setProperty(--font-sans, stack) or removeProperty

    Note over AP,DOM: User changes font
    AP->>ZS: setFont(id)
    ZS->>ZS: "set({ font: id })"
    ZS->>DOM: setProperty(--font-sans, stack) or removeProperty
    ZS->>LS: persist middleware writes font to localStorage
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "feat(settings): move Appearance into its..." | Re-trigger Greptile

</div>

<div className="appearance-panel__row">
<span className="appearance-panel__label">Font</span>

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.

P2 Hardcoded UI string violates i18n hard rule

CLAUDE.md states: "All UI strings go through i18n (t('...') keys in locales/*.json)". The new "Font" label is a hardcoded English string — it should be a t('appearance.font') call backed by a key in en.json. The existing "UI scale", "Color theme", and "Appearance" labels on this same panel are also hardcoded, suggesting this is a pre-existing gap rather than something unique to this PR, but the new string still introduces another violation. The en.json hunk only adds "appearance" for the tab label and omits a font key entirely.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

system: '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif',
serif: 'Georgia, "Times New Roman", serif',
mono: 'ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace',
rounded: '"SF Pro Rounded", "Nunito", "Quicksand", system-ui, sans-serif',

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.

P2 "Rounded" font silently degrades on Windows/Linux

The rounded stack is "SF Pro Rounded", "Nunito", "Quicksand", system-ui, sans-serif. SF Pro Rounded is macOS/iOS only. Nunito and Quicksand are Google web fonts — they won't be present on a fresh Windows or Linux install without a prior web-font download. The effective fallback on most non-Apple machines is system-ui, sans-serif, which has no rounded appearance. A user who selects "Rounded" on Windows will see no visual difference from the default Inter stack, making the option silently confusing. Adding a parenthetical like "Rounded (macOS)" in the label, or replacing the font-family entry with a more widely available rounded font (e.g. "Century Gothic" which ships on Windows), would make the cross-platform behaviour predictable.

Fix in Claude Code

Comment on lines +20 to 26
import type { PrefsSlice, FontId } from './prefsSlice';
import { createPrefsSlice, FONT_OPTIONS, FONT_STACKS } from './prefsSlice';

// Re-export font preference tables so panels can import from the store root.
export type { FontId };
export { FONT_OPTIONS, FONT_STACKS };
import type { GlossarySlice } from './glossarySlice';

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.

P2 The re-export statements are sandwiched between import declarations, which is non-idiomatic and can confuse bundlers and linters that expect all imports to be grouped before other module-level statements. Moving the exports after all imports is the standard ES module pattern.

Suggested change
import type { PrefsSlice, FontId } from './prefsSlice';
import { createPrefsSlice, FONT_OPTIONS, FONT_STACKS } from './prefsSlice';
// Re-export font preference tables so panels can import from the store root.
export type { FontId };
export { FONT_OPTIONS, FONT_STACKS };
import type { GlossarySlice } from './glossarySlice';
import type { PrefsSlice, FontId } from './prefsSlice';
import { createPrefsSlice, FONT_OPTIONS, FONT_STACKS } from './prefsSlice';
import type { GlossarySlice } from './glossarySlice';
// Re-export font preference tables so panels can import from the store root.
export type { FontId };
export { FONT_OPTIONS, FONT_STACKS };

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with 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.

Inline comments:
In `@frontend/src/components/settings/AppearancePanel.jsx`:
- Around line 72-79: The "Font" label and aria-label on the Select in
AppearancePanel.jsx are hardcoded; replace them with i18n lookups (e.g. use
t('appearance.font') for both the visible label and aria-label) and ensure the
component uses the existing translation hook (e.g. the t function already used
elsewhere in this component) and that corresponding keys are added to locales
JSON (appearance.font). Update the span text and the Select aria-label to call
t(...) rather than the literal "Font".

In `@frontend/src/store/prefsSlice.ts`:
- Around line 23-30: FONT_OPTIONS currently embeds hardcoded English labels
which prevents localization; replace the label strings with i18n keys and update
consumers to call the translation function at render time. Specifically, change
FONT_OPTIONS (and any type FontId if needed) to store translation keys like
'prefs.fonts.inter', 'prefs.fonts.system', etc., then in the settings UI
component(s) that consume FONT_OPTIONS call the i18n t(...) function (e.g.,
t(option.labelKey)) instead of using the raw label; ensure locales/*.json
include those keys and update any tests that assert exact label text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1fadba09-a3d2-44d7-a717-a9110cb9f47b

📥 Commits

Reviewing files that changed from the base of the PR and between e3d7815 and 11538b8.

📒 Files selected for processing (7)
  • frontend/src/App.jsx
  • frontend/src/components/settings/AppearancePanel.jsx
  • frontend/src/components/settings/AppearancePanel.test.jsx
  • frontend/src/i18n/locales/en.json
  • frontend/src/pages/Settings.jsx
  • frontend/src/store/index.ts
  • frontend/src/store/prefsSlice.ts

Comment on lines +72 to +79
<span className="appearance-panel__label">Font</span>
<Select
size="xs"
value={font}
onChange={(e) => setFont(e.target.value)}
data-testid="appearance-font-select"
aria-label="Font"
>

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the new font field label/aria text via t(...).

The newly added font control introduces hardcoded user-facing text ("Font" and aria-label="Font"). Please route these through i18n keys.

Suggested change
+import { useTranslation } from 'react-i18next';

 export default function AppearancePanel() {
+  const { t } = useTranslation();
   // ...
-        <span className="appearance-panel__label">Font</span>
+        <span className="appearance-panel__label">{t('appearance.font_label')}</span>
         <Select
           size="xs"
           value={font}
           onChange={(e) => setFont(e.target.value)}
           data-testid="appearance-font-select"
-          aria-label="Font"
+          aria-label={t('appearance.font_label')}
         >

As per coding guidelines, "All user-facing text in the UI must go through the i18n translation layer using t('...') keys in locales/*.json files, never hardcode non-English text".

📝 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.

Suggested change
<span className="appearance-panel__label">Font</span>
<Select
size="xs"
value={font}
onChange={(e) => setFont(e.target.value)}
data-testid="appearance-font-select"
aria-label="Font"
>
<span className="appearance-panel__label">{t('appearance.font_label')}</span>
<Select
size="xs"
value={font}
onChange={(e) => setFont(e.target.value)}
data-testid="appearance-font-select"
aria-label={t('appearance.font_label')}
>
🤖 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 `@frontend/src/components/settings/AppearancePanel.jsx` around lines 72 - 79,
The "Font" label and aria-label on the Select in AppearancePanel.jsx are
hardcoded; replace them with i18n lookups (e.g. use t('appearance.font') for
both the visible label and aria-label) and ensure the component uses the
existing translation hook (e.g. the t function already used elsewhere in this
component) and that corresponding keys are added to locales JSON
(appearance.font). Update the span text and the Select aria-label to call t(...)
rather than the literal "Font".

Comment on lines +23 to +30
export const FONT_OPTIONS: { id: FontId; label: string }[] = [
{ id: 'default', label: 'Inter (default)' },
{ id: 'system', label: 'System' },
{ id: 'serif', label: 'Serif' },
{ id: 'mono', label: 'Monospace' },
{ id: 'rounded', label: 'Rounded' },
{ id: 'readable', label: 'Readable' },
];

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move font option labels to i18n keys instead of hardcoded strings.

FONT_OPTIONS currently stores English UI labels directly. Since these labels are rendered in the settings UI, this blocks localization for the new font selector.

Suggested refactor
-export const FONT_OPTIONS: { id: FontId; label: string }[] = [
-  { id: 'default',  label: 'Inter (default)' },
-  { id: 'system',   label: 'System' },
-  { id: 'serif',    label: 'Serif' },
-  { id: 'mono',     label: 'Monospace' },
-  { id: 'rounded',  label: 'Rounded' },
-  { id: 'readable', label: 'Readable' },
+export const FONT_OPTIONS: { id: FontId; labelKey: string }[] = [
+  { id: 'default',  labelKey: 'appearance.font.default' },
+  { id: 'system',   labelKey: 'appearance.font.system' },
+  { id: 'serif',    labelKey: 'appearance.font.serif' },
+  { id: 'mono',     labelKey: 'appearance.font.mono' },
+  { id: 'rounded',  labelKey: 'appearance.font.rounded' },
+  { id: 'readable', labelKey: 'appearance.font.readable' },
 ];

As per coding guidelines, "All user-facing text in the UI must go through the i18n translation layer using t('...') keys in locales/*.json files, never hardcode non-English text".

🤖 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 `@frontend/src/store/prefsSlice.ts` around lines 23 - 30, FONT_OPTIONS
currently embeds hardcoded English labels which prevents localization; replace
the label strings with i18n keys and update consumers to call the translation
function at render time. Specifically, change FONT_OPTIONS (and any type FontId
if needed) to store translation keys like 'prefs.fonts.inter',
'prefs.fonts.system', etc., then in the settings UI component(s) that consume
FONT_OPTIONS call the i18n t(...) function (e.g., t(option.labelKey)) instead of
using the raw label; ensure locales/*.json include those keys and update any
tests that assert exact label text.

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.

1 participant