Merge branch 'native-authoring' into jsx-component-properties

# Conflicts:
#	packages/core/src/design-jsx/reference/authoring.md
#	packages/docs/reference/design-authoring.md
#	skills/open-pencil/references/design-authoring.md
This commit is contained in:
Danila Poyarkov 2026-09-13 20:55:05 +03:00
commit b4f119a01b
5 changed files with 75 additions and 32 deletions

View file

@ -26,8 +26,8 @@ This reference describes scene creation, not React DOM output. Use the `render`
## Variables and components
- Create document variables before referencing them with `designVar('id-or-name')`. `defineVars` groups references; it does not create variable collections.
- COLOR references work in paint props. FLOAT references work in `w`, `h`, `gap`, padding, corner radii, `strokeWidth`, `opacity`, text `size`/`fontSize`, `lineHeight`, and `letterSpacing`; grid gaps and wrapped `rowGap` also support them.
- References preserve real graph bindings, not just copied values. Initial scalar layout resolves the parent's inherited collection mode. Missing or incorrectly typed scalar variables are errors.
- COLOR references work in paint props. FLOAT references work in `w`, `h`, `gap`, padding, corner radii, `strokeWidth`, `opacity`, text `size`/`fontSize`, `lineHeight`, and `letterSpacing`. Grid `columnGap`/`rowGap` and wrapped flex `rowGap` also support FLOAT references; grid `gap` overrides both axis-specific gaps. Use numbers or FLOAT references for these scalar props, not CSS unit strings.
- References preserve real graph bindings, not just copied values. Set the intended collection mode on the parent before creating scalar-bound content: initial scalar layout resolves that inherited mode. This does not guarantee automatic scalar layout recomputation after a later mode switch. Verify resulting geometry as well as paint when changing modes. Missing or incorrectly typed scalar variables are errors.
- `bind` maps supported scene-field paths to variable IDs or references when no shorthand exists. Use semantic tokens consistently rather than declaring unused collections.
- A reusable JavaScript function shares source code, not component identity. Use `Component`, `ComponentSet`, and `Instance` for editable main components and linked instances.
- `Instance` resolves an existing component through `of`, `component`, or `componentId`. Component-set children named `variant=Primary`, for example, define variants that can be selected when instantiating the set.
@ -35,6 +35,7 @@ This reference describes scene creation, not React DOM output. Use the `render`
- Child `propertyRefs` connect fields to stable property IDs, for example `[{ propertyId: 'message', field: 'TEXT' }]`. Supported fields are `TEXT`, `VISIBLE`, and `INSTANCE_SWAP`; text and swap references require text and instance nodes respectively. References do not depend on layer names.
- Instance assignments use the native string values (including `'true'` / `'false'` for BOOLEAN properties and component IDs for swaps). For example `Instance({ of: noteId, properties: { message: 'Updated review' } })`. Assignments persist through component synchronization; unknown IDs and invalid values fail rather than silently creating inert overrides. Select variants through component-set variant props, not through instance property assignments.
- Reuse existing local or library components before recreating them. Keep meaningful text, visibility, and swap properties exposed rather than hand-editing cloned child nodes.
- Distinguish a main component's default size from its instance's placement constraints. Verify the actual instance bounds in narrower parents; a requested Fill dimension alone is not proof that inherited sizing changed. Do not compensate for a sizing mismatch with guessed heights, clipping, or manually positioned siblings.
## Verification

View file

@ -17,7 +17,7 @@ export const AUTHORING_EXAMPLES: readonly AuthoringExample[] = [
{
title: 'Variable-bound spacing and typography',
jsx: dedent`<Frame name="Bound note" w={280} h="hug" flex="col" gap={designVar('Space/small')} p={designVar('Space/medium')} bg="#FFFFFF">
<Text name="Message" w="fill" size={designVar('Type/body')} color="#252A31">A note that grows with its content.</Text>
<Text name="Message" w="fill" size={designVar('Type/body')} lineHeight={designVar('Type/body-leading')} letterSpacing={designVar('Type/body-tracking')} color="#252A31">A note that grows with its content.</Text>
</Frame>`
}
]

View file

