From 988617617c7ff9e29138037c56e9bd746fa1e548 Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Tue, 3 Mar 2026 11:11:47 +0100 Subject: [PATCH] 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 --- AGENTS.md | 35 ++++++++----------- src/ui/button.cljc | 6 ++-- src/ui/button.css | 65 ++++++++++++++++++++++++++++++++++ src/ui/css/gen.clj | 77 ++++++----------------------------------- test/ui/button_test.clj | 46 ++++++++++++------------ test/ui/theme_test.clj | 12 +++---- 6 files changed, 123 insertions(+), 118 deletions(-) create mode 100644 src/ui/button.css diff --git a/AGENTS.md b/AGENTS.md index aa86473..e843061 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,7 @@ src/ ui/ theme.cljc # Token helpers (css-var) button.cljc # Button component (reference implementation) + button.css # Button component styles (read by gen.clj) css/gen.clj # EDN → CSS generator (babashka) dist/ 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`) - **`: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 -(defn component-css-card [] - (str/join "\n\n" - [".card { +```css +/* src/ui/card.css */ +.card { padding: 1rem; border-radius: var(--radius-md); background: var(--bg-1); -}" - ".card--elevated { - box-shadow: var(--shadow-1); -}"])) +} -;; In generate-css, add to the components list: -(defn generate-css [{:keys [tokens themes]}] - (let [...] - (str/join "\n\n" [root-block dark-attr dark-media - (component-css-button) - (component-css-card) ;; <-- add here - ""]))) +.card-elevated { + box-shadow: var(--shadow-1); +} ``` +No changes needed in `gen.clj` — it collects all `src/ui/*.css` files automatically. + 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 - Include hover/focus/disabled states - 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 (testing "default variant" - (is (= ["card" "card--default"] (card/card-class-list {})))) + (is (= ["card" "card-default"] (card/card-class-list {})))) (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`: diff --git a/src/ui/button.cljc b/src/ui/button.cljc index 8245258..2995f24 100644 --- a/src/ui/button.cljc +++ b/src/ui/button.cljc @@ -11,12 +11,12 @@ (defn button-class-list "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]}] (let [v (or (some-> variant kw-name) default-variant) s (or (some-> size kw-name) default-size)] - (cond-> ["btn" (str "btn--" v)] - (not= s "md") (conj (str "btn--" s))))) + (cond-> ["btn" (str "btn-" v)] + (not= s "md") (conj (str "btn-" s))))) (defn button-classes "Generate CSS class string for a button. Returns a space-joined string." diff --git a/src/ui/button.css b/src/ui/button.css new file mode 100644 index 0000000..81ec2cf --- /dev/null +++ b/src/ui/button.css @@ -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; +} diff --git a/src/ui/css/gen.clj b/src/ui/css/gen.clj index ba1ae82..1315170 100644 --- a/src/ui/css/gen.clj +++ b/src/ui/css/gen.clj @@ -1,6 +1,6 @@ (ns ui.css.gen - (:require [clojure.edn :as edn] - [clojure.java.io :as io] + (:require [babashka.fs :as fs] + [clojure.edn :as edn] [clojure.string :as str])) (defn read-tokens @@ -32,68 +32,13 @@ transition: background-color 0.2s, color 0.2s; }") -(defn component-css-button - "Generate BEM-lite CSS for the button component." - [] - (str/join "\n\n" - [".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; -}"])) +(defn collect-component-css + "Read all .css files from the component source directory." + [dir] + (->> (fs/glob dir "*.css") + (sort-by fs/file-name) + (map #(slurp (str %))) + (str/join "\n\n"))) (defn generate-css "Generate the full CSS output from parsed token data." @@ -106,7 +51,7 @@ (str/replace (tokens->css-block dark-tokens) #"(?m)^ " " ") "\n }\n}") 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 ""]))) (defn build-theme! @@ -114,6 +59,6 @@ [{:keys [input output]}] (let [token-data (read-tokens input) css (generate-css token-data)] - (io/make-parents output) + (fs/create-dirs (fs/parent output)) (spit output css) (println (str "Generated " output " (" (count (str/split-lines css)) " lines)")))) diff --git a/test/ui/button_test.clj b/test/ui/button_test.clj index 24cb4b2..bc56271 100644 --- a/test/ui/button_test.clj +++ b/test/ui/button_test.clj @@ -4,54 +4,54 @@ (deftest button-class-list-test (testing "default variant and size" - (is (= ["btn" "btn--secondary"] (button/button-class-list {})))) + (is (= ["btn" "btn-secondary"] (button/button-class-list {})))) (testing "explicit variant" - (is (= ["btn" "btn--primary"] (button/button-class-list {:variant :primary}))) - (is (= ["btn" "btn--ghost"] (button/button-class-list {:variant :ghost}))) - (is (= ["btn" "btn--danger"] (button/button-class-list {:variant :danger})))) + (is (= ["btn" "btn-primary"] (button/button-class-list {:variant :primary}))) + (is (= ["btn" "btn-ghost"] (button/button-class-list {:variant :ghost}))) + (is (= ["btn" "btn-danger"] (button/button-class-list {:variant :danger})))) (testing "explicit size" - (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" "btn--lg"] (button/button-class-list {:size :lg})))) + (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" "btn-lg"] (button/button-class-list {:size :lg})))) (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 (testing "default variant and size" - (is (= "btn btn--secondary" (button/button-classes {})))) + (is (= "btn btn-secondary" (button/button-classes {})))) (testing "explicit variant" - (is (= "btn btn--primary" (button/button-classes {:variant :primary}))) - (is (= "btn btn--ghost" (button/button-classes {:variant :ghost}))) - (is (= "btn btn--danger" (button/button-classes {:variant :danger}))) - (is (= "btn btn--secondary" (button/button-classes {:variant :secondary})))) + (is (= "btn btn-primary" (button/button-classes {:variant :primary}))) + (is (= "btn btn-ghost" (button/button-classes {:variant :ghost}))) + (is (= "btn btn-danger" (button/button-classes {:variant :danger}))) + (is (= "btn btn-secondary" (button/button-classes {:variant :secondary})))) (testing "explicit size" - (is (= "btn btn--secondary btn--sm" (button/button-classes {:size :sm}))) - (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-sm" (button/button-classes {:size :sm}))) + (is (= "btn btn-secondary" (button/button-classes {:size :md}))) + (is (= "btn btn-secondary btn-lg" (button/button-classes {:size :lg})))) (testing "variant + size combined" - (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-primary btn-lg" (button/button-classes {:variant :primary :size :lg}))) + (is (= "btn btn-danger btn-sm" (button/button-classes {:variant :danger :size :sm})))) (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)" - (is (= "btn btn--secondary" (button/button-classes {:size nil})))) + (is (= "btn btn-secondary" (button/button-classes {:size nil})))) (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 (testing "basic button renders correct hiccup (clj target)" (let [result (button/button {:variant :primary} "Click me")] (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))))) (testing "disabled button has disabled attr" @@ -64,7 +64,7 @@ (testing "extra class gets appended" (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" (let [result (button/button {:variant :primary :attrs {:id "my-btn"}} "Test")] diff --git a/test/ui/theme_test.clj b/test/ui/theme_test.clj index 7a5eab2..65ef9a0 100644 --- a/test/ui/theme_test.clj +++ b/test/ui/theme_test.clj @@ -42,12 +42,12 @@ (testing "contains button component CSS" (is (str/includes? css ".btn {")) - (is (str/includes? css ".btn--primary {")) - (is (str/includes? css ".btn--secondary {")) - (is (str/includes? css ".btn--ghost {")) - (is (str/includes? css ".btn--danger {")) - (is (str/includes? css ".btn--sm {")) - (is (str/includes? css ".btn--lg {")) + (is (str/includes? css ".btn-primary {")) + (is (str/includes? css ".btn-secondary {")) + (is (str/includes? css ".btn-ghost {")) + (is (str/includes? css ".btn-danger {")) + (is (str/includes? css ".btn-sm {")) + (is (str/includes? css ".btn-lg {")) (is (str/includes? css ".btn:disabled {"))))) (deftest tokens-roundtrip-test