From e34d240b4d14de72e46ba45efe14977cdd9820af Mon Sep 17 00:00:00 2001 From: giswqs Date: Fri, 7 Aug 2026 01:09:42 -0400 Subject: [PATCH 1/3] feat(ui): add app typeface and a real dark-mode elevation ladder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- apps/geolibre-desktop/package.json | 2 + apps/geolibre-desktop/src/index.css | 23 ++++++++++++ apps/geolibre-desktop/src/main.tsx | 10 +++++ package-lock.json | 20 ++++++++++ packages/ui/src/components/dialog.tsx | 2 +- packages/ui/src/globals.css | 53 ++++++++++++++++++++------- 6 files changed, 95 insertions(+), 15 deletions(-) diff --git a/apps/geolibre-desktop/package.json b/apps/geolibre-desktop/package.json index ddef308329..67494d68dc 100644 --- a/apps/geolibre-desktop/package.json +++ b/apps/geolibre-desktop/package.json @@ -31,6 +31,8 @@ "@dvt3d/maplibre-three-plugin": "^1.7.1", "@electric-sql/pglite": "^0.5.4", "@electric-sql/pglite-postgis": "^0.2.4", + "@fontsource-variable/ibm-plex-sans": "^5.3.0", + "@fontsource/ibm-plex-mono": "^5.3.0", "@geolibre/core": "*", "@geolibre/embed": "*", "@geolibre/map": "*", diff --git a/apps/geolibre-desktop/src/index.css b/apps/geolibre-desktop/src/index.css index 4483c6a387..800505c3b8 100644 --- a/apps/geolibre-desktop/src/index.css +++ b/apps/geolibre-desktop/src/index.css @@ -1,9 +1,32 @@ @import "@geolibre/ui/globals.css"; + /* Supplies the custom-color/border-radius theme that @geolibre/ui's globals.css relies on via @apply (background, border, etc.). globals.css is intentionally config-agnostic; the consuming app provides the Tailwind config here. */ @config "../tailwind.config.js"; +/* App typeface: IBM Plex Sans for UI, IBM Plex Mono for the numeric/coordinate + readouts (StatusBar, code, feature IDs). One matched superfamily, so the mono + readouts and the surrounding UI share a voice. The @font-face rules are + loaded from main.tsx — see the note there for why they cannot be @imported + here. Self-hosted rather than CDN-loaded: the desktop build must render + offline, and the Tauri CSP is `default-src 'self'`. + + This block drives both the `font-sans`/`font-mono` utilities and Tailwind's + preflight default, so no `font-family` on is needed. + + Plex covers latin, latin-ext, cyrillic, greek and vietnamese only. The + locales it does NOT cover (ar, fa, hi, ja, ka, ko, th, zh) rely on per-glyph + fallback to the system stack below — which is why that stack must stay a + full, ordered list rather than a bare `sans-serif`. */ +@theme { + --font-sans: + "IBM Plex Sans Variable", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, + "Noto Sans", "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + --font-mono: + "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; +} + html, body, #root { diff --git a/apps/geolibre-desktop/src/main.tsx b/apps/geolibre-desktop/src/main.tsx index bf94071561..e557c2dc7b 100644 --- a/apps/geolibre-desktop/src/main.tsx +++ b/apps/geolibre-desktop/src/main.tsx @@ -1,6 +1,16 @@ import "./lib/symbol-dispose-polyfill"; import React from "react"; import ReactDOM from "react-dom/client"; +/* App typeface — see the --font-sans/--font-mono note in index.css. + These must be imported from JS, not via `@import` in index.css: Tailwind v4 + resolves CSS @imports itself and inlines them before Vite sees them, so the + relative `url(./files/*.woff2)` in fontsource's CSS is never rewritten into + an asset reference and no font file is emitted into dist/. The result builds + clean and 404s at runtime, silently falling back to system fonts. Importing + from JS routes the CSS through Vite's asset pipeline instead. */ +import "@fontsource-variable/ibm-plex-sans/wght.css"; +import "@fontsource/ibm-plex-mono/400.css"; +import "@fontsource/ibm-plex-mono/700.css"; import "@geoman-io/maplibre-geoman-free/dist/maplibre-geoman.css"; import "@maplibre/maplibre-gl-directions/dist/style.css"; import "maplibre-gl-3d-tiles/style.css"; diff --git a/package-lock.json b/package-lock.json index 56ec9704d3..00072aaebe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,8 @@ "@dvt3d/maplibre-three-plugin": "^1.7.1", "@electric-sql/pglite": "^0.5.4", "@electric-sql/pglite-postgis": "^0.2.4", + "@fontsource-variable/ibm-plex-sans": "^5.3.0", + "@fontsource/ibm-plex-mono": "^5.3.0", "@geolibre/core": "*", "@geolibre/embed": "*", "@geolibre/map": "*", @@ -3876,6 +3878,24 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@fontsource-variable/ibm-plex-sans": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/ibm-plex-sans/-/ibm-plex-sans-5.3.0.tgz", + "integrity": "sha512-agG8tXFEo0hD9+J7npa4vbbWult52eMLVaQ6WQRlhs/iCAojrMAoejru85W9HTVXHfyUj96KM7gp/KGAS87XaQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/ibm-plex-mono": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz", + "integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@geoarrow/deck.gl-geoarrow": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@geoarrow/deck.gl-geoarrow/-/deck.gl-geoarrow-0.4.1.tgz", diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index 58a7184034..51d7183d6d 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -37,7 +37,7 @@ export const DialogContent = React.forwardRef< Date: Fri, 7 Aug 2026 07:43:31 -0400 Subject: [PATCH 2/3] Address Claude review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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))}`. --- packages/ui/src/components/dialog.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index 51d7183d6d..61c7620992 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -45,7 +45,10 @@ export const DialogContent = React.forwardRef<
{children}
- + {/* ring-offset matches the popover surface this sits on, not --background. + Inert today (no ring-offset width utility is applied, so the offset + colour never paints) but wrong the moment one is added. */} + Close From cd6532767b9bd9e2cd5d0319b58855ebba4cdd3c Mon Sep 17 00:00:00 2001 From: giswqs Date: Fri, 7 Aug 2026 07:53:14 -0400 Subject: [PATCH 3/3] Address Claude review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- packages/ui/src/globals.css | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/globals.css b/packages/ui/src/globals.css index 67d3d33759..850f4e8d83 100644 --- a/packages/ui/src/globals.css +++ b/packages/ui/src/globals.css @@ -69,8 +69,11 @@ --destructive-foreground: 210 40% 98%; --border: 220 14% 26%; --input: 220 12% 38%; - /* Matches --primary, as every non-default accent scheme below already does, - so the focus ring stays visible against the lifted surfaces. */ + /* Matches --primary, so the focus ring stays visible against the lifted + surfaces (4.98:1 on --background, 4.64:1 on --card). The violet, rose and + amber schemes below already pair ring with primary this way; emerald was + the one exception and is brought into line there, since the lifted + surfaces left its darker ring almost no headroom. */ --ring: 217.2 91.2% 59.8%; } @@ -95,10 +98,17 @@ --primary-foreground: 355.7 100% 97.3%; --ring: 142.1 76.2% 36.3%; } + /* Unlike violet/rose/amber, emerald's dark ring used to sit well below its + primary (142.4 71.8% 29.2%). Against the old near-black canvas that still + cleared the 3:1 focus-indicator floor at 3.98:1, but the lifted surfaces in + .dark drop it to 3.65:1 on --background and 3.13:1 on --popover — passing + by ~4%, so any later surface tweak would break it. Pairing ring with + primary restores the headroom (8.04:1 on --background) and makes the + ring == primary invariant hold across every scheme. */ [data-theme="emerald"].dark { --primary: 142.1 70.6% 45.3%; --primary-foreground: 144.9 80.4% 10%; - --ring: 142.4 71.8% 29.2%; + --ring: 142.1 70.6% 45.3%; } [data-theme="rose"] {