Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
8 changes: 5 additions & 3 deletions .github/agents/Specs.adr.accept.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ You **MUST** consider the user input before proceeding (if not empty).
- If no entry exists in the Draft table (older ADR created before INDEX tracking), add the row directly to the Accepted table.

6. **Determine release branch**: Release branches follow the `release/<pkg>-<version>` convention and may jointly cover multiple published packages (e.g., `release/schema-0.21.0-cli-0.16.0`). Do **not** invent a bare version-number branch (e.g., `0.21.0`).
1. Read the ADR's `## Semver Decision` section to find the schema target version.
2. Find the active in-flight release branch with `git branch -r --list 'origin/release/*'`. If one already exists that targets this schema version, use it as `$RELEASE_BRANCH` (the ADR folds into the in-progress release rather than opening a new one).
3. Only if no matching release branch exists, create one from `main` following the `release/<pkg>-<version>` convention, naming every package the release will publish.
1. Find active in-flight release branches with `git branch -r --list 'origin/release/*'`.
2. **Default: use the existing active release branch.** ADR branches are started from the current release branch, so the active branch is the correct target. Do not cross-reference the ADR's semver version against the branch name — the branch name reflects where the release *started*, not the final published version.
3. If exactly one release branch exists, use it as `$RELEASE_BRANCH` without asking.
4. If multiple release branches exist, pick the one the ADR branch was based on (`git merge-base --fork-point` or ask the user).
5. Only if **no** release branch exists at all, create one from `main` following the `release/<pkg>-<version>` convention, naming every package the release will publish.

7. **Create PR**: Commit any uncommitted changes (the status flip and INDEX update), push `$BRANCH`, and open a PR into the release branch using `gh pr create --base $RELEASE_BRANCH`.

Expand Down
8 changes: 4 additions & 4 deletions .github/agents/Specs.adr.create.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ You **MUST** consider the user input before proceeding (if not empty).
1. **Gather intent** — collect all information from the user before doing any exploration. This step has two parts:

**Step 1a — Detect release branch (silent, no user prompt)**
Before asking any questions, run `git rev-parse --abbrev-ref HEAD` to get the current branch. Infer `RELEASE_BRANCH`:
- If the branch looks like a semver release (e.g., `0.13.0`), use it directly as `RELEASE_BRANCH`.
- If the branch matches an ADR name pattern (e.g., `009-color-values`), determine the release branch by reading `package.json` version.
- If the branch is `main` or doesn't match either pattern, fall back to reading `package.json` version and using the current minor version as `RELEASE_BRANCH`.
Before asking any questions, find the active release branch with `git branch -r --list 'origin/release/*'`. Infer `RELEASE_BRANCH`:
- If exactly one `origin/release/*` branch exists, use it as `RELEASE_BRANCH`. Do not cross-reference it against `package.json` version — the branch name reflects where the release started, not the final published version.
- If multiple `origin/release/*` branches exist, pick the most recently created one (latest commit date) as the default and surface it in Question 2 for confirmation.
- If no `origin/release/*` branch exists, fall back to `main` and note this in Question 2 so the user can confirm or provide a branch.

**Step 1b — Sync release branch and determine next ADR number (silent, no user prompt)**
Run `git fetch origin $RELEASE_BRANCH` and fast-forward the local branch if behind (`git pull origin $RELEASE_BRANCH`). Then determine `NEXT_ADR_NUMBER` by finding the highest existing ADR number across **both** sources (zero-padded to 3 digits):
Expand Down
179 changes: 179 additions & 0 deletions adr/059-border-style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# ADR: Stroke Dash Pattern

**Branch**: `059-border-style`
**Created**: 2026-06-26
**Status**: ACCEPTED
**Deciders**: Nathan Curtis (author)
**Supersedes**: *(none)*

---

## Context

`Styles` already carries `strokes` (color), `strokeAlign`, and `strokeWeight` — but has no way to express whether a stroke is dashed or its dash geometry. Figma exposes `strokeDashes` as an array of alternating dash and gap lengths. Code platforms expose dash geometry as a structured object with explicit `dash` and `gap` components.

