Last active
April 21, 2025 11:48
-
-
Save spacegangster/cd0402b1820577150c5616e03c2a9689 to your computer and use it in GitHub Desktop.
Cascading Theme Compiler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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