From b8b78f7e5771886bcf53b2d293ba31c2ff576e93 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 17 Apr 2026 17:32:59 -0500 Subject: [PATCH] feat(design): apply NTMS design system tokens Adopt the handoff design system: navy-slate surfaces with indigo-violet accent (#6366F1 dark / #5458E3 light), Inter as the UI font, hairline 1px borders, softer corner radii. Add propagation-scale and amateur-band CSS vars so JS hooks can pull theme-aware colors via getComputedStyle. Polish: drop table zebra striping for hover-on-base-200 rows with uppercase small-caps headers, bump .header to text-2xl, tighten nav spacing with a hairline divider, fix broken text-brand -> text-primary on the auth screens. Wire --band-* into contacts_map_hook: resolve once at mount, re-resolve on data-theme mutation so polylines recolor when the theme toggles. --- assets/css/app.css | 169 ++++++++++++------ assets/js/contacts_map_hook.ts | 70 +++++++- .../components/core_components.ex | 24 +-- lib/microwaveprop_web/components/layouts.ex | 6 +- .../user_registration_html/new.html.heex | 2 +- .../user_session_html/new.html.heex | 2 +- 6 files changed, 199 insertions(+), 74 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index 2eee35d9..986b1395 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -1,6 +1,10 @@ /* Leaflet map CSS (vendored) */ @import "../vendor/leaflet/leaflet.css"; +/* Inter — primary UI font. Design spec calls for weights 400/500/600/700. + JetBrains Mono kept as escape hatch for code blocks. */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap'); + /* See the Tailwind configuration guide for advanced usage https://tailwindcss.com/docs/configuration */ @@ -33,38 +37,44 @@ curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui-theme.js We ship with two themes, a light one inspired on Phoenix colors and a dark one inspired on Elixir colors. Build your own at: https://daisyui.com/theme-generator/ */ +/* NTMS Propagation Prediction — dark theme (primary). + Navy-slate surfaces, indigo-violet accent. Values derived from the + design-system handoff: bg-2 (#1E2230) is the elevated surface class + (cards, nav), bg-1 (#161922) is the app body, bg-0 (#0F1218) is the + deepest page fill. Kept in this codebase's base-100/200/300 convention + where base-100 is the *lightest* dark surface (elevated). */ @plugin "../vendor/daisyui-theme" { name: "dark"; default: false; prefersdark: true; color-scheme: "dark"; - --color-base-100: oklch(30.33% 0.016 252.42); - --color-base-200: oklch(25.26% 0.014 253.1); - --color-base-300: oklch(20.15% 0.012 254.09); - --color-base-content: oklch(97.807% 0.029 256.847); - --color-primary: oklch(58% 0.233 277.117); - --color-primary-content: oklch(96% 0.018 272.314); - --color-secondary: oklch(58% 0.233 277.117); - --color-secondary-content: oklch(96% 0.018 272.314); - --color-accent: oklch(60% 0.25 292.717); - --color-accent-content: oklch(96% 0.016 293.756); - --color-neutral: oklch(37% 0.044 257.287); - --color-neutral-content: oklch(98% 0.003 247.858); - --color-info: oklch(58% 0.158 241.966); - --color-info-content: oklch(97% 0.013 236.62); - --color-success: oklch(60% 0.118 184.704); - --color-success-content: oklch(98% 0.014 180.72); - --color-warning: oklch(66% 0.179 58.318); - --color-warning-content: oklch(98% 0.022 95.277); - --color-error: oklch(58% 0.253 17.585); - --color-error-content: oklch(96% 0.015 12.422); - --radius-selector: 0.25rem; - --radius-field: 0.25rem; - --radius-box: 0.5rem; + --color-base-100: #1E2230; + --color-base-200: #161922; + --color-base-300: #0F1218; + --color-base-content: #F5F6FA; + --color-primary: #6366F1; + --color-primary-content: #FFFFFF; + --color-secondary: #7376F4; + --color-secondary-content: #FFFFFF; + --color-accent: #6366F1; + --color-accent-content: #FFFFFF; + --color-neutral: #262B3B; + --color-neutral-content: #F5F6FA; + --color-info: #60A5FA; + --color-info-content: #0A1A2B; + --color-success: #2BD4A4; + --color-success-content: #052A1E; + --color-warning: #F5A524; + --color-warning-content: #2A1A00; + --color-error: #F05674; + --color-error-content: #FFFFFF; + --radius-selector: 0.375rem; + --radius-field: 0.375rem; + --radius-box: 0.75rem; --size-selector: 0.21875rem; --size-field: 0.21875rem; - --border: 1.5px; - --depth: 1; + --border: 1px; + --depth: 0; --noise: 0; } @@ -73,33 +83,33 @@ default: true; prefersdark: false; color-scheme: "light"; - --color-base-100: oklch(98% 0 0); - --color-base-200: oklch(96% 0.001 286.375); - --color-base-300: oklch(92% 0.004 286.32); - --color-base-content: oklch(21% 0.006 285.885); - --color-primary: oklch(70% 0.213 47.604); - --color-primary-content: oklch(98% 0.016 73.684); - --color-secondary: oklch(55% 0.027 264.364); - --color-secondary-content: oklch(98% 0.002 247.839); - --color-accent: oklch(0% 0 0); - --color-accent-content: oklch(100% 0 0); - --color-neutral: oklch(44% 0.017 285.786); - --color-neutral-content: oklch(98% 0 0); - --color-info: oklch(62% 0.214 259.815); - --color-info-content: oklch(97% 0.014 254.604); - --color-success: oklch(70% 0.14 182.503); - --color-success-content: oklch(98% 0.014 180.72); - --color-warning: oklch(66% 0.179 58.318); - --color-warning-content: oklch(98% 0.022 95.277); - --color-error: oklch(58% 0.253 17.585); - --color-error-content: oklch(96% 0.015 12.422); - --radius-selector: 0.25rem; - --radius-field: 0.25rem; - --radius-box: 0.5rem; + --color-base-100: #FFFFFF; + --color-base-200: #F7F8FB; + --color-base-300: #F1F3F8; + --color-base-content: #10131C; + --color-primary: #5458E3; + --color-primary-content: #FFFFFF; + --color-secondary: #4A4ED1; + --color-secondary-content: #FFFFFF; + --color-accent: #5458E3; + --color-accent-content: #FFFFFF; + --color-neutral: #323849; + --color-neutral-content: #FFFFFF; + --color-info: #2563EB; + --color-info-content: #FFFFFF; + --color-success: #149E74; + --color-success-content: #FFFFFF; + --color-warning: #C77700; + --color-warning-content: #FFFFFF; + --color-error: #D0324D; + --color-error-content: #FFFFFF; + --radius-selector: 0.375rem; + --radius-field: 0.375rem; + --radius-box: 0.75rem; --size-selector: 0.21875rem; --size-field: 0.21875rem; - --border: 1.5px; - --depth: 1; + --border: 1px; + --depth: 0; --noise: 0; } @@ -114,6 +124,65 @@ /* Make LiveView wrapper divs transparent for layout */ [data-phx-session], [data-phx-teleported-src] { display: contents } +/* ========================================================================== + NTMS design tokens — propagation quality + band palette. + Held outside daisyUI's theme plugin so they can be referenced directly + from HEEx templates and JS hooks (e.g. the heatmap canvas). + ========================================================================== */ +:root, +:root[data-theme="dark"] { + --prop-excellent: #2BD4A4; + --prop-good: #9FE6C8; + --prop-marginal: #F5D968; + --prop-poor: #F59E4B; + --prop-negligible: #EF6B6B; + + --band-2m: #60A5FA; + --band-70cm: #A78BFA; + --band-23cm: #F472B6; + --band-13cm: #FB7185; + --band-9cm: #FB923C; + --band-6cm: #FBBF24; + --band-3cm: #A3E635; + --band-1-25cm: #34D399; + --band-6mm: #22D3EE; +} + +:root[data-theme="light"] { + --prop-excellent: #149E74; + --prop-good: #5FC79D; + --prop-marginal: #E5B83B; + --prop-poor: #E58A3B; + --prop-negligible: #D45757; + + --band-2m: #2563EB; + --band-70cm: #7C3AED; + --band-23cm: #DB2777; + --band-13cm: #E11D48; + --band-9cm: #EA580C; + --band-6cm: #B45309; + --band-3cm: #65A30D; + --band-1-25cm: #059669; + --band-6mm: #0891B2; +} + +/* Inter as the default UI font. JetBrains Mono reserved for code blocks. + Callsigns and grid squares are intentionally sans + uppercase (not mono) + to match product convention. */ +html, body { + font-family: + "Inter", ui-sans-serif, system-ui, -apple-system, + "Segoe UI", Roboto, sans-serif; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + font-variant-numeric: tabular-nums; +} + +/* Callsign / grid styling helpers. Sans with tight tracking in upper + case; tabular digits so columns align. */ +.callsign { font-weight: 600; letter-spacing: 0.02em; text-transform: uppercase; } +.grid-square { font-weight: 500; color: var(--color-base-content); opacity: 0.85; } + /* Fix daisyUI select text alignment — when select class is on the . */ diff --git a/assets/js/contacts_map_hook.ts b/assets/js/contacts_map_hook.ts index 9a47ddfa..196c0c9d 100644 --- a/assets/js/contacts_map_hook.ts +++ b/assets/js/contacts_map_hook.ts @@ -30,6 +30,7 @@ interface ContactsMapHook extends ViewHook { map: L.Map delegatedChange: (e: Event) => void delegatedClick: (e: Event) => void + themeObserver: MutationObserver | null initMap(this: ContactsMapHook): void buildLines(this: ContactsMapHook): void syncBandLayers(this: ContactsMapHook): void @@ -38,16 +39,26 @@ interface ContactsMapHook extends ViewHook { updateCount(this: ContactsMapHook, count: number): void wireControls(this: ContactsMapHook): void unwireControls(this: ContactsMapHook): void + restyleLines(this: ContactsMapHook): void } -const BAND_COLORS: Record = { - 1296: "#475569", - 2304: "#7c3aed", - 3400: "#4f46e5", - 5760: "#2563eb", - 10000: "#059669", - 24000: "#d97706", - 47000: "#ea580c", +// Amateur-band palette. Nine of these map to the design system's --band-* +// CSS vars (defined in assets/css/app.css); the higher mm-wave bands above +// 47 GHz fall back to hard-coded hues since the design set only spans up +// to 6mm. CSS vars are resolved once per page via getComputedStyle because +// Leaflet writes the color onto the SVG `stroke` attribute, which does not +// evaluate `var()` references. +const BAND_VAR: Record = { + 1296: "--band-23cm", + 2304: "--band-13cm", + 3400: "--band-9cm", + 5760: "--band-6cm", + 10000: "--band-3cm", + 24000: "--band-1-25cm", + 47000: "--band-6mm", +} + +const BAND_FALLBACK: Record = { 68000: "#dc2626", 75000: "#c026d3", 122000: "#db2777", @@ -55,6 +66,24 @@ const BAND_COLORS: Record = { 241000: "#b91c1c", } +let BAND_COLORS: Record | null = null + +function resolveBandColors(): Record { + if (BAND_COLORS) return BAND_COLORS + const styles = getComputedStyle(document.documentElement) + const resolved: Record = { ...BAND_FALLBACK } + for (const [band, cssVar] of Object.entries(BAND_VAR)) { + const value = styles.getPropertyValue(cssVar).trim() + if (value) resolved[Number(band)] = value + } + BAND_COLORS = resolved + return resolved +} + +function invalidateBandColors(): void { + BAND_COLORS = null +} + function bandLabel(mhz: number): string { if (mhz >= 1000) { const ghz = mhz / 1000 @@ -64,7 +93,7 @@ function bandLabel(mhz: number): string { } function bandColor(band: number): string { - return BAND_COLORS[band] || "#64748b" + return resolveBandColors()[band] || "#64748b" } export const ContactsMap = { @@ -76,6 +105,7 @@ export const ContactsMap = { this.bandGroups = new Map() this.lines = [] this.dotLayer = L.layerGroup() + this.themeObserver = null // Callsign filter still roundtrips through LiveView (debounced) — it's // cross-cutting and fires rarely. Band toggles are purely client-side. @@ -87,6 +117,18 @@ export const ContactsMap = { this.wireControls() + // Re-color polylines when the theme toggles. Leaflet writes `stroke` as + // an SVG attribute (which does not resolve CSS vars), so we re-read the + // resolved --band-* values and call setStyle on each line. + this.themeObserver = new MutationObserver(() => { + invalidateBandColors() + this.restyleLines() + }) + this.themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-theme"] + }) + requestAnimationFrame(() => this.initMap()) fetch(fetchUrl) @@ -105,6 +147,16 @@ export const ContactsMap = { destroyed(this: ContactsMapHook) { this.unwireControls() + if (this.themeObserver) { + this.themeObserver.disconnect() + this.themeObserver = null + } + }, + + restyleLines(this: ContactsMapHook) { + for (const entry of this.lines) { + entry.line.setStyle({ color: bandColor(entry.band) }) + } }, wireControls(this: ContactsMapHook) { diff --git a/lib/microwaveprop_web/components/core_components.ex b/lib/microwaveprop_web/components/core_components.ex index ac5bacd3..61dfa971 100644 --- a/lib/microwaveprop_web/components/core_components.ex +++ b/lib/microwaveprop_web/components/core_components.ex @@ -322,10 +322,10 @@ defmodule MicrowavepropWeb.CoreComponents do ~H"""
-

+

{render_slot(@inner_block)}

-

+

{render_slot(@subtitle)}

@@ -371,13 +371,13 @@ defmodule MicrowavepropWeb.CoreComponents do end ~H""" - - +
+ - - - + -
+ {col[:label]} + {gettext("Actions")}
{render_slot(col, @row_item.(row))} -
+
+
<%= for action <- @action do %> {render_slot(action, @row_item.(row))} <% end %> diff --git a/lib/microwaveprop_web/components/layouts.ex b/lib/microwaveprop_web/components/layouts.ex index 324aa820..1e8d020b 100644 --- a/lib/microwaveprop_web/components/layouts.ex +++ b/lib/microwaveprop_web/components/layouts.ex @@ -36,10 +36,10 @@ defmodule MicrowavepropWeb.Layouts do def app(assigns) do ~H""" -