Without this property, a component spec cannot describe common stroke treatments (dashed outlines, focus rings, divider rules) that are meaningful to both design and code consumers.

---

## Decision Drivers

- **Additive-only**: new fields must be optional to remain `MINOR`
- **Structural, not token-bindable**: dash geometry is a layout primitive in every target platform — not a value that would be token-bound; must use a named structural type (`StrokeDashPattern | null`) rather than `Style`
- **Code-platform naming first**: field names must reflect code-platform consensus before Figma API terminology (constitution rule VI)
- **No redundancy**: presence/absence of the dash pattern object is a complete discriminant between solid and dashed strokes; a separate enum field is not needed if it carries no independent information
- **Naming consistency with existing stroke family**: new fields should share the `stroke` prefix established by `strokes`, `strokeWeight`, and `strokeAlign`
- **Type ↔ schema symmetry**: new types require matching schema definitions

---

## Options Considered

### Option A: `strokeDashPattern` only *(Selected)*

Add a single `strokeDashPattern?: StrokeDashPattern | null` field to `Styles`. Presence signals a dashed stroke; absence (or `null`) signals a solid stroke. No separate style enum.

**Pros**:
- Presence/absence is a complete discriminant — `null` means solid, `{ dash, gap }` means dashed
- No redundancy: a `strokeStyle: DASHED` field without a `strokeDashPattern` is incomplete; the geometry *is* the style declaration
- Naming is consistent with the existing `stroke*` family (`strokes`, `strokeWeight`, `strokeAlign`)
- Simpler contract — one field instead of two; consumers check for presence, not enum value

**Cons / Trade-offs**:
- Consumers that want to emit `DASHED` without specifying geometry cannot do so — but Figma always provides explicit `strokeDashes` values, so this case does not arise in practice

---

### Option B: `strokeStyle` + `strokeDashPattern` *(Rejected)*

Add both a `strokeStyle: 'SOLID' | 'DASHED' | null` enum field and a `strokeDashPattern` geometry field.

**Rejected because**: `strokeStyle` is fully redundant — its value can always be inferred from whether `strokeDashPattern` is present. Two fields for one concept without independent information violates the minimal-surface driver. Additionally, `strokeStyle` risks reader confusion: `strokes` is the color field in this same `Styles` object, and `strokeStyle` sounds like a style variant of that color rather than a line-style enum.

---

### Option C: `borderStyle` + `borderDashPattern` *(Rejected)*

Use the `border` prefix (`borderStyle: 'SOLID' | 'DASHED'`, `borderDashPattern: { dash, gap }`) following the CSS box-model vocabulary.

**Rejected because**: CSS box model is the only major platform that uses "border" for this concept. SwiftUI uses `StrokeStyle` / `.stroke()`; Jetpack Compose uses `Stroke` and `PathEffect.dashPathEffect`. The existing `Styles` fields already establish the `stroke` prefix for this semantic family. Switching to `border` would create an inconsistent naming split within the same object — `strokes`/`strokeWeight`/`strokeAlign` on one side, `borderStyle`/`borderDashPattern` on the other — for no cross-platform naming gain.

---

## Decision

### Type changes (`types/`)

| File | Change | Bump |
|------|--------|------|
| `Styles.ts` | Add `StrokeDashPattern` interface — `{ dash: number; gap: number }` | MINOR |
| `Styles.ts` | Add `strokeDashPattern?: StrokeDashPattern \| null` to `Styles` | MINOR |
| `Styles.ts` | Add `'strokeDashPattern'` to `StyleKey` union | MINOR |

**Example — new type** (`types/Styles.ts`):

```yaml
StrokeDashPattern:
dash: number # dash segment length in pixels
gap: number # gap segment length in pixels
```

**Example — new Styles field**:

```yaml
# Before
styles:
strokes: "#FF0000"
strokeWeight: 2

# After — strokeDashPattern presence indicates a dashed stroke
styles:
strokes: "#FF0000"
strokeWeight: 2
strokeDashPattern:
dash: 8
gap: 4
```

### Schema changes (`schema/`)

