Skip to content

Adds app-wide prop defaults and refines Vue bindings - #275

Open
razorfever wants to merge 10 commits into
mainfrom
development
Open

Adds app-wide prop defaults and refines Vue bindings#275
razorfever wants to merge 10 commits into
mainfrom
development

Conversation

@razorfever

Copy link
Copy Markdown
Contributor

Introduces a new API for managing app-wide default values for component properties, as defined in ADR-0048.

  • App-wide Prop Defaults (applyDefaults/resetDefaults):

    • Allows consumers to set preferred values for certain component properties (e.g., labelOnTop, size, texts) once per tag, applying them to all instances.
    • Ensures mounted components react live to changes in defaults, facilitating dynamic configurations like runtime language switching.
    • Establishes a clear precedence: an explicit instance attribute or property always overrides an app-wide default, which in turn overrides the built-in default.
    • Provides fail-loud validation for unknown tags or non-defaultable properties.
    • Updates element behavior: unset defaultable properties now read undefined and default attributes (e.g., size="default") are no longer reflected on the host.
    • Integrates comprehensive documentation, including an ADR, glossary entries, API table badges, a dedicated customization guide section, and a live playground.
    • This feature is re-exported from @cscfi/csc-ui-react.
  • Vue .prop Modifier Removal:

    • Rectifies an issue where Vue's .prop modifier on custom element bindings (:attribute.prop) silently broke hyphenated names or was unnecessary, leading to incorrect property assignment.
    • Updates all relevant documentation examples to use plain bindings (:attribute="value") for custom element properties.
    • Enforces this change with a new lint rule in the documentation's example parity check.

razorfever and others added 10 commits September 11, 2026 14:19
…-0048)

A consumer sets a defaultable prop once for every instance of a tag —
`applyDefaults({ 'c-text-field': { labelOnTop: true } })` — instead of
repeating it per element; `resetDefaults()` clears it. Later calls merge,
a key set to `undefined` clears it, and mounted elements follow live.

- Only props tagged `@defaultable <built-in>` take part: labelOnTop,
  hideDetails, shadow and size on c-text-field, c-select, c-autocomplete
  and c-tree-select; itemsPerPage on the three list fields; texts on those
  three and c-data-table. Each declares `undefined` in withDefaults and
  resolves host attribute → own property → app default → built-in through
  the shared useAppDefault helper. An explicit instance value always wins.
- The analyzer derives the manifest default and `csc.defaultable` from the
  tag, generates the `AppDefaults` type and the `DEFAULTABLE_PROPS`
  runtime allow-list into the tag map, and lints tag, withDefaults,
  resolver call and template bindings into agreement.
- Docs: "app default" badge and footnote in the props tables, a new
  Customization section with per-flavor code and a live playground, and a
  sentence in the three Texts usage docs. Both functions are re-exported
  from @cscfi/csc-ui-react.
- Visible changes: unset defaultable props read `undefined` on the element
  instead of the built-in, and hosts no longer carry the reflected default
  attributes (`size="default"`, `items-per-page="6"`).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t in the parity check

Vue sets every declared prop of a csc-ui element as a property on its own
(camelizing hyphenated names), while `.prop` forces the literal key, so
`:level-labels.prop` wrote a dead `el['level-labels']` and the tree-select
demos fell back to "Choose Level 1". The c-data-table selection and
expansion examples had the same dead bindings (`get-row-id`,
`expanded-content`), so the expansion demo rendered no expanded content.

Remove all 52 `.prop` bindings from the 26 Vue canons, reword the four
usage.md notes that recommended `:items.prop` / `:value.prop`, and make
`check-example-parity.mjs` fail on any `.prop` in a canon. Verified on the
production preview: every tree-select header names its level, and the
expansion demo expands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Introduce automated tests for packages/csc-ui: Vitest 5 browser mode on the
devcontainer's Playwright Chromium for anything that touches a registered
custom element, plus a node project for pure code. Specs compile SFC source
through the SFC plugins shared with the build (vite.plugins.ts), are colocated
with the component, and are excluded from the published declarations.

- src/test/harness.ts: mount/settle/recordEvents/consoleSpy and per-theme-mode
  visual baselines; fonts pinned to Liberation via fonts.conf so arm64 and x64
  rasterise alike
- src/test/dist-smoke.spec.ts: the built bundle upgrades every tag, exposes the
  public API, registers @Property once and shares one Tailwind sheet
- src/test/toolchain.node.spec.ts: the Dockerfile's playwright pin equals the
  installed client
- c-button proof spec with light and dark baselines
- ADR-0049, glossary "Verification" terms (value control defined), CLAUDE.md
  testing policy, plan record in _plan/component-tests.md
- lint:tokens is strict; `pnpm lint` aggregates the passing guards

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tests

Add the repository's first verification workflow: install, build csc-ui and
the React wrapper, run the csc-ui lint aggregate (tokens strict, a11y, ramp,
chart) and type-check, lint the docs package and its example parity, then run
every Vitest project in Playwright Chromium. The contrast audit runs as a
report only — its failing pairs are brand colours. Screenshot diffs are
uploaded on failure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…elper specs

