Skip to content

Instantly share code, notes, and snippets.

@spacegangster
Last active April 21, 2025 11:48
Show Gist options
  • Save spacegangster/cd0402b1820577150c5616e03c2a9689 to your computer and use it in GitHub Desktop.
Save spacegangster/cd0402b1820577150c5616e03c2a9689 to your computer and use it in GitHub Desktop.
Cascading Theme Compiler
(ns dev-cards.views.theme-compiler
"Generate themed controls from compact / declarative theme descriptions.
You define a system of transformations that later works with any basic color inputs.
Roughly it's like this :
{:theme/name \"Dreamer\"
:theme/params {:params/control-height :40px}
:theme/controls
{:controls/button
{:variants/default
{:state/default
{:cursor :pointer,
:height [:theme/params :params/control-height]
:background {:h 200, :s 50 :l 60}}
:state/hover
{:background {:adjust {:l +5}}}"
(:require [clojure.string :as str]
[clojure.walk :as walk]))
;;; Utility functions (to eliminate dependencies)
;; From common.functions
(defn map-entries
"Maps over the entries of a map, expecting [k v] vectors to be returned."
[m f]
(into {} (map f m)))
(defn map-values
"Maps over the values of a map"
[m f]
(reduce-kv
(fn [acc k v]
(assoc acc k (f v)))
{}
m))
(defn filter-keys
"Filter a map by its keys"
[m pred]
(into {} (filter (fn [[k _]] (pred k)) m)))
;; From space-ui.primitives
(defn- map->hsl-str
[{:keys [h s l a] :as m
:or {h 0 s 0 l 0}}]
(if a
(str "hsla(" h ", " s "%, " l "%, " a ")")
(str "hsl(" h ", " s "%, " l "%)")))
(defn hsl-str
([map:hsl] (cond-> map:hsl (map? map:hsl) (map->hsl-str)))
([h s l] (str "hsl(" h ", " s "%, " l "%)"))
([h s l a] (str "hsla(" h ", " s "%, " l "%, " a ")")))
(defn hsl-map? [v]
(and (map? v) (or (:h v) (:s v) (:l v))))
(defn auto-hsl [v]
(cond-> v (hsl-map? v) hsl-str))
(defn autopx [v]
(cond-> v
(number? v) (str "px")
(keyword v) (name)))
(defn border-solid
([color] (border-solid 1 color))
([thickness color]
(str (autopx thickness) " solid " (hsl-str color))))
(def ff-main "-apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif")
;;; Color utilities
(defn color-mod [color-map & {:as color-mod-map}]
(merge-with + color-map color-mod-map))
(assert (= {:h 120 :s 80 :l 70} (color-mod {:h 120 :s 100 :l 50} :s -20 :l 20)))
(def cm color-mod)
(defn hsl-interpolate
"Interpolates between two HSL colors based on a coefficient.
coeff is a number between 0 and 1.
Returns a new HSL color map."
[hsl-src hsl-dst coeff]
(let [take-from-src (- 1 coeff)
a (when (:a hsl-src) (+ (* take-from-src (:a hsl-src)) (* coeff (:a hsl-dst))))]
(cond->
{:h (+ (* take-from-src (:h hsl-src)) (* coeff (:h hsl-dst)))
:s (+ (* take-from-src (:s hsl-src)) (* coeff (:s hsl-dst)))
:l (+ (* take-from-src (:l hsl-src)) (* coeff (:l hsl-dst)))}
a (assoc :a a))))
(assert
(= {:h 50, :s 51.5, :l 51.5} (hsl-interpolate {:h 0, :s 3, :l 3} {:h 100, :s 100, :l 100} 0.5)))
;;; Declarative Color System
(defn is-light?
"Determines if a color is light based on its luminance."
[color]
(> (:l color) 50))
(defn hsl? [color]
(and (map? color)
(or (:h color) (:s color) (:l color) (:a color))))
(defn color-ref-def? [color-def]
(and (map? color-def)
(:ref color-def)))
(def light-color-system-graph
"For light themes"
{:theme/params {:text-light-threshold 60
:theme-dark-l-max 40}
:theme/base {:pane {:ref [:theme/gradient-avg] :adjust {:l +10 :s -30}}
:secondary-bg {:ref [:theme/gradient-avg] :adjust {:l -5 :s -30}}
:input-bg {:ref [:theme/gradient-avg] :adjust {:l +5 :s -40}}
:text {:ref [:theme/gradient-avg] :adjust {:l -50 :s -20}}
:border {:ref [:theme/base :secondary-bg] :adjust {:l -10}}
:primary {:h 220 :s 80 :l 60}
:primary-text {:h 220 :s 0 :l 98}
:danger {:h 0 :s 85 :l 55}
:focus-glow {:ref [:theme/base :primary] :adjust {:a 0.3}}}
:theme/states {:hover {:adjust {:h 0 :s 0 :l -5}}
:active {:adjust {:l -10}}
:focus {:glow {:ref [:theme/base :primary] :adjust {:a 0.2}}}}
:theme/contexts {:on-pane {:text {:ref [:theme/base :text]}
:pane {:ref [:theme/base :pane]}
:border {:ref [:theme/base :border] :adjust {:a 0.8}}}
:on-gradient {:text {:ref [:theme/base :text] :adjust {:l +10}}
:border {:ref [:theme/gradient-avg] :adjust {:l -20 :a 0.4}}}}})
(defn resolve-color [color-graph color-def]
;(println "resolve color entry 1" color-graph)
;(println "resolve color entry 2" color-def)
(cond
(hsl? color-def) color-def
(color-ref-def? color-def)
(let [ref (:ref color-def)
base (resolve-color color-graph (get-in color-graph ref))
adjustments (:adjust color-def {})]
(if base
(color-mod base adjustments)
(do
(prn color-graph)
(throw (js/Error. (str "Invalid color reference: " ref))))))
(string? color-def) color-def
:else color-def))
(defn compile-color-system
"Compiles color system graph into usable theme colors based on gradient"
[gradient color-graph]
(let [gradient-start (:start gradient)
gradient-end (:end gradient)
gradient-avg (hsl-interpolate gradient-start gradient-end 0.5)
avg-lightness (:l gradient-avg)
theme-type (if (> avg-lightness 50) :theme.type/light :theme.type/dark)
color-graph (assoc color-graph :theme/gradient-avg gradient-avg)
resolve-color- (partial resolve-color color-graph)
theme-base
(walk/postwalk
(fn [node]
(cond-> node (color-ref-def? node) (resolve-color- node)))
color-graph)]
(merge
{:theme/gradient gradient
:theme/type theme-type}
theme-base)))
;; Usage
;;; Example gradient definition
(def dreamer-gradient
{:start {:h 340.5 :s 95 :l 91}
:end {:h 177.4 :s 66 :l 79}})
(def dreamer-theme
(compile-color-system dreamer-gradient light-color-system-graph))
;;; Declarative Trait System
(def trait-system-graph
"This is the default one, for bare on gradient look"
{:theme/params {:params/base-font-family ff-main
:params/control-height "40px"
:params/control-spacing "16px"}
:theme/controls
{:controls/basic
{:control/sort-index 0
:variants/default
{:state/default
{:font-weight 400
:cursor :pointer
:font-size :16px
:transition "all 0.2s ease"}}}
:controls/boxy
{:control/sort-index 1
:control/uses :controls/basic
:variants/default
{:state/default
{:height [:theme/params :params/control-height]
:padding-inline [:theme/params :params/control-spacing]
:border-radius :4px}}}
:controls/bordered
{:control/sort-index 2
:control/uses :controls/boxy
:variants/default
{:state/default
{:height [:theme/params :params/control-height]
:border "1px solid"}}}
:controls/button
{:control/sort-index 3
:control/uses :controls/boxy
:variants/default
{:state/default
{:height [:theme/params :params/control-height]
:background [:theme/base :secondary-bg]
:border-color [:theme/base :border]
:color [:theme/base :text]}
:state/hover {:background {:adjust {:l +5}}
:color {:adjust {:l +5}}}
:state/active {:background {:adjust {:l -10}}
:color {:adjust {:l -10}}
:margin-top :1px}}
:variants/primary
{:state/default
{:background [:theme/base :primary]
:color [:theme/base :primary-text]}}}
:controls/input
{:control/sort-index 4
:control/uses :controls/bordered
:variants/default
{:state/default
{:background [:theme/base :input-bg]}}}
:controls/link
{:control/sort-index 5
:control/uses :controls/basic
:variants/default
{:state/default
{:text-decoration "none"
:transition "color 0.2s ease"}}}}})
;;; Trait System Utilities
(defn path-ref? [value]
(and (not (map-entry? value))
(vector? value) (keyword? (first value))))
(defn conform-uses [uses]
(cond
(keyword? uses) [uses]
(vector? uses) uses
(nil? uses) nil
:else (throw (js/Error. (str "Invalid :control/uses value: " uses)))))
(defn merge-states [state-a state-b]
(if (and (map? state-a) (map? state-b))
(merge state-a state-b)
(or state-b state-a)))
(defn merge-variants [variant-a variant-b]
(if (and (map? variant-a) (map? variant-b))
(merge-with merge-states variant-a variant-b)
(or variant-b variant-a)))
(defn merge-controls [control-a control-b]
(merge-with merge-variants control-a control-b))
(assert
(= {:control/compiled? true, :control/uses :controls/boxy, :variants/default {}}
(merge-controls
{:control/compiled? true, :control/uses :controls/basic, :variants/default {}}
{:control/uses :controls/boxy, :variants/default {}})))
(defn mod-val
"Modifies a base value by a map of adjustments
example: (mod-val {:h 0 :s 0 :l 10} {:adjust {:h 10 :l -10}}) -> {:h 10 :s 0 :l 0}"
[base-val val-mod]
(if-let [adjust (:adjust val-mod)]
(color-mod base-val adjust)
base-val))
(assert (= {:h 10 :s 0 :l 0} (mod-val {:h 0 :s 0 :l 10} {:adjust {:h 10 :l -10}})))
(defn compile-state
"Compiles a control state.
State def describes values changed compared to default state,
or specific adjustments to same values in the default state"
[default-state state-def]
(map-entries
; can be optional, maybe we should copy props like this only between the same states, but not between default and active for example
(merge default-state state-def)
(fn [[prop-name val-mod]]
(let [base-val (get default-state prop-name)
is-mod? (:adjust val-mod)]
[prop-name (if is-mod? (mod-val base-val val-mod) val-mod)]))))
(assert
(= {:font-weight 400, :height "40px"}
(compile-state {:font-weight 400} {:height "40px"})))
(assert
(= {:background {:h 10, :s 0, :l 0}}
(compile-state
{:background {:h 0 :s 0 :l 10}}
{:background {:adjust {:h 10 :l -10}}})))
(defn compile-variant
([variant-def] (compile-variant nil variant-def))
([default-variant variant-def]
(let [default-state (compile-state (:state/default default-variant) (:state/default variant-def))
_ (def ds default-state)
base-states (merge-variants (dissoc default-variant :state/default)
(dissoc variant-def :state/default))
non-default-states (map-entries
base-states
(fn [[state-name state-def]]
[state-name (compile-state default-state state-def)]))]
(assoc non-default-states :state/default default-state))))
(assert
(= {:state/default {:background {:h 10, :s 0, :l 30}}
:state/hover {:background {:h 20, :s 0, :l 20}}}
(compile-variant
{:state/default {:background {:h 10 :s 0 :l 30}}
:state/hover {:background {:adjust {:h 10 :l -10}}}})))
(assert
(= {:state/default {:background {:h 60, :s 0, :l 40}}
:state/hover {:background {:h 70, :s 0, :l 30}}
:state/active {:background {:h 60, :s 0, :l 40}, :border "1px solid"}}
(compile-variant
{:state/default {:background {:h 50 :s 0 :l 10}}
:state/active {:border "1px solid"}}
{:state/default {:background {:adjust {:h 10 :s 0 :l 30}}}
:state/hover {:background {:adjust {:h 10 :l -10}}}})))
(defn is-state-name? [key]
(and (keyword? key) (.startsWith (str key) ":state/")))
(defn is-variant-name? [variant-name]
(str/starts-with? (str variant-name) ":variants/"))
(defn compile-merged-control
([new-control] (compile-merged-control nil new-control))
([base-control new-control]
(let [default-variant (compile-variant (:variants/default base-control) (:variants/default new-control))
_ (def dv default-variant)
pre-comp-control (merge-controls (dissoc base-control :variants/default)
(dissoc new-control :variants/default))
non-default-variants (map-entries
pre-comp-control
(fn [[variant-name variant-def]]
(if (is-variant-name? variant-name)
(let [base-control-variant (get base-control variant-name)
new-variant-1 (compile-variant base-control-variant variant-def)
new-variant (compile-variant default-variant new-variant-1)]
[variant-name new-variant])
[variant-name variant-def])))]
(assoc non-default-variants :variants/default default-variant))))
;; simplest
(assert
(= {:variants/default {:state/default {:background {:h 20 :s 0 :l 60}}}}
(compile-merged-control
{:variants/default {:state/default {:background {:h 10 :s 0 :l 30}}}}
{:variants/default {:state/default {:background {:adjust {:h 10 :s 0 :l 30}}}}})))
;; harder
(assert
(= {:variants/default
{:state/default {:background {:h 20, :s 0, :l 60}}
:state/hover {:background {:h 10, :s 0, :l 40}},}
:variants/primary
{:state/default {:background {:h 200, :s 50, :l 60}}
:state/hover {:background {:h 200, :s 50, :l 60},
:border-color {:h 200, :s 50, :l 60}}}}
(compile-merged-control
{:variants/default {:state/default {:background {:h 10 :s 0 :l 30}}
:state/hover {:background {:h 10 :s 0 :l 40}}}
:variants/primary {:state/default {:background {:h 200 :s 50 :l 60}}}}
{:variants/default {:state/default {:background {:adjust {:h 10 :s 0 :l 30}}}}
:variants/primary {:state/hover {:border-color {:h 200 :s 50 :l 60}}}})))
(defn merge-used [used-controls]
(if (> (count used-controls) 1)
(reduce merge-controls used-controls)
(first used-controls)))
(defn compile-control-by-name--logic
"Compiles a control inside a trait graph"
[trait-graph control-name]
(let [controls (:theme/controls trait-graph)
control-def (get controls control-name)
conformed-uses (conform-uses (:control/uses control-def))
used-controls (mapv #(get controls %) conformed-uses)
merged-def (merge-used used-controls)
_ (def md merged-def)
_ (def cd control-def)
compiled-control (-> (compile-merged-control merged-def control-def)
(assoc :control/compiled? true))]
(assoc-in trait-graph [:theme/controls control-name] compiled-control)))
(def graph1
{:theme/gradient {:start {:h 340.5, :s 95, :l 91}, :end {:h 177.4, :s 66, :l 79}},
:theme/type :theme.type/light,
:theme/params {:params/base-font-family "-apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif",
:params/control-height "40px",
:params/control-spacing "16px"},
:theme/base {:primary-text {:h 220, :s 0, :l 98},
:pane {:h 258.95, :s 50.5, :l 95},
:primary {:h 220, :s 80, :l 60},
:input-bg {:h 258.95, :s 40.5, :l 90},
:focus-glow {:h 220, :s 80, :l 60, :a 0.3},
:danger {:h 0, :s 85, :l 55},
:border {:h 258.95, :s 50.5, :l 70},
:text {:h 258.95, :s 60.5, :l 35},
:secondary-bg {:h 258.95, :s 50.5, :l 80}},
:theme/states {:hover {:adjust {:h 0, :s 0, :l -5}},
:active {:adjust {:l -10}},
:focus {:glow {:h 220, :s 80, :l 60, :a 0.2}}},
:theme/contexts {:on-pane {:text {:h 258.95, :s 60.5, :l 35},
:pane {:h 258.95, :s 50.5, :l 95},
:border {:h 258.95, :s 50.5, :l 70, :a 0.8}},
:on-gradient {:text {:h 258.95, :s 60.5, :l 45}, :border {:h 258.95, :s 80.5, :l 65, :a 0.4}}},
:theme/gradient-avg {:h 258.95, :s 80.5, :l 85},
:theme/controls {:controls/basic {:control/sort-index 0
:variants/default {:state/default {:font-weight 400,
:cursor :pointer,
:font-size :16px,
:transition "all 0.2s ease"}}},
:controls/boxy {:control/sort-index 1
:control/uses :controls/basic,
:variants/default {:state/default {:height "40px",
:padding-inline "16px",
:border-radius :4px}}},
:controls/bordered {:control/sort-index 2
:control/uses :controls/boxy,
:variants/default {:state/default {:height "40px", :border "1px solid"}}},
:controls/button {:control/sort-index 3
:control/uses :controls/boxy,
:variants/default {:state/default {:background {:h 258.95, :s 50.5, :l 80},
:border-color {:h 258.95, :s 50.5, :l 70},
:color {:h 258.95, :s 60.5, :l 35}},
:state/hover {:background {:adjust {:l 5}},
:color {:adjust {:l 5}}},
:state/active {:background {:adjust {:l -10}},
:color {:adjust {:l -10}},
:margin-top :1px}},
:variants/primary {:state/default {:background {:h 220, :s 80, :l 60},
:color {:h 220, :s 0, :l 98}}}},
:controls/input {:control/sort-index 4
:control/uses :controls/bordered,
:variants/default {:state/default {:background {:h 258.95, :s 40.5, :l 90}}}},
:controls/link {:control/sort-index 5
:variants/default {:text-decoration "none", :transition "color 0.2s ease"}}}})
(defn compile-control-by-name
"Compiles a control inside a trait graph"
[trait-graph control-name]
(let [controls (:theme/controls trait-graph)
control-def (get controls control-name)]
(if (:control/compiled? control-def)
trait-graph
(let [uses (conform-uses (:control/uses control-def))
trait-graph (if-not uses trait-graph (reduce compile-control-by-name trait-graph uses))]
(compile-control-by-name--logic trait-graph control-name)))))
(compile-control-by-name graph1 :controls/button)
(defn compile-graph-controls
"Compiles the trait graph into a map of controls"
[trait-graph]
(let [controls (:theme/controls trait-graph)
atom:graph (atom trait-graph)]
(doseq [[control-name control-def] controls]
(let [new-graph (compile-control-by-name @atom:graph control-name)]
(reset! atom:graph new-graph)))
@atom:graph))
(defn compile-traits-system
"Compiles trait system by resolving all references and inheritance"
[compiled-colorscheme trait-graph]
(let [trait-graph-with-colors (merge compiled-colorscheme trait-graph)
resolve-ref (fn [node]
(if (path-ref? node)
(get-in trait-graph-with-colors node)
node))
resolve-state-props
(fn [node]
(if (map-entry? node)
(let [[key val] node]
(if (and (is-state-name? key) (map? val))
[key (map-values val resolve-ref)]
node))
node))
; First pass - resolve path references
trait-graph-with-refs (walk/postwalk
resolve-state-props
trait-graph-with-colors)]
(compile-graph-controls trait-graph-with-refs)))
;; trait system output
(def expected-compiled-trait-graph
{:theme/gradient {:start {:h 340.5, :s 95, :l 91}, :end {:h 177.4, :s 66, :l 79}},
:theme/type :theme.type/light,
:theme/params {:params/base-font-family "-apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif",
:params/control-height "40px",
:params/control-spacing "16px"},
:theme/base {:primary-text {:h 220, :s 0, :l 98},
:pane {:h 258.95, :s 50.5, :l 95},
:primary {:h 220, :s 80, :l 60},
:input-bg {:h 258.95, :s 40.5, :l 90},
:focus-glow {:h 220, :s 80, :l 60, :a 0.3},
:danger {:h 0, :s 85, :l 55},
:border {:h 258.95, :s 50.5, :l 70},
:text {:h 258.95, :s 60.5, :l 35},
:secondary-bg {:h 258.95, :s 50.5, :l 80}},
:theme/states {:hover {:adjust {:h 0, :s 0, :l -5}},
:active {:adjust {:l -10}},
:focus {:glow {:h 220, :s 80, :l 60, :a 0.2}}},
:theme/contexts {:on-pane {:text {:h 258.95, :s 60.5, :l 35},
:pane {:h 258.95, :s 50.5, :l 95},
:border {:h 258.95, :s 50.5, :l 70, :a 0.8}},
:on-gradient {:text {:h 258.95, :s 60.5, :l 45}, :border {:h 258.95, :s 80.5, :l 65, :a 0.4}}},
:theme/gradient-avg {:h 258.95, :s 80.5, :l 85},
:theme/controls {:controls/basic {:control/sort-index 0
:variants/default {:state/default {:font-weight 400,
:cursor :pointer,
:font-size :16px,
:transition "all 0.2s ease"}},
:control/compiled? true},
:controls/boxy {:control/sort-index 1
:control/compiled? true,
:control/uses :controls/basic,
:variants/default {:state/default {:font-weight 400,
:cursor :pointer,
:font-size :16px,
:transition "all 0.2s ease",
:height "40px",
:padding-inline "16px",
:border-radius :4px}}},
:controls/bordered {:control/sort-index 2
:control/compiled? true,
:control/uses :controls/boxy,
:variants/default {:state/default {:font-weight 400,
:cursor :pointer,
:font-size :16px,
:transition "all 0.2s ease",
:height "40px",
:padding-inline "16px",
:border-radius :4px,
:border "1px solid"}}},
:controls/button {:control/sort-index 3
:control/compiled? true,
:control/uses :controls/boxy,
:variants/primary {:state/hover {:color {:h 258.95, :s 60.5, :l 40},
:font-size :16px,
:transition "all 0.2s ease",
:font-weight 400,
:background {:h 258.95, :s 50.5, :l 85},
:cursor :pointer,
:border-color {:h 258.95, :s 50.5, :l 70},
:border-radius :4px,
:padding-inline "16px",
:height "40px"},
:state/active {:color {:h 258.95, :s 60.5, :l 25},
:font-size :16px,
:transition "all 0.2s ease",
:font-weight 400,
:margin-top :1px,
:background {:h 258.95, :s 50.5, :l 70},
:cursor :pointer,
:border-color {:h 258.95, :s 50.5, :l 70},
:border-radius :4px,
:padding-inline "16px",
:height "40px"},
:state/default {:color {:h 220, :s 0, :l 98},
:font-size :16px,
:transition "all 0.2s ease",
:font-weight 400,
:background {:h 220, :s 80, :l 60},
:cursor :pointer,
:border-color {:h 258.95, :s 50.5, :l 70},
:border-radius :4px,
:padding-inline "16px",
:height "40px"}},
:variants/default {:state/hover {:color {:h 258.95, :s 60.5, :l 40},
:font-size :16px,
:transition "all 0.2s ease",
:font-weight 400,
:background {:h 258.95, :s 50.5, :l 85},
:cursor :pointer,
:border-color {:h 258.95, :s 50.5, :l 70},
:border-radius :4px,
:padding-inline "16px",
:height "40px"},
:state/active {:color {:h 258.95, :s 60.5, :l 25},
:font-size :16px,
:transition "all 0.2s ease",
:font-weight 400,
:margin-top :1px,
:background {:h 258.95, :s 50.5, :l 70},
:cursor :pointer,
:border-color {:h 258.95, :s 50.5, :l 70},
:border-radius :4px,
:padding-inline "16px",
:height "40px"},
:state/default {:color {:h 258.95, :s 60.5, :l 35},
:font-size :16px,
:transition "all 0.2s ease",
:font-weight 400,
:background {:h 258.95, :s 50.5, :l 80},
:cursor :pointer,
:border-color {:h 258.95, :s 50.5, :l 70},
:border-radius :4px,
:padding-inline "16px",
:height "40px"}}},
:controls/input {:control/sort-index 4
:control/compiled? true,
:control/uses :controls/bordered,
:variants/default {:state/default {:font-size :16px,
:transition "all 0.2s ease",
:font-weight 400,
:background {:h 258.95, :s 40.5, :l 90},
:cursor :pointer,
:border "1px solid",
:border-radius :4px,
:padding-inline "16px",
:height "40px"}}},
:controls/link {:control/sort-index 5
:control/compiled? true,
:control/uses :controls/basic,
:variants/default {:state/default {:font-weight 400,
:cursor :pointer,
:font-size :16px,
:transition "color 0.2s ease",
:text-decoration "none"}}}}})
(assert
(= expected-compiled-trait-graph
(compile-traits-system dreamer-theme trait-system-graph)))
(defn compile-theme
"Compiles complete theme by combining color and trait systems"
[gradient color-graph trait-graph]
(let [compiled-colors (compile-color-system gradient color-graph)]
(compile-traits-system compiled-colors trait-graph)))
;; Compile the complete dreamer theme
(def compiled-dreamer-theme
(compile-theme dreamer-gradient light-color-system-graph trait-system-graph))
;;; CSS Generation from Trait System
(defn state-to-css-map [state-props]
(map-values state-props auto-hsl))
(defn state-id->selector [state-id]
(case state-id
:state/default nil
:state/hover "&:hover"
:state/active "&:active"
:state/focus "&:focus"
(str "&--" (name state-id))))
;; Format for Garden CSS
(defn variant-to-garden
"Compiles a variant into Garden CSS format with default state at top level
and other states nested with :& selectors"
[variant-selector variant-def]
(assert (:state/default variant-def) (str "please don't make null default states for now, " variant-selector))
(let [compile-state (fn [state-name]
(if-let [state-props (not-empty (get variant-def state-name))]
[(state-id->selector state-name)
(state-to-css-map state-props)]))
first-states (filterv
identity
[variant-selector
(state-to-css-map (:state/default variant-def))
;;
(compile-state :state/hover)
(compile-state :state/active)
(compile-state :state/focus)])
other-states (dissoc variant-def :state/default, :state/hover, :state/active, :state/focus)
nested-states (mapv compile-state (keys other-states))]
(into first-states nested-states)))
(variant-to-garden
".button"
(get-in expected-compiled-trait-graph [:theme/controls :controls/button :variants/default]))
(defn control-to-garden
"Compiles all variants of a control into Garden CSS format"
[control-name control-def]
(let [control-selector (str "." (name control-name))
default-variant-rules (variant-to-garden control-selector (:variants/default control-def))
variants (-> (filter-keys control-def is-variant-name?)
(dissoc :variants/default))
other-variants-rules (mapv
(fn [[variant-name variant-def]]
(let [variant-selector (str "&--" (name variant-name))]
(variant-to-garden variant-selector variant-def)))
variants)]
(into default-variant-rules other-variants-rules)))
(control-to-garden
:controls/button
(get-in expected-compiled-trait-graph [:theme/controls :controls/button]))
(defn theme-to-garden
"Compiles the theme into Garden CSS format"
[theme]
(let [controls (get-in theme [:theme/controls])
controls-sorted (sort-by (comp :control/sort-index val) controls)]
(doall
(for [[control-name control-def] controls-sorted]
(control-to-garden control-name control-def)))))
(theme-to-garden compiled-dreamer-theme)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment