Skip to content

feat(core): add property() factory for typed custom properties#301

Merged
alexgrozav merged 2 commits into
mainfrom
agent/roux/b3f28ab0
Jul 12, 2026
Merged

feat(core): add property() factory for typed custom properties#301
alexgrozav merged 2 commits into
mainfrom
agent/roux/b3f28ab0

Conversation

@alexgrozav

Copy link
Copy Markdown
Contributor

Summary

Phase 1 of the @property spike (SF-10). Adds a property() factory to @styleframe/core for typed custom properties. It composes the existing variable() + atRule('property', …) and returns the same Variable — one custom property, one node in the reference graph. No new token type, no type guard, transpiler untouched: CSS @property emission reuses the existing at-rule consumer.

const space = property('space', '0px', { syntax: '<length>' });
// --space: 0px;  (value declaration, in the calling container)
// @property --space { syntax: "<length>"; inherits: false; initial-value: 0px; }
ref(space); // same Variable — resolves exactly as before

Design decisions (the three open questions)

1. Hoisting / de-dup. The @property block is only valid at the top level, so it is always registered on root, regardless of the calling container. It is de-duped by custom-property name — calling property('space', …) twice emits one @property --space block. The value declaration itself stays in the calling container via the normal variable() path.

  • Known limitation: the registration name is the raw --<instance.name>; it does not reflect custom variableNameFn prefixing or dotted-name transforms. Same limitation as the existing raw -- hatch. Reconciling the two name-derivation paths is deferred to Phase 2.

2. initial-value reconciliation. initialValue defaults to the variable's value, so property('space', '0px', { syntax: '<length>' }) yields initial-value: 0px. Callers can override with an explicit initialValue. Both the --x: declaration and the @property block are emitted.

3. syntax → TS type map. Out of scope here. syntax is an opaque string in Phase 1; the tighter, syntax-derived narrowing that would flow through ref() / Reference<Name> is the Phase 2 RFC (the Variable / Reference type thread), not this PR.

Output stability

Additive. Generated CSS/TS/DTS are byte-identical when property() is unused — the factory only adds an at-rule node to the tree when called.

Test plan

  • engine/core/src/tokens/property.test.ts — 7 unit tests (variable creation, @property registration, inherits default/override, initial-value default/override, de-dup, numeric values)
  • engine/transpiler/src/consume/css/property.test.ts — exact CSS output assertion for the @property block + value declaration
  • pnpm build:nodocs — 11 tasks successful
  • pnpm --filter @styleframe/core --filter @styleframe/transpiler test — core 706 passed, transpiler 883 passed
  • pnpm typecheck — 15 successful
  • oxlint — clean on changed files

Two pre-existing context-shape assertions (declarations.test.ts, selector.test.ts) were updated to include the new property key.

Changeset: @styleframe/core minor.

Composes variable() with an @Property at-rule and returns the same
Variable, so a typed custom property is one node in the reference graph.
The @Property block is hoisted to root and de-duped by name;
initial-value defaults to the variable's value. Transpiler untouched —
CSS emission reuses the existing at-rule consumer.
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
storybook-styleframe-dev Ready Ready Preview, Comment Jul 12, 2026 3:28pm
styleframe-dev Ready Ready Preview, Comment Jul 12, 2026 3:28pm

Request Review

@changeset-bot

changeset-bot Bot commented Jul 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 61f4d0f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@styleframe/core Minor
@styleframe/playground Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Adds the load-bearing case Étoile flagged: property() called inside a
nested selector lands the value declaration in that container while the
@Property registration hoists to root. Also documents the de-dup guard's
initial-value/value desync.
@alexgrozav alexgrozav merged commit e325096 into main Jul 12, 2026
15 checks passed
@alexgrozav alexgrozav deleted the agent/roux/b3f28ab0 branch July 12, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant