feat(ui): add app typeface and a real dark-mode elevation ladder - #1754
feat(ui): add app typeface and a real dark-mode elevation ladder#1754giswqs wants to merge 1 commit into
Conversation
The theme was unmodified shadcn/ui: the stock slate + blue palette, and no font declared anywhere, so the app rendered in Segoe UI on Windows, Roboto on Linux and SF on macOS. Typography - Self-host IBM Plex Sans (UI) + IBM Plex Mono (coordinate/numeric readouts) via @fontsource, and drive them from a Tailwind v4 `@theme` block so both the `font-sans`/`font-mono` utilities and preflight pick them up. One matched superfamily, so the StatusBar readout and the surrounding UI share a voice. - The @font-face rules are imported from main.tsx, not index.css: Tailwind v4 resolves CSS @imports itself and inlines them before Vite sees them, so fontsource's relative `url(./files/*.woff2)` was never rewritten and no font file was emitted into dist/. That builds clean and 404s at runtime, silently falling back to system fonts. - Roman weight axis only, and no CDN — the desktop build must render offline under the Tauri CSP (`default-src 'self'`). Subsets are unicode-range scoped, so a Latin user downloads ~59 KB and the ar/fa/hi/ja/ka/ko/th/zh locales fetch nothing and fall through to the system stack per glyph. Elevation - Dark mode had --background, --card and --popover all at 222.2 84% 4.9%, and --border, --input, --muted, --secondary and --accent all at 217.2 32.6% 17.5% — five tokens, one colour. Panels, dialogs and menus were indistinguishable from the canvas, separated only by a 1px border. Replace with an explicit ladder: background 9% / card 12% / popover 15% / muted 20% / accent 22%. - Drop dark-mode saturation from 84% to ~22%: the old value was a strongly blue-tinted near-black that clashed with satellite imagery in the map view. - Split --input (a control boundary) from --border (decorative); both were identical. Contrast against their surfaces goes 1.23:1 -> 1.84:1 (light) and 1.37:1 -> 2.58:1 (dark). Still short of the 3:1 that WCAG 1.4.11 asks of control boundaries — closing that fully means a visibly heavier field outline, which is a design call left for follow-up. - Point Dialog at bg-popover; it used bg-background, which under the new ladder would put the highest-elevation surface on the lowest step. Map-floating panels stay on bg-background/90..95 — they are translucent scrims over imagery, where the base step is correct. - Desaturate the light neutrals from 40% to ~16% so the chrome reads grey next to the map rather than tinting blue, and set the dark --ring to --primary, as every non-default accent scheme already does. Every foreground/surface pair clears WCAG AA 4.5:1; the tightest is muted-foreground on accent at 4.82:1. Verified in the built app with Playwright in both themes: fonts report as loaded rather than fallback, and the three dark surfaces measure as distinct pixels (canvas 17,21,28 / toolbar 24,28,37 / menu and dialog 30,35,46).
📝 WalkthroughWalkthroughThe desktop app now uses IBM Plex fonts with system fallbacks. Shared light and dark theme tokens were updated for neutral surfaces, borders, inputs, and focus rings. Dialogs now use the popover background color. ChangesVisual styling
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. 🔧 Biome (2.5.6)apps/geolibre-desktop/src/index.cssFile contains syntax errors that prevent linting: Line 6: Tailwind-specific syntax is disabled.; Line 22: Tailwind-specific syntax is disabled. packages/ui/src/globals.cssFile contains syntax errors that prevent linting: Line 133: Tailwind-specific syntax is disabled.; Line 137: Tailwind-specific syntax is disabled. 🔧 ESLint
ESLint install failed: dependency version conflict. Check your lock file or package.json. 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 |
🔍 Cloudflare PR preview
|
There was a problem hiding this comment.
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 `@apps/geolibre-desktop/package.json`:
- Around line 34-35: Update the workspace lockfile to include the new
dependencies declared in apps/geolibre-desktop/package.json by running npm
install --package-lock-only from the workspace root, then commit the resulting
package-lock.json.
In `@packages/ui/src/globals.css`:
- Around line 27-30: Keep --border as the decorative token and introduce a
dedicated control-boundary token with at least 3:1 contrast in both themes;
update the MapLibre controls in maplibre-dggal.ts and other affected
interactive-border consumers to use it instead of --border or --input. Add
contrast checks covering normal, hover, and focus states, while preserving
existing decorative-edge usage.
🪄 Autofix
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: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 2a711dde-1a20-4f1c-9bab-a98b70eb23a1
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (5)
apps/geolibre-desktop/package.jsonapps/geolibre-desktop/src/index.cssapps/geolibre-desktop/src/main.tsxpackages/ui/src/components/dialog.tsxpackages/ui/src/globals.css
| /* --border is decorative (dividers, panel edges); --input is a control | ||
| boundary, so it is deliberately darker. See the note in .dark below. */ | ||
| --border: 220 15% 88%; | ||
| --input: 220 13% 76%; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Use a tested control-boundary token for interactive borders.
Line 29 and Line 70 define --border as a decorative token, but Lines 802-823 in packages/plugins/src/plugins/maplibre-dggal.ts use it for actual button borders. The PR also reports that --input remains below 3:1. If these borders are the only visible boundary, MapLibre controls and inputs have insufficient non-text contrast.
Keep --border for decorative edges. Use a dedicated control-boundary token that reaches at least 3:1 in both themes. Update the affected consumers and add contrast checks for normal, hover, and focus states.
Also applies to: 70-71
🤖 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/ui/src/globals.css` around lines 27 - 30, Keep --border as the
decorative token and introduce a dedicated control-boundary token with at least
3:1 contrast in both themes; update the MapLibre controls in maplibre-dggal.ts
and other affected interactive-border consumers to use it instead of --border or
--input. Add contrast checks covering normal, hover, and focus states, while
preserving existing decorative-edge usage.
There was a problem hiding this comment.
Good catch on the factual part, and I'm leaving this thread open rather than resolving it, because it's a scope call for a human.
Confirmed: maplibre-dggal.ts does use --border as a real control boundary, not just decoration — :808 (button) and :834 (select), both 1px solid hsl(var(--border)). That's worth recording for the follow-up.
But this PR improves those ratios, it doesn't regress them. The 3:1 gap is pre-existing:
| token / surface | before | after | 1.4.11 target |
|---|---|---|---|
--border on light bg |
1.23:1 | 1.34:1 | 3:1 |
--border on dark card |
1.37:1 | 1.62:1 | 3:1 |
--input on light bg |
1.23:1 | 1.84:1 | 3:1 |
--input on dark card |
1.37:1 | 2.58:1 | 3:1 |
So every affected boundary is measurably closer to the target than on main, and none is worse.
Why not close it here: reaching a true 3:1 in light mode needs roughly L 58% — a visibly heavy field outline that changes the look of every input, select and plugin control app-wide. That's a design decision, not a token tweak, and it's the reason the PR description calls it out as explicit follow-up rather than folding it in silently. Your own label agrees (🏗️ Heavy lift).
Leaving open for @giswqs to decide whether to take it now or as a separate PR.
Code reviewI reviewed this PR closely, including recomputing the WCAG contrast ratios cited in the code comments (e.g. Bugs: None found. Security: None found — fonts are self-hosted (no CDN), consistent with the Tauri CSP's Performance: None found — font subsetting/weight choices are deliberate and small (~59 KB for Latin), Quality: Minor/low-confidence: CLAUDE.md: No violations — CSP/tile-host allowlist untouched (correctly, since nothing external was added), no Overall this is a well-scoped, carefully verified change; I found nothing worth blocking on. |
🔍 GitHub Pages PR preview
|
What
The theme was unmodified shadcn/ui: the stock slate + blue palette, and no font declared anywhere — so the app rendered in Segoe UI on Windows, Roboto on Linux and SF on macOS. This is the first of a planned set of UI modernization changes (typography + elevation); shadow/motion/radius systematization is deliberately left for follow-up.
Typography
@fontsource, driven from a Tailwind v4@themeblock so both thefont-sans/font-monoutilities and preflight pick them up. One matched superfamily, so theStatusBarreadout and the surrounding UI share a voice.default-src 'self'). No CSP change was needed — fonts are emitted as same-origin files, and none are inlined asdata:URIs.One trap worth flagging for review
The
@font-facerules are imported frommain.tsx, notindex.css. Tailwind v4 resolves CSS@imports itself and inlines them before Vite sees them, so fontsource's relativeurl(./files/*.woff2)was never rewritten into an asset reference and zero font files were emitted intodist/. That build succeeds and 404s at runtime, silently falling back to system fonts — i.e. the change would have looked done and been a no-op. Importing from JS routes the CSS through Vite's asset pipeline.Size
Roman weight axis only, and subsets are
unicode-range-scoped:dist/The
ar/fa/hi/ja/ka/ko/th/zhlocales fetch zero Plex bytes — Plex has no coverage there, so they fall through to the system stack per glyph (which is why that fallback list is kept full and ordered).font-display: swap, so nothing blocks first paint.Elevation
Dark mode had
--background,--cardand--popoverall at222.2 84% 4.9%, and--border,--input,--muted,--secondaryand--accentall at217.2 32.6% 17.5%— five tokens, one colour. Panels, dialogs and menus were indistinguishable from the canvas, separated only by a 1px border.background 9%/card 12%/popover 15%/muted 20%/accent 22%(accent must read above popover, since menu items sit on it).--input(a control boundary) is split from--border(decorative); they were identical.Dialogmoves tobg-popover— it usedbg-background, which under the new ladder would put the highest-elevation surface on the lowest step. Map-floating panels intentionally stay onbg-background/90..95: they are translucent scrims over imagery, where the base step is correct.--ringnow matches--primary, as every non-default accent scheme already does.Accessibility
Every foreground/surface pair clears WCAG AA 4.5:1; the tightest is
muted-foregroundonaccentat 4.82:1.Input-border contrast improves from 1.23:1 → 1.84:1 (light) and 1.37:1 → 2.58:1 (dark). That is still short of the 3:1 WCAG 1.4.11 asks of control boundaries — both the old and new values fail it. Closing it fully needs a visibly heavier field outline (roughly
L 58%in light), which is a design decision rather than a token tweak, so it is left for follow-up rather than smuggled into this PR.Verification
npm run build— clean; font files confirmed emitted intodist/assets/.npm run test:frontend— 5473 pass, 0 fail.pre-commit run --files <changed>— all hooks pass.document.fonts.check()reports Plex Sans and Plex Mono as genuinely loaded (not fallback), and the three dark surfaces measure as distinct sampled pixels — canvas(17,21,28), toolbar(24,28,37), menu and dialog(30,35,46). Light mode verified white by pixel sample.Light mode is deliberately the smaller change here: it gets the typeface, desaturated neutrals and crisper borders, but no elevation ladder, because white-on-white surfaces there are load-bearing for the 42 map-floating panels. Light-mode depth is a shadow-scale problem, which is the follow-up.
Summary by CodeRabbit