@mildastudio/milda 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  archetypesByName: () => archetypesByName,
27
27
  atomsByName: () => atomsByName,
28
28
  collectActiveStates: () => collectActiveStates,
29
+ compositesByName: () => compositesByName,
29
30
  conformance: () => conformance,
30
31
  findToken: () => findToken,
31
32
  lex: () => lex,
@@ -47,6 +48,7 @@ var TOKEN_VALUE_KIND = {
47
48
  shadow: "shadow",
48
49
  typography: "typography",
49
50
  fontFamily: "fontFamily",
51
+ fontWeight: "fontWeight",
50
52
  duration: "duration",
51
53
  opacity: "opacity",
52
54
  blur: "blur",
@@ -576,6 +578,7 @@ var composites = [
576
578
  { atom: "presentation", as: "P", params: { surface: "inline" } }
577
579
  ],
578
580
  composition: [{ kind: "wire", from: "T.change", to: "P.toggle" }],
581
+ emits: [{ name: "toggle" }],
579
582
  note: "\u2248 toggle(expanded) + presentation(inline); exposes expanded"
580
583
  },
581
584
  {
@@ -613,6 +616,7 @@ var composites = [
613
616
  kind: "composite",
614
617
  name: "stepping-nav",
615
618
  builtFrom: ["navigation"],
619
+ emits: [{ name: "stepChange" }, { name: "complete" }],
616
620
  note: "\u2248 navigation(ordered) + constraint; ordered progress through steps; exposes active, complete"
617
621
  },
618
622
  {
@@ -966,7 +970,11 @@ var archetypes = [
966
970
  }
967
971
  ],
968
972
  composes: ["selection.single", "presentation", "browse", "dismiss", "masking"],
969
- realizationNote: "web calendar-grid \xB7 iOS wheel (momentum+detent+haptic) \xB7 Android dialog"
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"
970
978
  },
971
979
  {
972
980
  kind: "archetype",
@@ -998,7 +1006,10 @@ var archetypes = [
998
1006
  }
999
1007
  ],
1000
1008
  composes: ["selection.single", "presentation", "browse", "dismiss", "masking"],
1001
- realizationNote: "iOS wheel \xB7 Android dialog"
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"
1002
1013
  },
1003
1014
  {
1004
1015
  kind: "archetype",
@@ -1021,11 +1032,25 @@ var archetypes = [
1021
1032
  children: [
1022
1033
  { name: "area", role: "container" },
1023
1034
  { name: "hue", role: "container" },
1024
- { name: "input", role: "input" }
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
+ }
1025
1043
  ]
1026
1044
  }
1027
1045
  ],
1028
- composes: ["range", "value-entry", "presentation", "dismiss"]
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"
1029
1054
  },
1030
1055
  {
1031
1056
  kind: "archetype",
@@ -1502,10 +1527,11 @@ var archetypes = [
1502
1527
  },
1503
1528
  {
1504
1529
  kind: "archetype",
1505
- name: "Image",
1530
+ name: "Media",
1506
1531
  group: "Media",
1507
- contract: "bitmap w/ alt + fit",
1532
+ contract: "image/video/audio/embed w/ alt + fit (proposal 0028)",
1508
1533
  produces: { level: "phrase", interactive: false },
1534
+ anatomy: [{ name: "media", role: "content" }],
1509
1535
  composes: []
1510
1536
  },
1511
1537
  {
@@ -1535,6 +1561,14 @@ var facets = [
1535
1561
  covers: "edge: color + width (hairline|thin|thick) + style",
1536
1562
  divergence: "iOS often overlay-stroke"
1537
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)" },
1538
1572
  { name: "corner", covers: "corner radius (token role)" },
1539
1573
  { name: "inset", covers: "internal padding (block/inline)" },
1540
1574
  { name: "gap", covers: "spacing between children" },
@@ -1549,6 +1583,17 @@ var facets = [
1549
1583
  divergence: "web -webkit-line-clamp \xB7 .lineLimit \xB7 maxLines+overflow"
1550
1584
  },
1551
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)" },
1552
1597
  {
1553
1598
  name: "elevation",
1554
1599
  covers: "depth",
@@ -1659,6 +1704,9 @@ var preludeV0 = {
1659
1704
  nonBehaviors
1660
1705
  };
1661
1706
  var atomsByName = Object.fromEntries(atoms.map((a) => [a.name, a]));
1707
+ var compositesByName = Object.fromEntries(
1708
+ composites.map((c) => [c.name, c])
1709
+ );
1662
1710
  var archetypesByName = Object.fromEntries(
1663
1711
  archetypes.map((a) => [a.name, a])
1664
1712
  );
@@ -1734,6 +1782,8 @@ function facetTokenRefs(facets2) {
1734
1782
  if ("token" in v) ids.push(v.token);
1735
1783
  else if ("inset" in v) {
1736
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);
1737
1787
  }
1738
1788
  }
1739
1789
  return ids;
@@ -1766,12 +1816,13 @@ function validate(doc, prelude) {
1766
1816
  for (const tok of layer.tokens) {
1767
1817
  if (tok.value.kind === "alias") {
1768
1818
  const where = `token ${tok.name} (${tok.id})`;
1769
- if (!groupIds.has(tok.value.by)) {
1819
+ if (tok.value.by !== "" && !groupIds.has(tok.value.by)) {
1770
1820
  issues.push({
1771
1821
  severity: "error",
1772
1822
  code: "alias.unknownGroup",
1773
1823
  message: `alias by unknown context group "${tok.value.by}"`,
1774
- where
1824
+ where,
1825
+ id: tok.id
1775
1826
  });
1776
1827
  }
1777
1828
  for (const refId of Object.values(tok.value.refs)) {
@@ -1780,7 +1831,8 @@ function validate(doc, prelude) {
1780
1831
  severity: "error",
1781
1832
  code: "alias.danglingRef",
1782
1833
  message: `alias ref points at missing token "${refId}"`,
1783
- where
1834
+ where,
1835
+ id: tok.id
1784
1836
  });
1785
1837
  }
1786
1838
  }
@@ -1789,12 +1841,13 @@ function validate(doc, prelude) {
1789
1841
  }
1790
1842
  for (const comp of doc.components) {
1791
1843
  const where = `component ${comp.name} (${comp.id})`;
1792
- if (!archetypeNames.has(comp.archetype)) {
1844
+ if (comp.archetype !== "" && !archetypeNames.has(comp.archetype)) {
1793
1845
  issues.push({
1794
1846
  severity: "error",
1795
1847
  code: "component.unknownArchetype",
1796
1848
  message: `unknown archetype "${comp.archetype}"`,
1797
- where
1849
+ where,
1850
+ id: comp.id
1798
1851
  });
1799
1852
  }
1800
1853
  const propIds = new Set(comp.contract.props.map((p) => p.id));
@@ -1804,7 +1857,8 @@ function validate(doc, prelude) {
1804
1857
  severity: "error",
1805
1858
  code: "param.danglingProp",
1806
1859
  message: `param "${param}" binds missing prop "${binding.prop}"`,
1807
- where
1860
+ where,
1861
+ id: comp.id
1808
1862
  });
1809
1863
  }
1810
1864
  }
@@ -1825,7 +1879,8 @@ function checkPart(comp, partName, part, ctx) {
1825
1879
  severity: "error",
1826
1880
  code: "facet.unknown",
1827
1881
  message: `unknown facet "${name}"`,
1828
- where: scope
1882
+ where: scope,
1883
+ id: comp.id
1829
1884
  });
1830
1885
  }
1831
1886
  }
@@ -1835,7 +1890,8 @@ function checkPart(comp, partName, part, ctx) {
1835
1890
  severity: "error",
1836
1891
  code: "facet.danglingToken",
1837
1892
  message: `facet references missing token "${id}"`,
1838
- where: scope
1893
+ where: scope,
1894
+ id: comp.id
1839
1895
  });
