Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
65f95f6
feat(adr): draft ADR 042 — Composition as a First-Class Type
nathanacurtis Apr 29, 2026
796264c
feat(adr): revise ADR 042 with clarified composition model
nathanacurtis Apr 29, 2026
5dbc885
feat(adr): revise ADR 042 — no abbreviations, $extensions for slot de…
nathanacurtis Apr 29, 2026
7859344
feat(adr): clarify slot-bound container constraint in ADR 042
nathanacurtis Apr 29, 2026
6cc7303
feat(adr): revise ADR 042 — ComponentExample discriminated union
nathanacurtis Apr 29, 2026
bfb1ffc
feat(adr): add ActionList sensitizing example and one-level-deep prin…
nathanacurtis Apr 29, 2026
8a0c2b2
feat(adr): narrow ADR-042 to Composition structural type; stub 043-045
nathanacurtis Apr 29, 2026
3443aef
feat(adr): author ADRs 043-045 — component examples, slot content, Pr…
nathanacurtis Apr 29, 2026
a2d9205
feat(adr): strengthen ADR-042 options; require elements+layout; fix e…
nathanacurtis Apr 29, 2026
1317146
chore: start @directededges/specs-schema v0.21.0 development
nathanacurtis May 6, 2026
57a578c
chore: start @directededges/specs-cli v0.14.0 development
nathanacurtis May 6, 2026
89a83d3
chore: reserve ADR-044 in INDEX
nathanacurtis May 7, 2026
783043c
feat(site): redesign doc site layout and navigation
nathanacurtis May 7, 2026
4ac7c7c
docs(adr-044): redesign slot content around Component.slotContent + S…
nathanacurtis May 11, 2026
d4787e3
docs(adr-045): tighten PropConfigurations PropBinding context and rat…
nathanacurtis May 11, 2026
b060c84
docs(adr): renumber 043-045 to 046-048; reserve 049 for Nested Slot C…
nathanacurtis May 11, 2026
59b1375
Merge remote-tracking branch 'origin/main' into release/schema-0.21.0…
nathanacurtis May 11, 2026
a1331ca
Merge branch 'release/schema-0.21.0+cli-0.14.0' into 042-composition-…
nathanacurtis May 11, 2026
005f392
docs(adr-046): remove slots field and kind discriminator
nathanacurtis May 11, 2026
f16654b
docs(adr-049): draft options A-D for nested slot compositions; add re…
nathanacurtis May 11, 2026
84af67f
docs(adr-046,047,049): JSON Pointer references; rename to instanceExa…
nathanacurtis May 11, 2026
d297319
Extensions to examples and reviews
nathanacurtis May 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
390 changes: 126 additions & 264 deletions adr/042-composition-type.md

Large diffs are not rendered by default.

262 changes: 262 additions & 0 deletions adr/046-component-examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# ADR: Component Instance Examples — InstanceExample and Component.instanceExamples

**Branch**: `046-component-examples`
**Created**: 2026-04-29
**Status**: DRAFT
**Deciders**: Nathan Curtis (author)
**Depends on**: [ADR-042 — Composition Structural Type](042-composition-type)
**Extended by**: [ADR-047 — Slot Content](047-slot-content), [ADR-049 — Nested Slot Compositions](049-nested-slot-compositions) *(slot fill is expressed via `propConfigurations`; this ADR no longer carries a separate `slots` field)*

---

## Context

ADR-042 established `Composition` as the structural base type. That type has no authoring home yet — no field on `Component` accepts it, and no consumer-facing entry point exists.

Components need a named catalogue of pre-configured examples: a documented set of specific prop values that expresses canonical usages. Tooling can discover and render these without inspecting the raw variant tree. Authors can reference them by key instead of repeating configuration inline.

This ADR covers the simplest case: an example defined entirely by scalar prop values (`state`, `title`, `description`, and similar string/number/boolean props). Slot fills — including the recursive composition story — are scoped out and handled by ADR-049 through `Element.propConfigurations`, not via a dedicated field on `InstanceExample`.

`InstanceExample` is the type for this case. It captures:
- An optional human-readable label (`title`)
- Scalar prop values (`propConfigurations`)

`Component.instanceExamples` is the named record that holds `InstanceExample` entries.

---

## Decision Drivers

