Skip to content

Instantly share code, notes, and snippets.

@sagittaros
Last active Aug 15, 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]
[myapp.config :as config]))
(s/def ::class string?)
(s/def ::cs sequential?) ;; shorthand for classes
(s/def ::dims set?)
(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 ::dims ::bs
::layout ::magnet ::spread ::align]))
(def ^:private debug-classes config/debug?)
(defn- opts->twclasses
[{:keys [class cs dims 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 [(dim->class [dim]
(or (dim {:screen "h-screen w-screen"
:h "h-full"
:vh "h-screen"
:w "w-full"
:vw "w-screen"})
;; allow arbitrary class if not from preset
(name dim)))
(breakpoint->class [[breakpoint breakpoint-opts]]
(->> breakpoint-opts
opts->twclasses
(map #(str/join ":" [(name breakpoint) %]))))]
(->> (lazy-cat
;; tokenize class string
(when class (-> class (str/split #" ")))
;; turn class keywords to strings
(map name cs)
;; preset width/height
(map dim->class dims)
;; 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)))
(defn <div> [& props]
(apply tailwind-element {} props))
(defn <cols> [& props]
(apply tailwind-element {:cs [:flex :flex-row]} props))
(defn <stacks> [& props]
(apply tailwind-element {:cs [:flex :flex-col]} props))
(defn <v-center> [& props]
(apply tailwind-element {:cs [:flex :flex-row :items-center]} props))
(defn <h-center> [& props]
(apply tailwind-element {:cs [:flex :flex-col :items-center]} props))
(defn <relative> [& props]
(apply tailwind-element {:cs [:relative]} props))
(defn <overlay> [z & props]
(apply tailwind-element {:cs [:absolute :inset-0 :flex
z :pointer-events-none]} props))
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

This comment has been minimized.

Copy link
Owner Author

@sagittaros 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

This comment has been minimized.

Copy link
Owner Author

@sagittaros sagittaros commented Jul 30, 2021

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