1840
1896
  }
1841
1897
  }
@@ -1847,7 +1903,8 @@ function checkPart(comp, partName, part, ctx) {
1847
1903
  severity: "error",
1848
1904
  code: "layout.danglingToken",
1849
1905
  message: `layout references missing token "${id}"`,
1850
- where: scope
1906
+ where: scope,
1907
+ id: comp.id
1851
1908
  });
1852
1909
  }
1853
1910
  }
@@ -1861,7 +1918,8 @@ function checkPart(comp, partName, part, ctx) {
1861
1918
  severity: "error",
1862
1919
  code: "state.unknown",
1863
1920
  message: `unknown state "${stateName}"`,
1864
- where: scope
1921
+ where: scope,
1922
+ id: comp.id
1865
1923
  });
1866
1924
  }
1867
1925
  checkFacets(override?.facets, scope);
@@ -1939,13 +1997,16 @@ var PUNCT = {
1939
1997
  "}": "rbrace",
1940
1998
  "(": "lparen",
1941
1999
  ")": "rparen",
2000
+ "[": "lbracket",
2001
+ "]": "rbracket",
1942
2002
  "<": "lt",
1943
2003
  ">": "gt",
1944
2004
  ":": "colon",
1945
2005
  "=": "equals",
1946
2006
  ",": "comma",
1947
2007
  ";": "semi",
1948
- "?": "question"
2008
+ "?": "question",
2009
+ "@": "at"
1949
2010
  };
1950
2011
  function lex(src) {
1951
2012
  const tokens = [];
@@ -2016,6 +2077,21 @@ function lex(src) {
2016
2077
  push("arrow", "->", startPos, startLine, startCol);
2017
2078
  continue;
2018
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
+ }
2019
2095
  if (PUNCT[c]) {
2020
2096
  advance();
2021
2097
  push(PUNCT[c], c, startPos, startLine, startCol);
@@ -2043,10 +2119,24 @@ var ctxId = (groupName, ctxName) => `ctx.${seg(groupName)}.${seg(ctxName)}`;
2043
2119
  var tokId = (path) => `tok.${path.replace(/\//g, ".")}`;
2044
2120
  var stId = (name) => `st.${seg(name)}`;
2045
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
+ }
2046
2128
  var FACET_MAP = {
2047
2129
  fill: "fill",
2048
2130
  ink: "ink",
2049
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",
2050
2140
  corner: "corner",
2051
2141
  inset: "inset",
2052
2142
  gap: "gap",
@@ -2058,9 +2148,9 @@ var FACET_MAP = {
2058
2148
  opacity: "opacity",
2059
2149
  ring: "ring",
2060
2150
  backdrop: "backdrop",
2061
- weight: null,
2062
- font: null,
2063
- slant: null
2151
+ weight: "weight",
2152
+ font: "font",
2153
+ slant: "slant"
2064
2154
  };
2065
2155
  var TYPE_MAP = {
2066
2156
  color: "color",
@@ -2072,6 +2162,8 @@ var TYPE_MAP = {
2072
2162
  typography: "typography",
2073
2163
  "font-family": "fontFamily",
2074
2164
  fontFamily: "fontFamily",
2165
+ "font-weight": "fontWeight",
2166
+ fontWeight: "fontWeight",
2075
2167
  duration: "duration",
2076
2168
  opacity: "opacity",
2077
2169
  blur: "blur",
@@ -2095,10 +2187,34 @@ function hexToColor(hex) {
2095
2187
  if (h.length === 8) value.alpha = byte(3);
2096
2188
  return value;
2097
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
+ }
2098
2208
  var Parser = class {
2099
2209
  constructor(toks) {
2100
2210
  this.pos = 0;
2101
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;
2102
2218
  // Tokens synthesized while parsing (inline literals hoisted out of alias slots —
2103
2219
  // the canonical AST's `refs` holds only token ids, so an inline value must become
2104
2220
  // its own token). Deduped by (type, serialized value). Flushed into the layer.
@@ -2118,6 +2234,73 @@ var Parser = class {
2118
2234
  this.issue("warning", "inline-literal", `inline ${astType} literal in an alias slot hoisted to token ${id}`, tok);
2119
2235
  return id;
2120
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
+ }
2121
2304
  peek() {
2122
2305
  return this.toks[this.pos];
2123
2306
  }
@@ -2143,6 +2326,28 @@ var Parser = class {
2143
2326
  if (!this.at("eof")) this.next();
2144
2327
  return true;
2145
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
+ }
2146
2351
  issue(severity, code, message, tok = this.peek()) {
2147
2352
  this.issues.push({ severity, code, message, line: tok.line, col: tok.col });
2148
2353
  }
@@ -2173,7 +2378,8 @@ var Parser = class {
2173
2378
  }
2174
2379
  // ── context-group NAME { ctx ctx … } ──
2175
2380
  parseContextGroup() {
2176
- this.next();
2381
+ const kw = this.next();
2382
+ const nameTok = this.peek();
2177
2383
  const name = this.expectWord("cg-name");
2178
2384
  this.expect("lbrace", "cg-lbrace");
2179
2385
  const contexts = [];
@@ -2182,11 +2388,27 @@ var Parser = class {
2182
2388
  this.pos++;
2183
2389
  continue;
2184
2390
  }
2391
+ const ctxTok = this.peek();
2185
2392
  const ctxName = this.expectWord("cg-ctx");
2186
2393
  if (!ctxName) break;
2187
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
+ });
2188
2403
  }
2189
- this.expect("rbrace", "cg-rbrace");
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
+ });
2190
2412
  return { id: cgId(name), name, contexts };
2191
2413
  }
2192
2414
  // ── raw value literal → a JS value (string | number | nested object) ──
@@ -2195,6 +2417,12 @@ var Parser = class {
2195
2417
  if (t.type === "string") return this.next().value;
2196
2418
  if (t.type === "lbrace") return this.parseStructured();
2197
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
+ }
2198
2426
  const w = this.next().value;
2199
2427
  return NUMBER_RE.test(w) ? Number(w) : w;
2200
2428
  }
