From 709adcdd3ec2b53eac862f8dddbd162d1b9d2c40 Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Thu, 5 Feb 2026 20:57:59 +0000 Subject: [PATCH] fix(ui): tokenize neutral text colors for dark mode contrast Replace ~268 hardcoded text-stone-* classes with theme-aware token utilities (text-text, text-text-secondary, text-text-muted) across all templates and JS components. Adjust dark mode token values to meet WCAG AA contrast ratios on dark surfaces. --- CLAUDE.md | 51 +++--- static/css/input.css | 11 +- static/js/components/searchable-select.js | 2 +- templates/base.html | 6 +- templates/pages/account.html | 44 ++--- templates/pages/add.html | 150 +++++++++--------- templates/pages/checkin.html | 68 ++++---- templates/pages/cli_callback.html | 4 +- templates/pages/data.html | 2 +- templates/pages/home.html | 52 +++--- templates/pages/login.html | 4 +- templates/pages/register.html | 8 +- templates/pages/timeline.html | 10 +- templates/partials/bag_card.html | 10 +- .../partials/forms/scan_result_form.html | 34 ++-- templates/partials/lists/bag_list.html | 22 +-- templates/partials/lists/brew_list.html | 32 ++-- templates/partials/lists/cafe_list.html | 22 +-- templates/partials/lists/cup_list.html | 22 +-- templates/partials/lists/gear_list.html | 20 +-- templates/partials/lists/roast_list.html | 28 ++-- templates/partials/lists/roaster_list.html | 22 +-- templates/partials/lists/table.html | 12 +- templates/partials/location_search.html | 2 +- templates/partials/nav.html | 18 +-- templates/partials/nearby_cafes.html | 8 +- templates/partials/timeline_month.html | 12 +- 27 files changed, 341 insertions(+), 335 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1abb98e..d16e96d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -409,6 +409,9 @@ Colors are defined as raw CSS custom properties in `:root` (light) and `[data-th | `--accent-hover` | `#ea580c` | `#f97316` | `bg-accent-hover`, `hover:bg-accent-hover` | | `--accent-subtle` | `#fff7ed` (orange-50) | `rgba(234,88,12,0.1)` | `bg-accent-subtle` | | `--accent-text` | `#ffffff` | `#ffffff` | `text-accent-text` | +| `--text` | `#1c1917` (stone-900) | `#e7e5e4` (stone-200) | `text-text` | +| `--text-secondary` | `#57534e` (stone-600) | `#d6d3d1` (stone-300) | `text-text-secondary` | +| `--text-muted` | `#78716c` (stone-500) | `#a8a29e` (stone-400) | `text-text-muted` | A base layer rule sets the default border color so bare `border` / `divide-y` classes use the theme: @@ -420,7 +423,7 @@ A base layer rule sets the default border color so bare `border` / `divide-y` cl } ``` -Neutral text colors (`text-stone-800`, `text-stone-500`, etc.) are used directly — they are not tokenised since they're static in both themes. +Neutral text colors use the tokenised utilities (`text-text`, `text-text-secondary`, `text-text-muted`) which adapt automatically between light and dark themes. Do not use hardcoded `text-stone-*` classes for neutral text — always use the token-based classes. #### Dark Mode @@ -459,11 +462,11 @@ Extra utilities (e.g., `w-28 justify-center whitespace-nowrap`) can be added alo | Level | Classes | Use | |-------|---------|-----| | Page title | `text-3xl font-semibold` | Top-level `

` on each page | -| Section title | `text-lg font-semibold text-stone-800` | `

` / `

` for form card titles | -| Subsection title | `text-base font-semibold text-stone-800` | `

` within cards (e.g. "Confirm cafe details") | -| Form section label | `text-xs font-semibold text-stone-500 uppercase tracking-wide` | `

` grouping related fields (e.g. "Coffee", "Grinder", "Water") | -| Body / description | `text-sm text-stone-600` | Paragraph text below headings | -| Muted secondary | `text-xs text-stone-500` | Subtext in option lists, metadata | +| Section title | `text-lg font-semibold text-text` | `

` / `

` for form card titles | +| Subsection title | `text-base font-semibold text-text` | `

` within cards (e.g. "Confirm cafe details") | +| Form section label | `text-xs font-semibold text-text-muted uppercase tracking-wide` | `

` grouping related fields (e.g. "Coffee", "Grinder", "Water") | +| Body / description | `text-sm text-text-secondary` | Paragraph text below headings | +| Muted secondary | `text-xs text-text-muted` | Subtext in option lists, metadata | | Accent title | `text-2xl font-semibold text-accent` | Login / register page headers | Page headers follow a consistent structure — title + constrained description: @@ -471,7 +474,7 @@ Page headers follow a consistent structure — title + constrained description: ```html

Page Title

-

Short description of what the page does.

+

Short description of what the page does.

