From 850d791697ab895167ce76be6a9c0e4de13b8949 Mon Sep 17 00:00:00 2001 From: Nathan Curtis <1165904+nathanacurtis@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:58:46 -0400 Subject: [PATCH 1/2] feat(schema): rename SlotProp.minItems/maxItems to minChildren/maxChildren (ADR-056) (#158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(release): improve changelog quality guidance in release agents Require human-friendly prose in release summaries and individual bullets ("So what?" framing, not API descriptions). Add empty-section pruning. Co-Authored-By: Claude Sonnet 4.6 * feat(schema): rename SlotProp.minItems/maxItems to minChildren/maxChildren (ADR-056) Aligns slot constraint field names with Figma native slotSettings API. Adds anyOf to SlotProp, populated from preferredValues when allowPreferredValuesOnly is true. Bumps schema to 0.25.0 (breaking change). Co-Authored-By: Claude Sonnet 4.6 * adr(056): mark ADR-056 as accepted Co-Authored-By: Claude Sonnet 4.6 * docs: update slot constraints docs for minChildren/maxChildren rename (ADR-056) Updates schema/props, config/slot-constraints, guides/slot-constraints, and analyze/props to reflect the minItems/maxItems → minChildren/maxChildren rename and the new native slotSettings source alongside code-only props. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- .github/agents/CLI.release.agent.md | 11 +- .github/agents/Schema.release.agent.md | 16 +- adr/056-slot-children-constraints.md | 184 ++++++++++++++++++ adr/INDEX.md | 1 + packages/schema/schema/component.schema.json | 10 +- packages/schema/schema/workspace.schema.json | 2 +- packages/schema/types/Config.ts | 2 +- packages/schema/types/Props.ts | 8 +- site/src/content/docs/cli/analyze/props.md | 18 +- .../content/docs/config/slot-constraints.md | 16 +- .../content/docs/guides/slot-constraints.md | 46 ++--- site/src/content/docs/schema/config.md | 2 +- site/src/content/docs/schema/props.md | 9 +- 13 files changed, 257 insertions(+), 68 deletions(-) create mode 100644 adr/056-slot-children-constraints.md diff --git a/.github/agents/CLI.release.agent.md b/.github/agents/CLI.release.agent.md index 8201b84..192ecdb 100644 --- a/.github/agents/CLI.release.agent.md +++ b/.github/agents/CLI.release.agent.md @@ -41,8 +41,15 @@ All commands in this agent run from the **CLI package directory**: `packages/cli 2. **Verify CHANGELOG**: Read `packages/cli/CHANGELOG.md`. Confirm: - An entry exists for this version (e.g., `## [0.6.0]`) - The entry has a date (use today if missing) - - A **Summary** line exists at the top of the version entry (immediately after the heading), providing a high-level overview of this release - - The entry has content under Added/Changed/Removed/Fixed + - A **Summary paragraph** exists at the top of the version entry (immediately after the heading). This is written last, after all bullets are done. It must: + - Open with the most important user-facing capability in this release + - Answer "So what?" — what can users now do that they couldn't before? + - Avoid implementation vocabulary (registries, transformer pipelines, ref swaps, upstream) in favor of plain language + - Be 2–4 sentences; every sentence should carry meaning a user cares about + - If missing or too technical, draft a replacement and update the file before continuing + - Individual bullets lead with a **bold plain sentence** that describes what the change *enables* — not just what was added. "A new command for analyzing props across a catalog" not "New command for on-demand analysis passes over component specs." If bullets read like API docs, rewrite them before continuing. + - **Empty sections are removed** — if `### Changed`, `### Removed`, or `### Fixed` has no bullets, delete that section heading entirely. Do not leave placeholder empty sections. + - The entry has content under at least one of Added/Changed/Fixed - A **Dependency updates** subsection summarizes what changed in upstream packages. To write this: 1. Read the specs-schema CHANGELOG (`packages/schema/CHANGELOG.md`) for the `` entry 2. Read the specs-from-figma CHANGELOG for the `` entry diff --git a/.github/agents/Schema.release.agent.md b/.github/agents/Schema.release.agent.md index 6454e2f..34d1c3a 100644 --- a/.github/agents/Schema.release.agent.md +++ b/.github/agents/Schema.release.agent.md @@ -36,13 +36,15 @@ All commands in this agent run from the **schema package directory**: `packages/ 2. **Verify CHANGELOG**: Read `packages/schema/CHANGELOG.md`. Confirm: - An entry exists for this version (e.g., `## [0.16.0]`) - The entry has an appended date (e.g., `## [0.16.0] - 2026-04-05`). If missing, use today's date. - - A **Summary** line exists at the top of the version entry (immediately after the heading), providing a 3–4 sentence high-level overview of the release changes. - - If missing, draft one by reading the entry's Added/Changed/Removed/Fixed sections and add it. - - When summarizing: - - Combine related points into a single summary sentence where possible - - Favor summarizing the most impactful changes, such as features at a higher-level in the spec and/or with more individual changelog items - - - The entry groups content under Added/Changed/Removed/Fixed + - A **Summary paragraph** exists at the top of the version entry (immediately after the heading). This is written last, after all bullets are done. It must: + - Open with the most important user-facing capability in this release + - Answer "So what?" — what can consumers of this schema now express or do that they couldn't before? + - Avoid implementation vocabulary (type aliases, discriminated unions, internal identifiers) in favor of plain language about what the types *enable* + - Be 2–4 sentences; every sentence should carry meaning a user cares about + - If missing or too technical, draft a replacement and update the file before continuing + - Individual bullets lead with a **bold plain sentence** that describes what the property or type *enables* — not just what was added. If bullets read like type declaration docs, rewrite them before continuing. + - **Empty sections are removed** — if `### Changed`, `### Removed`, or `### Fixed` has no bullets, delete that section heading entirely. + - The entry groups content under at least one of Added/Changed/Removed/Fixed If incomplete, STOP and report what's missing. diff --git a/adr/056-slot-children-constraints.md b/adr/056-slot-children-constraints.md new file mode 100644 index 0000000..fd131f5 --- /dev/null +++ b/adr/056-slot-children-constraints.md @@ -0,0 +1,184 @@ +# ADR 056: Rename `SlotProp.minItems`/`maxItems` to `minChildren`/`maxChildren` + +**Branch**: `adr/056-slot-children-constraints` +**Created**: 2026-06-12 +**Status**: ACCEPTED +**Deciders**: Nathan Curtis (author) +**Supersedes**: *(none)* + +--- + +## Context + +`SlotProp` (introduced in ADR-014) carries optional `minItems` and `maxItems` fields populated by the `slotConstraints` processing config. The names were chosen to echo JSON Schema's array vocabulary. + +Figma's native `SLOT` property type now exposes a `slotSettings` object directly on `componentPropertyDefinitions` entries. Its shape is: + +```json +{ + "type": "SLOT", + "slotSettings": { + "minChildren": 1, + "maxChildren": 1, + "allowPreferredValuesOnly": true, + "stretchChildOnInsert": false, + "displayEmptyByDefault": false + }, + "preferredValues": [ + { "type": "COMPONENT_SET", "key": "48b446783d63308a34b5efc506fb4f0e407a8a2a" } + ] +} +``` + +Figma uses `minChildren`/`maxChildren` — not `minItems`/`maxItems`. Additionally, `allowPreferredValuesOnly: true` combined with the `preferredValues` array provides a richer, first-class source for `anyOf` (the permitted component types), replacing the code-only prop convention. + +The mismatch between the current schema field names (`minItems`/`maxItems`) and the Figma-native names (`minChildren`/`maxChildren`) is unnecessary friction: it requires a translation layer in specs-from-figma, creates confusion when reading Figma output alongside spec output, and diverges from UI design vocabulary where "children" is the universal term for nested content across platforms (React `children`, SwiftUI child views, Jetpack Compose content lambdas, web slots). + +--- + +## Decision Drivers + +- **Align with Figma-native names**: `minChildren`/`maxChildren` match the Figma API, eliminating a translation layer in specs-from-figma +- **Platform generality**: "children" is the idiomatic term for slot content across React, SwiftUI, Compose, and web — "items" is an array-centric term that implies ordered list semantics +- **Types and schema in sync**: the rename must be applied symmetrically to `types/Props.ts` and `schema/component.schema.json` +- **No runtime logic in this package**: only type declarations and schema + +--- + +## Options Considered + +### Option A: Rename to `minChildren`/`maxChildren` *(Selected)* + +Rename both fields in `SlotProp` and the JSON schema. A MAJOR bump is required because the old field names are removed. + +**Pros**: +- Direct 1:1 correspondence with Figma's `slotSettings.minChildren`/`maxChildren` — specs-from-figma can write through without translation +- "Children" is the canonical term in every major UI framework for content passed into a component — more portable than "items" +- Removes the translation layer that would otherwise be needed when reading `slotSettings` from `componentPropertyDefinitions` + +**Cons / Trade-offs**: +- MAJOR bump: any serialized spec using `minItems`/`maxItems` must be migrated. In practice, `slotConstraints` is a new opt-in feature with limited adoption, making the blast radius small. + +--- + +### Option B: Keep `minItems`/`maxItems`, add translation in specs-from-figma *(Rejected)* + +Leave the schema unchanged; have specs-from-figma silently map `minChildren` → `minItems` when reading `slotSettings`. + +**Rejected because**: it introduces a permanent mismatch between the schema vocabulary and the upstream source (Figma API), complicates debugging, and delays the naming problem rather than solving it. The feature is new enough that a MAJOR rename is cheap now. + +--- + +## Decision + +### Type changes (`types/`) + +| File | Change | Bump | +|------|--------|------| +| `Props.ts` | Rename `minItems?: number` → `minChildren?: number` on `SlotProp` | MAJOR | +| `Props.ts` | Rename `maxItems?: number` → `maxChildren?: number` on `SlotProp` | MAJOR | + +**Example — new shape** (`types/Props.ts`): +```ts +// Before +export interface SlotProp { + type: 'slot'; + default?: string | null; + nullable?: boolean; + minItems?: number; // @since 0.14.0 + maxItems?: number; // @since 0.14.0 + anyOf?: string[]; + $extensions?: PropExtensions; +} + +// After +export interface SlotProp { + type: 'slot'; + default?: string | null; + nullable?: boolean; + minChildren?: number; // @since 0.25.0 + maxChildren?: number; // @since 0.25.0 + anyOf?: string[]; + $extensions?: PropExtensions; +} +``` + +### Schema changes (`schema/`) + +| File | Change | Bump | +|------|--------|------| +| `component.schema.json` | Rename property `minItems` → `minChildren` under `#/definitions/SlotProp` | MAJOR | +| `component.schema.json` | Rename property `maxItems` → `maxChildren` under `#/definitions/SlotProp` | MAJOR | + +**Example — new shape** (`schema/component.schema.json`): +```json +"SlotProp": { + "properties": { + "minChildren": { + "type": "integer", + "minimum": 0, + "description": "Minimum number of children this slot accepts" + }, + "maxChildren": { + "type": "integer", + "minimum": 0, + "description": "Maximum number of children this slot accepts" + } + } +} +``` + +### Notes + +- `anyOf` is unchanged — it is not a Figma-native field name and remains appropriate for the spec vocabulary (permitted component types). +- The `@since` JSDoc annotation on the renamed fields advances to `0.25.0` (the next schema version). + +--- + +## Type ↔ Schema Impact + +- **Symmetric**: Yes — both `minChildren` and `maxChildren` are renamed in both `types/Props.ts` and `schema/component.schema.json`. +- **Parity check**: `SlotProp.minChildren` → `#/definitions/SlotProp/properties/minChildren`; same for `maxChildren`. + +--- + +## Downstream Impact + +| Consumer | Impact | Action required | +|----------|--------|-----------------| +| `specs-from-figma` | Writes `minItems`/`maxItems` onto `SlotProp` in `SlotConstraints.promote()` | Update to write `minChildren`/`maxChildren`; update `CONSTRAINT_SUFFIXES` and `parseConstraintKey` to match code-only prop naming convention | +| `specs-cli` | Reads `SlotProp` for schema validation and output serialization | Recompile against updated schema package — no logic changes required | +| Serialized spec files | Any `.yaml`/`.json` spec using `minItems` or `maxItems` on a slot prop fails schema validation | Rename fields in spec files | +| `specs-plugin-2` | Uses `specs-from-figma` types at build time | Rebuild against updated `specs-from-figma` | + +### specs-from-figma implementation notes + +Beyond the field rename, specs-from-figma gains a new reading path for slot constraints: + +**Reading priority for `minChildren`/`maxChildren`**: +1. If the slot's `componentPropertyDefinitions` entry has `slotSettings.minChildren`/`maxChildren` defined, use those values directly (no code-only prop required). +2. Otherwise, fall back to code-only props named `{slotName} minChildren` / `{slotName} maxChildren`. + +**Reading priority for `anyOf`**: +1. If `slotSettings.allowPreferredValuesOnly === true`, resolve `anyOf` from `preferredValues` — look up each entry's `key` to its component (or component set) name asynchronously, using the same runtime-agnostic resolution pattern as `instanceOf` (plugin: `figma.importComponentByKeyAsync`; REST: indexer key lookup). +2. Otherwise, fall back to code-only prop `{slotName} anyOf` if present. + +The code-only prop convention (`minItems`/`maxItems` suffixes) must also be updated to `minChildren`/`maxChildren` to remain consistent with the renamed schema fields. Existing libraries using the old code-only prop names will need to update their prop names in Figma. Libraries that upgrade to native `slotSettings` require no code-only props at all. + +--- + +## Semver Decision + +**Version bump**: `0.24.0 → 0.25.0` (`MAJOR`) + +**Justification**: Two existing optional fields (`minItems`, `maxItems`) are removed and replaced by new fields with different names. Per constitution section III, removing any field — optional or not — is a MAJOR change because existing serialized specs referencing those fields become invalid. + +--- + +## Consequences + +- specs-from-figma reads `slotSettings` natively from `componentPropertyDefinitions`, with no translation layer for field names. +- Libraries using Figma's native slot settings (`minChildren`/`maxChildren` via `slotSettings`) can drop the code-only prop convention entirely. +- Libraries still using code-only props must rename their props from `{slot} minItems` / `{slot} maxItems` to `{slot} minChildren` / `{slot} maxChildren`. +- Downstream consumers reading `SlotProp` must rename field access; schema validation will catch mismatches at generate time. +- `anyOf` is now populated from `preferredValues` (native Figma source) when `allowPreferredValuesOnly` is true — more reliable than a free-text code-only prop. diff --git a/adr/INDEX.md b/adr/INDEX.md index 5b1179a..96f0cce 100644 --- a/adr/INDEX.md +++ b/adr/INDEX.md @@ -4,6 +4,7 @@ | # | Title | Highlights | |---|-------|------------| +| 056 | Rename `SlotProp.minItems`/`maxItems` → `minChildren`/`maxChildren` | Align with Figma-native `slotSettings` field names; `anyOf` populated from `preferredValues` when `allowPreferredValuesOnly` is true | | 055 | Variant State Classification via `processing.states` | | | 054 | Workspace Schema File | | | 053 | Transform Command and Configuration | | diff --git a/packages/schema/schema/component.schema.json b/packages/schema/schema/component.schema.json index 2ee8896..56821f9 100644 --- a/packages/schema/schema/component.schema.json +++ b/packages/schema/schema/component.schema.json @@ -395,15 +395,15 @@ "nullable": { "type": "boolean" }, - "minItems": { + "minChildren": { "type": "integer", "minimum": 0, - "description": "Minimum number of items this slot accepts" + "description": "Minimum number of children this slot accepts" }, - "maxItems": { + "maxChildren": { "type": "integer", "minimum": 0, - "description": "Maximum number of items this slot accepts" + "description": "Maximum number of children this slot accepts" }, "anyOf": { "type": "array", @@ -525,7 +525,7 @@ "properties": { "path": { "type": "array", - "minItems": 1, + "minChildren": 1, "items": { "type": "string" }, diff --git a/packages/schema/schema/workspace.schema.json b/packages/schema/schema/workspace.schema.json index a588b45..d057d65 100644 --- a/packages/schema/schema/workspace.schema.json +++ b/packages/schema/schema/workspace.schema.json @@ -96,7 +96,7 @@ "slotConstraints": { "type": "boolean", "default": false, - "description": "Whether to consolidate slot constraints (anyOf, minItems, maxItems) from code-only props into the slot property" + "description": "Whether to consolidate slot constraints (anyOf, minChildren, maxChildren) from code-only props into the slot property" }, "variantDepth": { "type": "number", diff --git a/packages/schema/types/Config.ts b/packages/schema/types/Config.ts index 130a63f..4401e3d 100644 --- a/packages/schema/types/Config.ts +++ b/packages/schema/types/Config.ts @@ -79,7 +79,7 @@ export interface Config { glyphNamePattern?: string; /** Naming pattern used to detect the code-only props container layer (e.g. "Code only props"). Optional; absence means no code-only prop extraction. */ codeOnlyPropsPattern?: string; - /** Whether to consolidate slot constraints (anyOf, minItems, maxItems) from code-only props into the slot property. Optional; defaults to false. @since 0.14.0 */ + /** Whether to consolidate slot constraints (anyOf, minChildren, maxChildren) from code-only props into the slot property. Optional; defaults to false. @since 0.14.0 */ slotConstraints?: boolean; /** Depth of variant expansion: 1-3 or 9999 for unlimited. Optional; defaults to 9999. */ variantDepth?: 1 | 2 | 3 | 9999; diff --git a/packages/schema/types/Props.ts b/packages/schema/types/Props.ts index cc8e97a..0002031 100644 --- a/packages/schema/types/Props.ts +++ b/packages/schema/types/Props.ts @@ -104,10 +104,10 @@ export interface SlotProp { default?: string | null; /** Whether this slot prop accepts a null value */ nullable?: boolean; - /** Minimum number of items this slot accepts. @since 0.14.0 */ - minItems?: number; - /** Maximum number of items this slot accepts. @since 0.14.0 */ - maxItems?: number; + /** Minimum number of children this slot accepts. @since 0.25.0 */ + minChildren?: number; + /** Maximum number of children this slot accepts. @since 0.25.0 */ + maxChildren?: number; /** Component type names permitted in this slot. @since 0.14.0 */ anyOf?: string[]; /** DTCG §5.2.3 platform-specific extensions. @since 0.14.0 */ diff --git a/site/src/content/docs/cli/analyze/props.md b/site/src/content/docs/cli/analyze/props.md index 5c73d29..785b4a0 100644 --- a/site/src/content/docs/cli/analyze/props.md +++ b/site/src/content/docs/cli/analyze/props.md @@ -53,8 +53,8 @@ enumCount: 3 default: filled nullable: false slotAnyOf: null -slotMinItems: null -slotMaxItems: null +slotMinChildren: null +slotMaxChildren: null figmaType: null ``` @@ -71,8 +71,8 @@ Each prop entry has: | `default` | Default value, or `null` | | `nullable` | `true` when the prop accepts `null` | | `slotAnyOf` | Allowed component types for a slot prop, or `null` | -| `slotMinItems` | Minimum slot items, or `null` | -| `slotMaxItems` | Maximum slot items, or `null` | +| `slotMinChildren` | Minimum slot children, or `null` | +| `slotMaxChildren` | Maximum slot children, or `null` | | `figmaType` | Figma property type (`VARIANT`, `TEXT`, etc.), or `null` | ## Aggregate Structure @@ -181,14 +181,14 @@ slots: name: items anyOf: - dsActionListItem - minItems: null - maxItems: null + minChildren: null + maxChildren: null nullable: false - component: dsButton name: children anyOf: null - minItems: null - maxItems: null + minChildren: null + maxChildren: null nullable: false ``` @@ -288,7 +288,7 @@ Are slot constraints (`anyOf`, `minItems`, `maxItems`) used consistently? Identify slots that would benefit from tighter constraints. ### Actions -- **[component.slotName]** — add `anyOf: [X]`, `minItems: N`. Breaking: N. +- **[component.slotName]** — add `anyOf: [X]`, `minChildren: N`. Breaking: N. ## Open Questions diff --git a/site/src/content/docs/config/slot-constraints.md b/site/src/content/docs/config/slot-constraints.md index 9d6b492..e9dcc79 100644 --- a/site/src/content/docs/config/slot-constraints.md +++ b/site/src/content/docs/config/slot-constraints.md @@ -3,7 +3,7 @@ title: "Slot Constraints" description: "Consolidate slot constraints from code-only props into the slot property" --- -Consolidate slot constraints (`anyOf`, `minItems`, `maxItems`) from code-only props into the slot property. In the example below, the `DS Alert / Actions` subcomponent carries its constraints as code-only props named `childrenAnyof`, `childrenMinitems`, and `childrenMaxitems`. +Consolidate slot constraints (`anyOf`, `minChildren`, `maxChildren`) into the slot property. Constraints are read from two sources — Figma's native `slotSettings` API (when the slot has native settings configured) and code-only props (the legacy naming convention). Both sources produce the same output fields. :::tip[Guide] See the [Slot Constraints](/specs/guides/slot-constraints/) guide as well as the [Figma Slots for Repeating Items](https://nathanacurtis.substack.com/p/figma-slots-for-repeating-items) blog post for how constraint consolidation works. @@ -14,7 +14,7 @@ See the [Slot Constraints](/specs/guides/slot-constraints/) guide as well as the ```yaml config: processing: - slotConstraints: true # Consolidate slot constraints from code-only props + slotConstraints: true ``` ## Result @@ -26,13 +26,13 @@ config: "props": { "children": { "type": "slot" }, "childrenAnyof": { "type": "string", "examples": ["DS Button"] }, - "childrenMaxitems": { "type": "string", "examples": ["2"] }, - "childrenMinitems": { "type": "string", "examples": ["1"] } + "childrenMaxchildren": { "type": "string", "examples": ["2"] }, + "childrenMinchildren": { "type": "string", "examples": ["1"] } } } ``` -**With** consolidation (`true`) they collapse into the `children` slot as `anyOf`/`minItems`/`maxItems`, and the standalone props are removed: +**With** consolidation (`true`) they collapse into the `children` slot as `anyOf`/`minChildren`/`maxChildren`, and the standalone props are removed: ```json { @@ -40,8 +40,8 @@ config: "children": { "type": "slot", "anyOf": ["DS Button"], - "minItems": 1, - "maxItems": 2 + "minChildren": 1, + "maxChildren": 2 } } } @@ -51,7 +51,7 @@ config: - **Type**: boolean - **Default**: `false` -- **Effect**: When `true`, slot constraint metadata discovered in code-only props is merged into the corresponding slot property. When `false`, slot constraints are not consolidated. +- **Effect**: When `true`, slot constraint metadata is merged into the corresponding slot property — sourced from Figma native `slotSettings` first, falling back to code-only props. When `false`, slot constraints are not consolidated. ## Path diff --git a/site/src/content/docs/guides/slot-constraints.md b/site/src/content/docs/guides/slot-constraints.md index 022a28d..d6fa703 100644 --- a/site/src/content/docs/guides/slot-constraints.md +++ b/site/src/content/docs/guides/slot-constraints.md @@ -5,23 +5,31 @@ description: "Express quantity and content-type constraints on slot props" -Slots describe placeable content areas in a component — regions where consumers insert child components at runtime. By default, a `SlotProp` captures the slot's default content and nullability but says nothing about **how many items** the slot accepts or **which component types** are allowed. The `slotConstraints` feature promotes those rules to first-class fields on every slot prop. +Slots describe placeable content areas in a component — regions where consumers insert child components at runtime. By default, a `SlotProp` captures the slot's default content and nullability but says nothing about **how many children** the slot accepts or **which component types** are allowed. The `slotConstraints` feature promotes those rules to first-class fields on every slot prop. ## The Problem -Design systems often have slots with implicit rules. An avatar group accepts 1–4 avatars. A toolbar permits only `Button` and `IconButton`. In Figma, these constraints live as code-only text properties on the slot's container layer — props named things like "Min Items", "Max Items", and "Permitted Items." Without `slotConstraints`, those props appear as ordinary code-only extensions, buried under `$extensions` metadata. Consumers must know to look there and interpret the naming convention themselves. +Design systems often have slots with implicit rules. An avatar group accepts 1–4 avatars. A toolbar permits only `Button` and `IconButton`. Without `slotConstraints`, those constraints are either absent from specs entirely or buried as code-only extension metadata. Consumers must know to look there and interpret the naming convention themselves. ## What It Does -When `slotConstraints` is enabled, the processing engine detects constraint-related code-only props on slot layers and consolidates them into three optional fields directly on `SlotProp`: +When `slotConstraints` is enabled, the processing engine reads constraint data from two sources and consolidates it into three optional fields directly on `SlotProp`: | Field | Type | Meaning | |-------|------|---------| -| `minItems` | `number` | Minimum number of items the slot accepts | -| `maxItems` | `number` | Maximum number of items the slot accepts | +| `minChildren` | `number` | Minimum number of children the slot accepts | +| `maxChildren` | `number` | Maximum number of children the slot accepts | | `anyOf` | `string[]` | Component type names permitted in the slot | -The field names deliberately echo JSON Schema's array-constraint vocabulary, making their semantics immediately familiar. +### Source 1 — Figma native `slotSettings` + +Figma's component property API exposes `slotSettings` on SLOT-typed properties. When present, `minChildren` and `maxChildren` are read directly. When `allowPreferredValuesOnly` is true, `anyOf` is resolved from the slot's `preferredValues` list (Figma component keys are looked up asynchronously and resolved to component names). + +### Source 2 — code-only props (fallback) + +If `slotSettings` is absent or doesn't supply a field, the engine falls back to the code-only prop naming convention: a text prop named `{slotName} minChildren` (or the legacy `{slotName} minItems`), `{slotName} maxChildren` (or `{slotName} maxItems`), or `{slotName} anyOf` on the slot's code-only props container. + +Both sources produce the same output. Native `slotSettings` takes priority; code-only props fill in any gaps. ### Before (without `slotConstraints`) @@ -30,18 +38,6 @@ items: type: slot default: null nullable: true - $extensions: - com.figma: - codeOnlyProps: - Min Items: - type: string - default: "1" - Max Items: - type: string - default: "4" - Permitted Items: - type: string - default: "Avatar" ``` ### After (with `slotConstraints: true`) @@ -51,17 +47,17 @@ items: type: slot default: null nullable: true - minItems: 1 - maxItems: 4 + minChildren: 1 + maxChildren: 4 anyOf: - Avatar ``` -Constraints move from supplementary metadata to the prop's top-level contract. Consumers read slot rules the same way they read `default` or `nullable` — no indirection required. +Constraints move from implicit convention to the prop's top-level contract. Consumers read slot rules the same way they read `default` or `nullable` — no indirection required. ## When to Use It -Enable `slotConstraints` when your Figma library follows the code-only prop naming convention for slot constraints ("Min Items", "Max Items", "Permitted Items"). If your library does not use that convention, the flag has no effect. +Enable `slotConstraints` when your Figma library uses native slot settings, code-only props for slot constraints, or both. The flag has no effect on slots that carry neither. This feature is most valuable when: @@ -71,11 +67,9 @@ This feature is most valuable when: ## Configuration -Add `slotConstraints: true` under `model.processing` in your config file: - ```yaml # specs.config.yaml -model: +config: processing: slotConstraints: true ``` @@ -86,7 +80,7 @@ model: Slot constraints describe **intrinsic slot semantics** — they define what the slot *is*, not where the data came from. A code implementation enforces the same min/max regardless of whether the spec originated in Figma. That's why the fields live directly on `SlotProp` rather than inside `$extensions` platform metadata. -The `anyOf` field uses plain component-name strings, consistent with `instanceOf` on anatomy elements. No cross-component reference resolution is required. +The field names `minChildren`/`maxChildren` align with Figma's native `slotSettings` API, making the source data and the spec output use the same vocabulary. The `anyOf` field uses plain component-name strings, consistent with `instanceOf` on anatomy elements. ## Further Reading diff --git a/site/src/content/docs/schema/config.md b/site/src/content/docs/schema/config.md index d2d379e..d261af4 100644 --- a/site/src/content/docs/schema/config.md +++ b/site/src/content/docs/schema/config.md @@ -12,7 +12,7 @@ Controls how specs are generated. See the [feature guides](/specs/features/) for | `subcomponents` | `object` | — | Subcomponent detection — `scope` (NESTED or PAGE), `match` patterns, `exclude` patterns. Absent = no detection | | `glyphNamePattern` | `string` | — | Name prefix for identifying glyph/icon instances | | `codeOnlyPropsPattern` | `string` | — | Name pattern for code-only prop containers | -| `slotConstraints` | `boolean` | `false` | Emit `minItems`, `maxItems`, `anyOf` on slot props | +| `slotConstraints` | `boolean` | `false` | Emit `minChildren`, `maxChildren`, `anyOf` on slot props | | `variantDepth` | `1 \| 2 \| 3 \| 9999` | `9999` | Maximum variant nesting depth (9999 = unlimited) | | `details` | `'FULL' \| 'LAYERED'` | `'LAYERED'` | Output detail level | | `inferNumberProps` | `boolean` | `false` | Infer number-typed props from Figma variant values | diff --git a/site/src/content/docs/schema/props.md b/site/src/content/docs/schema/props.md index c08c3f5..eb36901 100644 --- a/site/src/content/docs/schema/props.md +++ b/site/src/content/docs/schema/props.md @@ -59,12 +59,12 @@ Inferred from Figma variant values when [`inferNumberProps`](/specs/schema/confi | `type` | `'slot'` | Yes | | | `default` | `string \| null` | No | Default slot content | | `nullable` | `boolean` | No | Whether `null` is a valid value | -| `minItems` | `number` | No | Minimum number of items (since 0.14.0) | -| `maxItems` | `number` | No | Maximum number of items (since 0.14.0) | +| `minChildren` | `number` | No | Minimum number of children the slot accepts (since 0.25.0) | +| `maxChildren` | `number` | No | Maximum number of children the slot accepts (since 0.25.0) | | `anyOf` | `string[]` | No | Permitted component type names (since 0.14.0) | | `$extensions` | `PropExtensions` | No | Vendor extensions | -Slot constraint properties (`minItems`, `maxItems`, `anyOf`) are emitted when [`slotConstraints`](/specs/schema/config.md/#processing) is enabled in config. +Slot constraint properties (`minChildren`, `maxChildren`, `anyOf`) are emitted when [`slotConstraints`](/specs/schema/config.md/#processing) is enabled in config. ## Extensions @@ -88,5 +88,6 @@ The `$extensions` object holds vendor-specific metadata. Currently only the `com ## Further Reading - [ADR 027 — Code-Only Props](https://github.com/DirectedEdges/specs/blob/main/adr/027-code-only-props.md) — surfaces Figma code-only props with `$extensions` source metadata -- [ADR 028 — Slot Quantity and Content Constraints](https://github.com/DirectedEdges/specs/blob/main/adr/028-slot-constraints.md) — adds `minItems`, `maxItems`, `anyOf` to SlotProp +- [ADR 028 — Slot Quantity and Content Constraints](https://github.com/DirectedEdges/specs/blob/main/adr/028-slot-constraints.md) — adds `anyOf` to SlotProp; originally added `minItems`/`maxItems` (renamed in ADR-056) +- [ADR 056 — Rename SlotProp.minItems/maxItems → minChildren/maxChildren](https://github.com/DirectedEdges/specs/blob/main/adr/056-slot-children-constraints.md) — aligns field names with Figma native `slotSettings`; adds native `preferredValues` resolution - [ADR 029 — NumberProp](https://github.com/DirectedEdges/specs/blob/main/adr/029-number-prop.md) — adds the `NumberProp` type with opt-in inference From 2f634030c8c1c674bd6a006f7edab78d1530ca2d Mon Sep 17 00:00:00 2001 From: Nathan Curtis <1165904+nathanacurtis@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:05:54 -0400 Subject: [PATCH 2/2] Revert "feat(schema): rename SlotProp.minItems/maxItems to minChildren/maxChildren (ADR-056) (#158)" This reverts commit 850d791697ab895167ce76be6a9c0e4de13b8949. --- .github/agents/CLI.release.agent.md | 11 +- .github/agents/Schema.release.agent.md | 16 +- adr/056-slot-children-constraints.md | 184 ------------------ adr/INDEX.md | 1 - packages/schema/schema/component.schema.json | 10 +- packages/schema/schema/workspace.schema.json | 2 +- packages/schema/types/Config.ts | 2 +- packages/schema/types/Props.ts | 8 +- site/src/content/docs/cli/analyze/props.md | 18 +- .../content/docs/config/slot-constraints.md | 16 +- .../content/docs/guides/slot-constraints.md | 46 +++-- site/src/content/docs/schema/config.md | 2 +- site/src/content/docs/schema/props.md | 9 +- 13 files changed, 68 insertions(+), 257 deletions(-) delete mode 100644 adr/056-slot-children-constraints.md diff --git a/.github/agents/CLI.release.agent.md b/.github/agents/CLI.release.agent.md index 192ecdb..8201b84 100644 --- a/.github/agents/CLI.release.agent.md +++ b/.github/agents/CLI.release.agent.md @@ -41,15 +41,8 @@ All commands in this agent run from the **CLI package directory**: `packages/cli 2. **Verify CHANGELOG**: Read `packages/cli/CHANGELOG.md`. Confirm: - An entry exists for this version (e.g., `## [0.6.0]`) - The entry has a date (use today if missing) - - A **Summary paragraph** exists at the top of the version entry (immediately after the heading). This is written last, after all bullets are done. It must: - - Open with the most important user-facing capability in this release - - Answer "So what?" — what can users now do that they couldn't before? - - Avoid implementation vocabulary (registries, transformer pipelines, ref swaps, upstream) in favor of plain language - - Be 2–4 sentences; every sentence should carry meaning a user cares about - - If missing or too technical, draft a replacement and update the file before continuing - - Individual bullets lead with a **bold plain sentence** that describes what the change *enables* — not just what was added. "A new command for analyzing props across a catalog" not "New command for on-demand analysis passes over component specs." If bullets read like API docs, rewrite them before continuing. - - **Empty sections are removed** — if `### Changed`, `### Removed`, or `### Fixed` has no bullets, delete that section heading entirely. Do not leave placeholder empty sections. - - The entry has content under at least one of Added/Changed/Fixed + - A **Summary** line exists at the top of the version entry (immediately after the heading), providing a high-level overview of this release + - The entry has content under Added/Changed/Removed/Fixed - A **Dependency updates** subsection summarizes what changed in upstream packages. To write this: 1. Read the specs-schema CHANGELOG (`packages/schema/CHANGELOG.md`) for the `` entry 2. Read the specs-from-figma CHANGELOG for the `` entry diff --git a/.github/agents/Schema.release.agent.md b/.github/agents/Schema.release.agent.md index 34d1c3a..6454e2f 100644 --- a/.github/agents/Schema.release.agent.md +++ b/.github/agents/Schema.release.agent.md @@ -36,15 +36,13 @@ All commands in this agent run from the **schema package directory**: `packages/ 2. **Verify CHANGELOG**: Read `packages/schema/CHANGELOG.md`. Confirm: - An entry exists for this version (e.g., `## [0.16.0]`) - The entry has an appended date (e.g., `## [0.16.0] - 2026-04-05`). If missing, use today's date. - - A **Summary paragraph** exists at the top of the version entry (immediately after the heading). This is written last, after all bullets are done. It must: - - Open with the most important user-facing capability in this release - - Answer "So what?" — what can consumers of this schema now express or do that they couldn't before? - - Avoid implementation vocabulary (type aliases, discriminated unions, internal identifiers) in favor of plain language about what the types *enable* - - Be 2–4 sentences; every sentence should carry meaning a user cares about - - If missing or too technical, draft a replacement and update the file before continuing - - Individual bullets lead with a **bold plain sentence** that describes what the property or type *enables* — not just what was added. If bullets read like type declaration docs, rewrite them before continuing. - - **Empty sections are removed** — if `### Changed`, `### Removed`, or `### Fixed` has no bullets, delete that section heading entirely. - - The entry groups content under at least one of Added/Changed/Removed/Fixed + - A **Summary** line exists at the top of the version entry (immediately after the heading), providing a 3–4 sentence high-level overview of the release changes. + - If missing, draft one by reading the entry's Added/Changed/Removed/Fixed sections and add it. + - When summarizing: + - Combine related points into a single summary sentence where possible + - Favor summarizing the most impactful changes, such as features at a higher-level in the spec and/or with more individual changelog items + + - The entry groups content under Added/Changed/Removed/Fixed If incomplete, STOP and report what's missing. diff --git a/adr/056-slot-children-constraints.md b/adr/056-slot-children-constraints.md deleted file mode 100644 index fd131f5..0000000 --- a/adr/056-slot-children-constraints.md +++ /dev/null @@ -1,184 +0,0 @@ -# ADR 056: Rename `SlotProp.minItems`/`maxItems` to `minChildren`/`maxChildren` - -**Branch**: `adr/056-slot-children-constraints` -**Created**: 2026-06-12 -**Status**: ACCEPTED -**Deciders**: Nathan Curtis (author) -**Supersedes**: *(none)* - ---- - -## Context - -`SlotProp` (introduced in ADR-014) carries optional `minItems` and `maxItems` fields populated by the `slotConstraints` processing config. The names were chosen to echo JSON Schema's array vocabulary. - -Figma's native `SLOT` property type now exposes a `slotSettings` object directly on `componentPropertyDefinitions` entries. Its shape is: - -```json -{ - "type": "SLOT", - "slotSettings": { - "minChildren": 1, - "maxChildren": 1, - "allowPreferredValuesOnly": true, - "stretchChildOnInsert": false, - "displayEmptyByDefault": false - }, - "preferredValues": [ - { "type": "COMPONENT_SET", "key": "48b446783d63308a34b5efc506fb4f0e407a8a2a" } - ] -} -``` - -Figma uses `minChildren`/`maxChildren` — not `minItems`/`maxItems`. Additionally, `allowPreferredValuesOnly: true` combined with the `preferredValues` array provides a richer, first-class source for `anyOf` (the permitted component types), replacing the code-only prop convention. - -The mismatch between the current schema field names (`minItems`/`maxItems`) and the Figma-native names (`minChildren`/`maxChildren`) is unnecessary friction: it requires a translation layer in specs-from-figma, creates confusion when reading Figma output alongside spec output, and diverges from UI design vocabulary where "children" is the universal term for nested content across platforms (React `children`, SwiftUI child views, Jetpack Compose content lambdas, web slots). - ---- - -## Decision Drivers - -- **Align with Figma-native names**: `minChildren`/`maxChildren` match the Figma API, eliminating a translation layer in specs-from-figma -- **Platform generality**: "children" is the idiomatic term for slot content across React, SwiftUI, Compose, and web — "items" is an array-centric term that implies ordered list semantics -- **Types and schema in sync**: the rename must be applied symmetrically to `types/Props.ts` and `schema/component.schema.json` -- **No runtime logic in this package**: only type declarations and schema - ---- - -## Options Considered - -### Option A: Rename to `minChildren`/`maxChildren` *(Selected)* - -Rename both fields in `SlotProp` and the JSON schema. A MAJOR bump is required because the old field names are removed. - -**Pros**: -- Direct 1:1 correspondence with Figma's `slotSettings.minChildren`/`maxChildren` — specs-from-figma can write through without translation -- "Children" is the canonical term in every major UI framework for content passed into a component — more portable than "items" -- Removes the translation layer that would otherwise be needed when reading `slotSettings` from `componentPropertyDefinitions` - -**Cons / Trade-offs**: -- MAJOR bump: any serialized spec using `minItems`/`maxItems` must be migrated. In practice, `slotConstraints` is a new opt-in feature with limited adoption, making the blast radius small. - ---- - -### Option B: Keep `minItems`/`maxItems`, add translation in specs-from-figma *(Rejected)* - -Leave the schema unchanged; have specs-from-figma silently map `minChildren` → `minItems` when reading `slotSettings`. - -**Rejected because**: it introduces a permanent mismatch between the schema vocabulary and the upstream source (Figma API), complicates debugging, and delays the naming problem rather than solving it. The feature is new enough that a MAJOR rename is cheap now. - ---- - -## Decision - -### Type changes (`types/`) - -| File | Change | Bump | -|------|--------|------| -| `Props.ts` | Rename `minItems?: number` → `minChildren?: number` on `SlotProp` | MAJOR | -| `Props.ts` | Rename `maxItems?: number` → `maxChildren?: number` on `SlotProp` | MAJOR | - -**Example — new shape** (`types/Props.ts`): -```ts -// Before -export interface SlotProp { - type: 'slot'; - default?: string | null; - nullable?: boolean; - minItems?: number; // @since 0.14.0 - maxItems?: number; // @since 0.14.0 - anyOf?: string[]; - $extensions?: PropExtensions; -} - -// After -export interface SlotProp { - type: 'slot'; - default?: string | null; - nullable?: boolean; - minChildren?: number; // @since 0.25.0 - maxChildren?: number; // @since 0.25.0 - anyOf?: string[]; - $extensions?: PropExtensions; -} -``` - -### Schema changes (`schema/`) - -| File | Change | Bump | -|------|--------|------| -| `component.schema.json` | Rename property `minItems` → `minChildren` under `#/definitions/SlotProp` | MAJOR | -| `component.schema.json` | Rename property `maxItems` → `maxChildren` under `#/definitions/SlotProp` | MAJOR | - -**Example — new shape** (`schema/component.schema.json`): -```json -"SlotProp": { - "properties": { - "minChildren": { - "type": "integer", - "minimum": 0, - "description": "Minimum number of children this slot accepts" - }, - "maxChildren": { - "type": "integer", - "minimum": 0, - "description": "Maximum number of children this slot accepts" - } - } -} -``` - -### Notes - -- `anyOf` is unchanged — it is not a Figma-native field name and remains appropriate for the spec vocabulary (permitted component types). -- The `@since` JSDoc annotation on the renamed fields advances to `0.25.0` (the next schema version). - ---- - -## Type ↔ Schema Impact - -- **Symmetric**: Yes — both `minChildren` and `maxChildren` are renamed in both `types/Props.ts` and `schema/component.schema.json`. -- **Parity check**: `SlotProp.minChildren` → `#/definitions/SlotProp/properties/minChildren`; same for `maxChildren`. - ---- - -## Downstream Impact - -| Consumer | Impact | Action required | -|----------|--------|-----------------| -| `specs-from-figma` | Writes `minItems`/`maxItems` onto `SlotProp` in `SlotConstraints.promote()` | Update to write `minChildren`/`maxChildren`; update `CONSTRAINT_SUFFIXES` and `parseConstraintKey` to match code-only prop naming convention | -| `specs-cli` | Reads `SlotProp` for schema validation and output serialization | Recompile against updated schema package — no logic changes required | -| Serialized spec files | Any `.yaml`/`.json` spec using `minItems` or `maxItems` on a slot prop fails schema validation | Rename fields in spec files | -| `specs-plugin-2` | Uses `specs-from-figma` types at build time | Rebuild against updated `specs-from-figma` | - -### specs-from-figma implementation notes - -Beyond the field rename, specs-from-figma gains a new reading path for slot constraints: - -**Reading priority for `minChildren`/`maxChildren`**: -1. If the slot's `componentPropertyDefinitions` entry has `slotSettings.minChildren`/`maxChildren` defined, use those values directly (no code-only prop required). -2. Otherwise, fall back to code-only props named `{slotName} minChildren` / `{slotName} maxChildren`. - -**Reading priority for `anyOf`**: -1. If `slotSettings.allowPreferredValuesOnly === true`, resolve `anyOf` from `preferredValues` — look up each entry's `key` to its component (or component set) name asynchronously, using the same runtime-agnostic resolution pattern as `instanceOf` (plugin: `figma.importComponentByKeyAsync`; REST: indexer key lookup). -2. Otherwise, fall back to code-only prop `{slotName} anyOf` if present. - -The code-only prop convention (`minItems`/`maxItems` suffixes) must also be updated to `minChildren`/`maxChildren` to remain consistent with the renamed schema fields. Existing libraries using the old code-only prop names will need to update their prop names in Figma. Libraries that upgrade to native `slotSettings` require no code-only props at all. - ---- - -## Semver Decision - -**Version bump**: `0.24.0 → 0.25.0` (`MAJOR`) - -**Justification**: Two existing optional fields (`minItems`, `maxItems`) are removed and replaced by new fields with different names. Per constitution section III, removing any field — optional or not — is a MAJOR change because existing serialized specs referencing those fields become invalid. - ---- - -## Consequences - -- specs-from-figma reads `slotSettings` natively from `componentPropertyDefinitions`, with no translation layer for field names. -- Libraries using Figma's native slot settings (`minChildren`/`maxChildren` via `slotSettings`) can drop the code-only prop convention entirely. -- Libraries still using code-only props must rename their props from `{slot} minItems` / `{slot} maxItems` to `{slot} minChildren` / `{slot} maxChildren`. -- Downstream consumers reading `SlotProp` must rename field access; schema validation will catch mismatches at generate time. -- `anyOf` is now populated from `preferredValues` (native Figma source) when `allowPreferredValuesOnly` is true — more reliable than a free-text code-only prop. diff --git a/adr/INDEX.md b/adr/INDEX.md index 96f0cce..5b1179a 100644 --- a/adr/INDEX.md +++ b/adr/INDEX.md @@ -4,7 +4,6 @@ | # | Title | Highlights | |---|-------|------------| -| 056 | Rename `SlotProp.minItems`/`maxItems` → `minChildren`/`maxChildren` | Align with Figma-native `slotSettings` field names; `anyOf` populated from `preferredValues` when `allowPreferredValuesOnly` is true | | 055 | Variant State Classification via `processing.states` | | | 054 | Workspace Schema File | | | 053 | Transform Command and Configuration | | diff --git a/packages/schema/schema/component.schema.json b/packages/schema/schema/component.schema.json index 56821f9..2ee8896 100644 --- a/packages/schema/schema/component.schema.json +++ b/packages/schema/schema/component.schema.json @@ -395,15 +395,15 @@ "nullable": { "type": "boolean" }, - "minChildren": { + "minItems": { "type": "integer", "minimum": 0, - "description": "Minimum number of children this slot accepts" + "description": "Minimum number of items this slot accepts" }, - "maxChildren": { + "maxItems": { "type": "integer", "minimum": 0, - "description": "Maximum number of children this slot accepts" + "description": "Maximum number of items this slot accepts" }, "anyOf": { "type": "array", @@ -525,7 +525,7 @@ "properties": { "path": { "type": "array", - "minChildren": 1, + "minItems": 1, "items": { "type": "string" }, diff --git a/packages/schema/schema/workspace.schema.json b/packages/schema/schema/workspace.schema.json index d057d65..a588b45 100644 --- a/packages/schema/schema/workspace.schema.json +++ b/packages/schema/schema/workspace.schema.json @@ -96,7 +96,7 @@ "slotConstraints": { "type": "boolean", "default": false, - "description": "Whether to consolidate slot constraints (anyOf, minChildren, maxChildren) from code-only props into the slot property" + "description": "Whether to consolidate slot constraints (anyOf, minItems, maxItems) from code-only props into the slot property" }, "variantDepth": { "type": "number", diff --git a/packages/schema/types/Config.ts b/packages/schema/types/Config.ts index 4401e3d..130a63f 100644 --- a/packages/schema/types/Config.ts +++ b/packages/schema/types/Config.ts @@ -79,7 +79,7 @@ export interface Config { glyphNamePattern?: string; /** Naming pattern used to detect the code-only props container layer (e.g. "Code only props"). Optional; absence means no code-only prop extraction. */ codeOnlyPropsPattern?: string; - /** Whether to consolidate slot constraints (anyOf, minChildren, maxChildren) from code-only props into the slot property. Optional; defaults to false. @since 0.14.0 */ + /** Whether to consolidate slot constraints (anyOf, minItems, maxItems) from code-only props into the slot property. Optional; defaults to false. @since 0.14.0 */ slotConstraints?: boolean; /** Depth of variant expansion: 1-3 or 9999 for unlimited. Optional; defaults to 9999. */ variantDepth?: 1 | 2 | 3 | 9999; diff --git a/packages/schema/types/Props.ts b/packages/schema/types/Props.ts index 0002031..cc8e97a 100644 --- a/packages/schema/types/Props.ts +++ b/packages/schema/types/Props.ts @@ -104,10 +104,10 @@ export interface SlotProp { default?: string | null; /** Whether this slot prop accepts a null value */ nullable?: boolean; - /** Minimum number of children this slot accepts. @since 0.25.0 */ - minChildren?: number; - /** Maximum number of children this slot accepts. @since 0.25.0 */ - maxChildren?: number; + /** Minimum number of items this slot accepts. @since 0.14.0 */ + minItems?: number; + /** Maximum number of items this slot accepts. @since 0.14.0 */ + maxItems?: number; /** Component type names permitted in this slot. @since 0.14.0 */ anyOf?: string[]; /** DTCG §5.2.3 platform-specific extensions. @since 0.14.0 */ diff --git a/site/src/content/docs/cli/analyze/props.md b/site/src/content/docs/cli/analyze/props.md index 785b4a0..5c73d29 100644 --- a/site/src/content/docs/cli/analyze/props.md +++ b/site/src/content/docs/cli/analyze/props.md @@ -53,8 +53,8 @@ enumCount: 3 default: filled nullable: false slotAnyOf: null -slotMinChildren: null -slotMaxChildren: null +slotMinItems: null +slotMaxItems: null figmaType: null ``` @@ -71,8 +71,8 @@ Each prop entry has: | `default` | Default value, or `null` | | `nullable` | `true` when the prop accepts `null` | | `slotAnyOf` | Allowed component types for a slot prop, or `null` | -| `slotMinChildren` | Minimum slot children, or `null` | -| `slotMaxChildren` | Maximum slot children, or `null` | +| `slotMinItems` | Minimum slot items, or `null` | +| `slotMaxItems` | Maximum slot items, or `null` | | `figmaType` | Figma property type (`VARIANT`, `TEXT`, etc.), or `null` | ## Aggregate Structure @@ -181,14 +181,14 @@ slots: name: items anyOf: - dsActionListItem - minChildren: null - maxChildren: null + minItems: null + maxItems: null nullable: false - component: dsButton name: children anyOf: null - minChildren: null - maxChildren: null + minItems: null + maxItems: null nullable: false ``` @@ -288,7 +288,7 @@ Are slot constraints (`anyOf`, `minItems`, `maxItems`) used consistently? Identify slots that would benefit from tighter constraints. ### Actions -- **[component.slotName]** — add `anyOf: [X]`, `minChildren: N`. Breaking: N. +- **[component.slotName]** — add `anyOf: [X]`, `minItems: N`. Breaking: N. ## Open Questions diff --git a/site/src/content/docs/config/slot-constraints.md b/site/src/content/docs/config/slot-constraints.md index e9dcc79..9d6b492 100644 --- a/site/src/content/docs/config/slot-constraints.md +++ b/site/src/content/docs/config/slot-constraints.md @@ -3,7 +3,7 @@ title: "Slot Constraints" description: "Consolidate slot constraints from code-only props into the slot property" --- -Consolidate slot constraints (`anyOf`, `minChildren`, `maxChildren`) into the slot property. Constraints are read from two sources — Figma's native `slotSettings` API (when the slot has native settings configured) and code-only props (the legacy naming convention). Both sources produce the same output fields. +Consolidate slot constraints (`anyOf`, `minItems`, `maxItems`) from code-only props into the slot property. In the example below, the `DS Alert / Actions` subcomponent carries its constraints as code-only props named `childrenAnyof`, `childrenMinitems`, and `childrenMaxitems`. :::tip[Guide] See the [Slot Constraints](/specs/guides/slot-constraints/) guide as well as the [Figma Slots for Repeating Items](https://nathanacurtis.substack.com/p/figma-slots-for-repeating-items) blog post for how constraint consolidation works. @@ -14,7 +14,7 @@ See the [Slot Constraints](/specs/guides/slot-constraints/) guide as well as the ```yaml config: processing: - slotConstraints: true + slotConstraints: true # Consolidate slot constraints from code-only props ``` ## Result @@ -26,13 +26,13 @@ config: "props": { "children": { "type": "slot" }, "childrenAnyof": { "type": "string", "examples": ["DS Button"] }, - "childrenMaxchildren": { "type": "string", "examples": ["2"] }, - "childrenMinchildren": { "type": "string", "examples": ["1"] } + "childrenMaxitems": { "type": "string", "examples": ["2"] }, + "childrenMinitems": { "type": "string", "examples": ["1"] } } } ``` -**With** consolidation (`true`) they collapse into the `children` slot as `anyOf`/`minChildren`/`maxChildren`, and the standalone props are removed: +**With** consolidation (`true`) they collapse into the `children` slot as `anyOf`/`minItems`/`maxItems`, and the standalone props are removed: ```json { @@ -40,8 +40,8 @@ config: "children": { "type": "slot", "anyOf": ["DS Button"], - "minChildren": 1, - "maxChildren": 2 + "minItems": 1, + "maxItems": 2 } } } @@ -51,7 +51,7 @@ config: - **Type**: boolean - **Default**: `false` -- **Effect**: When `true`, slot constraint metadata is merged into the corresponding slot property — sourced from Figma native `slotSettings` first, falling back to code-only props. When `false`, slot constraints are not consolidated. +- **Effect**: When `true`, slot constraint metadata discovered in code-only props is merged into the corresponding slot property. When `false`, slot constraints are not consolidated. ## Path diff --git a/site/src/content/docs/guides/slot-constraints.md b/site/src/content/docs/guides/slot-constraints.md index d6fa703..022a28d 100644 --- a/site/src/content/docs/guides/slot-constraints.md +++ b/site/src/content/docs/guides/slot-constraints.md @@ -5,31 +5,23 @@ description: "Express quantity and content-type constraints on slot props" -Slots describe placeable content areas in a component — regions where consumers insert child components at runtime. By default, a `SlotProp` captures the slot's default content and nullability but says nothing about **how many children** the slot accepts or **which component types** are allowed. The `slotConstraints` feature promotes those rules to first-class fields on every slot prop. +Slots describe placeable content areas in a component — regions where consumers insert child components at runtime. By default, a `SlotProp` captures the slot's default content and nullability but says nothing about **how many items** the slot accepts or **which component types** are allowed. The `slotConstraints` feature promotes those rules to first-class fields on every slot prop. ## The Problem -Design systems often have slots with implicit rules. An avatar group accepts 1–4 avatars. A toolbar permits only `Button` and `IconButton`. Without `slotConstraints`, those constraints are either absent from specs entirely or buried as code-only extension metadata. Consumers must know to look there and interpret the naming convention themselves. +Design systems often have slots with implicit rules. An avatar group accepts 1–4 avatars. A toolbar permits only `Button` and `IconButton`. In Figma, these constraints live as code-only text properties on the slot's container layer — props named things like "Min Items", "Max Items", and "Permitted Items." Without `slotConstraints`, those props appear as ordinary code-only extensions, buried under `$extensions` metadata. Consumers must know to look there and interpret the naming convention themselves. ## What It Does -When `slotConstraints` is enabled, the processing engine reads constraint data from two sources and consolidates it into three optional fields directly on `SlotProp`: +When `slotConstraints` is enabled, the processing engine detects constraint-related code-only props on slot layers and consolidates them into three optional fields directly on `SlotProp`: | Field | Type | Meaning | |-------|------|---------| -| `minChildren` | `number` | Minimum number of children the slot accepts | -| `maxChildren` | `number` | Maximum number of children the slot accepts | +| `minItems` | `number` | Minimum number of items the slot accepts | +| `maxItems` | `number` | Maximum number of items the slot accepts | | `anyOf` | `string[]` | Component type names permitted in the slot | -### Source 1 — Figma native `slotSettings` - -Figma's component property API exposes `slotSettings` on SLOT-typed properties. When present, `minChildren` and `maxChildren` are read directly. When `allowPreferredValuesOnly` is true, `anyOf` is resolved from the slot's `preferredValues` list (Figma component keys are looked up asynchronously and resolved to component names). - -### Source 2 — code-only props (fallback) - -If `slotSettings` is absent or doesn't supply a field, the engine falls back to the code-only prop naming convention: a text prop named `{slotName} minChildren` (or the legacy `{slotName} minItems`), `{slotName} maxChildren` (or `{slotName} maxItems`), or `{slotName} anyOf` on the slot's code-only props container. - -Both sources produce the same output. Native `slotSettings` takes priority; code-only props fill in any gaps. +The field names deliberately echo JSON Schema's array-constraint vocabulary, making their semantics immediately familiar. ### Before (without `slotConstraints`) @@ -38,6 +30,18 @@ items: type: slot default: null nullable: true + $extensions: + com.figma: + codeOnlyProps: + Min Items: + type: string + default: "1" + Max Items: + type: string + default: "4" + Permitted Items: + type: string + default: "Avatar" ``` ### After (with `slotConstraints: true`) @@ -47,17 +51,17 @@ items: type: slot default: null nullable: true - minChildren: 1 - maxChildren: 4 + minItems: 1 + maxItems: 4 anyOf: - Avatar ``` -Constraints move from implicit convention to the prop's top-level contract. Consumers read slot rules the same way they read `default` or `nullable` — no indirection required. +Constraints move from supplementary metadata to the prop's top-level contract. Consumers read slot rules the same way they read `default` or `nullable` — no indirection required. ## When to Use It -Enable `slotConstraints` when your Figma library uses native slot settings, code-only props for slot constraints, or both. The flag has no effect on slots that carry neither. +Enable `slotConstraints` when your Figma library follows the code-only prop naming convention for slot constraints ("Min Items", "Max Items", "Permitted Items"). If your library does not use that convention, the flag has no effect. This feature is most valuable when: @@ -67,9 +71,11 @@ This feature is most valuable when: ## Configuration +Add `slotConstraints: true` under `model.processing` in your config file: + ```yaml # specs.config.yaml -config: +model: processing: slotConstraints: true ``` @@ -80,7 +86,7 @@ config: Slot constraints describe **intrinsic slot semantics** — they define what the slot *is*, not where the data came from. A code implementation enforces the same min/max regardless of whether the spec originated in Figma. That's why the fields live directly on `SlotProp` rather than inside `$extensions` platform metadata. -The field names `minChildren`/`maxChildren` align with Figma's native `slotSettings` API, making the source data and the spec output use the same vocabulary. The `anyOf` field uses plain component-name strings, consistent with `instanceOf` on anatomy elements. +The `anyOf` field uses plain component-name strings, consistent with `instanceOf` on anatomy elements. No cross-component reference resolution is required. ## Further Reading diff --git a/site/src/content/docs/schema/config.md b/site/src/content/docs/schema/config.md index d261af4..d2d379e 100644 --- a/site/src/content/docs/schema/config.md +++ b/site/src/content/docs/schema/config.md @@ -12,7 +12,7 @@ Controls how specs are generated. See the [feature guides](/specs/features/) for | `subcomponents` | `object` | — | Subcomponent detection — `scope` (NESTED or PAGE), `match` patterns, `exclude` patterns. Absent = no detection | | `glyphNamePattern` | `string` | — | Name prefix for identifying glyph/icon instances | | `codeOnlyPropsPattern` | `string` | — | Name pattern for code-only prop containers | -| `slotConstraints` | `boolean` | `false` | Emit `minChildren`, `maxChildren`, `anyOf` on slot props | +| `slotConstraints` | `boolean` | `false` | Emit `minItems`, `maxItems`, `anyOf` on slot props | | `variantDepth` | `1 \| 2 \| 3 \| 9999` | `9999` | Maximum variant nesting depth (9999 = unlimited) | | `details` | `'FULL' \| 'LAYERED'` | `'LAYERED'` | Output detail level | | `inferNumberProps` | `boolean` | `false` | Infer number-typed props from Figma variant values | diff --git a/site/src/content/docs/schema/props.md b/site/src/content/docs/schema/props.md index eb36901..c08c3f5 100644 --- a/site/src/content/docs/schema/props.md +++ b/site/src/content/docs/schema/props.md @@ -59,12 +59,12 @@ Inferred from Figma variant values when [`inferNumberProps`](/specs/schema/confi | `type` | `'slot'` | Yes | | | `default` | `string \| null` | No | Default slot content | | `nullable` | `boolean` | No | Whether `null` is a valid value | -| `minChildren` | `number` | No | Minimum number of children the slot accepts (since 0.25.0) | -| `maxChildren` | `number` | No | Maximum number of children the slot accepts (since 0.25.0) | +| `minItems` | `number` | No | Minimum number of items (since 0.14.0) | +| `maxItems` | `number` | No | Maximum number of items (since 0.14.0) | | `anyOf` | `string[]` | No | Permitted component type names (since 0.14.0) | | `$extensions` | `PropExtensions` | No | Vendor extensions | -Slot constraint properties (`minChildren`, `maxChildren`, `anyOf`) are emitted when [`slotConstraints`](/specs/schema/config.md/#processing) is enabled in config. +Slot constraint properties (`minItems`, `maxItems`, `anyOf`) are emitted when [`slotConstraints`](/specs/schema/config.md/#processing) is enabled in config. ## Extensions @@ -88,6 +88,5 @@ The `$extensions` object holds vendor-specific metadata. Currently only the `com ## Further Reading - [ADR 027 — Code-Only Props](https://github.com/DirectedEdges/specs/blob/main/adr/027-code-only-props.md) — surfaces Figma code-only props with `$extensions` source metadata -- [ADR 028 — Slot Quantity and Content Constraints](https://github.com/DirectedEdges/specs/blob/main/adr/028-slot-constraints.md) — adds `anyOf` to SlotProp; originally added `minItems`/`maxItems` (renamed in ADR-056) -- [ADR 056 — Rename SlotProp.minItems/maxItems → minChildren/maxChildren](https://github.com/DirectedEdges/specs/blob/main/adr/056-slot-children-constraints.md) — aligns field names with Figma native `slotSettings`; adds native `preferredValues` resolution +- [ADR 028 — Slot Quantity and Content Constraints](https://github.com/DirectedEdges/specs/blob/main/adr/028-slot-constraints.md) — adds `minItems`, `maxItems`, `anyOf` to SlotProp - [ADR 029 — NumberProp](https://github.com/DirectedEdges/specs/blob/main/adr/029-number-prop.md) — adds the `NumberProp` type with opt-in inference