feat: add algorithmic size and font scales
Add :scales config to tokens.edn with two generated scales: - Size (linear): --size-1 through --size-16, base 0.25rem × N - Font (geometric): --font-xs through --font-3xl, ratio 1.25 gen.clj computes values and emits them into :root only (not dark theme blocks). button.css now uses scale vars instead of raw rem. Change base or ratio in tokens.edn to recompute the entire scale.
This commit is contained in:
39
AGENTS.md
39
AGENTS.md
@@ -149,6 +149,8 @@ No changes needed in `gen.clj` — it collects all `src/ui/*.css` files automati
|
|||||||
CSS conventions:
|
CSS conventions:
|
||||||
- Utility-style flat classes: `.component`, `.component-variant`, `.component-size`
|
- Utility-style flat classes: `.component`, `.component-variant`, `.component-size`
|
||||||
- Use `var(--token-name)` for all colors, borders, shadows, radii
|
- Use `var(--token-name)` for all colors, borders, shadows, radii
|
||||||
|
- Use `var(--size-N)` for spacing/padding/gap/line-height — no raw `rem` values
|
||||||
|
- Use `var(--font-*)` for font-size — no raw `rem` values
|
||||||
- Include hover/focus/disabled states
|
- Include hover/focus/disabled states
|
||||||
- Keep specificity flat — no nesting beyond `:hover:not(:disabled)`
|
- Keep specificity flat — no nesting beyond `:hover:not(:disabled)`
|
||||||
|
|
||||||
@@ -204,9 +206,44 @@ Semantic + scale tokens in `src/theme/tokens.edn`:
|
|||||||
- **Shadows**: `shadow-0/1/2/3` (increasing elevation)
|
- **Shadows**: `shadow-0/1/2/3` (increasing elevation)
|
||||||
- **Radii**: `radius-sm/md/lg`
|
- **Radii**: `radius-sm/md/lg`
|
||||||
|
|
||||||
|
### Algorithmic scales
|
||||||
|
|
||||||
|
Defined in `:scales` in `tokens.edn`. Generated into `:root` only (not duplicated in dark theme blocks).
|
||||||
|
|
||||||
|
**Size scale** — linear: `--size-N = base × N`
|
||||||
|
|
||||||
|
```edn
|
||||||
|
:size {:base 0.25 :unit "rem" :steps 16}
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces `--size-1: 0.25rem` through `--size-16: 4rem`. Use for all spacing, padding, gap, and line-height values.
|
||||||
|
|
||||||
|
**Font scale** — geometric: `--font-{label} = base × ratio^power`
|
||||||
|
|
||||||
|
```edn
|
||||||
|
:font {:base 1 :unit "rem" :ratio 1.25
|
||||||
|
:steps [[-2 "xs"] [-1 "sm"] [0 "base"] [1 "md"] [2 "lg"] [3 "xl"] [4 "2xl"] [5 "3xl"]]}
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces `--font-xs: 0.64rem` through `--font-3xl: 3.052rem`. Use for all font-size values.
|
||||||
|
|
||||||
|
To adjust the entire scale, change `base` or `ratio` — all values recompute on `bb build-theme`.
|
||||||
|
|
||||||
|
**Usage in component CSS:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
.btn {
|
||||||
|
padding: var(--size-2) var(--size-4);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
line-height: var(--size-5);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rule: never use raw `rem` values in component CSS** — always reference a scale variable.
|
||||||
|
|
||||||
### Adding tokens
|
### Adding tokens
|
||||||
|
|
||||||
Add to both `:tokens` (light) and `:themes > :dark` in `tokens.edn`. They must have the same keys — the `tokens-roundtrip-test` enforces this.
|
Add to both `:tokens` (light) and `:themes > :dark` in `tokens.edn`. They must have the same keys — the `tokens-roundtrip-test` enforces this. Scale config (`:scales`) is theme-independent and lives at the top level.
|
||||||
|
|
||||||
### Dark mode
|
### Dark mode
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
{:tokens
|
{:scales
|
||||||
|
{:size {:base 0.25 :unit "rem" :steps 16}
|
||||||
|
:font {:base 1 :unit "rem" :ratio 1.25
|
||||||
|
:steps [[-2 "xs"]
|
||||||
|
[-1 "sm"]
|
||||||
|
[0 "base"]
|
||||||
|
[1 "md"]
|
||||||
|
[2 "lg"]
|
||||||
|
[3 "xl"]
|
||||||
|
[4 "2xl"]
|
||||||
|
[5 "3xl"]]}}
|
||||||
|
|
||||||
|
:tokens
|
||||||
{:bg-0 "#ffffff"
|
{:bg-0 "#ffffff"
|
||||||
:bg-1 "#f5f5f5"
|
:bg-1 "#f5f5f5"
|
||||||
:bg-2 "#e8e8e8"
|
:bg-2 "#e8e8e8"
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
padding: 0.5rem 1rem;
|
padding: var(--size-2) var(--size-4);
|
||||||
font-size: 0.875rem;
|
font-size: var(--font-sm);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.25rem;
|
line-height: var(--size-5);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -48,15 +48,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-sm {
|
.btn-sm {
|
||||||
padding: 0.25rem 0.5rem;
|
padding: var(--size-1) var(--size-2);
|
||||||
font-size: 0.75rem;
|
font-size: var(--font-xs);
|
||||||
line-height: 1rem;
|
line-height: var(--size-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-lg {
|
.btn-lg {
|
||||||
padding: 0.75rem 1.5rem;
|
padding: var(--size-3) var(--size-6);
|
||||||
font-size: 1rem;
|
font-size: var(--font-base);
|
||||||
line-height: 1.5rem;
|
line-height: var(--size-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
|
|||||||
@@ -21,6 +21,40 @@
|
|||||||
(map (fn [[k v]] (str " " (token->css-var k) ": " v ";")))
|
(map (fn [[k v]] (str " " (token->css-var k) ": " v ";")))
|
||||||
(str/join "\n")))
|
(str/join "\n")))
|
||||||
|
|
||||||
|
(defn format-number
|
||||||
|
"Format a number: strip trailing zeros, max 3 decimal places."
|
||||||
|
[n]
|
||||||
|
(let [s (format "%.3f" (double n))]
|
||||||
|
(-> s
|
||||||
|
(str/replace #"0+$" "")
|
||||||
|
(str/replace #"\.$" ""))))
|
||||||
|
|
||||||
|
(defn generate-size-scale
|
||||||
|
"Generate linear size scale: --size-N = base * N."
|
||||||
|
[{:keys [base unit steps]}]
|
||||||
|
(->> (range 1 (inc steps))
|
||||||
|
(map (fn [n]
|
||||||
|
(str " --size-" n ": " (format-number (* base n)) unit ";")))
|
||||||
|
(str/join "\n")))
|
||||||
|
|
||||||
|
(defn generate-font-scale
|
||||||
|
"Generate geometric font scale: --font-{name} = base * ratio^power."
|
||||||
|
[{:keys [base unit ratio steps]}]
|
||||||
|
(->> steps
|
||||||
|
(map (fn [[power label]]
|
||||||
|
(str " --font-" label ": "
|
||||||
|
(format-number (* base (Math/pow ratio power)))
|
||||||
|
unit ";")))
|
||||||
|
(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))))))
|
||||||
|
|
||||||
(defn base-css
|
(defn base-css
|
||||||
"Generate base body/reset styles."
|
"Generate base body/reset styles."
|
||||||
[]
|
[]
|
||||||
@@ -42,9 +76,12 @@
|
|||||||
|
|
||||||
(defn generate-css
|
(defn generate-css
|
||||||
"Generate the full CSS output from parsed token data."
|
"Generate the full CSS output from parsed token data."
|
||||||
[{:keys [tokens themes]}]
|
[{:keys [tokens themes scales]}]
|
||||||
(let [dark-tokens (get themes :dark)
|
(let [dark-tokens (get themes :dark)
|
||||||
root-block (str ":root {\n" (tokens->css-block tokens) "\n}")
|
scale-vars (when scales (generate-scales scales))
|
||||||
|
root-block (str ":root {\n" (tokens->css-block tokens)
|
||||||
|
(when scale-vars (str "\n" scale-vars))
|
||||||
|
"\n}")
|
||||||
dark-attr (str "[data-theme=\"dark\"] {\n" (tokens->css-block dark-tokens) "\n}")
|
dark-attr (str "[data-theme=\"dark\"] {\n" (tokens->css-block dark-tokens) "\n}")
|
||||||
dark-media (str "@media (prefers-color-scheme: dark) {\n"
|
dark-media (str "@media (prefers-color-scheme: dark) {\n"
|
||||||
" :root:not([data-theme=\"light\"]) {\n"
|
" :root:not([data-theme=\"light\"]) {\n"
|
||||||
|
|||||||
@@ -40,6 +40,21 @@
|
|||||||
(testing "dark media query excludes explicit light theme"
|
(testing "dark media query excludes explicit light theme"
|
||||||
(is (str/includes? css ":root:not([data-theme=\"light\"])")))
|
(is (str/includes? css ":root:not([data-theme=\"light\"])")))
|
||||||
|
|
||||||
|
(testing "contains size scale variables"
|
||||||
|
(doseq [n (range 1 17)]
|
||||||
|
(is (str/includes? css (str "--size-" n ":"))
|
||||||
|
(str "Missing size-" n))))
|
||||||
|
|
||||||
|
(testing "contains font scale variables"
|
||||||
|
(doseq [label ["xs" "sm" "base" "md" "lg" "xl" "2xl" "3xl"]]
|
||||||
|
(is (str/includes? css (str "--font-" label ":"))
|
||||||
|
(str "Missing font-" label))))
|
||||||
|
|
||||||
|
(testing "scales only in :root, not in dark theme blocks"
|
||||||
|
(let [dark-block (second (str/split css #"\[data-theme=\"dark\"\]"))]
|
||||||
|
(is (not (str/includes? dark-block "--size-1:")))
|
||||||
|
(is (not (str/includes? dark-block "--font-base:")))))
|
||||||
|
|
||||||
(testing "contains button component CSS"
|
(testing "contains button component CSS"
|
||||||
(is (str/includes? css ".btn {"))
|
(is (str/includes? css ".btn {"))
|
||||||
(is (str/includes? css ".btn-primary {"))
|
(is (str/includes? css ".btn-primary {"))
|
||||||
|
|||||||
Reference in New Issue
Block a user