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.
This commit is contained in:
parent
2464e6fb1c
commit
b8b78f7e57
6 changed files with 199 additions and 74 deletions
|
|
@ -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 <select>
|
||||
element directly, the nested .select select rules cause negative margins
|
||||
and misaligned text. Reset them so text sits centered like <input>. */
|
||||
|
|
|
|||
|
|
@ -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<number, string> = {
|
||||
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<number, string> = {
|
||||
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<number, string> = {
|
||||
68000: "#dc2626",
|
||||
75000: "#c026d3",
|
||||
122000: "#db2777",
|
||||
|
|
@ -55,6 +66,24 @@ const BAND_COLORS: Record<number, string> = {
|
|||
241000: "#b91c1c",
|
||||
}
|
||||
|
||||
let BAND_COLORS: Record<number, string> | null = null
|
||||
|
||||
function resolveBandColors(): Record<number, string> {
|
||||
if (BAND_COLORS) return BAND_COLORS
|
||||
const styles = getComputedStyle(document.documentElement)
|
||||
const resolved: Record<number, string> = { ...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) {
|
||||
|
|
|
|||
|
|
@ -322,10 +322,10 @@ defmodule MicrowavepropWeb.CoreComponents do
|
|||
~H"""
|
||||
<header class={[@actions != [] && "flex items-center justify-between gap-6", "pb-4"]}>
|
||||
<div>
|
||||
<h1 class="text-lg font-semibold leading-8">
|
||||
<h1 class="text-2xl font-semibold leading-snug tracking-tight">
|
||||
{render_slot(@inner_block)}
|
||||
</h1>
|
||||
<p :if={@subtitle != []} class="text-sm text-base-content/70">
|
||||
<p :if={@subtitle != []} class="text-sm text-base-content/70 mt-0.5">
|
||||
{render_slot(@subtitle)}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -371,13 +371,13 @@ defmodule MicrowavepropWeb.CoreComponents do
|
|||
end
|
||||
|
||||
~H"""
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<table class="table">
|
||||
<thead class="text-xs uppercase tracking-wide text-base-content/60">
|
||||
<tr>
|
||||
<th :for={col <- @col}>
|
||||
<th :for={col <- @col} class="font-medium">
|
||||
<span
|
||||
:if={col[:sort_field]}
|
||||
class="cursor-pointer select-none inline-flex items-center gap-1"
|
||||
class="cursor-pointer select-none inline-flex items-center gap-1 hover:text-base-content"
|
||||
phx-click="sort"
|
||||
phx-value-field={col[:sort_field]}
|
||||
phx-value-table={@sort_target}
|
||||
|
|
@ -396,13 +396,17 @@ defmodule MicrowavepropWeb.CoreComponents do
|
|||
</span>
|
||||
<span :if={!col[:sort_field]}>{col[:label]}</span>
|
||||
</th>
|
||||
<th :if={@action != []}>
|
||||
<th :if={@action != []} class="font-medium">
|
||||
<span class="sr-only">{gettext("Actions")}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
|
||||
<tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
|
||||
<tr
|
||||
:for={row <- @rows}
|
||||
id={@row_id && @row_id.(row)}
|
||||
class="hover:bg-base-200 transition-colors"
|
||||
>
|
||||
<td
|
||||
:for={col <- @col}
|
||||
phx-click={@row_click && @row_click.(row)}
|
||||
|
|
@ -410,8 +414,8 @@ defmodule MicrowavepropWeb.CoreComponents do
|
|||
>
|
||||
{render_slot(col, @row_item.(row))}
|
||||
</td>
|
||||
<td :if={@action != []} class="w-0 font-semibold">
|
||||
<div class="flex gap-4">
|
||||
<td :if={@action != []} class="w-0 font-medium text-right">
|
||||
<div class="flex gap-4 justify-end">
|
||||
<%= for action <- @action do %>
|
||||
{render_slot(action, @row_item.(row))}
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ defmodule MicrowavepropWeb.Layouts do
|
|||
|
||||
def app(assigns) do
|
||||
~H"""
|
||||
<header class="navbar px-4 sm:px-6 lg:px-8">
|
||||
<header class="navbar px-4 sm:px-6 lg:px-8 border-b border-base-200">
|
||||
<div class="flex-1 gap-4">
|
||||
<a href="/" class="font-semibold">NTMS Propagation Prediction</a>
|
||||
<nav class="flex gap-2">
|
||||
<a href="/" class="font-semibold tracking-tight">NTMS Propagation Prediction</a>
|
||||
<nav class="flex gap-1">
|
||||
<.link navigate="/map" class="btn btn-ghost btn-sm">Map</.link>
|
||||
<.link navigate="/path" class="btn btn-ghost btn-sm">Path</.link>
|
||||
<.link navigate="/beacons" class="btn btn-ghost btn-sm">Beacons</.link>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
Register for an account
|
||||
<:subtitle>
|
||||
Already registered?
|
||||
<.link navigate={~p"/users/log-in"} class="font-semibold text-brand hover:underline">
|
||||
<.link navigate={~p"/users/log-in"} class="font-semibold text-primary hover:underline">
|
||||
Log in
|
||||
</.link>
|
||||
to your account now.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<% else %>
|
||||
Don't have an account? <.link
|
||||
navigate={~p"/users/register"}
|
||||
class="font-semibold text-brand hover:underline"
|
||||
class="font-semibold text-primary hover:underline"
|
||||
phx-no-format
|
||||
>Sign up</.link> for an account now.
|
||||
<% end %>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue