Skip to content

Instantly share code, notes, and snippets.

@inscapist
Last active November 15, 2022 00:36
Show Gist options
  • Save inscapist/afb78a1c3f96ea9cc7707ed2a04cf03c to your computer and use it in GitHub Desktop.
Save inscapist/afb78a1c3f96ea9cc7707ed2a04cf03c to your computer and use it in GitHub Desktop.
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]]]])
@inscapist
Copy link
Author

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