feat(theme): add HSL-based color scale generation with jon/color-tools

Replace hardcoded hex color tokens with algorithmic color scales generated
from HSL parameters. Each scale is defined by hue, saturation, and
lightness steps in tokens.edn, then converted to hex via jon/color-tools.

Color scales (gray, accent, danger, success, warning) generate 11 stops
each (50–950) into :root. Semantic tokens (bg-0, fg-0, accent, etc.)
reference scale variables with var(--gray-50), var(--accent-500), etc.
Dark theme switches which stop each semantic token points to.

Gray scale uses hue 240 with tapered saturation for a purplish tint
matching the activity-tracker aesthetic. Accent is vivid purple (hue 252).
Border radii bumped to 6/10/16px for a rounder feel.

To shift the entire palette (e.g. warm gray), change hue/saturation in
tokens.edn and run `bb build-theme`.
This commit is contained in:
Florian Schroedl
2026-03-11 11:31:47 +01:00
parent 13508f4654
commit fa38d9f9c3
5 changed files with 207 additions and 42 deletions

View File

@@ -1,5 +1,11 @@
{:scales
{;; ─── Scales ────────────────────────────────────────────────────────
;; Generated into :root only. Not duplicated in dark theme blocks.
;; Color scales use HSL: [label lightness] or [label lightness saturation].
;; Change hue/saturation to shift the entire palette (e.g. purplish gray).
:scales
{:size {:base 0.25 :unit "rem" :steps 16}
:font {:base 1 :unit "rem" :ratio 1.25
:steps [[-2 "xs"]
[-1 "sm"]
@@ -8,57 +14,135 @@
[2 "lg"]
[3 "xl"]
[4 "2xl"]
[5 "3xl"]]}}
[5 "3xl"]]}
:color
{;; Gray — purplish tint (hue 240). Change hue for warm/cool grays.
;; Saturation tapers at lighter end for subtlety.
:gray {:hue 240 :saturation 18
:steps [[50 97 14]
[100 95 14]
[200 90 12]
[300 82 10]
[400 64 10]
[500 46 10]
[600 34 12]
[700 26 16]
[800 15 18]
[900 9 18]
[950 5 18]]}
;; Accent — vivid purple, matching activity-tracker (#7c5cfc ≈ hsl 252 96 67)
:accent {:hue 252 :saturation 96
:steps [[50 97 100]
[100 94 95]
[200 89 95]
[300 82 95]
[400 75 93]
[500 67 96]
[600 57 84]
[700 49 72]
[800 41 69]
[900 34 67]
[950 22 73]]}
;; Danger — red
:danger {:hue 0 :saturation 84
:steps [[50 97 86]
[100 94 93]
[200 87 95]
[300 77 90]
[400 66 86]
[500 55 84]
[600 47 80]
[700 40 74]
[800 33 70]
[900 27 65]
[950 15 75]]}
;; Success — green
:success {:hue 142 :saturation 71
:steps [[50 97 78]
[100 93 77]
[200 87 73]
[300 77 72]
[400 61 72]
[500 44 69]
[600 36 64]
[700 30 57]
[800 24 51]
[900 20 46]
[950 12 50]]}
;; Warning — amber
:warning {:hue 38 :saturation 92
:steps [[50 97 89]
[100 93 95]
[200 87 96]
[300 78 95]
[400 68 93]
[500 55 92]
[600 47 88]
[700 40 80]
[800 33 75]
[900 27 70]
[950 18 72]]}}}
;; ─── Semantic tokens (light theme) ────────────────────────────────
;; Reference color scale variables with var(--scale-step).
:tokens
{:bg-0 "#ffffff"
:bg-1 "#f5f5f5"
:bg-2 "#e8e8e8"
:fg-0 "#1a1a1a"
:fg-1 "#4a4a4a"
:fg-2 "#8a8a8a"
:accent "#2563eb"
{:bg-0 "var(--gray-50)"
:bg-1 "var(--gray-100)"
:bg-2 "var(--gray-200)"
:fg-0 "var(--gray-950)"
:fg-1 "var(--gray-600)"
:fg-2 "var(--gray-400)"
:accent "var(--accent-500)"
:fg-on-accent "#ffffff"
:danger "#dc2626"
:danger "var(--danger-500)"
:fg-on-danger "#ffffff"
:success "#16a34a"
:success "var(--success-500)"
:fg-on-success "#ffffff"
:warning "#d97706"
:warning "var(--warning-500)"
:fg-on-warning "#ffffff"
:border-0 "1px solid #e0e0e0"
:border-1 "1px solid #cccccc"
:border-2 "1px solid #999999"
:border-0 "1px solid var(--gray-200)"
:border-1 "1px solid var(--gray-300)"
:border-2 "1px solid var(--gray-500)"
:shadow-0 "0 1px 2px rgba(0,0,0,0.05)"
:shadow-1 "0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06)"
:shadow-2 "0 4px 6px rgba(0,0,0,0.1), 0 2px 4px rgba(0,0,0,0.06)"
:shadow-3 "0 10px 15px rgba(0,0,0,0.1), 0 4px 6px rgba(0,0,0,0.05)"
:radius-sm "4px"
:radius-md "6px"
:radius-lg "12px"}
:radius-sm "6px"
:radius-md "10px"
:radius-lg "16px"}
;; ─── Dark theme overrides ─────────────────────────────────────────
;; Same keys, different scale stops. Scales themselves don't change.
:themes
{:dark
{:bg-0 "#121212"
:bg-1 "#1e1e1e"
:bg-2 "#2a2a2a"
:fg-0 "#e8e8e8"
:fg-1 "#b0b0b0"
:fg-2 "#707070"
:accent "#3b82f6"
{:bg-0 "var(--gray-950)"
:bg-1 "var(--gray-900)"
:bg-2 "var(--gray-800)"
:fg-0 "var(--gray-50)"
:fg-1 "var(--gray-300)"
:fg-2 "var(--gray-500)"
:accent "var(--accent-400)"
:fg-on-accent "#ffffff"
:danger "#ef4444"
:danger "var(--danger-400)"
:fg-on-danger "#ffffff"
:success "#22c55e"
:success "var(--success-400)"
:fg-on-success "#ffffff"
:warning "#f59e0b"
:warning "var(--warning-400)"
:fg-on-warning "#ffffff"
:border-0 "1px solid #2a2a2a"
:border-1 "1px solid #3a3a3a"
:border-2 "1px solid #555555"
:border-0 "1px solid var(--gray-800)"
:border-1 "1px solid var(--gray-700)"
:border-2 "1px solid var(--gray-500)"
:shadow-0 "0 1px 2px rgba(0,0,0,0.2)"
:shadow-1 "0 1px 3px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.2)"
:shadow-2 "0 4px 6px rgba(0,0,0,0.3), 0 2px 4px rgba(0,0,0,0.2)"
:shadow-3 "0 10px 15px rgba(0,0,0,0.3), 0 4px 6px rgba(0,0,0,0.2)"
:radius-sm "4px"
:radius-md "6px"
:radius-lg "12px"}}}
:radius-sm "6px"
:radius-md "10px"
:radius-lg "16px"}}}

