Skip to content

Instantly share code, notes, and snippets.

@sagittaros
Last active Oct 18, 2021
Embed
What would you like to do?
ChakraUI inspired tailwind *layouts*, in reagent/hiccup
(ns myapp.tailwind
(:require
[clojure.spec.alpha :as s]
[clojure.string :as str]
[vl.config :as config]))
(s/def ::class (s/or :string string? :vector vector?))
(s/def ::cs sequential?) ;; shorthand for classes
(s/def ::dim #{:screen :fill :h :vh :w :vw})
(s/def ::position #{:x-center :y-center :center}) ;; position utils for relative elements
(s/def ::bs map?) ;; shorthand for breakpoints
(s/def ::layout #{:cols :stacks})
(s/def ::flex #{:fixed :fill :shrinkable :expandable :no-grow :no-shrink})
(s/def ::magnet #{:start :end :center})
(s/def ::spread #{:max :equal :min})
(s/def ::align #{:stretch :start :end :center :baseline})
(s/def ::tailwind-opts
(s/keys :opt-un [::class ::cs ::dim ::bs
::layout ::magnet ::spread ::align]))
(def ^:private debug-classes config/debug?)
(defn- opts->twclasses
[{:keys [class cs dim position layout flex magnet spread align bs]
:as tailwind-opts}]
(if-not (s/valid? ::tailwind-opts tailwind-opts)
(throw (ex-info "Invalid tailwind opts"
(s/explain-data ::tailwind-opts tailwind-opts)))
(letfn [(breakpoint->class [[breakpoint breakpoint-opts]]
(->> breakpoint-opts
opts->twclasses
(map #(str/join ":" [(name breakpoint) %]))))]
(->> (lazy-cat
;; tokenize class string
(when class
(if (vector? class) class
(-> class (str/split #" "))))
;; turn class keywords to strings
(->> cs
(remove nil?)
(map name))
;; preset width/height
(when dim
(case dim
:screen ["h-screen" "w-screen"]
:fill ["h-full" "w-full"]
:h ["h-full"]
:vh ["h-screen"]
:w ["w-full"]
:vw ["w-screen"]))
;; relative positions
(when position
(case position
:x-center ["left-1/2" "-translate-x-1/2"]
:y-center ["top-1/2" "-translate-y-1/2"]
:center ["left-1/2" "-translate-x-1/2"
"top-1/2" "-translate-y-1/2"]))
;; layouts
(when layout
(case layout
:cols ["flex" "flex-row"]
:stacks ["flex" "flex-col"]))
[;; flex variants
(when flex
(case flex
:fixed "flex-none"
:fill "flex-1" ;; fill regardless of existing size. Used for layouting
:shrinkable "flex-initial"
:expandable "flex-auto" ;; expand proportionate to existing size
:no-grow "flex-grow-0"
:no-shrink "flex-shrink-0"))
;; https://tailwindcss.com/docs/justify-content
(when magnet
(case magnet
:start "justify-start"
:end "justify-end"
:center "justify-center"))
;; https://tailwindcss.com/docs/justify-content
(when spread
(case spread
:max "justify-between"
:equal "justify-around"
:min "justify-evenly"))
;; https://tailwindcss.com/docs/align-items
(when align
(case align
:stretch "items-stretch"
:start "items-start"
:end "items-end"
:center "items-center"
:baseline "items-baseline"))]
;; responsive classes
(mapcat breakpoint->class bs))
(remove nil?)))))
(defn- render [opts children]
[:div (as-> opts o'
(assoc o' :class (->> o' opts->twclasses (str/join " ")))
(let [dsl [:cs :dims :flex :layout :bs :magnet :spread :align]]
(apply dissoc o' (if debug-classes [:cs] dsl))))
(into [:<>] children)])
(defn- unpack-props [& [opts & rest :as props]]
(if (map? opts)
[opts rest]
[{} props]))
(defn tailwind-element
"Coerce hiccup children into opts and children, while allowing some extensions"
[{classes :cs :or {classes []}} & props]
(let [[opts children] (apply unpack-props props)]
(render
(update opts :cs concat classes)
children)))
const PREFIXES = ["tab:"]
function cljsExtractor(content) {
let matches = (content.match(/(["']).*?\1/g) || [])
.map((m) => m.replace(/"/g, ""))
.flatMap((m) => m.split(/[\s]+/))
.filter((s) => s.search(/[^$?+<>=]/) !== -1);
matches = matches.concat(matches.flatMap((m) => m.split(/[.]/)))
matches = matches.concat(
matches.flatMap((s) => PREFIXES.map((p) => p + s))
);
return matches;
}
module.exports = {
mode: "jit", // https://tailwindcss.com/docs/just-in-time-mode
purge: {
// in prod look at shadow-cljs output file in dev look at runtime, which will change files that are actually compiled; postcss watch should be a whole lot faster
content:
process.env.NODE_ENV == "production"
? ["./resources/public/js/main.js"]
: ["./resources/public/js/cljs-runtime/*.js"],
extract: cljsExtractor,
},
darkMode: false, // or 'media' or 'class'
theme: {
fontFamily: {
title: ["Nova Oval", "cursive"],
body: ["Montserrat", "sans-serif"],
},
extend: {
screens: {
// tablet
tab: { max: "960px" },
},
},
},
variants: {
extend: {
textDecoration: ["focus-visible"],
},
},
plugins: [],
};
(defn participants []
[ui/<stacks> {:magnet :end
:dims #{:w}}
[:div
(let [participants (<sub :room-participants)]
[ui/<cols> {:dims #{:w}
:spread :max
:cs [:tab:hidden :justify-between :h-27 :p-3
:bg-gradient-to-t :from-grey-ultradarc]}
[ui/<cols> {:cs [:pointer-events-auto]}
(for [{:keys [username fullname]} participants]
^{:key username} [user-image {:src (h/gen-avatar fullname)
:username username
:fullname fullname}])]
[sign-out]])]])
(defn overlays []
[:<>
[ui/<overlay> :z-10 [lane]]
[ui/<overlay> :z-10 [participants]]
(let [[desktop tablet] [:bottom-14 :bottom-0]
responsive {:tab {:layout :stacks
:cs [:bg-grey-ultradarc :pt-9 :pb-7 tablet]}}]
[ui/<relative> {:cs [desktop]
:bs responsive}
[song-info]
[actions]])])
(defn room-ui []
[:div.font-body
[ui/<cols> {:dims #{:vh}}
[ui/<relative> {:flex :fill
:cs [:overflow-hidden :bg-black]
:bs {:tab {:layout :stacks}}}
[player-view]
[overlays]]]])
@sagittaros
Copy link
Author

sagittaros commented Jul 30, 2021

This is my setup for tailwind components in clojurescript.

The key mapping is found in the function opts->twclasses. I am not a frontend dev by training so I rely on my own naming scheme.
Refer to the Clojure spec for the usage.

Tailwind uses purgecss underneath, hence we need to augment the extractor to return responsive classes, or they won't be included in the CSS build. Note that purge.extract option is only available in the latest tailwindcss version

@sagittaros
Copy link
Author

sagittaros commented Jul 30, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment