-
Notifications
You must be signed in to change notification settings - Fork 3
RFC 001: Component Dictionary #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nathanacurtis
wants to merge
2
commits into
main
Choose a base branch
from
rfc/001-component-dictionary
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # CLI invocation and configuration | ||
|
|
||
| Companion to [`README.md`](README.md). The main RFC references this surface from the Architecture section; this file carries the full flag list, config schema, and resolution order. | ||
|
|
||
| ## Selection | ||
|
|
||
| `--emit` accepts file names, group names, or `all` / `defaults` / `none`, composing with `+` / `-`: | ||
|
|
||
| ``` | ||
| specs generate ./specs # defaults | ||
| specs generate ./specs --emit all # every built-in | ||
| specs generate ./specs --emit defaults,platform # add a whole group | ||
| specs generate ./specs --emit defaults,+react,+css # add specific files | ||
| specs generate ./specs --emit defaults,-elements # drop a default | ||
| ``` | ||
|
|
||
| Resolution: per-component sidecar (`component.emit.yaml`) > CLI flag > workspace `Config.emit` > built-in defaults. | ||
|
|
||
| ## `Config.emit` block | ||
|
|
||
| `Config.emit` extends the existing `specs-schema` `Config` type and carries `preset`, `include`, `exclude`, and per-emitter `options`: | ||
|
|
||
| ```yaml | ||
| config: | ||
| emit: | ||
| preset: defaults # group name | 'all' | 'none' | ||
| include: [platform, css] | ||
| exclude: [skeleton] | ||
| options: | ||
| css: { keys: KEBAB } # falls back to config.format.keys | ||
| tailwind: { layer: components, tokenPathTransform: kebab } | ||
| react: { textSlotAsChildren: button } # first text slot by default | ||
| ``` | ||
|
|
||
| ## Identifier casing | ||
|
|
||
| Identifier casing reuses `config.format.keys`; per-emitter `keys` override is available where a platform forces a specific style (e.g. CSS forces kebab regardless of root config). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| # Dictionary composition | ||
|
|
||
| Companion to [`README.md`](README.md). Principles 8 and 10 in the main RFC establish the architecture; this file carries the full reference — the format split, the three lifecycle classes that follow from it, authoring patterns, filesystem layouts, and per-platform losslessness rationale. | ||
|
|
||
| ## The format split is the architecture | ||
|
|
||
| The deepest organizing principle is the format choice itself (Principle 10). Structured contracts (YAML, JSON, or any schema-validated representation) carry what the schema validates: anatomy, props, layout, variants, tokens, bindings — anything adversarially robust against drift. Prose (MD) carries what the schema can't: a11y rationale, behavioral notes, usage prose, design intent. The format choice tells a consumer (and a tool) what lifecycle to expect: | ||
|
|
||
| - **Structured contract files** (YAML in v1; JSON or other schema-validated formats in principle) — schema-validated, regenerable, diffable on facts. A typo fails schema check; values can be cross-referenced and re-derived. | ||
| - **MD files** — authored or projected for humans, validatable only on structure, prose-shaped. A typo doesn't fail anything; consumers read MD like documentation, not like a contract. | ||
|
|
||
| Class membership (generated / authored / smoothed) is a second-order distinction that follows from format + naming. The format alone is enough to know whether a file is a fact-carrier or a prose-carrier. | ||
|
|
||
| ## Lifecycle classes (within the format split) | ||
|
|
||
| | Class | Lifecycle | Examples | Naming convention | | ||
| |---|---|---|---| | ||
| | **Generated** | Regenerable byte-exactly by `specs generate`. The source of truth among derivatives. | `button.md`, `button.docs.md`, `button.react.md`, `button.css`, … | Standard emitter output names | | ||
| | **Authored** | Team-written, persistent, version-controlled, never overwritten by generation. Carries concerns the schema doesn't (Pattern B) or extends a generated edge (Pattern A). | `button.a11y.md`, `button.behavior.md`, `button.usage.md`, `button.team.react.md` | Suffix from a known allowlist (B) or `team.<emitter>` (A) | | ||
| | **Smoothed** | Agent-produced from generated + authored inputs. May drift; replayable. | `button.smoothed.md`, `button.smoothed.docs.md`, `button.smoothed.react.md` | `smoothed.<emitter>` | | ||
|
|
||
| ## Authoring patterns (decided) | ||
|
|
||
| - **Pattern A — extend a generated output.** Write a sibling: `button.team.react.md` extends `button.react.md`. Don't edit the generated file in place — that breaks the regeneration contract (Principle 5). Consumers compose the pair at retrieval time. | ||
| - **Pattern B — supplement with new concerns.** Write a sibling at a known suffix. Starter allowlist: `a11y`, `behavior`, `usage`, `examples`, `migration`. Teams extend the allowlist via `Config.emit.includeAuthored: [<suffix>, ...]`. | ||
| - **Pattern C — agentic adaptation.** Smoothing layer; covered separately (`specs smooth`). Outputs land at `smoothed.<emitter>` by default, but the smoothing tool's operator chooses (Principle 5). | ||
|
|
||
| ## Consumer composition order | ||
|
|
||
| A retrieving agent or human reader composes the dictionary in this order: | ||
|
|
||
| 1. **Generated files** establish shape and canonical projections of the contract. | ||
| 2. **Authored sidecars** layer team-specific concerns and extensions. | ||
| 3. **Smoothed siblings** add prose / pattern recognition (when present). | ||
| 4. **The contract** resolves any precision question by definition (Principle 6). | ||
|
|
||
| Authored content never overrides generated *facts* — it adds to or extends them. Smoothed content never overrides authored content. The contract wins everything on precision. | ||
|
|
||
| ## Filesystem layouts | ||
|
|
||
| Two valid layouts; teams pick: | ||
|
|
||
| **Flat** — recommended for small dictionaries: | ||
|
|
||
| ``` | ||
| button/ | ||
| button.yaml | ||
| button.md, button.docs.md, button.react.md, ... (generated) | ||
| button.a11y.md, button.behavior.md (authored — Pattern B) | ||
| button.team.react.md (authored — Pattern A) | ||
| button.smoothed.md (smoothed) | ||
| ``` | ||
|
|
||
| Teams that want stricter separation can also nest under `generated/`, `authored/`, and `smoothed/` subfolders — useful when `.gitignore`-ing the regenerable `generated/` is desirable. | ||
|
|
||
| ## Per-platform losslessness — what edge artifacts owe their idiom | ||
|
|
||
| Principle 7 says edge artifacts must be lossless within their idiom. In practice: | ||
|
|
||
| | Edge artifact | Lossless within its idiom? | Why | | ||
| |---|---|---| | ||
| | `contract.ts` | Yes — by construction | Pure types and slot signatures; no platform opinions required | | ||
| | `tokens.json`, workspace dictionary files | Yes — within their slice | Pure data inversions of the spec | | ||
| | `css`, `tailwind.css` | Yes for the deterministic ruleset | One class per anatomy element, custom property per token, `[data-*]` selectors per variant. Compound selectors and shared declarations belong in Pattern A siblings if a team wants them | | ||
| | `md`, `docs.md` | Lossless for *orientation* and *data reference* respectively; neither is a per-variant values reference (by design) | Values live in the contract. `md` is the short index; `docs.md` is the comprehensive scripted reference. Neither restates per-variant precision values inline | | ||
| | `react.md`, `webcomponents.md`, `ios.md` | **Worked examples, not edge artifacts** | Losslessness would require platform opinions Principle 4 forbids. These ship as reference renderings of what a mechanical projection looks like in each idiom — copy, modify, throw away — and are explicitly not the React/iOS engineer's starting point. The load-bearing edge for typed-language consumers is `contract.ts` | | ||
|
|
||
| The platform `.md` files are worked examples by design: the scripted output stops where platform opinions would begin, and team-specific completion happens via Pattern A authoring on top of `contract.ts` or via custom emitters. Principle 9 makes this a feedback loop — when many teams write the same extension, that extension belongs in the emitter (or in the schema, if it's a content question). |
35 changes: 35 additions & 0 deletions
35
rfc/001-component-dictionary/script-inference-boundaries.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Script / inference boundaries | ||
|
|
||
| Companion to [`README.md`](README.md). The main RFC establishes the boundary as a principle (1, 4, 5) and summarizes it under Architecture; this file makes it concrete per output and lists the reusable patterns that sit on the script side of the seam. | ||
|
|
||
| Every emitter is a pure, scriptable transform of the spec — no LLM calls, no invented identifiers, no inferred prose. Each output has a *script ceiling* (everything mechanically derivable from the contract) and an *inference seam* (where a downstream agent could usefully add value beyond what scripts can do). Naming both per output makes implementation scope explicit and gives the smoothing layer a concrete brief. | ||
|
|
||
| ## Per-output ceiling and seam | ||
|
|
||
| | Output / group | Script ceiling | Inference seam | | ||
| |---|---|---| | ||
| | `md`, `layout.md`, `elements.md`, `provenance.md` | Direct walk of the spec: prop tables, anatomy, layout tree, element-first override tables, metadata footer | One-line role descriptions, when-to-use guidance, exception callouts on outlier variants, "this element's behavior pattern resembles X" | | ||
| | `md` | Short structural index under a fixed template; every fact byte-traceable to a contract field | Plain-language framing, invariants that need pattern recognition (e.g. "focus universally overrides border weight"), labeling outlier configs as exceptions | | ||
| | `docs.md` | Comprehensive scripted reference — overview counts, anatomy, full API tables, layout, structure, color, effects, conditional logic, token index, provenance | Editorial prose (role descriptions, "when to use" guidance, design-intent annotations) — those belong in the smoothed sibling, not here | | ||
| | `tokens.json` | Inverted view of the contract — flat list of every `$token` referenced with per-element/per-variant usage map. Shape the contract doesn't store directly | None — inference-free by construction; any prose belongs in an adjacent `.md` output | | ||
| | `contract.ts` | Types, defaults, slot signatures, nullable/optional flags | None in v1 — doc comments could be inference territory but belong upstream in the spec | | ||
| | `react.md`, `webcomponents.md`, `ios.md` (worked examples) | Reference renderings: prop types, default-bearing call signatures, attribute reflection, slot maps grounded in `instanceOf` values. Not the engineer's starting point — `contract.ts` is | Idiomatic component naming for instance slots, which slot maps to `children`, attribute-vs-property choices, modifier-chain idioms — these are explicitly out of the worked example's scope | | ||
| | `css` | One class per anatomy element, custom property per `$token`, `[data-*]` selectors per variant configuration | Collapsing repeated declarations into shared selectors; opinionated reset rules | | ||
| | `tailwind.css` | `@layer components` block, `@apply` chains keyed on `data-*` attrs, configurable token-path → theme-path transform | Choosing between `@apply` chains and arbitrary value syntax; collapsing redundant utilities | | ||
| | `fixture.json`, `skeleton.html` | One fixture per variant entry plus boolean-prop coverage; default tree as HTML with `data-*` attrs | Hand-picked "interesting" combos, anti-pattern callouts, ARIA hints | | ||
| | `dictionary.*` (workspace) | Counts, token usage rollups, instance graph from `instanceOf` references | Composition smells, shared-base suggestions, structural-similarity rollups | | ||
| | Accessibility (`role`, `aria-*`, focus order, keyboard) | None today — the schema doesn't carry this | Entirely inference territory; the structured spec gives much richer input than raw nodes (anatomy → roles, layout → focus order, bindings → visibility rules) | | ||
|
|
||
| ## Reusable patterns on the script side | ||
|
|
||
| A few patterns recur across emitters. Build them once at the registry level rather than per-emitter: | ||
|
|
||
| - **Pattern collapse** — when N variants share a prop value and exhibit identical style deltas, collapse to a single rule (e.g. surface "focus always uses `mode/focus/outlineColor` and `strokeWeight: 2`" mechanically, not as inferred prose). | ||
| - **Configurable conventions** — for the few choices that look like opinions but are knob-able (text-slot → `children`, token-path → theme-path), expose `emit.options.<emitter>.*` rather than picking a default silently. | ||
| - **Verbatim identifiers** — anatomy keys, prop names, token paths emit verbatim from the spec. No casing tweaks beyond what `config.format.keys` already declares. | ||
|
|
||
| Every emitter ships with a fixture test: input contract + emitter version → exact byte-equal output. | ||
|
|
||
| ## Agent smoothing layer | ||
|
|
||
| Inference work — anything in the right column above — runs downstream of `specs generate` in a separate stage (e.g. an opt-in `specs smooth` command). How it writes its output — sibling files like `*.smoothed.md`, in-place edits, a separate tree — is the smoothing tool operator's choice, not a constraint imposed by this pipeline. Regeneration is the contract on our side: `specs generate` always restores scripted output to canonical form, so any downstream choice is reversible (Principle 5). The concrete backlog of inference work — prose generation, pattern recognition, exception callouts, platform idioms, accessibility, usage examples — lives in [`smoothing-backlog.md`](smoothing-backlog.md). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| // No React/Vue/Svelte imports. Pure types + slot signatures. | ||
| export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'invisible'; | ||
| export type ButtonSize = 'small' | 'medium' | 'large'; | ||
| export type ButtonState = 'rest' | 'focus' | 'hover' | 'pressed' | 'disabled' | 'inactive'; | ||
| export type ButtonAlign = 'center' | 'start'; | ||
|
|
||
| export interface ButtonProps { | ||
| variant?: ButtonVariant; // default 'secondary' | ||
| size?: ButtonSize; // default 'medium' | ||
| state?: ButtonState; // default 'rest' | ||
| alignContent?: ButtonAlign; // default 'center' | ||
| counter?: boolean; // default false | ||
| dropdown?: boolean; // default false | ||
| leadingVisual?: string | null; // default null | ||
| trailingVisual?: string | null; // default null | ||
| } | ||
|
|
||
| export interface ButtonSlots { | ||
| /* visible when leadingVisual != null */ search?: unknown; | ||
| /* always */ button: string; | ||
| /* visible when counter == true */ counterLabel?: unknown; | ||
| /* visible when trailingVisual != null */ trailingVisual?: unknown; | ||
| /* visible when dropdown == true */ dropdown?: unknown; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| /* Generated. Slots map 1:1 to anatomy keys. */ | ||
| .button { | ||
| --bg: var(--mode-button-default-bgColor-rest); | ||
| --fg: var(--mode-button-default-fgColor-rest); | ||
| --border: var(--mode-button-default-borderColor-rest); | ||
| --radius: var(--functional-size-borderRadius-medium); | ||
| --gap: var(--pattern-size-control-medium-gap); | ||
| --pad-y: var(--pattern-size-control-medium-paddingBlock); | ||
| --pad-x: var(--pattern-size-control-medium-paddingInline-normal); | ||
|
|
||
| display: inline-flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| gap: var(--gap); | ||
| padding: var(--pad-y) var(--pad-x); | ||
| background: var(--bg); | ||
| color: var(--fg); | ||
| border: 1px solid var(--border); | ||
| border-radius: var(--radius); | ||
| } | ||
|
|
||
| .button[data-variant="primary"] { --bg: var(--mode-button-primary-bgColor-rest); /* … */ } | ||
| .button[data-variant="danger"] { --border: var(--mode-button-danger-borderColor-rest); /* … */ } | ||
| .button[data-variant="invisible"] { --bg: var(--mode-button-invisible-bgColor-rest); border: 0; } | ||
|
|
||
| .button[data-size="small"] { --pad-x: var(--pattern-size-control-small-paddingInline-condensed); /* … */ } | ||
| .button[data-size="large"] { --pad-x: var(--pattern-size-control-large-paddingInline-spacious); /* … */ } | ||
|
|
||
| .button[data-state="hover"] { --bg: var(--mode-button-default-bgColor-hover); } | ||
| .button[data-state="pressed"] { --bg: var(--mode-button-default-bgColor-active); } | ||
| .button[data-state="disabled"] { --border: var(--mode-button-default-borderColor-disabled); } | ||
|
|
||
| .button__search, | ||
| .button__trailingVisual, | ||
| .button__dropdown { width: 16px; height: 16px; } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comments aren't a contract, they're just documentation.
I'd advocate for stronger typing. In fact that's something most React component devs miss, which leaves users guessing what props are usable together or not. This can also lead to runtime errors when an edge case isn't officially supported but ended up being shipped in the wild.
I gave it a go (I tested it, it compiles!):