diff --git a/resources/templates/components/modal-card.html b/resources/templates/components/modal-card.html
new file mode 100644
index 00000000..0ebc7085
--- /dev/null
+++ b/resources/templates/components/modal-card.html
@@ -0,0 +1,15 @@
+{# Base modal-card chrome (single-step: header / optional side panel / body / footer).
+ A child template extends this and fills the head / side_panel / body / footer blocks,
+ so the whole card renders from one shared context in a single render call. Enter
+ triggers the footer save button via $refs.next. Mirrors transaction-edit/edit-modal
+ (the string-slot version still used by Transaction Edit). #}
+
+
{% block head %}{% endblock %}
+
+ {% block side_panel %}{% endblock %}
+
{% block body %}{% endblock %}
+
+
{% block footer %}{% endblock %}
+
diff --git a/resources/templates/transaction-bulk-code/account-entries.html b/resources/templates/transaction-bulk-code/account-entries.html
deleted file mode 100644
index b0bd047e..00000000
--- a/resources/templates/transaction-bulk-code/account-entries.html
+++ /dev/null
@@ -1,2 +0,0 @@
-{# Wrapper around the expense-account grid (the body of the accounts validated-field). #}
-
{{ grid|safe }}
diff --git a/resources/templates/transaction-bulk-code/account-grid.html b/resources/templates/transaction-bulk-code/account-grid.html
index 16f1a2c7..8697e3cf 100644
--- a/resources/templates/transaction-bulk-code/account-grid.html
+++ b/resources/templates/transaction-bulk-code/account-grid.html
@@ -1,7 +1,7 @@
{# Expense-account grid -- fully template-driven. A single for-loop over the per-row
- view-models (bulk-code/account-row-vm), each delegating to account-row.html. Replaces
- the Clojure data-grid / data-grid-row / data-grid-cell composition. The trailing
- "New account" button posts the whole #bulk-code-form (op=new-account). #}
+ view-models (bulk-code/account-row-vm), each delegating to account-row.html. The
+ trailing "New account" button (a-button partial) posts the whole #bulk-code-form
+ (op=new-account). #}
@@ -13,9 +13,9 @@
- {% for row in rows %}{% include "templates/transaction-bulk-code/account-row.html" %}{% endfor %}
+ {% for row in accounts.rows %}{% include "templates/transaction-bulk-code/account-row.html" %}{% endfor %}
-
{{ new_account_button|safe }}
+
{% 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 %}
diff --git a/resources/templates/transaction-bulk-code/account-row.html b/resources/templates/transaction-bulk-code/account-row.html
index fc220848..0a343e63 100644
--- a/resources/templates/transaction-bulk-code/account-row.html
+++ b/resources/templates/transaction-bulk-code/account-row.html
@@ -1,10 +1,8 @@
-{# One expense-account row, read entirely from a loop-bound `row` view-model
- (bulk-code/account-row-vm). The account typeahead reuses the shared
- components/typeahead.html partial (its context mapped in from row.account via a with
- block); the location select, percentage input, and remove button are inlined plain
- HTML. The location cell (#account-location-N) swaps just itself on account change; the
- remove button swaps the whole #bulk-code-form. Every dynamic attribute arrives
- pre-serialized as a string. #}
+{# One expense-account row, read from a loop-bound `row` view-model (account-row-vm). The
+ account typeahead, location select, and remove button all reuse the shared component
+ partials (typeahead.html / location-select.html / a-icon-button via its ctx); only the
+ table layout is inline. The location cell (#account-location-N) swaps just itself on
+ account change; the remove button swaps the whole #bulk-code-form. #}
@@ -15,9 +13,7 @@
-
+ {% with name=row.location.name classes=row.location.classes options=row.location.options %}{% include "templates/components/location-select.html" %}{% endwith %}
diff --git a/resources/templates/transaction-bulk-code/body.html b/resources/templates/transaction-bulk-code/body.html
new file mode 100644
index 00000000..6bb0d44e
--- /dev/null
+++ b/resources/templates/transaction-bulk-code/body.html
@@ -0,0 +1,29 @@
+{# Bulk-code modal body: vendor typeahead (a change repopulates the default account via a
+ whole-form swap), status select, and the expense-account grid. Each field inlines the
+ validated-field wrapper (label + error
) and pulls its control in from the shared
+ component partials; all data comes from the form view-model. #}
+
+
+
+
+
+ {% 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 %}
+
{{ vendor.error }}
+
+
+
+
+
+ {% with name=status.name classes=status.classes attrs=status.attrs options=status.options %}{% include "templates/components/select.html" %}{% endwith %}
+
{{ status.error }}
+
+
+
+
Expense Accounts
+
+
{% include "templates/transaction-bulk-code/account-grid.html" %}
+
{{ accounts.error }}
+
+
+
+
diff --git a/resources/templates/transaction-bulk-code/bulk-code-body.html b/resources/templates/transaction-bulk-code/bulk-code-body.html
deleted file mode 100644
index b76a613f..00000000
--- a/resources/templates/transaction-bulk-code/bulk-code-body.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{# Bulk-code modal body: vendor field (a change repopulates the default account via a
- whole-form swap), status select, and the expense-account grid. #}
-
-
-
{{ vendor_field|safe }}
-
-
{{ status_field|safe }}
-
-
Expense Accounts
- {{ accounts_field|safe }}
-
-
-
diff --git a/resources/templates/transaction-bulk-code/bulk-code-form.html b/resources/templates/transaction-bulk-code/bulk-code-form.html
deleted file mode 100644
index eeb851b0..00000000
--- a/resources/templates/transaction-bulk-code/bulk-code-form.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{# Top-level plain form for bulk-code (no wizard). The resolved (not-locked) transaction
- id set rides in hidden ids[] fields -- the analog of the edit modal's single db/id
- hidden -- so the selection survives form-changed / submit posts without an EDN snapshot
- or a filter round-trip. #}
-
diff --git a/resources/templates/transaction-bulk-code/card.html b/resources/templates/transaction-bulk-code/card.html
new file mode 100644
index 00000000..6bb79a87
--- /dev/null
+++ b/resources/templates/transaction-bulk-code/card.html
@@ -0,0 +1,6 @@
+{# Bulk-code modal card: extends the shared modal-card base and fills its blocks with the
+ bulk-code head / body / footer partials. No side panel. #}
+{% extends "templates/components/modal-card.html" %}
+{% block head %}{% include "templates/transaction-bulk-code/head.html" %}{% endblock %}
+{% block body %}{% include "templates/transaction-bulk-code/body.html" %}{% endblock %}
+{% block footer %}{% include "templates/transaction-bulk-code/footer.html" %}{% endblock %}
diff --git a/resources/templates/transaction-bulk-code/footer.html b/resources/templates/transaction-bulk-code/footer.html
index 8f2c5bab..62294ce4 100644
--- a/resources/templates/transaction-bulk-code/footer.html
+++ b/resources/templates/transaction-bulk-code/footer.html
@@ -1,5 +1,5 @@
-{# Modal footer: the form-errors sink on the left, the Save button on the right.
- Both are pre-rendered fragments (errors sink = form-errors.html, save = sc/button). #}
+{# 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. #}
-
{{ form_errors|safe }}{{ save_button|safe }}
+
{% 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 %}
diff --git a/resources/templates/transaction-bulk-code/form.html b/resources/templates/transaction-bulk-code/form.html
new file mode 100644
index 00000000..78d1f5ee
--- /dev/null
+++ b/resources/templates/transaction-bulk-code/form.html
@@ -0,0 +1,5 @@
+{# Single render entrypoint for the bulk-code form. The route passes one view-model and
+ every sub-template composes from it via includes/blocks. The resolved (not-locked)
+ transaction id set rides in hidden ids[] fields so the selection survives
+ form-changed / submit posts without an EDN snapshot or a filter round-trip. #}
+
diff --git a/resources/templates/transaction-bulk-code/head.html b/resources/templates/transaction-bulk-code/head.html
index 8c86db0f..a573d4eb 100644
--- a/resources/templates/transaction-bulk-code/head.html
+++ b/resources/templates/transaction-bulk-code/head.html
@@ -1,2 +1,2 @@
{# Modal header label: how many transactions this bulk-code operation will touch. #}
-
Bulk editing {{ count }} transactions
+
Bulk editing {{ head_count }} transactions
diff --git a/resources/templates/transaction-bulk-code/open.html b/resources/templates/transaction-bulk-code/open.html
new file mode 100644
index 00000000..1f5cf720
--- /dev/null
+++ b/resources/templates/transaction-bulk-code/open.html
@@ -0,0 +1,3 @@
+{# Modal-open response: the transitioner shell the modal stack expects, wrapping the whole
+ form. Composes in one render from the shared view-model. #}
+
{% include "templates/transaction-bulk-code/form.html" %}