@ -28,8 +28,8 @@ This reference describes scene creation, not React DOM output. Use the `render`
## Variables and components
- Create document variables before referencing them with `designVar('id-or-name')`. `defineVars` groups references; it does not create variable collections.
- COLOR references work in paint props. FLOAT references work in `w`, `h`, `gap`, padding, corner radii, `strokeWidth`, `opacity`, text `size`/`fontSize`, `lineHeight`, and `letterSpacing`; grid gaps and wrapped `rowGap` also support them.
- References preserve real graph bindings, not just copied values. Initial scalar layout resolves the parent's inherited collection mode. Missing or incorrectly typed scalar variables are errors.
- COLOR references work in paint props. FLOAT references work in `w`, `h`, `gap`, padding, corner radii, `strokeWidth`, `opacity`, text `size`/`fontSize`, `lineHeight`, and `letterSpacing`. Grid `columnGap`/`rowGap` and wrapped flex `rowGap` also support FLOAT references; grid `gap` overrides both axis-specific gaps. Use numbers or FLOAT references for these scalar props, not CSS unit strings.
- References preserve real graph bindings, not just copied values. Set the intended collection mode on the parent before creating scalar-bound content: initial scalar layout resolves that inherited mode. This does not guarantee automatic scalar layout recomputation after a later mode switch. Verify resulting geometry as well as paint when changing modes. Missing or incorrectly typed scalar variables are errors.
- `bind` maps supported scene-field paths to variable IDs or references when no shorthand exists. Use semantic tokens consistently rather than declaring unused collections.
- A reusable JavaScript function shares source code, not component identity. Use `Component`, `ComponentSet`, and `Instance` for editable main components and linked instances.
- `Instance` resolves an existing component through `of`, `component`, or `componentId`. Component-set children named `variant=Primary`, for example, define variants that can be selected when instantiating the set.
@ -37,6 +37,7 @@ This reference describes scene creation, not React DOM output. Use the `render`
- Child `propertyRefs` connect fields to stable property IDs, for example `[{ propertyId: 'message', field: 'TEXT' }]`. Supported fields are `TEXT`, `VISIBLE`, and `INSTANCE_SWAP`; text and swap references require text and instance nodes respectively. References do not depend on layer names.
- Instance assignments use the native string values (including `'true'` / `'false'` for BOOLEAN properties and component IDs for swaps). For example `Instance({ of: noteId, properties: { message: 'Updated review' } })`. Assignments persist through component synchronization; unknown IDs and invalid values fail rather than silently creating inert overrides. Select variants through component-set variant props, not through instance property assignments.
- Reuse existing local or library components before recreating them. Keep meaningful text, visibility, and swap properties exposed rather than hand-editing cloned child nodes.
- Distinguish a main component's default size from its instance's placement constraints. Verify the actual instance bounds in narrower parents; a requested Fill dimension alone is not proof that inherited sizing changed. Do not compensate for a sizing mismatch with guessed heights, clipping, or manually positioned siblings.
## Verification
@ -57,7 +58,7 @@ The examples below are executed by the authoring-reference tests. Create the nam
```jsx
<Frame name="Bound note" w={280} h="hug" flex="col" gap={designVar('Space/small')} p={designVar('Space/medium')} bg="#FFFFFF">
<Text name="Message" w="fill" size={designVar('Type/body')} color="#252A31">A note that grows with its content.</Text>
<Text name="Message" w="fill" size={designVar('Type/body')} lineHeight={designVar('Type/body-leading')} letterSpacing={designVar('Type/body-tracking')} color="#252A31">A note that grows with its content.</Text>
</Frame>
```

View file

@ -28,8 +28,8 @@ This reference describes scene creation, not React DOM output. Use the `render`
## Variables and components
- Create document variables before referencing them with `designVar('id-or-name')`. `defineVars` groups references; it does not create variable collections.
- COLOR references work in paint props. FLOAT references work in `w`, `h`, `gap`, padding, corner radii, `strokeWidth`, `opacity`, text `size`/`fontSize`, `lineHeight`, and `letterSpacing`; grid gaps and wrapped `rowGap` also support them.
- References preserve real graph bindings, not just copied values. Initial scalar layout resolves the parent's inherited collection mode. Missing or incorrectly typed scalar variables are errors.
- COLOR references work in paint props. FLOAT references work in `w`, `h`, `gap`, padding, corner radii, `strokeWidth`, `opacity`, text `size`/`fontSize`, `lineHeight`, and `letterSpacing`. Grid `columnGap`/`rowGap` and wrapped flex `rowGap` also support FLOAT references; grid `gap` overrides both axis-specific gaps. Use numbers or FLOAT references for these scalar props, not CSS unit strings.
- References preserve real graph bindings, not just copied values. Set the intended collection mode on the parent before creating scalar-bound content: initial scalar layout resolves that inherited mode. This does not guarantee automatic scalar layout recomputation after a later mode switch. Verify resulting geometry as well as paint when changing modes. Missing or incorrectly typed scalar variables are errors.
- `bind` maps supported scene-field paths to variable IDs or references when no shorthand exists. Use semantic tokens consistently rather than declaring unused collections.
- A reusable JavaScript function shares source code, not component identity. Use `Component`, `ComponentSet`, and `Instance` for editable main components and linked instances.
- `Instance` resolves an existing component through `of`, `component`, or `componentId`. Component-set children named `variant=Primary`, for example, define variants that can be selected when instantiating the set.
@ -37,6 +37,7 @@ This reference describes scene creation, not React DOM output. Use the `render`
- Child `propertyRefs` connect fields to stable property IDs, for example `[{ propertyId: 'message', field: 'TEXT' }]`. Supported fields are `TEXT`, `VISIBLE`, and `INSTANCE_SWAP`; text and swap references require text and instance nodes respectively. References do not depend on layer names.
- Instance assignments use the native string values (including `'true'` / `'false'` for BOOLEAN properties and component IDs for swaps). For example `Instance({ of: noteId, properties: { message: 'Updated review' } })`. Assignments persist through component synchronization; unknown IDs and invalid values fail rather than silently creating inert overrides. Select variants through component-set variant props, not through instance property assignments.
- Reuse existing local or library components before recreating them. Keep meaningful text, visibility, and swap properties exposed rather than hand-editing cloned child nodes.
- Distinguish a main component's default size from its instance's placement constraints. Verify the actual instance bounds in narrower parents; a requested Fill dimension alone is not proof that inherited sizing changed. Do not compensate for a sizing mismatch with guessed heights, clipping, or manually positioned siblings.
## Verification
@ -57,7 +58,7 @@ The examples below are executed by the authoring-reference tests. Create the nam
```jsx
<Frame name="Bound note" w={280} h="hug" flex="col" gap={designVar('Space/small')} p={designVar('Space/medium')} bg="#FFFFFF">
<Text name="Message" w="fill" size={designVar('Type/body')} color="#252A31">A note that grows with its content.</Text>
<Text name="Message" w="fill" size={designVar('Type/body')} lineHeight={designVar('Type/body-leading')} letterSpacing={designVar('Type/body-tracking')} color="#252A31">A note that grows with its content.</Text>
</Frame>
```