- **Named record, not array** — examples must be referenceable by key; `Element.$extensions['com.figma'].defaultComposition` (ADR-047) and the slot-fill mechanism (ADR-049) both point to keys, not indices
- **No discriminator field** — `Component.instanceExamples` will only ever contain `InstanceExample` entries (slot fills are `Element.propConfigurations` per ADR-049; Figma authoring defaults are `Component.slotContent` per ADR-047). No `kind` field is needed because no other shape competes for the same record
- **Scalar-only `propConfigurations`** — `InstanceExample` represents a documented configuration for human readers and tooling, not a live data binding; `PropBinding` belongs in `Element.propConfigurations` (ADR-048), not here
- **Scalar-prop scope here; slot fill is ADR-049 territory** — `InstanceExample` documents scalar prop usages only. Filling slot props is part of the broader composition-recursion story and is handled through `Element.propConfigurations` per ADR-049, not via a dedicated field on `InstanceExample`
- **Additive-only** — new optional field `Component.instanceExamples`; no existing type changed → MINOR
- **Type ↔ schema symmetry** — every field has a schema counterpart (Constitution §I)
- **No runtime logic** — type declarations and schema only (Constitution §II)

---

## Options Considered

### Option A: `InstanceExample` as a record member *(Selected)*

Add `InstanceExample { title?, propConfigurations? }` and a named record `InstanceExamples` on `Component`. No discriminator field — `InstanceExamples` only holds one shape.

```yaml
# ActionListItem — instance examples covering scalar prop variants
title: Action List Item
anatomy:
root:
type: container
label:
type: text
description:
type: text

props:
state:
type: string
title:
type: string
description:
type: string

instanceExamples:
defaultState:
title: Action List Item – default
propConfigurations:
state: default
title: Browse all issues
description: 12 open · 3 closed

activeState:
title: Action List Item – active
propConfigurations:
state: active
title: Browse all issues
description: 12 open · 3 closed

dangerState:
title: Action List Item – danger
propConfigurations:
state: danger
title: Delete branch
description: This action cannot be undone
```

**Pros**:
- Named keys enable reference-by-string from `defaultComposition` (ADR-047) and from slot-fill mechanisms (ADR-049) without type coupling
- Scalar-only `propConfigurations` keeps `InstanceExample` simple and human-readable
- No discriminator field — minimum surface, since no other shape shares the record

**Cons / Trade-offs**:
- None at the scope of this ADR; slot fill is intentionally deferred to ADR-049

---

### Option B: Flat scalar map *(Rejected)*

Store examples as `Record<string, Record<string, string | number | boolean>>` — a named map of prop value maps, with no wrapping object.

**Rejected because**: There's no place for per-example metadata. `title` (and any future additive fields like `description`, `tags`, `deprecated`, `guidelines`) would have to be smuggled into the prop-name space with reserved-key conventions, which collides with real prop names and adds parser complexity. The `InstanceExample` wrapper is a thin object that gives metadata a clean home for free.

---

### Option C: Extend `Variant` for examples *(Rejected)*

Reuse the existing `Variant` type for examples — a `Variant` already has `configuration?`, `elements?`, `layout?`.

**Rejected because**: `Variant` represents a display state driven by prop combination. `InstanceExample` is a documented usage configuration with human intent. They are conceptually distinct; merging them would force `Variant` to carry optional fields that apply to only one of its two roles.

---

## Decision

### Type changes (`types/`)

| File | Change | Bump |
|------|--------|------|
| New: `InstanceExample.ts` | Add `InstanceExample`, `InstanceExamples` | MINOR |
| `Component.ts` | Add optional `examples?: InstanceExamples` | MINOR |
| `index.ts` | Export `InstanceExample`, `InstanceExamples` | MINOR |

**New types** (`types/InstanceExample.ts`):

```yaml
# InstanceExample — a pre-configured usage of the whole component
InstanceExample:
title?: string
propConfigurations?:
Record<string, string | number | boolean> # scalar prop values only
# slot prop fills are NOT here — see ADR-049

# InstanceExamples — named record on Component (field: instanceExamples)
InstanceExamples: Record<string, InstanceExample>
```

**Extended `Component`** (`types/Component.ts`):

```yaml
# Before
Component:
title: string
anatomy: Anatomy
props?: Props
subcomponents?: Subcomponents
default: Variant
variants?: Variants
invalidVariantCombinations?: PropConfigurations[]
metadata?: Metadata

# After
Component:
title: string
anatomy: Anatomy
props?: Props
subcomponents?: Subcomponents
default: Variant
variants?: Variants
invalidVariantCombinations?: PropConfigurations[]
metadata?: Metadata
instanceExamples?: InstanceExamples # new — named usages of this component (scalar prop configurations)
```

