@texuli/spec 0.8.0 → 0.10.0

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.
Files changed (35) hide show
  1. package/README.md +42 -11
  2. package/dist/build.d.ts +5 -0
  3. package/dist/build.js +1 -0
  4. package/dist/codegen/kotlin-tokens.d.ts +38 -0
  5. package/dist/codegen/kotlin-tokens.js +138 -0
  6. package/dist/codegen/kotlin.d.ts +3 -1
  7. package/dist/codegen/kotlin.js +5 -2
  8. package/dist/design-system/index.d.ts +11 -0
  9. package/dist/design-system/index.js +10 -0
  10. package/dist/design-system/schema.d.ts +3 -0
  11. package/dist/design-system/schema.js +164 -0
  12. package/dist/design-system/validate.d.ts +69 -0
  13. package/dist/design-system/validate.js +273 -0
  14. package/dist/design-system/vocabulary.d.ts +70 -0
  15. package/dist/design-system/vocabulary.js +351 -0
  16. package/dist/format.js +10 -1
  17. package/dist/generated/typescript/blueprint.d.ts +69 -12
  18. package/dist/generated/typescript/blueprint.js +1 -1
  19. package/dist/generated/typescript/catalog-schema.js +1 -1
  20. package/dist/generated/typescript/design-system.d.ts +1110 -0
  21. package/dist/generated/typescript/design-system.js +3 -0
  22. package/dist/generated/typescript/diagnostics.js +1 -1
  23. package/dist/generated/typescript/manifest.js +1 -1
  24. package/dist/generated/typescript/vocabulary.d.ts +15 -1
  25. package/dist/generated/typescript/vocabulary.js +2 -2
  26. package/dist/generated/typescript/workflow.js +1 -1
  27. package/dist/migrations/0.8.0-0.9.0.d.ts +8 -0
  28. package/dist/migrations/0.8.0-0.9.0.js +11 -0
  29. package/dist/migrations/0.9.0-0.10.0.d.ts +10 -0
  30. package/dist/migrations/0.9.0-0.10.0.js +13 -0
  31. package/dist/migrations/index.js +4 -0
  32. package/dist/validate.js +12 -0
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/package.json +5 -1
@@ -0,0 +1,351 @@
1
+ /**
2
+ * The closed vocabulary of a design-system document (ADR-021), as a compact
3
+ * table: the validator walks it, and `scripts/design-system-schema.ts`
4
+ * projects it into `schemas/design-system.schema.json` (TypeScript and
5
+ * Kotlin types follow from that schema). Kept as data, not a bundled JSON
6
+ * Schema, so `@texuli/spec/design-system` stays a few KB.
7
+ *
8
+ * A node is a `$defs` name (a string), a closed object (a key ending in `!`
9
+ * is required), or a tagged tuple:
10
+ * `['e', 'a b']` enum · `['n' | 'i', min, max]` number or integer ·
11
+ * `['s', pattern, maxLength]` string · `['t', maxLength]` text without
12
+ * control or bidi characters · `['l', item, min, max, unique?]` list ·
13
+ * `['m', value, maxEntries, keyPattern]` map · `['b']` boolean ·
14
+ * `['k', value]` constant · `['u', 'i' | 'f', capBytes]` an image or font
15
+ * source · `['x']` an `extends` reference.
16
+ */
17
+ export const DESIGN_SYSTEM_FORMAT = 'texuli/design-system';
18
+ export const DESIGN_SYSTEM_FORMAT_VERSION = 1;
19
+ /** Stable codes `validate` and `migrate` raise (ADR-021). */
20
+ export const DESIGN_SYSTEM_CODES = [
21
+ 'unknown-key',
22
+ 'invalid-type',
23
+ 'invalid-value',
24
+ 'required',
25
+ 'format-version',
26
+ 'extends-range',
27
+ 'extends-cycle',
28
+ 'extends-depth',
29
+ 'url-refused',
30
+ 'inline-bytes',
31
+ ];
32
+ /** Guarantee codes the compiler raises (ADR-022); reserved here, never raised by the spec. */
33
+ export const DESIGN_SYSTEM_COMPILER_CODES = ['aa-text', 'aa-focus', 'target-size'];
34
+ /** Caps: visited values, depth, errors, the `extends` chain and inline bytes (decoded). */
35
+ export const DESIGN_SYSTEM_LIMITS = Object.freeze({
36
+ values: 20_000,
37
+ depth: 16,
38
+ errors: 64,
39
+ extendsDepth: 4,
40
+ logo: 32_768,
41
+ favicon: 8_192,
42
+ font: 131_072,
43
+ inlineTotal: 262_144,
44
+ });
45
+ const ID = '[a-z][a-z0-9]*(?:-[a-z0-9]+)*';
46
+ const SEMVER = '(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?';
47
+ const NUM = '(?:\\d{1,3}(?:\\.\\d{1,6})?|\\.\\d{1,6})';
48
+ const PATH = '(?:\\.?\\/)?(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\\/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*\\.';
49
+ /** Shared patterns (also written into the schema). */
50
+ export const DS_PATTERNS = {
51
+ id: new RegExp(`^${ID}$`),
52
+ semver: new RegExp(`^${SEMVER}$`),
53
+ ref: new RegExp(`^(?:texuli:${ID}|${ID}@${SEMVER})$`),
54
+ color: new RegExp(`^(?:#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|oklch\\(${NUM}%? ${NUM} ${NUM}(?: \\/ ${NUM}%?)?\\))$`),
55
+ image: new RegExp(`^${PATH}(?:png|jpe?g|gif|webp|avif|svg|ico)$`),
56
+ font: new RegExp(`^${PATH}(?:woff2?|ttf|otf)$`),
57
+ // Free text refuses control, bidi and invisible characters: the tokens
58
+ // importers' set (C0, C1, soft hyphen, Arabic letter mark, Mongolian vowel
59
+ // separator, zero-width space, non-joiner and joiner, LRM and RLM, word
60
+ // joiner and invisible operators, bidi embeddings, overrides and isolates,
61
+ // byte order mark), plus the line and paragraph separators and the tag
62
+ // characters. The schema compiles patterns with the `u` flag.
63
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: text refuses control, bidi and invisible characters
64
+ text: /^[^\u0000-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff\u{e0000}-\u{e007f}]*$/u,
65
+ key: /^[a-z][a-z0-9-]{0,47}$/,
66
+ };
67
+ const P = DS_PATTERNS;
68
+ const color = 'colorValue';
69
+ const keys = (names, node) => Object.fromEntries(names.split(' ').map((name) => [name, node]));
70
+ /** Map keys no map may use, whatever its pattern. */
71
+ export const DS_RESERVED_KEYS = new Set([
72
+ '__proto__',
73
+ 'constructor',
74
+ 'prototype',
75
+ ]);
76
+ /** The semantic roles of `@texuli/tokens` (`SEMANTIC_ROLES`); a test keeps them equal. */
77
+ export const DS_ROLES = 'background foreground surface surfaceForeground elevated elevatedForeground muted mutedForeground subtle subtleForeground border borderStrong input ring primary primaryForeground primarySoft primarySoftForeground secondary secondaryForeground accent accentForeground destructive destructiveForeground destructiveSoft destructiveSoftForeground success successForeground successSoft successSoftForeground warning warningForeground warningSoft warningSoftForeground info infoForeground infoSoft infoSoftForeground sidebar sidebarForeground sidebarBorder sidebarPrimary overlay scrim shadow link linkVisited selection selectionForeground canvas';
78
+ /** Component token kinds: the letter in {@link DS_COMPONENT_TOKENS} → its value `$defs`. */
79
+ export const DS_TOKEN_KINDS = {
80
+ r: 'radiusStep',
81
+ d: 'dimension',
82
+ t: 'typeRoleName',
83
+ b: 'borderStep',
84
+ e: 'elevationLevel',
85
+ o: 'opacityStep',
86
+ c: 'roleName',
87
+ k: 'strokeWidth',
88
+ };
89
+ /**
90
+ * Every component of the token registry (`COMPONENT_TOKEN_FAMILIES` of
91
+ * `@texuli/tokens`, z-index tokens left out) and the defaults vocabulary
92
+ * (`DESIGN_DEFAULTS_VOCABULARY` of `texuli`): `key:kind` pairs. A test
93
+ * keeps it equal to both.
94
+ */
95
+ export const DS_COMPONENT_TOKENS = {
96
+ accordion: 'trigger-gap:d trigger-padding-block:d trigger-font-size:t trigger-font-weight:t disabled-opacity:o',
97
+ alert: 'radius:r border-width:b padding-block:d padding-inline:d gap:d font-size:t icon-size:d',
98
+ 'aspect-ratio': 'radius:r',
99
+ avatar: 'size-xs:d size-sm:d size-md:d size-lg:d radius:r font-weight:t',
100
+ badge: 'radius:r padding-block:d padding-inline:d gap:d font-size:t font-weight:t line-height:t border-width:b',
101
+ board: 'column-gap:d column-radius:r column-padding:d card-radius:r card-elevation:e',
102
+ breadcrumb: '',
103
+ button: 'height-sm:d height-md:d height-lg:d height-touch:d padding-inline-sm:d padding-inline-md:d padding-inline-lg:d gap:d radius:r font-size:t font-size-sm:t font-size-lg:t font-weight:t line-height:t icon-size:d border-width:b elevation:e disabled-opacity:o',
104
+ 'canvas-toolbar': 'gap:d padding:d button-radius:r button-font-size:t',
105
+ card: 'radius:r border-width:b elevation:e padding-inline:d padding-block:d background:c title-font-size:t title-font-weight:t title-line-height:t description-font-size:t',
106
+ carousel: 'radius:r fallback-padding:d',
107
+ 'chart-frame': 'title-font-size:t',
108
+ 'chat-bubble': 'radius:r tail-radius:r padding-block:d padding-inline:d',
109
+ 'chat-composer': 'radius:r padding:d gap:d',
110
+ chip: '',
111
+ choice: 'gap:d switch-gap:d label-font-size:t label-font-weight:t description-font-size:t disabled-opacity:o',
112
+ code: 'font-size:t font-weight:t block-padding-block:d block-padding-inline:d block-radius:r block-line-height:t block-border-width:b',
113
+ 'color-picker': 'gap:d swatch-size:d swatch-radius:r',
114
+ combobox: 'height-sm:d height-md:d height-lg:d radius:r border-width:b font-size-sm:t font-size-md:t font-size-lg:t',
115
+ command: 'radius:r border-width:b font-size-sm:t font-size-md:t font-size-lg:t input-gap:d input-padding-inline:d',
116
+ 'copy-button': '',
117
+ 'data-grid': 'gap:d radius:r pager-font-size:t',
118
+ date: 'font-size:t label-font-weight:t control-radius:r control-border-width:b panel-padding:d panel-radius:r panel-elevation:e description-font-size:t',
119
+ dialog: 'radius:r border-width:b elevation:e font-size:t line-height:t title-font-size:t title-font-weight:t title-line-height:t alert-padding:d scrim-opacity:o',
120
+ empty: 'gap:d padding-inline:d padding-block:d title-font-size:t title-font-weight:t description-font-size:t',
121
+ field: 'gap:d label-font-size:t label-font-weight:t label-line-height:t description-font-size:t description-line-height:t',
122
+ 'file-upload': 'gap:d zone-padding-block:d zone-padding-inline:d zone-border-width:b zone-radius:r browse-height:d',
123
+ forms: 'gap:d',
124
+ icon: 'size-xs:d size-sm:d size-md:d size-lg:d stroke-width:k',
125
+ 'icon-button': '',
126
+ input: 'height:d height-touch:d padding-block:d padding-inline:d radius:r border-width:b elevation:e font-size:t font-size-touch:t font-weight:t textarea-padding-block:d disabled-opacity:o',
127
+ kbd: 'min-size:d padding-block:d radius:r border-width:b font-weight:t',
128
+ label: '',
129
+ menu: 'trigger-height-sm:d trigger-height-md:d trigger-height-lg:d trigger-radius:r trigger-border-width:b trigger-font-size:t trigger-font-weight:t content-padding-block:d content-radius:r content-elevation:e content-font-size:t',
130
+ navigation: 'font-size:t list-gap:d trigger-radius:r chevron-opacity:o',
131
+ 'number-field': 'radius:r border-width:b padding-block:d padding-inline:d stepper-size:d',
132
+ pagination: '',
133
+ 'pin-input': 'gap:d cell-inline-size:d cell-block-size:d cell-radius:r cell-font-size:t',
134
+ popover: 'padding-block:d padding-inline:d radius:r border-width:b elevation:e font-size:t title-font-size:t title-font-weight:t title-line-height:t',
135
+ progress: 'gap:d font-size:t radius:r track-height-sm:d track-height-md:d track-height-lg:d',
136
+ 'radio-group': '',
137
+ rating: 'stars-gap:d label-font-size:t label-font-weight:t star-radius:r',
138
+ resizable: 'radius:r border-width:b',
139
+ 'resizable-handle': '',
140
+ 'scroll-area': 'radius:r border-width:b padding:d scrollbar-size:d scrollbar-radius:r thumb-min-size:d',
141
+ select: 'height-sm:d height-md:d height-lg:d padding-inline-sm:d padding-inline-md:d padding-inline-lg:d gap:d radius:r border-width:b font-size-sm:t font-size-md:t font-size-lg:t font-weight:t chevron-size:d chevron-opacity:o',
142
+ separator: 'thickness:b',
143
+ sheet: 'gap:d padding:d border-width:b elevation:e font-size:t line-height:t title-font-size:t title-font-weight:t title-line-height:t scrim-opacity:o',
144
+ sidebar: 'font-size:t inset-radius:r header-padding-inline:d header-gap:d body-gap:d body-padding-block:d body-padding-inline:d',
145
+ skeleton: 'radius:r height-sm:d height-md:d height-lg:d circle-size:d',
146
+ slider: 'thumb-border-width:b thumb-elevation:e disabled-opacity:o',
147
+ spinner: 'radius:r',
148
+ spreadsheet: 'gap:d tab-radius:r tab-padding-inline:d bar-radius:r',
149
+ stat: 'gap:d padding:d radius:r border-width:b elevation:e label-font-size:t label-font-weight:t value-font-size:t value-font-weight:t icon-size:d icon-radius:r',
150
+ stepper: 'gap:d trigger-radius:r marker-size:d marker-radius:r marker-border-width:b marker-font-size:t marker-font-weight:t check-size:d',
151
+ table: 'radius:r border-width:b font-size:t head-font-size:t head-font-weight:t',
152
+ tabs: 'gap:d list-gap:d list-radius:r trigger-gap:d trigger-height-touch:d trigger-font-size:t trigger-font-weight:t disabled-opacity:o',
153
+ tag: '',
154
+ 'tags-input': 'height:d height-touch:d gap:d padding-inline:d radius:r border-width:b elevation:e tag-radius:r tag-font-size:t disabled-opacity:o',
155
+ timeline: 'marker-size:d marker-radius:r time-font-size:t time-font-weight:t',
156
+ toast: 'gap:d padding-block:d radius:r border-width:b elevation:e icon-size:d title-font-size:t title-font-weight:t',
157
+ toggle: 'height-sm:d height-md:d height-lg:d padding-inline-sm:d padding-inline-md:d padding-inline-lg:d gap:d radius:r border-width:b font-size:t font-weight:t line-height:t disabled-opacity:o',
158
+ 'toggle-group': '',
159
+ toolbar: 'gap:d padding:d radius:r border-width:b item-height-sm:d item-font-size:t item-font-weight:t disabled-opacity:o',
160
+ tooltip: 'radius:r font-size:t font-weight:t line-height:t elevation:e',
161
+ tree: 'gap:d padding:d radius:r row-gap:d row-padding-block:d checkbox-size:d checkbox-radius:r',
162
+ 'workflow-node': 'padding-block:d padding-inline:d radius:r elevation:e',
163
+ };
164
+ /** Component defaults: `[variants, sizes]`, space-separated; a test keeps it equal to `texuli`. */
165
+ export const DS_COMPONENT_DEFAULTS = {
166
+ accordion: ['default bordered', ''],
167
+ avatar: ['', 'xs sm md lg'],
168
+ badge: ['default secondary outline success warning destructive info neutral', ''],
169
+ breadcrumb: ['', 'sm md lg'],
170
+ button: ['default secondary outline ghost destructive link', 'sm md lg icon'],
171
+ card: ['elevated flat', ''],
172
+ chip: ['default secondary outline success warning destructive info neutral', ''],
173
+ choice: ['', 'sm md'],
174
+ code: ['inline block', ''],
175
+ combobox: ['outline soft', 'sm md lg'],
176
+ command: ['outline soft', 'sm md lg'],
177
+ 'copy-button': ['', 'sm md'],
178
+ date: ['', 'sm md'],
179
+ dialog: ['', 'sm md lg xl'],
180
+ empty: ['', 'sm md'],
181
+ 'file-upload': ['default compact', ''],
182
+ icon: ['', 'xs sm md lg'],
183
+ 'icon-button': ['default secondary outline ghost destructive link', 'sm md lg icon'],
184
+ label: ['', 'sm md'],
185
+ menu: ['default soft', 'sm md lg'],
186
+ navigation: ['', 'sm md lg'],
187
+ 'number-field': ['', 'sm md lg'],
188
+ pagination: ['outline soft', 'sm md lg'],
189
+ 'pin-input': ['', 'sm md lg'],
190
+ popover: ['elevated flat', 'sm md lg'],
191
+ progress: ['', 'sm md lg'],
192
+ 'radio-group': ['default card', 'sm md'],
193
+ rating: ['', 'sm md lg'],
194
+ resizable: ['plain bordered', ''],
195
+ 'resizable-handle': ['', 'sm md'],
196
+ 'scroll-area': ['', 'sm md lg'],
197
+ select: ['outline soft', 'sm md lg'],
198
+ separator: ['default subtle strong', ''],
199
+ sheet: ['', 'sm md lg xl'],
200
+ sidebar: ['default inset', ''],
201
+ skeleton: ['', 'sm md lg'],
202
+ slider: ['', 'sm md lg'],
203
+ spinner: ['', 'sm md lg'],
204
+ stat: ['elevated flat', ''],
205
+ tabs: ['line pills enclosed', ''],
206
+ tag: ['default secondary outline success warning destructive info neutral', ''],
207
+ toggle: ['default outline', 'sm md lg'],
208
+ 'toggle-group': ['default outline', 'sm md lg'],
209
+ toolbar: ['default outline ghost', 'sm md'],
210
+ tree: ['default bordered', ''],
211
+ };
212
+ const components = {};
213
+ for (const [id, pairs] of Object.entries(DS_COMPONENT_TOKENS)) {
214
+ const shape = {};
215
+ for (const pair of pairs ? pairs.split(' ') : []) {
216
+ const [key, kind] = pair.split(':');
217
+ shape[key] = DS_TOKEN_KINDS[kind];
218
+ }
219
+ const defaults = DS_COMPONENT_DEFAULTS[id];
220
+ if (defaults) {
221
+ const [variant, size] = defaults;
222
+ shape.defaults = {
223
+ ...(variant ? { variant: ['e', variant] } : {}),
224
+ ...(size ? { size: ['e', size] } : {}),
225
+ };
226
+ }
227
+ components[id] = shape;
228
+ }
229
+ const sections = keys('color typography shape space elevation surface motion layout icons components assets voice accessibility', '');
230
+ for (const name of Object.keys(sections))
231
+ sections[name] = name;
232
+ /** Every named definition. */
233
+ export const DS_DEFS = {
234
+ colorValue: ['s', P.color, 64],
235
+ roleName: ['e', DS_ROLES],
236
+ roleMap: keys(DS_ROLES, color),
237
+ dataVizPalette: {
238
+ categorical: ['l', color, 8, 8],
239
+ sequential: ['l', color, 9, 9],
240
+ diverging: ['l', color, 9, 9],
241
+ grid: color,
242
+ axis: color,
243
+ },
244
+ fontFile: {
245
+ 'src!': ['u', 'f', DESIGN_SYSTEM_LIMITS.font],
246
+ weight: ['s', /^[1-9]00(?: [1-9]00)?$/, 7],
247
+ style: ['e', 'normal italic'],
248
+ display: ['e', 'swap fallback optional block auto'],
249
+ preload: ['b'],
250
+ },
251
+ fontFamily: {
252
+ 'stack!': ['l', ['s', /^-?[A-Za-z0-9][A-Za-z0-9 _.-]{0,31}$/, 33], 1, 8],
253
+ files: ['l', 'fontFile', 0, 8],
254
+ },
255
+ typeRole: {
256
+ family: ['e', 'sans heading mono serif'],
257
+ size: ['n', 8, 128],
258
+ weight: ['i', 100, 900],
259
+ line: ['n', 1, 2.5],
260
+ tracking: ['n', -0.1, 0.2],
261
+ },
262
+ typeRoleName: ['e', 'display title heading body label caption code'],
263
+ radiusStep: ['e', 'none xs sm md lg xl full'],
264
+ spaceStep: [
265
+ 'e',
266
+ 'space-0 space-1 space-2 space-3 space-4 space-5 space-6 space-7 space-8 space-9 space-10 space-11 space-12',
267
+ ],
268
+ dimension: [
269
+ 'e',
270
+ 'space-0 space-1 space-2 space-3 space-4 space-5 space-6 space-7 space-8 space-9 space-10 space-11 space-12 control-xs control-sm control-md control-lg icon-xs icon-sm icon-md icon-lg target-min target-touch',
271
+ ],
272
+ borderStep: ['e', 'none thin thick'],
273
+ elevationLevel: ['i', 0, 5],
274
+ opacityStep: ['e', 'disabled muted scrim'],
275
+ strokeWidth: ['n', 0.5, 4],
276
+ asset: ['u', 'i', DESIGN_SYSTEM_LIMITS.logo],
277
+ color: {
278
+ brand: {
279
+ 'seed!': color,
280
+ secondary: color,
281
+ accent: color,
282
+ neutral: ['e', 'zinc slate stone gray warm cool tinted'],
283
+ },
284
+ pin: keys('primary secondary accent destructive success warning info', color),
285
+ roles: { light: 'roleMap', dark: 'roleMap' },
286
+ modes: ['l', ['e', 'light dark'], 1, 2, 1],
287
+ dataViz: { light: 'dataVizPalette', dark: 'dataVizPalette', cvdSafe: ['b'] },
288
+ status: keys('success warning danger info neutral', color),
289
+ },
290
+ typography: {
291
+ families: keys('sans heading mono serif', 'fontFamily'),
292
+ scale: { base: ['n', 12, 24], ratio: ['n', 1.05, 1.618] },
293
+ roles: keys('display title heading body label caption code', 'typeRole'),
294
+ },
295
+ shape: { radius: ['n', 0, 48], radiusScale: ['n', 0, 4], borderWidth: ['n', 0, 4] },
296
+ space: { base: ['n', 2, 8], density: ['n', 0.75, 1.25] },
297
+ elevation: { style: ['e', 'flat soft crisp glow layered'] },
298
+ surface: { style: ['e', 'solid glass'], glass: { alpha: ['n', 0, 1], blur: ['n', 0, 64] } },
299
+ motion: {
300
+ personality: ['e', 'none subtle standard expressive'],
301
+ durations: keys('fast base slow', ['i', 0, 2000]),
302
+ easing: ['e', 'standard emphasized enter exit linear'],
303
+ },
304
+ layout: {
305
+ contentMax: ['i', 320, 2560],
306
+ wideMax: ['i', 320, 2560],
307
+ breakpoints: keys('sm md lg', ['i', 320, 2560]),
308
+ grid: {
309
+ columns: keys('base sm md lg', ['i', 1, 24]),
310
+ gutter: keys('base sm md lg', 'spaceStep'),
311
+ },
312
+ },
313
+ icons: {
314
+ style: ['e', 'outline filled'],
315
+ stroke: ['n', 0.5, 3],
316
+ size: keys('sm md lg', ['n', 8, 64]),
317
+ },
318
+ components,
319
+ assets: {
320
+ logo: keys('light dark mark wordmark', 'asset'),
321
+ favicon: ['u', 'i', DESIGN_SYSTEM_LIMITS.favicon],
322
+ },
323
+ voice: {
324
+ tone: ['l', ['s', /^[a-z][a-z-]{0,31}$/, 32], 0, 8, 1],
325
+ lexicon: ['m', ['t', 64], 200, P.key],
326
+ guidelines: ['l', ['t', 200], 0, 12],
327
+ },
328
+ accessibility: {
329
+ contrast: ['e', 'AA AAA'],
330
+ targetMin: ['i', 24, 64],
331
+ focus: ['e', 'ring outline glow inset'],
332
+ focusWidth: ['n', 1, 6],
333
+ focusOffset: ['n', 0, 8],
334
+ },
335
+ variant: sections,
336
+ };
337
+ /** The document root. */
338
+ export const DS_ROOT = {
339
+ 'format!': ['k', DESIGN_SYSTEM_FORMAT],
340
+ 'formatVersion!': ['k', DESIGN_SYSTEM_FORMAT_VERSION],
341
+ 'id!': ['s', P.id, 64],
342
+ 'version!': ['s', P.semver, 64],
343
+ 'name!': ['t', 80],
344
+ description: ['t', 500],
345
+ owner: ['t', 80],
346
+ license: ['t', 80],
347
+ tags: ['l', ['s', /^[a-z0-9][a-z0-9-]{0,31}$/, 32], 0, 16, 1],
348
+ extends: ['x'],
349
+ ...sections,
350
+ variants: ['m', 'variant', 16, P.key],
351
+ };
package/dist/format.js CHANGED
@@ -9,7 +9,16 @@ export const LINE_WIDTH = 100;
9
9
  */