@@ -2236,6 +2464,7 @@ var Parser = class {
2236
2464
  }
2237
2465
  }
2238
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 } };
2239
2468
  if (astType === "duration" && typeof raw === "number") return { kind: "raw", value: { ms: raw } };
2240
2469
  if (astType === "tonal" && typeof raw === "number") return { kind: "raw", value: { level: raw } };
2241
2470
  return { kind: "raw", value: raw };
@@ -2261,7 +2490,11 @@ var Parser = class {
2261
2490
  this.expect("arrow", "alias-arrow");
2262
2491
  const slot = this.peek();
2263
2492
  let refId = "";
2264
- if (slot.type === "string" || slot.type === "lbrace" || slot.type === "word" && NUMBER_RE.test(slot.value)) {
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)) {
2265
2498
  refId = this.hoistLiteral(astType, this.parseRawLiteral(), slot);
2266
2499
  } else {
2267
2500
  const refPath = this.expectWord("alias-ref");
@@ -2303,6 +2536,15 @@ var Parser = class {
2303
2536
  return this.typedRaw(astType, raw, t);
2304
2537
  }
2305
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
+ }
2306
2548
  const refPath = this.next().value;
2307
2549
  this.issue("warning", "ref-sugar", `unconditional ref "${refPath}" lowered to a default-only alias`, t);
2308
2550
  return { kind: "alias", by: "", refs: { default: tokId(refPath) } };
@@ -2342,6 +2584,7 @@ var Parser = class {
2342
2584
  }