``` @@ -485,17 +488,17 @@ Use `w-full` for full-width CTAs. Use `py-3` for larger touch targets on final s **Secondary** — cancel, back, alternative actions: ```html - ``` @@ -609,7 +612,7 @@ Each page template passes `nav_active` to the layout. Nav links use conditional {% if nav_active == "data" %} class="text-accent font-medium" {% else %} - class="text-stone-500 hover:text-stone-800 transition" + class="text-text-muted hover:text-text transition" {% endif %} ``` @@ -673,7 +676,7 @@ The `` custom element (defined in `static/js/components/searc {% for roaster in roaster_options %} {% endfor %} @@ -899,7 +902,7 @@ List partials live in `templates/partials/lists/` (e.g., `roaster_list.html`, `b
{% if items.is_empty() && !navigator.has_search() %}

No {entities} recorded yet. Use the form above to add your first {entity}. @@ -915,7 +918,7 @@ List partials live in `templates/partials/lists/` (e.g., `roaster_list.html`, `b ... {% if items.is_empty() %} -

No {entities} match your search.
+
No {entities} match your search.
{% endif %} {% call table::pagination_header(items, navigator, "#{entity}-list") %} {% if items.has_next() %} @@ -953,12 +956,12 @@ Three macros are available: Every table has a sortable "Added" column as its **first column**, sorted by `created-at`. It uses a distinct smaller style to visually separate it from content columns: ```html - + {{ item.created_at }} ``` -The `text-xs font-medium text-stone-600` classes give it a muted, compact appearance compared to the default `text-sm` body text. +The `text-xs font-medium text-text-secondary` classes give it a muted, compact appearance compared to the default `text-sm` body text. ### Actions Column @@ -984,7 +987,7 @@ Tables use the `responsive-table` CSS class which converts rows to card-style la ```html
{{ brew.roast_name }}
- + ``` diff --git a/static/css/input.css b/static/css/input.css index 907c5ae..5ae3f58 100644 --- a/static/css/input.css +++ b/static/css/input.css @@ -28,8 +28,8 @@ /* Text */ --text: #1c1917; /* stone-900 */ - --text-secondary: #78716c; /* stone-500 */ - --text-muted: #a8a29e; /* stone-400 */ + --text-secondary: #57534e; /* stone-600 */ + --text-muted: #78716c; /* stone-500 */ } [data-theme="dark"] { @@ -48,8 +48,8 @@ --accent-text: #ffffff; --text: #e7e5e4; - --text-secondary: #a8a29e; - --text-muted: #78716c; + --text-secondary: #d6d3d1; + --text-muted: #a8a29e; } /* ── Theme: map raw vars to Tailwind utilities ─────────────────── */ @@ -62,6 +62,9 @@ --color-accent-hover: var(--accent-hover); --color-accent-subtle: var(--accent-subtle); --color-accent-text: var(--accent-text); + --color-text: var(--text); + --color-text-secondary: var(--text-secondary); + --color-text-muted: var(--text-muted); } /* ── Base: default border color ────────────────────────────────── */ diff --git a/static/js/components/searchable-select.js b/static/js/components/searchable-select.js index 74e94d1..bbbedf6 100644 --- a/static/js/components/searchable-select.js +++ b/static/js/components/searchable-select.js @@ -33,7 +33,7 @@ customElements.define("searchable-select", class extends HTMLElement { const clear = document.createElement("button"); clear.type = "button"; - clear.className = "absolute right-2 top-1/2 -translate-y-1/2 text-stone-400 hover:text-stone-600 transition"; + clear.className = "absolute right-2 top-1/2 -translate-y-1/2 text-text-muted hover:text-text-secondary transition"; clear.innerHTML = ''; const selectedWrap = document.createElement("div"); diff --git a/templates/base.html b/templates/base.html index 66220e1..39366e4 100644 --- a/templates/base.html +++ b/templates/base.html @@ -97,11 +97,11 @@ }); - +
{% include "partials/nav.html" %} {% block content %}{% endblock %} -
diff --git a/templates/pages/account.html b/templates/pages/account.html index d79d84c..c138beb 100644 --- a/templates/pages/account.html +++ b/templates/pages/account.html @@ -13,12 +13,12 @@
- {% call icons::key("inline h-4 w-4 mr-1 text-accent") %}{{ passkey.name }} + {% call icons::key("inline h-4 w-4 mr-1 text-accent") %}{{ passkey.name }}
{% if passkeys.len() > 1 %} {% endif %}
-
+

Added {{ passkey.created_at }}

{% if let Some(last_used) = passkey.last_used_at %}

Last used {{ last_used }}

@@ -39,7 +39,7 @@
{% if passkeys.len() <= 1 %} -

Add another passkey before removing your only one.

+

Add another passkey before removing your only one.

{% endif %} @@ -47,7 +47,7 @@