Init
This commit is contained in:
48
src/theme/tokens.edn
Normal file
48
src/theme/tokens.edn
Normal file
@@ -0,0 +1,48 @@
|
||||
{:tokens
|
||||
{:bg-0 "#ffffff"
|
||||
:bg-1 "#f5f5f5"
|
||||
:bg-2 "#e8e8e8"
|
||||
:fg-0 "#1a1a1a"
|
||||
:fg-1 "#4a4a4a"
|
||||
:fg-2 "#8a8a8a"
|
||||
:accent "#2563eb"
|
||||
:fg-on-accent "#ffffff"
|
||||
:danger "#dc2626"
|
||||
:fg-on-danger "#ffffff"
|
||||
:success "#16a34a"
|
||||
:fg-on-success "#ffffff"
|
||||
:border-0 "1px solid #e0e0e0"
|
||||
:border-1 "1px solid #cccccc"
|
||||
:border-2 "1px solid #999999"
|
||||
: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"}
|
||||
|
||||
:themes
|
||||
{:dark
|
||||
{:bg-0 "#121212"
|
||||
:bg-1 "#1e1e1e"
|
||||
:bg-2 "#2a2a2a"
|
||||
:fg-0 "#e8e8e8"
|
||||
:fg-1 "#b0b0b0"
|
||||
:fg-2 "#707070"
|
||||
:accent "#3b82f6"
|
||||
:fg-on-accent "#ffffff"
|
||||
:danger "#ef4444"
|
||||
:fg-on-danger "#ffffff"
|
||||
:success "#22c55e"
|
||||
:fg-on-success "#ffffff"
|
||||
:border-0 "1px solid #2a2a2a"
|
||||
:border-1 "1px solid #3a3a3a"
|
||||
:border-2 "1px solid #555555"
|
||||
: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"}}}
|
||||
64
src/ui/button.cljc
Normal file
64
src/ui/button.cljc
Normal file
@@ -0,0 +1,64 @@
|
||||
(ns ui.button
|
||||
(:require [clojure.string :as str]))
|
||||
|
||||
;; In squint, keywords are strings — name is identity
|
||||
#?(:squint (defn- kw-name [s] s)
|
||||
:cljs (defn- kw-name [s] (name s))
|
||||
:clj (defn- kw-name [s] (name s)))
|
||||
|
||||
(def default-variant "secondary")
|
||||
(def default-size "md")
|
||||
|
||||
(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\"]."
|
||||
[{: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)))))
|
||||
|
||||
(defn button-classes
|
||||
"Generate CSS class string for a button. Returns a space-joined string."
|
||||
[opts]
|
||||
(str/join " " (button-class-list opts)))
|
||||
|
||||
(defn button
|
||||
"Render a button element. Works across all targets via reader conditionals.
|
||||
|
||||
Props:
|
||||
:variant - :primary, :secondary, :ghost, :danger
|
||||
:size - :sm, :md, :lg
|
||||
:on-click - click handler (ignored in :clj target)
|
||||
:disabled - boolean
|
||||
:class - additional CSS classes (string or vector)
|
||||
:attrs - additional HTML attributes map"
|
||||
[{:keys [variant size on-click disabled class attrs] :as _props} & children]
|
||||
#?(:squint
|
||||
(let [classes (cond-> (button-classes {:variant variant :size size})
|
||||
class (str " " class))
|
||||
base-attrs (merge {:class classes}
|
||||
(when disabled {:disabled true})
|
||||
attrs)]
|
||||
(into [:button (cond-> base-attrs
|
||||
on-click (assoc :on-click on-click))]
|
||||
children))
|
||||
|
||||
:cljs
|
||||
(let [cls (button-class-list {:variant variant :size size})
|
||||
classes (cond-> cls
|
||||
class (conj class))
|
||||
base-attrs (merge {:class classes}
|
||||
(when disabled {:disabled true})
|
||||
attrs)]
|
||||
(into [:button (cond-> base-attrs
|
||||
on-click (assoc-in [:on :click] on-click))]
|
||||
children))
|
||||
|
||||
:clj
|
||||
(let [classes (cond-> (button-classes {:variant variant :size size})
|
||||
class (str " " class))
|
||||
base-attrs (merge {:class classes}
|
||||
(when disabled {:disabled true})
|
||||
attrs)]
|
||||
(into [:button base-attrs] children))))
|
||||
107
src/ui/css/gen.clj
Normal file
107
src/ui/css/gen.clj
Normal file
@@ -0,0 +1,107 @@
|
||||
(ns ui.css.gen
|
||||
(:require [clojure.edn :as edn]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(defn read-tokens
|
||||
"Read and parse the tokens EDN file."
|
||||
[path]
|
||||
(edn/read-string (slurp path)))
|
||||
|
||||
(defn token->css-var
|
||||
"Convert a token keyword to a CSS variable name."
|
||||
[k]
|
||||
(str "--" (name k)))
|
||||
|
||||
(defn tokens->css-block
|
||||
"Generate CSS variable declarations from a token map."
|
||||
[tokens]
|
||||
(->> tokens
|
||||
(sort-by key)
|
||||
(map (fn [[k v]] (str " " (token->css-var k) ": " v ";")))
|
||||
(str/join "\n")))
|
||||
|
||||
(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 generate-css
|
||||
"Generate the full CSS output from parsed token data."
|
||||
[{:keys [tokens themes]}]
|
||||
(let [dark-tokens (get themes :dark)
|
||||
root-block (str ":root {\n" (tokens->css-block tokens) "\n}")
|
||||
dark-attr (str "[data-theme=\"dark\"] {\n" (tokens->css-block dark-tokens) "\n}")
|
||||
dark-media (str "@media (prefers-color-scheme: dark) {\n"
|
||||
" :root:not([data-theme=\"light\"]) {\n"
|
||||
(str/replace (tokens->css-block dark-tokens) #"(?m)^ " " ")
|
||||
"\n }\n}")
|
||||
components (component-css-button)]
|
||||
(str/join "\n\n" [root-block dark-attr dark-media components ""])))
|
||||
|
||||
(defn build-theme!
|
||||
"Read tokens from file and write generated CSS to output."
|
||||
[{:keys [input output]}]
|
||||
(let [token-data (read-tokens input)
|
||||
css (generate-css token-data)]
|
||||
(io/make-parents output)
|
||||
(spit output css)
|
||||
(println (str "Generated " output " (" (count (str/split-lines css)) " lines)"))))
|
||||
7
src/ui/theme.cljc
Normal file
7
src/ui/theme.cljc
Normal file
@@ -0,0 +1,7 @@
|
||||
(ns ui.theme)
|
||||
|
||||
(defn css-var
|
||||
"Reference a CSS variable by token keyword.
|
||||
(css-var :accent) => \"var(--accent)\""
|
||||
[token]
|
||||
(str "var(--" (name token) ")"))
|
||||
Reference in New Issue
Block a user