2343
2585
  parseTokenDecl(_order) {
2344
2586
  const startTok = this.next();
2587
+ const nameTok = this.peek();
2345
2588
  const path = this.expectWord("token-path");
2346
2589
  if (!path) return null;
2347
2590
  this.expect("colon", "token-colon");
@@ -2350,6 +2593,13 @@ var Parser = class {
2350
2593
  if (!astType) this.issue("warning", "token-type", `non-canonical token type "${typeName}" kept as-is`, startTok);
2351
2594
  this.expect("equals", "token-equals");
2352
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
+ });
2353
2603
  return {
2354
2604
  id: tokId(path),
2355
2605
  name: path,
@@ -2461,11 +2711,19 @@ var Parser = class {
2461
2711
  return { kind: "ref", ref: stId(w) };
2462
2712
  }
2463
2713
  parseSharedType() {
2464
- this.next();
2714
+ const kw = this.next();
2715
+ const nameTok = this.peek();
2465
2716
  const name = this.expectWord("type-name");
2466
2717
  if (!name) return null;
2467
2718
  this.expect("equals", "type-equals");
2468
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
+ });
2469
2727
  return { id: stId(name), name, definition };
2470
2728
  }
2471
2729
  // ════════════════════════════════════════════════════════════════════════
@@ -2495,6 +2753,125 @@ var Parser = class {
2495
2753
  }
2496
2754
  return { prop, eq };
2497
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
+ }
2498
2875
  parsePropDefault() {
2499
2876
  const t = this.peek();
2500
2877
  if (t.type === "string") return this.next().value;
@@ -2517,6 +2894,7 @@ var Parser = class {
2517
2894
  return void 0;
2518
2895
  }
2519
2896
  parsePropDecl() {
2897
+ const nameTok = this.peek();
2520
2898
  const name = this.expectWord("prop-name");
2521
2899
  if (!name) return null;
2522
2900
  const type = this.parseTypeExpr();
@@ -2530,6 +2908,28 @@ var Parser = class {
2530
2908
  this.next();
2531
2909
  prop.binding = this.expectWord("bind-target");
2532
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
+ });
2533
2933
  return prop;
2534
2934
  }
2535
2935
  parseProps(props) {
@@ -2557,6 +2957,7 @@ var Parser = class {
2557
2957
  this.pos++;
2558
2958
  continue;
2559
2959
  }
2960
+ const nameTok = this.peek();
2560
2961
  const name = this.expectWord("event-name");
2561
2962
  if (name) {
2562
2963
  const e = { name };
@@ -2565,6 +2966,14 @@ var Parser = class {
2565
2966
  e.from = this.expectWord("event-from");
2566
2967
  }
2567
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
+ });
2568
2977
  }
2569
2978
  if (this.at("semi") || this.at("newline")) this.pos++;
2570
2979
  this.ensureProgress(mark);
