refactor: replace BEM with utility classes, move component CSS to files
- Rename CSS classes from BEM double-dash (btn--primary) to flat utility-style single-dash (btn-primary) - Move button CSS from inline Clojure string in gen.clj to src/ui/button.css next to button.cljc - gen.clj now auto-collects all src/ui/*.css files via babashka.fs glob - Replace clojure.java.io with babashka.fs throughout gen.clj - Update AGENTS.md to reflect new conventions
This commit is contained in:
35
AGENTS.md
35
AGENTS.md
@@ -10,6 +10,7 @@ src/
|
|||||||
ui/
|
ui/
|
||||||
theme.cljc # Token helpers (css-var)
|
theme.cljc # Token helpers (css-var)
|
||||||
button.cljc # Button component (reference implementation)
|
button.cljc # Button component (reference implementation)
|
||||||
|
button.css # Button component styles (read by gen.clj)
|
||||||
css/gen.clj # EDN → CSS generator (babashka)
|
css/gen.clj # EDN → CSS generator (babashka)
|
||||||
dist/
|
dist/
|
||||||
theme.css # Generated CSS (tokens + component styles)
|
theme.css # Generated CSS (tokens + component styles)
|
||||||
@@ -126,33 +127,27 @@ Key rules:
|
|||||||
- **`:squint` branch**: `:class` is a string, `:style` is a string-keyed map, events are flat (`:on-click`)
|
- **`:squint` branch**: `:class` is a string, `:style` is a string-keyed map, events are flat (`:on-click`)
|
||||||
- **`:clj` branch**: `:class` and `:style` are both strings, no event handlers
|
- **`:clj` branch**: `:class` and `:style` are both strings, no event handlers
|
||||||
|
|
||||||
### 2. Add CSS to `src/ui/css/gen.clj`
|
### 2. Add CSS file `src/ui/COMPONENT.css`
|
||||||
|
|
||||||
Add a `component-css-card` function and include it in `generate-css`:
|
Create a plain CSS file next to the `.cljc` file. The generator automatically reads all `.css` files from `src/ui/`:
|
||||||
|
|
||||||
```clojure
|
```css
|
||||||
(defn component-css-card []
|
/* src/ui/card.css */
|
||||||
(str/join "\n\n"
|
.card {
|
||||||
[".card {
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: var(--bg-1);
|
background: var(--bg-1);
|
||||||
}"
|
}
|
||||||
".card--elevated {
|
|
||||||
box-shadow: var(--shadow-1);
|
|
||||||
}"]))
|
|
||||||
|
|
||||||
;; In generate-css, add to the components list:
|
.card-elevated {
|
||||||
(defn generate-css [{:keys [tokens themes]}]
|
box-shadow: var(--shadow-1);
|
||||||
(let [...]
|
}
|
||||||
(str/join "\n\n" [root-block dark-attr dark-media
|
|
||||||
(component-css-button)
|
|
||||||
(component-css-card) ;; <-- add here
|
|
||||||
""])))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
No changes needed in `gen.clj` — it collects all `src/ui/*.css` files automatically.
|
||||||
|
|
||||||
CSS conventions:
|
CSS conventions:
|
||||||
- BEM-lite: `.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
|
||||||
- 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)`
|
||||||
@@ -168,9 +163,9 @@ Test the pure class-generation functions (they run in `:clj` via Babashka):
|
|||||||
|
|
||||||
(deftest card-class-list-test
|
(deftest card-class-list-test
|
||||||
(testing "default variant"
|
(testing "default variant"
|
||||||
(is (= ["card" "card--default"] (card/card-class-list {}))))
|
(is (= ["card" "card-default"] (card/card-class-list {}))))
|
||||||
(testing "explicit variant"
|
(testing "explicit variant"
|
||||||
(is (= ["card" "card--elevated"] (card/card-class-list {:variant :elevated})))))
|
(is (= ["card" "card-elevated"] (card/card-class-list {:variant :elevated})))))
|
||||||
```
|
```
|
||||||
|
|
||||||
Register new test namespaces in `bb.edn`:
|
Register new test namespaces in `bb.edn`:
|
||||||
|
|||||||
@@ -11,12 +11,12 @@
|
|||||||
|
|
||||||
(defn button-class-list
|
(defn button-class-list
|
||||||
"Generate a vector of CSS class strings for a button given variant and size.
|
"Generate a vector of CSS class strings for a button given variant and size.
|
||||||
Returns e.g. [\"btn\" \"btn--primary\" \"btn--lg\"]."
|
Returns e.g. [\"btn\" \"btn-primary\" \"btn-lg\"]."
|
||||||
[{:keys [variant size]}]
|
[{:keys [variant size]}]
|
||||||
(let [v (or (some-> variant kw-name) default-variant)
|
(let [v (or (some-> variant kw-name) default-variant)
|
||||||
s (or (some-> size kw-name) default-size)]
|
s (or (some-> size kw-name) default-size)]
|
||||||
(cond-> ["btn" (str "btn--" v)]
|
(cond-> ["btn" (str "btn-" v)]
|
||||||
(not= s "md") (conj (str "btn--" s)))))
|
(not= s "md") (conj (str "btn-" s)))))
|
||||||
|
|
||||||
(defn button-classes
|
(defn button-classes
|
||||||
"Generate CSS class string for a button. Returns a space-joined string."
|
"Generate CSS class string for a button. Returns a space-joined string."
|
||||||
|
|||||||
65
src/ui/button.css
Normal file
65
src/ui/button.css
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--fg-on-accent);
|
||||||
|
}
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--bg-1);
|
||||||
|
color: var(--fg-0);
|
||||||
|
border: var(--border-0);
|
||||||
|
}
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background: var(--bg-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--fg-0);
|
||||||
|
}
|
||||||
|
.btn-ghost:hover:not(:disabled) {
|
||||||
|
background: var(--bg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--danger);
|
||||||
|
color: var(--fg-on-danger);
|
||||||
|
}
|
||||||
|
.btn-danger:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lg {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
(ns ui.css.gen
|
(ns ui.css.gen
|
||||||
(:require [clojure.edn :as edn]
|
(:require [babashka.fs :as fs]
|
||||||
[clojure.java.io :as io]
|
[clojure.edn :as edn]
|
||||||
[clojure.string :as str]))
|
[clojure.string :as str]))
|
||||||
|
|
||||||
(defn read-tokens
|
(defn read-tokens
|
||||||
@@ -32,68 +32,13 @@
|
|||||||
transition: background-color 0.2s, color 0.2s;
|
transition: background-color 0.2s, color 0.2s;
|
||||||
}")
|
}")
|
||||||
|
|
||||||
(defn component-css-button
|
(defn collect-component-css
|
||||||
"Generate BEM-lite CSS for the button component."
|
"Read all .css files from the component source directory."
|
||||||
[]
|
[dir]
|
||||||
(str/join "\n\n"
|
(->> (fs/glob dir "*.css")
|
||||||
[".btn {
|
(sort-by fs/file-name)
|
||||||
display: inline-flex;
|
(map #(slurp (str %)))
|
||||||
align-items: center;
|
(str/join "\n\n")))
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5em;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
|
|
||||||
font-family: inherit;
|
|
||||||
}"
|
|
||||||
".btn--primary {
|
|
||||||
background: var(--accent);
|
|
||||||
color: var(--fg-on-accent);
|
|
||||||
}
|
|
||||||
.btn--primary:hover:not(:disabled) {
|
|
||||||
filter: brightness(1.1);
|
|
||||||
}"
|
|
||||||
".btn--secondary {
|
|
||||||
background: var(--bg-1);
|
|
||||||
color: var(--fg-0);
|
|
||||||
border: var(--border-0);
|
|
||||||
}
|
|
||||||
.btn--secondary:hover:not(:disabled) {
|
|
||||||
background: var(--bg-2);
|
|
||||||
}"
|
|
||||||
".btn--ghost {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--fg-0);
|
|
||||||
}
|
|
||||||
.btn--ghost:hover:not(:disabled) {
|
|
||||||
background: var(--bg-1);
|
|
||||||
}"
|
|
||||||
".btn--danger {
|
|
||||||
background: var(--danger);
|
|
||||||
color: var(--fg-on-danger);
|
|
||||||
}
|
|
||||||
.btn--danger:hover:not(:disabled) {
|
|
||||||
filter: brightness(1.1);
|
|
||||||
}"
|
|
||||||
".btn--sm {
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1rem;
|
|
||||||
}"
|
|
||||||
".btn--lg {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
}"
|
|
||||||
".btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}"]))
|
|
||||||
|
|
||||||
(defn generate-css
|
(defn generate-css
|
||||||
"Generate the full CSS output from parsed token data."
|
"Generate the full CSS output from parsed token data."
|
||||||
@@ -106,7 +51,7 @@
|
|||||||
(str/replace (tokens->css-block dark-tokens) #"(?m)^ " " ")
|
(str/replace (tokens->css-block dark-tokens) #"(?m)^ " " ")
|
||||||
"\n }\n}")
|
"\n }\n}")
|
||||||
base (base-css)
|
base (base-css)
|
||||||
components (component-css-button)]
|
components (collect-component-css "src/ui")]
|
||||||
(str/join "\n\n" [root-block dark-attr dark-media base components ""])))
|
(str/join "\n\n" [root-block dark-attr dark-media base components ""])))
|
||||||
|
|
||||||
(defn build-theme!
|
(defn build-theme!
|
||||||
@@ -114,6 +59,6 @@
|
|||||||
[{:keys [input output]}]
|
[{:keys [input output]}]
|
||||||
(let [token-data (read-tokens input)
|
(let [token-data (read-tokens input)
|
||||||
css (generate-css token-data)]
|
css (generate-css token-data)]
|
||||||
(io/make-parents output)
|
(fs/create-dirs (fs/parent output))
|
||||||
(spit output css)
|
(spit output css)
|
||||||
(println (str "Generated " output " (" (count (str/split-lines css)) " lines)"))))
|
(println (str "Generated " output " (" (count (str/split-lines css)) " lines)"))))
|
||||||
|
|||||||
@@ -4,54 +4,54 @@
|
|||||||
|
|
||||||
(deftest button-class-list-test
|
(deftest button-class-list-test
|
||||||
(testing "default variant and size"
|
(testing "default variant and size"
|
||||||
(is (= ["btn" "btn--secondary"] (button/button-class-list {}))))
|
(is (= ["btn" "btn-secondary"] (button/button-class-list {}))))
|
||||||
|
|
||||||
(testing "explicit variant"
|
(testing "explicit variant"
|
||||||
(is (= ["btn" "btn--primary"] (button/button-class-list {:variant :primary})))
|
(is (= ["btn" "btn-primary"] (button/button-class-list {:variant :primary})))
|
||||||
(is (= ["btn" "btn--ghost"] (button/button-class-list {:variant :ghost})))
|
(is (= ["btn" "btn-ghost"] (button/button-class-list {:variant :ghost})))
|
||||||
(is (= ["btn" "btn--danger"] (button/button-class-list {:variant :danger}))))
|
(is (= ["btn" "btn-danger"] (button/button-class-list {:variant :danger}))))
|
||||||
|
|
||||||
(testing "explicit size"
|
(testing "explicit size"
|
||||||
(is (= ["btn" "btn--secondary" "btn--sm"] (button/button-class-list {:size :sm})))
|
(is (= ["btn" "btn-secondary" "btn-sm"] (button/button-class-list {:size :sm})))
|
||||||
(is (= ["btn" "btn--secondary"] (button/button-class-list {:size :md})))
|
(is (= ["btn" "btn-secondary"] (button/button-class-list {:size :md})))
|
||||||
(is (= ["btn" "btn--secondary" "btn--lg"] (button/button-class-list {:size :lg}))))
|
(is (= ["btn" "btn-secondary" "btn-lg"] (button/button-class-list {:size :lg}))))
|
||||||
|
|
||||||
(testing "variant + size combined"
|
(testing "variant + size combined"
|
||||||
(is (= ["btn" "btn--primary" "btn--lg"] (button/button-class-list {:variant :primary :size :lg})))))
|
(is (= ["btn" "btn-primary" "btn-lg"] (button/button-class-list {:variant :primary :size :lg})))))
|
||||||
|
|
||||||
(deftest button-classes-test
|
(deftest button-classes-test
|
||||||
(testing "default variant and size"
|
(testing "default variant and size"
|
||||||
(is (= "btn btn--secondary" (button/button-classes {}))))
|
(is (= "btn btn-secondary" (button/button-classes {}))))
|
||||||
|
|
||||||
(testing "explicit variant"
|
(testing "explicit variant"
|
||||||
(is (= "btn btn--primary" (button/button-classes {:variant :primary})))
|
(is (= "btn btn-primary" (button/button-classes {:variant :primary})))
|
||||||
(is (= "btn btn--ghost" (button/button-classes {:variant :ghost})))
|
(is (= "btn btn-ghost" (button/button-classes {:variant :ghost})))
|
||||||
(is (= "btn btn--danger" (button/button-classes {:variant :danger})))
|
(is (= "btn btn-danger" (button/button-classes {:variant :danger})))
|
||||||
(is (= "btn btn--secondary" (button/button-classes {:variant :secondary}))))
|
(is (= "btn btn-secondary" (button/button-classes {:variant :secondary}))))
|
||||||
|
|
||||||
(testing "explicit size"
|
(testing "explicit size"
|
||||||
(is (= "btn btn--secondary btn--sm" (button/button-classes {:size :sm})))
|
(is (= "btn btn-secondary btn-sm" (button/button-classes {:size :sm})))
|
||||||
(is (= "btn btn--secondary" (button/button-classes {:size :md})))
|
(is (= "btn btn-secondary" (button/button-classes {:size :md})))
|
||||||
(is (= "btn btn--secondary btn--lg" (button/button-classes {:size :lg}))))
|
(is (= "btn btn-secondary btn-lg" (button/button-classes {:size :lg}))))
|
||||||
|
|
||||||
(testing "variant + size combined"
|
(testing "variant + size combined"
|
||||||
(is (= "btn btn--primary btn--lg" (button/button-classes {:variant :primary :size :lg})))
|
(is (= "btn btn-primary btn-lg" (button/button-classes {:variant :primary :size :lg})))
|
||||||
(is (= "btn btn--danger btn--sm" (button/button-classes {:variant :danger :size :sm}))))
|
(is (= "btn btn-danger btn-sm" (button/button-classes {:variant :danger :size :sm}))))
|
||||||
|
|
||||||
(testing "nil variant falls back to default"
|
(testing "nil variant falls back to default"
|
||||||
(is (= "btn btn--secondary" (button/button-classes {:variant nil}))))
|
(is (= "btn btn-secondary" (button/button-classes {:variant nil}))))
|
||||||
|
|
||||||
(testing "nil size falls back to default (md, no size class)"
|
(testing "nil size falls back to default (md, no size class)"
|
||||||
(is (= "btn btn--secondary" (button/button-classes {:size nil}))))
|
(is (= "btn btn-secondary" (button/button-classes {:size nil}))))
|
||||||
|
|
||||||
(testing "string variants work"
|
(testing "string variants work"
|
||||||
(is (= "btn btn--primary" (button/button-classes {:variant "primary"})))))
|
(is (= "btn btn-primary" (button/button-classes {:variant "primary"})))))
|
||||||
|
|
||||||
(deftest button-component-test
|
(deftest button-component-test
|
||||||
(testing "basic button renders correct hiccup (clj target)"
|
(testing "basic button renders correct hiccup (clj target)"
|
||||||
(let [result (button/button {:variant :primary} "Click me")]
|
(let [result (button/button {:variant :primary} "Click me")]
|
||||||
(is (= :button (first result)))
|
(is (= :button (first result)))
|
||||||
(is (= "btn btn--primary" (get-in result [1 :class])))
|
(is (= "btn btn-primary" (get-in result [1 :class])))
|
||||||
(is (= "Click me" (nth result 2)))))
|
(is (= "Click me" (nth result 2)))))
|
||||||
|
|
||||||
(testing "disabled button has disabled attr"
|
(testing "disabled button has disabled attr"
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
|
|
||||||
(testing "extra class gets appended"
|
(testing "extra class gets appended"
|
||||||
(let [result (button/button {:variant :primary :class "extra"} "Test")]
|
(let [result (button/button {:variant :primary :class "extra"} "Test")]
|
||||||
(is (= "btn btn--primary extra" (get-in result [1 :class])))))
|
(is (= "btn btn-primary extra" (get-in result [1 :class])))))
|
||||||
|
|
||||||
(testing "extra attrs get merged"
|
(testing "extra attrs get merged"
|
||||||
(let [result (button/button {:variant :primary :attrs {:id "my-btn"}} "Test")]
|
(let [result (button/button {:variant :primary :attrs {:id "my-btn"}} "Test")]
|
||||||
|
|||||||
@@ -42,12 +42,12 @@
|
|||||||
|
|
||||||
(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 {"))
|
||||||
(is (str/includes? css ".btn--secondary {"))
|
(is (str/includes? css ".btn-secondary {"))
|
||||||
(is (str/includes? css ".btn--ghost {"))
|
(is (str/includes? css ".btn-ghost {"))
|
||||||
(is (str/includes? css ".btn--danger {"))
|
(is (str/includes? css ".btn-danger {"))
|
||||||
(is (str/includes? css ".btn--sm {"))
|
(is (str/includes? css ".btn-sm {"))
|
||||||
(is (str/includes? css ".btn--lg {"))
|
(is (str/includes? css ".btn-lg {"))
|
||||||
(is (str/includes? css ".btn:disabled {")))))
|
(is (str/includes? css ".btn:disabled {")))))
|
||||||
|
|
||||||
(deftest tokens-roundtrip-test
|
(deftest tokens-roundtrip-test
|
||||||
|
|||||||
Reference in New Issue
Block a user