Appearance
ADR-0079: Every interactive control comes from one ported component set
Status: Accepted Date: 2026-09-20
Decisions
D-160 Buttons and tab bars come from BaseButton and BaseTab, and a raw control is a defect
src/volt/Button.vue and its variants are ported from the work-permit frontend, which is where this repository's Card.vue and Toast.vue already came from. src/components/base/BaseButton.vue wraps it and is what every surface calls, with a variant of primary, secondary or danger. src/components/base/BaseTab.vue and BaseTabWindow.vue are ported beside it, including their ?tab= query sync, so an open tab is linkable and survives a reload.
Both ports are behavioural. Their colours are retinted to this repository's @theme tokens on arrival — the work-permit originals hardcode bg-white, border-primary and --p-gray-4, and a second palette is what D-142 exists to prevent.
A raw <button> or a hand-rolled tab strip on any surface is a defect, asserted by a source sweep in the shape of OnePalette.spec.ts rather than left to review. The sweep covers the curator's build as well as the Handler's: bg-alarm is out of reach on both, but the control vocabulary is not palette and a curator pressing an unstyled control is the same defect as a Handler doing it.
Context
The prototype test reported that controls on the curation surfaces show no hover state, no pointer cursor and no consistent colour. They are raw <button> elements with border utilities, written one at a time. The Handler surfaces are the same story with different utilities.
src/volt/ exists and holds two components, so the pattern is established and half-used. The owner asked for BaseButton and BaseTab, which are the names used in the sibling repositories — and that repository has no BaseButton: its button is the volt component, and only the tab bar is a hand-rolled Base*. Both halves of the name therefore have to be decided rather than copied.
Decision
Port the volt buttons, wrap them in BaseButton so the call site reads in the vocabulary the owner uses across their repositories, and port BaseTab/BaseTabWindow as they are, retinted. Convert every existing control on both builds.
Rejected options
- Use PrimeVue's styled components directly — rejected: the preset theme's colours are not this application's palette, so it would fight D-142 on every surface.
- Add volt components here without a
Base*wrapper — rejected by the owner: the call sites should read in the same vocabulary as the sibling frontends, and variant naming should be ours rather than PrimeVue's. - Port the work-permit tab components wholesale, volt
Tabsincluded — rejected: it would put two tab mechanisms in the repository on day one, anduseTabItemsplus a portedBaseTabis already a complete answer.
This ruling may not be re-decided
If your change contradicts an Accepted ADR: stop and raise it. Do not implement over it. Superseding requires a new ADR that says so; never edit an Accepted one.
Consequences
Hover, focus, active and disabled behaviour are decided once and are the same everywhere, including on the curator's surfaces, which until now were styled by whoever wrote them last.
What becomes hard: the curator bundle gains the wrapper and the volt component. It is deliberately free of the axios stack (see CurationVerification.ts) and this does not change that, but it is no longer true that the curator surfaces carry no shared component code.
Coverage
No upstream finding lands here. The ruling came from a Handler's prototype test on 2026-09-20 and an owner ruling on component vocabulary (D-104).