@smallpen/cli 0.1.0-alpha.5 → 0.1.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/command-contract.mjs +1416 -0
- package/bin/command-tree.mjs +512 -0
- package/bin/design-output.mjs +1039 -0
- package/bin/help-topics.mjs +241 -0
- package/bin/help.mjs +379 -478
- package/bin/read-output.mjs +219 -0
- package/bin/result-files.mjs +232 -0
- package/bin/reuse-advice.mjs +73 -0
- package/bin/schema.mjs +607 -152
- package/bin/smallpen.mjs +4185 -1330
- package/bin/warning-output.mjs +2 -2
- package/bin/windows-artifacts.mjs +72 -0
- package/bin/workflow.mjs +55 -0
- package/package.json +3 -3
package/bin/schema.mjs
CHANGED
|
@@ -6,8 +6,6 @@ import {
|
|
|
6
6
|
AXIS_ROLES,
|
|
7
7
|
CANONICAL_SCHEMA_RULES,
|
|
8
8
|
CONTEXT_AXIS_KINDS,
|
|
9
|
-
INITIALIZATION_QUESTION_IDS,
|
|
10
|
-
initializationQuestions,
|
|
11
9
|
INSTANCE_OVERRIDE_FIELDS,
|
|
12
10
|
OPERATION_SCHEMAS,
|
|
13
11
|
SMALLPEN_FORMAT_CAPABILITIES,
|
|
@@ -15,19 +13,39 @@ import {
|
|
|
15
13
|
suggestField,
|
|
16
14
|
TOKEN_BINDING_FIELD_TYPES,
|
|
17
15
|
TOKEN_VALUE_SHAPES,
|
|
16
|
+
THEME_INTENT_ACTIONS,
|
|
17
|
+
TOKEN_INTENT_ACTIONS,
|
|
18
|
+
TOKEN_SET_FIELDS,
|
|
19
|
+
COMPONENT_EDIT_ACTIONS,
|
|
20
|
+
ASSET_KINDS,
|
|
21
|
+
ASSET_EDIT_FIELDS,
|
|
22
|
+
INTERACTION_INTENT_ACTIONS,
|
|
23
|
+
PROTOTYPE_EVENTS,
|
|
24
|
+
PROTOTYPE_ACTIONS,
|
|
25
|
+
VARIANT_NODE_TYPES,
|
|
18
26
|
} from "@smallpen/core";
|
|
27
|
+
import { COMMAND_CONTRACTS, commandContract } from "./command-contract.mjs";
|
|
28
|
+
import { CLI_RULES } from "./workflow.mjs";
|
|
29
|
+
import { helpTopic, nextQuery } from "./help-topics.mjs";
|
|
30
|
+
import { pageItems } from "./read-output.mjs";
|
|
31
|
+
import {
|
|
32
|
+
COMMAND_GROUPS,
|
|
33
|
+
PUBLIC_COMMANDS,
|
|
34
|
+
groupDefinition,
|
|
35
|
+
actionContract,
|
|
36
|
+
} from "./command-tree.mjs";
|
|
19
37
|
|
|
20
38
|
const FORMAT = SMALLPEN_FORMAT_CAPABILITIES;
|
|
21
39
|
const OPERATION_TYPES = FORMAT.canonicalWrite.operationTypes;
|
|
22
|
-
|
|
23
|
-
const
|
|
40
|
+
// The default `smallpen init` layout: one self-contained Package.
|
|
41
|
+
const PACKAGE = "acme/acme.smallpen";
|
|
24
42
|
const REVISION = "<package.revision from smallpen inspect PACKAGE --json>";
|
|
25
43
|
|
|
26
44
|
export const PACKAGE_ROLES = Object.freeze({
|
|
27
45
|
foundation:
|
|
28
|
-
"
|
|
46
|
+
"Token sets and themes, Context Axes (density, viewport, ...) and Component Sets. In the default layout (one self-contained Package) write them to that Package; in the foundation-product layout write them to the Foundation (<name>-foundation.smallpen).",
|
|
29
47
|
product:
|
|
30
|
-
"Screens, Presentations, Scenarios, requirements
|
|
48
|
+
"Screens, Presentations, Scenarios, requirements. In the default layout they live in the same self-contained Package; in the foundation-product layout they live in the Product, which resolves the Foundation's Tokens, themes and components through its dependency. view, validate, export, token list and search take the Package with the Screens.",
|
|
31
49
|
});
|
|
32
50
|
|
|
33
51
|
function batchFor(operations, batchId) {
|
|
@@ -43,7 +61,7 @@ export const INSTANCE_NODE_EXAMPLE = Object.freeze({
|
|
|
43
61
|
height: 48,
|
|
44
62
|
id: "node_add_button",
|
|
45
63
|
instance: {
|
|
46
|
-
component: { assetId: "cmp_button", packageId: "
|
|
64
|
+
component: { assetId: "cmp_button", packageId: "pkg_acme" },
|
|
47
65
|
variant: { axis_style: "primary" },
|
|
48
66
|
},
|
|
49
67
|
name: "Add button",
|
|
@@ -57,18 +75,18 @@ export const INIT_ANSWERS_EXAMPLE = Object.freeze({
|
|
|
57
75
|
audience: "Small teams",
|
|
58
76
|
contextAxes: [
|
|
59
77
|
{
|
|
60
|
-
defaultValue: "
|
|
78
|
+
defaultValue: "Light",
|
|
61
79
|
id: "axis_theme",
|
|
62
80
|
kind: "theme",
|
|
63
81
|
name: "Theme",
|
|
64
|
-
values: ["
|
|
82
|
+
values: ["Light", "Dark"],
|
|
65
83
|
},
|
|
66
84
|
],
|
|
67
85
|
firstJourney: "Add a task",
|
|
68
86
|
firstOutput: "Task list screen",
|
|
69
87
|
firstScenario: "Default",
|
|
70
88
|
firstScreen: "Home",
|
|
71
|
-
foundationChoice: "
|
|
89
|
+
foundationChoice: "self-contained",
|
|
72
90
|
initialComponents: ["Button"],
|
|
73
91
|
initialTokens: ["color.brand", "spacing.md", "radius.md"],
|
|
74
92
|
kindDetails: ["responsive"],
|
|
@@ -80,78 +98,107 @@ export const INIT_ANSWERS_EXAMPLE = Object.freeze({
|
|
|
80
98
|
});
|
|
81
99
|
|
|
82
100
|
const TYPOGRAPHY_TOKEN = {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
101
|
+
setId: "tset_base",
|
|
102
|
+
token: {
|
|
103
|
+
id: "tok_type_heading",
|
|
104
|
+
name: "type.heading",
|
|
105
|
+
type: "typography",
|
|
106
|
+
value: { fontFamily: "Source Sans Pro", fontSize: 24, fontWeight: 700 },
|
|
87
107
|
},
|
|
88
|
-
|
|
89
|
-
path: "type.heading",
|
|
90
|
-
tokenId: "tok_type_heading",
|
|
91
|
-
type: "put-token",
|
|
108
|
+
type: "put-set-token",
|
|
92
109
|
};
|
|
93
110
|
|
|
94
|
-
//
|
|
95
|
-
// Each step runs in a test against a fresh `smallpen init` workspace.
|
|
111
|
+
// A generic, executable atomic-write example. Scene design order lives in Skills.
|
|
96
112
|
export const WORKED_EXAMPLE = Object.freeze([
|
|
97
113
|
{
|
|
98
|
-
title: "Create
|
|
99
|
-
argv: ["
|
|
100
|
-
files: {
|
|
101
|
-
note: "
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
title: "Add Tokens to the Foundation (token intents need no revision)",
|
|
105
|
-
argv: ["token", FOUNDATION, "--intent", "tokens.json", "--json"],
|
|
106
|
-
files: { "tokens.json": { operations: [OPS["put-token"].example, TYPOGRAPHY_TOKEN] } },
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
title: "Add a Button Component Set with a configuration axis",
|
|
110
|
-
argv: ["apply", FOUNDATION, "--batch", "button.json", "--json"],
|
|
111
|
-
files: {
|
|
112
|
-
"button.json": batchFor([OPS["put-component-set"].example], "button-v1"),
|
|
113
|
-
},
|
|
114
|
-
note: "This replaces the starter cmp_button that init seeded (put-component-set with an existing id replaces it whole). baseRevision: run smallpen inspect acme/acme-foundation.smallpen --json and copy package.revision.",
|
|
114
|
+
title: "Create a blank package",
|
|
115
|
+
argv: ["project", "init", PACKAGE, "--name", "Acme", "--json"],
|
|
116
|
+
files: {},
|
|
117
|
+
note: "The reply returns the package revision and initial canvas IDs. No business brief is needed.",
|
|
115
118
|
},
|
|
116
119
|
{
|
|
117
|
-
title: "
|
|
118
|
-
argv: ["
|
|
120
|
+
title: "Create one canvas through an atomic batch",
|
|
121
|
+
argv: ["advanced", "apply", PACKAGE, "--batch", "canvas.json", "--json"],
|
|
119
122
|
files: {
|
|
120
|
-
"
|
|
121
|
-
|
|
123
|
+
"canvas.json": batchFor(
|
|
124
|
+
[
|
|
122
125
|
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
type: "put-screen",
|
|
127
|
+
screen: {
|
|
128
|
+
id: "scr_demo",
|
|
129
|
+
name: "Canvas",
|
|
130
|
+
basePresentationId: "pres_demo_base",
|
|
131
|
+
counterparts: [],
|
|
132
|
+
presentations: [
|
|
133
|
+
{
|
|
134
|
+
id: "pres_demo_base",
|
|
135
|
+
name: "Base",
|
|
136
|
+
rootId: "node_demo_root",
|
|
137
|
+
interactions: [],
|
|
138
|
+
viewport: { width: 600, height: 400 },
|
|
139
|
+
nodes: {
|
|
140
|
+
node_demo_root: {
|
|
141
|
+
id: "node_demo_root",
|
|
142
|
+
name: "Canvas",
|
|
143
|
+
type: "FRAME",
|
|
144
|
+
children: [],
|
|
145
|
+
x: 0,
|
|
146
|
+
y: 0,
|
|
147
|
+
width: 600,
|
|
148
|
+
height: 400,
|
|
149
|
+
fills: [{ type: "solid", color: "#ffffff" }],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
],
|
|
126
154
|
},
|
|
127
155
|
},
|
|
128
|
-
INSTANCE_NODE_EXAMPLE,
|
|
129
156
|
],
|
|
130
|
-
|
|
131
|
-
|
|
157
|
+
"canvas-v1",
|
|
158
|
+
),
|
|
132
159
|
},
|
|
133
|
-
note: "
|
|
160
|
+
note: "Copy the revision from the previous reply (or inspect PACKAGE) into baseRevision before applying the batch. Existing IDs replace whole definitions; use update for partial edits.",
|
|
134
161
|
},
|
|
135
162
|
{
|
|
136
|
-
title: "
|
|
137
|
-
argv: ["apply",
|
|
163
|
+
title: "Add text to that canvas",
|
|
164
|
+
argv: ["advanced", "apply", PACKAGE, "--batch", "text.json", "--json"],
|
|
138
165
|
files: {
|
|
139
|
-
"
|
|
166
|
+
"text.json": batchFor(
|
|
140
167
|
[
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
168
|
+
{
|
|
169
|
+
type: "add-presentation-node",
|
|
170
|
+
screenId: "scr_demo",
|
|
171
|
+
presentationId: "pres_demo_base",
|
|
172
|
+
parentId: "node_demo_root",
|
|
173
|
+
node: {
|
|
174
|
+
id: "node_title",
|
|
175
|
+
type: "TEXT",
|
|
176
|
+
name: "Title",
|
|
177
|
+
text: "Example",
|
|
178
|
+
children: [],
|
|
179
|
+
x: 16,
|
|
180
|
+
y: 16,
|
|
181
|
+
width: 300,
|
|
182
|
+
height: 32,
|
|
183
|
+
fills: [{ type: "solid", color: "#111111" }],
|
|
184
|
+
textStyle: {
|
|
185
|
+
fontFamily: "Source Sans Pro",
|
|
186
|
+
fontSize: 18,
|
|
187
|
+
fontWeight: 400,
|
|
188
|
+
lineHeight: 1.3,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
144
192
|
],
|
|
145
|
-
"
|
|
193
|
+
"text-v1",
|
|
146
194
|
),
|
|
147
195
|
},
|
|
148
|
-
note: "
|
|
196
|
+
note: "Use the revision the previous write returned. Day-to-day pages are drawn by name with page draw; apply is for exact operations such as an undo batch.",
|
|
149
197
|
},
|
|
150
198
|
{
|
|
151
|
-
title: "
|
|
152
|
-
argv: ["
|
|
199
|
+
title: "Read the selected canvas as a text wireframe",
|
|
200
|
+
argv: ["view", PACKAGE, "--page", "Canvas", "--as", "wireframe", "--json"],
|
|
153
201
|
files: {},
|
|
154
|
-
note: "Then: smallpen evidence acme/acme.smallpen --output review --json",
|
|
155
202
|
},
|
|
156
203
|
]);
|
|
157
204
|
|
|
@@ -190,7 +237,9 @@ const EXISTING_ID_NOTES = {
|
|
|
190
237
|
"put-component-set":
|
|
191
238
|
"An existing componentSet.id is replaced whole (no merge). init seeds one Component Set per initialComponents answer, for example cmp_button with variant var_button_default and no axes; the init reply lists them in seeded.componentSets.",
|
|
192
239
|
"put-token":
|
|
193
|
-
"An existing tokenId is replaced whole (no merge). init seeds one Token per initialTokens answer, for example tok_color_brand, tok_spacing_md, tok_radius_md; the init reply lists them in seeded.tokens.",
|
|
240
|
+
"An existing tokenId is replaced whole (no merge). init seeds one Token per initialTokens answer, for example tok_color_brand, tok_spacing_md, tok_radius_md (in the token set tset_base: change them with put-set-token or set-token-value); the init reply lists them in seeded.tokens.",
|
|
241
|
+
"put-set-token":
|
|
242
|
+
"An existing token id in the set is replaced whole (no merge). init seeds one Token per initialTokens answer in tset_base, for example tok_color_brand, tok_spacing_md, tok_radius_md; the init reply lists them in seeded.tokens.",
|
|
194
243
|
};
|
|
195
244
|
|
|
196
245
|
function existingIdNote(type, schema) {
|
|
@@ -206,12 +255,19 @@ function operationDetail(type) {
|
|
|
206
255
|
return {
|
|
207
256
|
...(existingId ? { existingId } : {}),
|
|
208
257
|
fields: {
|
|
209
|
-
type: {
|
|
258
|
+
type: {
|
|
259
|
+
description: "Operation type",
|
|
260
|
+
required: true,
|
|
261
|
+
type: "string",
|
|
262
|
+
value: type,
|
|
263
|
+
},
|
|
210
264
|
...schema.fields,
|
|
211
265
|
},
|
|
212
266
|
inverse: schema.inverse,
|
|
213
267
|
package: schema.package,
|
|
214
|
-
packageRole:
|
|
268
|
+
packageRole:
|
|
269
|
+
PACKAGE_ROLES[schema.package] ??
|
|
270
|
+
"Either Package; it changes the Package you pass",
|
|
215
271
|
purpose: schema.purpose,
|
|
216
272
|
...(schema.oneOf ? { requiresOneOf: schema.oneOf } : {}),
|
|
217
273
|
unknownFields: "rejected with unknown_operation_field",
|
|
@@ -225,16 +281,19 @@ const NODE_FIELD_TYPES = {
|
|
|
225
281
|
backgroundBlur: "object or array of objects",
|
|
226
282
|
blur: "object or array of objects",
|
|
227
283
|
"blend-mode": "string",
|
|
228
|
-
constraints: "constraints-h
|
|
284
|
+
constraints: `constraints-h: ${CANONICAL_SCHEMA_RULES.constraints["constraints-h"].join(" | ")}; constraints-v: ${CANONICAL_SCHEMA_RULES.constraints["constraints-v"].join(" | ")} (how a child follows its parent when the parent is resized in the editor). When a copy is resized: left keeps x; right moves x by the growth; leftright grows width by it; center moves x by half of it; scale scales x and width by new/old size (top, bottom, topbottom alike). Without a constraint, a child of a FRAME, COMPONENT or INSTANCE stays left/top; other children scale. A flex layout places its children unless they are layout-item-absolute`,
|
|
229
285
|
content: "Penpot path content",
|
|
230
286
|
cornerRadius: "number or [topLeft, topRight, bottomRight, bottomLeft]",
|
|
231
|
-
fills:
|
|
287
|
+
fills:
|
|
288
|
+
'[{type:"solid", color:"#rrggbb", opacity?}] (also linear-gradient, radial-gradient, image)',
|
|
232
289
|
flipX: "boolean",
|
|
233
290
|
flipY: "boolean",
|
|
234
291
|
grids: "array of objects",
|
|
235
292
|
growType: `TEXT only: ${CANONICAL_SCHEMA_RULES.textGrowTypes.join(" | ")}`,
|
|
236
|
-
interactions:
|
|
237
|
-
|
|
293
|
+
interactions:
|
|
294
|
+
"array of native App configurations; set them by name with flow link and flow unlink. Canonical put-interaction is separate.",
|
|
295
|
+
layout:
|
|
296
|
+
'flex/grid layout: "flex" | "grid" plus layout-* fields (strings; layout-gap/layout-padding/layout-item-margin are objects)',
|
|
238
297
|
locked: "boolean",
|
|
239
298
|
mediaRef: "IMAGE only: media_... id or {packageId, assetId}",
|
|
240
299
|
name: "non-empty string",
|
|
@@ -243,12 +302,15 @@ const NODE_FIELD_TYPES = {
|
|
|
243
302
|
points: "PATH only: array of points",
|
|
244
303
|
proportionLock: "boolean",
|
|
245
304
|
rotation: "number, 0 <= r < 360",
|
|
246
|
-
shadow:
|
|
247
|
-
|
|
305
|
+
shadow:
|
|
306
|
+
'{offsetX, offsetY, blur, spread, color, opacity?, inset?, hidden?} or an array of them; px numbers, for example {"offsetX":0,"offsetY":2,"blur":8,"spread":0,"color":"#00000033"}. Bind a shadow Token with tokenBindings.shadow instead to follow themes',
|
|
307
|
+
strokes: `[{type:"solid", color, width, alignment?:${CANONICAL_SCHEMA_RULES.strokeAlignments.join("|")}, style?:${CANONICAL_SCHEMA_RULES.strokeStyles.join("|")}, dash?, gap?, capStart?, capEnd?}]. alignment defaults to inner; an open path always strokes centered. Dash patterns follow Penpot, for stroke width w: dashed = w+10 on, w+10 off (dash/gap override them); dotted = round dots w across, w+5 apart; mixed = w+5 on, w+5 off, w+1 on, w+5 off. capStart/capEnd (PATH ends): ${CANONICAL_SCHEMA_RULES.strokeCaps.join(" | ")}; cap sizes grow with w: triangle-arrow tip 1.42w past the end and 8.5w wide, circle-marker radius 2w, square-marker side 4.24w, diamond-marker corners 3w from the end`,
|
|
248
308
|
text: "TEXT only: plain text (string)",
|
|
249
|
-
textBlocks:
|
|
309
|
+
textBlocks:
|
|
310
|
+
"TEXT only: [{runs:[{text, textStyle?, fills?}], textStyle?}] that join to text",
|
|
250
311
|
textStyle: "TEXT only: see textStyle below",
|
|
251
|
-
tokenBindings:
|
|
312
|
+
tokenBindings:
|
|
313
|
+
"{field: {packageId, assetId:tok_...}}; see tokenBindings below",
|
|
252
314
|
touched: "array of Penpot touched groups",
|
|
253
315
|
visible: "boolean",
|
|
254
316
|
};
|
|
@@ -269,11 +331,12 @@ function nodeTopic() {
|
|
|
269
331
|
summary:
|
|
270
332
|
"A node is one layer. A Presentation stores nodes flat in nodes:{id: node}; children lists child ids. x and y are relative to the parent node.",
|
|
271
333
|
required: {
|
|
272
|
-
children:
|
|
334
|
+
children:
|
|
335
|
+
"array of child node ids (ids only; add each child as its own node)",
|
|
273
336
|
height: "number",
|
|
274
|
-
id: "node_
|
|
337
|
+
id: "must start with node_ (node_[A-Za-z0-9_-]+), unique in its Presentation (or variant); the nodes key equals id",
|
|
275
338
|
name: "non-empty string",
|
|
276
|
-
type: CANONICAL_SCHEMA_RULES.nodeTypes.join(" | "),
|
|
339
|
+
type: `${CANONICAL_SCHEMA_RULES.nodeTypes.join(" | ")}. Inside a Component variant only ${VARIANT_NODE_TYPES.join(", ")} (no ELLIPSE, GROUP or PATH)`,
|
|
277
340
|
width: "number",
|
|
278
341
|
x: "number, relative to the parent",
|
|
279
342
|
y: "number, relative to the parent",
|
|
@@ -283,28 +346,112 @@ function nodeTopic() {
|
|
|
283
346
|
CANONICAL_SCHEMA_RULES.nodeChangeFields.map((field) => [
|
|
284
347
|
field,
|
|
285
348
|
NODE_FIELD_TYPES[field] ??
|
|
286
|
-
(field.startsWith("constraints")
|
|
287
|
-
|
|
288
|
-
|
|
349
|
+
(field.startsWith("constraints")
|
|
350
|
+
? NODE_FIELD_TYPES.constraints
|
|
351
|
+
: field.startsWith("layout")
|
|
352
|
+
? "layout field; see layout"
|
|
353
|
+
: field === "height" ||
|
|
354
|
+
field === "width" ||
|
|
355
|
+
field === "x" ||
|
|
356
|
+
field === "y"
|
|
357
|
+
? "number"
|
|
289
358
|
: "see validator"),
|
|
290
359
|
]),
|
|
291
360
|
),
|
|
361
|
+
layout: {
|
|
362
|
+
values: {
|
|
363
|
+
layout: ["flex", "grid"],
|
|
364
|
+
"layout-flex-dir": ["row", "row-reverse", "column", "column-reverse"],
|
|
365
|
+
"layout-wrap-type": ["nowrap", "wrap"],
|
|
366
|
+
"layout-justify-content": [
|
|
367
|
+
"start",
|
|
368
|
+
"center",
|
|
369
|
+
"end",
|
|
370
|
+
"space-between",
|
|
371
|
+
"space-around",
|
|
372
|
+
"space-evenly",
|
|
373
|
+
"stretch",
|
|
374
|
+
],
|
|
375
|
+
"layout-align-items": ["start", "end", "center", "stretch"],
|
|
376
|
+
"layout-item-h-sizing": ["fill", "fix", "auto"],
|
|
377
|
+
"layout-item-v-sizing": ["fill", "fix", "auto"],
|
|
378
|
+
},
|
|
379
|
+
objects: {
|
|
380
|
+
"layout-gap": '{"row-gap":12,"column-gap":12}; numeric pixels',
|
|
381
|
+
"layout-padding":
|
|
382
|
+
'{"p1":16,"p2":16,"p3":16,"p4":16}; top, right, bottom, left',
|
|
383
|
+
"layout-item-margin":
|
|
384
|
+
'{"m1":0,"m2":0,"m3":0,"m4":0}; top, right, bottom, left',
|
|
385
|
+
},
|
|
386
|
+
childOrder:
|
|
387
|
+
"children uses Penpot layer order. row/column positions the last child first; row-reverse/column-reverse positions the first child first. For a row displaying A then B, store children:[B,A].",
|
|
388
|
+
sizing:
|
|
389
|
+
"Set layout-item-h-sizing on a child for horizontal size and layout-item-v-sizing for vertical size. fill shares the remaining main-axis space after fixed children, gap and padding; on the cross axis it fills the inner parent size. fix uses stored width/height. auto currently uses stored width/height too; supply measured text bounds. layout-item-absolute:true excludes a child from flex flow.",
|
|
390
|
+
projection:
|
|
391
|
+
"These are Penpot layout values, not enforced enum constraints. SmallPen reflows one flex line with gap, padding, direction, fill, start/center/end alignment and start/center/end/space-between justification. Grid, wrapping, margins, auto/min/max sizing, stretch and other modes report layout_projection_partial in view/check/PNG and skipped layout reflow in coverage; stored bounds are not App-equivalent layout validation.",
|
|
392
|
+
example: {
|
|
393
|
+
rootId: "node_layout_root",
|
|
394
|
+
nodes: {
|
|
395
|
+
node_layout_root: {
|
|
396
|
+
id: "node_layout_root",
|
|
397
|
+
name: "Row",
|
|
398
|
+
type: "FRAME",
|
|
399
|
+
x: 0,
|
|
400
|
+
y: 0,
|
|
401
|
+
width: 320,
|
|
402
|
+
height: 80,
|
|
403
|
+
children: ["node_fill", "node_fixed"],
|
|
404
|
+
layout: "flex",
|
|
405
|
+
"layout-flex-dir": "row",
|
|
406
|
+
"layout-gap": { "row-gap": 12, "column-gap": 12 },
|
|
407
|
+
"layout-padding": { p1: 16, p2: 16, p3: 16, p4: 16 },
|
|
408
|
+
},
|
|
409
|
+
node_fixed: {
|
|
410
|
+
id: "node_fixed",
|
|
411
|
+
name: "First, fixed",
|
|
412
|
+
type: "RECTANGLE",
|
|
413
|
+
x: 0,
|
|
414
|
+
y: 0,
|
|
415
|
+
width: 80,
|
|
416
|
+
height: 48,
|
|
417
|
+
children: [],
|
|
418
|
+
"layout-item-h-sizing": "fix",
|
|
419
|
+
},
|
|
420
|
+
node_fill: {
|
|
421
|
+
id: "node_fill",
|
|
422
|
+
name: "Second, fill",
|
|
423
|
+
type: "RECTANGLE",
|
|
424
|
+
x: 0,
|
|
425
|
+
y: 0,
|
|
426
|
+
width: 1,
|
|
427
|
+
height: 1,
|
|
428
|
+
children: [],
|
|
429
|
+
"layout-item-h-sizing": "fill",
|
|
430
|
+
"layout-item-v-sizing": "fill",
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
},
|
|
292
435
|
textStyle: {
|
|
293
436
|
fields: CANONICAL_SCHEMA_RULES.textStyleFields,
|
|
294
|
-
numbers:
|
|
437
|
+
numbers:
|
|
438
|
+
"fontSize (px), fontWeight (100-950), lineHeight, letterSpacing (px) are numbers",
|
|
439
|
+
lineHeight:
|
|
440
|
+
"A multiplier of fontSize: 1.4 means 140% (Penpot's line height). Not px: the editor reads 24 as 24 times the font size. The same holds for lineHeight in a typography Token. Default 1.2.",
|
|
441
|
+
fonts:
|
|
442
|
+
"Only Source Sans Pro (Latin) is bundled. Any other fontFamily, including one for Chinese or another script, must be imported into the package first with smallpen asset font import; otherwise render falls back to Source Sans Pro, reports font_render_fallback, and text it cannot draw is a text_missing_glyphs issue.",
|
|
295
443
|
values: CANONICAL_SCHEMA_RULES.textStyleEnums,
|
|
296
444
|
note: "Text alignment is textStyle.textAlign, not a node field.",
|
|
297
445
|
},
|
|
298
446
|
tokenBindings: {
|
|
299
|
-
shape: '{"fill": {"packageId":"
|
|
447
|
+
shape: '{"fill": {"packageId":"pkg_acme","assetId":"tok_color_brand"}}',
|
|
300
448
|
fieldTokenTypes: tokenBindingTable(),
|
|
301
449
|
notBindable:
|
|
302
|
-
"x, y, rotation, and layout-* fields
|
|
303
|
-
note:
|
|
304
|
-
"A binding replaces the raw value when the design resolves. sizing Tokens on width/height change the rendered size. paddingTop/Right/Bottom/Left and itemSpacing accept spacing Tokens but only record intent: layout reads layout-padding and layout-gap, which take numbers.",
|
|
450
|
+
"x, y, rotation, and layout-* fields cannot bind Tokens directly. Use itemSpacing, rowGap/columnGap, or paddingTop/Right/Bottom/Left to bind layout spacing.",
|
|
451
|
+
note: "A binding replaces the raw value when the design resolves. width/height drive size. itemSpacing drives both layout-gap axes; rowGap/columnGap override one axis. paddingTop/Right/Bottom/Left drive layout-padding p1/p2/p3/p4. Unbound sides keep their raw numbers; bound spacing is at least zero.",
|
|
305
452
|
},
|
|
306
453
|
changes:
|
|
307
|
-
"update-presentation-node, update-node, and update-component-node changes take the fields above; null clears an optional field;
|
|
454
|
+
"update-presentation-node, update-node, and update-component-node changes take only the mutable fields above; null clears an optional field. id, type and children cannot be changed here. For Presentation child order use reorder-presentation-children; for membership use add/move/remove-presentation-node. For a component's elements prefer component define, which redraws the variants by name.",
|
|
308
455
|
example: NODE_EXAMPLE,
|
|
309
456
|
};
|
|
310
457
|
}
|
|
@@ -312,57 +459,99 @@ function nodeTopic() {
|
|
|
312
459
|
function nodeTypesTopic() {
|
|
313
460
|
return {
|
|
314
461
|
types: {
|
|
315
|
-
COMPONENT:
|
|
462
|
+
COMPONENT:
|
|
463
|
+
"Root of a Component variant, or a located Component main node",
|
|
316
464
|
ELLIPSE: "Ellipse inside x/y/width/height",
|
|
317
465
|
FRAME: "Container; fills, strokes, cornerRadius, layout",
|
|
318
466
|
GROUP: "Plain group of children",
|
|
319
|
-
IMAGE: "Requires mediaRef (import with smallpen import
|
|
467
|
+
IMAGE: "Requires mediaRef (import with smallpen asset media import)",
|
|
320
468
|
INSTANCE: "Requires instance; see: smallpen schema instance",
|
|
321
469
|
PATH: "pathData or points; strokes with capStart/capEnd for arrows",
|
|
322
470
|
RECTANGLE: "Rectangle; fills, strokes, cornerRadius",
|
|
323
471
|
TEXT: "Requires text; textStyle, textBlocks, growType",
|
|
324
472
|
},
|
|
325
473
|
presentationTypes: CANONICAL_SCHEMA_RULES.nodeTypes,
|
|
326
|
-
variantTypes: "
|
|
474
|
+
variantTypes: VARIANT_NODE_TYPES.join(", "),
|
|
475
|
+
variantNote:
|
|
476
|
+
"ELLIPSE, GROUP and PATH are allowed only in Screen Presentations, not in Component variants: use a RECTANGLE with cornerRadius for a circle, a FRAME to group.",
|
|
327
477
|
};
|
|
328
478
|
}
|
|
329
479
|
|
|
330
480
|
function tokenTopic() {
|
|
331
481
|
return {
|
|
332
482
|
summary:
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
'[{"when":{"axis_theme":"dark"},"value":"#1c1b1f"}]: value per Context (exactly when + value)',
|
|
342
|
-
visibility: "public | private",
|
|
343
|
-
deprecated: "boolean",
|
|
344
|
-
},
|
|
483
|
+
'Tokens live in token sets (Penpot / Tokens Studio / DTCG $themes model). A Token is {id:tok_..., name:"color.surface", type, value}. Themes switch which sets are active; see: smallpen schema theme.',
|
|
484
|
+
setToken: {
|
|
485
|
+
id: "tok_... permanent id; bindings reference it",
|
|
486
|
+
name: "dot path, for example color.surface; a later active set overrides an earlier one with the same name. A name is either a Token or a group, never both: with color.brand present, color.brand.hover is rejected (token_name_collision); use color.brand-hover, or name them color.brand.default and color.brand.hover from the start",
|
|
487
|
+
type: FORMAT.canonicalPackage.tokenTypes.join(" | "),
|
|
488
|
+
value:
|
|
489
|
+
'value matching type; see: smallpen schema token-types. An alias is "{other.token.name}"',
|
|
490
|
+
description: "optional string",
|
|
345
491
|
},
|
|
346
492
|
operations: {
|
|
347
|
-
|
|
493
|
+
appCreate: TOKEN_INTENT_ACTIONS["create-row"].example,
|
|
494
|
+
appChangeValue: TOKEN_INTENT_ACTIONS["set-value"].example,
|
|
495
|
+
create: "put-set-token {setId, token}",
|
|
348
496
|
changeValue: "set-token-value {tokenId, value}",
|
|
349
497
|
bind: "set-token-binding {screenId, nodeId, field, binding:{packageId, assetId}}, or tokenBindings on the node",
|
|
350
|
-
remove: "
|
|
498
|
+
remove: "remove-token (fails while bound; see smallpen impact)",
|
|
499
|
+
sets: "put-token-set, delete-token-set; themes: put-token-theme, delete-token-theme, set-active-token-themes",
|
|
351
500
|
},
|
|
501
|
+
dtcgFiles:
|
|
502
|
+
"put-token {filePath, path, tokenId, definition:{$type, $value, $extensions:{smallpen:{id}}}} writes a DTCG token file. Prefer put-set-token: init makes no DTCG file. filePath names a DTCG file (created when missing, for example tokens/dtcg.json); it must not be the token library entry with sets and themes (tokens/foundation.json or tokens/tokens.json after init; smallpen tokens PACKAGE --json shows each Token's filePath). Its Tokens are always active and do not take part in themes; $extensions.smallpen.contextValues still resolve with --context but are deprecated for themes (warning context_values_deprecated).",
|
|
352
503
|
tokenBindings: tokenBindingTable(),
|
|
353
|
-
themes:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
504
|
+
themes: "smallpen schema theme",
|
|
505
|
+
read: "smallpen tokens <package> --theme Theme/Dark --json lists resolved values and the themes (themes[].active)",
|
|
506
|
+
definitions:
|
|
507
|
+
"smallpen tokens <package> --definitions [--group GROUP] --json lists exact stored Token objects in all option Sets, including packageId/setId/group/option. Missing cells have no item and do not inherit Default. --theme/--context cannot be combined with --definitions.",
|
|
508
|
+
authoring:
|
|
509
|
+
"token set PACKAGE --intent FILE writes Tokens by name with default and per-option values; token delete --path removes one. See schema token-set.",
|
|
510
|
+
example: OPS["put-set-token"].example,
|
|
357
511
|
typographyExample: TYPOGRAPHY_TOKEN,
|
|
358
|
-
intent: { operations: [OPS["put-token"].example] },
|
|
512
|
+
intent: { operations: [OPS["put-set-token"].example] },
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function themeTopic() {
|
|
517
|
+
return {
|
|
518
|
+
summary:
|
|
519
|
+
"A theme is Penpot's token theme: group/name plus the token sets it activates. One theme per group is active. A Token resolves to the last active set that defines its name, so a theme set overrides the base set by name. This is the model Penpot's token manager, Tokens Studio and DTCG $themes share.",
|
|
520
|
+
model: {
|
|
521
|
+
set: "{id:tset_..., name, description?, tokens:[{id:tok_..., name, type, value}]}",
|
|
522
|
+
theme: "{id:theme_..., group, name, setIds:[tset_...], description?}",
|
|
523
|
+
selection:
|
|
524
|
+
"activeThemeIds/activeSetIds = stored App selection; defaultThemeIds = optional independent project defaults, one per group. Missing defaults use Default, else first library option. With no themes, CLI uses defaultSetIds, or all library sets in order for legacy data",
|
|
525
|
+
},
|
|
526
|
+
initLayout:
|
|
527
|
+
"init turns every theme-kind contextAxes answer into one group: set base (active in every theme) plus one native Group/Option Set per value, preserving group/option labels, and a theme per value activating [base, that value's set]. Example: Theme/Light = [base, Theme/Light], Theme/Dark = [base, Theme/Dark]. Stable IDs still use slugs. Existing packages keep their original Set paths.",
|
|
528
|
+
recipe: [
|
|
529
|
+
'put-set-token {setId:"tset_base", token:{id:"tok_color_surface", name:"color.surface", type:"color", value:"#ffffff"}}',
|
|
530
|
+
'put-set-token {setId:"tset_theme_dark", token:{id:"tok_color_surface_dark", name:"color.surface", type:"color", value:"#1c1b1f"}}',
|
|
531
|
+
"bind nodes to tok_color_surface (the base id); Theme/Dark shows #1c1b1f",
|
|
532
|
+
'smallpen view PACKAGE --theme Theme/Dark --json (read only); set-active-token-themes {themePaths:["Theme/Dark"]} stores it',
|
|
533
|
+
],
|
|
534
|
+
read: "token theme list, view, validate and export use project defaults, not App active themes; --theme GROUP/NAME overrides only named groups. None of these reads write. A Presentation alone does not choose themes. Use token theme list PACKAGE to discover project-defined groups/options/defaults; platform, language and light/dark are optional examples, not required dimensions.",
|
|
535
|
+
foundationProduct:
|
|
536
|
+
"In the foundation-product layout the Foundation owns the sets and themes. The Product stores which Foundation themes it uses in manifest dependencies[0].activeThemeIds (Foundation theme ids); set-active-token-themes on the Product writes it. The Product's own active sets sit on top of the Foundation's by name.",
|
|
537
|
+
operations: {
|
|
538
|
+
createSet: OPS["put-token-set"].example,
|
|
539
|
+
addToken: OPS["put-set-token"].example,
|
|
540
|
+
createTheme: OPS["put-token-theme"].example,
|
|
541
|
+
activate: OPS["set-active-token-themes"].example,
|
|
542
|
+
},
|
|
543
|
+
contexts:
|
|
544
|
+
"Context Axes (smallpen schema context) are for non-token choices such as viewport or platform. Token contextValues on theme axes are deprecated; smallpen migrate-themes PACKAGE --output NEW converts them to sets and themes.",
|
|
359
545
|
};
|
|
360
546
|
}
|
|
361
547
|
|
|
362
548
|
function tokenTypesTopic() {
|
|
363
549
|
return {
|
|
364
550
|
types: Object.fromEntries(
|
|
365
|
-
FORMAT.canonicalPackage.tokenTypes.map((type) => [
|
|
551
|
+
FORMAT.canonicalPackage.tokenTypes.map((type) => [
|
|
552
|
+
type,
|
|
553
|
+
TOKEN_VALUE_SHAPES[type],
|
|
554
|
+
]),
|
|
366
555
|
),
|
|
367
556
|
alias: 'Any type also accepts "{path.of.another.token}" of the same type.',
|
|
368
557
|
};
|
|
@@ -372,11 +561,13 @@ function componentSetTopic() {
|
|
|
372
561
|
return {
|
|
373
562
|
summary:
|
|
374
563
|
"A Component Set is one component with variants. Axes name the choices; each variant selects one value per Axis and holds its own node tree.",
|
|
564
|
+
variantNodeTypes: `${VARIANT_NODE_TYPES.join(", ")} only. ELLIPSE, GROUP and PATH are rejected in variants (they work in Screen Presentations): use a RECTANGLE with cornerRadius for a circle, a FRAME to group, an IMAGE (asset media import) for an icon.`,
|
|
375
565
|
fields: {
|
|
376
566
|
id: "cmp_...",
|
|
377
567
|
name: "non-empty string",
|
|
378
568
|
axes: "[{id:axis_..., name, role, domain?}]",
|
|
379
|
-
variants:
|
|
569
|
+
variants:
|
|
570
|
+
"[{id:var_..., selection:{axisId: value}, rootId, nodes:{id: node}}]",
|
|
380
571
|
visibility: "public | private (default public)",
|
|
381
572
|
category: "optional string",
|
|
382
573
|
description: "optional string",
|
|
@@ -387,7 +578,8 @@ function componentSetTopic() {
|
|
|
387
578
|
values: AXIS_ROLES,
|
|
388
579
|
configuration:
|
|
389
580
|
"An option the designer picks: style (primary/secondary), size, content, icon. domain is optional but recommended.",
|
|
390
|
-
state:
|
|
581
|
+
state:
|
|
582
|
+
"An interaction state: default, hover, pressed, disabled. domain is required.",
|
|
391
583
|
modelling:
|
|
392
584
|
"Model primary/secondary as one configuration Axis (axis_style, domain [primary, secondary]) and hover/disabled as a separate state Axis. Every variant must select every Axis, and each selection must be unique.",
|
|
393
585
|
},
|
|
@@ -395,9 +587,11 @@ function componentSetTopic() {
|
|
|
395
587
|
"Variant node x/y are relative to the parent; the root is usually COMPONENT at 0,0. Node ids may repeat across variants so Instance overrides keep working.",
|
|
396
588
|
operations: {
|
|
397
589
|
create: "put-component-set (whole set) or put-variant (one variant)",
|
|
398
|
-
edit: "update-component-node {
|
|
590
|
+
edit: "update-component-node {componentId, variantId, nodeId, changes}",
|
|
399
591
|
remove: "delete-variant, delete-component-set",
|
|
400
592
|
},
|
|
593
|
+
instancesAfterChanges:
|
|
594
|
+
"A write that leaves an Instance in the same Package selecting a variant that no longer exists (deleted variant, changed Axes) is rejected with stale_instance_variant. Product Instances of a Foundation set are checked by smallpen validate PRODUCT and listed in staleInstances; fix them with select-instance-variant.",
|
|
401
595
|
package: "foundation",
|
|
402
596
|
example: OPS["put-component-set"].example.componentSet,
|
|
403
597
|
};
|
|
@@ -410,20 +604,22 @@ function instanceTopic() {
|
|
|
410
604
|
node: {
|
|
411
605
|
type: "INSTANCE",
|
|
412
606
|
instance: {
|
|
413
|
-
component: "{packageId: owning Package (
|
|
607
|
+
component: "{packageId: owning Package (pkg_acme), assetId: cmp_...}",
|
|
414
608
|
variant: "{axisId: value} for every Axis of the Component Set",
|
|
415
|
-
overrides:
|
|
609
|
+
overrides:
|
|
610
|
+
'{"<sourceNodeId>:<field>": value}; prefer set-instance-override',
|
|
416
611
|
},
|
|
417
612
|
size: "x/y/width/height of the Instance itself",
|
|
418
613
|
},
|
|
419
614
|
overrides: {
|
|
420
615
|
path: "<sourceNodeId>:<field>, or <nestedInstanceId>__<sourceNodeId>:<field> inside a nested Instance",
|
|
421
616
|
fields: [...INSTANCE_OVERRIDE_FIELDS],
|
|
422
|
-
operations:
|
|
617
|
+
operations:
|
|
618
|
+
"set-instance-override, clear-instance-override, select-instance-variant",
|
|
423
619
|
},
|
|
424
620
|
pageIntent:
|
|
425
|
-
'In
|
|
426
|
-
find: "smallpen search
|
|
621
|
+
'In page draw an element {"use":"Button","props":{"Style":"secondary"},"set":{"Label.text":"Add task"}} places a copy; each setting becomes a checked instance override.',
|
|
622
|
+
find: "smallpen component search <product> --query button --json returns recommendedInsertion.element, ready to add to a page draw intent",
|
|
427
623
|
example: {
|
|
428
624
|
node: INSTANCE_NODE_EXAMPLE,
|
|
429
625
|
parentId: "node_home_root",
|
|
@@ -442,7 +638,8 @@ function presentationTopic() {
|
|
|
442
638
|
id: "pres_...",
|
|
443
639
|
name: "non-empty string",
|
|
444
640
|
platform: "optional string",
|
|
445
|
-
viewport:
|
|
641
|
+
viewport:
|
|
642
|
+
"{width, height}: the declared device size (metadata; render ignores it)",
|
|
446
643
|
nodes: "{node_id: node}",
|
|
447
644
|
rootId: "root node id (or rootIds for several roots)",
|
|
448
645
|
interactions: "[] (see put-interaction)",
|
|
@@ -467,7 +664,8 @@ function screenTopic() {
|
|
|
467
664
|
presentations: "[Presentation]; see: smallpen schema presentation",
|
|
468
665
|
counterparts: "[] (links between Presentations)",
|
|
469
666
|
},
|
|
470
|
-
defaultScreen:
|
|
667
|
+
defaultScreen:
|
|
668
|
+
"set-default-screen chooses the page reads and renders use when no page is named",
|
|
471
669
|
example: OPS["put-screen"].example.screen,
|
|
472
670
|
};
|
|
473
671
|
}
|
|
@@ -475,18 +673,24 @@ function screenTopic() {
|
|
|
475
673
|
function scenarioTopic() {
|
|
476
674
|
return {
|
|
477
675
|
summary:
|
|
478
|
-
"A Scenario is a reproducible design state: a target Screen/Presentation, a Context, and declarative actions.
|
|
676
|
+
"A Scenario is a reproducible design state: a target Screen/Presentation, a Context, optional token themes, and declarative actions.",
|
|
479
677
|
fields: {
|
|
480
678
|
id: "scn_...",
|
|
481
679
|
name: "non-empty string",
|
|
482
|
-
target:
|
|
680
|
+
target:
|
|
681
|
+
'{kind:"screen", screen:{packageId, assetId:scr_...}, presentationId}',
|
|
483
682
|
context: "{axisId: value} for the Context Axes",
|
|
683
|
+
themes:
|
|
684
|
+
'optional ["Group/Name"] token themes the Scenario shows; --theme overrides them',
|
|
484
685
|
actions: "[] or declarative actions (set-override, ...)",
|
|
485
|
-
expectedVisibleNodeIds:
|
|
686
|
+
expectedVisibleNodeIds:
|
|
687
|
+
"Exact set of visible canonical target Node IDs after actions, excluding nodes hidden by ancestors. Instance internals are generated IDs and must not be listed. Use read --full for editable IDs; validate reports the actual canonical visibleNodeIds on mismatch.",
|
|
486
688
|
fixture: "{key: scalar}",
|
|
487
|
-
viewport:
|
|
689
|
+
viewport:
|
|
690
|
+
"{width, height, scale}: declared size (metadata; render uses the root node size)",
|
|
488
691
|
},
|
|
489
|
-
update:
|
|
692
|
+
update:
|
|
693
|
+
"put-scenario with the same id replaces the Scenario, for example to keep its viewport in step with the root node.",
|
|
490
694
|
example: OPS["put-scenario"].example.scenario,
|
|
491
695
|
};
|
|
492
696
|
}
|
|
@@ -494,7 +698,7 @@ function scenarioTopic() {
|
|
|
494
698
|
function contextTopic() {
|
|
495
699
|
return {
|
|
496
700
|
summary:
|
|
497
|
-
"Context Axes are finite design dimensions such as
|
|
701
|
+
"Context Axes are finite design dimensions such as viewport, density or locale. The Package (or the Foundation of a pair) declares them in contexts/*.json; select values with --context AXIS=VALUE. Themes are token themes instead (smallpen schema theme).",
|
|
498
702
|
axis: {
|
|
499
703
|
id: "axis_...",
|
|
500
704
|
name: "non-empty string",
|
|
@@ -504,8 +708,8 @@ function contextTopic() {
|
|
|
504
708
|
},
|
|
505
709
|
profile: "{id:ctx_..., name, values:{axisId: value}, default?: boolean}",
|
|
506
710
|
themes:
|
|
507
|
-
"
|
|
508
|
-
init: "
|
|
711
|
+
"Themes are token sets + themes, not Context Axes: see smallpen schema theme. A theme-kind init answer becomes token themes.",
|
|
712
|
+
init: "project init creates no Context Axes. Use explicit operations to add project-defined axes when needed.",
|
|
509
713
|
example: OPS["put-context-file"].example.contextFile,
|
|
510
714
|
};
|
|
511
715
|
}
|
|
@@ -515,19 +719,22 @@ function batchTopic() {
|
|
|
515
719
|
summary:
|
|
516
720
|
"Every write is one atomic Operation Batch. Nothing is written unless every operation is valid.",
|
|
517
721
|
batch: {
|
|
518
|
-
baseRevision:
|
|
722
|
+
baseRevision:
|
|
723
|
+
"package.revision from smallpen inspect PACKAGE --json; a stale value is rejected",
|
|
519
724
|
batchId: "1-192 characters [A-Za-z0-9_.-], new for every write",
|
|
520
725
|
operations: "array; see: smallpen schema operations",
|
|
521
726
|
},
|
|
522
727
|
packages: PACKAGE_ROLES,
|
|
523
728
|
commands: {
|
|
524
|
-
apply:
|
|
525
|
-
|
|
729
|
+
apply:
|
|
730
|
+
"smallpen apply PACKAGE --batch BATCH.json [--dry-run] [--explain] [--diff]",
|
|
731
|
+
token:
|
|
732
|
+
"smallpen token PACKAGE --intent {operations:[...]}: like apply, with baseRevision and batchId filled in",
|
|
526
733
|
page: "smallpen page PRODUCT --intent {screenId, presentationId?, parentId?, index?, nodes:[node, ...]}: one add-presentation-node per node, all siblings under parentId (default: the root); overrides on an INSTANCE node become set-instance-override operations",
|
|
527
734
|
flow: "smallpen flow PRODUCT --intent: same as page, for flowchart nodes",
|
|
528
735
|
},
|
|
529
736
|
result:
|
|
530
|
-
"revision, changedFiles, affectedIds,
|
|
737
|
+
"batchId, revision, changedFiles, affectedIds, changed (false for no-op or replay), alreadyApplied, dryRun and summary. changeReportPath records exact before/after values. reverseEdit/inverseBatchPath describe an explicit new apply write with its own identity and a fixed baseRevision, not history rollback. After later edits, read current fields and construct a new scoped edit; do not force an old snapshot by changing its baseRevision. --inverse-out FILE chooses a durable file. --full includes exact details. Reuse batchId only for identical retries; ledger retains at most 500 identities.",
|
|
531
738
|
dryRun: "--dry-run validates and returns the result without writing",
|
|
532
739
|
workedExample: WORKED_EXAMPLE,
|
|
533
740
|
};
|
|
@@ -536,23 +743,199 @@ function batchTopic() {
|
|
|
536
743
|
function initTopic() {
|
|
537
744
|
return {
|
|
538
745
|
summary:
|
|
539
|
-
"
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
746
|
+
"Create a blank package with one canvas and Theme/Default. No business brief, question state, starter Tokens, components or platform copies.",
|
|
747
|
+
command:
|
|
748
|
+
"smallpen project init PATH [--name NAME] [--layout single|foundation-product] --json",
|
|
749
|
+
path: "A .smallpen path creates that package directly; a directory creates PATH/BASENAME.smallpen inside a new workspace; --name changes the display name, not the filename.",
|
|
750
|
+
layouts: {
|
|
751
|
+
single: "Default: one self-contained package.",
|
|
752
|
+
"foundation-product":
|
|
753
|
+
"A new directory containing a blank Foundation and a Product that resolves its Tokens and components.",
|
|
754
|
+
},
|
|
755
|
+
existing: "Reject an existing target without changing its contents.",
|
|
756
|
+
nextOperations: [
|
|
757
|
+
nextQuery(["schema", "command", "project", "init", "--json"]),
|
|
758
|
+
],
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// page set and component set take the same fields.
|
|
763
|
+
function setTopic(command) {
|
|
764
|
+
return {
|
|
765
|
+
command,
|
|
766
|
+
fields: {
|
|
767
|
+
set: "FIELD=VALUE, repeatable; VALUE is JSON (320, [8,16], {\"Style\":\"ghost\"}, true) or plain text; {token.path} binds a Token",
|
|
768
|
+
element: "fields: width, height (number, fill, hug), fill, stroke, radius, opacity, shadow, visible, rotation, margin, min/max sizes; texts: text, color, font, fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, textTransform, textDecoration, textAlign; containers: layout (row|column|none), gap, padding, align, justify",
|
|
769
|
+
copy: "a copy of a component: props, text {\"Element\": \"text\"}, set {\"Element.field\": value}, width, height (numbers), visible, opacity",
|
|
770
|
+
itself: "without --element: the page version (or the variant) itself, for example its width and height",
|
|
771
|
+
},
|
|
772
|
+
example: [
|
|
773
|
+
"page set app.smallpen --page Board --element Card --set width=320 --set fill={color.surface}",
|
|
774
|
+
"page set app.smallpen --page Board --element \"Top bar / Title\" --set text={text.board.title} --set fontSize=24",
|
|
775
|
+
"page set app.smallpen --page Board --element Save --set props={\"Style\":\"ghost\"}",
|
|
776
|
+
"page set app.smallpen --page Board --platform mobile --set width=390 --set height=900",
|
|
777
|
+
"component set app.smallpen --component Button --element Label --all --set fontWeight=600",
|
|
778
|
+
],
|
|
779
|
+
notes: [
|
|
780
|
+
"Only the named fields change; everything else, links included, stays. Texts are measured again and containers that hug grow or shrink, as page draw does.",
|
|
781
|
+
"Versions and variants often differ on purpose: an element (or the page or component itself) in several of them needs --platform (--variant) to name one, or --all to change every one; the refusal lists where it is.",
|
|
782
|
+
"A value written over a Token binding replaces the binding.",
|
|
783
|
+
],
|
|
544
784
|
};
|
|
545
785
|
}
|
|
546
786
|
|
|
547
787
|
const TOPICS = {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
788
|
+
workflow: {
|
|
789
|
+
build: () => CLI_RULES,
|
|
790
|
+
summary: "CLI operation rules (compatibility alias for help rules)",
|
|
791
|
+
},
|
|
792
|
+
"component-define": {
|
|
793
|
+
build: () => ({
|
|
794
|
+
command: "component define PACKAGE --intent FILE --json",
|
|
795
|
+
fields: {
|
|
796
|
+
name: "component name; defining it again replaces it and keeps its id",
|
|
797
|
+
properties: "{Property: [values]}; every combination becomes a variant",
|
|
798
|
+
base: "element tree (see element) shared by all variants",
|
|
799
|
+
variants: '[{when: {Property: value}, set: {"Child.prop": value}}]: changes applied to matching variants, in order',
|
|
800
|
+
},
|
|
801
|
+
element: {
|
|
802
|
+
name: "element name; children are found by name in set paths and instance text",
|
|
803
|
+
text: "makes a text element; color, font ({typography token}), fontFamily (a family imported with asset font import, or {font-family token}), fontSize, fontWeight, lineHeight, textAlign",
|
|
804
|
+
children: "child elements; a container lays them out as a column unless layout says row or none",
|
|
805
|
+
layout: "row | column | none; in none the CLI places children so they do not overlap: give place (\"below Header\", {rightOf: \"Sidebar\", gap: 24}) or nothing, x/y only to pin one",
|
|
806
|
+
"gap / padding": "number or {token}; padding takes 1, 2 or 4 values",
|
|
807
|
+
"align / justify": "start | center | end (align also stretch; justify also space-between)",
|
|
808
|
+
"width / height": 'number, "fill" (or "100%") or "hug"; the only text in a row fills by default, several texts in a row hug',
|
|
809
|
+
"fill / stroke / radius / opacity / shadow": 'value or {token}; stroke is a color or {color, width, style}, width one value or four sides [top, right, bottom, left]; radius is one value or four corners [top-left, top-right, bottom-right, bottom-left]',
|
|
810
|
+
margin: "number or {token}; 1, 2 or 4 values, like padding",
|
|
811
|
+
"minWidth / maxWidth / minHeight / maxHeight": "number or {token}",
|
|
812
|
+
"letterSpacing / lineHeight / textTransform / textDecoration": "text only; value or {token} (textTransform: uppercase|lowercase|capitalize|none, textDecoration: underline|line-through|none)",
|
|
813
|
+
rotation: "degrees or {token}",
|
|
814
|
+
visible: "false hides it (for example per platform in a variant), or {flag} for a boolean Token",
|
|
815
|
+
use: 'component name for an instance, with props {Property: value}, text {"Element": "text"} and set {"Element.field": value}; field is text, visible, opacity, fill, color, name, stroke, radius, width, height, shadow, fontSize, fontWeight, fontFamily, letterSpacing, lineHeight, or props (switch a nested component: {"Icon.props": {"Size": "lg"}}); no element name sets the copy itself; a {token} value is the copy\'s own binding',
|
|
816
|
+
},
|
|
817
|
+
example: {
|
|
818
|
+
name: "Button",
|
|
819
|
+
properties: { Style: ["primary", "secondary"], Size: ["sm", "md"] },
|
|
820
|
+
base: { layout: "row", padding: [8, "{space.4}"], fill: "{color.brand}", radius: "{radius.md}", align: "center",
|
|
821
|
+
children: [{ name: "Label", text: "Button", color: "#ffffff", font: "{type.label}" }] },
|
|
822
|
+
variants: [
|
|
823
|
+
{ when: { Style: "secondary" }, set: { fill: "#ffffff", stroke: "{color.brand}", "Label.color": "{color.brand}" } },
|
|
824
|
+
{ when: { Size: "md" }, set: { padding: [12, 24] } },
|
|
825
|
+
],
|
|
826
|
+
},
|
|
827
|
+
notes: [
|
|
828
|
+
"The CLI creates node ids, child order, layout fields and Token bindings; text is measured with the renderer's fonts and hugging elements are sized from it.",
|
|
829
|
+
"A value in braces binds that Token by path; any other value is literal. view marks literal visual values so you can bind them later.",
|
|
830
|
+
"text: \"{text.path}\" binds a string Token and visible: \"{flag.path}\" a boolean one, so text follows the language option and an element shows or hides per option; instance text: {Label: \"{text.path}\"} works too.",
|
|
831
|
+
],
|
|
832
|
+
}),
|
|
833
|
+
summary: "Define a component by name: base elements, properties and variant changes",
|
|
834
|
+
},
|
|
835
|
+
"page-draw": {
|
|
836
|
+
build: () => ({
|
|
837
|
+
command: "page draw PACKAGE --intent FILE --json",
|
|
838
|
+
fields: {
|
|
839
|
+
page: "page name; a new page is created",
|
|
840
|
+
module: "business module; pages of one module share the name prefix Module / Page and one row block on the canvas",
|
|
841
|
+
canvas: "optional canvas name; by default every page is on one canvas (Penpot links and prototype flows only reach boards on the same canvas); give a name only to split pages off",
|
|
842
|
+
platform: "desktop (default), mobile or tablet: the page version to draw; size defaults to the platform's",
|
|
843
|
+
into: "redraw only this container's content",
|
|
844
|
+
children: "element tree (schema component-define, element); instances use components by name",
|
|
845
|
+
},
|
|
846
|
+
example: {
|
|
847
|
+
page: "Board", module: "Tasks", platform: "desktop", layout: "row", fill: "{color.bg.canvas}",
|
|
848
|
+
children: [
|
|
849
|
+
{ name: "Sidebar", width: 240, height: "fill", children: [{ name: "Logo", text: "Flowboard" }] },
|
|
850
|
+
{ name: "Content", width: "fill", height: "fill", padding: 24, gap: 16, children: [
|
|
851
|
+
{ name: "Top bar", layout: "row", width: "fill", align: "center", children: [
|
|
852
|
+
{ name: "Title", text: "Sprint 24", width: "fill" },
|
|
853
|
+
{ name: "New task", use: "Button", props: { Style: "primary" }, text: { Label: "New task" } },
|
|
854
|
+
] },
|
|
855
|
+
] },
|
|
856
|
+
],
|
|
857
|
+
},
|
|
858
|
+
notes: [
|
|
859
|
+
"Drawing a page again redraws it; elements drawn again under the same names keep their ids and interactions.",
|
|
860
|
+
"An instance whose new text is longer than its slot grows by the measured difference.",
|
|
861
|
+
"Link elements with flow link --from \"Page / Element\" --to Page.",
|
|
862
|
+
"Move an element later with page move --page Page --element Name --direction up|down|left|right [--steps N]; the CLI picks the coordinates.",
|
|
863
|
+
"The CLI lays the canvas out: each module (Tasks / ... pages) is a row block, one line per platform, pages left to right from the start page along links, more room between modules. Reorder a page with page move --page Page --direction left|right; see it with canvas list or view --canvas NAME.",
|
|
864
|
+
"Strings are Tokens: bind text with \"{text.path}\" (string Token) and visibility with \"{flag.path}\" (boolean Token); check a language with view --theme Language/<option>, for example Language/Chinese.",
|
|
865
|
+
],
|
|
866
|
+
}),
|
|
867
|
+
summary: "Draw a page or one container from named elements and component instances",
|
|
868
|
+
},
|
|
869
|
+
"page-set": {
|
|
870
|
+
build: () => setTopic("page set PACKAGE --page NAME [--platform P] [--element NAME] --set FIELD=VALUE ... --json"),
|
|
871
|
+
summary: "Change some fields of one element (or of a page itself) without drawing it again",
|
|
872
|
+
},
|
|
873
|
+
"component-fields": {
|
|
874
|
+
build: () => setTopic("component set PACKAGE --component NAME [--variant V] [--element NAME] --set FIELD=VALUE ... --json"),
|
|
875
|
+
summary: "Change some fields of one element of a component (or of the component itself) in its variants without defining it again",
|
|
876
|
+
},
|
|
877
|
+
|
|
878
|
+
"token-set": {
|
|
879
|
+
build: () => ({
|
|
880
|
+
command: "token set PACKAGE --intent FILE --json",
|
|
881
|
+
fields: { tokens: `[{${TOKEN_SET_FIELDS.join(", ")}}]` },
|
|
882
|
+
example: {
|
|
883
|
+
tokens: [
|
|
884
|
+
{ name: "color.brand", type: "color", value: "#4f46e5", values: { "Theme/Dark": "#818cf8" } },
|
|
885
|
+
{ name: "space.4", type: "spacing", value: 16 },
|
|
886
|
+
{ name: "color.action", type: "color", value: "{color.brand}" },
|
|
887
|
+
],
|
|
888
|
+
},
|
|
889
|
+
notes: [
|
|
890
|
+
"Name Tokens by path and theme options as Group/Option. Where values are stored is the CLI's business.",
|
|
891
|
+
"value is the Token's value wherever no option overrides it; values override named options. Each call changes only what it names.",
|
|
892
|
+
"group (optional) names the theme group whose options a new Token should vary by. type is required for a new Token.",
|
|
893
|
+
"Write an alias as a value in braces, for example {color.brand}. Read results with token list: each row lists values that differ under other options.",
|
|
894
|
+
"Strings and flags are Tokens too: type string (with a Language group, values per language) and boolean. token export --type string --by Language writes one JSON file per language.",
|
|
895
|
+
],
|
|
896
|
+
}),
|
|
897
|
+
summary: "Set Tokens by name: default and per-option values in one call",
|
|
898
|
+
},
|
|
899
|
+
command: {
|
|
900
|
+
summary:
|
|
901
|
+
"One command's parameters, types, defaults and constraints (smallpen schema command NAME)",
|
|
902
|
+
},
|
|
903
|
+
commands: {
|
|
904
|
+
build: () => ({
|
|
905
|
+
commands: PUBLIC_COMMANDS.map(({ command: name, purpose }) => ({
|
|
906
|
+
name,
|
|
907
|
+
purpose,
|
|
908
|
+
})),
|
|
909
|
+
next: "smallpen schema command NAME --json",
|
|
910
|
+
}),
|
|
911
|
+
summary: "Command names and purposes; fetch one parameter schema at a time",
|
|
912
|
+
},
|
|
913
|
+
batch: {
|
|
914
|
+
build: batchTopic,
|
|
915
|
+
summary: "Batch contract, Package roles, and a worked example",
|
|
916
|
+
},
|
|
917
|
+
"component-set": {
|
|
918
|
+
build: componentSetTopic,
|
|
919
|
+
summary: "Component Set, Axis roles, variants",
|
|
920
|
+
},
|
|
921
|
+
context: {
|
|
922
|
+
build: contextTopic,
|
|
923
|
+
summary: "Context Axes and profiles (viewport, density, locale, ...)",
|
|
924
|
+
},
|
|
925
|
+
init: {
|
|
926
|
+
build: initTopic,
|
|
927
|
+
summary: "Blank package paths, layouts and creation behavior",
|
|
928
|
+
},
|
|
552
929
|
instance: { build: instanceTopic, summary: "INSTANCE nodes and overrides" },
|
|
553
930
|
node: { build: nodeTopic, summary: "Node fields, textStyle, tokenBindings" },
|
|
554
|
-
"node-types": {
|
|
555
|
-
|
|
931
|
+
"node-types": {
|
|
932
|
+
build: nodeTypesTopic,
|
|
933
|
+
summary: "What each node type requires",
|
|
934
|
+
},
|
|
935
|
+
operation: {
|
|
936
|
+
summary:
|
|
937
|
+
"One operation: fields, types, example, inverse (smallpen schema operation TYPE)",
|
|
938
|
+
},
|
|
556
939
|
operations: {
|
|
557
940
|
build: () => ({
|
|
558
941
|
operations: OPERATION_TYPES.map(operationSummary),
|
|
@@ -560,18 +943,35 @@ const TOPICS = {
|
|
|
560
943
|
}),
|
|
561
944
|
summary: "Every operation type with its purpose and required fields",
|
|
562
945
|
},
|
|
563
|
-
presentation: {
|
|
946
|
+
presentation: {
|
|
947
|
+
build: presentationTopic,
|
|
948
|
+
summary: "Presentation fields, size, and resizing",
|
|
949
|
+
},
|
|
564
950
|
scenario: { build: scenarioTopic, summary: "Scenario fields and viewport" },
|
|
565
951
|
screen: { build: screenTopic, summary: "Screen fields" },
|
|
566
|
-
|
|
567
|
-
|
|
952
|
+
theme: {
|
|
953
|
+
build: themeTopic,
|
|
954
|
+
summary: "Token sets and themes, --theme, Foundation + Product themes",
|
|
955
|
+
},
|
|
956
|
+
token: {
|
|
957
|
+
build: tokenTopic,
|
|
958
|
+
summary: "Tokens in token sets, DTCG files, bindings",
|
|
959
|
+
},
|
|
960
|
+
"token-types": {
|
|
961
|
+
build: tokenTypesTopic,
|
|
962
|
+
summary: "The value each Token type accepts",
|
|
963
|
+
},
|
|
568
964
|
};
|
|
569
|
-
const TOPIC_ALIASES = { theme: "context" };
|
|
570
965
|
|
|
571
966
|
export const SCHEMA_TOPICS = Object.freeze(Object.keys(TOPICS));
|
|
572
967
|
|
|
573
|
-
|
|
574
|
-
|
|
968
|
+
|
|
969
|
+
export function schemaTopic(
|
|
970
|
+
name,
|
|
971
|
+
argument,
|
|
972
|
+
{ full = true, limit, offset = 0, action } = {},
|
|
973
|
+
) {
|
|
974
|
+
if (name === undefined) {
|
|
575
975
|
return {
|
|
576
976
|
topics: Object.fromEntries(
|
|
577
977
|
Object.entries(TOPICS).map(([name, { summary }]) => [name, summary]),
|
|
@@ -579,10 +979,18 @@ export function schemaTopic(topicName, argument) {
|
|
|
579
979
|
usage: "smallpen schema TOPIC [--json]; smallpen schema operation TYPE",
|
|
580
980
|
};
|
|
581
981
|
}
|
|
582
|
-
if (
|
|
583
|
-
|
|
982
|
+
if (name === "command") {
|
|
983
|
+
if (Object.hasOwn(COMMAND_GROUPS, argument)) {
|
|
984
|
+
if (groupDefinition(argument, action)) return { topic: name, ...helpTopic(argument, action) };
|
|
985
|
+
return { topic: name, ...actionContract(argument, action) };
|
|
986
|
+
}
|
|
987
|
+
if (action !== undefined)
|
|
988
|
+
throw new SmallPenError(
|
|
989
|
+
"unexpected_argument",
|
|
990
|
+
`Unexpected command action: ${action}`,
|
|
991
|
+
);
|
|
992
|
+
return { topic: name, ...commandContract(argument) };
|
|
584
993
|
}
|
|
585
|
-
const name = TOPIC_ALIASES[topicName] ?? topicName;
|
|
586
994
|
if (name === "operation") {
|
|
587
995
|
if (!Object.hasOwn(OPS, argument ?? "")) {
|
|
588
996
|
const suggestion = suggestField(argument, OPERATION_TYPES);
|
|
@@ -590,20 +998,29 @@ export function schemaTopic(topicName, argument) {
|
|
|
590
998
|
"unknown_schema_operation",
|
|
591
999
|
`${argument === undefined ? "smallpen schema operation requires TYPE" : `Unknown operation type: ${argument}`}.` +
|
|
592
1000
|
`${suggestion ? ` Did you mean ${suggestion}?` : ""} Types: ${OPERATION_TYPES.join(", ")}`,
|
|
593
|
-
{
|
|
1001
|
+
{
|
|
1002
|
+
validTypes: [...OPERATION_TYPES],
|
|
1003
|
+
...(suggestion ? { suggestion } : {}),
|
|
1004
|
+
},
|
|
594
1005
|
);
|
|
595
1006
|
}
|
|
596
|
-
|
|
1007
|
+
const detail = operationDetail(argument);
|
|
1008
|
+
return { topic: "operation", type: argument, ...detail };
|
|
597
1009
|
}
|
|
598
1010
|
if (!Object.hasOwn(TOPICS, name)) {
|
|
599
1011
|
const suggestion = suggestField(name, Object.keys(TOPICS));
|
|
600
1012
|
throw new SmallPenError(
|
|
601
1013
|
"unknown_schema_topic",
|
|
602
|
-
`Unknown schema topic: ${
|
|
1014
|
+
`Unknown schema topic: ${name}.${suggestion ? ` Did you mean ${suggestion}?` : ""} ` +
|
|
603
1015
|
`Topics: ${Object.keys(TOPICS).join(", ")}`,
|
|
604
|
-
{
|
|
1016
|
+
{
|
|
1017
|
+
validTopics: Object.keys(TOPICS),
|
|
1018
|
+
...(suggestion ? { suggestion } : {}),
|
|
1019
|
+
},
|
|
605
1020
|
);
|
|
606
1021
|
}
|
|
1022
|
+
if (name === "workflow") return helpTopic("workflow", argument, { full });
|
|
1023
|
+
const value = TOPICS[name].build();
|
|
607
1024
|
if (argument !== undefined) {
|
|
608
1025
|
throw new SmallPenError(
|
|
609
1026
|
"unexpected_argument",
|
|
@@ -611,5 +1028,43 @@ export function schemaTopic(topicName, argument) {
|
|
|
611
1028
|
{ argument },
|
|
612
1029
|
);
|
|
613
1030
|
}
|
|
614
|
-
|
|
1031
|
+
if (["commands", "operations"].includes(name)) {
|
|
1032
|
+
const items = value[name].map((item) => {
|
|
1033
|
+
if (full || name === "commands") return item;
|
|
1034
|
+
const { required: _required, requiresOneOf: _oneOf, ...summary } = item;
|
|
1035
|
+
return summary;
|
|
1036
|
+
});
|
|
1037
|
+
const page = pageItems(items, {
|
|
1038
|
+
limit: limit ?? (full ? items.length : 20),
|
|
1039
|
+
offset,
|
|
1040
|
+
});
|
|
1041
|
+
return {
|
|
1042
|
+
topic: name,
|
|
1043
|
+
[name]: page.items,
|
|
1044
|
+
page: page.page,
|
|
1045
|
+
nextOperations: [
|
|
1046
|
+
...(page.page.hasMore
|
|
1047
|
+
? [
|
|
1048
|
+
nextQuery([
|
|
1049
|
+
"schema",
|
|
1050
|
+
name,
|
|
1051
|
+
"--limit",
|
|
1052
|
+
String(page.page.limit),
|
|
1053
|
+
"--offset",
|
|
1054
|
+
String(offset + page.page.limit),
|
|
1055
|
+
...(full ? ["--full"] : []),
|
|
1056
|
+
"--json",
|
|
1057
|
+
]),
|
|
1058
|
+
]
|
|
1059
|
+
: []),
|
|
1060
|
+
nextQuery([
|
|
1061
|
+
"schema",
|
|
1062
|
+
name === "commands" ? "command" : "operation",
|
|
1063
|
+
name === "commands" ? "<name>" : "<type>",
|
|
1064
|
+
"--json",
|
|
1065
|
+
]),
|
|
1066
|
+
],
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
return { topic: name, ...value };
|
|
615
1070
|
}
|