Last active
November 15, 2022 00:36
-
-
Save inscapist/afb78a1c3f96ea9cc7707ed2a04cf03c to your computer and use it in GitHub Desktop.
ChakraUI inspired tailwind *layouts*, in reagent/hiccup
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 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))) |
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
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: [], | |
}; |
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
(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]]]]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 thatpurge.extract
option is only available in the latest tailwindcss version