@@ -2737,6 +3146,16 @@ var Parser = class {
2737
3146
  if (mapped === "inset") {
2738
3147
  return { name: "inset", value: { inset: this.parseInsetArgs() } };
2739
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
+ }
2740
3159
  const t = this.peek();
2741
3160
  let value;
2742
3161
  if (t.type === "string") {
@@ -2745,6 +3164,7 @@ var Parser = class {
2745
3164
  const v = this.next().value;
2746
3165
  if (NUMBER_RE.test(v)) value = { literal: Number(v) };
2747
3166
  else if (isRawValue(v)) value = { raw: v };
3167
+ else if (!v.includes(".")) value = { raw: v };
2748
3168
  else value = { token: tokId(v) };
2749
3169
  } else {
2750
3170
  this.issue("error", "facet-value", `expected a facet value, got ${t.type}`);
@@ -2766,11 +3186,13 @@ var Parser = class {
2766
3186
  this.applyStyleBody(variant);
2767
3187
  return variant;
2768
3188
  }
2769
- // `when COND { … }` → a state-less prop-conditioned PartVariant (proposal 0025).
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.
2770
3193
  parseWhenBlock() {
2771
3194
  this.next();
2772
- const when = this.parseRealizeCond();
2773
- const variant = { when };
3195
+ const variant = { whenPredicate: this.parsePredicate() };
2774
3196
  this.applyStyleBody(variant);
2775
3197
  return variant;
2776
3198
  }
@@ -2797,9 +3219,26 @@ var Parser = class {
2797
3219
  this.expect("rbrace", "style-rbrace");
2798
3220
  if (Object.keys(facets2).length) target.facets = facets2;
2799
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
+ }
2800
3238
  // `part NAME { pstmt* }` → [name, Part]
2801
3239
  parsePart() {
2802
- this.next();
3240
+ const kw = this.next();
3241
+ const nameTok = this.peek();
2803
3242
  const name = this.expectWord("part-name");
2804
3243
  if (!name) return null;
2805
3244
  this.expect("lbrace", "part-lbrace");
@@ -2821,13 +3260,18 @@ var Parser = class {
2821
3260
  } else {
2822
3261
  part.layout = this.parseLayoutBody();
2823
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();
2824
3268
  } else if (this.atWord("state")) {
2825
3269
  variants.push(this.parseStateBlock());
2826
3270
  } else if (this.atWord("when")) {
2827
3271
  variants.push(this.parseWhenBlock());
2828
3272
  } else if (this.atWord("slot")) {
2829
- this.issue("warning", "slot", "part `slot` has no AST Part field yet \u2014 skipped");
2830
- this.skipToTerminator();
3273
+ this.next();
3274
+ part.slot = this.parseSlot();
2831
3275
  } else {
2832
3276
  const f = this.parseFacetStmt();
2833
3277
  if (f) facets2[f.name] = f.value;
@@ -2835,9 +3279,17 @@ var Parser = class {
2835
3279
  if (this.at("semi") || this.at("newline")) this.pos++;
2836
3280
  this.ensureProgress(mark);
2837
3281
  }
2838
- this.expect("rbrace", "part-rbrace");
3282
+ const close = this.expect("rbrace", "part-rbrace");
2839
3283
  if (Object.keys(facets2).length) part.facets = facets2;
2840
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
+ });
2841
3293
  return [name, part];
2842
3294
  }
2843
3295
  // `realize TARGET [when COND] { rstmt* }`
@@ -2881,6 +3333,14 @@ var Parser = class {
2881
3333
  } else if (this.atWord("note")) {
2882
3334
  this.next();
2883
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();
2884
3344
  } else if (this.atWord("wire")) {
2885
3345
  const w = this.parseWire();
2886
3346
  if (w && w.kind === "wire") wires.push({ from: w.from, to: w.to, ...w.arg ? { arg: w.arg } : {} });
@@ -2922,6 +3382,21 @@ var Parser = class {
2922
3382
  this.next();
2923
3383
  commit.on = this.parseIdentListWords();
2924
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
+ }
2925
3400
  return commit;
2926
3401
  }
2927
3402
  // A run of bare words up to a terminator (for `drop a b c`, `commit … on click enter`).
@@ -2931,13 +3406,18 @@ var Parser = class {
2931
3406
  return out;
2932
3407
  }
2933
3408
  parseComponent() {
2934
- this.next();
3409
+ const kw = this.next();
3410
+ const nameTok = this.peek();
2935
3411
  const name = this.expectWord("component-name");
2936
3412
  if (!name) return null;
3413
+ const componentId = cmpId(name);
3414
+ const prevComponentId = this.currentComponentId;
3415
+ this.currentComponentId = componentId;
2937
3416
  let description;
2938
3417
  if (this.at("string")) description = this.next().value;
2939
3418
  this.expect("lbrace", "component-lbrace");
2940
3419
  let archetype = "";
3420
+ let meta;
2941
3421
  const props = [];
2942
3422
  const events = [];
2943
3423
  const uses = [];
@@ -2953,6 +3433,8 @@ var Parser = class {
2953
3433
  if (this.atWord("archetype")) {
2954
3434
  this.next();
2955
3435
  archetype = this.expectWord("archetype");
3436
+ } else if (this.atWord("meta")) {
3437
+ meta = this.parseMetaBlock();
2956
3438
  } else if (this.atWord("props")) {
2957
3439
  this.parseProps(props);
2958
3440
  } else if (this.atWord("events")) {
@@ -2986,9 +3468,17 @@ var Parser = class {
2986
3468
  }
2987
3469
  this.ensureProgress(mark);
2988
3470
  }
2989
- this.expect("rbrace", "component-rbrace");
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
+ });
2990
3480
  const component = {
2991
- id: cmpId(name),
3481
+ id: componentId,
2992
3482
  name,
2993
3483
  archetype,
2994
3484
  contract: { params: {}, props, events },
@@ -3000,18 +3490,91 @@ var Parser = class {
3000
3490
  const composition = { uses, coordination };
3001
3491
  component.composition = composition;
3002
3492
  }
3493
+ if (meta && Object.keys(meta).length) component.meta = meta;
3003
3494
  return component;
3004
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
+ }
3005
3548
  // ── top-level document driver ──
3006
3549
  parseDocument(opts) {
3007
3550
  const contextGroups = [];
3008
3551
  const layer = { id: "layer.foundations", name: "Foundations", order: 0, tokens: [] };
3009
3552
  const sharedTypes = [];
3010
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;
3011
3560
  this.skipNewlines();
3012
3561
  while (!this.at("eof")) {
3013
3562
  const mark = this.pos;
3014
- if (this.atWord("context-group")) {
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")) {
3015
3578
  const cg = this.parseContextGroup();
3016
3579
  if (cg) contextGroups.push(cg);
3017
3580
  } else if (this.atWord("foundations")) {
@@ -3031,19 +3594,20 @@ var Parser = class {
3031
3594
  this.skipNewlines();
3032
3595
  }
3033
3596
  return {
3034
- milda: opts.milda ?? "0.1",
3597
+ milda: mildaVer,
3035
3598
  kind: "design-system",
3036
- prelude: opts.prelude ?? "0.0.2",
3599
+ prelude: preludeVer,
3037
3600
  meta: {
3038
- id: `ds.${seg(opts.name ?? "design-system")}`,
3039
- name: opts.name ?? "Design system",
3040
- version: "0.0.0"
3601
+ id: `ds.${seg(docName)}`,
3602
+ name: docName,
3603
+ version: docVersion,
3604
+ ...docDescription ? { description: docDescription } : {}
3041
3605
  },
3042
3606
  foundations: {
3043
3607
  contextGroups,
3044
3608
  layers: layer.tokens.length ? [layer] : [],
3045
3609
  sharedTypes,
3046
- metadata: []
3610
+ metadata
3047
3611
  },
3048
3612
  components
3049
3613
  };
@@ -3057,12 +3621,13 @@ function parseMildaDocument(src, opts = {}) {
3057
3621
  const le = e;
3058
3622
  return {
3059
3623
  document: emptyDocument(opts),
3060
- issues: [{ severity: "error", code: "lex", message: le.message, line: le.line ?? 1, col: le.col ?? 1 }]
3624
+ issues: [{ severity: "error", code: "lex", message: le.message, line: le.line ?? 1, col: le.col ?? 1 }],
3625
+ spans: []
3061
3626
  };
3062
3627
  }
3063
3628
  const p = new Parser(toks);
3064
3629
  const document = p.parseDocument(opts);
3065
- return { document, issues: p.issues };
3630
+ return { document, issues: p.issues, spans: p.spans };
3066
3631
  }
3067
3632
  function emptyDocument(opts) {
3068
3633
  return {
@@ -3082,6 +3647,7 @@ function emptyDocument(opts) {
3082
3647
  archetypesByName,
3083
3648
  atomsByName,
3084
3649
  collectActiveStates,
3650
+ compositesByName,
3085
3651
  conformance,
3086
3652
  findToken,
3087
3653
  lex,