View File

@@ -1,7 +1,8 @@
(ns ui.css.gen
(:require [babashka.fs :as fs]
[clojure.edn :as edn]
[clojure.string :as str]))
[clojure.string :as str]
[jon.color-tools :as color]))
(defn read-tokens
"Read and parse the tokens EDN file."
@@ -47,13 +48,37 @@
unit ";")))
(str/join "\n")))
(defn generate-color-scale
"Generate CSS variables for a named color scale.
Each step is [label lightness] or [label lightness saturation].
Uses hsl->hex from jon.color-tools for conversion."
[scale-name {:keys [hue saturation steps]}]
(->> steps
(map (fn [step]
(let [[label lightness sat] (if (= 3 (count step))
step
[(first step) (second step) saturation])
hex (color/hsl->hex [hue sat lightness])]
(str " --" (name scale-name) "-" label ": " hex ";"))))
(str/join "\n")))
(defn generate-color-scales
"Generate all color scale CSS variables."
[color-scales]
(->> color-scales
(sort-by key)
(map (fn [[scale-name config]]
(generate-color-scale scale-name config)))
(str/join "\n")))
(defn generate-scales
"Generate CSS variable declarations for all scales."
[scales]
(str/join "\n"
(cond-> []
(:size scales) (conj (generate-size-scale (:size scales)))
(:font scales) (conj (generate-font-scale (:font scales))))))
(:size scales) (conj (generate-size-scale (:size scales)))
(:font scales) (conj (generate-font-scale (:font scales)))
(:color scales) (conj (generate-color-scales (:color scales))))))
(defn base-css
"Generate base body/reset styles."