Conformance suites (src/test/conformance) run the shared contract of a kind
over every tag, enrolled from generated data: all 72 components (upgrade,
no console output, root part, host-attribute fallthrough, adopted sheets),
the 14 value controls (no emission on programmatic value, one interaction →
one update:value + one input with the host value already set, '' and null
read as empty, a plain v-model round trip) and the 6 anchored overlays (open
through the trigger, Escape, light dismiss, focus home, clean removal). The
analyzer now emits VALUE_TAGS into src/tag-name-map.ts for the enrolment.

The API snapshot (src/api-snapshot.node.spec.ts) condenses each tag's
manifest surface into src/components/<tag>/api.snapshot.json plus an entry
snapshot of runtime exports and public types; the strict analyzer keeps the
manifest consistent, the snapshot keeps it unchanged without a reviewed diff.

Three deviations are pinned in the inverse so a fix must shrink the list:
c-pagination emits on mount and on programmatic value change; c-checkbox and
c-switch mount checked from a null v-model ('' casts to true for a
boolean | number | string prop); c-menu drops focus to <body> on Escape with
a slotted c-button trigger. See _plan/component-tests.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…u and the peek cap

Behaviour specs colocated with the components, seeded from the last three
library fixes: c-select's selected-row check, scalar value resolution and
highlight seeding on every open path (d54ae8a); c-modal's initial focus
after slotted elements upgrade and the ring-free dialog fallback (5e501b9);
the peek cap measured on real row boxes, the scrollbar-free menu ending on a
half-visible row, and the re-measure a frame after the row set changes
(8ef62d1). Six visual baselines per theme mode.

Harness: the pointer is parked at the page corner before every test (files
share one page, and a pointer left over a control painted its hover state
into a baseline); the browser context runs with prefers-reduced-motion so
modal keyframes do not gate assertions.

Two more deviations pinned in the inverse: c-modal dispatches the value
events twice per dismissal and once on a programmatic close (the native
close event fires in a later task than the internalClose flag covers), and
c-select emits at mount for a slotted <c-option selected>.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…elect and applyDefaults

c-autocomplete: content nested in a slotted option renders on the first
open and follows later attribute changes (2d3d34b), match marking stays
inside the label region with metacharacters taken literally (ADR-0045),
change:query per open and keystroke, multiple mode with virtual highlight
and select-all, external mode's loading row and committed labels, and the
items-per-page peek cap. c-tree-select: the 127-check smoke run of
2026-09-11 as seventeen specs — open paths, level browsing with breadcrumb
and step counter, leaf commit and the closed-field format, climbing and
descending, reopening at the committed level, return-object, search with
paths and marks, select-branch row, disabled branches, clear, peek re-cap
and uneven trees. applyDefaults: the six element-level cases from
_plan/app-defaults.md. Eight visual baselines.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…uilt package

A Vitest browser project in the docs package mounts each of the 122 canon
examples with plain @vitejs/plugin-vue and the built @cscfi/csc-ui, then
asserts a clean upgrade: no console output or Vue warning, every c-*
descendant defined with a shadow root, no hyphenated own property (the dead
`.prop` binding class of fault), and the scroll lock released. Fourteen
representative canons carry a visual baseline per theme mode.

The `<docs>` custom-block plugin moves out of nuxt.config.ts into
scripts/ignore-example-docs-blocks.mjs so the Nuxt build and the smoke share
it, and the browser options every project shares (Playwright Chromium,
reduced motion, pinned fonts, screenshot comparator) live once in
vitest.browser.shared.ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`pnpm test` in csc-ui failed intermittently in CTreeSelect.spec.ts: a
Playwright click on the chevron or the clear button did nothing — no error,
focus left on the button, panel closed. The event reached its target and
bubbled to the document, but none of Vue's listeners on the path ran.

The devcontainer's wall clock steps backwards by ~135 ms every few seconds
(measured in Node and in Chromium). Vue stamps every native event with
`Date.now()` and drops it in each listener attached at or after the stamp
(`createInvoker`, the vuejs/vue#6566 guard), so a click landing within a
step of a mount was silently discarded.

- `src/test/monotonicClock.ts`, installed over `Date.now` by the browser
  setup: never decreasing, one millisecond past the last reading over a
  step, then following real time. A node spec pins the arithmetic; a
  browser spec reproduces the dropped click with a stepping fake clock and
  shows the shim closing it.
- c-tree-select fixture: the block host collapsed to 46 px in the
  inline-block stage, putting the field's centre on the chevron and pinning
  the panel and the `search` baseline to 46 px. It now mounts 320 px wide
  and `open()` asserts the panel opened. Baselines regenerated.
- `pnpm type-check` had been red since the shared browser block moved to
  the repo root: `vue-tsc --build` pins rootDir to the package, so
  tsconfig.json sets `rootDir: "../.."`; the unhandled-error filter accepts
  Vitest's `TestError` shape.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Running the suite on a macOS host failed six screenshot comparisons and
nothing else: fontconfig has no effect there, so text renders narrower
(212 px instead of 246 px panels) with 2–4% glyph differences. Baselines are
authored in the devcontainer and compared in CI, both Linux, where
`fonts.conf` pins the glyphs (ADR-0049).

`matchScreenshotInBothModes` and the example smoke now compare only when
`server.platform === 'linux'` and print one notice per file elsewhere; the
behaviour assertions around a skipped comparison still run, and
`test:update` cannot write a baseline from a non-Linux checkout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant