feat(ui): add app typeface and a real dark-mode elevation ladder - #1754
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).
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 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 Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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)packages/ui/src/globals.cssFile contains syntax errors that prevent linting: Line 143: Tailwind-specific syntax is disabled.; Line 147: 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
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
|
- Point DialogClose's ring-offset at --popover instead of --background. The
previous commit moved DialogContent to bg-popover, which left the close
button declaring a ring offset colour for a surface it no longer sits on.
Inert today — no ring-offset width utility is applied, so the offset colour
never paints — but wrong the moment one is added. Verified the
`ring-offset-popover` utility generates:
`.ring-offset-popover{--tw-ring-offset-color:hsl(var(--popover))}`.
|
Addressed the one item from the Claude review summary in 3ba72c8.
.ring-offset-popover{--tw-ring-offset-color:hsl(var(--popover))}Also checked the |
Code reviewBugs
Security
Performance
Quality
CLAUDE.md
|
- Correct the --ring comment in .dark. It claimed every non-default accent scheme already pairs ring with primary; emerald did not (--ring 142.4 71.8% 29.2% vs --primary 142.1 70.6% 45.3%). Violet, rose and amber do. - Pair emerald's dark --ring with its --primary. Checking the above surfaced a real side effect of this PR: against the old near-black canvas emerald's darker ring cleared the 3:1 focus-indicator floor at 3.98:1, but the lifted surfaces drop it to 3.65:1 on --background and 3.13:1 on --popover — passing by ~4%, so any later surface tweak would silently break it. Pairing with primary restores headroom (8.04:1 on --background) and makes the ring == primary invariant actually hold across all four schemes.
Code reviewI reviewed the full diff (font self-hosting, Tailwind Bugs: None found. The Security: No CSP change is needed, as claimed — Performance: None — font subsetting and Quality: The CLAUDE.md adherence: Consistent with repo conventions — no new external tile/font host was added (self-hosted via No inline comments were posted since no defects were identified. |
…ius (#1764) * feat(ui): codify map-floating glass, theme-aware shadows, tighter radius Follow-up to #1754. Three changes, all working through tokens rather than per-component edits. Map-floating chrome ("instrument glass") - The translucent-blur treatment was already in use across 11 components, but copy-pasted with drifted values: bg-background/90 vs /95, backdrop-blur-sm vs -md vs bare backdrop-blur. Replace all of them with one `.map-glass` class so the look has a single home. - Encodes the rule the app was already half-following: chrome docked BESIDE the map (toolbar, status bar, side panels) is opaque; panels floating OVER the map are translucent, because translucency is what signals "this sits above your data". - Deliberately a plain unlayered class, not an `@utility`. It is applied next to components that already emit a bg-* utility (MapGrid's Button variant="outline" carries bg-background); as a utility both would sit in the same cascade layer and the winner would depend on Tailwind's internal sort rather than intent. - Added to MapGrid's pane controls, which float over the map but never had it. Deliberately NOT added to PixelTimeSeriesControl or the dialogs: that panel is content (a chart) rather than chrome, and dialogs sit on a dimmed overlay, not on the map. DesktopShell's file-drop scrim keeps its own bg-background/70, being a full-cover dim rather than a panel. - Falls back to 97% opaque via @supports where backdrop-filter is unavailable; at 85% with no blur, panels over live imagery are unreadable. Theme-aware shadow scale - Tailwind v4 bakes a 10%-black colour into its shadow utilities, so all ~65 shadow sites in the app were effectively invisible in dark mode and elevation rested entirely on the surface ladder from #1754. Redefine --shadow-sm/md/lg/xl against inheritable --elev-penumbra/--elev-umbra tokens that switch per theme. Fixes every existing site without touching a component. - Those tokens are plain custom properties precisely so they inherit: Tailwind registers --tw-shadow-color with `inherits: false`, so retinting shadows by setting that on :root silently does nothing. - Colours are written bare; Tailwind adds the var(--tw-shadow-color, …) wrapper itself, so `shadow-<color>` overrides keep working. Other - --radius 0.5rem -> 0.375rem. A dense tool UI reads sharper, and the derived md/sm steps stay positive. This is the most subjective change here. - One global prefers-reduced-motion guard. Two components respected the preference against ~53 transition sites; motion is reduced to a near-instant cross-fade rather than removed, since an abrupt swap is its own problem. Verified in the built app with Playwright: 5 .map-glass panels render as rgba(17,21,28,0.85) with blur(12px) saturate(1.4) and radius 4px, shadows resolve to rgba(0,0,0,0.4)/rgba(0,0,0,0.55) in dark instead of the old invisible 10% black, and the toolbar stays rgb(255,255,255) opaque in light. Not verified: backdrop-filter cost on real GPU compositing. The test browser runs SwiftShader, where blur on/off medians were identical (66.8 vs 66.7ms) at a software-bound ~15fps, so the measurement says nothing useful. Note the blur radius went from 4px to 12px plus a saturate(), so GPU cost is higher than on main — worth a look on real hardware. * Address Claude review feedback - Move .map-glass into Tailwind's utilities layer (@Utility) instead of leaving it unlayered. Unlayered CSS beats every layered declaration for the same property, and cascade-layer precedence resolves before specificity, so the unlayered rule also beat `hover:bg-accent` — killing the hover fill on MapGrid's two pane buttons and the CollaborationStatusBadge toggle. Confirmed dead in the built app before the fix (idle == hovered on all three), and restored after (idle rgba(17,21,28,0.85) -> hover rgb(46,53,66) = --accent). The @supports fallback is nested inside the utility so it stays in the layer; hoisting it out would reintroduce the same bug where backdrop-filter is unsupported. - Switch MapGrid's layers trigger from Button variant="outline" to "ghost". Inside the utilities layer, outline's own bg-background ties with map-glass on specificity and won on order, so that button rendered opaque beside its glass siblings. ghost sets no base background and still supplies hover:bg-accent; the border is restored explicitly. (bg-transparent was tried first and is worse — tailwind-merge drops bg-background, but bg-transparent then beats map-glass and the button renders fully see-through.) - Add --shadow-xs and bare --shadow to the elevation scale. Only sm/md/lg/xl were redefined, but shadow-xs is what @geolibre/ui's form primitives use (input, textarea, select, color-field, color-ramp-select) plus 7 app sites, so every input, textarea, select and colour field in the app was still on the invisible-in-dark 10% black this scale exists to fix. Verified: a real input's shadow now resolves to rgba(0,0,0,0.4) in dark mode. shadow-2xl and shadow-inner are unused and left out, with a note to extend if that changes. * Address Claude review feedback - Exempt indeterminate progress indicators from the reduced-motion guard. The blanket `animation-iteration-count: 1` gave every spinner a single 0.01ms iteration and then froze it mid-rotation, so during a long raster/segmentation/ conversion job the UI would read as hung — a worse outcome for the same user than the abrupt transitions the guard exists to fix. 70 animate-spin sites and 6 animate-pulse were affected. Slowed rather than stopped (1.5s / 3s, still infinite): the preference targets large vestibular-triggering motion, which a small in-place spinner is not, so this keeps the "still working" signal while calming it. Verified under emulated prefers-reduced-motion: animate-spin resolves to 1.5s/infinite and animate-pulse to 3s/infinite, while a decorative `animate-in fade-in-0` is still suppressed to 0.00001s/1. - Bump Dialog from shadow-lg to shadow-xl. The scale documents shadow-xl as "modal dialog", but the shared primitive every modal renders through was still on shadow-lg — the same step as "panel floating over the map" — so the ladder's one-step-per-surface invariant was false for the one component that most literally is a modal dialog. Bumping the primitive makes the documented mapping true rather than aspirational.
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