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:
@@ -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 %}
|
||||
<div class="htmx-indicator flex items-center">
|
||||
{% include "templates/components/spinner.html" %}
|
||||
|
||||
@@ -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>
|
||||
</a>
|
||||
|
||||
@@ -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">
|
||||
{% include "templates/components/spinner.html" %}
|
||||
{% if loading_label %}<div class="ml-3">Loading...</div>{% endif %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# Location <select> for a transaction account row. Plain-HTML attributes -- the Selmer
|
||||
migration target (no Hiccup keyword/string attribute ambiguity). Rendered into the
|
||||
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 %}
|
||||
<option value="{{ opt.value }}"{% if opt.selected %} selected{% endif %}>{{ opt.label }}</option>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
{# Money input (number, step=0.01, right-aligned). sc/money-input builds `attrs`. #}
|
||||
<input {{ attrs|safe }}>
|
||||
{# Money input (number, step=0.01, right-aligned). Owns its class base; callers pass the
|
||||
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 }}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# Generic <select>. options = [{value, label, selected}]. Plain-HTML attributes -- the
|
||||
Selmer migration target (no Hiccup keyword/string attribute ambiguity). Extra attrs
|
||||
(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 %}
|
||||
<option value="{{ opt.value }}"{% if opt.selected %} selected{% endif %}>{{ opt.label }}</option>
|
||||
{% endfor %}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{% if disabled %}
|
||||
<span x-text="value.label"></span>
|
||||
{% 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}"
|
||||
@keydown.down.prevent.stop="tippy?.show();"
|
||||
@keydown.backspace="tippy?.hide(); value = {value: '', label: '' }"
|
||||
@@ -34,7 +34,7 @@
|
||||
x-destroy="if ($refs.input) {$refs.input.focus();}">
|
||||
<input type="text"
|
||||
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"
|
||||
placeholder="{{ placeholder }}"
|
||||
@change.stop=""
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{# 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`
|
||||
carries any pass-through div attributes (the per-row location cell hangs its hx-* /
|
||||
x-dispatch swap wiring here); `body` is the pre-rendered inner control HTML;
|
||||
`errors_str` is the comma-joined string errors (empty when none). #}
|
||||
<div class="{{ classes }}"{{ attrs|safe }}>
|
||||
Owns the group / has-error toggle; `has_error` is set when the field has errors,
|
||||
`extra` is the caller's own class. `attrs` carries any pass-through div attributes (the
|
||||
per-row location cell hangs its hx-* / x-dispatch swap wiring here); `body` is the
|
||||
pre-rendered inner control HTML; `errors_str` is the comma-joined string errors. #}
|
||||
<div class="group {% if has_error %}has-error {% endif %}{{ extra }}"{{ attrs|safe }}>
|
||||
{% if label %}
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ label }}</label>
|
||||
{% endif %}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<tbody>
|
||||
{% 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">
|
||||
<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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-2" id="{{ row.location_cell_id }}">
|
||||
<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>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
<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>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
<div {{ vendor.changed_attrs|safe }}>
|
||||
<div class="{{ vendor.field_classes }}">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="{{ status.field_classes }}">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{# 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. #}
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user