You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Frontend ADR 0003 already settles the doctrine: shared components know nothing of the domain, surface components compose them, and lint forbids raw affordance HTML. It holds. What it never covered is structure — tables, lists, layout — because nobody had built those components, so nobody could ban drawing them by hand.
The result is measurable today:
Shared components
14 (tribnest carries ~30 in the same role)
Raw colours outside the tokens
0 — check-theme-tokens.mjs holds
Spacings outside the tokens
3, all in FieldFrame.vue
Raw table / tr / td / ul / li in surfaces
24, in CatalogueView.vue and VatRates.vue
A page showing the components
none
Decisions taken 2026-09-04
A table takes typed cells, never slots
The generic table accepts a closed vocabulary of cell kinds — text, number, state pill, thumbnail, actions. A surface component translates its domain object into those cells: ProductTable(products) and OrderTable(orders) both feed the one Table. Changing how a row looks is then one edit, in one file, which is the entire point.
A need the vocabulary does not cover adds a cell kind to the generic component, with its own mockup. It does not open a slot.
The alternative is rejected with evidence. tribnest's DataTable.vue exposes a per-column slot (#cell-${key}), and its consumers fill those slots with markup:
The generic table exists, the rule is written, and hand-written markup came back into the pages through the slot. Repeating that here would buy the kit and keep the problem. This goes in a frontend ADR, because it is a decision with a rejected alternative and because the next person will otherwise reach for slots as the obvious answer.
Shared knows nothing, surface takes the object
The frontend is data-driven at the surface layer: a surface component takes an instance of a domain object and derives what to show. A shared component never does — ProductCard(product) yes, Button(product) never. This is ADR 0003's line, restated because "data-driven" is exactly the phrasing under which the domain creeps into a button.
Layout components exist from the start
A stack, a grid, a page. Without them "no markup in a page" dies at the first page that needs to put two blocks one above the other, and the escape hatch becomes the norm.
The gallery lives at /dev/design-system
Out of the production build. It serves the team and the visual validation, not the merchant, and it does not travel in every installation.
One gallery, not one per kind of component. The reference project split theirs between primitives (/dev/ui-kit) and assembled patterns (/dev/app-anatomy), watched them drift until neither had a criterion for where a new element belonged, and merged them back into /dev/design-system (their frontend ADR 0013). Naming it right costs one line today. Composites land here under their own heading.
The design loop, graded
mockup → validation → implementation (+ doc/ADR when a decision was taken) → entry in the kit → validation. A sub-component goes through the same loop first, before the component that uses it.
Graded on purpose: a component that renders one affordance enters the kit without a prior mockup — the kit page is its mockup. A component that composes a surface goes through the mockup first. A loop that demands two validations for a link is a loop that gets bypassed by the third week.
Sub-issues
Sequential: each one is the vocabulary the next is written in.
Restyling anything. This epic changes where markup lives, not what it looks like; a component that renders differently after migration is a defect, not an improvement.
Frontend ADR 0003 already settles the doctrine: shared components know nothing of the domain, surface components compose them, and lint forbids raw affordance HTML. It holds. What it never covered is structure — tables, lists, layout — because nobody had built those components, so nobody could ban drawing them by hand.
The result is measurable today:
check-theme-tokens.mjsholdsFieldFrame.vuetable/tr/td/ul/liin surfacesCatalogueView.vueandVatRates.vueDecisions taken 2026-09-04
A table takes typed cells, never slots
The generic table accepts a closed vocabulary of cell kinds — text, number, state pill, thumbnail, actions. A surface component translates its domain object into those cells:
ProductTable(products)andOrderTable(orders)both feed the oneTable. Changing how a row looks is then one edit, in one file, which is the entire point.A need the vocabulary does not cover adds a cell kind to the generic component, with its own mockup. It does not open a slot.
The alternative is rejected with evidence. tribnest's
DataTable.vueexposes a per-column slot (#cell-${key}), and its consumers fill those slots with markup:The generic table exists, the rule is written, and hand-written markup came back into the pages through the slot. Repeating that here would buy the kit and keep the problem. This goes in a frontend ADR, because it is a decision with a rejected alternative and because the next person will otherwise reach for slots as the obvious answer.
Shared knows nothing, surface takes the object
The frontend is data-driven at the surface layer: a surface component takes an instance of a domain object and derives what to show. A shared component never does —
ProductCard(product)yes,Button(product)never. This is ADR 0003's line, restated because "data-driven" is exactly the phrasing under which the domain creeps into a button.Layout components exist from the start
A stack, a grid, a page. Without them "no markup in a page" dies at the first page that needs to put two blocks one above the other, and the escape hatch becomes the norm.
The gallery lives at
/dev/design-systemOut of the production build. It serves the team and the visual validation, not the merchant, and it does not travel in every installation.
One gallery, not one per kind of component. The reference project split theirs between primitives (
/dev/ui-kit) and assembled patterns (/dev/app-anatomy), watched them drift until neither had a criterion for where a new element belonged, and merged them back into/dev/design-system(their frontend ADR 0013). Naming it right costs one line today. Composites land here under their own heading.The design loop, graded
mockup → validation → implementation (+ doc/ADR when a decision was taken) → entry in the kit → validation. A sub-component goes through the same loop first, before the component that uses it.Graded on purpose: a component that renders one affordance enters the kit without a prior mockup — the kit page is its mockup. A component that composes a surface goes through the mockup first. A loop that demands two validations for a link is a loop that gets bypassed by the third week.
Sub-issues
Sequential: each one is the vocabulary the next is written in.
Out of scope