@mildastudio/milda 0.1.1 → 0.2.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.
- package/README.md +16 -9
- package/dist/index.cjs +7849 -0
- package/dist/index.d.cts +1485 -0
- package/dist/index.d.mts +1227 -555
- package/dist/index.mjs +7728 -3568
- package/package.json +14 -9
- package/dist/index.d.ts +0 -813
- package/dist/index.js +0 -3660
package/dist/index.js
DELETED
|
@@ -1,3660 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// index.ts
|
|
21
|
-
var index_exports = {};
|
|
22
|
-
__export(index_exports, {
|
|
23
|
-
LexError: () => LexError,
|
|
24
|
-
TOKEN_VALUE_KIND: () => TOKEN_VALUE_KIND,
|
|
25
|
-
allTokensOfType: () => allTokensOfType,
|
|
26
|
-
archetypesByName: () => archetypesByName,
|
|
27
|
-
atomsByName: () => atomsByName,
|
|
28
|
-
collectActiveStates: () => collectActiveStates,
|
|
29
|
-
compositesByName: () => compositesByName,
|
|
30
|
-
conformance: () => conformance,
|
|
31
|
-
findToken: () => findToken,
|
|
32
|
-
lex: () => lex,
|
|
33
|
-
parseMildaDocument: () => parseMildaDocument,
|
|
34
|
-
preludeV0: () => preludeV0,
|
|
35
|
-
resolveFacets: () => resolveFacets,
|
|
36
|
-
resolveLayout: () => resolveLayout,
|
|
37
|
-
resolveToken: () => resolveToken,
|
|
38
|
-
validate: () => validate
|
|
39
|
-
});
|
|
40
|
-
module.exports = __toCommonJS(index_exports);
|
|
41
|
-
|
|
42
|
-
// types/tokens.ts
|
|
43
|
-
var TOKEN_VALUE_KIND = {
|
|
44
|
-
color: "color",
|
|
45
|
-
spacing: "dimension",
|
|
46
|
-
radius: "dimension",
|
|
47
|
-
size: "dimension",
|
|
48
|
-
shadow: "shadow",
|
|
49
|
-
typography: "typography",
|
|
50
|
-
fontFamily: "fontFamily",
|
|
51
|
-
fontWeight: "fontWeight",
|
|
52
|
-
duration: "duration",
|
|
53
|
-
opacity: "opacity",
|
|
54
|
-
blur: "blur",
|
|
55
|
-
tonal: "tonal",
|
|
56
|
-
border: "border",
|
|
57
|
-
elevation: "facet",
|
|
58
|
-
ring: "facet",
|
|
59
|
-
backdrop: "facet",
|
|
60
|
-
none: "facet"
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// prelude/preludeV0.ts
|
|
64
|
-
var atoms = [
|
|
65
|
-
{
|
|
66
|
-
kind: "atom",
|
|
67
|
-
name: "activation",
|
|
68
|
-
doc: "invoke an action + emit event/payload on a trigger",
|
|
69
|
-
params: [{ name: "payload" }],
|
|
70
|
-
states: [{ id: "ready", initial: true }],
|
|
71
|
-
events: [{ name: "trigger" }],
|
|
72
|
-
transitions: [
|
|
73
|
-
{
|
|
74
|
-
from: "ready",
|
|
75
|
-
on: "trigger",
|
|
76
|
-
when: "enabled",
|
|
77
|
-
to: "ready",
|
|
78
|
-
emit: { event: "invoke", arg: "payload" }
|
|
79
|
-
}
|
|
80
|
-
],
|
|
81
|
-
emits: [{ name: "invoke" }],
|
|
82
|
-
note: "transducer (single state); foundational atom under almost every interactive archetype"
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
kind: "atom",
|
|
86
|
-
name: "scheduler",
|
|
87
|
-
doc: "timed transition: auto-dismiss, auto-advance, debounce, delay",
|
|
88
|
-
modes: ["timeout", "debounce", "interval"],
|
|
89
|
-
params: [{ name: "t" }, { name: "mode" }],
|
|
90
|
-
states: [{ id: "idle", initial: true }, { id: "pending" }],
|
|
91
|
-
events: [{ name: "arm" }, { name: "cancel" }],
|
|
92
|
-
transitions: [
|
|
93
|
-
{ from: "idle", on: "arm", to: "pending" },
|
|
94
|
-
{ from: "pending", after: "t", to: "idle", emit: { event: "fire" } },
|
|
95
|
-
{ from: "pending", on: "arm", to: "pending", note: "restart (debounce)" },
|
|
96
|
-
{ from: "pending", on: "cancel", to: "idle" }
|
|
97
|
-
],
|
|
98
|
-
emits: [{ name: "fire" }],
|
|
99
|
-
note: "load-bearing: all time-based interactions compose from it"
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
kind: "atom",
|
|
103
|
-
name: "toggle",
|
|
104
|
-
doc: "flip a boolean (BooleanInput core; tristate \u2192 +mixed)",
|
|
105
|
-
params: [{ name: "v", bind: true }],
|
|
106
|
-
states: [{ id: "off", initial: true }, { id: "on" }],
|
|
107
|
-
events: [{ name: "toggle" }, { name: "set", args: ["b"] }],
|
|
108
|
-
transitions: [
|
|
109
|
-
{ from: "off", on: "toggle", to: "on", emit: { event: "change", arg: true } },
|
|
110
|
-
{ from: "on", on: "toggle", to: "off", emit: { event: "change", arg: false } },
|
|
111
|
-
{ from: "any", on: "set", to: { expr: "b?on:off" }, note: "controlled sync, no emit" }
|
|
112
|
-
],
|
|
113
|
-
exposes: [{ state: "checked", when: "state==on" }],
|
|
114
|
-
emits: [{ name: "change" }]
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
kind: "atom",
|
|
118
|
-
name: "selection",
|
|
119
|
-
doc: "choose member(s) of a collection",
|
|
120
|
-
modes: ["single", "multiple", "range", "none"],
|
|
121
|
-
params: [{ name: "C" }, { name: "selected", bind: true }],
|
|
122
|
-
context: [{ name: "sel", type: "C|none", init: "none" }],
|
|
123
|
-
states: [{ id: "empty", initial: true }, { id: "chosen" }],
|
|
124
|
-
events: [{ name: "select", args: ["x"] }, { name: "clear" }, { name: "set", args: ["x"] }],
|
|
125
|
-
transitions: [
|
|
126
|
-
{
|
|
127
|
-
from: "any",
|
|
128
|
-
on: "select",
|
|
129
|
-
when: "x\u2208C",
|
|
130
|
-
to: "chosen",
|
|
131
|
-
ctx: [{ target: "sel", expr: "x" }],
|
|
132
|
-
emit: { event: "change", arg: "x" }
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
from: "chosen",
|
|
136
|
-
on: "clear",
|
|
137
|
-
to: "empty",
|
|
138
|
-
ctx: [{ target: "sel", expr: "none" }],
|
|
139
|
-
emit: { event: "change", arg: "none" }
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
from: "any",
|
|
143
|
-
on: "set",
|
|
144
|
-
to: { expr: "x?chosen:empty" },
|
|
145
|
-
ctx: [{ target: "sel", expr: "x" }],
|
|
146
|
-
note: "controlled sync, no emit"
|
|
147
|
-
}
|
|
148
|
-
],
|
|
149
|
-
exposes: [{ state: "selected", arg: "x", when: "state==chosen \u2227 sel==x" }],
|
|
150
|
-
emits: [{ name: "change" }],
|
|
151
|
-
note: "multiple=set; range=anchor+extent (date range, shift-click, cell range)"
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
kind: "atom",
|
|
155
|
-
name: "browse",
|
|
156
|
-
doc: "move a cursor over a collection",
|
|
157
|
-
modes: ["roving", "spatial", "tree", "scroll-detent"],
|
|
158
|
-
requires: [{ anyOf: ["input.keyboard", "input.remote"], when: "mode\u2208{roving,tree}" }],
|
|
159
|
-
params: [{ name: "C" }, { name: "mode" }],
|
|
160
|
-
context: [{ name: "active", type: "C|none", init: "none" }],
|
|
161
|
-
events: [
|
|
162
|
-
{ name: "move", args: ["dir"] },
|
|
163
|
-
{ name: "to", args: ["x"] }
|
|
164
|
-
],
|
|
165
|
-
transitions: [
|
|
166
|
-
{
|
|
167
|
-
from: "any",
|
|
168
|
-
on: "move",
|
|
169
|
-
to: "any",
|
|
170
|
-
ctx: [{ target: "active", expr: "neighbor(active,dir,mode,wrap)" }],
|
|
171
|
-
emit: { event: "activeChanged", arg: "active" }
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
from: "any",
|
|
175
|
-
on: "to",
|
|
176
|
-
to: "any",
|
|
177
|
-
ctx: [{ target: "active", expr: "x" }],
|
|
178
|
-
emit: { event: "activeChanged", arg: "x" }
|
|
179
|
-
}
|
|
180
|
-
],
|
|
181
|
-
exposes: [{ state: "active", arg: "x", when: "active==x" }],
|
|
182
|
-
emits: [{ name: "activeChanged" }],
|
|
183
|
-
note: "neighbor(): roving=linear \xB7 spatial=2D \xB7 tree=in/out/sib \xB7 scroll-detent=nearest"
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
kind: "atom",
|
|
187
|
-
name: "presentation",
|
|
188
|
-
doc: "manage a surface",
|
|
189
|
-
modes: ["inline", "popover", "menu", "sheet", "dialog", "push", "wheel"],
|
|
190
|
-
params: [{ name: "surface" }],
|
|
191
|
-
states: [{ id: "closed", initial: true }, { id: "open" }],
|
|
192
|
-
events: [{ name: "open" }, { name: "close" }, { name: "toggle" }],
|
|
193
|
-
transitions: [
|
|
194
|
-
{ from: "closed", on: "toggle|open", to: "open", emit: { event: "opened" } },
|
|
195
|
-
{ from: "open", on: "toggle|close", to: "closed", emit: { event: "closed" } }
|
|
196
|
-
],
|
|
197
|
-
exposes: [{ state: "open", when: "state==open" }],
|
|
198
|
-
emits: [{ name: "opened" }, { name: "closed" }],
|
|
199
|
-
note: "surface is realization-only"
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
kind: "atom",
|
|
203
|
-
name: "dismiss",
|
|
204
|
-
doc: "close on a configured trigger",
|
|
205
|
-
params: [{ name: "when" }],
|
|
206
|
-
states: [{ id: "armed", initial: true }],
|
|
207
|
-
events: [{ name: "trigger" }],
|
|
208
|
-
transitions: [
|
|
209
|
-
{ from: "armed", on: "trigger", to: "armed", emit: { event: "dismissRequested" } }
|
|
210
|
-
],
|
|
211
|
-
emits: [{ name: "dismissRequested" }],
|
|
212
|
-
note: "trigger \u2208 configured [outside, escape, commit, swipe-down, timeout]"
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
kind: "atom",
|
|
216
|
-
name: "focus-scope",
|
|
217
|
-
doc: "trap / contain / restore focus within a region (modal a11y)",
|
|
218
|
-
requires: [{ anyOf: ["input.keyboard", "input.remote"] }],
|
|
219
|
-
states: [{ id: "inactive", initial: true }, { id: "active" }],
|
|
220
|
-
events: [{ name: "activate" }, { name: "deactivate" }],
|
|
221
|
-
transitions: [
|
|
222
|
-
{
|
|
223
|
-
from: "inactive",
|
|
224
|
-
on: "activate",
|
|
225
|
-
to: "active",
|
|
226
|
-
note: "save prior focus; move focus into region; trap Tab"
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
from: "active",
|
|
230
|
-
on: "deactivate",
|
|
231
|
-
to: "inactive",
|
|
232
|
-
note: "release trap; restore prior focus"
|
|
233
|
-
}
|
|
234
|
-
],
|
|
235
|
-
note: "no visual states (exposes none) \u2014 a behaviour guarantee, not a paint surface; realized as a focus trap on web, OS-owned on native sheets"
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
kind: "atom",
|
|
239
|
-
name: "value-entry",
|
|
240
|
-
doc: "free text/value entry; commit on change|blur|enter",
|
|
241
|
-
params: [{ name: "value", bind: true }, { name: "commit" }],
|
|
242
|
-
context: [
|
|
243
|
-
{ name: "value", type: "V" },
|
|
244
|
-
{ name: "draft", type: "V", init: "value" }
|
|
245
|
-
],
|
|
246
|
-
states: [{ id: "idle", initial: true }, { id: "editing" }],
|
|
247
|
-
events: [
|
|
248
|
-
{ name: "focus" },
|
|
249
|
-
{ name: "input", args: ["t"] },
|
|
250
|
-
{ name: "enter" },
|
|
251
|
-
{ name: "blur" },
|
|
252
|
-
{ name: "set", args: ["v"] }
|
|
253
|
-
],
|
|
254
|
-
transitions: [
|
|
255
|
-
{ from: "idle", on: "focus", to: "editing" },
|
|
256
|
-
{
|
|
257
|
-
from: "editing",
|
|
258
|
-
on: "input",
|
|
259
|
-
to: "editing",
|
|
260
|
-
ctx: [{ target: "draft", expr: "t" }],
|
|
261
|
-
when: "change\u2208commit",
|
|
262
|
-
emit: { event: "change", arg: "t" },
|
|
263
|
-
note: "value:=t only when change\u2208commit"
|
|
264
|
-
},
|
|
265
|
-
{
|
|
266
|
-
from: "editing",
|
|
267
|
-
on: "enter",
|
|
268
|
-
when: "enter\u2208commit",
|
|
269
|
-
to: "editing",
|
|
270
|
-
ctx: [{ target: "value", expr: "draft" }],
|
|
271
|
-
emit: { event: "change", arg: "draft" }
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
from: "editing",
|
|
275
|
-
on: "blur",
|
|
276
|
-
to: "idle",
|
|
277
|
-
note: "when blur\u2208commit: value:=draft, emit change(draft); else draft:=value"
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
from: "any",
|
|
281
|
-
on: "set",
|
|
282
|
-
to: "any",
|
|
283
|
-
ctx: [
|
|
284
|
-
{ target: "value", expr: "v" },
|
|
285
|
-
{ target: "draft", expr: "v" }
|
|
286
|
-
],
|
|
287
|
-
note: "controlled, no emit"
|
|
288
|
-
}
|
|
289
|
-
],
|
|
290
|
-
exposes: [
|
|
291
|
-
{ state: "focused", when: "state==editing" },
|
|
292
|
-
{ state: "empty", when: "value empty" },
|
|
293
|
-
{ state: "dirty", when: "draft\u2260value" }
|
|
294
|
-
],
|
|
295
|
-
emits: [{ name: "change" }]
|
|
296
|
-
},
|
|
297
|
-
{
|
|
298
|
-
kind: "atom",
|
|
299
|
-
name: "range",
|
|
300
|
-
doc: "value(s) along a min/max track (single/dual thumb)",
|
|
301
|
-
params: [
|
|
302
|
-
{ name: "value", bind: true },
|
|
303
|
-
{ name: "min" },
|
|
304
|
-
{ name: "max" },
|
|
305
|
-
{ name: "step" },
|
|
306
|
-
{ name: "thumbs" }
|
|
307
|
-
],
|
|
308
|
-
context: [
|
|
309
|
-
{ name: "value", type: "V|{lo,hi}" },
|
|
310
|
-
{ name: "active", type: "thumb|none", init: "none" }
|
|
311
|
-
],
|
|
312
|
-
states: [{ id: "idle", initial: true }, { id: "dragging" }],
|
|
313
|
-
events: [
|
|
314
|
-
{ name: "grab", args: ["t"] },
|
|
315
|
-
{ name: "to", args: ["v"] },
|
|
316
|
-
{ name: "release" },
|
|
317
|
-
{ name: "by", args: ["t", "d"] },
|
|
318
|
-
{ name: "set", args: ["v"] }
|
|
319
|
-
],
|
|
320
|
-
transitions: [
|
|
321
|
-
{ from: "idle", on: "grab", to: "dragging", ctx: [{ target: "active", expr: "t" }] },
|
|
322
|
-
{
|
|
323
|
-
from: "dragging",
|
|
324
|
-
on: "to",
|
|
325
|
-
to: "dragging",
|
|
326
|
-
ctx: [{ target: "value[active]", expr: "clamp+snap(v); lo\u2264hi" }],
|
|
327
|
-
emit: { event: "change", arg: "value" }
|
|
328
|
-
},
|
|
329
|
-
{ from: "dragging", on: "release", to: "idle", emit: { event: "commit", arg: "value" } },
|
|
330
|
-
{
|
|
331
|
-
from: "idle",
|
|
332
|
-
on: "by",
|
|
333
|
-
to: "idle",
|
|
334
|
-
ctx: [{ target: "value[t]", expr: "clamp(value[t]+d)" }],
|
|
335
|
-
emit: { event: "change", arg: "value" },
|
|
336
|
-
note: "keyboard"
|
|
337
|
-
},
|
|
338
|
-
{ from: "any", on: "set", to: "any", ctx: [{ target: "value", expr: "v" }] }
|
|
339
|
-
],
|
|
340
|
-
exposes: [{ state: "active", arg: "t", when: "state==dragging \u2227 active==t" }],
|
|
341
|
-
emits: [{ name: "change" }, { name: "commit" }],
|
|
342
|
-
note: "live (change) vs committed (commit)"
|
|
343
|
-
},
|
|
344
|
-
{
|
|
345
|
-
kind: "atom",
|
|
346
|
-
name: "stepping",
|
|
347
|
-
doc: "increment/decrement a value",
|
|
348
|
-
params: [{ name: "value", bind: true }, { name: "min" }, { name: "max" }, { name: "step" }],
|
|
349
|
-
context: [{ name: "value", type: "number" }],
|
|
350
|
-
events: [{ name: "inc" }, { name: "dec" }, { name: "set", args: ["v"] }],
|
|
351
|
-
transitions: [
|
|
352
|
-
{
|
|
353
|
-
from: "any",
|
|
354
|
-
on: "inc",
|
|
355
|
-
when: "value+step\u2264max",
|
|
356
|
-
to: "any",
|
|
357
|
-
ctx: [{ target: "value", expr: "value+step" }],
|
|
358
|
-
emit: { event: "change", arg: "value" }
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
from: "any",
|
|
362
|
-
on: "dec",
|
|
363
|
-
when: "value-step\u2265min",
|
|
364
|
-
to: "any",
|
|
365
|
-
ctx: [{ target: "value", expr: "value-step" }],
|
|
366
|
-
emit: { event: "change", arg: "value" }
|
|
367
|
-
},
|
|
368
|
-
{ from: "any", on: "set", to: "any", ctx: [{ target: "value", expr: "clamp(v)" }] }
|
|
369
|
-
],
|
|
370
|
-
exposes: [
|
|
371
|
-
{ state: "atMin", when: "value\u2264min" },
|
|
372
|
-
{ state: "atMax", when: "value\u2265max" }
|
|
373
|
-
],
|
|
374
|
-
emits: [{ name: "change" }]
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
kind: "atom",
|
|
378
|
-
name: "validation",
|
|
379
|
-
doc: "valid/invalid against rules (PURE \u2014 does not listen to input)",
|
|
380
|
-
params: [{ name: "value" }, { name: "rules" }],
|
|
381
|
-
context: [{ name: "errors", type: "list", init: "[]" }],
|
|
382
|
-
states: [{ id: "pristine", initial: true }, { id: "valid" }, { id: "invalid" }],
|
|
383
|
-
events: [{ name: "check" }, { name: "reset" }],
|
|
384
|
-
transitions: [
|
|
385
|
-
{
|
|
386
|
-
from: "any",
|
|
387
|
-
on: "check",
|
|
388
|
-
to: { expr: "eval(rules)?valid:invalid" },
|
|
389
|
-
ctx: [{ target: "errors", expr: "eval(rules).errors" }]
|
|
390
|
-
},
|
|
391
|
-
{ from: "any", on: "reset", to: "pristine", ctx: [{ target: "errors", expr: "[]" }] }
|
|
392
|
-
],
|
|
393
|
-
exposes: [
|
|
394
|
-
{ state: "invalid", when: "state==invalid" },
|
|
395
|
-
{ state: "valid", when: "state==valid" },
|
|
396
|
-
{ state: "pristine", when: "state==pristine" }
|
|
397
|
-
],
|
|
398
|
-
emits: [{ name: "validity" }],
|
|
399
|
-
note: "the when:change|blur|submit policy = a wire from value-entry/submission into check"
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
kind: "atom",
|
|
403
|
-
name: "submission",
|
|
404
|
-
doc: "form-level orchestration: collective validation, submit, focus-first-error",
|
|
405
|
-
params: [{ name: "fields" }],
|
|
406
|
-
context: [{ name: "fields", type: "set<validation>" }],
|
|
407
|
-
states: [{ id: "editing", initial: true }, { id: "submitting" }, { id: "submitted" }],
|
|
408
|
-
events: [{ name: "submit" }, { name: "success" }, { name: "failure", args: ["e"] }],
|
|
409
|
-
transitions: [
|
|
410
|
-
{
|
|
411
|
-
from: "editing",
|
|
412
|
-
on: "submit",
|
|
413
|
-
to: "editing",
|
|
414
|
-
do: "broadcast fields.check",
|
|
415
|
-
note: "when allValid \u2192 submitting emit submitStart(values); else \u2192 editing do firstInvalid.focus"
|
|
416
|
-
},
|
|
417
|
-
{ from: "submitting", on: "success", to: "submitted", emit: { event: "submitSuccess" } },
|
|
418
|
-
{
|
|
419
|
-
from: "submitting",
|
|
420
|
-
on: "failure",
|
|
421
|
-
to: "editing",
|
|
422
|
-
emit: { event: "submitError", arg: "e" }
|
|
423
|
-
}
|
|
424
|
-
],
|
|
425
|
-
exposes: [
|
|
426
|
-
{ state: "loading", when: "state==submitting" },
|
|
427
|
-
{ state: "dirty", when: "any(fields,f\u2192f.dirty)" }
|
|
428
|
-
],
|
|
429
|
-
emits: [{ name: "submitStart" }, { name: "submitSuccess" }, { name: "submitError" }],
|
|
430
|
-
note: "fields: DYNAMIC set of validation machines; FAN-OUT broadcast + FAN-IN aggregate/select"
|
|
431
|
-
},
|
|
432
|
-
{
|
|
433
|
-
kind: "atom",
|
|
434
|
-
name: "gesture",
|
|
435
|
-
doc: "swipe | long-press | pinch | pan | drag | drop",
|
|
436
|
-
modes: ["swipe", "long-press", "pinch", "pan", "drag", "drop"],
|
|
437
|
-
requires: [{ anyOf: ["input.touch"], when: "per gesture" }],
|
|
438
|
-
params: [{ name: "kind" }, { name: "thresholds" }],
|
|
439
|
-
context: [{ name: "start" }, { name: "current" }, { name: "t0" }],
|
|
440
|
-
states: [{ id: "idle", initial: true }, { id: "tracking" }, { id: "recognized" }],
|
|
441
|
-
events: [{ name: "down", args: ["p"] }, { name: "move", args: ["p"] }, { name: "up" }],
|
|
442
|
-
transitions: [
|
|
443
|
-
{ from: "idle", on: "down", to: "tracking", ctx: [{ target: "start", expr: "p" }] },
|
|
444
|
-
{
|
|
445
|
-
from: "tracking",
|
|
446
|
-
on: "move",
|
|
447
|
-
to: "tracking",
|
|
448
|
-
ctx: [{ target: "current", expr: "p" }],
|
|
449
|
-
when: "exceeds(\u03B8)",
|
|
450
|
-
emit: { event: "begin" },
|
|
451
|
-
note: "recognized when threshold exceeded"
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
from: "tracking",
|
|
455
|
-
after: "t",
|
|
456
|
-
to: "recognized",
|
|
457
|
-
emit: { event: "begin" },
|
|
458
|
-
note: "long-press"
|
|
459
|
-
},
|
|
460
|
-
{ from: "tracking", on: "up", to: "idle", note: "canceled, threshold not met" },
|
|
461
|
-
{ from: "recognized", on: "move", to: "recognized", emit: { event: "update", arg: "delta" } },
|
|
462
|
-
{ from: "recognized", on: "up", to: "idle", emit: { event: "end" } }
|
|
463
|
-
],
|
|
464
|
-
exposes: [{ state: "dragging", when: "state==recognized" }],
|
|
465
|
-
emits: [{ name: "begin" }, { name: "update" }, { name: "end" }],
|
|
466
|
-
note: "load-bearing: all drag-based interactions compose from it"
|
|
467
|
-
},
|
|
468
|
-
{
|
|
469
|
-
kind: "atom",
|
|
470
|
-
name: "async-state",
|
|
471
|
-
doc: "idle | loading | success | error lifecycle",
|
|
472
|
-
states: [{ id: "idle", initial: true }, { id: "loading" }, { id: "success" }, { id: "error" }],
|
|
473
|
-
events: [{ name: "start" }, { name: "resolve", args: ["v"] }, { name: "reject", args: ["e"] }],
|
|
474
|
-
transitions: [
|
|
475
|
-
{ from: "any", on: "start", to: "loading", do: "run" },
|
|
476
|
-
{ from: "loading", on: "resolve", to: "success", emit: { event: "success", arg: "v" } },
|
|
477
|
-
{ from: "loading", on: "reject", to: "error", emit: { event: "error", arg: "e" } }
|
|
478
|
-
],
|
|
479
|
-
exposes: [
|
|
480
|
-
{ state: "loading", when: "state==loading" },
|
|
481
|
-
{ state: "success", when: "state==success" },
|
|
482
|
-
{ state: "error", when: "state==error" }
|
|
483
|
-
],
|
|
484
|
-
emits: [{ name: "success" }, { name: "error" }]
|
|
485
|
-
},
|
|
486
|
-
{
|
|
487
|
-
kind: "atom",
|
|
488
|
-
name: "navigation",
|
|
489
|
-
doc: "switch between destinations (carries a history stack)",
|
|
490
|
-
params: [{ name: "dests" }],
|
|
491
|
-
context: [{ name: "stack", type: "list", init: "[root]" }],
|
|
492
|
-
events: [{ name: "go", args: ["d"] }, { name: "back" }],
|
|
493
|
-
transitions: [
|
|
494
|
-
{
|
|
495
|
-
from: "any",
|
|
496
|
-
on: "go",
|
|
497
|
-
to: "any",
|
|
498
|
-
ctx: [{ target: "stack", expr: "stack.push(d)" }],
|
|
499
|
-
emit: { event: "navigate", arg: "current" }
|
|
500
|
-
},
|
|
501
|
-
{
|
|
502
|
-
from: "any",
|
|
503
|
-
on: "back",
|
|
504
|
-
when: "|stack|>1",
|
|
505
|
-
to: "any",
|
|
506
|
-
ctx: [{ target: "stack", expr: "stack.pop()" }],
|
|
507
|
-
emit: { event: "navigate", arg: "current" }
|
|
508
|
-
}
|
|
509
|
-
],
|
|
510
|
-
exposes: [{ state: "current", arg: "d", when: "current==d" }],
|
|
511
|
-
emits: [{ name: "navigate" }],
|
|
512
|
-
note: "history stack shared with undo/redo, wizard-back; richer than selection.single + activation"
|
|
513
|
-
},
|
|
514
|
-
{
|
|
515
|
-
kind: "atom",
|
|
516
|
-
name: "scroll",
|
|
517
|
-
doc: "observe scroll position: infinite-load, scroll-spy, reveal-on-scroll, parallax",
|
|
518
|
-
params: [{ name: "region" }],
|
|
519
|
-
context: [{ name: "pos", init: "0" }],
|
|
520
|
-
events: [{ name: "scrolled", args: ["p"] }],
|
|
521
|
-
transitions: [
|
|
522
|
-
{
|
|
523
|
-
from: "any",
|
|
524
|
-
on: "scrolled",
|
|
525
|
-
to: "any",
|
|
526
|
-
ctx: [{ target: "pos", expr: "p" }],
|
|
527
|
-
emit: { event: "atEnd", arg: "p" },
|
|
528
|
-
note: "emit atEnd when p\u2265max, nearEnd when p\u2265max-\u03B8"
|
|
529
|
-
}
|
|
530
|
-
],
|
|
531
|
-
exposes: [
|
|
532
|
-
{ state: "atStart", when: "pos\u22640" },
|
|
533
|
-
{ state: "atEnd", when: "pos\u2265max" },
|
|
534
|
-
{ state: "scrolled", when: "pos>0" }
|
|
535
|
-
],
|
|
536
|
-
emits: [{ name: "atEnd" }, { name: "nearEnd" }],
|
|
537
|
-
note: "atEnd \u2192 wire \u2192 async-state.start = infinite-load"
|
|
538
|
-
},
|
|
539
|
-
{
|
|
540
|
-
kind: "atom",
|
|
541
|
-
name: "progress",
|
|
542
|
-
doc: "determinate / indeterminate completion",
|
|
543
|
-
modes: ["determinate", "indeterminate"],
|
|
544
|
-
params: [{ name: "value" }, { name: "max" }],
|
|
545
|
-
events: [{ name: "set", args: ["v"] }],
|
|
546
|
-
transitions: [
|
|
547
|
-
{
|
|
548
|
-
from: "any",
|
|
549
|
-
on: "set",
|
|
550
|
-
to: "any",
|
|
551
|
-
ctx: [{ target: "value", expr: "v" }],
|
|
552
|
-
emit: { event: "change", arg: "value" }
|
|
553
|
-
}
|
|
554
|
-
],
|
|
555
|
-
exposes: [{ state: "complete", when: "value\u2265max" }],
|
|
556
|
-
emits: [{ name: "change" }]
|
|
557
|
-
},
|
|
558
|
-
{
|
|
559
|
-
kind: "atom",
|
|
560
|
-
name: "liveness",
|
|
561
|
-
doc: "announce dynamic changes (polite/assertive)",
|
|
562
|
-
modes: ["polite", "assertive"],
|
|
563
|
-
requires: [{ anyOf: ["a11y.screenreader"] }],
|
|
564
|
-
events: [{ name: "announce", args: ["msg"] }],
|
|
565
|
-
transitions: [
|
|
566
|
-
{ from: "any", on: "announce", to: "any", emit: { event: "announced", arg: "msg" } }
|
|
567
|
-
],
|
|
568
|
-
emits: [{ name: "announced" }]
|
|
569
|
-
}
|
|
570
|
-
];
|
|
571
|
-
var composites = [
|
|
572
|
-
{
|
|
573
|
-
kind: "composite",
|
|
574
|
-
name: "disclosure",
|
|
575
|
-
builtFrom: ["toggle", "presentation"],
|
|
576
|
-
uses: [
|
|
577
|
-
{ atom: "toggle", as: "T" },
|
|
578
|
-
{ atom: "presentation", as: "P", params: { surface: "inline" } }
|
|
579
|
-
],
|
|
580
|
-
composition: [{ kind: "wire", from: "T.change", to: "P.toggle" }],
|
|
581
|
-
emits: [{ name: "toggle" }],
|
|
582
|
-
note: "\u2248 toggle(expanded) + presentation(inline); exposes expanded"
|
|
583
|
-
},
|
|
584
|
-
{
|
|
585
|
-
kind: "composite",
|
|
586
|
-
name: "typeahead",
|
|
587
|
-
builtFrom: ["scheduler", "browse"],
|
|
588
|
-
uses: [
|
|
589
|
-
{ atom: "scheduler", as: "S", params: { mode: "debounce" } },
|
|
590
|
-
{ atom: "browse", as: "B" }
|
|
591
|
-
],
|
|
592
|
-
note: "jump to a member by typed prefix; requires input.keyboard"
|
|
593
|
-
},
|
|
594
|
-
{
|
|
595
|
-
kind: "composite",
|
|
596
|
-
name: "dnd",
|
|
597
|
-
builtFrom: ["gesture", "ordering"],
|
|
598
|
-
note: "drag-and-drop move/reorder across containers; ordering is a non-behavior (collection mutation)"
|
|
599
|
-
},
|
|
600
|
-
{
|
|
601
|
-
kind: "composite",
|
|
602
|
-
name: "resize",
|
|
603
|
-
builtFrom: ["gesture"],
|
|
604
|
-
uses: [{ atom: "gesture", as: "G", params: { kind: "drag" } }],
|
|
605
|
-
note: "drag-resize a region/panel; requires input.pointer"
|
|
606
|
-
},
|
|
607
|
-
{
|
|
608
|
-
kind: "composite",
|
|
609
|
-
name: "auto-dismiss",
|
|
610
|
-
builtFrom: ["scheduler"],
|
|
611
|
-
uses: [{ atom: "scheduler", as: "S", params: { mode: "timeout" } }],
|
|
612
|
-
composition: [{ kind: "wire", from: "S.fire", to: "dismissRequested" }],
|
|
613
|
-
note: "self-close after a timeout (toast)"
|
|
614
|
-
},
|
|
615
|
-
{
|
|
616
|
-
kind: "composite",
|
|
617
|
-
name: "stepping-nav",
|
|
618
|
-
builtFrom: ["navigation"],
|
|
619
|
-
emits: [{ name: "stepChange" }, { name: "complete" }],
|
|
620
|
-
note: "\u2248 navigation(ordered) + constraint; ordered progress through steps; exposes active, complete"
|
|
621
|
-
},
|
|
622
|
-
{
|
|
623
|
-
kind: "composite",
|
|
624
|
-
name: "pressability",
|
|
625
|
-
builtFrom: ["gesture"],
|
|
626
|
-
note: "press/hover/active feedback (ripple, highlight); contributes pressed/hovered feedback states ONLY \u2014 does not fire the action (that is activation)"
|
|
627
|
-
}
|
|
628
|
-
];
|
|
629
|
-
var archetypes = [
|
|
630
|
-
{
|
|
631
|
-
kind: "archetype",
|
|
632
|
-
name: "Button",
|
|
633
|
-
group: "Actions",
|
|
634
|
-
contract: "label/icon, fires action",
|
|
635
|
-
produces: { level: "phrase", interactive: true },
|
|
636
|
-
anatomy: [
|
|
637
|
-
{ name: "icon", role: "icon", optional: true },
|
|
638
|
-
{ name: "label", role: "text" }
|
|
639
|
-
],
|
|
640
|
-
composes: ["activation", "pressability", "async-state?"],
|
|
641
|
-
realizationNote: "Material FAB/elevated/tonal; iOS .borderedProminent"
|
|
642
|
-
},
|
|
643
|
-
{
|
|
644
|
-
kind: "archetype",
|
|
645
|
-
name: "ToggleButton",
|
|
646
|
-
group: "Actions",
|
|
647
|
-
contract: "on/off action",
|
|
648
|
-
produces: { level: "phrase", interactive: true },
|
|
649
|
-
anatomy: [
|
|
650
|
-
{ name: "indicator", role: "icon", optional: true },
|
|
651
|
-
{ name: "label", role: "text" }
|
|
652
|
-
],
|
|
653
|
-
composes: ["toggle", "pressability"]
|
|
654
|
-
},
|
|
655
|
-
{
|
|
656
|
-
kind: "archetype",
|
|
657
|
-
name: "MenuButton",
|
|
658
|
-
group: "Actions",
|
|
659
|
-
contract: "opens a menu of actions",
|
|
660
|
-
produces: { level: "region", interactive: false },
|
|
661
|
-
anatomy: [
|
|
662
|
-
{
|
|
663
|
-
name: "trigger",
|
|
664
|
-
role: "control",
|
|
665
|
-
children: [
|
|
666
|
-
{ name: "label", role: "text" },
|
|
667
|
-
{ name: "indicator", role: "icon", optional: true }
|
|
668
|
-
]
|
|
669
|
-
},
|
|
670
|
-
{
|
|
671
|
-
name: "surface",
|
|
672
|
-
role: "container",
|
|
673
|
-
children: [{ name: "item", role: "item", children: [{ name: "label", role: "text" }] }]
|
|
674
|
-
}
|
|
675
|
-
],
|
|
676
|
-
composes: ["presentation", "browse", "typeahead?", "dismiss"]
|
|
677
|
-
},
|
|
678
|
-
{
|
|
679
|
-
kind: "archetype",
|
|
680
|
-
name: "Link",
|
|
681
|
-
group: "Actions",
|
|
682
|
-
contract: "navigates",
|
|
683
|
-
produces: { level: "phrase", interactive: true },
|
|
684
|
-
anatomy: [
|
|
685
|
-
{ name: "label", role: "text" },
|
|
686
|
-
{ name: "icon", role: "icon", optional: true }
|
|
687
|
-
],
|
|
688
|
-
composes: ["activation", "pressability"]
|
|
689
|
-
},
|
|
690
|
-
{
|
|
691
|
-
kind: "archetype",
|
|
692
|
-
name: "Form",
|
|
693
|
-
group: "Inputs",
|
|
694
|
-
contract: "a set of fields, one submit",
|
|
695
|
-
produces: { level: "region", interactive: false },
|
|
696
|
-
anatomy: [
|
|
697
|
-
{ name: "content", role: "container" },
|
|
698
|
-
{ name: "submit", role: "control", children: [{ name: "label", role: "text" }] },
|
|
699
|
-
{ name: "error", role: "text", optional: true }
|
|
700
|
-
],
|
|
701
|
-
composes: ["submission"]
|
|
702
|
-
},
|
|
703
|
-
{
|
|
704
|
-
kind: "archetype",
|
|
705
|
-
name: "Field",
|
|
706
|
-
group: "Inputs",
|
|
707
|
-
contract: "label + control + error/help wrapper",
|
|
708
|
-
produces: { level: "region", interactive: false },
|
|
709
|
-
anatomy: [
|
|
710
|
-
{ name: "label", role: "text" },
|
|
711
|
-
{ name: "control", role: "container" },
|
|
712
|
-
{ name: "help", role: "text", optional: true },
|
|
713
|
-
{ name: "error", role: "text", optional: true }
|
|
714
|
-
],
|
|
715
|
-
composes: ["validation"]
|
|
716
|
-
},
|
|
717
|
-
{
|
|
718
|
-
kind: "archetype",
|
|
719
|
-
name: "TextField",
|
|
720
|
-
group: "Inputs",
|
|
721
|
-
contract: "string value",
|
|
722
|
-
produces: { level: "region", interactive: false },
|
|
723
|
-
anatomy: [
|
|
724
|
-
{ name: "icon", role: "icon", optional: true },
|
|
725
|
-
{ name: "control", role: "input" }
|
|
726
|
-
],
|
|
727
|
-
composes: ["value-entry", "validation?"]
|
|
728
|
-
},
|
|
729
|
-
{
|
|
730
|
-
kind: "archetype",
|
|
731
|
-
name: "TextArea",
|
|
732
|
-
group: "Inputs",
|
|
733
|
-
contract: "multiline string value",
|
|
734
|
-
produces: { level: "region", interactive: false },
|
|
735
|
-
anatomy: [{ name: "control", role: "input" }],
|
|
736
|
-
composes: ["value-entry", "validation?"]
|
|
737
|
-
},
|
|
738
|
-
{
|
|
739
|
-
kind: "archetype",
|
|
740
|
-
name: "SearchField",
|
|
741
|
-
group: "Inputs",
|
|
742
|
-
contract: "query string + clear",
|
|
743
|
-
produces: { level: "region", interactive: false },
|
|
744
|
-
anatomy: [
|
|
745
|
-
{ name: "icon", role: "icon", optional: true },
|
|
746
|
-
{ name: "control", role: "input" },
|
|
747
|
-
{ name: "clear", role: "control", optional: true }
|
|
748
|
-
],
|
|
749
|
-
composes: ["value-entry", "filter?"]
|
|
750
|
-
},
|
|
751
|
-
{
|
|
752
|
-
kind: "archetype",
|
|
753
|
-
name: "BooleanInput",
|
|
754
|
-
group: "Inputs",
|
|
755
|
-
contract: "one bool",
|
|
756
|
-
produces: { level: "region", interactive: false },
|
|
757
|
-
anatomy: [
|
|
758
|
-
{ name: "control", role: "input" },
|
|
759
|
-
{ name: "indicator", role: "icon" },
|
|
760
|
-
{ name: "label", role: "text", optional: true }
|
|
761
|
-
],
|
|
762
|
-
composes: ["toggle"],
|
|
763
|
-
realizationNote: "iOS: checkbox \u2192 switch (no indeterminate)"
|
|
764
|
-
},
|
|
765
|
-
{
|
|
766
|
-
kind: "archetype",
|
|
767
|
-
name: "ChoiceGroup",
|
|
768
|
-
group: "Inputs",
|
|
769
|
-
contract: "one-of-N",
|
|
770
|
-
produces: { level: "region", interactive: false },
|
|
771
|
-
anatomy: [
|
|
772
|
-
{
|
|
773
|
-
name: "option",
|
|
774
|
-
role: "item",
|
|
775
|
-
children: [
|
|
776
|
-
{ name: "indicator", role: "icon" },
|
|
777
|
-
{ name: "label", role: "text" }
|
|
778
|
-
]
|
|
779
|
-
}
|
|
780
|
-
],
|
|
781
|
-
composes: ["selection.single", "browse"],
|
|
782
|
-
realizationNote: "radio stack | segmented (node.group); iOS: radio \u2192 wheel/segmented"
|
|
783
|
-
},
|
|
784
|
-
{
|
|
785
|
-
kind: "archetype",
|
|
786
|
-
name: "SingleSelect",
|
|
787
|
-
group: "Inputs",
|
|
788
|
-
contract: "one of a known set",
|
|
789
|
-
produces: { level: "region", interactive: false },
|
|
790
|
-
anatomy: [
|
|
791
|
-
{
|
|
792
|
-
name: "trigger",
|
|
793
|
-
role: "control",
|
|
794
|
-
children: [
|
|
795
|
-
{ name: "value", role: "text" },
|
|
796
|
-
{ name: "indicator", role: "icon" }
|
|
797
|
-
]
|
|
798
|
-
},
|
|
799
|
-
{
|
|
800
|
-
name: "surface",
|
|
801
|
-
role: "container",
|
|
802
|
-
children: [
|
|
803
|
-
{
|
|
804
|
-
name: "option",
|
|
805
|
-
role: "item",
|
|
806
|
-
children: [
|
|
807
|
-
{ name: "check", role: "icon", optional: true },
|
|
808
|
-
{ name: "label", role: "text" }
|
|
809
|
-
]
|
|
810
|
-
}
|
|
811
|
-
]
|
|
812
|
-
}
|
|
813
|
-
],
|
|
814
|
-
composes: ["selection.single", "presentation", "browse", "dismiss", "typeahead?"],
|
|
815
|
-
uses: [
|
|
816
|
-
{ atom: "activation", as: "A" },
|
|
817
|
-
{ atom: "presentation", as: "P", params: { surface: "popover" } },
|
|
818
|
-
{ atom: "selection", as: "S", params: { mode: "single", over: "options" } },
|
|
819
|
-
{ atom: "browse", as: "B", params: { over: "options", mode: "roving" } },
|
|
820
|
-
{ atom: "dismiss", as: "D", params: { when: "[outside,escape]" } }
|
|
821
|
-
],
|
|
822
|
-
composition: [
|
|
823
|
-
{ kind: "wire", from: "A.invoke", to: "P.toggle" },
|
|
824
|
-
{ kind: "wire", from: "P.opened", to: "B.to", arg: "S.sel ?? first" },
|
|
825
|
-
{ kind: "gate", machine: "D", activeWhen: "P.open" },
|
|
826
|
-
{ kind: "wire", from: "D.dismissRequested", to: "P.close" },
|
|
827
|
-
{ kind: "wire", from: "option-activate", to: "S.select", arg: "B.active" },
|
|
828
|
-
{ kind: "wire", from: "S.change", to: "P.close", when: "closeOnSelect" }
|
|
829
|
-
],
|
|
830
|
-
exposes: [
|
|
831
|
-
{ state: "open", when: "P.open" },
|
|
832
|
-
{ state: "selected", when: "S.selected" },
|
|
833
|
-
{ state: "active", when: "B.active" }
|
|
834
|
-
],
|
|
835
|
-
emits: [{ name: "change", from: "S.change" }],
|
|
836
|
-
realize: [
|
|
837
|
-
{
|
|
838
|
-
target: "web",
|
|
839
|
-
surface: "popover",
|
|
840
|
-
browse: "roving",
|
|
841
|
-
commit: "explicit",
|
|
842
|
-
note: "place bottom-start; flip; matchWidth; commit on click/enter"
|
|
843
|
-
},
|
|
844
|
-
{
|
|
845
|
-
target: "ios",
|
|
846
|
-
when: "style=wheel",
|
|
847
|
-
surface: "inline-wheel",
|
|
848
|
-
browse: "scroll-detent",
|
|
849
|
-
commit: "continuous",
|
|
850
|
-
drop: ["activation", "dismiss"],
|
|
851
|
-
wire: [{ from: "B.active-changed", to: "S.select", arg: "B.active" }]
|
|
852
|
-
}
|
|
853
|
-
],
|
|
854
|
-
realizationNote: "web popover-listbox \xB7 Android dropdown/dialog \xB7 iOS menu/sheet/push/wheel"
|
|
855
|
-
},
|
|
856
|
-
{
|
|
857
|
-
kind: "archetype",
|
|
858
|
-
name: "MultiSelect",
|
|
859
|
-
group: "Inputs",
|
|
860
|
-
contract: "subset of a set",
|
|
861
|
-
produces: { level: "region", interactive: false },
|
|
862
|
-
anatomy: [
|
|
863
|
-
{
|
|
864
|
-
name: "trigger",
|
|
865
|
-
role: "control",
|
|
866
|
-
children: [
|
|
867
|
-
{ name: "value", role: "text" },
|
|
868
|
-
{ name: "indicator", role: "icon" }
|
|
869
|
-
]
|
|
870
|
-
},
|
|
871
|
-
{
|
|
872
|
-
name: "surface",
|
|
873
|
-
role: "container",
|
|
874
|
-
children: [
|
|
875
|
-
{
|
|
876
|
-
name: "option",
|
|
877
|
-
role: "item",
|
|
878
|
-
children: [
|
|
879
|
-
{ name: "check", role: "icon" },
|
|
880
|
-
{ name: "label", role: "text" }
|
|
881
|
-
]
|
|
882
|
-
}
|
|
883
|
-
]
|
|
884
|
-
}
|
|
885
|
-
],
|
|
886
|
-
composes: ["selection.multiple", "presentation", "browse", "dismiss"]
|
|
887
|
-
},
|
|
888
|
-
{
|
|
889
|
-
kind: "archetype",
|
|
890
|
-
name: "Combobox",
|
|
891
|
-
group: "Inputs",
|
|
892
|
-
contract: "filtered single-select w/ entry",
|
|
893
|
-
produces: { level: "region", interactive: false },
|
|
894
|
-
anatomy: [
|
|
895
|
-
{ name: "control", role: "input" },
|
|
896
|
-
{
|
|
897
|
-
name: "surface",
|
|
898
|
-
role: "container",
|
|
899
|
-
children: [{ name: "option", role: "item", children: [{ name: "label", role: "text" }] }]
|
|
900
|
-
}
|
|
901
|
-
],
|
|
902
|
-
composes: ["value-entry", "filter", "selection.single", "browse", "presentation", "dismiss"]
|
|
903
|
-
},
|
|
904
|
-
{
|
|
905
|
-
kind: "archetype",
|
|
906
|
-
name: "Slider",
|
|
907
|
-
group: "Inputs",
|
|
908
|
-
contract: "value(s) on a range",
|
|
909
|
-
produces: { level: "region", interactive: false },
|
|
910
|
-
anatomy: [
|
|
911
|
-
{
|
|
912
|
-
name: "track",
|
|
913
|
-
role: "container",
|
|
914
|
-
children: [
|
|
915
|
-
{ name: "range", role: "container", optional: true },
|
|
916
|
-
{ name: "thumb", role: "control" }
|
|
917
|
-
]
|
|
918
|
-
},
|
|
919
|
-
{ name: "value", role: "text", optional: true }
|
|
920
|
-
],
|
|
921
|
-
composes: ["range", "gesture(pan)"]
|
|
922
|
-
},
|
|
923
|
-
{
|
|
924
|
-
kind: "archetype",
|
|
925
|
-
name: "Stepper",
|
|
926
|
-
group: "Inputs",
|
|
927
|
-
contract: "number \xB1",
|
|
928
|
-
produces: { level: "region", interactive: false },
|
|
929
|
-
anatomy: [
|
|
930
|
-
{ name: "decrement", role: "control", children: [{ name: "icon", role: "icon" }] },
|
|
931
|
-
{ name: "control", role: "input" },
|
|
932
|
-
{ name: "increment", role: "control", children: [{ name: "icon", role: "icon" }] }
|
|
933
|
-
],
|
|
934
|
-
composes: ["stepping", "value-entry"]
|
|
935
|
-
},
|
|
936
|
-
{
|
|
937
|
-
kind: "archetype",
|
|
938
|
-
name: "DatePicker",
|
|
939
|
-
group: "Inputs",
|
|
940
|
-
contract: "date value",
|
|
941
|
-
produces: { level: "region", interactive: false },
|
|
942
|
-
anatomy: [
|
|
943
|
-
{
|
|
944
|
-
name: "trigger",
|
|
945
|
-
role: "control",
|
|
946
|
-
children: [
|
|
947
|
-
{ name: "value", role: "text" },
|
|
948
|
-
{ name: "icon", role: "icon" }
|
|
949
|
-
]
|
|
950
|
-
},
|
|
951
|
-
{
|
|
952
|
-
name: "surface",
|
|
953
|
-
role: "container",
|
|
954
|
-
children: [
|
|
955
|
-
{
|
|
956
|
-
name: "header",
|
|
957
|
-
role: "container",
|
|
958
|
-
children: [
|
|
959
|
-
{ name: "prev", role: "control" },
|
|
960
|
-
{ name: "title", role: "text" },
|
|
961
|
-
{ name: "next", role: "control" }
|
|
962
|
-
]
|
|
963
|
-
},
|
|
964
|
-
{
|
|
965
|
-
name: "grid",
|
|
966
|
-
role: "container",
|
|
967
|
-
children: [{ name: "day", role: "item", children: [{ name: "label", role: "text" }] }]
|
|
968
|
-
}
|
|
969
|
-
]
|
|
970
|
-
}
|
|
971
|
-
],
|
|
972
|
-
composes: ["selection.single", "presentation", "browse", "dismiss", "masking"],
|
|
973
|
-
// Behavior is the shared framework-agnostic calendar core (proposal 0029,
|
|
974
|
-
// targets/behavior/calendar.ts); node.calendar picks single|range + weekStartsOn
|
|
975
|
-
// + disabledWeekdays. The `day` grid cell carries calendar states (today,
|
|
976
|
-
// outsideMonth, selected, rangeStart, rangeEnd, inRange) rendered as data-* attrs.
|
|
977
|
-
realizationNote: "web calendar-grid (single|range via node.calendar) \xB7 iOS wheel (momentum+detent+haptic) \xB7 Android dialog"
|
|
978
|
-
},
|
|
979
|
-
{
|
|
980
|
-
kind: "archetype",
|
|
981
|
-
name: "TimePicker",
|
|
982
|
-
group: "Inputs",
|
|
983
|
-
contract: "time value",
|
|
984
|
-
produces: { level: "region", interactive: false },
|
|
985
|
-
anatomy: [
|
|
986
|
-
{
|
|
987
|
-
name: "trigger",
|
|
988
|
-
role: "control",
|
|
989
|
-
children: [
|
|
990
|
-
{ name: "value", role: "text" },
|
|
991
|
-
{ name: "icon", role: "icon" }
|
|
992
|
-
]
|
|
993
|
-
},
|
|
994
|
-
{
|
|
995
|
-
name: "surface",
|
|
996
|
-
role: "container",
|
|
997
|
-
children: [
|
|
998
|
-
{
|
|
999
|
-
name: "column",
|
|
1000
|
-
role: "item",
|
|
1001
|
-
children: [
|
|
1002
|
-
{ name: "option", role: "item", children: [{ name: "label", role: "text" }] }
|
|
1003
|
-
]
|
|
1004
|
-
}
|
|
1005
|
-
]
|
|
1006
|
-
}
|
|
1007
|
-
],
|
|
1008
|
-
composes: ["selection.single", "presentation", "browse", "dismiss", "masking"],
|
|
1009
|
-
// Behavior is the shared framework-agnostic time core (proposal 0029,
|
|
1010
|
-
// targets/behavior/timepicker.ts); node.timePicker picks the slot step + 12/24h.
|
|
1011
|
-
// Web realization = a listbox of time slots; iOS wheel is a per-platform view.
|
|
1012
|
-
realizationNote: "web time-slot listbox (step via node.timePicker) \xB7 iOS wheel \xB7 Android dialog"
|
|
1013
|
-
},
|
|
1014
|
-
{
|
|
1015
|
-
kind: "archetype",
|
|
1016
|
-
name: "ColorPicker",
|
|
1017
|
-
group: "Inputs",
|
|
1018
|
-
contract: "color value",
|
|
1019
|
-
produces: { level: "region", interactive: false },
|
|
1020
|
-
anatomy: [
|
|
1021
|
-
{
|
|
1022
|
-
name: "trigger",
|
|
1023
|
-
role: "control",
|
|
1024
|
-
children: [
|
|
1025
|
-
{ name: "swatch", role: "icon" },
|
|
1026
|
-
{ name: "value", role: "text" }
|
|
1027
|
-
]
|
|
1028
|
-
},
|
|
1029
|
-
{
|
|
1030
|
-
name: "surface",
|
|
1031
|
-
role: "container",
|
|
1032
|
-
children: [
|
|
1033
|
-
{ name: "area", role: "container" },
|
|
1034
|
-
{ name: "hue", role: "container" },
|
|
1035
|
-
{ name: "alpha", role: "container" },
|
|
1036
|
-
{ name: "eyedropper", role: "control" },
|
|
1037
|
-
{ name: "input", role: "input" },
|
|
1038
|
-
{
|
|
1039
|
-
name: "swatches",
|
|
1040
|
-
role: "container",
|
|
1041
|
-
children: [{ name: "swatch", role: "item" }]
|
|
1042
|
-
}
|
|
1043
|
-
]
|
|
1044
|
-
}
|
|
1045
|
-
],
|
|
1046
|
-
composes: ["range", "value-entry", "presentation", "dismiss"],
|
|
1047
|
-
// Behavior is the shared framework-agnostic color core (proposal 0029/0035,
|
|
1048
|
-
// targets/behavior/color.ts): HSV<->RGB<->hex + OKLCH math and the area/hue/alpha
|
|
1049
|
-
// geometry, so every target computes an identical picker. node.colorPicker (0035)
|
|
1050
|
-
// picks the working space (srgb|oklch), the editable format tabs (hex/rgb/hsl/hsb),
|
|
1051
|
-
// opacity, eyedropper, preset swatches, and popover-vs-inline layout. The `alpha`,
|
|
1052
|
-
// `eyedropper` and `swatches` parts render only when their config flag is on.
|
|
1053
|
-
realizationNote: "web: SV/OKLCH area + hue + optional alpha, format tabs, eyedropper, swatches, popover|inline \xB7 native color input fallback"
|
|
1054
|
-
},
|
|
1055
|
-
{
|
|
1056
|
-
kind: "archetype",
|
|
1057
|
-
name: "FileUpload",
|
|
1058
|
-
group: "Inputs",
|
|
1059
|
-
contract: "file(s)",
|
|
1060
|
-
produces: { level: "region", interactive: false },
|
|
1061
|
-
anatomy: [
|
|
1062
|
-
{
|
|
1063
|
-
name: "dropzone",
|
|
1064
|
-
role: "container",
|
|
1065
|
-
children: [
|
|
1066
|
-
{ name: "icon", role: "icon" },
|
|
1067
|
-
{ name: "label", role: "text" }
|
|
1068
|
-
]
|
|
1069
|
-
},
|
|
1070
|
-
{ name: "input", role: "input" },
|
|
1071
|
-
{
|
|
1072
|
-
name: "list",
|
|
1073
|
-
role: "container",
|
|
1074
|
-
optional: true,
|
|
1075
|
-
children: [
|
|
1076
|
-
{
|
|
1077
|
-
name: "item",
|
|
1078
|
-
role: "item",
|
|
1079
|
-
children: [
|
|
1080
|
-
{ name: "name", role: "text" },
|
|
1081
|
-
{ name: "remove", role: "control" }
|
|
1082
|
-
]
|
|
1083
|
-
}
|
|
1084
|
-
]
|
|
1085
|
-
}
|
|
1086
|
-
],
|
|
1087
|
-
composes: ["activation", "gesture(drop)?", "async-state"]
|
|
1088
|
-
},
|
|
1089
|
-
{
|
|
1090
|
-
kind: "archetype",
|
|
1091
|
-
name: "TagsInput",
|
|
1092
|
-
group: "Inputs",
|
|
1093
|
-
contract: "list of tokens",
|
|
1094
|
-
produces: { level: "region", interactive: false },
|
|
1095
|
-
anatomy: [
|
|
1096
|
-
{
|
|
1097
|
-
name: "tag",
|
|
1098
|
-
role: "item",
|
|
1099
|
-
children: [
|
|
1100
|
-
{ name: "label", role: "text" },
|
|
1101
|
-
{ name: "remove", role: "control" }
|
|
1102
|
-
]
|
|
1103
|
-
},
|
|
1104
|
-
{ name: "control", role: "input" }
|
|
1105
|
-
],
|
|
1106
|
-
composes: ["value-entry", "selection.multiple"]
|
|
1107
|
-
},
|
|
1108
|
-
{
|
|
1109
|
-
kind: "archetype",
|
|
1110
|
-
name: "Rating",
|
|
1111
|
-
group: "Inputs",
|
|
1112
|
-
contract: "discrete score",
|
|
1113
|
-
produces: { level: "region", interactive: false },
|
|
1114
|
-
anatomy: [{ name: "item", role: "item", children: [{ name: "icon", role: "icon" }] }],
|
|
1115
|
-
composes: ["selection.single", "pressability"]
|
|
1116
|
-
},
|
|
1117
|
-
{
|
|
1118
|
-
kind: "archetype",
|
|
1119
|
-
name: "Disclosure",
|
|
1120
|
-
group: "Disclosure",
|
|
1121
|
-
contract: "expand region (a.k.a. Collapsible)",
|
|
1122
|
-
produces: { level: "region", interactive: false },
|
|
1123
|
-
anatomy: [
|
|
1124
|
-
{
|
|
1125
|
-
name: "trigger",
|
|
1126
|
-
role: "control",
|
|
1127
|
-
children: [
|
|
1128
|
-
{ name: "label", role: "text" },
|
|
1129
|
-
{ name: "indicator", role: "icon" }
|
|
1130
|
-
]
|
|
1131
|
-
},
|
|
1132
|
-
{ name: "panel", role: "container" }
|
|
1133
|
-
],
|
|
1134
|
-
composes: ["disclosure"]
|
|
1135
|
-
},
|
|
1136
|
-
{
|
|
1137
|
-
kind: "archetype",
|
|
1138
|
-
name: "Accordion",
|
|
1139
|
-
group: "Disclosure",
|
|
1140
|
-
contract: "group of disclosures (single/multi)",
|
|
1141
|
-
produces: { level: "region", interactive: false },
|
|
1142
|
-
anatomy: [
|
|
1143
|
-
{
|
|
1144
|
-
name: "item",
|
|
1145
|
-
role: "item",
|
|
1146
|
-
children: [
|
|
1147
|
-
{
|
|
1148
|
-
name: "header",
|
|
1149
|
-
role: "control",
|
|
1150
|
-
children: [
|
|
1151
|
-
{ name: "label", role: "text" },
|
|
1152
|
-
{ name: "indicator", role: "icon" }
|
|
1153
|
-
]
|
|
1154
|
-
},
|
|
1155
|
-
{ name: "panel", role: "container" }
|
|
1156
|
-
]
|
|
1157
|
-
}
|
|
1158
|
-
],
|
|
1159
|
-
composes: ["disclosure", "selection"]
|
|
1160
|
-
},
|
|
1161
|
-
{
|
|
1162
|
-
kind: "archetype",
|
|
1163
|
-
name: "Tabs",
|
|
1164
|
-
group: "Disclosure",
|
|
1165
|
-
contract: "switch panels",
|
|
1166
|
-
produces: { level: "region", interactive: false },
|
|
1167
|
-
anatomy: [
|
|
1168
|
-
{
|
|
1169
|
-
name: "list",
|
|
1170
|
-
role: "container",
|
|
1171
|
-
children: [{ name: "tab", role: "control", children: [{ name: "label", role: "text" }] }]
|
|
1172
|
-
},
|
|
1173
|
-
{ name: "panel", role: "container" }
|
|
1174
|
-
],
|
|
1175
|
-
composes: ["navigation", "browse"],
|
|
1176
|
-
realizationNote: "mobile: swipe-between; web: no swipe"
|
|
1177
|
-
},
|
|
1178
|
-
{
|
|
1179
|
-
kind: "archetype",
|
|
1180
|
-
name: "Dialog",
|
|
1181
|
-
group: "Disclosure",
|
|
1182
|
-
contract: "blocking layer (a.k.a. Modal)",
|
|
1183
|
-
produces: { level: "region", interactive: false },
|
|
1184
|
-
anatomy: [
|
|
1185
|
-
{ name: "trigger", role: "control", children: [{ name: "label", role: "text" }] },
|
|
1186
|
-
{ name: "backdrop", role: "container", optional: true },
|
|
1187
|
-
{
|
|
1188
|
-
name: "surface",
|
|
1189
|
-
role: "container",
|
|
1190
|
-
children: [
|
|
1191
|
-
{ name: "title", role: "text" },
|
|
1192
|
-
{ name: "content", role: "container" },
|
|
1193
|
-
{ name: "close", role: "control", optional: true }
|
|
1194
|
-
]
|
|
1195
|
-
}
|
|
1196
|
-
],
|
|
1197
|
-
composes: ["presentation(dialog)", "focus-scope", "dismiss", "backdrop"],
|
|
1198
|
-
realizationNote: "iOS sheet w/ detents"
|
|
1199
|
-
},
|
|
1200
|
-
{
|
|
1201
|
-
kind: "archetype",
|
|
1202
|
-
name: "Drawer",
|
|
1203
|
-
group: "Disclosure",
|
|
1204
|
-
contract: "edge-anchored panel (a.k.a. Sidebar)",
|
|
1205
|
-
produces: { level: "region", interactive: false },
|
|
1206
|
-
anatomy: [
|
|
1207
|
-
{ name: "trigger", role: "control" },
|
|
1208
|
-
{ name: "backdrop", role: "container", optional: true },
|
|
1209
|
-
{
|
|
1210
|
-
name: "surface",
|
|
1211
|
-
role: "container",
|
|
1212
|
-
children: [
|
|
1213
|
-
{ name: "content", role: "container" },
|
|
1214
|
-
{ name: "close", role: "control", optional: true }
|
|
1215
|
-
]
|
|
1216
|
-
}
|
|
1217
|
-
],
|
|
1218
|
-
composes: ["presentation(sheet)", "focus-scope", "dismiss", "gesture(swipe)?"]
|
|
1219
|
-
},
|
|
1220
|
-
{
|
|
1221
|
-
kind: "archetype",
|
|
1222
|
-
name: "Popover",
|
|
1223
|
-
group: "Disclosure",
|
|
1224
|
-
contract: "anchored transient layer",
|
|
1225
|
-
produces: { level: "region", interactive: false },
|
|
1226
|
-
anatomy: [
|
|
1227
|
-
{ name: "trigger", role: "control" },
|
|
1228
|
-
{ name: "surface", role: "container", children: [{ name: "content", role: "container" }] }
|
|
1229
|
-
],
|
|
1230
|
-
composes: ["presentation(popover)", "anchoring", "dismiss"]
|
|
1231
|
-
},
|
|
1232
|
-
{
|
|
1233
|
-
kind: "archetype",
|
|
1234
|
-
name: "Tooltip",
|
|
1235
|
-
group: "Disclosure",
|
|
1236
|
-
contract: "hover/focus hint",
|
|
1237
|
-
produces: { level: "region", interactive: false },
|
|
1238
|
-
anatomy: [
|
|
1239
|
-
{ name: "trigger", role: "control" },
|
|
1240
|
-
{ name: "surface", role: "container", children: [{ name: "content", role: "text" }] }
|
|
1241
|
-
],
|
|
1242
|
-
composes: ["presentation(popover)", "anchoring"],
|
|
1243
|
-
realizationNote: "requires input.pointer \u2192 tap/long-press fallback"
|
|
1244
|
-
},
|
|
1245
|
-
{
|
|
1246
|
-
kind: "archetype",
|
|
1247
|
-
name: "ContextMenu",
|
|
1248
|
-
group: "Disclosure",
|
|
1249
|
-
contract: "secondary-action menu",
|
|
1250
|
-
produces: { level: "region", interactive: false },
|
|
1251
|
-
anatomy: [
|
|
1252
|
-
{
|
|
1253
|
-
name: "surface",
|
|
1254
|
-
role: "container",
|
|
1255
|
-
children: [
|
|
1256
|
-
{
|
|
1257
|
-
name: "item",
|
|
1258
|
-
role: "item",
|
|
1259
|
-
children: [
|
|
1260
|
-
{ name: "icon", role: "icon", optional: true },
|
|
1261
|
-
{ name: "label", role: "text" }
|
|
1262
|
-
]
|
|
1263
|
-
}
|
|
1264
|
-
]
|
|
1265
|
-
}
|
|
1266
|
-
],
|
|
1267
|
-
composes: ["presentation(menu)", "browse", "dismiss"],
|
|
1268
|
-
realizationNote: "right-click / long-press"
|
|
1269
|
-
},
|
|
1270
|
-
{
|
|
1271
|
-
kind: "archetype",
|
|
1272
|
-
name: "Menu",
|
|
1273
|
-
group: "Navigation",
|
|
1274
|
-
contract: "hierarchical actions (a.k.a. MenuBar)",
|
|
1275
|
-
produces: { level: "region", interactive: false },
|
|
1276
|
-
anatomy: [
|
|
1277
|
-
{
|
|
1278
|
-
name: "surface",
|
|
1279
|
-
role: "container",
|
|
1280
|
-
children: [
|
|
1281
|
-
{
|
|
1282
|
-
name: "item",
|
|
1283
|
-
role: "item",
|
|
1284
|
-
children: [
|
|
1285
|
-
{ name: "icon", role: "icon", optional: true },
|
|
1286
|
-
{ name: "label", role: "text" }
|
|
1287
|
-
]
|
|
1288
|
-
}
|
|
1289
|
-
]
|
|
1290
|
-
}
|
|
1291
|
-
],
|
|
1292
|
-
composes: ["presentation", "browse", "typeahead?", "dismiss"]
|
|
1293
|
-
},
|
|
1294
|
-
{
|
|
1295
|
-
kind: "archetype",
|
|
1296
|
-
name: "NavigationBar",
|
|
1297
|
-
group: "Navigation",
|
|
1298
|
-
contract: "top-level destinations",
|
|
1299
|
-
produces: { level: "region", interactive: false },
|
|
1300
|
-
anatomy: [
|
|
1301
|
-
{
|
|
1302
|
-
name: "item",
|
|
1303
|
-
role: "item",
|
|
1304
|
-
children: [
|
|
1305
|
-
{ name: "icon", role: "icon", optional: true },
|
|
1306
|
-
{ name: "label", role: "text" }
|
|
1307
|
-
]
|
|
1308
|
-
}
|
|
1309
|
-
],
|
|
1310
|
-
composes: ["navigation"],
|
|
1311
|
-
realizationNote: "web top-nav \xB7 iOS tab-bar \xB7 Android bottom-nav/rail"
|
|
1312
|
-
},
|
|
1313
|
-
{
|
|
1314
|
-
kind: "archetype",
|
|
1315
|
-
name: "Breadcrumbs",
|
|
1316
|
-
group: "Navigation",
|
|
1317
|
-
contract: "path trail",
|
|
1318
|
-
produces: { level: "region", interactive: false },
|
|
1319
|
-
anatomy: [
|
|
1320
|
-
{ name: "item", role: "item", children: [{ name: "label", role: "text" }] },
|
|
1321
|
-
{ name: "separator", role: "icon", optional: true }
|
|
1322
|
-
],
|
|
1323
|
-
composes: ["navigation"],
|
|
1324
|
-
realizationNote: "iOS: \u2192 nav title + back"
|
|
1325
|
-
},
|
|
1326
|
-
{
|
|
1327
|
-
kind: "archetype",
|
|
1328
|
-
name: "Pagination",
|
|
1329
|
-
group: "Navigation",
|
|
1330
|
-
contract: "page index",
|
|
1331
|
-
produces: { level: "region", interactive: false },
|
|
1332
|
-
anatomy: [
|
|
1333
|
-
{ name: "previous", role: "control" },
|
|
1334
|
-
{ name: "item", role: "item", children: [{ name: "label", role: "text" }] },
|
|
1335
|
-
{ name: "next", role: "control" }
|
|
1336
|
-
],
|
|
1337
|
-
composes: ["navigation", "stepping"]
|
|
1338
|
-
},
|
|
1339
|
-
{
|
|
1340
|
-
kind: "archetype",
|
|
1341
|
-
name: "Tree",
|
|
1342
|
-
group: "Navigation",
|
|
1343
|
-
contract: "hierarchical collection",
|
|
1344
|
-
produces: { level: "region", interactive: false },
|
|
1345
|
-
anatomy: [
|
|
1346
|
-
{
|
|
1347
|
-
name: "item",
|
|
1348
|
-
role: "item",
|
|
1349
|
-
children: [
|
|
1350
|
-
{ name: "indicator", role: "icon" },
|
|
1351
|
-
{ name: "label", role: "text" }
|
|
1352
|
-
]
|
|
1353
|
-
}
|
|
1354
|
-
],
|
|
1355
|
-
composes: ["disclosure", "browse", "selection"]
|
|
1356
|
-
},
|
|
1357
|
-
{
|
|
1358
|
-
kind: "archetype",
|
|
1359
|
-
name: "Wizard",
|
|
1360
|
-
group: "Navigation",
|
|
1361
|
-
contract: "ordered flow (a.k.a. Stepper-nav)",
|
|
1362
|
-
produces: { level: "region", interactive: false },
|
|
1363
|
-
anatomy: [
|
|
1364
|
-
{
|
|
1365
|
-
name: "step",
|
|
1366
|
-
role: "item",
|
|
1367
|
-
children: [
|
|
1368
|
-
{ name: "indicator", role: "icon" },
|
|
1369
|
-
{ name: "label", role: "text" }
|
|
1370
|
-
]
|
|
1371
|
-
},
|
|
1372
|
-
{ name: "content", role: "container" },
|
|
1373
|
-
{ name: "back", role: "control" },
|
|
1374
|
-
{ name: "next", role: "control" }
|
|
1375
|
-
],
|
|
1376
|
-
composes: ["stepping-nav"]
|
|
1377
|
-
},
|
|
1378
|
-
{
|
|
1379
|
-
kind: "archetype",
|
|
1380
|
-
name: "List",
|
|
1381
|
-
group: "Collections",
|
|
1382
|
-
contract: "ordered items, optional select (a.k.a. ListBox)",
|
|
1383
|
-
produces: { level: "region", interactive: false },
|
|
1384
|
-
anatomy: [{ name: "item", role: "item", children: [{ name: "content", role: "container" }] }],
|
|
1385
|
-
composes: ["browse", "selection?", "virtualization?"],
|
|
1386
|
-
realizationNote: "iOS list: swipe-to-delete (gesture)"
|
|
1387
|
-
},
|
|
1388
|
-
{
|
|
1389
|
-
kind: "archetype",
|
|
1390
|
-
name: "Table",
|
|
1391
|
-
group: "Collections",
|
|
1392
|
-
contract: "rows\xD7cols, sort/select (a.k.a. DataGrid)",
|
|
1393
|
-
produces: { level: "region", interactive: false },
|
|
1394
|
-
anatomy: [
|
|
1395
|
-
{
|
|
1396
|
-
name: "header",
|
|
1397
|
-
role: "container",
|
|
1398
|
-
children: [{ name: "column", role: "item", children: [{ name: "label", role: "text" }] }]
|
|
1399
|
-
},
|
|
1400
|
-
{
|
|
1401
|
-
name: "body",
|
|
1402
|
-
role: "container",
|
|
1403
|
-
children: [{ name: "row", role: "item", children: [{ name: "cell", role: "item" }] }]
|
|
1404
|
-
}
|
|
1405
|
-
],
|
|
1406
|
-
composes: ["selection?", "ordering", "browse", "resize?", "virtualization?"],
|
|
1407
|
-
realizationNote: "aggregation/grouping is data, not a behavior"
|
|
1408
|
-
},
|
|
1409
|
-
{
|
|
1410
|
-
kind: "archetype",
|
|
1411
|
-
name: "Carousel",
|
|
1412
|
-
group: "Collections",
|
|
1413
|
-
contract: "paged horizontal items",
|
|
1414
|
-
produces: { level: "region", interactive: false },
|
|
1415
|
-
anatomy: [
|
|
1416
|
-
{ name: "viewport", role: "container", children: [{ name: "slide", role: "item" }] },
|
|
1417
|
-
{ name: "previous", role: "control", optional: true },
|
|
1418
|
-
{ name: "next", role: "control", optional: true }
|
|
1419
|
-
],
|
|
1420
|
-
composes: ["browse", "momentum-scroll", "gesture(swipe)"]
|
|
1421
|
-
},
|
|
1422
|
-
{
|
|
1423
|
-
kind: "archetype",
|
|
1424
|
-
name: "Grid",
|
|
1425
|
-
group: "Collections",
|
|
1426
|
-
contract: "2D arrangement of items",
|
|
1427
|
-
produces: { level: "region", interactive: false },
|
|
1428
|
-
anatomy: [{ name: "item", role: "item" }],
|
|
1429
|
-
composes: ["browse", "selection?"]
|
|
1430
|
-
},
|
|
1431
|
-
{
|
|
1432
|
-
kind: "archetype",
|
|
1433
|
-
name: "Toast",
|
|
1434
|
-
group: "Feedback",
|
|
1435
|
-
contract: "transient message (a.k.a. Snackbar)",
|
|
1436
|
-
produces: { level: "region", interactive: false },
|
|
1437
|
-
anatomy: [
|
|
1438
|
-
{ name: "icon", role: "icon", optional: true },
|
|
1439
|
-
{ name: "content", role: "text" },
|
|
1440
|
-
{ name: "action", role: "control", optional: true },
|
|
1441
|
-
{ name: "close", role: "control", optional: true }
|
|
1442
|
-
],
|
|
1443
|
-
composes: ["presentation", "auto-dismiss", "liveness"],
|
|
1444
|
-
realizationNote: "iOS: top banner; Android: snackbar bottom"
|
|
1445
|
-
},
|
|
1446
|
-
{
|
|
1447
|
-
kind: "archetype",
|
|
1448
|
-
name: "Alert",
|
|
1449
|
-
group: "Feedback",
|
|
1450
|
-
contract: "inline persistent message (a.k.a. Banner)",
|
|
1451
|
-
produces: { level: "region", interactive: false },
|
|
1452
|
-
anatomy: [
|
|
1453
|
-
{ name: "icon", role: "icon", optional: true },
|
|
1454
|
-
{ name: "title", role: "text", optional: true },
|
|
1455
|
-
{ name: "content", role: "text" },
|
|
1456
|
-
{ name: "close", role: "control", optional: true }
|
|
1457
|
-
],
|
|
1458
|
-
composes: ["liveness"]
|
|
1459
|
-
},
|
|
1460
|
-
{
|
|
1461
|
-
kind: "archetype",
|
|
1462
|
-
name: "ProgressBar",
|
|
1463
|
-
group: "Feedback",
|
|
1464
|
-
contract: "completion feedback (a.k.a. Spinner)",
|
|
1465
|
-
produces: { level: "region", interactive: false },
|
|
1466
|
-
anatomy: [
|
|
1467
|
-
{ name: "track", role: "container", children: [{ name: "indicator", role: "container" }] },
|
|
1468
|
-
{ name: "label", role: "text", optional: true }
|
|
1469
|
-
],
|
|
1470
|
-
composes: ["progress", "liveness"]
|
|
1471
|
-
},
|
|
1472
|
-
{
|
|
1473
|
-
kind: "archetype",
|
|
1474
|
-
name: "Skeleton",
|
|
1475
|
-
group: "Feedback",
|
|
1476
|
-
contract: "loading placeholder",
|
|
1477
|
-
produces: { level: "region", interactive: false },
|
|
1478
|
-
composes: ["async-state"]
|
|
1479
|
-
},
|
|
1480
|
-
{
|
|
1481
|
-
kind: "archetype",
|
|
1482
|
-
name: "Badge",
|
|
1483
|
-
group: "Feedback",
|
|
1484
|
-
contract: "numeric/status decoration",
|
|
1485
|
-
produces: { level: "phrase", interactive: false },
|
|
1486
|
-
anatomy: [{ name: "label", role: "text" }],
|
|
1487
|
-
composes: []
|
|
1488
|
-
},
|
|
1489
|
-
{
|
|
1490
|
-
kind: "archetype",
|
|
1491
|
-
name: "Meter",
|
|
1492
|
-
group: "Feedback",
|
|
1493
|
-
contract: "numeric gauge",
|
|
1494
|
-
produces: { level: "region", interactive: false },
|
|
1495
|
-
anatomy: [
|
|
1496
|
-
{ name: "track", role: "container", children: [{ name: "indicator", role: "container" }] },
|
|
1497
|
-
{ name: "label", role: "text", optional: true }
|
|
1498
|
-
],
|
|
1499
|
-
composes: []
|
|
1500
|
-
},
|
|
1501
|
-
{
|
|
1502
|
-
kind: "archetype",
|
|
1503
|
-
name: "EmptyState",
|
|
1504
|
-
group: "Feedback",
|
|
1505
|
-
contract: "no-content placeholder",
|
|
1506
|
-
produces: { level: "region", interactive: false },
|
|
1507
|
-
anatomy: [
|
|
1508
|
-
{ name: "icon", role: "icon", optional: true },
|
|
1509
|
-
{ name: "title", role: "text" },
|
|
1510
|
-
{ name: "description", role: "text", optional: true },
|
|
1511
|
-
{ name: "action", role: "control", optional: true }
|
|
1512
|
-
],
|
|
1513
|
-
composes: []
|
|
1514
|
-
},
|
|
1515
|
-
{
|
|
1516
|
-
kind: "archetype",
|
|
1517
|
-
name: "Avatar",
|
|
1518
|
-
group: "Media",
|
|
1519
|
-
contract: "identity image/initials",
|
|
1520
|
-
produces: { level: "phrase", interactive: false },
|
|
1521
|
-
anatomy: [
|
|
1522
|
-
{ name: "image", role: "content" },
|
|
1523
|
-
{ name: "fallback", role: "text", optional: true },
|
|
1524
|
-
{ name: "badge", role: "icon", optional: true }
|
|
1525
|
-
],
|
|
1526
|
-
composes: []
|
|
1527
|
-
},
|
|
1528
|
-
{
|
|
1529
|
-
kind: "archetype",
|
|
1530
|
-
name: "Media",
|
|
1531
|
-
group: "Media",
|
|
1532
|
-
contract: "image/video/audio/embed w/ alt + fit (proposal 0028)",
|
|
1533
|
-
produces: { level: "phrase", interactive: false },
|
|
1534
|
-
anatomy: [{ name: "media", role: "content" }],
|
|
1535
|
-
composes: []
|
|
1536
|
-
},
|
|
1537
|
-
{
|
|
1538
|
-
kind: "archetype",
|
|
1539
|
-
name: "Icon",
|
|
1540
|
-
group: "Media",
|
|
1541
|
-
contract: "vector glyph from a set",
|
|
1542
|
-
produces: { level: "phrase", interactive: false },
|
|
1543
|
-
composes: []
|
|
1544
|
-
},
|
|
1545
|
-
{
|
|
1546
|
-
kind: "archetype",
|
|
1547
|
-
name: "AspectRatio",
|
|
1548
|
-
group: "Media",
|
|
1549
|
-
contract: "ratio-locked box",
|
|
1550
|
-
produces: { level: "region", interactive: false },
|
|
1551
|
-
anatomy: [{ name: "content", role: "container" }],
|
|
1552
|
-
composes: [],
|
|
1553
|
-
realizationNote: "layout facet, not a behavioral archetype"
|
|
1554
|
-
}
|
|
1555
|
-
];
|
|
1556
|
-
var facets = [
|
|
1557
|
-
{ name: "fill", covers: "surface/background" },
|
|
1558
|
-
{ name: "ink", covers: "text + icon foreground" },
|
|
1559
|
-
{
|
|
1560
|
-
name: "border",
|
|
1561
|
-
covers: "edge: color + width (hairline|thin|thick) + style",
|
|
1562
|
-
divergence: "iOS often overlay-stroke"
|
|
1563
|
-
},
|
|
1564
|
-
{ name: "border.top", covers: "top edge (physical side)" },
|
|
1565
|
-
{ name: "border.right", covers: "right edge (physical side)" },
|
|
1566
|
-
{ name: "border.bottom", covers: "bottom edge (physical side)" },
|
|
1567
|
-
{ name: "border.left", covers: "left edge (physical side)" },
|
|
1568
|
-
{ name: "border.inline-start", covers: "leading edge (logical, writing-direction)" },
|
|
1569
|
-
{ name: "border.inline-end", covers: "trailing edge (logical, writing-direction)" },
|
|
1570
|
-
{ name: "border.block-start", covers: "block-start edge (logical)" },
|
|
1571
|
-
{ name: "border.block-end", covers: "block-end edge (logical)" },
|
|
1572
|
-
{ name: "corner", covers: "corner radius (token role)" },
|
|
1573
|
-
{ name: "inset", covers: "internal padding (block/inline)" },
|
|
1574
|
-
{ name: "gap", covers: "spacing between children" },
|
|
1575
|
-
{
|
|
1576
|
-
name: "text",
|
|
1577
|
-
covers: "typography role (body|caption|title\u2026) \u2014 bundles size/weight/line/tracking",
|
|
1578
|
-
divergence: "iOS Dynamic Type = OS-resolved role"
|
|
1579
|
-
},
|
|
1580
|
-
{
|
|
1581
|
-
name: "textClamp",
|
|
1582
|
-
covers: "truncate to N lines + ellipsis",
|
|
1583
|
-
divergence: "web -webkit-line-clamp \xB7 .lineLimit \xB7 maxLines+overflow"
|
|
1584
|
-
},
|
|
1585
|
-
{ name: "textWrap", covers: "wrap | nowrap | break" },
|
|
1586
|
-
{
|
|
1587
|
-
name: "weight",
|
|
1588
|
-
covers: "font-weight (typography axis \xA73, split out of the `text` role bundle)",
|
|
1589
|
-
divergence: "web font-weight \xB7 .fontWeight \xB7 TextStyle.fontWeight"
|
|
1590
|
-
},
|
|
1591
|
-
{
|
|
1592
|
-
name: "font",
|
|
1593
|
-
covers: "font-family (typography axis \xA73)",
|
|
1594
|
-
divergence: "web font-family \xB7 UIFont \xB7 Typeface"
|
|
1595
|
-
},
|
|
1596
|
-
{ name: "slant", covers: "font-style: normal | italic | oblique (typography axis \xA73)" },
|
|
1597
|
-
{
|
|
1598
|
-
name: "elevation",
|
|
1599
|
-
covers: "depth",
|
|
1600
|
-
divergence: "web box-shadow \xB7 Android tonal-elevation \xB7 iOS none/sheet"
|
|
1601
|
-
},
|
|
1602
|
-
{
|
|
1603
|
-
name: "overlay",
|
|
1604
|
-
covers: "state layer (Material hover/press tint)",
|
|
1605
|
-
divergence: "Material-specific; web \u2248 bg shift"
|
|
1606
|
-
},
|
|
1607
|
-
{ name: "opacity", covers: "transparency" },
|
|
1608
|
-
{
|
|
1609
|
-
name: "ring",
|
|
1610
|
-
covers: "focus indicator",
|
|
1611
|
-
divergence: "desktop ring \xB7 mobile none \xB7 tv strong highlight"
|
|
1612
|
-
},
|
|
1613
|
-
{ name: "backdrop", covers: "scrim / blur behind layer", divergence: "iOS material/vibrancy" }
|
|
1614
|
-
];
|
|
1615
|
-
var states = [
|
|
1616
|
-
{ name: "pressed", from: "pressability" },
|
|
1617
|
-
{ name: "hovered", from: "pressability" },
|
|
1618
|
-
{ name: "focused", from: "value-entry" },
|
|
1619
|
-
{ name: "active", from: "browse" },
|
|
1620
|
-
{ name: "selected", from: "selection" },
|
|
1621
|
-
{ name: "checked", from: "toggle" },
|
|
1622
|
-
{ name: "expanded", from: "disclosure" },
|
|
1623
|
-
{ name: "open", from: "presentation" },
|
|
1624
|
-
{ name: "disabled", from: "ambient (global gate)" },
|
|
1625
|
-
{ name: "readonly", from: "ambient" },
|
|
1626
|
-
{ name: "loading", from: "async-state" },
|
|
1627
|
-
{ name: "error", from: "async-state" },
|
|
1628
|
-
{ name: "success", from: "async-state" },
|
|
1629
|
-
{ name: "invalid", from: "validation" },
|
|
1630
|
-
{ name: "valid", from: "validation" },
|
|
1631
|
-
{ name: "indeterminate", from: "toggle (tristate)" },
|
|
1632
|
-
{ name: "dragging", from: "gesture" },
|
|
1633
|
-
{ name: "empty", from: "value-entry" },
|
|
1634
|
-
{ name: "complete", from: "progress / stepping-nav" },
|
|
1635
|
-
{ name: "first", from: "collection-position" },
|
|
1636
|
-
{ name: "last", from: "collection-position" },
|
|
1637
|
-
{ name: "odd", from: "collection-position" },
|
|
1638
|
-
{ name: "even", from: "collection-position" }
|
|
1639
|
-
];
|
|
1640
|
-
var capabilities = [
|
|
1641
|
-
{ name: "input.keyboard" },
|
|
1642
|
-
{ name: "input.pointer", note: "hover" },
|
|
1643
|
-
{ name: "input.touch" },
|
|
1644
|
-
{ name: "input.remote", note: "tv focus engine" },
|
|
1645
|
-
{
|
|
1646
|
-
name: "input.voice",
|
|
1647
|
-
status: "aspirational",
|
|
1648
|
-
note: "aspirational \u2014 declared but consumed by no atom yet (no voice/turn-taking machine in v0); see NOTES-modality-coverage"
|
|
1649
|
-
},
|
|
1650
|
-
{ name: "motion.haptic" },
|
|
1651
|
-
{ name: "runtime.dynamic-color", note: "Material You" },
|
|
1652
|
-
{ name: "display.density" },
|
|
1653
|
-
{ name: "a11y.screenreader" },
|
|
1654
|
-
{ name: "platform.web" },
|
|
1655
|
-
{ name: "platform.ios" },
|
|
1656
|
-
{ name: "platform.android" },
|
|
1657
|
-
{ name: "platform.tvos" }
|
|
1658
|
-
];
|
|
1659
|
-
var decorations = [
|
|
1660
|
-
{
|
|
1661
|
-
name: "motion",
|
|
1662
|
-
note: "decorates existing transitions (spring/flip/fade+slide); adds no states"
|
|
1663
|
-
},
|
|
1664
|
-
{
|
|
1665
|
-
name: "masking",
|
|
1666
|
-
note: "format/constrain entry (date/phone/currency) \u2014 decorates value-entry draft"
|
|
1667
|
-
},
|
|
1668
|
-
{
|
|
1669
|
-
name: "anchoring",
|
|
1670
|
-
note: "position a surface relative to a reference (placement/flip/offset/match-size) \u2014 decorates presentation(popover|menu)"
|
|
1671
|
-
},
|
|
1672
|
-
{
|
|
1673
|
-
name: "backdrop",
|
|
1674
|
-
note: "scrim / blur behind a layer \u2014 decorates a modal presentation; painted via the `backdrop` facet on the backdrop anatomy part"
|
|
1675
|
-
},
|
|
1676
|
-
{
|
|
1677
|
-
name: "momentum-scroll",
|
|
1678
|
-
note: "inertial scroll with snap / detent \u2014 decorates scroll/browse on touch surfaces"
|
|
1679
|
-
}
|
|
1680
|
-
];
|
|
1681
|
-
var nonBehaviors = [
|
|
1682
|
-
{ name: "filter", note: "derived view over a collection (data, not interaction)" },
|
|
1683
|
-
{ name: "virtualization", note: "render windowing" },
|
|
1684
|
-
{ name: "ordering", note: "collection mutation (reorder/sort)" },
|
|
1685
|
-
{ name: "aggregation", note: "data transformation (grouping/totals) \u2014 DataGrid math" }
|
|
1686
|
-
];
|
|
1687
|
-
var preludeV0 = {
|
|
1688
|
-
milda: "0.0.1",
|
|
1689
|
-
kind: "prelude",
|
|
1690
|
-
prelude: "0.0.1",
|
|
1691
|
-
meta: {
|
|
1692
|
-
id: "prelude.milda",
|
|
1693
|
-
name: "Milda Prelude",
|
|
1694
|
-
version: "0.0.2",
|
|
1695
|
-
description: "Built-in standard library: atoms, archetypes, facets, states, capabilities."
|
|
1696
|
-
},
|
|
1697
|
-
atoms,
|
|
1698
|
-
composites,
|
|
1699
|
-
archetypes,
|
|
1700
|
-
facets,
|
|
1701
|
-
states,
|
|
1702
|
-
capabilities,
|
|
1703
|
-
decorations,
|
|
1704
|
-
nonBehaviors
|
|
1705
|
-
};
|
|
1706
|
-
var atomsByName = Object.fromEntries(atoms.map((a) => [a.name, a]));
|
|
1707
|
-
var compositesByName = Object.fromEntries(
|
|
1708
|
-
composites.map((c) => [c.name, c])
|
|
1709
|
-
);
|
|
1710
|
-
var archetypesByName = Object.fromEntries(
|
|
1711
|
-
archetypes.map((a) => [a.name, a])
|
|
1712
|
-
);
|
|
1713
|
-
|
|
1714
|
-
// engine/resolveToken.ts
|
|
1715
|
-
function resolveToken(tokenId, contextIds, layers, _depth = 0) {
|
|
1716
|
-
if (_depth > 20) return null;
|
|
1717
|
-
const ids = typeof contextIds === "string" ? [contextIds] : contextIds;
|
|
1718
|
-
const token = findToken(tokenId, layers)?.token;
|
|
1719
|
-
if (!token) return null;
|
|
1720
|
-
const v = token.value;
|
|
1721
|
-
if (v.kind === "raw") return { kind: "value", type: token.type, value: v.value };
|
|
1722
|
-
if (v.kind === "escape") return { kind: "escape", raw: v.raw, note: v.note };
|
|
1723
|
-
const ref = ids.reduce((found, id) => found ?? v.refs[id], void 0) ?? v.refs["default"] ?? null;
|
|
1724
|
-
if (!ref) return null;
|
|
1725
|
-
return resolveToken(ref, contextIds, layers, _depth + 1);
|
|
1726
|
-
}
|
|
1727
|
-
function findToken(tokenId, layers) {
|
|
1728
|
-
for (const layer of layers) {
|
|
1729
|
-
const token = layer.tokens.find((t) => t.id === tokenId);
|
|
1730
|
-
if (token) return { token, layerId: layer.id };
|
|
1731
|
-
}
|
|
1732
|
-
return null;
|
|
1733
|
-
}
|
|
1734
|
-
function allTokensOfType(type, layers) {
|
|
1735
|
-
const sorted = [...layers].sort((a, b) => a.order - b.order);
|
|
1736
|
-
const result = [];
|
|
1737
|
-
for (const layer of sorted) {
|
|
1738
|
-
for (const token of layer.tokens) {
|
|
1739
|
-
if (token.type === type) result.push({ token, layerId: layer.id, layerName: layer.name });
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
return result;
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
// engine/resolveFacets.ts
|
|
1746
|
-
function resolveFacets(input) {
|
|
1747
|
-
const { part, activeStates } = input;
|
|
1748
|
-
let result = { ...part.facets ?? {} };
|
|
1749
|
-
const states2 = part.states ?? {};
|
|
1750
|
-
const order = input.stateOrder ?? Object.keys(states2);
|
|
1751
|
-
for (const s of order) {
|
|
1752
|
-
const override = activeStates.has(s) ? states2[s]?.facets : void 0;
|
|
1753
|
-
if (override) result = { ...result, ...override };
|
|
1754
|
-
}
|
|
1755
|
-
return result;
|
|
1756
|
-
}
|
|
1757
|
-
function resolveLayout(input) {
|
|
1758
|
-
const { part, activeStates } = input;
|
|
1759
|
-
let result = part.layout ? { ...part.layout } : void 0;
|
|
1760
|
-
const states2 = part.states ?? {};
|
|
1761
|
-
const order = input.stateOrder ?? Object.keys(states2);
|
|
1762
|
-
for (const s of order) {
|
|
1763
|
-
const override = activeStates.has(s) ? states2[s]?.layout : void 0;
|
|
1764
|
-
if (override) result = { ...result ?? {}, ...override };
|
|
1765
|
-
}
|
|
1766
|
-
return result;
|
|
1767
|
-
}
|
|
1768
|
-
function collectActiveStates(flags) {
|
|
1769
|
-
const active = /* @__PURE__ */ new Set();
|
|
1770
|
-
for (const [state, enabled] of Object.entries(flags)) {
|
|
1771
|
-
if (enabled) active.add(state);
|
|
1772
|
-
}
|
|
1773
|
-
return active;
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
// engine/validate.ts
|
|
1777
|
-
function facetTokenRefs(facets2) {
|
|
1778
|
-
if (!facets2) return [];
|
|
1779
|
-
const ids = [];
|
|
1780
|
-
for (const v of Object.values(facets2)) {
|
|
1781
|
-
if (!v) continue;
|
|
1782
|
-
if ("token" in v) ids.push(v.token);
|
|
1783
|
-
else if ("inset" in v) {
|
|
1784
|
-
for (const edge of Object.values(v.inset)) if (typeof edge === "string") ids.push(edge);
|
|
1785
|
-
} else if ("border" in v) {
|
|
1786
|
-
if (v.border.color) ids.push(v.border.color);
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
return ids;
|
|
1790
|
-
}
|
|
1791
|
-
function layoutTokenRefs(layout) {
|
|
1792
|
-
if (!layout) return [];
|
|
1793
|
-
const ids = [];
|
|
1794
|
-
if (layout.gap) ids.push(layout.gap);
|
|
1795
|
-
if (layout.inset) {
|
|
1796
|
-
for (const edge of Object.values(layout.inset)) if (typeof edge === "string") ids.push(edge);
|
|
1797
|
-
}
|
|
1798
|
-
for (const axis of [layout.size?.width, layout.size?.height]) {
|
|
1799
|
-
if (axis && typeof axis === "object") {
|
|
1800
|
-
const intent = "intent" in axis ? axis.intent : axis;
|
|
1801
|
-
if (intent && typeof intent === "object" && "fixed" in intent) ids.push(intent.fixed);
|
|
1802
|
-
if ("min" in axis && axis.min) ids.push(axis.min);
|
|
1803
|
-
if ("max" in axis && axis.max) ids.push(axis.max);
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
return ids;
|
|
1807
|
-
}
|
|
1808
|
-
function validate(doc, prelude) {
|
|
1809
|
-
const issues = [];
|
|
1810
|
-
const archetypeNames = new Set(prelude.archetypes.map((a) => a.name));
|
|
1811
|
-
const facetNames = new Set(prelude.facets.map((f) => f.name));
|
|
1812
|
-
const stateNames = new Set(prelude.states.map((s) => s.name));
|
|
1813
|
-
const groupIds = new Set(doc.foundations.contextGroups.map((g) => g.id));
|
|
1814
|
-
const layers = doc.foundations.layers;
|
|
1815
|
-
for (const layer of layers) {
|
|
1816
|
-
for (const tok of layer.tokens) {
|
|
1817
|
-
if (tok.value.kind === "alias") {
|
|
1818
|
-
const where = `token ${tok.name} (${tok.id})`;
|
|
1819
|
-
if (tok.value.by !== "" && !groupIds.has(tok.value.by)) {
|
|
1820
|
-
issues.push({
|
|
1821
|
-
severity: "error",
|
|
1822
|
-
code: "alias.unknownGroup",
|
|
1823
|
-
message: `alias by unknown context group "${tok.value.by}"`,
|
|
1824
|
-
where,
|
|
1825
|
-
id: tok.id
|
|
1826
|
-
});
|
|
1827
|
-
}
|
|
1828
|
-
for (const refId of Object.values(tok.value.refs)) {
|
|
1829
|
-
if (!findToken(refId, layers)) {
|
|
1830
|
-
issues.push({
|
|
1831
|
-
severity: "error",
|
|
1832
|
-
code: "alias.danglingRef",
|
|
1833
|
-
message: `alias ref points at missing token "${refId}"`,
|
|
1834
|
-
where,
|
|
1835
|
-
id: tok.id
|
|
1836
|
-
});
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
for (const comp of doc.components) {
|
|
1843
|
-
const where = `component ${comp.name} (${comp.id})`;
|
|
1844
|
-
if (comp.archetype !== "" && !archetypeNames.has(comp.archetype)) {
|
|
1845
|
-
issues.push({
|
|
1846
|
-
severity: "error",
|
|
1847
|
-
code: "component.unknownArchetype",
|
|
1848
|
-
message: `unknown archetype "${comp.archetype}"`,
|
|
1849
|
-
where,
|
|
1850
|
-
id: comp.id
|
|
1851
|
-
});
|
|
1852
|
-
}
|
|
1853
|
-
const propIds = new Set(comp.contract.props.map((p) => p.id));
|
|
1854
|
-
for (const [param, binding] of Object.entries(comp.contract.params)) {
|
|
1855
|
-
if ("prop" in binding && !propIds.has(binding.prop)) {
|
|
1856
|
-
issues.push({
|
|
1857
|
-
severity: "error",
|
|
1858
|
-
code: "param.danglingProp",
|
|
1859
|
-
message: `param "${param}" binds missing prop "${binding.prop}"`,
|
|
1860
|
-
where,
|
|
1861
|
-
id: comp.id
|
|
1862
|
-
});
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
for (const [partName, part] of Object.entries(comp.anatomy)) {
|
|
1866
|
-
checkPart(comp, partName, part, { facetNames, stateNames, layers, issues });
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
return issues;
|
|
1870
|
-
}
|
|
1871
|
-
function checkPart(comp, partName, part, ctx) {
|
|
1872
|
-
const where = `${comp.name} \u203A part ${partName}`;
|
|
1873
|
-
const { facetNames, stateNames, layers, issues } = ctx;
|
|
1874
|
-
const checkFacets = (facets2, scope) => {
|
|
1875
|
-
if (!facets2) return;
|
|
1876
|
-
for (const name of Object.keys(facets2)) {
|
|
1877
|
-
if (!facetNames.has(name)) {
|
|
1878
|
-
issues.push({
|
|
1879
|
-
severity: "error",
|
|
1880
|
-
code: "facet.unknown",
|
|
1881
|
-
message: `unknown facet "${name}"`,
|
|
1882
|
-
where: scope,
|
|
1883
|
-
id: comp.id
|
|
1884
|
-
});
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
for (const id of facetTokenRefs(facets2)) {
|
|
1888
|
-
if (!findToken(id, layers)) {
|
|
1889
|
-
issues.push({
|
|
1890
|
-
severity: "error",
|
|
1891
|
-
code: "facet.danglingToken",
|
|
1892
|
-
message: `facet references missing token "${id}"`,
|
|
1893
|
-
where: scope,
|
|
1894
|
-
id: comp.id
|
|
1895
|
-
});
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
};
|
|
1899
|
-
const checkLayoutRefs = (layout, scope) => {
|
|
1900
|
-
for (const id of layoutTokenRefs(layout)) {
|
|
1901
|
-
if (!findToken(id, layers)) {
|
|
1902
|
-
issues.push({
|
|
1903
|
-
severity: "error",
|
|
1904
|
-
code: "layout.danglingToken",
|
|
1905
|
-
message: `layout references missing token "${id}"`,
|
|
1906
|
-
where: scope,
|
|
1907
|
-
id: comp.id
|
|
1908
|
-
});
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
};
|
|
1912
|
-
checkFacets(part.facets, where);
|
|
1913
|
-
checkLayoutRefs(part.layout, where);
|
|
1914
|
-
for (const [stateName, override] of Object.entries(part.states ?? {})) {
|
|
1915
|
-
const scope = `${where} [${stateName}]`;
|
|
1916
|
-
if (!stateNames.has(stateName)) {
|
|
1917
|
-
issues.push({
|
|
1918
|
-
severity: "error",
|
|
1919
|
-
code: "state.unknown",
|
|
1920
|
-
message: `unknown state "${stateName}"`,
|
|
1921
|
-
where: scope,
|
|
1922
|
-
id: comp.id
|
|
1923
|
-
});
|
|
1924
|
-
}
|
|
1925
|
-
checkFacets(override?.facets, scope);
|
|
1926
|
-
checkLayoutRefs(override?.layout, scope);
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
// engine/conformance.ts
|
|
1931
|
-
function walkFacets(facets2, where, opts, holes) {
|
|
1932
|
-
if (!facets2) return;
|
|
1933
|
-
for (const [name, value] of Object.entries(facets2)) {
|
|
1934
|
-
if (!value) continue;
|
|
1935
|
-
if ("raw" in value) {
|
|
1936
|
-
holes.push({
|
|
1937
|
-
kind: "facet",
|
|
1938
|
-
name,
|
|
1939
|
-
where,
|
|
1940
|
-
reason: "raw escape-hatch value is a platform-specific leaf"
|
|
1941
|
-
});
|
|
1942
|
-
} else if ("token" in value) {
|
|
1943
|
-
const resolved = resolveToken(value.token, opts.contextIds, opts.layers);
|
|
1944
|
-
if (resolved === null) {
|
|
1945
|
-
holes.push({
|
|
1946
|
-
kind: "facet",
|
|
1947
|
-
name,
|
|
1948
|
-
where,
|
|
1949
|
-
reason: `token "${value.token}" does not resolve for target`
|
|
1950
|
-
});
|
|
1951
|
-
} else if (resolved.kind === "escape") {
|
|
1952
|
-
holes.push({
|
|
1953
|
-
kind: "facet",
|
|
1954
|
-
name,
|
|
1955
|
-
where,
|
|
1956
|
-
reason: `token "${value.token}" resolves to an escape-hatch leaf for this target`
|
|
1957
|
-
});
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
function walkPart(comp, partName, part, opts, holes) {
|
|
1963
|
-
const where = `${comp.name} \u203A ${partName}`;
|
|
1964
|
-
walkFacets(part.facets, where, opts, holes);
|
|
1965
|
-
for (const [stateName, override] of Object.entries(part.states ?? {})) {
|
|
1966
|
-
walkFacets(override?.facets, `${where} [${stateName}]`, opts, holes);
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
function conformance(comp, opts) {
|
|
1970
|
-
const holes = [];
|
|
1971
|
-
for (const [partName, part] of Object.entries(comp.anatomy)) {
|
|
1972
|
-
walkPart(comp, partName, part, opts, holes);
|
|
1973
|
-
}
|
|
1974
|
-
const substitutions = [];
|
|
1975
|
-
const realize = comp.realize.find((r) => r.target === opts.target);
|
|
1976
|
-
if (realize) {
|
|
1977
|
-
for (const dropped of realize.drop ?? [])
|
|
1978
|
-
substitutions.push({ behavior: dropped, via: "dropped" });
|
|
1979
|
-
for (const wire of realize.wire ?? [])
|
|
1980
|
-
substitutions.push({ behavior: `${wire.from} \u2192 ${wire.to}`, via: "realize wire" });
|
|
1981
|
-
}
|
|
1982
|
-
return { target: opts.target, complete: holes.length === 0, holes, substitutions };
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
// dsl/lexer.ts
|
|
1986
|
-
var LexError = class extends Error {
|
|
1987
|
-
constructor(message, line, col) {
|
|
1988
|
-
super(`${message} (line ${line}:${col})`);
|
|
1989
|
-
this.line = line;
|
|
1990
|
-
this.col = col;
|
|
1991
|
-
this.name = "LexError";
|
|
1992
|
-
}
|
|
1993
|
-
};
|
|
1994
|
-
var isWordChar = (c) => /[A-Za-z0-9_./%-]/.test(c);
|
|
1995
|
-
var PUNCT = {
|
|
1996
|
-
"{": "lbrace",
|
|
1997
|
-
"}": "rbrace",
|
|
1998
|
-
"(": "lparen",
|
|
1999
|
-
")": "rparen",
|
|
2000
|
-
"[": "lbracket",
|
|
2001
|
-
"]": "rbracket",
|
|
2002
|
-
"<": "lt",
|
|
2003
|
-
">": "gt",
|
|
2004
|
-
":": "colon",
|
|
2005
|
-
"=": "equals",
|
|
2006
|
-
",": "comma",
|
|
2007
|
-
";": "semi",
|
|
2008
|
-
"?": "question",
|
|
2009
|
-
"@": "at"
|
|
2010
|
-
};
|
|
2011
|
-
function lex(src) {
|
|
2012
|
-
const tokens = [];
|
|
2013
|
-
let i = 0;
|
|
2014
|
-
let line = 1;
|
|
2015
|
-
let col = 1;
|
|
2016
|
-
const n = src.length;
|
|
2017
|
-
const advance = (count = 1) => {
|
|
2018
|
-
for (let k = 0; k < count; k++) {
|
|
2019
|
-
if (src[i] === "\n") {
|
|
2020
|
-
line++;
|
|
2021
|
-
col = 1;
|
|
2022
|
-
} else {
|
|
2023
|
-
col++;
|
|
2024
|
-
}
|
|
2025
|
-
i++;
|
|
2026
|
-
}
|
|
2027
|
-
};
|
|
2028
|
-
const push = (type, value, startPos, startLine, startCol) => {
|
|
2029
|
-
tokens.push({ type, value, pos: startPos, line: startLine, col: startCol });
|
|
2030
|
-
};
|
|
2031
|
-
while (i < n) {
|
|
2032
|
-
const c = src[i];
|
|
2033
|
-
const startPos = i;
|
|
2034
|
-
const startLine = line;
|
|
2035
|
-
const startCol = col;
|
|
2036
|
-
if (c === "\n") {
|
|
2037
|
-
advance();
|
|
2038
|
-
if (tokens.length && tokens[tokens.length - 1].type !== "newline") {
|
|
2039
|
-
push("newline", "\n", startPos, startLine, startCol);
|
|
2040
|
-
}
|
|
2041
|
-
continue;
|
|
2042
|
-
}
|
|
2043
|
-
if (c === " " || c === " " || c === "\r") {
|
|
2044
|
-
advance();
|
|
2045
|
-
continue;
|
|
2046
|
-
}
|
|
2047
|
-
if (c === "#") {
|
|
2048
|
-
while (i < n && src[i] !== "\n") advance();
|
|
2049
|
-
continue;
|
|
2050
|
-
}
|
|
2051
|
-
if (c === '"') {
|
|
2052
|
-
advance();
|
|
2053
|
-
let value = "";
|
|
2054
|
-
while (i < n && src[i] !== '"') {
|
|
2055
|
-
if (src[i] === "\\" && i + 1 < n) {
|
|
2056
|
-
const next = src[i + 1];
|
|
2057
|
-
value += next === "n" ? "\n" : next === "t" ? " " : next;
|
|
2058
|
-
advance(2);
|
|
2059
|
-
continue;
|
|
2060
|
-
}
|
|
2061
|
-
if (src[i] === "\n") throw new LexError("Unterminated string", startLine, startCol);
|
|
2062
|
-
value += src[i];
|
|
2063
|
-
advance();
|
|
2064
|
-
}
|
|
2065
|
-
if (i >= n) throw new LexError("Unterminated string", startLine, startCol);
|
|
2066
|
-
advance();
|
|
2067
|
-
push("string", value, startPos, startLine, startCol);
|
|
2068
|
-
continue;
|
|
2069
|
-
}
|
|
2070
|
-
if (c === "-" && src[i + 1] === ">") {
|
|
2071
|
-
advance(2);
|
|
2072
|
-
push("arrow", "->", startPos, startLine, startCol);
|
|
2073
|
-
continue;
|
|
2074
|
-
}
|
|
2075
|
-
if (c === "\u2192") {
|
|
2076
|
-
advance();
|
|
2077
|
-
push("arrow", "->", startPos, startLine, startCol);
|
|
2078
|
-
continue;
|
|
2079
|
-
}
|
|
2080
|
-
if (c === "!" && src[i + 1] === "=") {
|
|
2081
|
-
advance(2);
|
|
2082
|
-
push("ne", "!=", startPos, startLine, startCol);
|
|
2083
|
-
continue;
|
|
2084
|
-
}
|
|
2085
|
-
if (c === "<" && src[i + 1] === "=") {
|
|
2086
|
-
advance(2);
|
|
2087
|
-
push("lte", "<=", startPos, startLine, startCol);
|
|
2088
|
-
continue;
|
|
2089
|
-
}
|
|
2090
|
-
if (c === ">" && src[i + 1] === "=") {
|
|
2091
|
-
advance(2);
|
|
2092
|
-
push("gte", ">=", startPos, startLine, startCol);
|
|
2093
|
-
continue;
|
|
2094
|
-
}
|
|
2095
|
-
if (PUNCT[c]) {
|
|
2096
|
-
advance();
|
|
2097
|
-
push(PUNCT[c], c, startPos, startLine, startCol);
|
|
2098
|
-
continue;
|
|
2099
|
-
}
|
|
2100
|
-
if (isWordChar(c)) {
|
|
2101
|
-
let value = "";
|
|
2102
|
-
while (i < n && isWordChar(src[i]) && !(src[i] === "-" && src[i + 1] === ">")) {
|
|
2103
|
-
value += src[i];
|
|
2104
|
-
advance();
|
|
2105
|
-
}
|
|
2106
|
-
push("word", value, startPos, startLine, startCol);
|
|
2107
|
-
continue;
|
|
2108
|
-
}
|
|
2109
|
-
advance();
|
|
2110
|
-
}
|
|
2111
|
-
push("eof", "", i, line, col);
|
|
2112
|
-
return tokens;
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
// dsl/parse.ts
|
|
2116
|
-
var seg = (s) => s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "x";
|
|
2117
|
-
var cgId = (name) => `cg.${seg(name)}`;
|
|
2118
|
-
var ctxId = (groupName, ctxName) => `ctx.${seg(groupName)}.${seg(ctxName)}`;
|
|
2119
|
-
var tokId = (path) => `tok.${path.replace(/\//g, ".")}`;
|
|
2120
|
-
var stId = (name) => `st.${seg(name)}`;
|
|
2121
|
-
var cmpId = (name) => `cmp.${seg(name)}`;
|
|
2122
|
-
function metaValueType(v) {
|
|
2123
|
-
if (typeof v === "number") return { kind: "number" };
|
|
2124
|
-
if (typeof v === "boolean") return { kind: "boolean" };
|
|
2125
|
-
if (Array.isArray(v)) return { kind: "array", item: { kind: "string" } };
|
|
2126
|
-
return { kind: "string" };
|
|
2127
|
-
}
|
|
2128
|
-
var FACET_MAP = {
|
|
2129
|
-
fill: "fill",
|
|
2130
|
-
ink: "ink",
|
|
2131
|
-
border: "border",
|
|
2132
|
-
"border.top": "border.top",
|
|
2133
|
-
"border.right": "border.right",
|
|
2134
|
-
"border.bottom": "border.bottom",
|
|
2135
|
-
"border.left": "border.left",
|
|
2136
|
-
"border.inline-start": "border.inline-start",
|
|
2137
|
-
"border.inline-end": "border.inline-end",
|
|
2138
|
-
"border.block-start": "border.block-start",
|
|
2139
|
-
"border.block-end": "border.block-end",
|
|
2140
|
-
corner: "corner",
|
|
2141
|
-
inset: "inset",
|
|
2142
|
-
gap: "gap",
|
|
2143
|
-
text: "text",
|
|
2144
|
-
"text.clamp": "textClamp",
|
|
2145
|
-
"text.wrap": "textWrap",
|
|
2146
|
-
elevation: "elevation",
|
|
2147
|
-
overlay: "overlay",
|
|
2148
|
-
opacity: "opacity",
|
|
2149
|
-
ring: "ring",
|
|
2150
|
-
backdrop: "backdrop",
|
|
2151
|
-
weight: "weight",
|
|
2152
|
-
font: "font",
|
|
2153
|
-
slant: "slant"
|
|
2154
|
-
};
|
|
2155
|
-
var TYPE_MAP = {
|
|
2156
|
-
color: "color",
|
|
2157
|
-
spacing: "spacing",
|
|
2158
|
-
radius: "radius",
|
|
2159
|
-
size: "size",
|
|
2160
|
-
dimension: "size",
|
|
2161
|
-
shadow: "shadow",
|
|
2162
|
-
typography: "typography",
|
|
2163
|
-
"font-family": "fontFamily",
|
|
2164
|
-
fontFamily: "fontFamily",
|
|
2165
|
-
"font-weight": "fontWeight",
|
|
2166
|
-
fontWeight: "fontWeight",
|
|
2167
|
-
duration: "duration",
|
|
2168
|
-
opacity: "opacity",
|
|
2169
|
-
blur: "blur",
|
|
2170
|
-
tonal: "tonal",
|
|
2171
|
-
border: "border",
|
|
2172
|
-
elevation: "elevation",
|
|
2173
|
-
ring: "ring",
|
|
2174
|
-
backdrop: "backdrop",
|
|
2175
|
-
none: "none"
|
|
2176
|
-
};
|
|
2177
|
-
var NUMBER_RE = /^-?\d+(\.\d+)?$/;
|
|
2178
|
-
var isRawValue = (v) => /^#/.test(v) || /[(%]/.test(v) || /\d(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|ms|s)$/.test(v);
|
|
2179
|
-
function hexToColor(hex) {
|
|
2180
|
-
const m = /^#([0-9a-fA-F]{3,8})$/.exec(hex.trim());
|
|
2181
|
-
if (!m) return null;
|
|
2182
|
-
let h = m[1];
|
|
2183
|
-
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
|
2184
|
-
if (h.length !== 6 && h.length !== 8) return null;
|
|
2185
|
-
const byte = (k) => parseInt(h.slice(k * 2, k * 2 + 2), 16) / 255;
|
|
2186
|
-
const value = { space: "srgb", channels: [byte(0), byte(1), byte(2)] };
|
|
2187
|
-
if (h.length === 8) value.alpha = byte(3);
|
|
2188
|
-
return value;
|
|
2189
|
-
}
|
|
2190
|
-
var COLOR_FNS = /* @__PURE__ */ new Set(["hex", "srgb", "rgb", "p3", "oklch", "hsl"]);
|
|
2191
|
-
var UNIT_RE = /^(-?\d*\.?\d+)(px|rem|em|%|ms|s)$/;
|
|
2192
|
-
function unitToValue(n, unit) {
|
|
2193
|
-
switch (unit) {
|
|
2194
|
-
case "px":
|
|
2195
|
-
return { value: n, unit: "fixed" };
|
|
2196
|
-
case "em":
|
|
2197
|
-
return { value: n, unit: "em" };
|
|
2198
|
-
case "%":
|
|
2199
|
-
return { value: n, unit: "percent" };
|
|
2200
|
-
case "ms":
|
|
2201
|
-
return { ms: n };
|
|
2202
|
-
case "s":
|
|
2203
|
-
return { ms: n * 1e3 };
|
|
2204
|
-
default:
|
|
2205
|
-
return { value: n, unit: "scaled" };
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
var Parser = class {
|
|
2209
|
-
constructor(toks) {
|
|
2210
|
-
this.pos = 0;
|
|
2211
|
-
this.issues = [];
|
|
2212
|
-
// Source spans for declared entities (see NodeSpan). Populated as declarations are
|
|
2213
|
-
// parsed; the AST itself never gains position fields.
|
|
2214
|
-
this.spans = [];
|
|
2215
|
-
// The component currently being parsed, so nested part/prop/event spans can point
|
|
2216
|
-
// back at it via `parentId`.
|
|
2217
|
-
this.currentComponentId = null;
|
|
2218
|
-
// Tokens synthesized while parsing (inline literals hoisted out of alias slots —
|
|
2219
|
-
// the canonical AST's `refs` holds only token ids, so an inline value must become
|
|
2220
|
-
// its own token). Deduped by (type, serialized value). Flushed into the layer.
|
|
2221
|
-
this.synth = [];
|
|
2222
|
-
this.synthKeys = /* @__PURE__ */ new Map();
|
|
2223
|
-
this.toks = toks;
|
|
2224
|
-
}
|
|
2225
|
-
// Hoist an inline literal into a synthesized token and return its id.
|
|
2226
|
-
hoistLiteral(astType, raw, tok) {
|
|
2227
|
-
const value = this.typedRaw(astType, raw, tok);
|
|
2228
|
-
const key = `${astType}\0${JSON.stringify(value)}`;
|
|
2229
|
-
const existing = this.synthKeys.get(key);
|
|
2230
|
-
if (existing) return existing;
|
|
2231
|
-
const id = `tok.lit-${seg(String(astType))}-${this.synth.length + 1}`;
|
|
2232
|
-
this.synth.push({ id, name: `(inline ${astType})`, type: astType, value });
|
|
2233
|
-
this.synthKeys.set(key, id);
|
|
2234
|
-
this.issue("warning", "inline-literal", `inline ${astType} literal in an alias slot hoisted to token ${id}`, tok);
|
|
2235
|
-
return id;
|
|
2236
|
-
}
|
|
2237
|
-
// Hoist an already-typed token value (an inline literal in an alias slot) into a
|
|
2238
|
-
// synthesized token and return its id.
|
|
2239
|
-
hoistValue(astType, value, tok) {
|
|
2240
|
-
const key = `${astType} ${JSON.stringify(value)}`;
|
|
2241
|
-
const existing = this.synthKeys.get(key);
|
|
2242
|
-
if (existing) return existing;
|
|
2243
|
-
const id = `tok.lit-${seg(String(astType))}-${this.synth.length + 1}`;
|
|
2244
|
-
this.synth.push({ id, name: `(inline ${astType})`, type: astType, value });
|
|
2245
|
-
this.synthKeys.set(key, id);
|
|
2246
|
-
this.issue("warning", "inline-literal", `inline ${astType} literal in an alias slot hoisted to token ${id}`, tok);
|
|
2247
|
-
return id;
|
|
2248
|
-
}
|
|
2249
|
-
// A self-typing value literal: a color-space function (`oklch(…)`, `hex(rrggbb)`, …)
|
|
2250
|
-
// or a unit-suffixed number (`8px`, `150ms`). Returns a raw TokenValue, or null if the
|
|
2251
|
-
// cursor isn't at such a literal (then it consumes nothing).
|
|
2252
|
-
tryParseLiteralValue() {
|
|
2253
|
-
const t = this.peek();
|
|
2254
|
-
if (t.type !== "word") return null;
|
|
2255
|
-
if (COLOR_FNS.has(t.value) && this.toks[this.pos + 1]?.type === "lparen") {
|
|
2256
|
-
return { kind: "raw", value: this.parseColorFn(t.value) };
|
|
2257
|
-
}
|
|
2258
|
-
const m = UNIT_RE.exec(t.value);
|
|
2259
|
-
if (m) {
|
|
2260
|
-
this.next();
|
|
2261
|
-
return { kind: "raw", value: unitToValue(Number(m[1]), m[2]) };
|
|
2262
|
-
}
|
|
2263
|
-
return null;
|
|
2264
|
-
}
|
|
2265
|
-
// `fn( a b c [alpha] )` — space-separated numeric channels (hex takes raw digits).
|
|
2266
|
-
parseColorFn(fn) {
|
|
2267
|
-
this.next();
|
|
2268
|
-
this.expect("lparen", "color-lparen");
|
|
2269
|
-
const nums = [];
|
|
2270
|
-
let hex = "";
|
|
2271
|
-
while (!this.at("rparen") && !this.at("eof")) {
|
|
2272
|
-
const w = this.next().value;
|
|
2273
|
-
if (fn === "hex") hex += w;
|
|
2274
|
-
else {
|
|
2275
|
-
const n = Number(w);
|
|
2276
|
-
if (!Number.isNaN(n)) nums.push(n);
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
this.expect("rparen", "color-rparen");
|
|
2280
|
-
if (fn === "hex") {
|
|
2281
|
-
return hexToColor(hex.startsWith("#") ? hex : `#${hex}`) ?? { space: "srgb", channels: [0, 0, 0] };
|
|
2282
|
-
}
|
|
2283
|
-
const c = (i) => nums[i] ?? 0;
|
|
2284
|
-
let cv;
|
|
2285
|
-
if (fn === "rgb") cv = { space: "srgb", channels: [c(0) / 255, c(1) / 255, c(2) / 255] };
|
|
2286
|
-
else if (fn === "p3") cv = { space: "display-p3", channels: [c(0), c(1), c(2)] };
|
|
2287
|
-
else cv = { space: fn, channels: [c(0), c(1), c(2)] };
|
|
2288
|
-
if (nums.length >= 4) cv.alpha = nums[3];
|
|
2289
|
-
return cv;
|
|
2290
|
-
}
|
|
2291
|
-
// Self-typing literals let us check the value's inherent kind against the token's
|
|
2292
|
-
// declared type (e.g. `token x : color = 8px` is a dimension in a color slot).
|
|
2293
|
-
checkLiteralType(astType, lit, tok) {
|
|
2294
|
-
if (lit.kind !== "raw") return;
|
|
2295
|
-
const v = lit.value;
|
|
2296
|
-
if (typeof v !== "object" || v === null) return;
|
|
2297
|
-
const cat = "space" in v ? "color" : "ms" in v ? "duration" : "unit" in v ? "dimension" : null;
|
|
2298
|
-
if (!cat) return;
|
|
2299
|
-
const expected = astType === "color" ? "color" : astType === "duration" ? "duration" : astType === "spacing" || astType === "radius" || astType === "size" ? "dimension" : null;
|
|
2300
|
-
if (expected && cat !== expected) {
|
|
2301
|
-
this.issue("error", "value-type", `${cat} literal in a "${astType}" token`, tok);
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
peek() {
|
|
2305
|
-
return this.toks[this.pos];
|
|
2306
|
-
}
|
|
2307
|
-
next() {
|
|
2308
|
-
return this.toks[this.pos++];
|
|
2309
|
-
}
|
|
2310
|
-
at(type) {
|
|
2311
|
-
return this.peek().type === type;
|
|
2312
|
-
}
|
|
2313
|
-
// A `word` whose text equals `w` (case-sensitive keyword check).
|
|
2314
|
-
atWord(w) {
|
|
2315
|
-
const t = this.peek();
|
|
2316
|
-
return t.type === "word" && t.value === w;
|
|
2317
|
-
}
|
|
2318
|
-
skipNewlines() {
|
|
2319
|
-
while (this.at("newline") || this.at("semi")) this.pos++;
|
|
2320
|
-
}
|
|
2321
|
-
// Safety net: if a loop body made no progress (a malformed token that no rule
|
|
2322
|
-
// consumed), force one token forward so parsing can never spin. Returns true when
|
|
2323
|
-
// it had to intervene (the caller can also choose to break).
|
|
2324
|
-
ensureProgress(mark) {
|
|
2325
|
-
if (this.pos !== mark) return false;
|
|
2326
|
-
if (!this.at("eof")) this.next();
|
|
2327
|
-
return true;
|
|
2328
|
-
}
|
|
2329
|
-
// ─── span recording (source positions for tooling) ─────────────────────────
|
|
2330
|
-
recordSpan(span) {
|
|
2331
|
-
this.spans.push(span);
|
|
2332
|
-
}
|
|
2333
|
-
startPos(tok) {
|
|
2334
|
-
return { line: tok.line, col: tok.col };
|
|
2335
|
-
}
|
|
2336
|
-
// End position of a token = its start advanced past its source text. Strings add 2
|
|
2337
|
-
// for the surrounding quotes (escape sequences make this a slight over-estimate,
|
|
2338
|
-
// affecting only a rarely-used string span — never an identifier or brace).
|
|
2339
|
-
endPos(tok) {
|
|
2340
|
-
const len = tok.type === "newline" || tok.type === "eof" ? 0 : tok.type === "string" ? tok.value.length + 2 : tok.value.length;
|
|
2341
|
-
return { line: tok.line, col: tok.col + len };
|
|
2342
|
-
}
|
|
2343
|
-
rangeOf(start, end) {
|
|
2344
|
-
return { start: this.startPos(start), end: this.endPos(end) };
|
|
2345
|
-
}
|
|
2346
|
-
// The most-recently-consumed token — closes a span that has no trailing brace
|
|
2347
|
-
// (a single-line `token`/`type` declaration ends at its value).
|
|
2348
|
-
lastTok() {
|
|
2349
|
-
return this.toks[Math.max(0, this.pos - 1)];
|
|
2350
|
-
}
|
|
2351
|
-
issue(severity, code, message, tok = this.peek()) {
|
|
2352
|
-
this.issues.push({ severity, code, message, line: tok.line, col: tok.col });
|
|
2353
|
-
}
|
|
2354
|
-
// Consume a token of `type`, or record an error and synthesize one.
|
|
2355
|
-
expect(type, code) {
|
|
2356
|
-
if (this.at(type)) return this.next();
|
|
2357
|
-
const tok = this.peek();
|
|
2358
|
-
this.issue("error", code, `expected ${type}, got ${tok.type} ${JSON.stringify(tok.value)}`);
|
|
2359
|
-
return tok;
|
|
2360
|
-
}
|
|
2361
|
-
// A bare word (ident/path/number token).
|
|
2362
|
-
expectWord(code) {
|
|
2363
|
-
if (this.at("word")) return this.next().value;
|
|
2364
|
-
this.issue("error", code, `expected identifier, got ${this.peek().type}`);
|
|
2365
|
-
return "";
|
|
2366
|
-
}
|
|
2367
|
-
// Skip a balanced `{ … }` block (already positioned at or before the `{`).
|
|
2368
|
-
skipBlock() {
|
|
2369
|
-
while (!this.at("lbrace") && !this.at("eof") && !this.at("newline")) this.next();
|
|
2370
|
-
if (!this.at("lbrace")) return;
|
|
2371
|
-
let depth = 0;
|
|
2372
|
-
do {
|
|
2373
|
-
const t = this.next();
|
|
2374
|
-
if (t.type === "lbrace") depth++;
|
|
2375
|
-
else if (t.type === "rbrace") depth--;
|
|
2376
|
-
else if (t.type === "eof") break;
|
|
2377
|
-
} while (depth > 0);
|
|
2378
|
-
}
|
|
2379
|
-
// ── context-group NAME { ctx ctx … } ──
|
|
2380
|
-
parseContextGroup() {
|
|
2381
|
-
const kw = this.next();
|
|
2382
|
-
const nameTok = this.peek();
|
|
2383
|
-
const name = this.expectWord("cg-name");
|
|
2384
|
-
this.expect("lbrace", "cg-lbrace");
|
|
2385
|
-
const contexts = [];
|
|
2386
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
2387
|
-
if (this.at("newline") || this.at("semi")) {
|
|
2388
|
-
this.pos++;
|
|
2389
|
-
continue;
|
|
2390
|
-
}
|
|
2391
|
-
const ctxTok = this.peek();
|
|
2392
|
-
const ctxName = this.expectWord("cg-ctx");
|
|
2393
|
-
if (!ctxName) break;
|
|
2394
|
-
contexts.push({ id: ctxId(name, ctxName), name: ctxName });
|
|
2395
|
-
this.recordSpan({
|
|
2396
|
-
id: ctxId(name, ctxName),
|
|
2397
|
-
kind: "context",
|
|
2398
|
-
name: ctxName,
|
|
2399
|
-
nameRange: this.rangeOf(ctxTok, ctxTok),
|
|
2400
|
-
fullRange: this.rangeOf(ctxTok, ctxTok),
|
|
2401
|
-
parentId: cgId(name)
|
|
2402
|
-
});
|
|
2403
|
-
}
|
|
2404
|
-
const close = this.expect("rbrace", "cg-rbrace");
|
|
2405
|
-
this.recordSpan({
|
|
2406
|
-
id: cgId(name),
|
|
2407
|
-
kind: "contextGroup",
|
|
2408
|
-
name,
|
|
2409
|
-
nameRange: this.rangeOf(nameTok, nameTok),
|
|
2410
|
-
fullRange: this.rangeOf(kw, close)
|
|
2411
|
-
});
|
|
2412
|
-
return { id: cgId(name), name, contexts };
|
|
2413
|
-
}
|
|
2414
|
-
// ── raw value literal → a JS value (string | number | nested object) ──
|
|
2415
|
-
parseRawLiteral() {
|
|
2416
|
-
const t = this.peek();
|
|
2417
|
-
if (t.type === "string") return this.next().value;
|
|
2418
|
-
if (t.type === "lbrace") return this.parseStructured();
|
|
2419
|
-
if (t.type === "word") {
|
|
2420
|
-
if (COLOR_FNS.has(t.value) && this.toks[this.pos + 1]?.type === "lparen") return this.parseColorFn(t.value);
|
|
2421
|
-
const m = UNIT_RE.exec(t.value);
|
|
2422
|
-
if (m) {
|
|
2423
|
-
this.next();
|
|
2424
|
-
return unitToValue(Number(m[1]), m[2]);
|
|
2425
|
-
}
|
|
2426
|
-
const w = this.next().value;
|
|
2427
|
-
return NUMBER_RE.test(w) ? Number(w) : w;
|
|
2428
|
-
}
|
|
2429
|
-
this.issue("error", "raw-value", `expected a value, got ${t.type}`);
|
|
2430
|
-
return "";
|
|
2431
|
-
}
|
|
2432
|
-
parseStructured() {
|
|
2433
|
-
this.expect("lbrace", "struct-lbrace");
|
|
2434
|
-
const obj = {};
|
|
2435
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
2436
|
-
const mark = this.pos;
|
|
2437
|
-
if (this.at("newline") || this.at("semi")) {
|
|
2438
|
-
this.pos++;
|
|
2439
|
-
continue;
|
|
2440
|
-
}
|
|
2441
|
-
const key = this.expectWord("struct-key");
|
|
2442
|
-
this.expect("colon", "struct-colon");
|
|
2443
|
-
const val = this.parseRawLiteral();
|
|
2444
|
-
if (key) obj[key] = val;
|
|
2445
|
-
if (this.at("semi") || this.at("newline")) this.pos++;
|
|
2446
|
-
this.ensureProgress(mark);
|
|
2447
|
-
}
|
|
2448
|
-
this.expect("rbrace", "struct-rbrace");
|
|
2449
|
-
return obj;
|
|
2450
|
-
}
|
|
2451
|
-
// ── convert a raw JS value to the canonical typed value for `astType` ──
|
|
2452
|
-
typedRaw(astType, raw, tok) {
|
|
2453
|
-
if (astType === "color" && typeof raw === "string") {
|
|
2454
|
-
const color = hexToColor(raw);
|
|
2455
|
-
if (color) return { kind: "raw", value: color };
|
|
2456
|
-
this.issue("warning", "color-nonhex", `non-hex color "${raw}" kept as an escape value`, tok);
|
|
2457
|
-
return { kind: "escape", raw, note: "non-hex color literal" };
|
|
2458
|
-
}
|
|
2459
|
-
if (astType === "spacing" || astType === "radius" || astType === "size") {
|
|
2460
|
-
if (typeof raw === "number") return { kind: "raw", value: { value: raw, unit: "scaled" } };
|
|
2461
|
-
if (raw && typeof raw === "object" && "value" in raw) {
|
|
2462
|
-
const r = raw;
|
|
2463
|
-
return { kind: "raw", value: { value: Number(r.value), unit: r.unit ?? "scaled" } };
|
|
2464
|
-
}
|
|
2465
|
-
}
|
|
2466
|
-
if (astType === "opacity" && typeof raw === "number") return { kind: "raw", value: { value: raw } };
|
|
2467
|
-
if (astType === "fontWeight" && typeof raw === "number") return { kind: "raw", value: { weight: raw } };
|
|
2468
|
-
if (astType === "duration" && typeof raw === "number") return { kind: "raw", value: { ms: raw } };
|
|
2469
|
-
if (astType === "tonal" && typeof raw === "number") return { kind: "raw", value: { level: raw } };
|
|
2470
|
-
return { kind: "raw", value: raw };
|
|
2471
|
-
}
|
|
2472
|
-
// ── alias by GROUP { Ctx -> <path | inline literal> ; default -> … } ──
|
|
2473
|
-
// A slot value is normally a token path (ref). The authored surface also allows an
|
|
2474
|
-
// inline literal (a hex color, a structured value) in a slot; since the AST's `refs`
|
|
2475
|
-
// holds only ids, such literals are hoisted to synthesized tokens (hoistLiteral).
|
|
2476
|
-
parseAlias(astType) {
|
|
2477
|
-
this.next();
|
|
2478
|
-
if (this.atWord("by")) this.next();
|
|
2479
|
-
else this.issue("error", "alias-by", "expected 'by'");
|
|
2480
|
-
const groupName = this.expectWord("alias-group");
|
|
2481
|
-
this.expect("lbrace", "alias-lbrace");
|
|
2482
|
-
const refs = {};
|
|
2483
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
2484
|
-
const mark = this.pos;
|
|
2485
|
-
if (this.at("newline") || this.at("semi")) {
|
|
2486
|
-
this.pos++;
|
|
2487
|
-
continue;
|
|
2488
|
-
}
|
|
2489
|
-
const ctxName = this.expectWord("alias-ctx");
|
|
2490
|
-
this.expect("arrow", "alias-arrow");
|
|
2491
|
-
const slot = this.peek();
|
|
2492
|
-
let refId = "";
|
|
2493
|
-
const lit = this.tryParseLiteralValue();
|
|
2494
|
-
if (lit) {
|
|
2495
|
-
this.checkLiteralType(astType, lit, slot);
|
|
2496
|
-
refId = this.hoistValue(astType, lit, slot);
|
|
2497
|
-
} else if (slot.type === "string" || slot.type === "lbrace" || slot.type === "word" && NUMBER_RE.test(slot.value)) {
|
|
2498
|
-
refId = this.hoistLiteral(astType, this.parseRawLiteral(), slot);
|
|
2499
|
-
} else {
|
|
2500
|
-
const refPath = this.expectWord("alias-ref");
|
|
2501
|
-
if (refPath) refId = tokId(refPath);
|
|
2502
|
-
}
|
|
2503
|
-
if (ctxName && refId) {
|
|
2504
|
-
const key = ctxName === "default" ? "default" : ctxId(groupName, ctxName);
|
|
2505
|
-
refs[key] = refId;
|
|
2506
|
-
}
|
|
2507
|
-
if (this.at("semi") || this.at("newline")) this.pos++;
|
|
2508
|
-
this.ensureProgress(mark);
|
|
2509
|
-
}
|
|
2510
|
-
this.expect("rbrace", "alias-rbrace");
|
|
2511
|
-
return { kind: "alias", by: cgId(groupName), refs };
|
|
2512
|
-
}
|
|
2513
|
-
// ── the right-hand side of `token … =` ──
|
|
2514
|
-
parseTokenValue(astType) {
|
|
2515
|
-
const t = this.peek();
|
|
2516
|
-
if (t.type === "word" && t.value === "alias") return this.parseAlias(astType);
|
|
2517
|
-
if (t.type === "word" && t.value === "escape") {
|
|
2518
|
-
this.next();
|
|
2519
|
-
const raw = this.at("string") ? this.next().value : this.expectWord("escape-raw");
|
|
2520
|
-
let escNote;
|
|
2521
|
-
if (this.atWord("note")) {
|
|
2522
|
-
this.next();
|
|
2523
|
-
escNote = this.at("string") ? this.next().value : this.expectWord("escape-note");
|
|
2524
|
-
}
|
|
2525
|
-
const esc = { kind: "escape", raw };
|
|
2526
|
-
if (escNote) esc.note = escNote;
|
|
2527
|
-
return esc;
|
|
2528
|
-
}
|
|
2529
|
-
if (t.type === "word" && t.value === "raw") {
|
|
2530
|
-
this.next();
|
|
2531
|
-
const raw = this.parseRawLiteral();
|
|
2532
|
-
return this.typedRaw(astType, raw, t);
|
|
2533
|
-
}
|
|
2534
|
-
if (t.type === "lbrace") {
|
|
2535
|
-
const raw = this.parseStructured();
|
|
2536
|
-
return this.typedRaw(astType, raw, t);
|
|
2537
|
-
}
|
|
2538
|
-
if (t.type === "word") {
|
|
2539
|
-
const lit = this.tryParseLiteralValue();
|
|
2540
|
-
if (lit) {
|
|
2541
|
-
this.checkLiteralType(astType, lit, t);
|
|
2542
|
-
return lit;
|
|
2543
|
-
}
|
|
2544
|
-
if (NUMBER_RE.test(t.value)) {
|
|
2545
|
-
this.next();
|
|
2546
|
-
return this.typedRaw(astType, Number(t.value), t);
|
|
2547
|
-
}
|
|
2548
|
-
const refPath = this.next().value;
|
|
2549
|
-
this.issue("warning", "ref-sugar", `unconditional ref "${refPath}" lowered to a default-only alias`, t);
|
|
2550
|
-
return { kind: "alias", by: "", refs: { default: tokId(refPath) } };
|
|
2551
|
-
}
|
|
2552
|
-
this.issue("error", "token-value", `expected a token value, got ${t.type}`);
|
|
2553
|
-
return { kind: "raw", value: "" };
|
|
2554
|
-
}
|
|
2555
|
-
// ── foundations { token PATH : TYPE = VALUE … } → one Layer ──
|
|
2556
|
-
parseFoundations(layer) {
|
|
2557
|
-
this.next();
|
|
2558
|
-
this.expect("lbrace", "foundations-lbrace");
|
|
2559
|
-
let order = 0;
|
|
2560
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
2561
|
-
const mark = this.pos;
|
|
2562
|
-
if (this.at("newline") || this.at("semi")) {
|
|
2563
|
-
this.pos++;
|
|
2564
|
-
continue;
|
|
2565
|
-
}
|
|
2566
|
-
if (this.atWord("token")) {
|
|
2567
|
-
const tok = this.parseTokenDecl(order);
|
|
2568
|
-
if (tok) {
|
|
2569
|
-
layer.tokens.push(tok);
|
|
2570
|
-
order++;
|
|
2571
|
-
}
|
|
2572
|
-
this.ensureProgress(mark);
|
|
2573
|
-
continue;
|
|
2574
|
-
}
|
|
2575
|
-
this.issue("error", "foundations-stmt", `unexpected ${JSON.stringify(this.peek().value)} in foundations`);
|
|
2576
|
-
while (!this.at("newline") && !this.at("semi") && !this.at("rbrace") && !this.at("eof")) this.next();
|
|
2577
|
-
this.ensureProgress(mark);
|
|
2578
|
-
}
|
|
2579
|
-
this.expect("rbrace", "foundations-rbrace");
|
|
2580
|
-
if (this.synth.length) {
|
|
2581
|
-
layer.tokens.push(...this.synth);
|
|
2582
|
-
this.synth = [];
|
|
2583
|
-
}
|
|
2584
|
-
}
|
|
2585
|
-
parseTokenDecl(_order) {
|
|
2586
|
-
const startTok = this.next();
|
|
2587
|
-
const nameTok = this.peek();
|
|
2588
|
-
const path = this.expectWord("token-path");
|
|
2589
|
-
if (!path) return null;
|
|
2590
|
-
this.expect("colon", "token-colon");
|
|
2591
|
-
const typeName = this.expectWord("token-type");
|
|
2592
|
-
const astType = TYPE_MAP[typeName];
|
|
2593
|
-
if (!astType) this.issue("warning", "token-type", `non-canonical token type "${typeName}" kept as-is`, startTok);
|
|
2594
|
-
this.expect("equals", "token-equals");
|
|
2595
|
-
const value = this.parseTokenValue(astType ?? typeName);
|
|
2596
|
-
this.recordSpan({
|
|
2597
|
-
id: tokId(path),
|
|
2598
|
-
kind: "token",
|
|
2599
|
-
name: path,
|
|
2600
|
-
nameRange: this.rangeOf(nameTok, nameTok),
|
|
2601
|
-
fullRange: this.rangeOf(startTok, this.lastTok())
|
|
2602
|
-
});
|
|
2603
|
-
return {
|
|
2604
|
-
id: tokId(path),
|
|
2605
|
-
name: path,
|
|
2606
|
-
type: astType ?? typeName,
|
|
2607
|
-
value
|
|
2608
|
-
};
|
|
2609
|
-
}
|
|
2610
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
2611
|
-
// Slice 2 — shared types
|
|
2612
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
2613
|
-
skipToTerminator() {
|
|
2614
|
-
while (!this.at("newline") && !this.at("semi") && !this.at("rbrace") && !this.at("eof")) this.next();
|
|
2615
|
-
}
|
|
2616
|
-
parseIdentList() {
|
|
2617
|
-
const out = [];
|
|
2618
|
-
if (this.at("word")) out.push(this.next().value);
|
|
2619
|
-
while (this.at("comma")) {
|
|
2620
|
-
this.next();
|
|
2621
|
-
if (this.at("word")) out.push(this.next().value);
|
|
2622
|
-
}
|
|
2623
|
-
return out;
|
|
2624
|
-
}
|
|
2625
|
-
parseObjectType() {
|
|
2626
|
-
this.expect("lbrace", "obj-lbrace");
|
|
2627
|
-
const fields = [];
|
|
2628
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
2629
|
-
const mark = this.pos;
|
|
2630
|
-
if (this.at("newline") || this.at("semi")) {
|
|
2631
|
-
this.pos++;
|
|
2632
|
-
continue;
|
|
2633
|
-
}
|
|
2634
|
-
const name = this.expectWord("field-name");
|
|
2635
|
-
let required = true;
|
|
2636
|
-
if (this.at("question")) {
|
|
2637
|
-
this.next();
|
|
2638
|
-
required = false;
|
|
2639
|
-
}
|
|
2640
|
-
this.expect("colon", "field-colon");
|
|
2641
|
-
const type = this.parseTypeExpr();
|
|
2642
|
-
if (name) fields.push({ id: `fld.${seg(name)}`, name, type, required });
|
|
2643
|
-
if (this.at("semi") || this.at("newline")) this.pos++;
|
|
2644
|
-
this.ensureProgress(mark);
|
|
2645
|
-
}
|
|
2646
|
-
this.expect("rbrace", "obj-rbrace");
|
|
2647
|
-
return { kind: "object", fields };
|
|
2648
|
-
}
|
|
2649
|
-
parseTypeExpr() {
|
|
2650
|
-
const t = this.peek();
|
|
2651
|
-
if (t.type !== "word") {
|
|
2652
|
-
this.issue("error", "type-expr", `expected a type, got ${t.type}`);
|
|
2653
|
-
return { kind: "string" };
|
|
2654
|
-
}
|
|
2655
|
-
const w = t.value;
|
|
2656
|
-
if (w === "string" || w === "number" || w === "boolean") {
|
|
2657
|
-
this.next();
|
|
2658
|
-
return { kind: w };
|
|
2659
|
-
}
|
|
2660
|
-
if (w === "enum") {
|
|
2661
|
-
this.next();
|
|
2662
|
-
this.expect("lparen", "enum-lparen");
|
|
2663
|
-
const values = this.parseIdentList();
|
|
2664
|
-
this.expect("rparen", "enum-rparen");
|
|
2665
|
-
return { kind: "enum", values };
|
|
2666
|
-
}
|
|
2667
|
-
if (w === "ref") {
|
|
2668
|
-
this.next();
|
|
2669
|
-
const name = this.expectWord("ref-name");
|
|
2670
|
-
return { kind: "ref", ref: stId(name) };
|
|
2671
|
-
}
|
|
2672
|
-
if (w === "array") {
|
|
2673
|
-
this.next();
|
|
2674
|
-
this.expect("lt", "array-lt");
|
|
2675
|
-
const item = this.parseTypeExpr();
|
|
2676
|
-
this.expect("gt", "array-gt");
|
|
2677
|
-
return { kind: "array", item };
|
|
2678
|
-
}
|
|
2679
|
-
if (w === "union") {
|
|
2680
|
-
this.next();
|
|
2681
|
-
this.expect("lparen", "union-lparen");
|
|
2682
|
-
const members = [this.parseTypeExpr()];
|
|
2683
|
-
while (this.at("comma")) {
|
|
2684
|
-
this.next();
|
|
2685
|
-
members.push(this.parseTypeExpr());
|
|
2686
|
-
}
|
|
2687
|
-
this.expect("rparen", "union-rparen");
|
|
2688
|
-
return { kind: "union", members };
|
|
2689
|
-
}
|
|
2690
|
-
if (w === "object") {
|
|
2691
|
-
this.next();
|
|
2692
|
-
return this.parseObjectType();
|
|
2693
|
-
}
|
|
2694
|
-
if (w === "component") {
|
|
2695
|
-
this.next();
|
|
2696
|
-
let componentIds;
|
|
2697
|
-
if (this.at("lparen")) {
|
|
2698
|
-
this.next();
|
|
2699
|
-
componentIds = this.parseIdentList().map(cmpId);
|
|
2700
|
-
this.expect("rparen", "component-rparen");
|
|
2701
|
-
}
|
|
2702
|
-
return { kind: "component", componentIds };
|
|
2703
|
-
}
|
|
2704
|
-
if (w === "context") {
|
|
2705
|
-
this.next();
|
|
2706
|
-
const group = this.expectWord("context-type");
|
|
2707
|
-
this.issue("warning", "context-type", `context-typed "${group}" lowered to ref (proposal 0001)`, t);
|
|
2708
|
-
return { kind: "ref", ref: cgId(group) };
|
|
2709
|
-
}
|
|
2710
|
-
this.next();
|
|
2711
|
-
return { kind: "ref", ref: stId(w) };
|
|
2712
|
-
}
|
|
2713
|
-
parseSharedType() {
|
|
2714
|
-
const kw = this.next();
|
|
2715
|
-
const nameTok = this.peek();
|
|
2716
|
-
const name = this.expectWord("type-name");
|
|
2717
|
-
if (!name) return null;
|
|
2718
|
-
this.expect("equals", "type-equals");
|
|
2719
|
-
const definition = this.parseTypeExpr();
|
|
2720
|
-
this.recordSpan({
|
|
2721
|
-
id: stId(name),
|
|
2722
|
-
kind: "sharedType",
|
|
2723
|
-
name,
|
|
2724
|
-
nameRange: this.rangeOf(nameTok, nameTok),
|
|
2725
|
-
fullRange: this.rangeOf(kw, this.lastTok())
|
|
2726
|
-
});
|
|
2727
|
-
return { id: stId(name), name, definition };
|
|
2728
|
-
}
|
|
2729
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
2730
|
-
// Slice 3 — components
|
|
2731
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
2732
|
-
// A condition's raw text: `prop` or `prop=value` (reused by wire/gate `when`).
|
|
2733
|
-
parseConditionText() {
|
|
2734
|
-
let text = this.expectWord("cond");
|
|
2735
|
-
if (this.at("equals")) {
|
|
2736
|
-
this.next();
|
|
2737
|
-
const v = this.at("string") ? this.next().value : this.expectWord("cond-val");
|
|
2738
|
-
text += `=${v}`;
|
|
2739
|
-
}
|
|
2740
|
-
return text;
|
|
2741
|
-
}
|
|
2742
|
-
// A prop condition → RealizeCond { prop, eq } (single `prop=value`).
|
|
2743
|
-
parseRealizeCond() {
|
|
2744
|
-
const prop = this.expectWord("rc-prop");
|
|
2745
|
-
let eq = true;
|
|
2746
|
-
if (this.at("equals")) {
|
|
2747
|
-
this.next();
|
|
2748
|
-
if (this.at("string")) eq = this.next().value;
|
|
2749
|
-
else {
|
|
2750
|
-
const v = this.expectWord("rc-val");
|
|
2751
|
-
eq = NUMBER_RE.test(v) ? Number(v) : v === "true" ? true : v === "false" ? false : v;
|
|
2752
|
-
}
|
|
2753
|
-
}
|
|
2754
|
-
return { prop, eq };
|
|
2755
|
-
}
|
|
2756
|
-
// ── Predicate expression parser (proposal 0032) ─────────────────────────────
|
|
2757
|
-
// Recursive-descent over the surface predicate grammar emitted by @mildastudio/
|
|
2758
|
-
// generate's `predToDsl` (targets/dsl/emit.ts). Precedence, lowest → highest:
|
|
2759
|
-
// or → and → unary(`not`) → primary(`( … )` | leaf)
|
|
2760
|
-
// A leaf names a source (`state s` / `context g` / `ancestor id s` / a bare prop path)
|
|
2761
|
-
// then an optional operator + value: a bare leaf is `set`, `not <leaf>` is `unset`, and
|
|
2762
|
-
// a parenthesized `not ( … )` negates a whole group. Values are bare idents/numbers/
|
|
2763
|
-
// bools or quoted strings (mirroring the emitter's `predValue`), coerced to a JS type.
|
|
2764
|
-
parsePredicate() {
|
|
2765
|
-
return this.parsePredOr();
|
|
2766
|
-
}
|
|
2767
|
-
parsePredOr() {
|
|
2768
|
-
const first = this.parsePredAnd();
|
|
2769
|
-
if (!this.atWord("or")) return first;
|
|
2770
|
-
const items = [first];
|
|
2771
|
-
while (this.atWord("or")) {
|
|
2772
|
-
this.next();
|
|
2773
|
-
items.push(this.parsePredAnd());
|
|
2774
|
-
}
|
|
2775
|
-
return { kind: "any", items };
|
|
2776
|
-
}
|
|
2777
|
-
parsePredAnd() {
|
|
2778
|
-
const first = this.parsePredUnary();
|
|
2779
|
-
if (!this.atWord("and")) return first;
|
|
2780
|
-
const items = [first];
|
|
2781
|
-
while (this.atWord("and")) {
|
|
2782
|
-
this.next();
|
|
2783
|
-
items.push(this.parsePredUnary());
|
|
2784
|
-
}
|
|
2785
|
-
return { kind: "all", items };
|
|
2786
|
-
}
|
|
2787
|
-
parsePredUnary() {
|
|
2788
|
-
if (this.atWord("not")) {
|
|
2789
|
-
this.next();
|
|
2790
|
-
if (this.at("lparen")) {
|
|
2791
|
-
this.next();
|
|
2792
|
-
const item = this.parsePredOr();
|
|
2793
|
-
this.expect("rparen", "pred-not-rparen");
|
|
2794
|
-
return { kind: "not", item };
|
|
2795
|
-
}
|
|
2796
|
-
return { kind: "cmp", ref: this.parsePredRef(), op: "unset" };
|
|
2797
|
-
}
|
|
2798
|
-
return this.parsePredPrimary();
|
|
2799
|
-
}
|
|
2800
|
-
parsePredPrimary() {
|
|
2801
|
-
if (this.at("lparen")) {
|
|
2802
|
-
this.next();
|
|
2803
|
-
const inner = this.parsePredOr();
|
|
2804
|
-
this.expect("rparen", "pred-rparen");
|
|
2805
|
-
return inner;
|
|
2806
|
-
}
|
|
2807
|
-
return this.parsePredLeaf();
|
|
2808
|
-
}
|
|
2809
|
-
// A comparison leaf: a source, then an optional operator + value. No operator ⇒ `set`.
|
|
2810
|
-
parsePredLeaf() {
|
|
2811
|
-
const ref = this.parsePredRef();
|
|
2812
|
-
const op = this.predOp();
|
|
2813
|
-
if (!op) return { kind: "cmp", ref, op: "set" };
|
|
2814
|
-
return { kind: "cmp", ref, op, value: this.parsePredValue() };
|
|
2815
|
-
}
|
|
2816
|
-
// The source of a leaf: `state <s>` / `context <g>` / `ancestor <id> <s>` / bare prop path.
|
|
2817
|
-
parsePredRef() {
|
|
2818
|
-
if (this.atWord("state")) {
|
|
2819
|
-
this.next();
|
|
2820
|
-
return { kind: "state", state: this.expectWord("pred-state") };
|
|
2821
|
-
}
|
|
2822
|
-
if (this.atWord("context")) {
|
|
2823
|
-
this.next();
|
|
2824
|
-
return { kind: "context", group: this.expectWord("pred-context") };
|
|
2825
|
-
}
|
|
2826
|
-
if (this.atWord("ancestor")) {
|
|
2827
|
-
this.next();
|
|
2828
|
-
const nodeId = this.expectWord("pred-ancestor-id");
|
|
2829
|
-
const state = this.expectWord("pred-ancestor-state");
|
|
2830
|
-
return { kind: "ancestor", nodeId, state };
|
|
2831
|
-
}
|
|
2832
|
-
return { kind: "prop", path: this.expectWord("pred-prop") };
|
|
2833
|
-
}
|
|
2834
|
-
// A comparison operator token → CompareOp, or null when the next token isn't one.
|
|
2835
|
-
predOp() {
|
|
2836
|
-
switch (this.peek().type) {
|
|
2837
|
-
case "equals":
|
|
2838
|
-
this.next();
|
|
2839
|
-
return "eq";
|
|
2840
|
-
case "ne":
|
|
2841
|
-
this.next();
|
|
2842
|
-
return "ne";
|
|
2843
|
-
case "lt":
|
|
2844
|
-
this.next();
|
|
2845
|
-
return "lt";
|
|
2846
|
-
case "lte":
|
|
2847
|
-
this.next();
|
|
2848
|
-
return "lte";
|
|
2849
|
-
case "gt":
|
|
2850
|
-
this.next();
|
|
2851
|
-
return "gt";
|
|
2852
|
-
case "gte":
|
|
2853
|
-
this.next();
|
|
2854
|
-
return "gte";
|
|
2855
|
-
default:
|
|
2856
|
-
if (this.atWord("contains")) {
|
|
2857
|
-
this.next();
|
|
2858
|
-
return "contains";
|
|
2859
|
-
}
|
|
2860
|
-
return null;
|
|
2861
|
-
}
|
|
2862
|
-
}
|
|
2863
|
-
// A leaf value: a quoted string, or a bare word coerced to number / boolean / string
|
|
2864
|
-
// (mirrors `parseRealizeCond` and the emitter's bare-vs-quoted `predValue`).
|
|
2865
|
-
parsePredValue() {
|
|
2866
|
-
const t = this.peek();
|
|
2867
|
-
if (t.type === "string") return this.next().value;
|
|
2868
|
-
if (t.type === "word") {
|
|
2869
|
-
const w = this.next().value;
|
|
2870
|
-
return NUMBER_RE.test(w) ? Number(w) : w === "true" ? true : w === "false" ? false : w;
|
|
2871
|
-
}
|
|
2872
|
-
this.issue("error", "pred-value", `expected a comparison value, got ${t.type}`);
|
|
2873
|
-
return "";
|
|
2874
|
-
}
|
|
2875
|
-
parsePropDefault() {
|
|
2876
|
-
const t = this.peek();
|
|
2877
|
-
if (t.type === "string") return this.next().value;
|
|
2878
|
-
if (t.type === "word" && t.value === "alias") {
|
|
2879
|
-
this.next();
|
|
2880
|
-
if (this.atWord("by")) this.next();
|
|
2881
|
-
this.expectWord("alias-group");
|
|
2882
|
-
this.skipBlock();
|
|
2883
|
-
this.issue("warning", "alias-default", "context-aliased prop default skipped (proposal 0001)", t);
|
|
2884
|
-
return void 0;
|
|
2885
|
-
}
|
|
2886
|
-
if (t.type === "word") {
|
|
2887
|
-
this.next();
|
|
2888
|
-
if (NUMBER_RE.test(t.value)) return Number(t.value);
|
|
2889
|
-
if (t.value === "true") return true;
|
|
2890
|
-
if (t.value === "false") return false;
|
|
2891
|
-
return t.value;
|
|
2892
|
-
}
|
|
2893
|
-
this.issue("error", "prop-default", `expected a default value, got ${t.type}`);
|
|
2894
|
-
return void 0;
|
|
2895
|
-
}
|
|
2896
|
-
parsePropDecl() {
|
|
2897
|
-
const nameTok = this.peek();
|
|
2898
|
-
const name = this.expectWord("prop-name");
|
|
2899
|
-
if (!name) return null;
|
|
2900
|
-
const type = this.parseTypeExpr();
|
|
2901
|
-
const prop = { id: `prop.${seg(name)}`, name, type };
|
|
2902
|
-
if (this.at("equals")) {
|
|
2903
|
-
this.next();
|
|
2904
|
-
const def = this.parsePropDefault();
|
|
2905
|
-
if (def !== void 0) prop.default = def;
|
|
2906
|
-
}
|
|
2907
|
-
if (this.atWord("bind")) {
|
|
2908
|
-
this.next();
|
|
2909
|
-
prop.binding = this.expectWord("bind-target");
|
|
2910
|
-
}
|
|
2911
|
-
const meta = {};
|
|
2912
|
-
while (this.at("at")) {
|
|
2913
|
-
this.next();
|
|
2914
|
-
const key = this.expectWord("annot-key");
|
|
2915
|
-
const t = this.peek();
|
|
2916
|
-
let value = true;
|
|
2917
|
-
if (t.type === "string") value = this.next().value;
|
|
2918
|
-
else if (t.type === "word") {
|
|
2919
|
-
const w = this.next().value;
|
|
2920
|
-
value = NUMBER_RE.test(w) ? Number(w) : w === "true" ? true : w === "false" ? false : w;
|
|
2921
|
-
}
|
|
2922
|
-
if (key) meta[key] = value;
|
|
2923
|
-
}
|
|
2924
|
-
if (Object.keys(meta).length) prop.meta = meta;
|
|
2925
|
-
this.recordSpan({
|
|
2926
|
-
id: `${this.currentComponentId ?? ""}#prop.${seg(name)}`,
|
|
2927
|
-
kind: "prop",
|
|
2928
|
-
name,
|
|
2929
|
-
nameRange: this.rangeOf(nameTok, nameTok),
|
|
2930
|
-
fullRange: this.rangeOf(nameTok, this.lastTok()),
|
|
2931
|
-
parentId: this.currentComponentId ?? void 0
|
|
2932
|
-
});
|
|
2933
|
-
return prop;
|
|
2934
|
-
}
|
|
2935
|
-
parseProps(props) {
|
|
2936
|
-
this.next();
|
|
2937
|
-
this.expect("lbrace", "props-lbrace");
|
|
2938
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
2939
|
-
const mark = this.pos;
|
|
2940
|
-
if (this.at("newline") || this.at("semi")) {
|
|
2941
|
-
this.pos++;
|
|
2942
|
-
continue;
|
|
2943
|
-
}
|
|
2944
|
-
const p = this.parsePropDecl();
|
|
2945
|
-
if (p) props.push(p);
|
|
2946
|
-
if (this.at("semi") || this.at("newline")) this.pos++;
|
|
2947
|
-
this.ensureProgress(mark);
|
|
2948
|
-
}
|
|
2949
|
-
this.expect("rbrace", "props-rbrace");
|
|
2950
|
-
}
|
|
2951
|
-
parseEvents(events) {
|
|
2952
|
-
this.next();
|
|
2953
|
-
this.expect("lbrace", "events-lbrace");
|
|
2954
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
2955
|
-
const mark = this.pos;
|
|
2956
|
-
if (this.at("newline") || this.at("semi")) {
|
|
2957
|
-
this.pos++;
|
|
2958
|
-
continue;
|
|
2959
|
-
}
|
|
2960
|
-
const nameTok = this.peek();
|
|
2961
|
-
const name = this.expectWord("event-name");
|
|
2962
|
-
if (name) {
|
|
2963
|
-
const e = { name };
|
|
2964
|
-
if (this.atWord("from")) {
|
|
2965
|
-
this.next();
|
|
2966
|
-
e.from = this.expectWord("event-from");
|
|
2967
|
-
}
|
|
2968
|
-
events.push(e);
|
|
2969
|
-
this.recordSpan({
|
|
2970
|
-
id: `${this.currentComponentId ?? ""}#event.${seg(name)}`,
|
|
2971
|
-
kind: "event",
|
|
2972
|
-
name,
|
|
2973
|
-
nameRange: this.rangeOf(nameTok, nameTok),
|
|
2974
|
-
fullRange: this.rangeOf(nameTok, this.lastTok()),
|
|
2975
|
-
parentId: this.currentComponentId ?? void 0
|
|
2976
|
-
});
|
|
2977
|
-
}
|
|
2978
|
-
if (this.at("semi") || this.at("newline")) this.pos++;
|
|
2979
|
-
this.ensureProgress(mark);
|
|
2980
|
-
}
|
|
2981
|
-
this.expect("rbrace", "events-rbrace");
|
|
2982
|
-
}
|
|
2983
|
-
// `uses ATOM [ ( params ) ] as ALIAS`
|
|
2984
|
-
parseUses() {
|
|
2985
|
-
this.next();
|
|
2986
|
-
const atom = this.expectWord("uses-atom");
|
|
2987
|
-
if (!atom) return null;
|
|
2988
|
-
if (this.at("lparen")) {
|
|
2989
|
-
this.next();
|
|
2990
|
-
while (!this.at("rparen") && !this.at("eof") && !this.at("newline")) this.next();
|
|
2991
|
-
if (this.at("rparen")) this.next();
|
|
2992
|
-
}
|
|
2993
|
-
if (this.atWord("as")) this.next();
|
|
2994
|
-
else this.issue("warning", "uses-as", "expected 'as' in uses");
|
|
2995
|
-
const as = this.expectWord("uses-alias");
|
|
2996
|
-
return { atom, as };
|
|
2997
|
-
}
|
|
2998
|
-
// The `machineref` target of a wire, with an optional `(arg)`.
|
|
2999
|
-
parseWireEnd(code) {
|
|
3000
|
-
const end = this.expectWord(code);
|
|
3001
|
-
let arg;
|
|
3002
|
-
if (this.at("lparen")) {
|
|
3003
|
-
this.next();
|
|
3004
|
-
if (this.at("word")) arg = this.next().value;
|
|
3005
|
-
while (!this.at("rparen") && !this.at("eof") && !this.at("newline")) this.next();
|
|
3006
|
-
if (this.at("rparen")) this.next();
|
|
3007
|
-
}
|
|
3008
|
-
return { end, arg };
|
|
3009
|
-
}
|
|
3010
|
-
// `wire FROM [when COND] -> TO[(arg)]`
|
|
3011
|
-
parseWire() {
|
|
3012
|
-
this.next();
|
|
3013
|
-
const from = this.expectWord("wire-from");
|
|
3014
|
-
if (!from) return null;
|
|
3015
|
-
let when;
|
|
3016
|
-
if (this.atWord("when")) {
|
|
3017
|
-
this.next();
|
|
3018
|
-
when = this.parseConditionText();
|
|
3019
|
-
}
|
|
3020
|
-
this.expect("arrow", "wire-arrow");
|
|
3021
|
-
const { end: to, arg } = this.parseWireEnd("wire-to");
|
|
3022
|
-
const wire = { kind: "wire", from, to };
|
|
3023
|
-
if (arg) wire.arg = arg;
|
|
3024
|
-
if (when) wire.when = when;
|
|
3025
|
-
return wire;
|
|
3026
|
-
}
|
|
3027
|
-
// `gate MACHINE active when COND`
|
|
3028
|
-
parseGate() {
|
|
3029
|
-
this.next();
|
|
3030
|
-
const machine = this.expectWord("gate-machine");
|
|
3031
|
-
if (!machine) return null;
|
|
3032
|
-
if (this.atWord("active")) this.next();
|
|
3033
|
-
if (this.atWord("when")) this.next();
|
|
3034
|
-
else this.issue("warning", "gate-when", "expected 'when' in gate");
|
|
3035
|
-
const activeWhen = this.parseConditionText();
|
|
3036
|
-
return { kind: "gate", machine, activeWhen };
|
|
3037
|
-
}
|
|
3038
|
-
// ── layout / facet bodies ──
|
|
3039
|
-
parseAxisIntent() {
|
|
3040
|
-
const w = this.expectWord("axis");
|
|
3041
|
-
if (w === "fit" || w === "fill") return w;
|
|
3042
|
-
if (w === "weight") {
|
|
3043
|
-
if (this.at("lparen")) {
|
|
3044
|
-
this.next();
|
|
3045
|
-
const n = Number(this.expectWord("weight-n"));
|
|
3046
|
-
if (this.at("rparen")) this.next();
|
|
3047
|
-
return { weight: n };
|
|
3048
|
-
}
|
|
3049
|
-
return { weight: 1 };
|
|
3050
|
-
}
|
|
3051
|
-
if (w === "fixed") {
|
|
3052
|
-
if (this.at("lparen")) {
|
|
3053
|
-
this.next();
|
|
3054
|
-
const p = this.expectWord("fixed-token");
|
|
3055
|
-
if (this.at("rparen")) this.next();
|
|
3056
|
-
return { fixed: tokId(p) };
|
|
3057
|
-
}
|
|
3058
|
-
this.issue("warning", "axis-fixed", "bare 'fixed' axis has no dimension; treated as fit");
|
|
3059
|
-
return "fit";
|
|
3060
|
-
}
|
|
3061
|
-
this.issue("warning", "axis-unknown", `unknown size intent "${w}"; treated as fit`);
|
|
3062
|
-
return "fit";
|
|
3063
|
-
}
|
|
3064
|
-
parseInsetArgs() {
|
|
3065
|
-
const args = [];
|
|
3066
|
-
while (this.at("word") && args.length < 2) args.push(this.next().value);
|
|
3067
|
-
if (args.length >= 2) return { block: tokId(args[0]), inline: tokId(args[1]) };
|
|
3068
|
-
if (args.length === 1) return { all: tokId(args[0]) };
|
|
3069
|
-
return { all: "" };
|
|
3070
|
-
}
|
|
3071
|
-
parseLayoutBody() {
|
|
3072
|
-
this.expect("lbrace", "layout-lbrace");
|
|
3073
|
-
const spec = {};
|
|
3074
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
3075
|
-
const mark = this.pos;
|
|
3076
|
-
if (this.at("newline") || this.at("semi")) {
|
|
3077
|
-
this.pos++;
|
|
3078
|
-
continue;
|
|
3079
|
-
}
|
|
3080
|
-
const key = this.expectWord("layout-key");
|
|
3081
|
-
switch (key) {
|
|
3082
|
-
case "stack":
|
|
3083
|
-
spec.stack = this.expectWord("stack");
|
|
3084
|
-
break;
|
|
3085
|
-
case "flow":
|
|
3086
|
-
spec.flow = true;
|
|
3087
|
-
break;
|
|
3088
|
-
case "gap":
|
|
3089
|
-
spec.gap = tokId(this.expectWord("gap"));
|
|
3090
|
-
break;
|
|
3091
|
-
case "align":
|
|
3092
|
-
spec.align = this.expectWord("align");
|
|
3093
|
-
break;
|
|
3094
|
-
case "distribute":
|
|
3095
|
-
spec.distribute = this.expectWord("distribute");
|
|
3096
|
-
break;
|
|
3097
|
-
case "wrap": {
|
|
3098
|
-
const w = this.expectWord("wrap");
|
|
3099
|
-
if (w === "wrap" || w === "wrap-reverse") spec.flow = true;
|
|
3100
|
-
break;
|
|
3101
|
-
}
|
|
3102
|
-
case "position": {
|
|
3103
|
-
const p = this.expectWord("position");
|
|
3104
|
-
spec.position = p === "overlay" || p === "flow" ? p : { pinned: p };
|
|
3105
|
-
break;
|
|
3106
|
-
}
|
|
3107
|
-
case "size": {
|
|
3108
|
-
const width = this.parseAxisIntent();
|
|
3109
|
-
const height = this.parseAxisIntent();
|
|
3110
|
-
spec.size = { width, height };
|
|
3111
|
-
break;
|
|
3112
|
-
}
|
|
3113
|
-
case "ratio": {
|
|
3114
|
-
const wv = Number(this.expectWord("ratio-w"));
|
|
3115
|
-
const hv = Number(this.expectWord("ratio-h"));
|
|
3116
|
-
spec.size = { ...spec.size ?? {}, width: { ratio: { w: wv, h: hv } } };
|
|
3117
|
-
break;
|
|
3118
|
-
}
|
|
3119
|
-
case "inset":
|
|
3120
|
-
spec.inset = this.parseInsetArgs();
|
|
3121
|
-
break;
|
|
3122
|
-
default:
|
|
3123
|
-
this.issue("warning", "layout-key", `unknown layout key "${key}" skipped`);
|
|
3124
|
-
this.skipToTerminator();
|
|
3125
|
-
}
|
|
3126
|
-
if (this.at("semi") || this.at("newline")) this.pos++;
|
|
3127
|
-
this.ensureProgress(mark);
|
|
3128
|
-
}
|
|
3129
|
-
this.expect("rbrace", "layout-rbrace");
|
|
3130
|
-
return spec;
|
|
3131
|
-
}
|
|
3132
|
-
// A single facet statement `facetname value+` → [FacetName, FacetValue] | null.
|
|
3133
|
-
parseFacetStmt() {
|
|
3134
|
-
const raw = this.expectWord("facet-name");
|
|
3135
|
-
const mapped = FACET_MAP[raw];
|
|
3136
|
-
if (mapped === void 0) {
|
|
3137
|
-
this.issue("warning", "facet-unknown", `unknown facet "${raw}" skipped`);
|
|
3138
|
-
this.skipToTerminator();
|
|
3139
|
-
return null;
|
|
3140
|
-
}
|
|
3141
|
-
if (mapped === null) {
|
|
3142
|
-
this.issue("warning", "facet-nohome", `facet "${raw}" has no AST FacetMap home (\xA73) \u2014 skipped`);
|
|
3143
|
-
this.skipToTerminator();
|
|
3144
|
-
return null;
|
|
3145
|
-
}
|
|
3146
|
-
if (mapped === "inset") {
|
|
3147
|
-
return { name: "inset", value: { inset: this.parseInsetArgs() } };
|
|
3148
|
-
}
|
|
3149
|
-
if (mapped === "border" || mapped.startsWith("border.")) {
|
|
3150
|
-
const border = {};
|
|
3151
|
-
while (this.at("word")) {
|
|
3152
|
-
const w = this.next().value;
|
|
3153
|
-
if (w === "solid" || w === "dashed" || w === "dotted") border.style = w;
|
|
3154
|
-
else if (w.includes(".")) border.color = tokId(w);
|
|
3155
|
-
else border.width = tokId(w);
|
|
3156
|
-
}
|
|
3157
|
-
return { name: mapped, value: { border } };
|
|
3158
|
-
}
|
|
3159
|
-
const t = this.peek();
|
|
3160
|
-
let value;
|
|
3161
|
-
if (t.type === "string") {
|
|
3162
|
-
value = { raw: this.next().value };
|
|
3163
|
-
} else if (t.type === "word") {
|
|
3164
|
-
const v = this.next().value;
|
|
3165
|
-
if (NUMBER_RE.test(v)) value = { literal: Number(v) };
|
|
3166
|
-
else if (isRawValue(v)) value = { raw: v };
|
|
3167
|
-
else if (!v.includes(".")) value = { raw: v };
|
|
3168
|
-
else value = { token: tokId(v) };
|
|
3169
|
-
} else {
|
|
3170
|
-
this.issue("error", "facet-value", `expected a facet value, got ${t.type}`);
|
|
3171
|
-
this.skipToTerminator();
|
|
3172
|
-
return null;
|
|
3173
|
-
}
|
|
3174
|
-
if (!this.at("semi") && !this.at("newline") && !this.at("rbrace")) this.skipToTerminator();
|
|
3175
|
-
return { name: mapped, value };
|
|
3176
|
-
}
|
|
3177
|
-
// `state NAME [when COND] { … }` → a PartVariant with one interaction state.
|
|
3178
|
-
parseStateBlock() {
|
|
3179
|
-
this.next();
|
|
3180
|
-
const name = this.expectWord("state-name");
|
|
3181
|
-
const variant = { states: [name] };
|
|
3182
|
-
if (this.atWord("when")) {
|
|
3183
|
-
this.next();
|
|
3184
|
-
variant.when = this.parseRealizeCond();
|
|
3185
|
-
}
|
|
3186
|
-
this.applyStyleBody(variant);
|
|
3187
|
-
return variant;
|
|
3188
|
-
}
|
|
3189
|
-
// `when <predicate> { … }` → a state-less predicate-conditioned PartVariant. The rich
|
|
3190
|
-
// predicate grammar (proposal 0032) supersedes the flat single-condition form: it carries
|
|
3191
|
-
// OR / NOT / operators / state·context·ancestor leaves. A plain `when prop = value` parses
|
|
3192
|
-
// to the degenerate single-`eq` predicate, so the legacy one-condition case still works.
|
|
3193
|
-
parseWhenBlock() {
|
|
3194
|
-
this.next();
|
|
3195
|
-
const variant = { whenPredicate: this.parsePredicate() };
|
|
3196
|
-
this.applyStyleBody(variant);
|
|
3197
|
-
return variant;
|
|
3198
|
-
}
|
|
3199
|
-
// Parse a `{ facet… / layout… }` style body into a variant's facets/layout.
|
|
3200
|
-
applyStyleBody(target) {
|
|
3201
|
-
this.expect("lbrace", "style-lbrace");
|
|
3202
|
-
const facets2 = {};
|
|
3203
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
3204
|
-
const mark = this.pos;
|
|
3205
|
-
if (this.at("newline") || this.at("semi")) {
|
|
3206
|
-
this.pos++;
|
|
3207
|
-
continue;
|
|
3208
|
-
}
|
|
3209
|
-
if (this.atWord("layout")) {
|
|
3210
|
-
this.next();
|
|
3211
|
-
target.layout = this.parseLayoutBody();
|
|
3212
|
-
} else {
|
|
3213
|
-
const f = this.parseFacetStmt();
|
|
3214
|
-
if (f) facets2[f.name] = f.value;
|
|
3215
|
-
}
|
|
3216
|
-
if (this.at("semi") || this.at("newline")) this.pos++;
|
|
3217
|
-
this.ensureProgress(mark);
|
|
3218
|
-
}
|
|
3219
|
-
this.expect("rbrace", "style-rbrace");
|
|
3220
|
-
if (Object.keys(facets2).length) target.facets = facets2;
|
|
3221
|
-
}
|
|
3222
|
-
// `slot [single|optional|many] [of content | of TYPE]` — a content slot on a part.
|
|
3223
|
-
// Bare `slot` defaults to a single content slot.
|
|
3224
|
-
parseSlot() {
|
|
3225
|
-
let arity = "single";
|
|
3226
|
-
if (this.atWord("single") || this.atWord("optional") || this.atWord("many")) {
|
|
3227
|
-
arity = this.next().value;
|
|
3228
|
-
}
|
|
3229
|
-
let accepts = { kind: "content" };
|
|
3230
|
-
if (this.atWord("of")) {
|
|
3231
|
-
this.next();
|
|
3232
|
-
const w = this.expectWord("slot-accepts");
|
|
3233
|
-
if (w && w !== "content") accepts = { kind: "type", ref: w };
|
|
3234
|
-
}
|
|
3235
|
-
this.skipToTerminator();
|
|
3236
|
-
return { arity, accepts };
|
|
3237
|
-
}
|
|
3238
|
-
// `part NAME { pstmt* }` → [name, Part]
|
|
3239
|
-
parsePart() {
|
|
3240
|
-
const kw = this.next();
|
|
3241
|
-
const nameTok = this.peek();
|
|
3242
|
-
const name = this.expectWord("part-name");
|
|
3243
|
-
if (!name) return null;
|
|
3244
|
-
this.expect("lbrace", "part-lbrace");
|
|
3245
|
-
const part = {};
|
|
3246
|
-
const facets2 = {};
|
|
3247
|
-
const variants = [];
|
|
3248
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
3249
|
-
const mark = this.pos;
|
|
3250
|
-
if (this.at("newline") || this.at("semi")) {
|
|
3251
|
-
this.pos++;
|
|
3252
|
-
continue;
|
|
3253
|
-
}
|
|
3254
|
-
if (this.atWord("layout")) {
|
|
3255
|
-
this.next();
|
|
3256
|
-
if (this.atWord("when")) {
|
|
3257
|
-
this.next();
|
|
3258
|
-
const when = this.parseRealizeCond();
|
|
3259
|
-
variants.push({ when, layout: this.parseLayoutBody() });
|
|
3260
|
-
} else {
|
|
3261
|
-
part.layout = this.parseLayoutBody();
|
|
3262
|
-
}
|
|
3263
|
-
} else if (this.atWord("present")) {
|
|
3264
|
-
this.next();
|
|
3265
|
-
if (this.atWord("when")) this.next();
|
|
3266
|
-
else this.issue("warning", "present-when", "expected 'when' after 'present'");
|
|
3267
|
-
part.presence = this.parsePredicate();
|
|
3268
|
-
} else if (this.atWord("state")) {
|
|
3269
|
-
variants.push(this.parseStateBlock());
|
|
3270
|
-
} else if (this.atWord("when")) {
|
|
3271
|
-
variants.push(this.parseWhenBlock());
|
|
3272
|
-
} else if (this.atWord("slot")) {
|
|
3273
|
-
this.next();
|
|
3274
|
-
part.slot = this.parseSlot();
|
|
3275
|
-
} else {
|
|
3276
|
-
const f = this.parseFacetStmt();
|
|
3277
|
-
if (f) facets2[f.name] = f.value;
|
|
3278
|
-
}
|
|
3279
|
-
if (this.at("semi") || this.at("newline")) this.pos++;
|
|
3280
|
-
this.ensureProgress(mark);
|
|
3281
|
-
}
|
|
3282
|
-
const close = this.expect("rbrace", "part-rbrace");
|
|
3283
|
-
if (Object.keys(facets2).length) part.facets = facets2;
|
|
3284
|
-
if (variants.length) part.variants = variants;
|
|
3285
|
-
this.recordSpan({
|
|
3286
|
-
id: `${this.currentComponentId ?? ""}#part.${seg(name)}`,
|
|
3287
|
-
kind: "part",
|
|
3288
|
-
name,
|
|
3289
|
-
nameRange: this.rangeOf(nameTok, nameTok),
|
|
3290
|
-
fullRange: this.rangeOf(kw, close),
|
|
3291
|
-
parentId: this.currentComponentId ?? void 0
|
|
3292
|
-
});
|
|
3293
|
-
return [name, part];
|
|
3294
|
-
}
|
|
3295
|
-
// `realize TARGET [when COND] { rstmt* }`
|
|
3296
|
-
parseRealize() {
|
|
3297
|
-
this.next();
|
|
3298
|
-
const target = this.expectWord("realize-target");
|
|
3299
|
-
if (!target) return null;
|
|
3300
|
-
let when = null;
|
|
3301
|
-
if (this.atWord("when")) {
|
|
3302
|
-
this.next();
|
|
3303
|
-
when = this.parseRealizeCond();
|
|
3304
|
-
}
|
|
3305
|
-
const r = { target, when };
|
|
3306
|
-
this.expect("lbrace", "realize-lbrace");
|
|
3307
|
-
const wires = [];
|
|
3308
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
3309
|
-
const mark = this.pos;
|
|
3310
|
-
if (this.at("newline") || this.at("semi")) {
|
|
3311
|
-
this.pos++;
|
|
3312
|
-
continue;
|
|
3313
|
-
}
|
|
3314
|
-
const kw = this.peek().value;
|
|
3315
|
-
if (this.atWord("presentation")) {
|
|
3316
|
-
this.next();
|
|
3317
|
-
const surface = this.at("word") ? this.next().value : void 0;
|
|
3318
|
-
const pres = {};
|
|
3319
|
-
if (surface) pres.surface = surface;
|
|
3320
|
-
if (this.at("lbrace")) this.parsePresentationBody(pres);
|
|
3321
|
-
r.presentation = pres;
|
|
3322
|
-
} else if (this.atWord("browse")) {
|
|
3323
|
-
this.next();
|
|
3324
|
-
const mode = this.at("word") ? this.next().value : void 0;
|
|
3325
|
-
r.browse = { mode };
|
|
3326
|
-
if (this.at("lbrace")) this.skipBlock();
|
|
3327
|
-
} else if (this.atWord("commit")) {
|
|
3328
|
-
this.next();
|
|
3329
|
-
r.commit = this.parseCommit();
|
|
3330
|
-
} else if (this.atWord("drop")) {
|
|
3331
|
-
this.next();
|
|
3332
|
-
r.drop = this.parseIdentListWords();
|
|
3333
|
-
} else if (this.atWord("note")) {
|
|
3334
|
-
this.next();
|
|
3335
|
-
r.note = this.at("string") ? this.next().value : this.expectWord("note");
|
|
3336
|
-
} else if (this.atWord("dismiss")) {
|
|
3337
|
-
this.next();
|
|
3338
|
-
const words = this.parseIdentListWords();
|
|
3339
|
-
r.dismiss = words.length === 1 && words[0] === "none" ? [] : words;
|
|
3340
|
-
} else if (this.atWord("backdrop")) {
|
|
3341
|
-
this.next();
|
|
3342
|
-
r.backdrop = this.at("word") ? this.next().value : void 0;
|
|
3343
|
-
this.skipToTerminator();
|
|
3344
|
-
} else if (this.atWord("wire")) {
|
|
3345
|
-
const w = this.parseWire();
|
|
3346
|
-
if (w && w.kind === "wire") wires.push({ from: w.from, to: w.to, ...w.arg ? { arg: w.arg } : {} });
|
|
3347
|
-
} else {
|
|
3348
|
-
this.issue("warning", "realize-stmt", `unknown realize statement "${kw}" skipped`);
|
|
3349
|
-
if (this.at("lbrace")) this.skipBlock();
|
|
3350
|
-
else this.skipToTerminator();
|
|
3351
|
-
}
|
|
3352
|
-
if (this.at("semi") || this.at("newline")) this.pos++;
|
|
3353
|
-
this.ensureProgress(mark);
|
|
3354
|
-
}
|
|
3355
|
-
this.expect("rbrace", "realize-rbrace");
|
|
3356
|
-
if (wires.length) r.wire = wires;
|
|
3357
|
-
return r;
|
|
3358
|
-
}
|
|
3359
|
-
parsePresentationBody(pres) {
|
|
3360
|
-
this.expect("lbrace", "pres-lbrace");
|
|
3361
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
3362
|
-
const mark = this.pos;
|
|
3363
|
-
if (this.at("newline") || this.at("semi")) {
|
|
3364
|
-
this.pos++;
|
|
3365
|
-
continue;
|
|
3366
|
-
}
|
|
3367
|
-
const key = this.expectWord("pres-key");
|
|
3368
|
-
if (key === "place") pres.place = this.expectWord("place");
|
|
3369
|
-
else if (key === "flip") pres.flip = true;
|
|
3370
|
-
else if (key === "matchWidth") pres.matchWidth = true;
|
|
3371
|
-
else this.skipToTerminator();
|
|
3372
|
-
if (this.at("semi") || this.at("newline")) this.pos++;
|
|
3373
|
-
this.ensureProgress(mark);
|
|
3374
|
-
}
|
|
3375
|
-
this.expect("rbrace", "pres-rbrace");
|
|
3376
|
-
}
|
|
3377
|
-
parseCommit() {
|
|
3378
|
-
const modeWord = this.expectWord("commit-mode");
|
|
3379
|
-
const mode = modeWord === "continuous" ? "continuous" : "explicit";
|
|
3380
|
-
const commit = { mode };
|
|
3381
|
-
if (this.atWord("on")) {
|
|
3382
|
-
this.next();
|
|
3383
|
-
commit.on = this.parseIdentListWords();
|
|
3384
|
-
}
|
|
3385
|
-
if (this.at("lbrace")) {
|
|
3386
|
-
this.next();
|
|
3387
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
3388
|
-
if (this.at("newline") || this.at("semi")) {
|
|
3389
|
-
this.pos++;
|
|
3390
|
-
continue;
|
|
3391
|
-
}
|
|
3392
|
-
const key = this.expectWord("commit-key");
|
|
3393
|
-
const val = this.at("word") ? this.next().value : void 0;
|
|
3394
|
-
if (key === "live" && val) commit.live = val;
|
|
3395
|
-
else if (key === "settled" && val) commit.settled = val;
|
|
3396
|
-
else this.skipToTerminator();
|
|
3397
|
-
}
|
|
3398
|
-
this.expect("rbrace", "commit-rbrace");
|
|
3399
|
-
}
|
|
3400
|
-
return commit;
|
|
3401
|
-
}
|
|
3402
|
-
// A run of bare words up to a terminator (for `drop a b c`, `commit … on click enter`).
|
|
3403
|
-
parseIdentListWords() {
|
|
3404
|
-
const out = [];
|
|
3405
|
-
while (this.at("word")) out.push(this.next().value);
|
|
3406
|
-
return out;
|
|
3407
|
-
}
|
|
3408
|
-
parseComponent() {
|
|
3409
|
-
const kw = this.next();
|
|
3410
|
-
const nameTok = this.peek();
|
|
3411
|
-
const name = this.expectWord("component-name");
|
|
3412
|
-
if (!name) return null;
|
|
3413
|
-
const componentId = cmpId(name);
|
|
3414
|
-
const prevComponentId = this.currentComponentId;
|
|
3415
|
-
this.currentComponentId = componentId;
|
|
3416
|
-
let description;
|
|
3417
|
-
if (this.at("string")) description = this.next().value;
|
|
3418
|
-
this.expect("lbrace", "component-lbrace");
|
|
3419
|
-
let archetype = "";
|
|
3420
|
-
let meta;
|
|
3421
|
-
const props = [];
|
|
3422
|
-
const events = [];
|
|
3423
|
-
const uses = [];
|
|
3424
|
-
const coordination = [];
|
|
3425
|
-
const anatomy = {};
|
|
3426
|
-
const realize = [];
|
|
3427
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
3428
|
-
const mark = this.pos;
|
|
3429
|
-
if (this.at("newline") || this.at("semi")) {
|
|
3430
|
-
this.pos++;
|
|
3431
|
-
continue;
|
|
3432
|
-
}
|
|
3433
|
-
if (this.atWord("archetype")) {
|
|
3434
|
-
this.next();
|
|
3435
|
-
archetype = this.expectWord("archetype");
|
|
3436
|
-
} else if (this.atWord("meta")) {
|
|
3437
|
-
meta = this.parseMetaBlock();
|
|
3438
|
-
} else if (this.atWord("props")) {
|
|
3439
|
-
this.parseProps(props);
|
|
3440
|
-
} else if (this.atWord("events")) {
|
|
3441
|
-
this.parseEvents(events);
|
|
3442
|
-
} else if (this.atWord("uses")) {
|
|
3443
|
-
const u = this.parseUses();
|
|
3444
|
-
if (u) uses.push(u);
|
|
3445
|
-
} else if (this.atWord("wire")) {
|
|
3446
|
-
const w = this.parseWire();
|
|
3447
|
-
if (w) coordination.push(w);
|
|
3448
|
-
} else if (this.atWord("gate")) {
|
|
3449
|
-
const g = this.parseGate();
|
|
3450
|
-
if (g) coordination.push(g);
|
|
3451
|
-
} else if (this.atWord("motion")) {
|
|
3452
|
-
this.issue("warning", "motion", "component-level motion parsed as a note \u2014 skipped");
|
|
3453
|
-
this.skipToTerminator();
|
|
3454
|
-
} else if (this.atWord("requires")) {
|
|
3455
|
-
this.next();
|
|
3456
|
-
this.parseIdentListWords();
|
|
3457
|
-
this.issue("warning", "requires", "component `requires` (capability gate) has no AST field \u2014 skipped");
|
|
3458
|
-
} else if (this.atWord("part")) {
|
|
3459
|
-
const p = this.parsePart();
|
|
3460
|
-
if (p) anatomy[p[0]] = p[1];
|
|
3461
|
-
} else if (this.atWord("realize")) {
|
|
3462
|
-
const r = this.parseRealize();
|
|
3463
|
-
if (r) realize.push(r);
|
|
3464
|
-
} else {
|
|
3465
|
-
this.issue("error", "component-stmt", `unexpected ${JSON.stringify(this.peek().value)} in component`);
|
|
3466
|
-
if (this.at("lbrace")) this.skipBlock();
|
|
3467
|
-
else this.skipToTerminator();
|
|
3468
|
-
}
|
|
3469
|
-
this.ensureProgress(mark);
|
|
3470
|
-
}
|
|
3471
|
-
const close = this.expect("rbrace", "component-rbrace");
|
|
3472
|
-
this.currentComponentId = prevComponentId;
|
|
3473
|
-
this.recordSpan({
|
|
3474
|
-
id: componentId,
|
|
3475
|
-
kind: "component",
|
|
3476
|
-
name,
|
|
3477
|
-
nameRange: this.rangeOf(nameTok, nameTok),
|
|
3478
|
-
fullRange: this.rangeOf(kw, close)
|
|
3479
|
-
});
|
|
3480
|
-
const component = {
|
|
3481
|
-
id: componentId,
|
|
3482
|
-
name,
|
|
3483
|
-
archetype,
|
|
3484
|
-
contract: { params: {}, props, events },
|
|
3485
|
-
anatomy,
|
|
3486
|
-
realize
|
|
3487
|
-
};
|
|
3488
|
-
if (description) component.description = description;
|
|
3489
|
-
if (uses.length || coordination.length) {
|
|
3490
|
-
const composition = { uses, coordination };
|
|
3491
|
-
component.composition = composition;
|
|
3492
|
-
}
|
|
3493
|
-
if (meta && Object.keys(meta).length) component.meta = meta;
|
|
3494
|
-
return component;
|
|
3495
|
-
}
|
|
3496
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
3497
|
-
// Header & metadata (proposal 0026)
|
|
3498
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
3499
|
-
parseMetaValue() {
|
|
3500
|
-
const t = this.peek();
|
|
3501
|
-
if (t.type === "string") return this.next().value;
|
|
3502
|
-
if (t.type === "lbracket") {
|
|
3503
|
-
this.next();
|
|
3504
|
-
const arr = [];
|
|
3505
|
-
while (!this.at("rbracket") && !this.at("eof")) {
|
|
3506
|
-
const mark = this.pos;
|
|
3507
|
-
if (this.at("comma") || this.at("newline") || this.at("semi")) {
|
|
3508
|
-
this.pos++;
|
|
3509
|
-
continue;
|
|
3510
|
-
}
|
|
3511
|
-
const it = this.peek();
|
|
3512
|
-
if (it.type === "string") arr.push(this.next().value);
|
|
3513
|
-
else if (it.type === "word") {
|
|
3514
|
-
const w = this.next().value;
|
|
3515
|
-
arr.push(NUMBER_RE.test(w) ? Number(w) : w === "true" ? true : w === "false" ? false : w);
|
|
3516
|
-
} else this.next();
|
|
3517
|
-
this.ensureProgress(mark);
|
|
3518
|
-
}
|
|
3519
|
-
if (this.at("rbracket")) this.next();
|
|
3520
|
-
return arr;
|
|
3521
|
-
}
|
|
3522
|
-
if (t.type === "word") {
|
|
3523
|
-
this.next();
|
|
3524
|
-
return NUMBER_RE.test(t.value) ? Number(t.value) : t.value === "true" ? true : t.value === "false" ? false : t.value;
|
|
3525
|
-
}
|
|
3526
|
-
this.issue("error", "meta-value", `expected a metadata value, got ${t.type}`);
|
|
3527
|
-
return "";
|
|
3528
|
-
}
|
|
3529
|
-
parseMetaBlock() {
|
|
3530
|
-
this.next();
|
|
3531
|
-
this.expect("lbrace", "meta-lbrace");
|
|
3532
|
-
const out = {};
|
|
3533
|
-
while (!this.at("rbrace") && !this.at("eof")) {
|
|
3534
|
-
const mark = this.pos;
|
|
3535
|
-
if (this.at("newline") || this.at("semi")) {
|
|
3536
|
-
this.pos++;
|
|
3537
|
-
continue;
|
|
3538
|
-
}
|
|
3539
|
-
const key = this.expectWord("meta-key");
|
|
3540
|
-
const val = this.parseMetaValue();
|
|
3541
|
-
if (key) out[key] = val;
|
|
3542
|
-
if (this.at("semi") || this.at("newline")) this.pos++;
|
|
3543
|
-
this.ensureProgress(mark);
|
|
3544
|
-
}
|
|
3545
|
-
this.expect("rbrace", "meta-rbrace");
|
|
3546
|
-
return out;
|
|
3547
|
-
}
|
|
3548
|
-
// ── top-level document driver ──
|
|
3549
|
-
parseDocument(opts) {
|
|
3550
|
-
const contextGroups = [];
|
|
3551
|
-
const layer = { id: "layer.foundations", name: "Foundations", order: 0, tokens: [] };
|
|
3552
|
-
const sharedTypes = [];
|
|
3553
|
-
const components = [];
|
|
3554
|
-
const metadata = [];
|
|
3555
|
-
let mildaVer = opts.milda ?? "0.1";
|
|
3556
|
-
let preludeVer = opts.prelude ?? "0.0.2";
|
|
3557
|
-
let docName = opts.name ?? "Design system";
|
|
3558
|
-
let docVersion = "0.0.0";
|
|
3559
|
-
let docDescription;
|
|
3560
|
-
this.skipNewlines();
|
|
3561
|
-
while (!this.at("eof")) {
|
|
3562
|
-
const mark = this.pos;
|
|
3563
|
-
if (this.atWord("milda")) {
|
|
3564
|
-
this.next();
|
|
3565
|
-
mildaVer = this.expectWord("milda-version") || mildaVer;
|
|
3566
|
-
} else if (this.atWord("prelude")) {
|
|
3567
|
-
this.next();
|
|
3568
|
-
preludeVer = this.expectWord("prelude-version") || preludeVer;
|
|
3569
|
-
} else if (this.atWord("meta")) {
|
|
3570
|
-
const m = this.parseMetaBlock();
|
|
3571
|
-
for (const [k, v] of Object.entries(m)) {
|
|
3572
|
-
if (k === "name") docName = String(v);
|
|
3573
|
-
else if (k === "version") docVersion = String(v);
|
|
3574
|
-
else if (k === "description") docDescription = String(v);
|
|
3575
|
-
else metadata.push({ id: `md.${seg(k)}`, key: k, type: metaValueType(v), value: v });
|
|
3576
|
-
}
|
|
3577
|
-
} else if (this.atWord("context-group")) {
|
|
3578
|
-
const cg = this.parseContextGroup();
|
|
3579
|
-
if (cg) contextGroups.push(cg);
|
|
3580
|
-
} else if (this.atWord("foundations")) {
|
|
3581
|
-
this.parseFoundations(layer);
|
|
3582
|
-
} else if (this.atWord("type")) {
|
|
3583
|
-
const st = this.parseSharedType();
|
|
3584
|
-
if (st) sharedTypes.push(st);
|
|
3585
|
-
} else if (this.atWord("component")) {
|
|
3586
|
-
const c = this.parseComponent();
|
|
3587
|
-
if (c) components.push(c);
|
|
3588
|
-
} else {
|
|
3589
|
-
const tok = this.peek();
|
|
3590
|
-
this.issue("error", "top-level", `unexpected top-level ${JSON.stringify(tok.value || tok.type)}`);
|
|
3591
|
-
this.next();
|
|
3592
|
-
}
|
|
3593
|
-
this.ensureProgress(mark);
|
|
3594
|
-
this.skipNewlines();
|
|
3595
|
-
}
|
|
3596
|
-
return {
|
|
3597
|
-
milda: mildaVer,
|
|
3598
|
-
kind: "design-system",
|
|
3599
|
-
prelude: preludeVer,
|
|
3600
|
-
meta: {
|
|
3601
|
-
id: `ds.${seg(docName)}`,
|
|
3602
|
-
name: docName,
|
|
3603
|
-
version: docVersion,
|
|
3604
|
-
...docDescription ? { description: docDescription } : {}
|
|
3605
|
-
},
|
|
3606
|
-
foundations: {
|
|
3607
|
-
contextGroups,
|
|
3608
|
-
layers: layer.tokens.length ? [layer] : [],
|
|
3609
|
-
sharedTypes,
|
|
3610
|
-
metadata
|
|
3611
|
-
},
|
|
3612
|
-
components
|
|
3613
|
-
};
|
|
3614
|
-
}
|
|
3615
|
-
};
|
|
3616
|
-
function parseMildaDocument(src, opts = {}) {
|
|
3617
|
-
let toks;
|
|
3618
|
-
try {
|
|
3619
|
-
toks = lex(src);
|
|
3620
|
-
} catch (e) {
|
|
3621
|
-
const le = e;
|
|
3622
|
-
return {
|
|
3623
|
-
document: emptyDocument(opts),
|
|
3624
|
-
issues: [{ severity: "error", code: "lex", message: le.message, line: le.line ?? 1, col: le.col ?? 1 }],
|
|
3625
|
-
spans: []
|
|
3626
|
-
};
|
|
3627
|
-
}
|
|
3628
|
-
const p = new Parser(toks);
|
|
3629
|
-
const document = p.parseDocument(opts);
|
|
3630
|
-
return { document, issues: p.issues, spans: p.spans };
|
|
3631
|
-
}
|
|
3632
|
-
function emptyDocument(opts) {
|
|
3633
|
-
return {
|
|
3634
|
-
milda: opts.milda ?? "0.1",
|
|
3635
|
-
kind: "design-system",
|
|
3636
|
-
prelude: opts.prelude ?? "0.0.2",
|
|
3637
|
-
meta: { id: `ds.${seg(opts.name ?? "design-system")}`, name: opts.name ?? "Design system", version: "0.0.0" },
|
|
3638
|
-
foundations: { contextGroups: [], layers: [], sharedTypes: [], metadata: [] },
|
|
3639
|
-
components: []
|
|
3640
|
-
};
|
|
3641
|
-
}
|
|
3642
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
3643
|
-
0 && (module.exports = {
|
|
3644
|
-
LexError,
|
|
3645
|
-
TOKEN_VALUE_KIND,
|
|
3646
|
-
allTokensOfType,
|
|
3647
|
-
archetypesByName,
|
|
3648
|
-
atomsByName,
|
|
3649
|
-
collectActiveStates,
|
|
3650
|
-
compositesByName,
|
|
3651
|
-
conformance,
|
|
3652
|
-
findToken,
|
|
3653
|
-
lex,
|
|
3654
|
-
parseMildaDocument,
|
|
3655
|
-
preludeV0,
|
|
3656
|
-
resolveFacets,
|
|
3657
|
-
resolveLayout,
|
|
3658
|
-
resolveToken,
|
|
3659
|
-
validate
|
|
3660
|
-
});
|