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..61c7620992 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -37,7 +37,7 @@ 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 diff --git a/packages/ui/src/globals.css b/packages/ui/src/globals.css index 477278250b..850f4e8d83 100644 --- a/packages/ui/src/globals.css +++ b/packages/ui/src/globals.css @@ -11,42 +11,70 @@ --popover-foreground: 222.2 84% 4.9%; --primary: 221.2 83.2% 53.3%; --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; + /* Neutrals sit at ~16% saturation rather than the stock 40%, so the chrome + reads as grey next to the map instead of tinting blue. --accent is one + step darker than --muted so hover fills register on white surfaces. */ + --secondary: 220 16% 96%; --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; + --muted: 220 16% 96%; /* Darkened from 46.9% L so secondary text meets WCAG AA (4.5:1) even on the muted/secondary backgrounds it is paired with (e.g. kbd badges). */ --muted-foreground: 215.4 16.3% 40%; - --accent: 210 40% 96.1%; + --accent: 220 16% 94%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; + /* --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%; --ring: 221.2 83.2% 53.3%; --radius: 0.5rem; } + /* Dark mode is built as an explicit elevation ladder. Previously --background, + --card and --popover were all 222.2 84% 4.9%, and --border, --input, + --muted, --secondary and --accent were all 217.2 32.6% 17.5% — five tokens, + one colour — so panels, dialogs and menus were indistinguishable from the + app canvas and only a 1px border separated them. Each step below is a + distinct surface: + + background 9% app canvas / base chrome + card 12% toolbar, docked panels + popover 15% menus, tooltips, dialogs + muted 20% inset fills (status bar, wells) + accent 22% hover fill — must read above popover, since menu items + sit on it + + Saturation also drops from 84% to ~22%: the old value was a strongly + blue-tinted near-black that clashed with satellite imagery in the map view. + Every foreground/surface pair here clears WCAG AA 4.5:1 (lowest is + muted-foreground on accent at 4.82:1). */ .dark { - --background: 222.2 84% 4.9%; + --background: 222 24% 9%; --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; + --card: 222 22% 12%; --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; + --popover: 222 21% 15%; --popover-foreground: 210 40% 98%; --primary: 217.2 91.2% 59.8%; --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; + --secondary: 220 18% 20%; --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; + --muted: 220 18% 20%; --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; + --accent: 220 18% 22%; --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 224.3 76.3% 48%; + --border: 220 14% 26%; + --input: 220 12% 38%; + /* 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%; } /* Accent color schemes. Each scheme overrides only the accent-bearing tokens @@ -70,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"] {