feat(ui-inputs): headless themeable Vue input package (ADR-0043)#160
Conversation
First SFC-carrying package in the Armory (ADR-0043). Headless Vue 3 form inputs
(FsField/FsLabel/FsError/FsTextInput/FsSelect) styled entirely through --fs-* CSS
custom properties (shipped styles.css), error-as-prop, no territory-service imports.
The behavioural core (label resolution, sort, select keyboard reducer, aria ids)
lives in pure src/internal/*.ts so Stryker (which mutates .ts) scores it.
Build: tsdown + unplugin-vue compiles .vue in the dual-format (rolldown) pipeline
while preserving the index.{mjs,cjs}+index.d.{mts,cts} contract validate:dist enforces;
dts:{vue:true} emits real SFC types (FsSelect stays generic <T>).
All 8 CI gates green locally: audit, format, lint (oxlint), build, typecheck
(vue-tsc), lint:pkg (publint+attw), coverage (100% incl. SFCs via v8), mutation
(100%, break:90 on .ts). scripts/lint-pkg.mjs now auto-excludes *.css export subpaths
from attw (asset exports are intentionally untyped) — general, ui-inputs is the first
package to need it. A coverage gap also surfaced a real bug: a disabled select could
be keyboard-opened (onKey lacked the disabled guard the trigger's :disabled gives clicks).
Version stays 0.0.0 — first publish (0.1.0 + npm Trusted Publisher grant) and the BIO
pilot consumer are follow-ups, not in this scaffolding PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZiMAqPwW89m4z2Zdn72fo
Deploying fs-packages with
|
| Latest commit: |
ae33143
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://7ae607ca.fs-packages.pages.dev |
| Branch Preview URL: | https://feat-ui-inputs-package.fs-packages.pages.dev |
Goosterhof
left a comment
There was a problem hiding this comment.
ADR-0043 ui-inputs — CI is RED; the "8/8 gates green" title is inaccurate 🛑
A strong first cut of the ADR-0043 package — headless internals split out (ids/label/select-keyboard/sort), error-as-prop, a --fs-* theme contract, and a per-package strict 100% coverage config. But it must not merge at da7973c8: the check gate (npm run test:coverage) fails.
🛑 Blocker — the 5 SFC component suites run 0 tests; the coverage gate is red
- CI
check→Run npm run test:coverageexits 1:FsError/FsField/FsLabel/FsSelect/FsTextInput.spec.tseach report(0 test)and fail withFailed to parse source for import analysis … Install @vitejs/plugin-vue to handle .vue files(FsError.vue:2). The plain-TS internal suites (sort/label/ids/select-keyboard) pass; only the.vueSFC imports break. Net: 629 tests pass but the five component suites never execute — the components ship with zero effective coverage in the gate, and the 100%-threshold config cannot actually be meeting its bar on unexecuted SFCs. - This is NOT a missing-plugin-in-the-package issue.
packages/ui-inputs/vitest.config.tscorrectlydefineProject({plugins: [Vue()]})viaunplugin-vue, which is present in devDeps. The failure is in the rootvitest run --coverage(workspacedefineWorkspace(['packages/*/vitest.config.ts'])): the per-project Vue transform isn't applied to the.vueimports under the aggregated coverage run. A per-packagenpm testalmost certainly passes — the likely source of the "8/8 green" claim — but CI runs the roottest:coveragepath, which is red. - Where to look: check whether root
vitest runwithout--coveragealso reproduces. If only--coveragefails, it's the v8 instrumentation transforming.vueoutside the project plugin; if both fail, the workspace isn't applyingdefineProjectplugins in the root run at all. Either way the fix is at the workspace/coverage wiring (hoist the Vue plugin where the coverage run sees it, or restructure so the aggregator honours the SFC transform), not the per-package config.
Description accuracy
"8/8 gates green" directly contradicts the red check. Please correct the PR body so a reviewer isn't told the gates pass when the SFC suites don't run — on a fleet-consumed Armory package that claim is load-bearing.
Contract review deferred (deliberately)
I have not yet reviewed the component/theme contract in depth. With the SFC suites not executing, the behavioural surface is unverified by CI, so the ADR-0043 fidelity pass (theme-var contract completeness, input a11y — label association / role="alert" / keyboard on FsSelect, the headless/structural split) waits on a green check. Fix the gate first; I'll do the contract pass on the next round.
COMMENT (self-authored — GitHub blocks a self-approve/-request-changes; the 🛑 is the verdict). Do not merge until check is green.
Automated war-room agent review — posted because this PR carries the Agent Review Requested label.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZiMAqPwW89m4z2Zdn72fo
Goosterhof
left a comment
There was a problem hiding this comment.
Round 2 — new head 980a5451, still 🛑 BLOCKED (now for two reasons)
The new commit (980a5451 "v0.1.0 + README for first publish") is a publish chore — it touches only README.md + package.json. It does not address the coverage-gate blocker, and it adds a new gate failure.
🛑 fid234 (SFC coverage) — STILL OPEN, unaddressed. The delta didn't touch the vitest/workspace wiring, so the root test:coverage SFC-parse failure stands. It's currently masked only because the check job now fails at an earlier step (format:check) and skips test:coverage entirely — it will fail again the moment format is fixed. The blocker is unchanged.
🛑 NEW — format:check reds the gate. check → Run npm run format:check (oxfmt --check .) exits 1 on packages/ui-inputs/README.md (not oxfmt-clean). Because it's the first gate step and the job is -e, every later step skips — lint/build/validate:dist/typecheck/lint:pkg/test:coverage/test:mutation all show skipped. Trivial fix (oxfmt packages/ui-inputs/README.md, or npm run format), but it's a hard gate stop right now.
Net: two gate failures, both must clear before merge — (1) oxfmt the README (trivial), then (2) the real work from round 1: the workspace coverage-vs-Vue-plugin wiring so the 5 SFC suites actually execute. The "8/8 gates green" title remains inaccurate.
COMMENT (self-authored). Still do not merge.
Automated war-room agent review — posted because this PR carries the Agent Review Requested label.
Goosterhof
left a comment
There was a problem hiding this comment.
Not really a fan of the Fs prefix for the components, we do want double names, but this is not right. Let's check an Emmie or Kendo for suggestions about naming.
Also not a fan of the internals being used for only one component, let's just put that inside the component. Much better to put it there instead of extracting it prematurely
…CI green) Addresses @Goosterhof's review: the "8/8 green" claim was measured with the per-PACKAGE `vitest run`, but CI runs the ROOT `vitest run --coverage` (workspace), where a plugin declared in a workspace-PROJECT config is NOT applied to `.vue` import analysis — so the 5 SFC suites reported `(0 test)` and the components shipped with zero effective coverage. Fix: hoist the Vue transform to a root `vitest.config.ts` (`@vitejs/plugin-vue`), which the aggregated run honours. Root run now: 35 files / 690 tests pass; ui-inputs files 100% stmts/branch/funcs/lines. tsdown build still uses unplugin-vue/rolldown. Also: format README.md (post-validation add tripped format:check) and sync package-lock.json to ui-inputs@0.1.0 (the version bump had left the lockfile at 0.0.0). Verified against the ROOT commands this time, not the per-package variant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZiMAqPwW89m4z2Zdn72fo
|
You're right, and the "8/8 green" claim was mine to answer for — I validated with the per-package Fixed in
Two secondary reds also cleared: Lesson logged: measure the gate by the command CI runs, not the per-package variant. Thanks for catching it before merge. |
…e-use internals, --ui-* tokens Addresses @Goosterhof's design review: - Naming: drop the `Fs` prefix for the sibling-aligned compound names kendo/emmie use — FsSelect→SingleSelect, FsTextInput→TextInput, FsField→FormField, FsLabel→FormLabel, FsError→FormError. - Internals: the extracted `src/internal/*` (getLabel/sort/select-keyboard/ids) were each used by exactly one component — pulled in to feed Stryker's .ts-only mutation glob, which is the tool wagging the dog. Inlined back into their components (labelOf + sort + keyboard nav into SingleSelect; error-id into FormField) and deleted the internal/ split. - Theme contract: `--fs-*` / `.fs-*` → `--ui-*` / `.ui-*` to match the ui-inputs family (fs read as "frontend service"). - Mutation gate: with logic inlined, there is no mutable .ts (Stryker mutates .ts, not .vue), so `test:mutation` is a documented no-op — behaviour is guarded by the 100% component-test coverage gate (component tests expanded to keep every inlined keyboard branch covered). stryker.config.mjs removed. Root gates green: build, typecheck (vue-tsc), lint, format:check, lint:pkg, root test:coverage (31 files / 672 tests, ui-inputs 100%). Published 0.1.0 dist is unaffected in runtime shape beyond the rename — a 0.2.0 will carry the new names. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZiMAqPwW89m4z2Zdn72fo
|
Both design points addressed in Naming — dropped the
Also re-namespaced the theme contract Internals — inlined, exactly your point. Root gates green (measured against the root commands): build, typecheck, lint, format:check, lint:pkg, root One flag: the published Ready for your deferred contract pass (a11y / theme-var completeness) whenever. |
…ld/FormLabel/FormError, --ui-* tokens) 0.1.0 shipped the old Fs* names; 0.2.0 carries the reviewed API. Merge publishes it via CI (TP grant wired). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZiMAqPwW89m4z2Zdn72fo
dmooibroek
left a comment
There was a problem hiding this comment.
Clean at HEAD b3e1a15. Both of the-general's prior blockers are fixed at 61fe2e3 (before HEAD): root vitest.config.ts now hoists @vitejs/plugin-vue so the aggregated vitest run --coverage transforms the 5 SFC suites (was unplugin-vue/vite in per-package config only, invisible to the root run); packages/ui-inputs/README.md reformatted to oxfmt column-alignment in the same commit. Verified live via git show 61fe2e3 + current file contents. No new findings above gate.
Make props required wherever the component is meaningless without them, keeping only genuinely-defaultable flags/decorations optional. - FormError: error + id required; drop the internal v-if — the parent (FormField) owns the presence decision via `v-if="error"`, so the leaf is a pure props->DOM component. Co-locates the guard with the describedby toggle in FormField. - FormField: id required (drop the useId() fallback — a consumer with no natural id passes `useId()` at the call site explicitly). label / required / error stay optional. - FormLabel: htmlFor required (a text-slot label with no `for` is an unassociated label — an a11y defect, not a valid state). - TextInput: id + model required (an unbound controlled input can never surface what was typed). type / placeholder / disabled / invalid / describedby stay optional. - SingleSelect: already correctly calibrated — no change. Specs trimmed to the surviving cases; README example carries an id. All 8 gates green locally (coverage 100%, vue-tsc clean). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CvXVLsRR86xmpmrxzhEQV
# Conflicts: # package-lock.json
ADR-0043 contract-fidelity follow-ups (cheap wins from the deferred pass): - required state now reaches assistive tech. FormField threads `required` into the control slot; TextInput/SingleSelect accept `required` and set `aria-required` on the control. Previously the required `*` marker was aria-hidden and visual-only — a screen-reader user was never told a field was required (WCAG 4.1.2 / 3.3.2). aria-required (not native `required`) keeps the error-as-prop model free of browser constraint validation. - SingleSelect listbox accessible name is now the `optionsLabel` prop (default 'Options') instead of a hardcoded English literal — was untranslatable on non-English territories. - theme contract completeness: add --ui-error-size (was asymmetric with --ui-label-size), --ui-menu-max-height, --ui-option-pad — the last spacing/size literals that escaped the --ui-* surface. Coverage stays 100% (23 tests); all gates green locally. The combatbox focus-exposure gap (aria-activedescendant + option ids + aria-selected semantics) is filed as a separate follow-up — it needs its own test surface and is out of scope for these cheap wins. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CvXVLsRR86xmpmrxzhEQV
There was a problem hiding this comment.
Delta re-review at ae33143 (a11y required-state conveyance + close theme-var gaps, 7f +43/-11): clean, no regression in the aria wiring or new theme vars. Full history now clean — prior blockers (SFC coverage, README format) fixed and stable across rounds; CI green, mergeable. APPROVE.
What
First package of the
ui-*family in the Armory (ADR-0043):@script-development/ui-inputs— headless, themeable Vue 3 form inputs. The build spike proven real and CI-green.Components:
FsField/FsLabel/FsError/FsTextInput/FsSelect(the select is the full port of the 4×-reinvented floating-ui listbox, generic<T extends SelectItem>, keyboard nav, click-outside).Design (per ADR-0043)
--fs-*CSS custom properties in a shippedstyles.css(consumersimport '@script-development/ui-inputs/style.css'and map their tokens once). No token vocabulary, no colour literal in the components. Kendo-soft and BIO-brutalist from one contract (the spike).error?: string; they never import a territory error service.src/internal/*.ts(label resolution, sort, the select keyboard reducer, aria-id wiring) so Stryker (which mutates.ts) scores the behavioural core; SFCs stay thin.Build
tsdown+unplugin-vuecompiles.vueinside the dual-format rolldown pipeline, preserving theindex.{mjs,cjs}+index.d.{mts,cts}contractvalidate:distenforces.dts:{vue:true}emits real SFC types —FsSelectstays generic, notany.Gates — all 8 green locally
audit · format · lint (oxlint) · build · typecheck (vue-tsc) · lint:pkg (publint+attw) · coverage 100% (incl. SFCs, v8) · mutation 100% (break:90 on
.ts).scripts/lint-pkg.mjsnow auto-excludes*.cssexport subpaths from attw (asset exports are intentionally untyped) — a general fix; ui-inputs is just the first package with a non-JS export.onKeylacked the disabled guard the trigger's native:disabledgives clicks). Fixed.Before merge (Commander actions — NOT in this PR)
publish.yml404s (the documented first-publish trap). Version deliberately left at0.0.0.0.1.0) + the BIO pilot consumer (wire it end-to-end to pressure-test the token-map + error-prop ergonomics) are the follow-ups before anything approaches1.0.🤖 Generated with Claude Code
https://claude.ai/code/session_01CZiMAqPwW89m4z2Zdn72fo