├── src
├── .gitkeep
└── lambdaisland
│ ├── ornament
│ ├── clerk_util.clj
│ └── watcher.clj
│ └── ornament.cljc
├── test
├── .gitkeep
└── lambdaisland
│ └── ornament_test.cljc
├── .VERSION_PREFIX
├── bin
├── kaocha
└── proj
├── .dir-locals.el
├── bb.edn
├── .gitignore
├── .clj-kondo
├── config.edn
└── hooks
│ └── ornament.clj
├── tests.edn
├── dev
├── build_notebooks.clj
└── user.clj
├── notebooks
├── template.clj
├── demo.clj
├── ornament_next.clj
└── attributes_and_properties.clj
├── repl_sessions
├── cssparser.clj
└── poke.clj
├── deps.edn
├── .github
└── workflows
│ └── main.yml
├── CHANGELOG.md
├── pom.xml
├── LICENSE.txt
└── README.md
/src/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.VERSION_PREFIX:
--------------------------------------------------------------------------------
1 | 1.17
--------------------------------------------------------------------------------
/bin/kaocha:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | [[ -d node_modules ]] || npm install ws
4 |
5 | clojure -M:test -m kaocha.runner "$@"
6 |
--------------------------------------------------------------------------------
/.dir-locals.el:
--------------------------------------------------------------------------------
1 | ((nil . ((cider-clojure-cli-global-options . "-A:dev:test:byo")
2 | (cider-default-cljs-repl . browser))))
3 |
--------------------------------------------------------------------------------
/bb.edn:
--------------------------------------------------------------------------------
1 | {:deps
2 | {lambdaisland/open-source {:git/url "https://github.com/lambdaisland/open-source"
3 | :git/sha "52d82093bde661cd8c57b2f1e8cfaf854575a583"}}}
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .cpcache
2 | .nrepl-port
3 | target
4 | repl
5 | scratch.clj
6 | .shadow-cljs
7 | target
8 | yarn.lock
9 | node_modules/
10 | .DS_Store
11 | resources/public/ui
12 | .store
13 | out
14 | .#*
15 | package.json
16 | package-lock.json
17 | .clerk
18 | public
19 | deps.local.edn
20 |
--------------------------------------------------------------------------------
/.clj-kondo/config.edn:
--------------------------------------------------------------------------------
1 | {:lint-as {lambdaisland.ornament/defprop clojure.core/def
2 | lambdaisland.ornament/defrules clojure.core/def}
3 | :hooks {:analyze-call {lambdaisland.ornament/defstyled hooks.ornament/defstyled}}
4 | :linters {:lambdaisland.ornament/invalid-syntax
5 | {:level :warning}}}
6 |
--------------------------------------------------------------------------------
/bin/proj:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bb
2 |
3 | (ns proj
4 | (:require [lioss.main :as lioss]))
5 |
6 | (lioss/main
7 | {:license :mpl
8 | :inception-year 2021
9 | :description "Clojure Styled Components"
10 | :group-id "com.lambdaisland"
11 | :aliases-as-optional-deps [:byo]})
12 |
13 | ;; Local Variables:
14 | ;; mode:clojure
15 | ;; End:
16 |
--------------------------------------------------------------------------------
/tests.edn:
--------------------------------------------------------------------------------
1 | #kaocha/v1
2 | {:plugins [#_:notifier
3 | :print-invocations
4 | :profiling]
5 | :tests [{:id :clj}
6 | {:id :cljs
7 | :type :kaocha.type/cljs
8 | :cljs/repl-env cljs.repl.browser/repl-env
9 | :cljs/timeout 20000}]
10 | :bindings {#_#_kaocha.type.cljs/*debug* true
11 | kaocha.stacktrace/*stacktrace-filters* []}}
12 |
--------------------------------------------------------------------------------
/dev/build_notebooks.clj:
--------------------------------------------------------------------------------
1 | (ns build-notebooks
2 | "Build notebooks as a static app on CI"
3 | (:require
4 | [clojure.java.io :as io]
5 | [nextjournal.clerk :as clerk]))
6 |
7 | (defn -main [sha]
8 | (clerk/build-static-app!
9 | {:paths (->> (file-seq (io/file "notebooks"))
10 | (remove (memfn ^java.io.File isDirectory))
11 | (map str))
12 | :bundle? false
13 | :path-prefix (str "ornament/sha/" sha "/")
14 | :git/sha sha
15 | :git/url "https://github.com/lambdaisland/ornament"
16 | :browse? false}))
17 |
--------------------------------------------------------------------------------
/notebooks/template.clj:
--------------------------------------------------------------------------------
1 | (ns notebooks.template
2 | (:require
3 | [lambdaisland.ornament :as o]
4 | [lambdaisland.ornament.clerk-util :refer [inline-styles render]]))
5 |
6 | ;; # Ornament Notebook Template
7 |
8 | ;; Define components
9 |
10 | (o/defstyled strong-link :a
11 | {:font-weight 1000})
12 |
13 | ;; Render them with Hiccup
14 |
15 | (render
16 | [strong-link {:href "https://github.com/lambdaisland/open-source"}
17 | "Check out our open source offerings!"])
18 |
19 | ;; Inline our styles last, so that this happens after all `defstyled`s are
20 | ;; defined.
21 |
22 | ^{:nextjournal.clerk/no-cache true}
23 | (inline-styles)
24 |
--------------------------------------------------------------------------------
/dev/user.clj:
--------------------------------------------------------------------------------
1 | (ns user)
2 |
3 | (defmacro jit [sym]
4 | `(requiring-resolve '~sym))
5 |
6 | (defn browse []
7 | ((jit clojure.java.browse/browse-url) "http://localhost:7777"))
8 |
9 | (def portal-instance (atom nil))
10 |
11 | (defn portal
12 | "Open a Portal window and register a tap handler for it. The result can be
13 | treated like an atom."
14 | []
15 | ;; Portal is both an IPersistentMap and an IDeref, which confuses pprint.
16 | (prefer-method @(jit clojure.pprint/simple-dispatch) clojure.lang.IPersistentMap clojure.lang.IDeref)
17 | (let [p ((jit portal.api/open) @portal-instance)]
18 | (reset! portal-instance p)
19 | (add-tap (jit portal.api/submit))
20 | p))
21 |
22 | (defn clerk! []
23 | ((jit nextjournal.clerk/serve!) {:watch-paths ["notebooks"]}))
24 |
--------------------------------------------------------------------------------
/src/lambdaisland/ornament/clerk_util.clj:
--------------------------------------------------------------------------------
1 | (ns lambdaisland.ornament.clerk-util
2 | (:require [lambdaisland.hiccup :as hiccup]
3 | [lambdaisland.ornament :as o]
4 | [nextjournal.clerk :as clerk]))
5 |
6 | (defn render
7 | "Render hiccup containing Ornament component references inside a Clerk
8 | notebook."
9 | [h]
10 | (clerk/html (hiccup/render h {:doctype? false})))
11 |
12 | (defn inline-styles
13 | "Inject our CSS styles into the Clerk document, so components render correctly.
14 | Add this at the end of your notebook, and add a 'no-cache' marker.
15 |
16 | ```
17 | ^{::clerk/no-cache true}
18 | (util/inline-styles)
19 | ```
20 | "
21 | []
22 | (render [:style (o/defined-styles)]))
23 |
24 | (defn expand
25 | "Expand a hiccup form with an ornament component to plain hiccup elements. Does not recurse."
26 | [[component & args]]
27 | (o/as-hiccup component args))
28 |
--------------------------------------------------------------------------------
/repl_sessions/cssparser.clj:
--------------------------------------------------------------------------------
1 | (ns repl-sessions.cssparser
2 | (:require [clojure.java.io :as io])
3 | (:import (com.steadystate.css.parser CSSOMParser SACParserCSS3 HandlerBase )
4 | (org.w3c.css.sac InputSource DocumentHandler)))
5 |
6 | ;; https://javadoc.io/static/net.sourceforge.cssparser/cssparser/0.9.11/com/steadystate/css/parser/CSSOMParser.html
7 |
8 | (def css3-parser (SACParserCSS3.))
9 | (def parser (CSSOMParser. css3-parser))
10 |
11 | (.setDocumentHandler css3-parser
12 | ^DocumentHandler
13 | (proxy [HandlerBase] []
14 | (ignorableAtRule [x y]
15 | (prn [x y]))))
16 |
17 | (def s
18 | (rand-nth (seq (.getRules
19 | (.getCssRules
20 | (.parseStyleSheet
21 | parser
22 | (InputSource. (io/reader (io/file "/home/arne/ARS/ductile/stylesheets/jit.css")))
23 | nil
24 | nil))))))
25 | (bean (.getStyle s))
26 |
--------------------------------------------------------------------------------
/notebooks/demo.clj:
--------------------------------------------------------------------------------
1 | (ns demo
2 | (:require
3 | [lambdaisland.hiccup :as hiccup]
4 | [lambdaisland.ornament :as o]
5 | [nextjournal.clerk :as clerk]))
6 |
7 | ;; # A Small Demonstration of Ornament
8 |
9 | ;; Helper to render components:
10 |
11 | (defn render [h]
12 | (clerk/html (hiccup/render h {:doctype? false})))
13 |
14 | ;; A relatively simple component, using Girouette (Tailwind-style) styling, and
15 | ;; leaning into the fact that we can organize our styles however we like,
16 | ;; including splitting things up and adding comments.
17 |
18 | (o/defstyled navbar :nav
19 | ;; layout
20 | :flex :space-x-4
21 | [:a :px-3 :py-2 :my-2]
22 | ;; fonts & borders
23 | :font-sans
24 | [:a :text-sm :font-medium :rounded-md]
25 | ;; colors
26 | :bg-gray-800
27 | [:a :text-gray-300 :hover:bg-gray-700 :hover:text-white
28 | :rounded-md :text-sm :font-medium
29 | [:&.active :bg-gray-900 :text-white]]
30 | ([links]
31 | (for [[{:keys [text href active?]}] links]
32 | [(if active? :a.active :a)
33 | {:href href}
34 | text])))
35 |
36 | ;; Let's see what that looks
37 |
38 | (render
39 | [navbar
40 | [[{:text "Lambda Island"
41 | :href "https://lambdaisland.com"
42 | :active? true}]
43 | [{:text "Gaiwan"
44 | :href "https://gaiwan.co"}]]])
45 |
46 | ;; We can also inspect all aspects of the component
47 |
48 | (o/as-garden navbar)
49 |
50 | (o/css navbar)
51 |
52 | (navbar [[{:text "Lambda Island"
53 | :href "https://lambdaisland.com"
54 | :active? true}]
55 | [{:text "Gaiwan"
56 | :href "https://gaiwan.co"}]])
57 |
58 | ;; Inline our styles last, so all component styles are certainly defined.
59 |
60 | ^{::clerk/no-cache true}
61 | (render [:style (o/defined-styles)])
62 |
--------------------------------------------------------------------------------
/repl_sessions/poke.clj:
--------------------------------------------------------------------------------
1 | (ns poke
2 | (:require
3 | [lambdaisland.ornament :as o]
4 | [lambdaisland.hiccup :as hiccup]))
5 |
6 | (set! *print-namespace-maps* false)
7 |
8 | (o/defstyled freebies-link :a
9 | {:font-size "1rem"
10 | :color "#cff9cf"
11 | :text-decoration "underline"})
12 |
13 | (o/rules freebies-link)
14 |
15 | (freebies-link {:href "/episodes/interceptors-concepts"} "hello")
16 |
17 | [:a {:class ["poke__freebies_link"]
18 | :href "/episodes/interceptors-concepts"} "hello"]
19 |
20 | (o/defstyled foo :div
21 | {:margin size-2})
22 | (o/css foo)
23 | (o/defprop size-2 "2rem")
24 | (o/defrules main-styles
25 | "Main application styles"
26 | [:.link {:color "blue"}]
27 | [:.link:visited {:color "purple"}]
28 | [:main
29 | [:.container {:width size-2}]])
30 |
31 | (garden.compiler/expand size-2)
32 | main-styles
33 | (o/defutil square {:aspect-ratio 1})
34 | o/props-registry
35 | (o/defined-garden)
36 | (o/defined-styles)
37 | (o/defstyled avatar :img
38 | {size-2 size-2}
39 | #_#_(garden.stylesheet/at-media {"print" true} [:& {:color "blue"}])
40 | (garden.stylesheet/at-keyframes "myanim" [:100% {:height "10px"}])
41 | )
42 | (#'garden.compiler/expand-stylesheet {size-2 size-2})
43 | (garden.compiler/compile-css [:& {size-2 size-2}])
44 |
45 | (hiccup/render [avatar {:style {size-2 "3rem"}}])
46 | (map class (o/process-rules
47 | (o/rules avatar)))
48 | duration |
49 | easing-function |
50 | delay |
51 | iteration-count |
52 | direction |
53 | fill-mode |
54 | play-state |
55 | name
56 |
57 | (o/defanimation pulse
58 | :duration
59 | "2s"
60 | :keyframes
61 | ["0%" "100%" {:opacity 1}]
62 | ["50%" {:opacity 0.5}])
63 |
64 | (o/css avatar)
65 |
66 | *e
67 | (o/defined-styles)
68 |
69 | (hiccup/render [:div {:class [square]}])
70 |
71 | ()
72 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 |
3 | :deps
4 | {org.clojure/clojure {:mvn/version "1.12.0"}
5 | com.lambdaisland/garden {:mvn/version "1.7.590"}
6 | girouette/girouette {:mvn/version "0.0.10"}
7 | meta-merge/meta-merge {:mvn/version "1.0.0"}}
8 |
9 | :aliases
10 | {:dev
11 | {:extra-paths ["dev"]
12 | :extra-deps {io.github.nextjournal/clerk {:mvn/version "0.17.1102"}
13 | com.lambdaisland/hiccup {:mvn/version "0.14.67"} }}
14 |
15 | :byo
16 | {:extra-deps {hawk/hawk {:mvn/version "0.2.11"}
17 | com.lambdaisland/glogi {:mvn/version "1.3.169"}
18 | io.pedestal/pedestal.log {:mvn/version "0.7.2"}}}
19 |
20 | :test
21 | {:extra-paths ["test"]
22 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}
23 | lambdaisland/kaocha-cljs {:mvn/version "1.5.154"}
24 | org.clojure/clojurescript {:mvn/version "1.11.132"}
25 | com.lambdaisland/glogi {:mvn/version "1.3.169"}
26 | ;; for lambdaisland.hiccup and lambdaisland.thicc, used in testing
27 | lambdaisland/webstuff {:git/url "https://github.com/lambdaisland/webstuff"
28 | :git/sha "f3ae2a2d41a4335d3da1757a3a21aa1dd1125eb1"
29 | #_#_:local/root "/home/arne/github/lambdaisland/webstuff"}}}
30 |
31 | :cssparser
32 | {:extra-deps {net.sourceforge.cssparser/cssparser {:mvn/version "0.9.30"}}}
33 |
34 | :nextjournal/clerk
35 | {:exec-fn nextjournal.clerk/build!
36 | :exec-args {:paths ["notebooks/demo.clj"
37 | "notebooks/attributes_and_properties.clj"]}
38 | :nextjournal.clerk/aliases [:dev]}}}
39 |
--------------------------------------------------------------------------------
/.clj-kondo/hooks/ornament.clj:
--------------------------------------------------------------------------------
1 | (ns hooks.ornament
2 | (:require [clj-kondo.hooks-api :as api]))
3 |
4 | (defn defstyled [{:keys [node]}]
5 | (let [[class-name html-tag & more] (rest (:children node))
6 | _ (when-not (and (api/token-node? class-name)
7 | (simple-symbol? (api/sexpr class-name)))
8 | (api/reg-finding! {:row (:row (meta class-name))
9 | :col (:col (meta class-name))
10 | :message "Style name must be a symbol"
11 | :type :lambdaisland.ornament/invalid-syntax}))
12 | ; _ (prn :class-name class-name)
13 | _ (when-not (api/keyword-node? html-tag)
14 | (api/reg-finding! {:row (:row (meta html-tag))
15 | :col (:col (meta html-tag))
16 | :message "Tag must be a keyword or an ornament-styled-component"
17 | :type :lambdaisland.ornament/invalid-syntax}))
18 | ; _ (prn :html-tag html-tag)
19 | ; _ (prn :more more)
20 | fn-tag (first (drop-while (fn [x]
21 | (or (api/string-node? x)
22 | (api/keyword-node? x)
23 | (api/map-node? x)
24 | (api/vector-node? x)))
25 | more))
26 | ; _ (prn :fn-tag fn-tag)
27 | _ (when (and fn-tag
28 | (not (api/list-node? fn-tag)))
29 | (api/reg-finding! {:row (:row (meta fn-tag))
30 | :col (:col (meta fn-tag))
31 | :message "Function part (if present) must be a list"
32 | :type :lambdaisland.ornament/invalid-syntax}))]
33 | (if (api/list-node? fn-tag)
34 | (let [[binding-vec & body] (:children fn-tag)
35 | fn-node (api/list-node
36 | (list*
37 | (api/token-node 'fn)
38 | binding-vec
39 | body))
40 | new-def-node (api/list-node
41 | (list (api/token-node 'def)
42 | class-name
43 | fn-node))]
44 | (prn :new-def-node (api/sexpr new-def-node))
45 | {:node new-def-node})
46 | ;; nil node
47 | (let [def-class-form (api/list-node
48 | (list (api/token-node 'def)
49 | class-name
50 | (api/token-node 'nil)))]
51 | (prn :def-class-form (api/sexpr def-class-form))
52 | {:node def-class-form}))))
53 |
--------------------------------------------------------------------------------
/src/lambdaisland/ornament/watcher.clj:
--------------------------------------------------------------------------------
1 | (ns lambdaisland.ornament.watcher
2 | "Watch the filesystem for changes, and regenerate the Ornament CSS file
3 |
4 | We generally combine this with Figwheel, and let figwheel handle reloading the
5 | CLJ files, as well as hot-loading the new CSS in the browser.
6 |
7 | For shadow-cljs use build-hooks, see the Ornament README for an example.
8 |
9 | Hawk and Glögi are BYO (you need to declare the dependencies yourself.)"
10 | (:require [clojure.java.io :as io]
11 | [hawk.core :as hawk]
12 | [lambdaisland.glogc :as log]
13 | [lambdaisland.ornament :as ornament])
14 | (:import [java.util Timer TimerTask]))
15 |
16 | (defn debounced
17 | "Debounce a function, it will be called at most once every delay-ms
18 | milliseconds."
19 | [f delay-ms]
20 | (let [timer (Timer.)
21 | last-task (atom nil)]
22 | (fn [& args]
23 | (let [task (proxy [TimerTask] [] (run [] (apply f args)))]
24 | (swap! last-task
25 | (fn [prev]
26 | (when prev (.cancel ^TimerTask prev))
27 | (.schedule ^Timer timer ^TimerTask task delay-ms)
28 | task)))
29 | nil)))
30 |
31 | (defn requires-ornament?
32 | "Does this Clojure file require the Ornament namespace?"
33 | [f]
34 | (try
35 | (with-open [rdr (-> f io/file io/reader java.io.PushbackReader.)]
36 | (->> rdr
37 | (read {:features #{:clj} :read-cond :allow})
38 | flatten
39 | (some '#{lambdaisland.ornament})))
40 | (catch Exception _
41 | false)))
42 |
43 | (defn make-output-fn [{:keys [outfile]
44 | :or {outfile "resources/public/css/compiled/ornament.css"}}]
45 | (debounced
46 | (fn []
47 | (log/debug :ornament-watcher/writing outfile)
48 | (io/make-parents outfile)
49 | (spit outfile (ornament/defined-styles)))
50 | 1000))
51 |
52 | (defn make-hawk-handler [opts]
53 | (let [write-ornament-css! (make-output-fn opts)]
54 | (fn [ctx {:keys [kind file]}]
55 | (when (requires-ornament? file)
56 | (write-ornament-css!)
57 | (when-let [cb (:callback opts)]
58 | (cb))))))
59 |
60 | (defn start-watcher!
61 | "Start a watcher which recreates the ornament CSS output file when source
62 | namespaces change.
63 |
64 | - `:watch-paths` The source directories to watch
65 | - `:outfile` The CSS file to write to
66 | - `:callback` Optional function to call after the CSS updates"
67 | [{:keys [watch-paths]
68 | :or {watch-paths ["src"]}
69 | :as opts}]
70 | (hawk/watch! [{:paths ["src"]
71 | :handler (make-hawk-handler opts)}]))
72 |
73 | (defn stop-watcher! [hawk]
74 | (hawk/stop! hawk))
75 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Delivery
2 |
3 | on: push
4 |
5 | jobs:
6 | Kaocha:
7 | runs-on: ${{matrix.sys.os}}
8 |
9 | strategy:
10 | matrix:
11 | sys:
12 | # - { os: macos-latest, shell: bash }
13 | - { os: ubuntu-latest, shell: bash }
14 | # - { os: windows-latest, shell: powershell }
15 |
16 | defaults:
17 | run:
18 | shell: ${{matrix.sys.shell}}
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | with:
23 | fetch_depth: 0
24 |
25 | - name: 🔧 Install java
26 | uses: actions/setup-java@v1
27 | with:
28 | java-version: '11.0.7'
29 |
30 | - name: 🔧 Install clojure
31 | uses: DeLaGuardo/setup-clojure@master
32 | with:
33 | cli: '1.10.3.943'
34 |
35 | - name: 🗝 maven cache
36 | uses: actions/cache@v4
37 | with:
38 | path: |
39 | ~/.m2
40 | ~/.gitlibs
41 | key: ${{ runner.os }}-maven-${{ github.sha }}
42 | restore-keys: |
43 | ${{ runner.os }}-maven-
44 |
45 | - name: 🧪 Run tests
46 | run: bin/kaocha clj
47 |
48 |
49 | Clerk-build:
50 | runs-on: ubuntu-latest
51 | steps:
52 | - uses: actions/checkout@v2
53 | with:
54 | fetch_depth: 0
55 |
56 | - name: 🔧 Install java
57 | uses: actions/setup-java@v1
58 | with:
59 | java-version: '11.0.7'
60 |
61 | - name: 🔧 Install clojure
62 | uses: DeLaGuardo/setup-clojure@master
63 | with:
64 | cli: '1.10.3.943'
65 |
66 | - name: 🗝 maven cache
67 | uses: actions/cache@v4
68 | with:
69 | path: |
70 | ~/.m2
71 | ~/.gitlibs
72 | key: ${{ runner.os }}-maven-${{ github.sha }}
73 | restore-keys: |
74 | ${{ runner.os }}-maven-
75 |
76 | - name: 🗝 Clerk Cache
77 | uses: actions/cache@v4
78 | with:
79 | path: .clerk
80 | key: ${{ runner.os }}-clerk-cache
81 |
82 | - name: 🏗 Build Clerk Static App with default Notebooks
83 | run: clojure -A:dev:test:byo -M -m build-notebooks '${{ github.sha }}'
84 |
85 | - name: 🔐 Google Auth
86 | uses: google-github-actions/auth@v0
87 | with:
88 | credentials_json: ${{ secrets.GCLOUD_SERVICE_KEY_JSON }}
89 |
90 | - name: 🔧 Setup Google Cloud SDK
91 | uses: google-github-actions/setup-gcloud@v0.3.0
92 |
93 | - name: 📠 Copy static build to bucket under SHA
94 | run: |
95 | gsutil cp -r public/build gs://lambdaisland-notebooks/ornament/sha/${{ github.sha }}
96 | gsutil cp -r public/build gs://lambdaisland-notebooks/ornament/branch/${{ github.ref_name }}
97 |
98 | - name: ✅ Add success status to report with link to snapshot
99 | uses: Sibz/github-status-action@v1
100 | with:
101 | authToken: ${{secrets.GITHUB_TOKEN}}
102 | context: 'Browse Clerk Notebooks'
103 | description: 'Ready'
104 | state: 'success'
105 | sha: ${{github.event.pull_request.head.sha || github.sha}}
106 | target_url: https://notebooks.lambdaisland.com/ornament/sha/${{ github.sha }}
107 |
--------------------------------------------------------------------------------
/notebooks/ornament_next.clj:
--------------------------------------------------------------------------------
1 | (ns ornament-next
2 | (:require
3 | [lambdaisland.hiccup :as hiccup]
4 | [lambdaisland.ornament :as o]
5 | [nextjournal.clerk :as clerk]))
6 |
7 | (reset! o/registry {})
8 | (reset! o/rules-registry {})
9 | (reset! o/props-registry {})
10 |
11 | ;; Original Ornament was all about styled components, meaning we put
12 | ;; Garden-syntax CSS inside your components to style them. Later on we added
13 | ;; support for Girouette, which means you can use shorthand tags similar to
14 | ;; Tailwind utility classes to define your style rules.
15 |
16 | ;; This is great, but it's not the full story. Ornament Next gives you several
17 | ;; news ways to define and structure your styles, and better dev-time
18 | ;; affordances.
19 |
20 | ;; Here's a regular old Ornament styled component, except that it now sports a
21 | ;; docstring. The docstring that actually gets set on the var also contains the
22 | ;; compiled CSS, and we set `:arglists`, so you can see how to use it aas a
23 | ;; component in your Hiccup.
24 |
25 | (o/defstyled user-form :form
26 | "Form used on the profile page"
27 | :mx-3)
28 |
29 | (:arglists (meta #'user-form))
30 | (:doc (meta #'user-form))
31 |
32 | ;; The new macros that follow all support docstrings.
33 |
34 | ;; ## defrules
35 |
36 | ;; The most basic one is `defrules`, which lets you define plain Garden CSS
37 | ;; rules that get prepended to your Ornament styles. Realistically there are
38 | ;; always still things you define globally, and you shouldn't have to jump
39 | ;; through extra hoops to do so. `defrules` still takes a name and optionally a
40 | ;; docstring, so you can split up your styles and document them.
41 |
42 | (o/defrules my-style
43 | "Some common defaults"
44 | [:* {:box-sizing "border-box"}]
45 | [:form :mx-2])
46 |
47 | ;; Ornament features like tailwing utilities or referencing components work here
48 | ;; too.
49 |
50 | (o/defstyled menu :nav
51 | :hidden)
52 |
53 | (o/defrules toggle-menu
54 | [:body.menu-open
55 | [menu :block]])
56 |
57 | (o/defined-garden)
58 |
59 | ;; ## defutil
60 |
61 | ;; There's now also `defutil` for defining utility classes. This is in a way
62 | ;; similar, in that it defines global CSS, but you get a handle onto something
63 | ;; that you can use like a CSS class.
64 |
65 | (o/defutil square
66 | "Ensure the element has the same width and height."
67 | {:aspect-ratio 1})
68 |
69 | ;; This creates a utility class in your CSS. Note that it's namespaced, like all
70 | ;; classes in Ornament, to be collision free.
71 |
72 | (o/defined-styles)
73 |
74 | ;; You can now use this in multiple ways, the simplest is direcly in hiccup.
75 |
76 | (hiccup/render [:img {:class [square]}])
77 |
78 |
79 | ;; You can also use it in styled components, to pull those additional style
80 | ;; rules into the CSS of the component.
81 |
82 | (o/defstyled avatar :img
83 | "A square avatar"
84 | square)
85 |
86 | (o/css avatar)
87 |
88 | ;; ## defprop
89 |
90 | ;; Modern CSS heavily leans on CSS custom properties, also known as variables.
91 | ;; These are especially useful for defining design tokens.
92 |
93 | ;; These can be defined with or without
94 |
95 | (o/defprop --without-default)
96 | (o/defprop --color-primary "hsla(201, 100%, 50%, 1)")
97 |
98 |
99 | (hiccup/render [:img {:style {:backgroun-color --color-primary}}])
100 |
101 | (o/defined-styles)
102 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Unreleased
2 |
3 | ## Added
4 |
5 | ## Fixed
6 |
7 | ## Changed
8 |
9 | # 1.17.150 (2025-10-08 / 3c39b56)
10 |
11 | ## Fixed
12 |
13 | - Make CSSProps implement print-dup, so they can be AOTd
14 |
15 | # 1.16.141 (2025-04-29 / 8c00784)
16 |
17 | ## Changed
18 |
19 | - Only include compiled CSS in cljs docstrings when the cljs optimization level
20 | is `:none`
21 |
22 | # 1.15.138 (2025-04-24 / 8299d3c)
23 |
24 | ## Fixed
25 |
26 | - Add a require-macros so defstyled can be referred from cljs directly
27 |
28 | # 1.14.134 (2025-04-24 / dadcb61)
29 |
30 | ## Fixed
31 |
32 | - Deal with more edge cases when referencing tokens inside style rules
33 |
34 | # 1.13.130 (2025-04-16 / 83c295f)
35 |
36 | ## Changed
37 |
38 | - [BREAKING] When setting a custom `:ornament/prefix` on the namespace, the
39 | separator `__` is no longer implied, to get the same result add `__` to the
40 | end of your prefix string.
41 |
42 | ## Added
43 |
44 | - Support docstrings, they come after the tagname, before any styles or tokens
45 | - If there's only a zero-arg render function (fn-tail), also emit a one-arg
46 | version that takes HTML attributes to be merged in.
47 | - Add `defrules`, for general garden CSS rules
48 | - Add `defprop`, for CSS custom properties (aka variables)
49 | - Add `defutil`, for standalone utility classes
50 | - Add `import-tokens!`, for importing W3C design token JSON files as properties (as per `defprop`)
51 | - Allow setting metadata on a child list, useful for reagent/react keys
52 |
53 | ## Fixed
54 |
55 | - Fix `defined-garden`
56 | - Use of `defrules` in pure-cljs namespaces
57 | - Fix implementation of ILookup on cljs
58 |
59 | # 1.12.107 (2023-09-27 / 2444e34)
60 |
61 | ## Fixed
62 |
63 | - Fix component resolution inside a set (in a rule of another component) (see tests for example)
64 |
65 | # 1.11.101 (2023-09-13 / 213279d)
66 |
67 | ## Fixed
68 |
69 | - Allow reusing the styles of one component directly inside another (see tests for example)
70 |
71 | # 1.10.94 (2023-08-30 / d1e1c3b)
72 |
73 | ## Fixed
74 |
75 | - Support using `defstyled` components as reagent form-2 components
76 |
77 | # 0.9.87 (2023-04-15 / dac82f4)
78 |
79 | ## Added
80 |
81 | - Added a `:tw-version` flag for the preflight, similar to `set-tokens!`
82 | - Document how to opt-in to Tailwind v3
83 |
84 | # 0.8.84 (2023-02-28 / 8d54daa)
85 |
86 | ## Added
87 |
88 | - Implement inheritance for fn-tails
89 |
90 | # 0.7.77 (2022-11-25 / a1f8d65)
91 |
92 | ## Added
93 |
94 | - Add Clerk garden setup
95 |
96 | ## Fixed
97 |
98 | - improved way to handle girouette v2 and v3 tokens
99 |
100 | # 0.6.69 (2022-10-11 / a629407)
101 |
102 | ## Fixed
103 |
104 | - Fixed an issue withe direct invocation of components with a render function (tail-fn)
105 |
106 | # 0.5.65 (2022-09-20 / 94cbebe)
107 |
108 | ## Added
109 |
110 | - Support attributes when using a top-level fragment in a rendering function
111 |
112 | # 0.4.34 (2022-01-25 / df056c8)
113 |
114 | ## Fixed
115 |
116 | - Fix cljdoc build
117 |
118 | # 0.3.30 (2022-01-25 / d37c5e4)
119 |
120 | ## Fixed
121 |
122 | - Improve ClojureScript support, in particular referencing components in other components style rules
123 | - Support vectors with multiple selectors, plus alternative syntax with sets
124 |
125 | # 0.2.19 (2021-11-29 / 6c8e226)
126 |
127 | ## Fixed
128 |
129 | - Fix issue where girouette tokens were not being applied to child elements. [See Github Issue](https://github.com/lambdaisland/ornament/issues/5)
130 |
131 | ## Changed
132 |
133 | - Bump Girouette to 0.0.6
134 |
135 | # 0.1.12 (2021-10-25 / d0a739b)
136 |
137 | ## Changed
138 |
139 | - Bump Girouette to 0.0.5
140 |
141 | # 0.0.7 (2021-10-01 / 52aa304)
142 |
143 | ## Added
144 |
145 | - Initial implementation
146 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
hello
" 269 | 270 | [with-body-derived "hello"] 271 | "hello
" 272 | 273 | ;; we're getting inconsistent but equivalent rendering here between clj and 274 | ;; cljs. Not ideal, but not a big deal either. Working around with reader 275 | ;; conditionals. 276 | ;; FIXME: write this in a more robust way, maintaining this is becoming a PITA 277 | [attrs-in-fragment "hello"] 278 | "hello
" 642 | ``` 643 | 644 | You can put multiple of these to deal with multiple arities 645 | 646 | ```clojure 647 | (o/defstyled multi-arity :p 648 | ([arg1] 649 | [:strong arg1]) 650 | ([arg1 arg2] 651 | [:<> 652 | [:strong arg1] [:em arg2]])) 653 | ``` 654 | 655 | Without render functions a styled component works almost like a plain HTML tag 656 | when using in Hiccup: the first argument, if it's a map, is treated as a map of 657 | HTML attributes, any following arguments are treated as children. 658 | 659 | When you supply your own render function this behavior changes. All arguments 660 | are passed to the render function, which then determines the element's 661 | attributes and children. 662 | 663 | To set custom attributes on the outer element from inside the render function, 664 | you use a properties map together with a fragment `:<>` identifier: 665 | 666 | ```clojure 667 | (o/defstyled my-compo :div 668 | ([props] 669 | [:<> {:title "hello"} "hello!"])) 670 | ``` 671 | 672 | If you pass a `:class` here it will get added to the class that Ornament 673 | generates for the component. 674 | 675 | When using a component that has a custom render function, you can set attributes 676 | by using the special `:lambdaisland.ornament/attrs` keyword. 677 | 678 | ```clojure 679 | [my-compo {:regular-prop 123 ::o/attrs {:title "heyo"}}] 680 | ``` 681 | 682 | Any `:class` or `:style` attributes passed in this way will be added to any 683 | classes or styles set inside the render function with `:<>`. Optionally for 684 | `:class` and `:style` you can replace the values instead of appending by adding 685 | a `^:replace` metadata on the vector / map. 686 | 687 | ```clojure 688 | [my-compo {::o/attrs {:class ^:replace ["one-class" "other-class"] 689 | :style {:text-color "blue"}}}] 690 | ``` 691 | 692 | In previous versions we supported `:class`, `:id` and `:style` at the top of the 693 | properties map, but that's no longer the case. 694 | 695 | There's an additional mechanic for setting attributes from inside the 696 | render-function, through metadata on the return value, but it is considered 697 | deprecated, since it's superseded by `[:<> {,,,attrs,,,}]`. 698 | 699 | ```clojure 700 | (o/defstyled nav-link :a 701 | ([{:keys [id]}] 702 | (let [{:keys [url title description]} (get-route id)] 703 | ^{:href url :title description} 704 | [:<> title]))) 705 | 706 | ;;=> 707 | Videos 708 | ``` 709 | 710 | ## Differences from Garden 711 | 712 | The rules section of a component is essentially 713 | [Garden](https://github.com/noprompt/garden) syntax. We run it through the 714 | Garden compiler, and so things that work in Garden generally work there as well, 715 | with some exceptions. 716 | 717 | Keywords that come first inside a vector are always treated as CSS selectors, as 718 | you would expect, but if they occur elsewhere then we first pass them to 719 | Girouette to expand to style rules class names. If Girouette does not recognize 720 | the keyword as a classname, then it's preserved in the Garden as-is. 721 | 722 | That means that generally things work as expected, since selectors and Girouette 723 | classes don't have much overlap. 724 | 725 | ```clojure 726 | ;; ✔️ :ol is recognized as a selector 727 | 728 | (o/defstyled list-wrapper :div 729 | [:ul :ol {:background-color "blue"}]) 730 | 731 | (o/css list-wrapper) 732 | ;; => ".ot__list_wrapper ul,.ot__list_wrapper ol{background-color:blue}" 733 | 734 | ;; ✔️ :bg-blue-500 is recognized as a utility class 735 | 736 | (o/defstyled list-wrapper :div 737 | [:ul :bg-blue-500]) 738 | 739 | (o/css list-wrapper) 740 | ;; => ".ot__list_wrapper ul{--gi-bg-opacity:1;background-color:rgba(59,130,246,var(--gi-bg-opacity))}" 741 | ``` 742 | 743 | But there is some potential for clashes, e.g. Girouette has a `:table` class. 744 | 745 | ```clojure 746 | ;; ❌ not what we wanted 747 | 748 | (o/defstyled fig-wrapper :div 749 | [:figure :table {:padding "1rem"}]) 750 | 751 | (o/css fig-wrapper) 752 | ;; => ".ot__fig_wrapper figure{display:table;padding:1rem}" 753 | ``` 754 | 755 | Instead use a set to make it explicit that these are multiple selectors. It's 756 | good practice to do this in general since it is more explicit and reduces 757 | ambiguity and chance of clashes. 758 | 759 | ```clojure 760 | (o/defstyled fig-wrapper :div 761 | [#{:figure :table} {:padding "1rem"}]) 762 | 763 | (o/css fig-wrapper) 764 | ;; => ".ot__fig_wrapper figure,.ot__fig_wrapper table{padding:1rem}" 765 | ``` 766 | 767 | ### Garden Extensions 768 | 769 | Ornament does a certain amount of pre-processing before passing the rules over 770 | to Garden for compilation. This allows us to support some extra syntax which we 771 | find more convenient. 772 | 773 | ### Special "tags" 774 | 775 | Use these as the first element in a vector to opt into special handling. Some of 776 | these are used where a selector would be used, others are helpers for defining 777 | property values. 778 | 779 | - `:at-media` 780 | 781 | You can add breakpoints for responsiveness to your components with `:at-media`. 782 | 783 | ```clojure 784 | (o/defstyled eps-container :div 785 | {:display "grid" 786 | :grid-gap "1rem" 787 | :grid-template-columns "repeat(auto-fill, minmax(20rem, 1fr))" 788 | :padding "0 1rem 1rem"} 789 | [:at-media {:min-width "40rem"} 790 | {:grid-gap "2rem" 791 | :padding "0 2rem 2rem"}]) 792 | ``` 793 | 794 | - `:cssfn` 795 | 796 | CSS functions can be invoked with `:cssfn` 797 | 798 | ```clojure 799 | (o/defstyled with-css-fn :a 800 | [:&:after {:content [:cssfn :attr "href"]}]) 801 | ``` 802 | 803 | - `:at-supports` 804 | 805 | Support for feature tests via `@supports` 806 | 807 | ```clojure 808 | (o/defstyled feature-check :div 809 | [:at-supports {:display "grid"} 810 | {:display "grid"}]) 811 | ``` 812 | 813 | - `:rgb` / `:hsl` / `:rgba` / `:hsla` 814 | 815 | Shorthands for color functions 816 | 817 | ```clojure 818 | (o/defstyled color-fns :div 819 | {:color [:rgb 150 30 75] 820 | :background-color [:hsla 235 100 50 0.5]}) 821 | 822 | (o/css color-fns) 823 | ;;=> 824 | ".ot__color_fns{color:#961e4b;background-color:hsla(235,100%,50%,0.5)}" 825 | ``` 826 | 827 | - `:str` 828 | 829 | Turns any strings into quoted strings, for cases where you need to put string content in your CSS. 830 | 831 | ```clojure 832 | (o/defstyled with-css-fn :a 833 | [:&:after {:content [:str " (" [:cssfn :attr "href"] ")"]}]) 834 | ``` 835 | 836 | #### Special property handling 837 | 838 | Some property names we recognize and treat special, mainly to make it less 839 | tedious to define composite values. 840 | 841 | - `:grid-area` / `:border` / `:margin` / `:padding` 842 | 843 | Treat vector values as space-separated lists, e.g. `:padding [10 0 15 0]`. 844 | Non-vector values are passed on unchanged. 845 | 846 | - `:grid-template-areas` 847 | 848 | Use nested vectors to define the areas 849 | 850 | ```clojure 851 | :grid-template-areas [["title" "title" "user"] 852 | ["controlbar" "controlbar" "controlbar"] 853 | ["...." "...." "...."] 854 | ["...." "...." "...."] 855 | ["...." "...." "...."] 856 | ``` 857 | 858 | ## Customizing Girouette 859 | 860 | Girouette is highly customizable. Out of the box it supports the same classes as 861 | Tailwind does, but you can customize the colors, fonts, or add completely new 862 | rules for recognizing class name. 863 | 864 | The `girouette-api` atom contains the result of `giroutte/make-api`. By 865 | replacing it you can customize how keywords are expanded to Garden. We provide a 866 | `set-tokens!` function which makes the common cases straightforward. This 867 | configures Girouette, so that these tokens become available inside Ornament 868 | style declarations. 869 | 870 | `set-tokens!` takes a map with these (optional) keys: 871 | 872 | - `:colors` : map from keyword to 6-digit hex color, without leading `#` 873 | - `:fonts`: map from keyword to font stack (comman separated string) 874 | - `:components`: sequence of Girouette components, each a map with `:id` 875 | (keyword), `:rules` (string, instaparse, can be omitted), and `:garden` (map, 876 | or function taking instaparse results and returning Garden map) 877 | - `:tw-version`: which Girouette defaults to use, either based on Tailwind 878 | v2, or v3. Valid values: `2`, `3`. Defaults to v2. 879 | 880 | ```clojure 881 | (o/set-tokens! {:colors {:primary "001122"} 882 | :fonts {:system "-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji"} 883 | :components [{:id :full-center 884 | :garden {:display "inline-flex" 885 | :align-items "center"}} 886 | {:id :full-center-bis 887 | :garden [:& :inline-flex :items-center]} 888 | {:id :custom-bullets 889 | :rules "custom-bullets = <'bullets-'> bullet-char 890 |
981 |
982 |
983 |
984 | ornament is part of a growing collection of quality Clojure libraries created and maintained
985 | by the fine folks at [Gaiwan](https://gaiwan.co).
986 |
987 | Pay it forward by [becoming a backer on our Open Collective](http://opencollective.com/lambda-island),
988 | so that we may continue to enjoy a thriving Clojure ecosystem.
989 |
990 | You can find an overview of our projects at [lambdaisland/open-source](https://github.com/lambdaisland/open-source).
991 |
992 |
993 |
994 |
995 |
996 |
997 |
998 | ## Contributing
999 |
1000 | Everyone has a right to submit patches to ornament, and thus become a contributor.
1001 |
1002 | Contributors MUST
1003 |
1004 | - adhere to the [LambdaIsland Clojure Style Guide](https://nextjournal.com/lambdaisland/clojure-style-guide)
1005 | - write patches that solve a problem. Start by stating the problem, then supply a minimal solution. `*`
1006 | - agree to license their contributions as EPL 1.0.
1007 | - not break the contract with downstream consumers. `**`
1008 | - not break the tests.
1009 |
1010 | Contributors SHOULD
1011 |
1012 | - update the CHANGELOG and README.
1013 | - add tests for new functionality.
1014 |
1015 | If you submit a pull request that adheres to these rules, then it will almost
1016 | certainly be merged immediately. However some things may require more
1017 | consideration. If you add new dependencies, or significantly increase the API
1018 | surface, then we need to decide if these changes are in line with the project's
1019 | goals. In this case you can start by [writing a pitch](https://nextjournal.com/lambdaisland/pitch-template),
1020 | and collecting feedback on it.
1021 |
1022 | `*` This goes for features too, a feature needs to solve a problem. State the problem it solves, then supply a minimal solution.
1023 |
1024 | `**` As long as this project has not seen a public release (i.e. is not on Clojars)
1025 | we may still consider making breaking changes, if there is consensus that the
1026 | changes are justified.
1027 |
1028 |
1029 |
1030 | ## License
1031 |
1032 | Copyright © 2021-2022 Arne Brasseur and contributors
1033 |
1034 | Available under the terms of the Eclipse Public License 1.0, see LICENSE.txt
1035 |
1036 |
--------------------------------------------------------------------------------
/src/lambdaisland/ornament.cljc:
--------------------------------------------------------------------------------
1 | (ns lambdaisland.ornament
2 | "CSS-in-clj(s)"
3 | #?@
4 | (:clj
5 | [(:require
6 | [clojure.string :as str]
7 | [clojure.walk :as walk]
8 | [garden.color :as gcolor]
9 | [garden.compiler :as gc]
10 | [garden.stylesheet :as gs]
11 | [garden.types :as gt]
12 | [garden.util :as gu]
13 | [girouette.tw.color :as girouette-color]
14 | [girouette.tw.core :as girouette]
15 | [girouette.tw.default-api :as girouette-default]
16 | [girouette.tw.preflight :as girouette-preflight]
17 | [girouette.tw.typography :as girouette-typography]
18 | [girouette.version :as girouette-version]
19 | [meta-merge.core :as meta-merge])]
20 | :cljs
21 | [(:require [clojure.string :as str] [garden.util :as gu])
22 | (:require-macros lambdaisland.ornament)]))
23 |
24 | #?(:clj
25 | (defonce ^{:doc "Registry of styled components
26 |
27 | Keys are fully qualified symbols (var names), values are maps with the
28 | individual `:tag`, `:rules`, `:classname`. We add an `:index` to be able to
29 | iterate over the components/styles in source order. This is now the
30 | preferred way to iterate over all styles (as in [[defined-styles]]), rather
31 | than the old approach of finding all vars with a given metadata attached to
32 | them.
33 |
34 | Clojure-only because we only deal with CSS on the backend, the frontend
35 | only knows about classnames. `:component` points at a StyledComponent
36 | instance that can be used to get the [[css]] for that component."}
37 | registry
38 | (atom {})))
39 |
40 | #?(:clj
41 | (defonce ^{:doc "Registry of plain CSS (Garden) rules"}
42 | rules-registry
43 | (atom {})))
44 |
45 | #?(:clj
46 | (defonce ^{:doc "Registry of custom properties"}
47 | props-registry
48 | (atom {})))
49 |
50 | (def ^:dynamic *strip-prefixes*
51 | "Prefixes to be stripped from class names in generated CSS"
52 | nil)
53 |
54 | (defprotocol StyledComponent
55 | (classname [_]
56 | "The CSS class name for this component, derived from the var and ns name.")
57 | (as-garden [_]
58 | "Return the styles for this component in Garden syntax (i.e. EDN data)")
59 | (css [_]
60 | "Compile this component's styles to CSS")
61 | (rules [_]
62 | "Get the rules passed to this component, without any processing.")
63 | (tag [_]
64 | "HTML tag (keyword) for this component")
65 | (component [_]
66 | "Function which is a Hiccup component, for styled components which have one or more function tails.")
67 | (as-hiccup [_ args]
68 | "Render to hiccup"))
69 |
70 | (declare process-rule)
71 |
72 | #?(:clj
73 | (do
74 | (defonce ^{:doc "Atom containing the return value
75 | of [[girouette/make-api]], making it possible to swap this out for your
76 | own Girouette instance. See also [[set-tokens!]] for a convenient API for
77 | common use cases."}
78 | girouette-api
79 | (atom nil))
80 |
81 | (def default-tokens-v2
82 | (delay
83 | {:components (-> @(requiring-resolve 'girouette.tw.default-api/all-tw-components)
84 | (girouette-version/filter-components-by-version [:tw 2]))
85 | :colors girouette-color/tw-v2-colors
86 | :fonts girouette-typography/tw-v2-font-family-map}))
87 |
88 | (def default-tokens-v3
89 | (delay
90 | {:components (-> @(requiring-resolve 'girouette.tw.default-api/all-tw-components)
91 | (girouette-version/filter-components-by-version [:tw 3]))
92 | :colors girouette-color/tw-v3-unified-colors-extended
93 | :fonts girouette-typography/tw-v2-font-family-map}))
94 |
95 | (def default-tokens default-tokens-v2)
96 |
97 | (defn set-tokens!
98 | "Set \"design tokens\": colors, fonts, and components
99 |
100 | This configures Girouette, so that these tokens become available inside
101 | Ornament style declarations.
102 |
103 | - `:colors` : map from keyword to 6-digit hex color, without leading `#`
104 | - `:fonts`: map from keyword to font stack (comman separated string)
105 | - `:components`: sequence of Girouette components, each a map with
106 | `:id` (keyword), `:rules` (string, instaparse, can be omitted), and
107 | `:garden` (map, or function taking instaparse results and returning Garden
108 | map)
109 | - `:tw-version`: which Girouette defaults to use, either based on Tailwind
110 | v2, or v3. Valid values: 2, 3.
111 |
112 | If `:rules` is omitted we assume this is a static token, and we'll
113 | generate a rule of the form `token-id = <'token-id'>`.
114 |
115 | `:garden` can be a function, in which case it receives a map with a
116 | `:compoent-data` key containing the instaparse parse tree. Literal maps or
117 | vectors are wrapped in a function, in case the returned Garden is fixed. The
118 | resulting Garden styles are processed again as in `defstyled`, so you can use
119 | other Girouette or other tokens in there as well. Use `[:&]` for returning
120 | multiple tokens/maps/stylesUse `[:&]` for returning multiple
121 | tokens/maps/styles.
122 |
123 | By default these are added to the Girouette defaults, which are in terms
124 | based on the Tailwind defaults. We still default to v2 (to avoid breaking
125 | changes), but you can opt-in to Tailwind v3 by adding `:tw-version 3`. Use
126 | meta-merge annotations (e.g. `{:colors ^:replace {...}}`) to change that
127 | behaviour."
128 | [{:keys [components colors fonts tw-version]
129 | :or {tw-version 2}}]
130 | (let [{:keys [components colors fonts]}
131 | (meta-merge/meta-merge
132 | (case tw-version
133 | 2 @default-tokens-v2
134 | 3 @default-tokens-v3)
135 | {:components
136 | (into (empty components)
137 | (map (fn [{:keys [id rules garden] :as c}]
138 | (cond-> c
139 | (not rules)
140 | (assoc :rules (str "\n" (name id) " = <'" (name id) "'>" "\n"))
141 | (not (fn? garden))
142 | (assoc :garden (constantly garden))
143 |
144 | :always
145 | (update :garden #(comp process-rule %)))))
146 | (flatten components))
147 | :colors (into (empty colors)
148 | (map (juxt (comp name key) val))
149 | colors)
150 | :fonts (into (empty fonts)
151 | (map (juxt (comp name key) val))
152 | fonts)})]
153 | (reset! girouette-api
154 | (girouette/make-api
155 | components
156 | {:color-map colors
157 | :font-family-map fonts}))))
158 |
159 | (defonce set-default-tokens (set-tokens! nil))
160 |
161 | (defn class-name->garden [n]
162 | ((:class-name->garden @girouette-api) n))
163 |
164 | (defmethod print-method ::styled [x writer]
165 | (.write writer (classname x)))
166 |
167 | (def munge-map
168 | {\@ "_CIRCA_"
169 | \! "_BANG_"
170 | \# "_SHARP_"
171 | \% "_PERCENT_"
172 | \& "_AMPERSAND_"
173 | \' "_SINGLEQUOTE_"
174 | \* "_STAR_"
175 | \+ "_PLUS_"
176 | \- "_"
177 | \/ "_SLASH_"
178 | \: "_COLON_"
179 | \[ "_LBRACK_"
180 | \{ "_LBRACE_"
181 | \< "_LT_"
182 | \\ "_BSLASH_"
183 | \| "_BAR_"
184 | \= "_EQ_"
185 | \] "_RBRACK_"
186 | \} "_RBRACE_"
187 | \> "_GT_"
188 | \^ "_CARET_"
189 | \~ "_TILDE_"
190 | \? "_QMARK_"})
191 |
192 | (defn munge-str
193 | ([s]
194 | (munge-str s munge-map))
195 | ([s munge-map]
196 | #?(:clj
197 | (let [sb (StringBuilder.)]
198 | (doseq [ch s]
199 | (if-let [repl (get munge-map ch)]
200 | (.append sb repl)
201 | (.append sb ch)))
202 | (str sb))
203 | :cljs
204 | (apply str (map #(get munge-map % %) s)))))
205 |
206 | (defn classname-for
207 | "Convert a fully qualified symbol into a CSS classname
208 |
209 | Munges special characters, and honors `:ornament/prefix` metadata on the
210 | namespace."
211 | [varsym]
212 | (let [prefix (or (:ornament/prefix (meta (the-ns (symbol (namespace varsym)))))
213 | (-> varsym
214 | namespace
215 | (str/replace #"\." "_")
216 | (str "__")))]
217 | (str prefix (munge-str (name varsym)))))
218 |
219 | (defn join-vector-by [sep val]
220 | (if (vector? val)
221 | (str/join sep val)
222 | val))
223 |
224 | (defmulti process-tag
225 | "Support some of our Garden extensions
226 |
227 | Convert tagged vectors in the component rules into plain Garden, e.g.
228 | `[:at-media]` or `[:rgb]`. Default implementation handles using styled
229 | components as selectors, or otherwise simply preserves the tag."
230 | (fn [[tag & _]] tag))
231 |
232 | (defmethod process-tag :default [v]
233 | (let [tag (first v)]
234 | (into (if (set? tag)
235 | (into [] tag)
236 | [(cond
237 | (= ::styled (type tag))
238 | (str "." (classname tag))
239 | (sequential? tag)
240 | (process-rule tag)
241 | :else
242 | tag)])
243 | (map process-rule (next v)))))
244 |
245 | (defmethod process-tag :at-media [[_ media-queries & rules]]
246 | (gs/at-media media-queries (into [:&] (map process-rule) rules)))
247 |
248 | (defmethod process-tag :cssfn [[_ fn-name & args]]
249 | (gt/->CSSFunction fn-name args))
250 |
251 | (defmethod process-tag :at-supports [[_ feature-queries & rules]]
252 | (gt/->CSSAtRule
253 | :feature
254 | {:feature-queries feature-queries
255 | :rules (list (into [:&] (map (comp process-rule)) rules))}))
256 |
257 | (defmethod process-tag :rgb [[_ r g b]]
258 | (gcolor/rgb [r g b]))
259 |
260 | (defmethod process-tag :hsl [[_ h s l]]
261 | (gcolor/hsl [h s l]))
262 |
263 | (defmethod process-tag :rgba [[_ r g b a]]
264 | (gcolor/rgba [r g b a]))
265 |
266 | (defmethod process-tag :hsla [[_ h s l a]]
267 | (gcolor/hsla [h s l a]))
268 |
269 | (defmethod process-tag :str [[_ & xs]]
270 | [(map #(if (string? %) (pr-str %) (process-rule %)) xs)])
271 |
272 | (defmulti process-property
273 | "Special handling of certain CSS properties. E.g. setting `:grid-template-areas`
274 | using a vector."
275 | (fn [prop val] prop))
276 |
277 | (defmethod process-property :default [_ val]
278 | (if (vector? val)
279 | (process-tag val)
280 | val))
281 |
282 | (defmethod process-property :grid-template-areas [_ val]
283 | (if (vector? val)
284 | (str/join " "
285 | (map (fn [row]
286 | (pr-str (str/join " " (map name row))))
287 | val))
288 | val))
289 |
290 | (defmethod process-property :grid-area [_ val] (join-vector-by " / " val))
291 | (defmethod process-property :border [_ val] (join-vector-by " " val))
292 | (defmethod process-property :margin [_ val] (join-vector-by " " val))
293 | (defmethod process-property :padding [_ val] (join-vector-by " " val))
294 |
295 | (defn process-rule
296 | "Process a single \"rule\" into plain Garden
297 |
298 | Components receive a list of rules. These can be Garden-style maps,
299 | Girouette-style keywords, or Garden-style vectors of selectors+rules. This
300 | function together with [[process-tag]] and [[process-property]] defines the
301 | recursive logic to turn this into something we can pass to the Garden
302 | compiler."
303 | [rule]
304 | (cond
305 | (record? rule) ; Prevent some defrecords in garden.types to be fudged
306 | rule
307 |
308 | (simple-keyword? rule)
309 | (let [girouette-garden (class-name->garden (name rule))]
310 | (cond
311 | (nil? girouette-garden)
312 | #_(throw (ex-info "Girouette style expansion failed" {:rule rule}))
313 | rule
314 |
315 | (and (record? girouette-garden)
316 | (= (:identifier girouette-garden) :media))
317 | (-> girouette-garden
318 | (update-in [:value :rules] (fn [rules]
319 | (map #(into [:&] (rest %)) rules))))
320 | :else
321 | (second girouette-garden)))
322 |
323 | (map? rule)
324 | (into {} (map (fn [[k v]] [k (process-property k v)])) rule)
325 |
326 | (vector? rule)
327 | (process-tag rule)
328 |
329 | :else
330 | rule))
331 |
332 | (defn process-rules
333 | "Process the complete set of rules for a component, see [[process-rule]]
334 |
335 | If multiple consecutive rules result in Garden property maps, then they get
336 | merged, to prevent unnecessary bloat of the compiled CSS."
337 | [rules]
338 | (let [add-rule (fn add-rule [acc r]
339 | (cond
340 | (and (vector? r)
341 | (or (= :& (first r))
342 | (= "&" (first r))))
343 | (reduce add-rule acc (next r))
344 |
345 | (and (map? r)
346 | (map? (last acc))
347 | (not (record? r))
348 | (not (record? (last acc))))
349 | (conj (vec (butlast acc))
350 | (merge (last acc) r))
351 |
352 | :else
353 | (conj acc r)))]
354 | (seq (reduce add-rule [] (map process-rule rules)))))))
355 |
356 | (defn add-class
357 | "Hiccup helper, add a CSS classname to an existing `:class` property
358 |
359 | We allow components to define `:class` as a string, a vector, or to use a
360 | styled component directly as a class. (This last behavior is to support some
361 | legacy code, we recommend using a wrapping vector in that case).
362 |
363 | This function handles these cases, and will always return a vector of class
364 | names."
365 | [classes class]
366 | (cond
367 | (nil? class)
368 | classes
369 |
370 | (sequential? class)
371 | (reduce add-class classes class)
372 |
373 | (string? classes)
374 | [class classes]
375 |
376 | (= ::styled (:type (meta classes)))
377 | [class (str classes)]
378 |
379 | (and (sequential? classes) (seq classes))
380 | (vec (cons class classes))
381 |
382 | :else
383 | [(str class)]))
384 |
385 | ;; vocab note: we call "attributes" the key-value pairs you can supply to a HTML
386 | ;; element, like `class`, `style`, or `href`. We call "properties" the map you
387 | ;; pass as the first child to a Ornament/Hiccup component. For component that
388 | ;; don't have a custom render functions these properties will be used as
389 | ;; attributes. For components that do have a custom render function it depends
390 | ;; on what the render function does. In this case you can still pass in
391 | ;; attributes directly using the special `:lambdaisland.ornament/attrs`
392 | ;; property.
393 | ;; See also the Attributes and Properties notebook.
394 |
395 | (defn merge-attr
396 | "Logic for merging two attribute values for the same key.
397 | - `class` : append the classname(s)
398 | - `style` : merge the right style map into the left"
399 | [k v1 v2]
400 | (case k
401 | :class (if (and (vector? v2) (:replace (meta v2)))
402 | v2
403 | (add-class v2 v1))
404 | :style (if (or (not (and (map? v1) (map? v2)))
405 | (:replace (meta v2)))
406 | v2
407 | (merge v1 v2))
408 | v2))
409 |
410 | (defn merge-attrs
411 | "Combine attribute maps"
412 | ([p1 p2]
413 | (when (or p1 p2)
414 | (let [merge-entry (fn [m e]
415 | (let [k (key e)
416 | v (val e)]
417 | (if (contains? m k)
418 | (assoc m k (merge-attr k (get m k) v))
419 | (assoc m k v))))]
420 | (reduce merge-entry (or p1 {}) p2))))
421 | ([p1 p2 & ps]
422 | (reduce merge-attrs (merge-attrs p1 p2) ps)))
423 |
424 | (defn attr-add-class [attrs class]
425 | (if class
426 | (update attrs :class add-class class)
427 | attrs))
428 |
429 | (defn expand-hiccup-tag-simple
430 | "Expand an ornament component being called directly with child elements, without
431 | custom render function."
432 | [tag css-class children extra-attrs]
433 | (let [child-meta (meta children)
434 | [tag attrs children :as result]
435 | (if (sequential? children)
436 | (as-> children $
437 | (if (= :<> (first $)) (next $) $)
438 | (if (map? (first $))
439 | (into [tag (attr-add-class
440 | (merge-attrs (first $) (meta children) extra-attrs)
441 | css-class)] (next $))
442 | (into [tag (attr-add-class
443 | (merge-attrs (meta children) extra-attrs)
444 | css-class)]
445 | (if (vector? $) (list $) $))))
446 | [tag (attr-add-class extra-attrs css-class) children])
447 | result (if child-meta
448 | (with-meta result child-meta)
449 | result)]
450 | (if (and (sequential? children) (= :<> (first children)))
451 | (recur tag nil children attrs)
452 | result)))
453 |
454 | (defn expand-hiccup-tag
455 | "Handle expanding/rendering the component to Hiccup
456 |
457 | For plain [[defstyled]] components this simply adds the CSS class name. For
458 | components with a render function this handles the expansion, and also handles
459 | fragments (`:<>`), optionally with an attributes map, and handles merging
460 | attributes passed in via the `::attrs` property."
461 | [tag css-class args component]
462 | (if component
463 | (let [result (apply component args)]
464 | (if (fn? result)
465 | (fn [& args]
466 | (expand-hiccup-tag-simple tag css-class (apply result args) (::attrs (first args))))
467 | (expand-hiccup-tag-simple tag css-class result (::attrs (first args)))))
468 | (expand-hiccup-tag-simple tag css-class (seq args) nil)))
469 |
470 | (defn styled
471 | ([varsym css-class tag rules component]
472 | #?(:clj
473 | ^{:type ::styled}
474 | (reify
475 | StyledComponent
476 | (classname [_]
477 | (reduce
478 | (fn [c p]
479 | (if (str/starts-with? (str c) p)
480 | (reduced (subs (str c) (count p)))
481 | c))
482 | css-class
483 | *strip-prefixes*))
484 | (as-garden [this]
485 | (into [(str "." (classname this))]
486 | (process-rules rules)))
487 | (css [this] (gc/compile-css
488 | {:pretty-print? false}
489 | (as-garden this)))
490 | (rules [_] rules)
491 | (tag [_] tag)
492 | (component [_] component)
493 | (as-hiccup [this children]
494 | (expand-hiccup-tag tag (classname this) children component))
495 |
496 | clojure.lang.IFn
497 | (invoke [this]
498 | (as-hiccup this nil))
499 | (invoke [this a]
500 | (as-hiccup this [a]))
501 | (invoke [this a b]
502 | (as-hiccup this [a b]))
503 | (invoke [this a b c]
504 | (as-hiccup this [a b c]))
505 | (invoke [this a b c d]
506 | (as-hiccup this [a b c d]))
507 | (invoke [this a b c d e]
508 | (as-hiccup this [a b c d e]))
509 | (invoke [this a b c d e f]
510 | (as-hiccup this [a b c d e f]))
511 | (invoke [this a b c d e f g]
512 | (as-hiccup this [a b c d e f g]))
513 | (invoke [this a b c d e f g h]
514 | (as-hiccup this [a b c d e f g h]))
515 | (invoke [this a b c d e f g h i]
516 | (as-hiccup this [a b c d e f g h i]))
517 | (invoke [this a b c d e f g h i j]
518 | (as-hiccup this [a b c d e f g h i j]))
519 | (invoke [this a b c d e f g h i j k]
520 | (as-hiccup this [a b c d e f g h i j k]))
521 | (invoke [this a b c d e f g h i j k l]
522 | (as-hiccup this [a b c d e f g h i j k l]))
523 | (invoke [this a b c d e f g h i j k l m]
524 | (as-hiccup this [a b c d e f g h i j k l m]))
525 | (invoke [this a b c d e f g h i j k l m n]
526 | (as-hiccup this [a b c d e f g h i j k l m n]))
527 | (invoke [this a b c d e f g h i j k l m n o]
528 | (as-hiccup this [a b c d e f g h i j k l m n o]))
529 | (invoke [this a b c d e f g h i j k l m n o p]
530 | (as-hiccup this [a b c d e f g h i j k l m n o p]))
531 | (invoke [this a b c d e f g h i j k l m n o p q]
532 | (as-hiccup this [a b c d e f g h i j k l m n o p q]))
533 | (invoke [this a b c d e f g h i j k l m n o p q r]
534 | (as-hiccup this [a b c d e f g h i j k l m n o p q r]))
535 | (invoke [this a b c d e f g h i j k l m n o p q r s]
536 | (as-hiccup this [a b c d e f g h i j k l m n o p q r s]))
537 | (applyTo [this args]
538 | (as-hiccup this args))
539 |
540 | Object
541 | (toString [this] (classname this))
542 |
543 | gc/IExpandable
544 | (expand [this]
545 | (mapcat
546 | (fn [rule]
547 | (gc/expand
548 | (if (map? rule)
549 | [:& rule]
550 | rule)))
551 | rules)))
552 |
553 | :cljs
554 | (let [render-fn
555 | (fn [& children]
556 | (expand-hiccup-tag tag
557 | css-class
558 | children
559 | component))
560 | component (specify! render-fn
561 | StyledComponent
562 | (classname [_] css-class)
563 | (as-garden [_] )
564 | (css [_] )
565 | (rules [_] )
566 | (tag [_] tag)
567 | (component [_] component)
568 | (as-hiccup [_ children]
569 | (expand-hiccup-tag tag css-class children component))
570 |
571 | Object
572 | (toString [_] css-class)
573 |
574 | ;; https://ask.clojure.org/index.php/11514/functions-with-metadata-can-not-take-more-than-20-arguments
575 | cljs.core/IMeta
576 | (-meta [_] {:type ::styled}))]
577 | (js/Object.defineProperty component "name" #js {:value (str varsym)})
578 | component))))
579 |
580 | #?(:clj
581 | (defn qualify-sym [env s]
582 | (when (symbol? s)
583 | (if (:ns env)
584 | ;; cljs
585 | (if (simple-symbol? s)
586 | (or (some-> env :ns :uses s name (symbol (name s)))
587 | (symbol (name (-> env :ns :name)) (name s)))
588 | (symbol (or (some-> env :ns :requires (get (symbol (namespace s))) name)
589 | (namespace s))
590 | (name s)))
591 |
592 | ;; clj
593 | (if (simple-symbol? s)
594 | (or (some-> (ns-refers *ns*) (get s) symbol)
595 | (symbol (str *ns*) (str s)))
596 | (let [ns (namespace s)
597 | n (name s)
598 | aliases (ns-aliases *ns*)]
599 | (symbol (or (some-> aliases (get (symbol ns)) ns-name str) ns) n)))))))
600 |
601 | #?(:clj
602 | (defn fn-tail? [o]
603 | (and (list? o)
604 | (vector? (first o)))))
605 |
606 | #?(:clj
607 | (defn update-index [registry varsym]
608 | (update-in registry [varsym :index] (fnil identity (count registry)))))
609 |
610 | #?(:clj
611 | (defn register! [reg varsym m]
612 | ;; We give each style an incrementing index so they get a predictable
613 | ;; order (i.e. source order). If a style is evaluated again (e.g. REPL use)
614 | ;; then it keeps its original index/position.
615 | (swap! reg
616 | (fn [reg]
617 | (-> reg
618 | (update varsym merge m)
619 | (update-index varsym))))))
620 |
621 | #?(:clj
622 | (defn cljs-optimization-level []
623 | (some->
624 | (try (requiring-resolve 'cljs.env/*compiler*)
625 | (catch Exception _))
626 | deref deref :options :optimizations)))
627 |
628 | #?(:clj
629 | (defn render-docstring
630 | "Add the compiled CSS to the docstring, for easy dev-time reference. Ignored
631 | when `*compile-files*` is true (AOT compiling Clojure), or cljs optimization
632 | level is not `:none` (prod CLJS builds), to prevent CSS from bloating up a
633 | production build."
634 | [docstring rules]
635 | (let [css (gc/compile-css (process-rules rules))]
636 | (str
637 | docstring
638 | (when (and (not *compile-files*)
639 | (#{:none nil} (cljs-optimization-level)))
640 | (str
641 | (when (and (not (str/blank? docstring))
642 | (not (str/blank? css)))
643 | (str "\n\n"))
644 | css))))))
645 |
646 | #?(:clj
647 | (defn component->selector [&env s]
648 | (if (symbol? s)
649 | (let [qsym (qualify-sym &env s)]
650 | (if (contains? @registry qsym)
651 | (str "." (get-in @registry [qsym :classname]))
652 | s))
653 | s)))
654 |
655 | #?(:clj
656 | (defn component->rules [&env s]
657 | (if (symbol? s)
658 | (let [qsym (qualify-sym &env s)]
659 | (if (contains? @registry qsym)
660 | (get-in @registry [qsym :rules])
661 | [s]))
662 | [s])))
663 |
664 | #?(:clj
665 | (defn prop->lvalue [&env s]
666 | (if (symbol? s)
667 | (let [qsym (qualify-sym &env s)]
668 | (if (contains? @props-registry qsym)
669 | (str "--" (get-in @props-registry [qsym :propname]))
670 | s))
671 | s)))
672 |
673 | #?(:clj
674 | (defn prop->rvalue [&env s]
675 | (if (symbol? s)
676 | (let [qsym (qualify-sym &env s)]
677 | (if (contains? @props-registry qsym)
678 | (str "var(--" (get-in @props-registry [qsym :propname]) ")")
679 | s))
680 | s)))
681 |
682 | #?(:clj
683 | (defn eval-rules [&env rules]
684 | ;; For ClojureScript support (but also used in Clojure-only), add the
685 | ;; Clojure-version of the styled component to the registry directly
686 | ;; during macroexpansion, so that even in a ClojureScript-only world
687 | ;; we can access it later to compile the styles, even though the
688 | ;; styles themselves are never part of a ClojureScript build.
689 | ;;
690 | ;; To allow using previously defined styled components as selectors
691 | ;; we do our own resolution of these symbols, if we recognize them.
692 | ;; This is necessary since in ClojureScript rules are fully handled
693 | ;; on the Clojure side (we don't want any of the CSS overhead in the
694 | ;; build output), and when defined defstyled in cljs files there are
695 | ;; no Clojure vars that we can resolve, so we need to resolve this
696 | ;; ourselves via the registry.
697 | (let [component->selector (partial component->selector &env)
698 | prop->rvalue (partial prop->rvalue &env)
699 | prop->lvalue (partial prop->lvalue &env)
700 | component->rules (partial component->rules &env)]
701 | (eval `(do
702 | (in-ns '~(ns-name *ns*))
703 | ~(walk/postwalk
704 | (fn [o]
705 | (cond
706 | (vector? o)
707 | (into [(if (set? (first o))
708 | (into #{} (map component->selector (first o)))
709 | (component->selector (first o)))]
710 | (mapcat component->rules)
711 | (next o))
712 | (map? o)
713 | (-> o
714 | (update-keys prop->lvalue)
715 | (update-vals prop->rvalue))
716 | :else
717 | o))
718 | (vec
719 | (mapcat component->rules rules))))))))
720 |
721 | #?(:clj
722 | (defmacro defstyled [sym tagname & styles]
723 | (let [varsym (symbol (name (ns-name *ns*)) (name sym))
724 | css-class (classname-for varsym)
725 | [docstring & styles] (if (string? (first styles)) styles (cons nil styles))
726 | [styles fn-tails] (split-with (complement fn-tail?) styles)
727 | tag (if (keyword? tagname)
728 | tagname
729 | (get-in @registry [(qualify-sym &env tagname) :tag]))
730 | rules (cond
731 | (keyword? tagname)
732 | (vec styles)
733 | (symbol? tagname)
734 | (into (or (:rules (get @registry (qualify-sym &env tagname))) [])
735 | styles))
736 | fn-tails (if (seq fn-tails)
737 | fn-tails
738 | (when (symbol? tagname)
739 | (:fn-tails (get @registry (qualify-sym &env tagname)))))
740 |
741 | fn-tails (when (seq fn-tails)
742 | (if (and (= 1 (count fn-tails))
743 | (= 0 (count (ffirst fn-tails))))
744 | `(([] ~@(rest (first fn-tails)))
745 | ([attrs#] [:<> attrs# (do ~@(rest (first fn-tails)))]))
746 | fn-tails))
747 | rules (eval-rules &env rules)]
748 | (register! registry
749 | varsym
750 | {:var varsym
751 | :tag tag
752 | :rules rules
753 | :classname css-class
754 | :fn-tails fn-tails
755 | :component (styled varsym
756 | css-class
757 | tag
758 | rules
759 | nil)})
760 |
761 | ;; Actual output of the macro, this creates a styled component as a var,
762 | ;; so that it can be used in Hiccup. This `styled` invocation in turn is
763 | ;; platform-specific, the ClojureScript version only knows how to render
764 | ;; the component with the appropriate classes, it has no knowledge of the
765 | ;; actual styles, which are expected to be rendered on the backend or
766 | ;; during compilation.
767 | `(def ~(with-meta sym
768 | {::css true
769 | :ornament (dissoc (get @registry varsym) :component :fn-tails)
770 | :arglists (if (seq fn-tails)
771 | `'~(map first fn-tails)
772 | ''([] [& children] [attrs & children]))
773 | :doc (render-docstring docstring [(into [(str "." css-class)] rules)])})
774 | (styled '~varsym
775 | ~css-class
776 | ~tag
777 | ~(when-not (:ns &env) rules)
778 | ~(when (seq fn-tails)
779 | `(fn ~@fn-tails)))))))
780 |
781 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
782 | ;; Rules
783 |
784 | #?(:clj
785 | (defmacro defrules
786 | "Define plain garden rules. Takes an optional docstring, and any number of
787 | Garden rules (vectors of selector + styles, possibly nested, at-rules, etc).
788 |
789 | Defines a var just so that you can inspect what's been evaluated, but the main
790 | action is the side-effect of registering the rules in a registry, which gets
791 | prepended to the rest of your Ornament CSS."
792 | [rules-name & rules]
793 | (let [[docstring & rules] (if (string? (first rules))
794 | rules
795 | (cons nil rules))
796 | varsym (qualify-sym &env rules-name)
797 | rules (process-rules
798 | (eval-rules &env rules))]
799 | (register! rules-registry varsym {:rules rules})
800 | (when-not (:ns &env)
801 | `(def ~rules-name ~(render-docstring docstring rules) '~rules)))))
802 |
803 | #?(:clj
804 | (defmacro defutil
805 | "Define utility class, takes a name for the class, optionally a docstring, and a
806 | style map. Use the util var in your styles or as as class in hiccup."
807 | ([util-name styles]
808 | `(defutil ~util-name ~nil ~styles))
809 | ([util-name docstring styles]
810 | (let [varsym (qualify-sym &env util-name)
811 | klzname (classname-for varsym)
812 | rules (list [(str "." klzname)
813 | (eval `(do
814 | (in-ns '~(ns-name *ns*))
815 | ~styles))])
816 | docstring (render-docstring docstring rules)]
817 | (register! rules-registry varsym {:rules rules})
818 | `(def ~util-name
819 | ~docstring
820 | (with-meta
821 | (reify
822 | Object
823 | (toString [_] ~klzname)
824 | gc/IExpandable
825 | (expand [_]
826 | (gc/expand
827 | [:& ~styles])))
828 | {:type ::util}))))))
829 |
830 | #?(:clj
831 | (defmethod print-method ::util [u writer]
832 | (.write writer (str u))))
833 |
834 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
835 | ;; Props
836 |
837 | (defprotocol CSSProp
838 | (lvalue [p])
839 | (rvalue [p]))
840 |
841 | #?(:clj
842 | (deftype CSSProperty [prop-name default]
843 | CSSProp
844 | (lvalue [_] (str "--" (name prop-name)))
845 | (rvalue [_] (str "var(--" (name prop-name) ")"))
846 | gu/ToString
847 | (to-str [this]
848 | (str "--" (name prop-name)))
849 | Object
850 | (toString [_] (str "var(--" (name prop-name) ")"))
851 | clojure.lang.ILookup
852 | (valAt [this kw] (when (= :default kw) default))
853 | (valAt [this kw fallback] (if (= :default kw) default fallback))
854 | clojure.lang.IMeta
855 | (meta [this] {:type ::prop}))
856 | )
857 |
858 | (defn css-prop [prop-name default]
859 | #?(:clj
860 | (->CSSProperty prop-name default)
861 | :cljs
862 | (with-meta
863 | (reify
864 | CSSProp
865 | (lvalue [_] (str "--" (name prop-name)))
866 | (rvalue [_] (str "var(--" (name prop-name) ")"))
867 | ILookup
868 | (-lookup [this kw] (when (= :default kw) default))
869 | (-lookup [this kw fallback] (if (= :default kw) default fallback))
870 | Object
871 | (toString [_]
872 | (str "--" (name prop-name))))
873 | {:type ::prop})))
874 |
875 | #?(:clj
876 | (defmethod print-method ::prop [p writer]
877 | (.write writer (lvalue p))))
878 |
879 | #?(:clj
880 | (defmethod print-dup CSSProperty [p writer]
881 | (.write writer (lvalue p))))
882 |
883 | #?(:clj
884 | (defn propname-for
885 | [propsym]
886 | (let [prefix (or (:ornament/prefix (meta (the-ns (symbol (namespace propsym)))))
887 | (-> propsym
888 | namespace
889 | (str/replace #"\." "-")
890 | (str "--")))]
891 | (str prefix (munge-str (str/replace (name propsym)
892 | #"^--" "") (dissoc munge-map \-))))))
893 |
894 | #?(:clj
895 | (defmacro defprop
896 | "Define a custom CSS property (variable). Use the resulting var either where a
897 | value is expected (will expand to `var(--var-name)`), or where a name is
898 | expected (e.g. to assign it in a context)."
899 | ([prop-name]
900 | `(defprop ~prop-name nil))
901 | ([prop-name value]
902 | `(defprop ~prop-name nil ~value))
903 | ([prop-name docstring value]
904 | (let [varsym (qualify-sym &env prop-name)
905 | propname (propname-for varsym)
906 | value (eval value)]
907 | (register! props-registry varsym {:propname propname :value value})
908 | `(def ~prop-name
909 | ~(str
910 | (when docstring
911 | (str docstring "\n\n"))
912 | "Default: " value)
913 | (css-prop '~propname ~value))))))
914 |
915 | #?(:clj
916 | (defn import-tokens*!
917 | ([tokens {:keys [include-values? prefix]
918 | :or {include-values? true
919 | prefix ""}}]
920 | (mapcat
921 | identity
922 | (for [[tname tdef] tokens]
923 | (let [tname (str prefix tname)
924 | {:strs [$description $value $type]} tdef
925 | more (into {} (remove (fn [[k v]] (= (first k) \$))) tdef)]
926 | (cond-> [`(defprop ~(symbol tname)
927 | ~@(when $description [(str $description "\n\nDefault: " $value)])
928 | ~@(when (and $value include-values?)
929 | [$value]))]
930 | (seq more)
931 | (into (import-tokens*! (str tname "-") more)))))))))
932 |
933 | #?(:clj
934 | (defmacro import-tokens!
935 | "Import a standard design tokens JSON file.
936 | Emits a sequence of `defprop`, i.e. it defines custom CSS properties (aka
937 | variables). See https://design-tokens.github.io/community-group/format/
938 | - tokens: parsed JSON, we don't bundle a parser, you have to do that yourself
939 | - opts: options map, supports `:prefix` and `:include-values?`. Has to be
940 | literal (used by the macro itself)
941 | - prefix: string prefix to add to the (clojure and CSS) var names
942 | - :include-values? false: only create the Clojure vars to access the props,
943 | don't include their definitions/values in the CSS. Presumably because you are
944 | loading CSS separately that already defines these.
945 | "
946 | ([tokens & [opts]]
947 | `(do ~@(import-tokens*! (eval tokens) opts)))))
948 |
949 | #?(:clj
950 | (defn defined-garden
951 | "All CSS defined through the different Ornament facilities (defprop, defstyled,
952 | defrules), in Garden syntax. Run this through `garden.compiler/compile-css`."
953 | []
954 | (concat
955 | (let [props (->> @props-registry
956 | vals
957 | (filter (comp some? :value)))]
958 | (when (seq props)
959 | [[":where(html)" (into {}
960 | (map (juxt (comp (partial str "--") :propname)
961 | :value))
962 | props)]]))
963 | (->> @rules-registry
964 | vals
965 | (sort-by :index)
966 | (mapcat :rules))
967 | (->> @registry
968 | vals
969 | (sort-by :index)
970 | (map (fn [{:keys [var tag rules classname]}]
971 | (as-garden (styled var classname tag rules nil))))))))
972 |
973 | #?(:clj
974 | (defn defined-styles
975 | "Collect all styles that have been defined, and compile them down to CSS. Use
976 | this to either spit out or inline a stylesheet with all your Ornament styles.
977 | Optionally the Tailwind preflight (reset) stylesheet can be prepended using
978 | `:preflight? true`. This defaults to Tailwind v2 (as provided by Girouette).
979 | Version 3 is available with `:tw-version 3`"
980 | [& [{:keys [preflight? tw-version compress?]
981 | :or {preflight? false
982 | tw-version 2
983 | compress? true}}]]
984 | (gc/compile-css
985 | {:pretty-print? (not compress?)}
986 | (cond->> (defined-garden)
987 | preflight? (concat (case tw-version
988 | 2 girouette-preflight/preflight-v2_0_3
989 | 3 girouette-preflight/preflight-v3_0_24))))))
990 |
991 | #?(:clj
992 | (defn cljs-restore-registry
993 | "Restore the Ornament registry based on a ClojureScript compiler env
994 |
995 | Due to caching some defstyled macros may not get recompiled, causing gaps in
996 | the CSS. To work around this we add Ornament data to the cljs analyzer var
997 | metadata, so it gets cached and restored with the rest of the analyzer state."
998 | [compiler-env]
999 | (when (empty? @registry)
1000 | (reset! registry
1001 | (into {}
1002 | (for [[_ {:keys [defs]}] (:cljs.analyzer/namespaces compiler-env)
1003 | [_ {{:keys [ornament]} :meta}] defs
1004 | :when ornament]
1005 | [(:var ornament) ornament]))))))
1006 |
1007 | (comment
1008 | (spit "/tmp/ornament.css" (defined-styles))
1009 |
1010 | (->> @rules-registry
1011 | vals
1012 | (sort-by :index)
1013 | (mapcat :rules)
1014 | process-rules))
1015 |
--------------------------------------------------------------------------------