(ns auto-ap.ssr.components.selmer "Selmer-rendered versions of the shared SSR components used by the Transaction Edit modal (see .claude/skills/ssr-form-migration). Each wrapper assembles a plain-data context and renders its own template under resources/templates/components/ via the interop bridge -- the element structure lives entirely in the .html templates; the only Clojure is data assembly. Dynamic HTMX/Alpine attributes (which vary per call site) are serialized to an attribute string by `attrs->str` and injected with {{ attrs|safe }}, so the templates stay free of per-attribute {% if %} ladders. Reuses class logic from auto-ap.ssr.components.inputs so output matches the Hiccup components byte-for-byte modulo Tailwind class ordering (verify by string-match + e2e, never byte-parity -- see selmer-conventions.md)." (:require [auto-ap.ssr.components.buttons :as btn] [auto-ap.ssr.components.inputs :as inputs] [auto-ap.ssr.hiccup-helper :as hh] [auto-ap.ssr.hx :as hx] [auto-ap.ssr.selmer :as sel] [clojure.string :as str] [hiccup.util :as hu])) (defn- attr-name [k] (if (keyword? k) (subs (str k) 1) (str k))) (defn attrs->str "Serialize an attribute map to an HTML attribute string with a leading space, so it concatenates after fixed template attributes: . nil/false values are dropped, true renders a bare boolean attribute, everything else renders name=\"escaped-value\". Mirrors how hiccup2 emits attributes." [m] (->> m (keep (fn [[k v]] (cond (nil? v) nil (false? v) nil (true? v) (str " " (attr-name k)) :else (str " " (attr-name k) "=\"" (hu/escape-html (if (keyword? v) (name v) (str v))) "\"")))) (apply str))) (defn render "Render a component partial and trim outer whitespace (so {# comments #} and the file's trailing newline don't leak into the embedding tree). Returns a raw-wrapped string ready to drop into Hiccup or another Selmer context value." [template ctx] (sel/raw (str/trim (sel/render template ctx)))) (defn- body->html "Render child content (Hiccup vectors and/or raw Selmer fragments) to an HTML string." [body] (->> (if (sequential? body) body [body]) (remove nil?) (map sel/hiccup->html) (apply str))) ;; --- leaf inputs ----------------------------------------------------------------- (defn hidden [{:keys [name value] :as params}] (render "templates/components/hidden.html" {:attrs (attrs->str (merge {:name name} (when (some? value) {:value value}) (dissoc params :name :value)))})) (defn text-input [{:keys [size] :as params}] (let [attrs (-> params (dissoc :error? :size) (assoc :type "text" :autocomplete "off") (update :class #(-> "" (hh/add-class inputs/default-input-classes) (hh/add-class %))) (update :class #(str % (inputs/use-size size))))] (render "templates/components/text-input.html" {:attrs (attrs->str attrs)}))) (defn money-input [{:keys [size] :as params}] (let [attrs (-> params (dissoc :size) (update :class (fnil hh/add-class "") inputs/default-input-classes) (update :class hh/add-class "appearance-none text-right") (update :class #(str % (inputs/use-size size))) (assoc :type "number" :step "0.01"))] (render "templates/components/money-input.html" {:attrs (attrs->str attrs)}))) ;; --- field wrapper --------------------------------------------------------------- (defn validated-field "Selmer port of com/validated-field (the errors- variant of field-): label + body + an always-present error
. Pass-through attrs land on the wrapping div (the account row's location cell hangs its swap wiring here)." [{:keys [label errors] :as params} & body] (let [classes (cond-> (or (:class params) "") (sequential? errors) (hh/add-class "has-error") :always (hh/add-class "group")) attrs (dissoc params :label :errors :error-source :error-key :class) errors-str (when (sequential? errors) (str/join ", " (filter string? errors)))] (render "templates/components/validated-field.html" {:label label :classes classes :attrs (attrs->str attrs) :body (body->html body) :errors_str (or errors-str "")}))) ;; --- buttons / badges / links ---------------------------------------------------- (defn badge [{:keys [color] :as params} & children] (let [classes (-> (hh/add-class "absolute inline-flex items-center z-10 justify-center w-6 h-6 text-xs font-black text-white \n border-3 border-white rounded-full -top-2 -right-2 dark:border-gray-900" (:class params)) (hh/add-class (or (some-> color (#(str "bg-" % "-300"))) "bg-red-300")))] (render "templates/components/badge.html" {:classes classes :attrs (attrs->str (dissoc params :class)) :body (body->html children)}))) (defn link [{:keys [class] :as params} & children] (render "templates/components/link.html" {:classes (str class " font-medium text-blue-600 dark:text-blue-500 hover:underline cursor-pointer") :attrs (attrs->str (dissoc params :class)) :body (body->html children)})) (defn button [{:keys [color disabled minimal-loading?] :as params} & children] (let [classes (cond-> (:class params) true (str " focus:ring-4 font-bold rounded-lg text-xs p-3 text-center mr-2 inline-flex items-center relative justify-center disabled:opacity-50" (btn/bg-colors color disabled)) (not disabled) (str " hover:scale-105 transition duration-100") disabled (str " cursor-not-allowed") (some? color) (str " text-white ") (nil? color) (str " bg-white dark:bg-gray-600 border-gray-300 dark:border-gray-700 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100 font-medium border border-gray-300 dark:border-gray-700"))] (render "templates/components/button.html" {:classes classes :attrs (attrs->str (dissoc params :class)) :loading_label (not minimal-loading?) :body (body->html children)}))) (defn a-button [{:keys [color disabled] :as params} & children] (let [indicator? (:indicator? params true) classes (cond-> (:class params) true (str " focus:ring-4 font-bold rounded-lg text-xs p-3 text-center mr-2 inline-flex items-center hover:scale-105 transition duration-100 justify-center") (= :secondary color) (str " text-white bg-blue-500 hover:bg-blue-600 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700") (= :primary color) (str " text-white bg-green-500 hover:bg-green-600 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 ") (= :secondary-light color) (str " text-blue-800 bg-white-200 border-gray-100 border hover:bg-blue-100 focus:ring-blue-100 dark:bg-blue-400 dark:hover:bg-blue-800 ") (some? color) (str " text-white " (btn/bg-colors color disabled)) (nil? color) (str " bg-white dark:bg-gray-600 border-gray-300 dark:border-gray-700 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100 font-medium border border-gray-300 dark:border-gray-700"))] (render "templates/components/a-button.html" {:classes classes :attrs (attrs->str (-> (dissoc params :class) (assoc :tabindex 0 :href (:href params "#")))) :indicator indicator? :body (body->html children)}))) (defn a-icon-button [{:keys [class] :as params} & children] (let [class-str (or class "") has-padding? (re-find #"\bp[xy]?-\d+(\.\d+)?\b" class-str) classes (str class-str (if has-padding? "" " p-3") " inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center text-sm font-medium border border-gray-300 dark:border-gray-700 text-center text-gray-500 hover:text-gray-800 rounded-lg dark:text-gray-400 dark:hover:text-gray-100")] (render "templates/components/a-icon-button.html" {:classes classes :attrs (attrs->str (-> (dissoc params :class) (assoc :href (or (:href params) "")))) :body (body->html children)}))) (defn button-group-button [{:keys [size] :or {size :normal} :as params} & children] (let [classes (cond-> (:class params) true (str " font-medium text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:ring-2 focus:ring-green-700 focus:text-green-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-green-500 dark:focus:text-white disabled:opacity-50") (= :small size) (str " text-xs px-3 py-2") (= :normal size) (str " text-sm px-4 py-2"))] (render "templates/components/button-group-button.html" {:classes classes :attrs (attrs->str (-> (dissoc params :class :size) (assoc :type (or (:type params) "button")))) :body (body->html children)}))) (defn button-group [{:keys [name]} & children] (render "templates/components/button-group.html" {:name name :body (body->html children)})) ;; --- radio-card ------------------------------------------------------------------ (defn radio-card "Selmer port of com/radio-card. NB: the Hiccup radio-card- has a dangling [:h3 title] the let discards, so only the