View file

@ -12,31 +12,41 @@ import SYSTEM_PROMPT from '@/app/ai/chat/system-prompt'
import { getNodeOrThrow } from '#tests/helpers/assert'
import { makeSceneGraph } from '#tests/helpers/scene'
function exampleGraph() {
const graph = makeSceneGraph()
graph.addCollection({
id: 'tokens',
name: 'Tokens',
modes: [
{ modeId: 'default', name: 'Default' },
{ modeId: 'compact', name: 'Compact' }
],
defaultModeId: 'default',
variableIds: []
})
for (const [name, value, compact] of [
['Space/small', 8, 4],
['Space/medium', 16, 12],
['Type/body', 12, 11],
['Type/body-leading', 20, 16],
['Type/body-tracking', 0.2, 0]
] as const) {
graph.addVariable({
id: name,
name,
type: 'FLOAT',
collectionId: 'tokens',
valuesByMode: { default: value, compact },
description: '',
hiddenFromPublishing: false
})
}
return graph
}
for (const example of AUTHORING_EXAMPLES) {
test(`shared authoring example: ${example.title}`, async () => {
const graph = makeSceneGraph()
graph.addCollection({
id: 'tokens',
name: 'Tokens',
modes: [{ modeId: 'default', name: 'Default' }],
defaultModeId: 'default',
variableIds: []
})
for (const [name, value] of [
['Space/small', 8],
['Space/medium', 16],
['Type/body', 12]
] as const) {
graph.addVariable({
id: name,
name,
type: 'FLOAT',
collectionId: 'tokens',
valuesByMode: { default: value },
description: '',
hiddenFromPublishing: false
})
}
const graph = exampleGraph()
const [result] = await renderJSX(graph, example.jsx)
const frame = getNodeOrThrow(graph, result.id)
expect(frame.width).toBe(280)
@ -47,6 +57,36 @@ for (const example of AUTHORING_EXAMPLES) {
})
}
test('shared bound example retains bindings and resolves the initial inherited mode', async () => {
const graph = exampleGraph()
const parent = graph.createNode('FRAME', graph.getPages()[0].id, {
variableModes: { tokens: 'compact' }
})
for (const example of AUTHORING_EXAMPLES) {
await renderJSX(graph, example.jsx, { parentId: parent.id })
}
const bound = graph.getChildren(parent.id).find((node) => node.boundVariables.itemSpacing)
if (!bound) throw new Error('Expected a variable-bound authoring example')
expect(bound.itemSpacing).toBe(4)
expect(bound.paddingTop).toBe(12)
expect(bound.boundVariables).toMatchObject({
itemSpacing: 'Space/small',
paddingTop: 'Space/medium',
paddingRight: 'Space/medium',
paddingBottom: 'Space/medium',
paddingLeft: 'Space/medium'
})
const [text] = graph.getChildren(bound.id)
expect(text.fontSize).toBe(11)
expect(text.lineHeight).toBe(16)
expect(text.letterSpacing).toBe(0)
expect(text.boundVariables).toMatchObject({
fontSize: 'Type/body',
lineHeight: 'Type/body-leading',
letterSpacing: 'Type/body-tracking'
})
})
test('public exports and runtime prompts use the same authoring reference', () => {
expect(BARREL_REFERENCE).toBe(JSX_REFERENCE)
expect(BARREL_CODEGEN).toBe(CODEGEN_PROMPT)