10
10
  export function formatJson(value) {
11
11
  // Round-trip through canonical JSON: sorted keys, undefined dropped.
12
- return `${print(JSON.parse(canonicalJson(value)), 0, '', 0)}\n`;
12
+ return escapeInvisible(`${print(JSON.parse(canonicalJson(value)), 0, '', 0)}\n`);
13
+ }
14
+ /**
15
+ * Invisible and bidi control characters as `\u` escapes (the same JSON
16
+ * value): the repository's Trojan Source check refuses them literally in
17
+ * committed files.
18
+ */
19
+ const INVISIBLE = /[\u200b-\u200f\u202a-\u202e\u2066-\u2069\ufeff]/g;
20
+ function escapeInvisible(text) {
21
+ return text.replace(INVISIBLE, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`);
13
22
  }
14
23
  function isScalar(value) {
15
24
  return value === null || typeof value !== 'object';
@@ -191,18 +191,21 @@ export interface BlueprintFormMessages {
191
191
  /**
192
192
  * One field of a closed type. Which members apply to which type, value types, option membership,
193
193
  * ranges and the conditions' field references are checked by @texuli/forms/model
194
- * (validateFormSchema). Formats are names; a pattern is never accepted.
194
+ * (validateFormSchema). Formats are names; a pattern is never accepted. password and otp are
195
+ * credential fields that agent output may use only where the host allows them; custom is painted by
196
+ * the host for a kind it registered (refused otherwise).
195
197
  */
196
198
  export interface BlueprintFormField {
197
199
  /** The key in the submitted values, unique in the form. */
198
200
  name: string;
199
- type: 'text' | 'textarea' | 'number' | 'email' | 'url' | 'date' | 'time' | 'select' | 'multiselect' | 'checkbox' | 'radio' | 'switch' | 'slider' | 'file' | 'tags';
201
+ type: 'text' | 'textarea' | 'number' | 'email' | 'url' | 'date' | 'time' | 'select' | 'multiselect' | 'checkbox' | 'radio' | 'switch' | 'slider' | 'file' | 'tags' | 'password' | 'otp' | 'color' | 'datetime' | 'custom';
200
202
  label: string;
201
203
  description?: string;
202
204
  placeholder?: string;
203
205
  /**
204
- * A value of the field's type (a string, number, boolean, YYYY-MM-DD, HH:MM or a list of option
205
- * values). Not for file fields.
206
+ * A value of the field's type (a string, number, boolean, YYYY-MM-DD, HH:MM, YYYY-MM-DDTHH:MM, a
207
+ * #hex color, a list of option values, or any JSON value for custom). Not for file, password or
208
+ * otp fields.
206
209
  */
207
210
  default?: unknown;
208
211
  options?: readonly BlueprintFormOption[];
@@ -210,9 +213,15 @@ export interface BlueprintFormField {
210
213
  when?: BlueprintFormWhen;
211
214
  minLength?: number;
212
215
  maxLength?: number;
213
- /** A number for number and slider; YYYY-MM-DD for date; HH:MM for time. */
216
+ /**
217
+ * A number for number and slider; YYYY-MM-DD for date; HH:MM for time; YYYY-MM-DDTHH:MM for
218
+ * datetime.
219
+ */
214
220
  min?: unknown;
215
- /** A number for number and slider; YYYY-MM-DD for date; HH:MM for time. */
221
+ /**
222
+ * A number for number and slider; YYYY-MM-DD for date; HH:MM for time; YYYY-MM-DDTHH:MM for
223
+ * datetime.
224
+ */
216
225
  max?: unknown;
217
226
  /** Above 0, for number and slider. */
218
227
  step?: number;
@@ -230,9 +239,32 @@ export interface BlueprintFormField {
230
239
  accept?: readonly string[];
231
240
  /** Largest accepted file in bytes; the host uploads files. */
232
241
  maxSize?: number;
233
- /** An autocomplete token; payment, credential and one-time-code tokens are absent. */
234
- autocomplete?: 'off' | 'name' | 'given-name' | 'family-name' | 'nickname' | 'email' | 'tel' | 'organization' | 'organization-title' | 'street-address' | 'address-level1' | 'address-level2' | 'postal-code' | 'country-name' | 'language' | 'url' | 'bday';
242
+ /**
243
+ * An autocomplete token; payment and one-time-code tokens are absent, and current-password and
244
+ * new-password apply to password fields only.
245
+ */
246
+ autocomplete?: 'off' | 'name' | 'given-name' | 'family-name' | 'nickname' | 'email' | 'tel' | 'organization' | 'organization-title' | 'street-address' | 'address-level1' | 'address-level2' | 'postal-code' | 'country-name' | 'language' | 'url' | 'bday' | 'current-password' | 'new-password';
235
247
  link?: BlueprintFormLink;
248
+ /**
249
+ * Custom fields: the kind the host paints; agent output may name only a kind the host registered.
250
+ */
251
+ kind?: string;
252
+ /**
253
+ * Custom fields: at most 20 scalar settings under field-like names, handed to the host painter.
254
+ */
255
+ config?: Readonly<Record<string, unknown>>;
256
+ /** Otp fields: the number of digits (default 6). */
257
+ length?: number;
258
+ /**
259
+ * Url fields: the allowed schemes (default https only); an http answer passes the untrusted URL
260
+ * policy as its https twin.
261
+ */
262
+ protocols?: readonly ('http' | 'https')[];
263
+ /**
264
+ * Select fields: whether an empty choice clears the answer (default: while the field is not
265
+ * required).
266
+ */
267
+ clearable?: boolean;
236
268
  }
237
269
  /** A group of fields (a fieldset when titled). */
238
270
  export interface BlueprintFormSection {
@@ -407,7 +439,9 @@ export interface BlueprintEnvelopeMotion {
407
439
  }
408
440
  /**
409
441
  * A Texuli document (spec 0.7.0, ADR-011): the format, version, metadata, page settings, block list
410
- * and annotations of @texuli/document. This contract checks the envelope of the document only; the
442
+ * and annotations of @texuli/document, and from spec 0.10.0 the design system it follows
443
+ * (designSystem, the blueprint's $defs.designSystemReference; exactly one of ref and inline is
444
+ * @texuli/document/schema's check). This contract checks the envelope of the document only; the
411
445
  * closed block vocabulary, text safety, the URL policy and the caps are checked by
412
446
  * @texuli/document/schema (schemas/document.schema.json) inside the runtime validator,
413
447
  * @texuli/catalog's validateDraft, at the host's tier.
@@ -427,6 +461,7 @@ export interface BlueprintDocument {
427
461
  blocks: readonly Readonly<Record<string, unknown>>[];
428
462
  /** Comment threads, suggestions and named versions, kept apart from content. */
429
463
  annotations?: Readonly<Record<string, unknown>>;
464
+ designSystem?: BlueprintDesignSystemReference;
430
465
  }
431
466
  /**
432
467
  * One RFC 6902 operation on a document. Paths are RFC 6901 pointers into the document;
@@ -550,6 +585,18 @@ export interface BlueprintSourceBinding {
550
585
  query?: BlueprintSourceQuery;
551
586
  map?: Readonly<Record<string, BlueprintSourceFieldName>>;
552
587
  }
588
+ /**
589
+ * A design system (spec 0.10.0, ADR-021 and ADR-024): exactly one of ref (an exact id@x.y.z the
590
+ * host resolves, never a range, a URL or a path; or texuli:<theme>, a library theme) and inline (a
591
+ * whole texuli/design-system document, checked by checkDesignSystemReference from
592
+ * @texuli/spec/design-system against schemas/design-system.schema.json, at the untrusted level for
593
+ * agent output). The validator refuses an empty reference or both keys.
594
+ */
595
+ export interface BlueprintDesignSystemReference {
596
+ ref?: string;
597
+ /** A whole design-system document. */
598
+ inline?: Readonly<Record<string, unknown>>;
599
+ }
553
600
  /**
554
601
  * Envelope version 1 around one catalog tree (ADR-004, audit G13). Mirrors what
555
602
  * @texuli/catalog/envelope reads; a declared status, activation or tier is display data, never
@@ -562,8 +609,12 @@ export interface BlueprintSourceBinding {
562
609
  * ({"$source": name, "query": {...}}), with a closed query, and forms as data: the closed form
563
610
  * schema a `schema-form` node carries ($defs.formSchema) with conditions and cross-field rules in
564
611
  * the closed expression AST of @texuli/core/expression ($defs.expression). Spec 0.8.0 (ADR-012)
565
- * adds the canvas shape a `canvas` node carries ($defs.canvas). Planned minor additions: the native
566
- * profile and several roots.
612
+ * adds the canvas shape a `canvas` node carries ($defs.canvas). Spec 0.9.0 refreshes the catalog
613
+ * projection only (an additive minor; the envelope shape is unchanged). Spec 0.10.0 (ADR-021,
614
+ * ADR-024) adds the design-system document (schemas/design-system.schema.json) and the optional
615
+ * envelope designSystem ($defs.designSystemReference): the design system the whole blueprint
616
+ * follows, including a deck or document at its root (an additive minor). Planned minor additions:
617
+ * the native profile and several roots.
567
618
  */
568
619
  export interface BlueprintEnvelope {
569
620
  format: 'texuli-blueprint';
@@ -587,7 +638,7 @@ export interface BlueprintEnvelope {
587
638
  /**
588
639
  * The spec version the envelope was written for. Omitted means 0.2.0; migrateBlueprint stamps it.
589
640
  */
590
- spec?: '0.3.0' | '0.4.0' | '0.5.0' | '0.6.0' | '0.7.0' | '0.8.0';
641
+ spec?: '0.3.0' | '0.4.0' | '0.5.0' | '0.6.0' | '0.7.0' | '0.8.0' | '0.9.0' | '0.10.0';
591
642
  /**
592
643
  * Local state declarations (ADR-008), at most 64. Names, counts and per-type rules are checked by
593
644
  * the validator.
@@ -595,4 +646,10 @@ export interface BlueprintEnvelope {
595
646
  state?: Readonly<Record<string, BlueprintStateDeclaration>>;
596
647
  flow?: BlueprintFlow;
597
648
  motion?: BlueprintEnvelopeMotion;
649
+ /**
650
+ * The design system the blueprint follows (spec 0.10.0, ADR-024): a reference the host resolves
651
+ * or a whole inline document. Players apply it to their root scope; a reference nothing resolves,
652
+ * or a document the compiler refuses, paints with the default theme and a visible notice.
653
+ */
654
+ designSystem?: BlueprintDesignSystemReference;
598
655
  }
@@ -1,3 +1,3 @@
1
1
  // Generated by @texuli/spec from schemas/blueprint.schema.json. Do not edit: run the spec generator.
2
- // Spec 0.8.0, digest f6b6388280d247eb9c3106a925111f35b90d1ec3b59c23652542f06a9f103daf
2
+ // Spec 0.10.0, digest afcb9e3ed6b74b833a0f7477a657168621785ee98602ff582aa047e2d86aef17
3
3
  export {};
@@ -1,3 +1,3 @@
1
1
  // Generated by @texuli/spec from schemas/catalog.schema.json. Do not edit: run the spec generator.
2
- // Spec 0.8.0, digest f6b6388280d247eb9c3106a925111f35b90d1ec3b59c23652542f06a9f103daf
2
+ // Spec 0.10.0, digest afcb9e3ed6b74b833a0f7477a657168621785ee98602ff582aa047e2d86aef17
3
3
  export {};