| File | Change | Bump |
|------|--------|------|
| `styles.schema.json` | Add `StrokeDashPattern` definition — object with required `dash` and `gap` (both `number`) | MINOR |
| `styles.schema.json` | Add `StrokeDashPatternStyleValue` definition — `oneOf [StrokeDashPattern, null]` | MINOR |
| `styles.schema.json` | Add `strokeDashPattern` property to `Styles` referencing `StrokeDashPatternStyleValue` | MINOR |

**Example — new schema definitions** (`schema/styles.schema.json`):

```yaml
StrokeDashPattern:
type: object
description: "Dash geometry for a dashed stroke. dash and gap are in pixels."
properties:
dash:
type: number
description: "Dash segment length in pixels"
gap:
type: number
description: "Gap segment length in pixels"
required: [dash, gap]
additionalProperties: false

StrokeDashPatternStyleValue:
description: >
Dash pattern for a stroke. Present (non-null) when the stroke is dashed;
null or absent when the stroke is solid. Structural property — not token-bindable.
oneOf:
- $ref: "#/definitions/StrokeDashPattern"
- type: null

# New Styles property
Styles.properties.strokeDashPattern:
$ref: "#/definitions/StrokeDashPatternStyleValue"
description: >
Dash geometry for a dashed stroke. Presence indicates a dashed stroke;
null or absent indicates solid. dash and gap are in pixels.
```

### Notes

- `strokeDashPattern` is not token-bindable — it is a structural property typed as a named type rather than `Style`, consistent with `LayoutMode`, `Position`, and `WrapAlignment`.
- Solid strokes are represented by omitting `strokeDashPattern` or setting it to `null`. No separate `strokeStyle` field is needed.
- Figma's `strokeDashes` array maps to `{ dash, gap }` using index 0 and 1. Patterns with more than two alternating values collapse to the first pair — transformer responsibility, not schema.

---

## Type ↔ Schema Impact

- **Symmetric**: Yes
- **Parity check**:
- `StrokeDashPattern` → `StrokeDashPattern` definition (object `{ dash, gap }`)
- `Styles.strokeDashPattern` → `Styles.properties.strokeDashPattern`
- `StyleKey` addition is type-only; no schema counterpart needed (it is an internal utility type, not schema-serialized)

---

## Downstream Impact

| Consumer | Impact | Action required |
|----------|--------|-----------------|
| `specs-from-figma` | New optional output field | Map Figma `strokeDashes` array to `strokeDashPattern: { dash, gap }` when non-empty; omit or emit `null` when the stroke is solid |
| `specs-cli` | Recompile against new types | No behavior change; new field passes through to output automatically |
| `specs-plugin-2` | Recompile against new types | No behavior change; new field rendered by existing style output path |

---

## Semver Decision

**Version bump**: `0.26.x → 0.27.0` (`MINOR`)

**Justification**: One new optional field on an existing type. No existing type signature, field name, or schema property is removed or renamed. Additive change → `MINOR` per constitution versioning rule.

---

## Consequences

- Consumers can represent dashed stroke treatments (dashed outlines, focus rings, divider rules) in component specs
- `strokeDashPattern` is a nullable structural property — consumers that do not support dashed strokes can safely ignore it
- Solid strokes require no change to existing output; the field is absent when not applicable
- Any tool validating against `schema/styles.schema.json` must update to the new version to accept the new property
6 changes: 3 additions & 3 deletions adr/060-subcomponent-source-metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Branch**: `060-subcomponent-source-metadata`
**Created**: 2026-06-27
**Status**: DRAFT
**Status**: ACCEPTED
**Deciders**: Nathan Curtis (author)
**Supersedes**: *(none)*

Expand Down Expand Up @@ -196,9 +196,9 @@ Subcomponent:

## Semver Decision

**Version bump**: `0.27.x → 0.28.0` (`MINOR`)
**Version bump**: within `0.27.x` — no version change required

**Justification**: All changes are additive — a new optional field on `Subcomponent` and a new exported type `SubcomponentSource`. No existing field is removed, renamed, or narrowed. Per the constitution: "MINOR for additive types or new optional fields."
**Justification**: This change ships as part of the active `release/schema-0.27.0-cli-0.23.0` release. All changes are additive (optional field, new exported type); no bump beyond the release version is warranted.