### Schema changes (`schema/`)

| File | Change | Bump |
|------|--------|------|
| `component.schema.json` | Add `#/definitions/InstanceExample` | MINOR |
| `component.schema.json` | Add `#/definitions/InstanceExamples` | MINOR |
| `component.schema.json` | Add `instanceExamples` property to `#/definitions/Component` | MINOR |

**New definition** (`#/definitions/InstanceExample`):

```yaml
InstanceExample:
type: object
description: "A pre-configured usage of the whole component: scalar prop values."
properties:
title:
type: string
propConfigurations:
type: object
description: "Scalar prop values for this example. Binding and slot fills are not permitted here."
additionalProperties:
oneOf:
- type: string
- type: number
- type: boolean
additionalProperties: false
```

**New definition** (`#/definitions/InstanceExamples`):

```yaml
InstanceExamples:
type: object
description: "Named examples for this component."
patternProperties:
"^[a-zA-Z0-9_-]+$":
$ref: "#/definitions/InstanceExample"
additionalProperties: false
```

**New property** in `#/definitions/Component/properties`:

```yaml
instanceExamples:
$ref: "#/definitions/InstanceExamples"
description: "Named instance examples (documented usages) for this component."
```

### Out of scope for this ADR

- **`Component.slotContent`** and **`SlotBinding.$extensions['com.figma'].default`** — Figma authoring defaults for component slots; see ADR-047
- **`Element.propConfigurations` PropBinding** — see ADR-048
- **Cross-boundary slot fill (recursion)** — filling a nested instance's slot prop from a parent context, including all forms of inline-Composition or named-composition reference; see ADR-049

### Notes

- `InstanceExample.propConfigurations` is scalar-only (`string | number | boolean`). Prop binding (`PropBinding`) belongs in `Element.propConfigurations`, which represents live data flow. `InstanceExample` represents a documented configuration — human-intended, not runtime-driven.
- `InstanceExample` does *not* include slot-fill information. Filling a slot is filling a prop; the value form (Composition object, named-composition key, etc.) lives on `Element.propConfigurations` and is settled by ADR-049. Earlier drafts of this ADR carried a `slots: Record<string, string>` field on `InstanceExample` as a placeholder; that field has been removed in favor of the unified mechanism.
- `InstanceExamples` uses `patternProperties` rather than `additionalProperties` on an object schema to satisfy Draft 7's handling of `$ref` alongside `additionalProperties: false`.

---

## Type ↔ Schema Impact

- **Symmetric**: Yes
- **Parity check**:
- `InstanceExample { title?, propConfigurations? }` ↔ `#/definitions/InstanceExample`
- `InstanceExamples = Record<string, InstanceExample>` ↔ `#/definitions/InstanceExamples` (`patternProperties`)
- `Component.instanceExamples?: InstanceExamples` ↔ `#/definitions/Component/properties/instanceExamples`

---

## Downstream Impact

| Consumer | Impact | Action required |
|----------|--------|-----------------|
| `specs-from-figma` | Must detect and emit `Component.instanceExamples` with `InstanceExample` entries from example frames | Read new types; implement instance example detection |
| `specs-cli` | Recompile; output includes `instanceExamples` key when present | Recompile; no breaking change |
| `specs-plugin-2` | Recompile; example rendering is a follow-on capability | Recompile; pass through example data initially |

---

## Semver Decision

**Version bump**: `0.19.0 → 0.20.0` (`MINOR`)

**Justification**: All changes are additive — new optional field `Component.instanceExamples`, new types `InstanceExample` and `InstanceExamples`; no existing type is removed or narrowed → MINOR per Constitution §III.

**Naming Governance** (Constitution §VI): `instanceExamples` qualifies the field against the lower-level `examples` patterns reused in `Props` (sample values) and `Anatomy` (sample content). Code-platform alignment is weak for this concept — it's a specs-schema-specific authoring construct — so this is rule 3 (no code-platform consensus; ambiguity inside the schema is the deciding factor).

---

## Consequences

- `Component.instanceExamples` is a first-class named record on `Component`; example configurations are discoverable alongside the component that owns them
- `InstanceExample` expresses a complete scalar-prop configuration in a single, referenceable entry
- All example keys are plain strings — no inline nesting, no cross-component references
- Slot fill is *not* part of `InstanceExample`; it lives on `Element.propConfigurations` per ADR-049, keeping `InstanceExample` cleanly scoped to scalar-prop documentation
Loading