Skip to content

Consolidate pre-major Components additions - #171

Merged
woksin merged 7 commits into
mainfrom
feat/components-open-minors
Aug 23, 2026
Merged

Consolidate pre-major Components additions#171
woksin merged 7 commits into
mainfrom
feat/components-open-minors

Conversation

@woksin

Copy link
Copy Markdown
Contributor

Summary

Consolidate the two independent open Components 3 additions that should ship before the renderer-breaking Components 4 release. Commit authorship and logical history from the original PRs are preserved.

The table-rendering proof in #108 is intentionally not included: Components 4 resolves the mandatory-renderer problem differently, while the longer-term headless Arc binding remains tracked in #109. The TypeScript 7 draft #106 remains deferred because its own description documents incompatible lint and Rollup tooling.

Added

Changed

  • Make Components 3 Storybook discovery and Vite configuration clean under the current toolchain

einariand others added 7 commits August 23, 2026 15:49
RangeHistogramFilter could only count values the browser already held, so a
range picker over a data set larger than the loaded page misrepresented what
each range actually selects - and there was no way to feed it a server-side
aggregation.
numericRange now takes an optional histogram of pre-counted buckets, rendered
as given and scaled against their own tallest bar. The counting itself moves
out of the component into buildHistogram so both paths are covered by specs.
values stays supported and unchanged for client-side counting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…layName
A `CommandForm` child was classified as a field or a column by exactly one
test: `component.displayName === 'CommandFormField'` (or `'CommandFormColumn'`).
`displayName` is React's public, writable diagnostic name and a routine target
for build tooling, so any transform that sets it unbinds every field — with no
error, no warning and every gate green. The field then renders with no
container: no label, no bound value, no change handler.
This adds a marker that such a transform cannot reach, checked first, with the
`displayName` comparison kept as a fallback:
- `CommandFormFieldMarker` / `CommandFormColumnMarker` — `Symbol.for` registry
keys, so `@cratis/arc.react` and `@cratis/components` resolve the same symbol
without importing it from each other. A named import would be a hard
module-link error against any version in this package's peer range that does
not export it, and a plain `Symbol()` would give a duplicate install two keys
that never compare equal.
- `isCommandFormField` / `isCommandFormColumn` — marker first, `displayName`
second — now used at all three read sites (`CommandDialog`, and both reads in
`CommandStepper`).
- `markAsCommandFormField` / `markAsCommandFormColumn` set the marker *and* the
legacy `displayName`; `CommandDialog.Column` is stamped through the latter.
The `displayName` path is retained indefinitely rather than deprecated. It is
what lets these two independently versioned packages interoperate in both
directions, and what keeps working every consumer who marks a field by hand.
Removing it would reproduce the very failure this change prevents.
Purely additive: no public API is removed and no existing consumer changes
behaviour.
This is the consumer half of the contract. The field marker only takes effect
once `@cratis/arc.react` stamps it; until then every path here falls back to
`displayName` exactly as before. Because both sides keep the fallback, the two
packages may ship in either order without a skew hazard.
Also documents that `displayName` is load-bearing on field and column
components, including the Storybook `reactDocgen: 'react-docgen-typescript'`
default that rewrites it and the `setDisplayName: false` setting that disables
it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRCRgRvMz9N8P8NGwR34MM
The first pass proved the marker path and left gaps around it. Adds:
- a component carrying *only* the marker and no displayName at all — the mirror
of the legacy-only case, and the one that shows the marker is sufficient by
itself rather than merely corroborating the label;
- what the marking helpers actually do — both identifiers set, no cross-marking,
and the component returned is the one that was marked, since both call styles
are used in this package;
- strictness of the check: a marker of `false`, or of a truthy non-boolean, is
not a marker;
- a renamed field nested inside `CommandDialog.Column`, which reaches the field
through `processChildren`' recursion rather than as a direct child — the
arrangement the column API exists for;
- `Symbol.keyFor` assertions on marker identity. That distinguishes
`Symbol.for('x')` from `Symbol('x')`, which nothing else about the value does,
and names the key the other package has to use. Those key strings are the
whole cross-package contract: changing one breaks it while every exported
identifier stays the same.
Both directions are now mutation-proven. Reverting the predicates to
legacy-string-only reds 11 tests across 7 files; removing the legacy fallback
instead reds 7 across 5 — including the pre-existing
`when_step_has_field_errors`, which stamps the string on a fake to make it a
field and is exactly the canary for that breaking change.
Also documents that the helpers replace any existing `displayName`, which is
forced rather than incidental — an older Arc binds by that exact string — so a
component needing its own diagnostic label cannot also be marked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRCRgRvMz9N8P8NGwR34MM
The marker makes the right thing possible; this makes the wrong thing visible.
Consumers hand-roll `CommandForm` fields, and the failure mode being fixed is
silent — a renamed component simply stops being a field, with no error, no
warning and every gate green — so a lint rule is the only place it surfaces at
authoring time.
Flags identifying a field or column by a hand-written `displayName` string in
either direction: stamping it (assignment, computed access, or object-literal
form as in `Object.assign`) and comparing against it (`===`/`!==`, either
operand order, including the `(x as { displayName?: string })` cast form this
package itself used). Points at `markAsCommandFormField`/`markAsCommandFormColumn`
and `isCommandFormField`/`isCommandFormColumn`, naming the right helper for the
string that was written.
Referring to the exported `CommandFormFieldDisplayName` /
`CommandFormColumnDisplayName` constants is not flagged, so the declarations
themselves and any deliberate legacy-path code stay clean. Going through the
helpers is strictly more permissive than the literal, never less — they still
set and honour the legacy `displayName` — so the rule never trades compatibility
for safety.
Verified end to end through the ESLint Linter, not only RuleTester: all three
shapes this repo carried before the marker existed are reported, each naming the
correct helper. 21 rule tests added.
This repo's own eslint config does not load the plugin, so this changes no gate
here; it is published surface for consumers and is enabled in
`configs.recommended` alongside the existing four rules.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRCRgRvMz9N8P8NGwR34MM
Two rules under .ai/rules were missed when this work was written.
American English (general.md, typescript.md "Language — American English
Only"): "recognise"/"recognised" become "recognize"/"recognized" across the
marker module, its specs, the ESLint rule and both docs pages. `Cancelled` is
left alone — that is the spelling of Arc's DialogResult member, an API name
rather than prose.
Spaces in it() descriptions (specs.typescript.md "Naming Conventions", where
it('should_return_invalid_result') is the explicit counter-example): every
it() in the new specs becomes a readable sentence. The pre-existing underscore
descriptions elsewhere are left untouched — they predate this work, and the
repo already runs 157 space-style descriptions against 57 underscore ones, so
the convention followed here is also the majority one.
No behavior change; identifiers, assertions and control flow are untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRCRgRvMz9N8P8NGwR34MM
@cratis/arc.react marks fields and columns with `isCommandFormField` and
`isCommandFormColumn` boolean properties. This package had reached for
`Symbol.for` registry keys instead, and the two markers cannot see each other.
Nothing threw, because both sides kept the legacy `displayName` fallback — but
that is what hid the defect. The marker did nothing across the package
boundary, so a field whose `displayName` a build transform had rewritten still
bound in a bare `CommandForm` and silently unbound inside a `CommandDialog` or
`CommandStepper`: the exact failure the marker was added to prevent, surviving
the fix, with every spec in both packages passing.
Arc's shape wins because arc owns the contract — it defines `asCommandFormField`
and `CommandForm` — and because the argument for the Symbol does not hold up. A
plain property needs no cross-package import either, since either side can test
`isCommandFormField === true` locally, so it gives up none of the version
decoupling; and no build transform renames arbitrary static properties, only
`displayName`, which is the whole hazard.
`CommandFormMarked` is duplicated here rather than imported: the peer range on
@cratis/arc.react spans versions that do not export it, so a named import would
be a hard module-link error rather than a graceful degrade.
Adds the spec neither package had — a component marked the way arc marks one,
with its displayName then overwritten, is recognized here; and one marked here
carries the exact property names arc reads. Renaming either marker now reds
that spec, where before it changed nothing observable in either repo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRCRgRvMz9N8P8NGwR34MM
@woksin
woksin merged commit b4ffbf9 into mainAug 23, 2026
12 of 13 checks passed
@woksin
woksin deleted the feat/components-open-minors branch August 23, 2026 13:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@woksin@einari