---

Expand Down
3 changes: 2 additions & 1 deletion adr/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

| # | Title | Highlights |
|---|-------|------------|
| 060 | Subcomponent Figma Source Identity — `Subcomponent.source` | |
| 059 | Border Style and Dash Pattern — `borderStyle` and `borderDashPattern` on `Styles` | |
| 058 | Wrapper Collapse Config Flag — `processing.wrapperCollapse` | |
| 057 | Fix `Metadata.generator.version` type: `number` → `string` | |
Expand Down Expand Up @@ -36,6 +35,8 @@

| # | Title | Highlights |
|---|-------|------------|
| 060 | Subcomponent Figma Source Identity — `Subcomponent.source` | Add optional `SubcomponentSource` (`pageId`, `nodeId`, `nodeType`) to `Subcomponent`; enables reverse-direction tools to resolve `SubcomponentRef` to Figma nodes |
| 059 | Stroke Dash Pattern — `strokeDashPattern` on `Styles` | Add `StrokeDashPattern { dash, gap }` structural type; presence = dashed stroke, null/absent = solid; not token-bindable |
| 058 | Collapsing Wrapped Primitives — `processing.collapsePrimitiveWrapper` | Add optional boolean to `Config.processing` (default false); strips plain container wrappers around a single text/glyph child and promotes the leaf to spec root |
| 057 | Fix `Metadata.generator.version` type: `number` → `string` | Corrects type mismatch — field holds semver strings (e.g. `"1.10.0"`) in all producers; was incorrectly typed as `number` |
| 055 | Variant State Classification via `processing.states` | Add `VariantStateEntry` type; add `Config.processing.states` — classifies Figma variant props as browser-driven or consumer-controlled for CSS selector and contract output |
Expand Down
42 changes: 28 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to `@directededges/specs-cli` are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.23.0] - 2026-07-01

Specs generated by the CLI now capture dashed stroke styling and record where each subcomponent lives back in the source Figma file — both come from upstream engine and schema updates, with no CLI-side changes required to pick them up.

### Changed

- **Generated specs now include dashed stroke data** — Any element with a dashed (not solid) stroke now emits `strokeDashPattern` in its styles, typed as `{ dash, gap }`. Previously dashed strokes fell through as unrecognized styling and were dropped from output.
- **Subcomponent entries now include their Figma source location** — Each entry under `subcomponents` in generated specs now carries a `source` field (`pageId`, `nodeId`, `nodeType`) pointing back to the originating Figma node.

### Dependency updates

- **`@directededges/specs-schema` bumped to `^0.27.0`** — Adds the `StrokeDashPattern` type backing dashed stroke output and `SubcomponentSource` backing the new subcomponent `source` field.
- **`@directededges/specs-from-figma` bumped to `^0.25.0`** — Implements dashed stroke extraction (ADR-059) and subcomponent source population (ADR-060). Also stops reformatting VARIANT enum values and defaults, so generated specs now match Figma's raw variant option strings exactly.


## [0.22.0] - 2026-06-22

Label and icon components can now produce cleaner specs when `collapsePrimitiveWrapper` is enabled — a plain frame wrapping a lone text or glyph element is stripped out and the primitive becomes the spec root. This release also fixes `specs fetch` crashing on large Figma files and adds clearer diagnostic output for 403 access errors.
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@directededges/specs-cli",
"version": "0.22.0",
"version": "0.23.0",
"description": "Command-line interface for Specs design system operations",
"type": "module",
"main": "./dist/index.js",
Expand All @@ -20,8 +20,8 @@
"test": "cd ../.. && vitest -c vitest.config.ts packages/cli/tests"
},
"dependencies": {
"@directededges/specs-schema": "^0.26.0",
"@directededges/specs-from-figma": "^0.24.0",
"@directededges/specs-schema": "^0.27.0",
"@directededges/specs-from-figma": "^0.25.0",
"commander": "^11.1.0",
"fs-extra": "^11.2.0",
"yaml": "^2.3.4",
Expand Down
Loading