@smallpen/core 0.1.0-alpha.1
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/package.json +18 -0
- package/src/canonical.mjs +64 -0
- package/src/capabilities.mjs +495 -0
- package/src/catalog.mjs +362 -0
- package/src/component-samples.mjs +84 -0
- package/src/components-domain.mjs +335 -0
- package/src/contexts.mjs +248 -0
- package/src/design-projection.mjs +657 -0
- package/src/design-read.mjs +825 -0
- package/src/design-system-authoring.mjs +102 -0
- package/src/design-system-canvas.mjs +862 -0
- package/src/design-system.mjs +437 -0
- package/src/design-validation.mjs +324 -0
- package/src/draft.mjs +784 -0
- package/src/effective-tokens.mjs +377 -0
- package/src/errors.mjs +12 -0
- package/src/index.mjs +112 -0
- package/src/initialization.mjs +310 -0
- package/src/package.mjs +5935 -0
- package/src/projection-values.mjs +138 -0
- package/src/requirements-domain.mjs +222 -0
- package/src/scenarios-domain.mjs +268 -0
- package/src/token-advice.mjs +272 -0
- package/src/token-import.mjs +607 -0
- package/src/tokens-domain.mjs +410 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
// DSP-003: Workbench Combination enumeration, pure preview evaluation and
|
|
2
|
+
// explicit edit-target resolution for the design system workbench.
|
|
3
|
+
//
|
|
4
|
+
// Terminology (CONTEXT.md): a Token Domain groups Token Variants; a Paired
|
|
5
|
+
// Theme activates one Variant's Token Set. The Workbench Combination is the
|
|
6
|
+
// *observed* selection and never mutates the project's Current Combination.
|
|
7
|
+
import { listEffectiveTokens } from "./effective-tokens.mjs";
|
|
8
|
+
import { tokenInventoryRows } from "./catalog.mjs";
|
|
9
|
+
import { loadPackageFromValues } from "./package.mjs";
|
|
10
|
+
|
|
11
|
+
function compareText(left, right) {
|
|
12
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function tokenLibraryEntries(snapshot) {
|
|
16
|
+
return snapshot.manifest.entries.tokens.map((entry) => ({
|
|
17
|
+
entry,
|
|
18
|
+
library: snapshot.entries[entry],
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function aliasPathOf(value) {
|
|
23
|
+
if (typeof value !== "string") return null;
|
|
24
|
+
return /^\{([^{}]+)\}$/.exec(value)?.[1] ?? null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// DSP-003-A: enumerate the Workbench Combinations offered to the user, from
|
|
28
|
+
// the Paired Themes already defined in the package. No names are hardcoded;
|
|
29
|
+
// selection is by stable theme ids. The project Current Combination is
|
|
30
|
+
// reported per domain so the UI can show what the workbench does NOT change.
|
|
31
|
+
export function enumerateWorkbenchCombinations(snapshot) {
|
|
32
|
+
const domains = new Map();
|
|
33
|
+
const activeThemeIds = new Set();
|
|
34
|
+
const libraries = tokenLibraryEntries(snapshot);
|
|
35
|
+
for (const { library } of libraries) {
|
|
36
|
+
for (const themeId of library.activeThemeIds ?? []) {
|
|
37
|
+
activeThemeIds.add(themeId);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const { library } of libraries) {
|
|
41
|
+
for (const theme of library.themes ?? []) {
|
|
42
|
+
const domain = theme.group;
|
|
43
|
+
if (!domains.has(domain)) {
|
|
44
|
+
domains.set(domain, {
|
|
45
|
+
currentProjectThemeId: null,
|
|
46
|
+
domain,
|
|
47
|
+
domainId: `domain_${domain}`,
|
|
48
|
+
variants: [],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const record = domains.get(domain);
|
|
52
|
+
if (activeThemeIds.has(theme.id)) {
|
|
53
|
+
record.currentProjectThemeId = theme.id;
|
|
54
|
+
}
|
|
55
|
+
record.variants.push({
|
|
56
|
+
name: theme.name,
|
|
57
|
+
setIds: [...(theme.setIds ?? [])],
|
|
58
|
+
themeId: theme.id,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const diagnostics = [];
|
|
63
|
+
const knownThemeIds = new Set(
|
|
64
|
+
libraries.flatMap(({ library }) =>
|
|
65
|
+
(library.themes ?? []).map((theme) => theme.id),
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
for (const themeId of activeThemeIds) {
|
|
69
|
+
if (!knownThemeIds.has(themeId)) {
|
|
70
|
+
diagnostics.push({
|
|
71
|
+
code: "combination_unknown_active_theme",
|
|
72
|
+
message: `Active theme id is not defined in any token library: ${themeId}`,
|
|
73
|
+
themeId,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const domainsList = [...domains.values()].sort((left, right) =>
|
|
78
|
+
compareText(left.domain, right.domain),
|
|
79
|
+
);
|
|
80
|
+
for (const domain of domainsList) {
|
|
81
|
+
if (!domain.currentProjectThemeId) {
|
|
82
|
+
diagnostics.push({
|
|
83
|
+
code: "combination_domain_unselected",
|
|
84
|
+
domain: domain.domain,
|
|
85
|
+
domainId: domain.domainId,
|
|
86
|
+
message: `Token Domain has no active Paired Theme: ${domain.domain}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { diagnostics, domains: domainsList };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// DSP-003-A: validate a workbench selection given as stable-id pairs
|
|
94
|
+
// [{domainId, themeId}]. Empty diagnostics means the combination is usable.
|
|
95
|
+
export function validateWorkbenchCombination(snapshot, combination) {
|
|
96
|
+
const { diagnostics: enumerationDiagnostics, domains } =
|
|
97
|
+
enumerateWorkbenchCombinations(snapshot);
|
|
98
|
+
const diagnostics = [...enumerationDiagnostics];
|
|
99
|
+
const byId = new Map(domains.map((domain) => [domain.domainId, domain]));
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (const selection of combination ?? []) {
|
|
102
|
+
const domain = byId.get(selection?.domainId);
|
|
103
|
+
if (!domain) {
|
|
104
|
+
diagnostics.push({
|
|
105
|
+
code: "combination_unknown_domain",
|
|
106
|
+
domainId: selection?.domainId ?? null,
|
|
107
|
+
message: `Unknown Token Domain: ${String(selection?.domainId)}`,
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (seen.has(domain.domainId)) {
|
|
112
|
+
diagnostics.push({
|
|
113
|
+
code: "combination_duplicate_domain",
|
|
114
|
+
domainId: domain.domainId,
|
|
115
|
+
message: `Domain selected more than once: ${domain.domain}`,
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
seen.add(domain.domainId);
|
|
120
|
+
const variant = domain.variants.find(
|
|
121
|
+
(candidate) => candidate.themeId === selection?.themeId,
|
|
122
|
+
);
|
|
123
|
+
if (!variant) {
|
|
124
|
+
diagnostics.push({
|
|
125
|
+
code: "combination_unknown_variant",
|
|
126
|
+
domainId: domain.domainId,
|
|
127
|
+
message: `Theme is not a variant of Token Domain ${domain.domain}: ${String(selection?.themeId)}`,
|
|
128
|
+
themeId: selection?.themeId ?? null,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const domain of domains) {
|
|
133
|
+
if (!seen.has(domain.domainId) && domain.variants.length > 0) {
|
|
134
|
+
diagnostics.push({
|
|
135
|
+
code: "combination_missing_domain",
|
|
136
|
+
domain: domain.domain,
|
|
137
|
+
domainId: domain.domainId,
|
|
138
|
+
message: `Workbench Combination must select Token Domain: ${domain.domain}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { diagnostics };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function clonePackageValues(snapshot) {
|
|
146
|
+
const values = new Map();
|
|
147
|
+
values.set("manifest.json", structuredClone(snapshot.manifest));
|
|
148
|
+
for (const [entry, value] of Object.entries(snapshot.entries)) {
|
|
149
|
+
values.set(entry, structuredClone(value));
|
|
150
|
+
}
|
|
151
|
+
return values;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function homeSetOf(previewSnapshot, token) {
|
|
155
|
+
const filePath = token.filePath;
|
|
156
|
+
const library = previewSnapshot.entries[filePath];
|
|
157
|
+
if (!library || !Array.isArray(library.sets)) return null;
|
|
158
|
+
for (const set of library.sets) {
|
|
159
|
+
if ((set.tokens ?? []).some((candidate) => candidate.id === token.id)) {
|
|
160
|
+
return { setId: set.id, setName: set.name };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function rawValueOf(previewSnapshot, token) {
|
|
167
|
+
const library = previewSnapshot.entries[token.filePath];
|
|
168
|
+
for (const set of library?.sets ?? []) {
|
|
169
|
+
for (const candidate of set.tokens ?? []) {
|
|
170
|
+
if (candidate.id === token.id) return candidate.value;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Raw/alias/resolved/source rows over an already-built preview snapshot.
|
|
177
|
+
function previewTokenRows(previewSnapshot) {
|
|
178
|
+
return listEffectiveTokens(previewSnapshot).map((effective) => {
|
|
179
|
+
const token = effective.token;
|
|
180
|
+
const home = homeSetOf(previewSnapshot, token) ?? {
|
|
181
|
+
setId: null,
|
|
182
|
+
setName: null,
|
|
183
|
+
};
|
|
184
|
+
const raw = rawValueOf(previewSnapshot, token);
|
|
185
|
+
return {
|
|
186
|
+
alias: aliasPathOf(raw),
|
|
187
|
+
homeSetId: home.setId,
|
|
188
|
+
homeSetName: home.setName,
|
|
189
|
+
ownerPackageId: previewSnapshot.manifest.packageId,
|
|
190
|
+
path: token.path,
|
|
191
|
+
raw,
|
|
192
|
+
resolved: effective.value,
|
|
193
|
+
setId: home.setId,
|
|
194
|
+
setName: home.setName,
|
|
195
|
+
sourceTokenId: token.id,
|
|
196
|
+
type: token.type,
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// DSP-003-B: pure evaluation preview. Patches the *observed* active themes in
|
|
202
|
+
// a cloned package (never the source, never an activate event) and runs the
|
|
203
|
+
// authoritative token resolver over it.
|
|
204
|
+
export async function createWorkbenchPreview(snapshot, combination) {
|
|
205
|
+
const { diagnostics } = validateWorkbenchCombination(snapshot, combination);
|
|
206
|
+
if (diagnostics.length > 0) {
|
|
207
|
+
const error = new Error(
|
|
208
|
+
`Invalid workbench combination: ${diagnostics
|
|
209
|
+
.map((entry) => entry.code)
|
|
210
|
+
.join(", ")}`,
|
|
211
|
+
);
|
|
212
|
+
error.code = "invalid_workbench_combination";
|
|
213
|
+
error.diagnostics = diagnostics;
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
const enumeration = enumerateWorkbenchCombinations(snapshot);
|
|
217
|
+
const values = clonePackageValues(snapshot);
|
|
218
|
+
const observedThemeByGroup = new Map(
|
|
219
|
+
(combination ?? []).map((selection) => {
|
|
220
|
+
const domain = enumeration.domains.find(
|
|
221
|
+
(candidate) => candidate.domainId === selection.domainId,
|
|
222
|
+
);
|
|
223
|
+
return [domain.domain, selection.themeId];
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
for (const libraryEntry of values.values()) {
|
|
227
|
+
if (!libraryEntry || !Array.isArray(libraryEntry.themes)) continue;
|
|
228
|
+
libraryEntry.activeThemeIds = libraryEntry.themes
|
|
229
|
+
.filter(
|
|
230
|
+
(theme) => observedThemeByGroup.get(theme.group) === theme.id,
|
|
231
|
+
)
|
|
232
|
+
.map((theme) => theme.id);
|
|
233
|
+
}
|
|
234
|
+
const preview = await loadPackageFromValues(snapshot.locator, values);
|
|
235
|
+
return {
|
|
236
|
+
combination: (combination ?? []).map((selection) => ({ ...selection })),
|
|
237
|
+
preview,
|
|
238
|
+
tokens: previewTokenRows(preview),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function editablePackageId(previewSnapshot) {
|
|
243
|
+
return previewSnapshot.manifest.packageId;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// DSP-003-C: explicit edit targets for one displayed value. Targets are only
|
|
247
|
+
// offered when unambiguous: the owning cell, the alias expression (when the
|
|
248
|
+
// raw value is an alias), the alias's shared target cell, and a variant
|
|
249
|
+
// override when the observed variant set differs from the cell's home set.
|
|
250
|
+
export function resolveWorkbenchEditTargets(
|
|
251
|
+
previewSnapshot,
|
|
252
|
+
combination,
|
|
253
|
+
token,
|
|
254
|
+
) {
|
|
255
|
+
const rows = previewTokenRows(previewSnapshot);
|
|
256
|
+
const row = rows.find(
|
|
257
|
+
(candidate) =>
|
|
258
|
+
candidate.path === token.path &&
|
|
259
|
+
candidate.ownerPackageId === token.ownerPackageId,
|
|
260
|
+
);
|
|
261
|
+
if (!row) {
|
|
262
|
+
return {
|
|
263
|
+
diagnostics: [{ code: "token_not_in_preview", path: token.path }],
|
|
264
|
+
targets: [],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const editablePackage = editablePackageId(previewSnapshot);
|
|
268
|
+
const homeReadOnly = row.ownerPackageId !== editablePackage;
|
|
269
|
+
const targets = [
|
|
270
|
+
{
|
|
271
|
+
kind: "cell",
|
|
272
|
+
ownerPackageId: row.ownerPackageId,
|
|
273
|
+
path: row.path,
|
|
274
|
+
readOnly: homeReadOnly,
|
|
275
|
+
setId: row.setId,
|
|
276
|
+
tokenId: row.sourceTokenId,
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
if (row.alias) {
|
|
280
|
+
targets.push({
|
|
281
|
+
kind: "alias-expression",
|
|
282
|
+
current: `{${row.alias}}`,
|
|
283
|
+
ownerPackageId: row.ownerPackageId,
|
|
284
|
+
path: row.path,
|
|
285
|
+
readOnly: homeReadOnly,
|
|
286
|
+
tokenId: row.sourceTokenId,
|
|
287
|
+
});
|
|
288
|
+
const target = rows.find((candidate) => candidate.path === row.alias);
|
|
289
|
+
if (target) {
|
|
290
|
+
targets.push({
|
|
291
|
+
kind: "shared-cell",
|
|
292
|
+
ownerPackageId: target.ownerPackageId,
|
|
293
|
+
path: target.path,
|
|
294
|
+
readOnly: target.ownerPackageId !== editablePackage,
|
|
295
|
+
setId: target.setId,
|
|
296
|
+
tokenId: target.sourceTokenId,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const domain = row.homeSetName ? row.homeSetName.split("/")[0] : null;
|
|
301
|
+
const selection = (combination ?? []).find(
|
|
302
|
+
(candidate) => candidate.domainId === `domain_${domain}`,
|
|
303
|
+
);
|
|
304
|
+
if (domain && selection) {
|
|
305
|
+
const observed = findSetByTheme(previewSnapshot, selection.themeId);
|
|
306
|
+
if (observed && observed.id !== row.setId) {
|
|
307
|
+
targets.push({
|
|
308
|
+
kind: "variant-override",
|
|
309
|
+
observedSetId: observed.id,
|
|
310
|
+
observedSetName: observed.name,
|
|
311
|
+
ownerPackageId: row.ownerPackageId,
|
|
312
|
+
path: row.path,
|
|
313
|
+
readOnly: homeReadOnly,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { diagnostics: [], targets };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function findSetByTheme(previewSnapshot, themeId) {
|
|
321
|
+
for (const entry of Object.values(previewSnapshot.entries)) {
|
|
322
|
+
if (!Array.isArray(entry?.themes)) continue;
|
|
323
|
+
const theme = entry.themes.find((candidate) => candidate.id === themeId);
|
|
324
|
+
if (theme) {
|
|
325
|
+
const set = (entry.sets ?? []).find((candidate) =>
|
|
326
|
+
(theme.setIds ?? []).includes(candidate.id),
|
|
327
|
+
);
|
|
328
|
+
if (set) return set;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// DSP-004-B: build the generated System Sheet as pure data. Every specimen
|
|
335
|
+
// carries a stable projection identity derived from its qualified source key
|
|
336
|
+
// and points at the real editable target (Token Cell or component variant).
|
|
337
|
+
// Decorative elements (labels, section headers, rulers, scale) are flagged
|
|
338
|
+
// separately and are never design nodes.
|
|
339
|
+
export function buildWorkbenchSheet(snapshot, options = {}) {
|
|
340
|
+
const libraries = options.libraries ?? [];
|
|
341
|
+
const foundation = options.foundation;
|
|
342
|
+
const packageId = snapshot.manifest.packageId;
|
|
343
|
+
const sections = new Map();
|
|
344
|
+
const sources = [
|
|
345
|
+
{ rows: tokenInventoryRows(snapshot, "product"), owner: packageId, source: "product" },
|
|
346
|
+
...(foundation
|
|
347
|
+
? [{ rows: tokenInventoryRows(foundation, "foundation"), owner: foundation.manifest.packageId, source: "foundation" }]
|
|
348
|
+
: []),
|
|
349
|
+
...libraries.map((library) => ({
|
|
350
|
+
rows: tokenInventoryRows(library, "library"),
|
|
351
|
+
owner: library.manifest.packageId,
|
|
352
|
+
source: "library",
|
|
353
|
+
})),
|
|
354
|
+
];
|
|
355
|
+
for (const { rows, owner } of sources) {
|
|
356
|
+
for (const row of rows) {
|
|
357
|
+
const domain = row.setName.split("/")[0];
|
|
358
|
+
const sectionId = `dsp-section-${domain}`;
|
|
359
|
+
if (!sections.has(sectionId)) {
|
|
360
|
+
sections.set(sectionId, {
|
|
361
|
+
id: sectionId,
|
|
362
|
+
kind: "section",
|
|
363
|
+
name: domain,
|
|
364
|
+
// The section header/ruler are generated decorations.
|
|
365
|
+
decoration: true,
|
|
366
|
+
specimens: [],
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
sections.get(sectionId).specimens.push({
|
|
370
|
+
specimenId: `dsp-specimen/${row.qualifiedKey}`,
|
|
371
|
+
kind: "token",
|
|
372
|
+
decoration: false,
|
|
373
|
+
active: row.active,
|
|
374
|
+
readOnly: owner !== packageId,
|
|
375
|
+
target: {
|
|
376
|
+
kind: "token-cell",
|
|
377
|
+
ownerPackageId: row.ownerPackageId,
|
|
378
|
+
qualifiedKey: row.qualifiedKey,
|
|
379
|
+
setId: row.setId,
|
|
380
|
+
setName: row.setName,
|
|
381
|
+
tokenId: row.tokenId ?? row.path,
|
|
382
|
+
path: row.path,
|
|
383
|
+
},
|
|
384
|
+
display: { type: row.type, value: row.value },
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Component specimens: one per (component set, variant).
|
|
389
|
+
const componentSetsOf = (snapshot, source, owner) =>
|
|
390
|
+
[...snapshot.manifest.entries.components]
|
|
391
|
+
.map((entry) => snapshot.entries[entry])
|
|
392
|
+
.filter((componentFile) => Array.isArray(componentFile.componentSets))
|
|
393
|
+
.flatMap((componentFile) => componentFile.componentSets)
|
|
394
|
+
.filter((componentSet) => !publicFilter(componentSet))
|
|
395
|
+
.flatMap((componentSet) =>
|
|
396
|
+
componentSet.variants.map((variant) => ({
|
|
397
|
+
specimenId: `dsp-specimen/component/${owner}/${componentSet.id}/${variant.id}`,
|
|
398
|
+
kind: "component",
|
|
399
|
+
decoration: false,
|
|
400
|
+
readOnly: source !== "product",
|
|
401
|
+
target: {
|
|
402
|
+
kind: "component-definition",
|
|
403
|
+
componentSetId: componentSet.id,
|
|
404
|
+
ownerPackageId: owner,
|
|
405
|
+
variantId: variant.id,
|
|
406
|
+
rootId: variant.rootId,
|
|
407
|
+
},
|
|
408
|
+
display: { name: componentSet.name, selection: variant.selection },
|
|
409
|
+
})),
|
|
410
|
+
);
|
|
411
|
+
function publicFilter(componentSet) {
|
|
412
|
+
return componentSet.deprecated === true;
|
|
413
|
+
}
|
|
414
|
+
for (const specimen of componentSetsOf(snapshot, "product", packageId)) {
|
|
415
|
+
const sectionId = "dsp-section-components";
|
|
416
|
+
if (!sections.has(sectionId)) {
|
|
417
|
+
sections.set(sectionId, {
|
|
418
|
+
id: sectionId,
|
|
419
|
+
kind: "section",
|
|
420
|
+
name: "Components",
|
|
421
|
+
decoration: true,
|
|
422
|
+
specimens: [],
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
sections.get(sectionId).specimens.push(specimen);
|
|
426
|
+
}
|
|
427
|
+
const orderedSections = [...sections.values()].sort((left, right) =>
|
|
428
|
+
compareText(left.id, right.id),
|
|
429
|
+
);
|
|
430
|
+
return {
|
|
431
|
+
id: "dsp-system-sheet",
|
|
432
|
+
kind: "system-sheet",
|
|
433
|
+
// Regenerable: the sheet is derived data and never persisted as pages.
|
|
434
|
+
regenerable: true,
|
|
435
|
+
sections: orderedSections,
|
|
436
|
+
};
|
|
437
|
+
}
|