refactor(ssr): shared component partials own their CSS classes

Bake the Tailwind class base into the shared Selmer component partials so the
partials own their markup and callers pass only data + a small variant
(width / size / color). Applies across all four modals that share them
(bulk-code, invoices, sales-summaries, transaction-edit).

- typeahead / select / location-select / money-input / validated-field /
  button / a-button / a-icon-button: the class base, the validated-field
  has-error toggle, and the button color ladders now live in the .html. The
  sc/*-ctx fns pass width / variant / extra / color plus the non-class attrs
  (computed exactly as before, so every non-class attribute is unchanged).
- bulk-code templates updated to the new partial contracts; account-row pulls
  money-input and a-icon-button in via includes.

Verified: every component's class SET is identical to before across all
variants (14/14 oracle match -- buttons reorder/dedupe classes, CSS is
order-independent); bulk-code full render is DOM-equivalent to the pre-sweep
baseline (class-set + attr-order normalized) for empty / populated / error;
browser QA of bulk-code (full flow) and transaction-edit (open + render) clean,
no JS errors; invoices + sales-summaries compile and render through the same
sc/* fns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 22:23:57 -07:00
parent ee558a34e9
commit f16c52d70b
15 changed files with 69 additions and 88 deletions

View File

@@ -1,4 +1,4 @@
<a class="{{ classes }}"{{ attrs|safe }}> <a class="{{ extra }} 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 {% if color = "secondary" %}text-white bg-blue-500 hover:bg-blue-600 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 focus:ring-blue-250{% elif color = "primary" %}text-white bg-green-500 hover:bg-green-600 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 focus:ring-green-250{% elif color = "red" %}text-white bg-red-500 hover:bg-red-600 focus:ring-red-250 dark:bg-red-600 dark:hover:bg-red-700{% else %}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{% endif %}"{{ attrs|safe }}>
{% if indicator %} {% if indicator %}
<div class="htmx-indicator flex items-center"> <div class="htmx-indicator flex items-center">
{% include "templates/components/spinner.html" %} {% include "templates/components/spinner.html" %}

View File

@@ -1,3 +1,3 @@
<a class="{{ classes }}"{{ attrs|safe }}> <a class="{{ extra }} 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"{{ attrs|safe }}>
<div class="h-4 w-4">{{ body|safe }}</div> <div class="h-4 w-4">{{ body|safe }}</div>
</a> </a>

View File

@@ -1,4 +1,4 @@
<button class="{{ classes }}"{{ attrs|safe }}> <button class="{{ extra }} 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 hover:scale-105 transition duration-100 {% if color = "primary" %}bg-green-500 hover:bg-green-600 focus:ring-green-250 dark:bg-green-600 dark:hover:bg-green-700 text-white{% elif color = "red" %}bg-red-500 hover:bg-red-600 focus:ring-red-250 dark:bg-red-600 dark:hover:bg-red-700 text-white{% else %}bg-white-500 hover:bg-white-600 focus:ring-white-250 dark:bg-white-600 dark:hover:bg-white-700 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{% endif %}"{{ attrs|safe }}>
<div class="htmx-indicator flex items-center absolute inset-0 justify-center"> <div class="htmx-indicator flex items-center absolute inset-0 justify-center">
{% include "templates/components/spinner.html" %} {% include "templates/components/spinner.html" %}
{% if loading_label %}<div class="ml-3">Loading...</div>{% endif %} {% if loading_label %}<div class="ml-3">Loading...</div>{% endif %}

View File

@@ -1,7 +1,7 @@
{# Location <select> for a transaction account row. Plain-HTML attributes -- the Selmer {# Location <select> for a transaction account row. Plain-HTML attributes -- the Selmer
migration target (no Hiccup keyword/string attribute ambiguity). Rendered into the migration target (no Hiccup keyword/string attribute ambiguity). Rendered into the
surrounding Hiccup row via the auto-ap.ssr.selmer interop bridge. #} surrounding Hiccup row via the auto-ap.ssr.selmer interop bridge. #}
<select name="{{ name }}" class="{{ classes }}"> <select name="{{ name }}" class="bg-gray-50 border text-sm rounded-lg block p-2.5 border-gray-300 text-gray-900 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 group-[.has-error]:bg-red-50 group-[.has-error]:border-red-500 group-[.has-error]:text-red-900 group-[.has-error]:placeholder-red-700 group-[.has-error]:focus:ring-red-500 group-[.has-error]:dark:bg-gray-700 group-[.has-error]:focus:border-red-500 group-[.has-error]:dark:text-red-500 group-[.has-error]:dark:placeholder-red-500 group-[.has-error]:dark:border-red-500 {{ variant }}">
{% for opt in options %} {% for opt in options %}
<option value="{{ opt.value }}"{% if opt.selected %} selected{% endif %}>{{ opt.label }}</option> <option value="{{ opt.value }}"{% if opt.selected %} selected{% endif %}>{{ opt.label }}</option>
{% endfor %} {% endfor %}

View File

@@ -1,2 +1,4 @@
{# Money input (number, step=0.01, right-aligned). sc/money-input builds `attrs`. #} {# Money input (number, step=0.01, right-aligned). Owns its class base; callers pass the
<input {{ attrs|safe }}> non-class attributes via attrs + a variant (width/size) class. Class set =
inputs/default-input-classes + appearance-none/text-right + the variant. #}
<input type="number" step="0.01" class="bg-gray-50 border text-sm rounded-lg block p-2.5 border-gray-300 text-gray-900 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 group-[.has-error]:bg-red-50 group-[.has-error]:border-red-500 group-[.has-error]:text-red-900 group-[.has-error]:placeholder-red-700 group-[.has-error]:focus:ring-red-500 group-[.has-error]:dark:bg-gray-700 group-[.has-error]:focus:border-red-500 group-[.has-error]:dark:text-red-500 group-[.has-error]:dark:placeholder-red-500 group-[.has-error]:dark:border-red-500 appearance-none text-right {{ variant }}"{{ attrs|safe }}>

View File

@@ -1,7 +1,7 @@
{# Generic <select>. options = [{value, label, selected}]. Plain-HTML attributes -- the {# Generic <select>. options = [{value, label, selected}]. Plain-HTML attributes -- the
Selmer migration target (no Hiccup keyword/string attribute ambiguity). Extra attrs Selmer migration target (no Hiccup keyword/string attribute ambiguity). Extra attrs
(hx-*, x-*) ride through {{ attrs|safe }}. #} (hx-*, x-*) ride through {{ attrs|safe }}. #}
<select name="{{ name }}" class="{{ classes }}"{{ attrs|safe }}> <select name="{{ name }}" class="bg-gray-50 border text-sm rounded-lg block p-2.5 border-gray-300 text-gray-900 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 group-[.has-error]:bg-red-50 group-[.has-error]:border-red-500 group-[.has-error]:text-red-900 group-[.has-error]:placeholder-red-700 group-[.has-error]:focus:ring-red-500 group-[.has-error]:dark:bg-gray-700 group-[.has-error]:focus:border-red-500 group-[.has-error]:dark:text-red-500 group-[.has-error]:dark:placeholder-red-500 group-[.has-error]:dark:border-red-500 {{ variant }}"{{ attrs|safe }}>
{% for opt in options %} {% for opt in options %}
<option value="{{ opt.value }}"{% if opt.selected %} selected{% endif %}>{{ opt.label }}</option> <option value="{{ opt.value }}"{% if opt.selected %} selected{% endif %}>{{ opt.label }}</option>
{% endfor %} {% endfor %}

View File

@@ -9,7 +9,7 @@
{% if disabled %} {% if disabled %}
<span x-text="value.label"></span> <span x-text="value.label"></span>
{% else %} {% else %}
<a class="{{ a_class }}" <a class="bg-gray-50 border text-sm rounded-lg block p-2.5 border-gray-300 text-gray-900 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 group-[.has-error]:bg-red-50 group-[.has-error]:border-red-500 group-[.has-error]:text-red-900 group-[.has-error]:placeholder-red-700 group-[.has-error]:focus:ring-red-500 group-[.has-error]:dark:bg-gray-700 group-[.has-error]:focus:border-red-500 group-[.has-error]:dark:text-red-500 group-[.has-error]:dark:placeholder-red-500 group-[.has-error]:dark:border-red-500 cursor-pointer {{ width }}"
x-tooltip.on.click="{content: ()=>($refs.dropdown?.innerHTML ?? ''), placement: 'bottom', onMount(i) {htmx.process(i.popper); }, popperOptions: {strategy: 'fixed', modifiers: [{name: 'flip', options: {fallbackPlacements: ['top']}}]}, theme: 'dropdown', allowHTML: true, interactive:true}" x-tooltip.on.click="{content: ()=>($refs.dropdown?.innerHTML ?? ''), placement: 'bottom', onMount(i) {htmx.process(i.popper); }, popperOptions: {strategy: 'fixed', modifiers: [{name: 'flip', options: {fallbackPlacements: ['top']}}]}, theme: 'dropdown', allowHTML: true, interactive:true}"
@keydown.down.prevent.stop="tippy?.show();" @keydown.down.prevent.stop="tippy?.show();"
@keydown.backspace="tippy?.hide(); value = {value: '', label: '' }" @keydown.backspace="tippy?.hide(); value = {value: '', label: '' }"
@@ -34,7 +34,7 @@
x-destroy="if ($refs.input) {$refs.input.focus();}"> x-destroy="if ($refs.input) {$refs.input.focus();}">
<input type="text" <input type="text"
autofocus autofocus
class="{{ search_class }}" class="bg-gray-50 border-bottom text-sm rounded-t-lg block p-2.5 text-gray-900 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 group-[.has-error]:bg-red-50 group-[.has-error]:border-red-500 group-[.has-error]:text-red-900 group-[.has-error]:placeholder-red-700 group-[.has-error]:focus:ring-red-500 group-[.has-error]:dark:bg-gray-700 group-[.has-error]:focus:border-red-500 group-[.has-error]:dark:text-red-500 group-[.has-error]:dark:placeholder-red-500 group-[.has-error]:dark:border-red-500 bg-gray-100 w-full {{ width }}"
x-model="search" x-model="search"
placeholder="{{ placeholder }}" placeholder="{{ placeholder }}"
@change.stop="" @change.stop=""

View File

@@ -1,9 +1,9 @@
{# Field wrapper with label + always-present error <p> (the errors- variant of field-). {# Field wrapper with label + always-present error <p> (the errors- variant of field-).
`classes` already folds group / has-error / caller class via hh/add-class; `attrs` Owns the group / has-error toggle; `has_error` is set when the field has errors,
carries any pass-through div attributes (the per-row location cell hangs its hx-* / `extra` is the caller's own class. `attrs` carries any pass-through div attributes (the
x-dispatch swap wiring here); `body` is the pre-rendered inner control HTML; per-row location cell hangs its hx-* / x-dispatch swap wiring here); `body` is the
`errors_str` is the comma-joined string errors (empty when none). #} pre-rendered inner control HTML; `errors_str` is the comma-joined string errors. #}
<div class="{{ classes }}"{{ attrs|safe }}> <div class="group {% if has_error %}has-error {% endif %}{{ extra }}"{{ attrs|safe }}>
{% if label %} {% if label %}
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ label }}</label> <label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ label }}</label>
{% endif %} {% endif %}

View File

@@ -15,7 +15,7 @@
<tbody> <tbody>
{% for row in accounts.rows %}{% include "templates/transaction-bulk-code/account-row.html" %}{% endfor %} {% for row in accounts.rows %}{% include "templates/transaction-bulk-code/account-row.html" %}{% endfor %}
<tr class="new-row border-b dark:border-gray-600 group hover:bg-gray-100 dark:hover:bg-gray-700"> <tr class="new-row border-b dark:border-gray-600 group hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="px-4 py-2" colspan="4">{% with classes=accounts.new_account.classes attrs=accounts.new_account.attrs indicator=accounts.new_account.indicator body=accounts.new_account.body %}{% include "templates/components/a-button.html" %}{% endwith %}</td> <td class="px-4 py-2" colspan="4">{% with color=accounts.new_account.color extra=accounts.new_account.extra attrs=accounts.new_account.attrs indicator=accounts.new_account.indicator body=accounts.new_account.body %}{% include "templates/components/a-button.html" %}{% endwith %}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -7,21 +7,21 @@
<input type="hidden" name="{{ row.db_id_name }}"{% if row.db_id_value %} value="{{ row.db_id_value }}"{% endif %}> <input type="hidden" name="{{ row.db_id_name }}"{% if row.db_id_value %} value="{{ row.db_id_value }}"{% endif %}>
<td class="px-4 py-2"> <td class="px-4 py-2">
<div class="{{ row.account_field_classes }}"> <div class="{{ row.account_field_classes }}">
<div class="flex flex-col">{% with x_data=row.account.x_data x_model=row.account.x_model key=row.account.key disabled=row.account.disabled a_class=row.account.a_class a_xinit=row.account.a_xinit search_class=row.account.search_class placeholder=row.account.placeholder hidden_attrs=row.account.hidden_attrs %}{% include "templates/components/typeahead.html" %}{% endwith %}</div> <div class="flex flex-col">{% with width=row.account.width x_data=row.account.x_data x_model=row.account.x_model key=row.account.key disabled=row.account.disabled a_xinit=row.account.a_xinit placeholder=row.account.placeholder hidden_attrs=row.account.hidden_attrs %}{% include "templates/components/typeahead.html" %}{% endwith %}</div>
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.account_error }}</p> <p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.account_error }}</p>
</div> </div>
</td> </td>
<td class="px-4 py-2" id="{{ row.location_cell_id }}"> <td class="px-4 py-2" id="{{ row.location_cell_id }}">
<div class="{{ row.location_field_classes }}"{{ row.location_field_attrs|safe }}> <div class="{{ row.location_field_classes }}"{{ row.location_field_attrs|safe }}>
{% with name=row.location.name classes=row.location.classes options=row.location.options %}{% include "templates/components/location-select.html" %}{% endwith %} {% with name=row.location.name variant=row.location.variant options=row.location.options %}{% include "templates/components/location-select.html" %}{% endwith %}
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.location_error }}</p> <p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.location_error }}</p>
</div> </div>
</td> </td>
<td class="px-4 py-2"> <td class="px-4 py-2">
<div class="{{ row.pct_field_classes }}"> <div class="{{ row.pct_field_classes }}">
<input {{ row.pct_attrs|safe }}> {% with variant=row.pct.variant attrs=row.pct.attrs %}{% include "templates/components/money-input.html" %}{% endwith %}
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.pct_error }}</p> <p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.pct_error }}</p>
</div> </div>
</td> </td>
<td class="px-4 py-2 align-top"><a class="{{ row.remove.classes }}"{{ row.remove.attrs|safe }}><div class="h-4 w-4">{% include "templates/components/svg-x.html" %}</div></a></td> <td class="px-4 py-2 align-top">{% with extra=row.remove.extra attrs=row.remove.attrs body=row.remove.body %}{% include "templates/components/a-icon-button.html" %}{% endwith %}</td>
</tr> </tr>

View File

@@ -7,14 +7,14 @@
<div {{ vendor.changed_attrs|safe }}> <div {{ vendor.changed_attrs|safe }}>
<div class="{{ vendor.field_classes }}"> <div class="{{ vendor.field_classes }}">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Vendor</label> <label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Vendor</label>
{% with x_data=vendor.ta.x_data x_model=vendor.ta.x_model key=vendor.ta.key disabled=vendor.ta.disabled a_class=vendor.ta.a_class a_xinit=vendor.ta.a_xinit search_class=vendor.ta.search_class placeholder=vendor.ta.placeholder hidden_attrs=vendor.ta.hidden_attrs %}{% include "templates/components/typeahead.html" %}{% endwith %} {% with width=vendor.ta.width x_data=vendor.ta.x_data x_model=vendor.ta.x_model key=vendor.ta.key disabled=vendor.ta.disabled a_xinit=vendor.ta.a_xinit placeholder=vendor.ta.placeholder hidden_attrs=vendor.ta.hidden_attrs %}{% include "templates/components/typeahead.html" %}{% endwith %}
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ vendor.error }}</p> <p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ vendor.error }}</p>
</div> </div>
</div> </div>
<div> <div>
<div class="{{ status.field_classes }}"> <div class="{{ status.field_classes }}">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Status</label> <label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Status</label>
{% with name=status.name classes=status.classes attrs=status.attrs options=status.options %}{% include "templates/components/select.html" %}{% endwith %} {% with name=status.name variant=status.variant attrs=status.attrs options=status.options %}{% include "templates/components/select.html" %}{% endwith %}
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ status.error }}</p> <p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ status.error }}</p>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
{# Modal footer: the form-errors sink on the left, the Save button on the right. Both {# Modal footer: the form-errors sink on the left, the Save button on the right. Both
pull from the shared view-model; the button reuses components/button.html. #} pull from the shared view-model; the button reuses components/button.html. #}
<div class="flex justify-end"> <div class="flex justify-end">
<div class="flex items-baseline gap-x-4">{% include "templates/transaction-bulk-code/form-errors.html" %}{% with classes=save.classes attrs=save.attrs loading_label=save.loading_label body=save.body %}{% include "templates/components/button.html" %}{% endwith %}</div> <div class="flex items-baseline gap-x-4">{% include "templates/transaction-bulk-code/form-errors.html" %}{% with color=save.color extra=save.extra attrs=save.attrs loading_label=save.loading_label body=save.body %}{% include "templates/components/button.html" %}{% endwith %}</div>
</div> </div>

View File

@@ -11,7 +11,6 @@
components byte-for-byte modulo Tailwind class ordering (verify by string-match + components byte-for-byte modulo Tailwind class ordering (verify by string-match +
e2e, never byte-parity -- see selmer-conventions.md)." e2e, never byte-parity -- see selmer-conventions.md)."
(:require (:require
[auto-ap.ssr.components.buttons :as btn]
[auto-ap.ssr.components.inputs :as inputs] [auto-ap.ssr.components.inputs :as inputs]
[auto-ap.ssr.hiccup-helper :as hh] [auto-ap.ssr.hiccup-helper :as hh]
[auto-ap.ssr.hx :as hx] [auto-ap.ssr.hx :as hx]
@@ -72,32 +71,26 @@
(update :class #(str % (inputs/use-size size))))] (update :class #(str % (inputs/use-size size))))]
(render "templates/components/text-input.html" {:attrs (attrs->str attrs)}))) (render "templates/components/text-input.html" {:attrs (attrs->str attrs)})))
(defn money-input-attrs (defn money-input-ctx
"The serialized attribute string for a money input. Split out so a template-driven "Plain-data context for templates/components/money-input.html. The class base is owned
grid can inline `<input {{ attrs|safe }}>` without re-deriving the class logic." by the template; this passes the non-class attributes (name/value/...) and the variant
[{:keys [size] :as params}] class (caller width + size). Split out so a template can include the partial directly."
(attrs->str (-> params [{:keys [size class] :as params}]
(dissoc :size) {:variant (str (or class "") (inputs/use-size size))
(update :class (fnil hh/add-class "") inputs/default-input-classes) :attrs (attrs->str (dissoc params :class :size))})
(update :class hh/add-class "appearance-none text-right")
(update :class #(str % (inputs/use-size size)))
(assoc :type "number" :step "0.01"))))
(defn money-input [params] (defn money-input [params]
(render "templates/components/money-input.html" {:attrs (money-input-attrs params)})) (render "templates/components/money-input.html" (money-input-ctx params)))
(defn select-ctx (defn select-ctx
"Plain-data context for templates/components/select.html. options = [[value label] ...]; "Plain-data context for templates/components/select.html. options = [[value label] ...];
`value` (string or keyword) marks the selected option. Split out so a template can `value` (string or keyword) marks the selected option. Split out so a template can
{% include %} the partial via {% with %} without re-deriving classes/selection." {% include %} the partial via {% with %} without re-deriving classes/selection."
[{:keys [name value options class] :as params}] [{:keys [name value options class] :as params}]
(let [classes (-> "" (let [sel (cond-> value (keyword? value) clojure.core/name)
(hh/add-class inputs/default-input-classes)
(hh/add-class (or class "")))
sel (cond-> value (keyword? value) clojure.core/name)
attrs (dissoc params :name :value :options :class)] attrs (dissoc params :name :value :options :class)]
{:name name {:name name
:classes classes :variant (or class "")
:attrs (attrs->str attrs) :attrs (attrs->str attrs)
:options (for [[v label] options] :options (for [[v label] options]
{:value v :label label :selected (= (str v) (str sel))})})) {:value v :label label :selected (= (str v) (str sel))})}))
@@ -130,11 +123,12 @@
"Selmer port of com/validated-field (the errors- variant of field-): label + body + "Selmer port of com/validated-field (the errors- variant of field-): label + body +
an always-present error <p>. Pass-through attrs land on the wrapping div (the account an always-present error <p>. Pass-through attrs land on the wrapping div (the account
row's location cell hangs its swap wiring here)." row's location cell hangs its swap wiring here)."
[{:keys [label errors] :as params} & body] [{:keys [label errors class] :as params} & body]
(let [attrs (dissoc params :label :errors :error-source :error-key :class)] (let [attrs (dissoc params :label :errors :error-source :error-key :class)]
(render "templates/components/validated-field.html" (render "templates/components/validated-field.html"
{:label label {:label label
:classes (validated-field-classes params) :has_error (sequential? errors)
:extra (or class "")
:attrs (attrs->str attrs) :attrs (attrs->str attrs)
:body (body->html body) :body (body->html body)
:errors_str (errors-str errors)}))) :errors_str (errors-str errors)})))
@@ -158,51 +152,42 @@
:body (body->html children)})) :body (body->html children)}))
(defn button-ctx (defn button-ctx
"Plain-data context for templates/components/button.html (classes/attrs/loading_label/body)." "Plain-data context for templates/components/button.html. The class base + color ladder
[{:keys [color disabled minimal-loading?] :as params} & children] are owned by the template; this passes the color (name), the caller's extra class, the
(let [classes (cond-> (:class params) non-class attrs, loading_label and body. NB: Selmer button callers only pass static
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" colors (primary); dynamic colors go through the Hiccup com/button."
(btn/bg-colors color disabled)) [{:keys [color minimal-loading?] :as params} & children]
(not disabled) (str " hover:scale-105 transition duration-100") {:color (some-> color name)
disabled (str " cursor-not-allowed") :extra (or (:class params) "")
(some? color) (str " text-white ") :attrs (attrs->str (dissoc params :class))
(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"))] :loading_label (not minimal-loading?)
{:classes classes :body (body->html children)})
:attrs (attrs->str (dissoc params :class))
:loading_label (not minimal-loading?)
:body (body->html children)}))
(defn button [params & children] (defn button [params & children]
(render "templates/components/button.html" (apply button-ctx params children))) (render "templates/components/button.html" (apply button-ctx params children)))
(defn a-button-ctx (defn a-button-ctx
"Plain-data context for templates/components/a-button.html (classes/attrs/indicator/body)." "Plain-data context for templates/components/a-button.html. The class base + color
[{:keys [color disabled] :as params} & children] ladder (secondary/primary/red/default) are owned by the template; this passes the
(let [indicator? (:indicator? params true) color (name), the caller's extra class, the non-class attrs, indicator and body."
classes (cond-> (:class params) [{:keys [color] :as params} & children]
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") {:color (some-> color name)
(= :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") :extra (or (:class params) "")
(= :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 ") :attrs (attrs->str (-> (dissoc params :class)
(= :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 ") (assoc :tabindex 0 :href (:href params "#"))))
(some? color) (str " text-white " (btn/bg-colors color disabled)) :indicator (:indicator? params true)
(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"))] :body (body->html children)})
{:classes classes
:attrs (attrs->str (-> (dissoc params :class)
(assoc :tabindex 0 :href (:href params "#"))))
:indicator indicator?
:body (body->html children)}))
(defn a-button [params & children] (defn a-button [params & children]
(render "templates/components/a-button.html" (apply a-button-ctx params children))) (render "templates/components/a-button.html" (apply a-button-ctx params children)))
(defn a-icon-button-ctx (defn a-icon-button-ctx
"Plain-data context for templates/components/a-icon-button.html (classes/attrs/body)." "Plain-data context for templates/components/a-icon-button.html. The fixed class base is
owned by the template; `extra` is the caller class plus the conditional p-3 padding."
[{:keys [class] :as params} & children] [{:keys [class] :as params} & children]
(let [class-str (or class "") (let [class-str (or class "")
has-padding? (re-find #"\bp[xy]?-\d+(\.\d+)?\b" class-str) has-padding? (re-find #"\bp[xy]?-\d+(\.\d+)?\b" class-str)]
classes (str class-str (if has-padding? "" " p-3") {:extra (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")]
{:classes classes
:attrs (attrs->str (-> (dissoc params :class) :attrs (attrs->str (-> (dissoc params :class)
(assoc :href (or (:href params) "")))) (assoc :href (or (:href params) ""))))
:body (body->html children)})) :body (body->html children)}))
@@ -322,12 +307,7 @@
:value {:value vval :label vlabel} :value {:value vval :label vlabel}
:tippy nil :search "" :active -1 :tippy nil :search "" :active -1
:elements (if vval [{:value vval :label vlabel}] [])}) :elements (if vval [{:value vval :label vlabel}] [])})
a-class (-> (hh/add-class (or class "") inputs/default-input-classes)
(hh/add-class "cursor-pointer"))
a-xinit (str "$nextTick(() => tippy = $el.__x_tippy); " x-init) a-xinit (str "$nextTick(() => tippy = $el.__x_tippy); " x-init)
search-class (-> (or class "")
(hh/add-class inputs/default-input-classes)
(hh/replace-wildcard ["rounded" "border"] "border-bottom bg-gray-100 rounded-t-lg w-full"))
hidden-attrs (-> params hidden-attrs (-> params
(dissoc :class :value-fn :content-fn :placeholder :x-model) (dissoc :class :value-fn :content-fn :placeholder :x-model)
(assoc "x-ref" "hidden" :type "hidden" ":value" "value.value" (assoc "x-ref" "hidden" :type "hidden" ":value" "value.value"
@@ -336,9 +316,8 @@
:x_model x-model :x_model x-model
:key (when id (str id "--" vval)) :key (when id (str id "--" vval))
:disabled disabled :disabled disabled
:a_class a-class :width (or class "")
:a_xinit a-xinit :a_xinit a-xinit
:search_class search-class
:placeholder placeholder :placeholder placeholder
:hidden_attrs (attrs->str hidden-attrs)})) :hidden_attrs (attrs->str hidden-attrs)}))

View File

@@ -157,9 +157,9 @@
(dc/pull (dc/db conn) '[:account/location] account-val))) (dc/pull (dc/db conn) '[:account/location] account-val)))
:client-locations (pull-attr (dc/db conn) :client/locations client-id) :client-locations (pull-attr (dc/db conn) :client/locations client-id)
:value (:location value)}) :value (:location value)})
:pct_attrs (sc/money-input-attrs {:name (account-field-name index :percentage) :pct (sc/money-input-ctx {:name (account-field-name index :percentage)
:class "w-16" :class "w-16"
:value (some-> (:percentage value) (* 100) long)}) :value (some-> (:percentage value) (* 100) long)})
:pct_field_classes (sc/validated-field-classes {:errors (get-in errors [:accounts index :percentage])}) :pct_field_classes (sc/validated-field-classes {:errors (get-in errors [:accounts index :percentage])})
:pct_error (sc/errors-str (get-in errors [:accounts index :percentage])) :pct_error (sc/errors-str (get-in errors [:accounts index :percentage]))
:remove (sc/a-icon-button-ctx {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-form-changed) :remove (sc/a-icon-button-ctx {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-form-changed)
@@ -168,7 +168,8 @@
:hx-select "#bulk-code-form" :hx-select "#bulk-code-form"
:hx-swap "outerHTML" :hx-swap "outerHTML"
:hx-include "closest form" :hx-include "closest form"
:class "account-remove-action"})})) :class "account-remove-action"}
(sc/render "templates/components/svg-x.html" {}))}))
(defn- form-ctx (defn- form-ctx
"The whole bulk-code form as one nested view-model. render-form / open-handler each make "The whole bulk-code form as one nested view-model. render-form / open-handler each make

View File

@@ -174,10 +174,9 @@
:else :else
[["Shared" "Shared"]]) [["Shared" "Shared"]])
selected (or value (ffirst options)) selected (or value (ffirst options))]
classes (str/join " " (conj (vec inputs/default-input-classes) "w-full"))]
{:name name {:name name
:classes classes :variant "w-full"
:options (for [[v label] options] :options (for [[v label] options]
{:value v :label label :selected (= v selected)})})) {:value v :label label :selected (= v selected)})}))