@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.mjs CHANGED
@@ -7,6 +7,7 @@ var TOKEN_VALUE_KIND = {
7
7
  shadow: "shadow",
8
8
  typography: "typography",
9
9
  fontFamily: "fontFamily",
10
+ fontWeight: "fontWeight",
10
11
  duration: "duration",
11
12
  opacity: "opacity",
12
13
  blur: "blur",
@@ -536,6 +537,7 @@ var composites = [
536
537
  { atom: "presentation", as: "P", params: { surface: "inline" } }
537
538
  ],
538
539
  composition: [{ kind: "wire", from: "T.change", to: "P.toggle" }],
540
+ emits: [{ name: "toggle" }],
539
541
  note: "\u2248 toggle(expanded) + presentation(inline); exposes expanded"
540
542
  },
541
543
  {
@@ -573,6 +575,7 @@ var composites = [
573
575
  kind: "composite",
574
576
  name: "stepping-nav",
575
577
  builtFrom: ["navigation"],
578
+ emits: [{ name: "stepChange" }, { name: "complete" }],
576
579
  note: "\u2248 navigation(ordered) + constraint; ordered progress through steps; exposes active, complete"
577
580
  },
578
581
  {
@@ -926,7 +929,11 @@ var archetypes = [
926
929
  }
927
930
  ],
928
931
  composes: ["selection.single", "presentation", "browse", "dismiss", "masking"],
929
- realizationNote: "web calendar-grid \xB7 iOS wheel (momentum+detent+haptic) \xB7 Android dialog"
932
+ // Behavior is the shared framework-agnostic calendar core (proposal 0029,
933
+ // targets/behavior/calendar.ts); node.calendar picks single|range + weekStartsOn
934
+ // + disabledWeekdays. The `day` grid cell carries calendar states (today,
935
+ // outsideMonth, selected, rangeStart, rangeEnd, inRange) rendered as data-* attrs.
936
+ realizationNote: "web calendar-grid (single|range via node.calendar) \xB7 iOS wheel (momentum+detent+haptic) \xB7 Android dialog"
930
937
  },
931
938
  {
932
939
  kind: "archetype",
@@ -958,7 +965,10 @@ var archetypes = [
958
965
  }
959
966
  ],
960
967
  composes: ["selection.single", "presentation", "browse", "dismiss", "masking"],
961
- realizationNote: "iOS wheel \xB7 Android dialog"
968
+ // Behavior is the shared framework-agnostic time core (proposal 0029,
969
+ // targets/behavior/timepicker.ts); node.timePicker picks the slot step + 12/24h.
970
+ // Web realization = a listbox of time slots; iOS wheel is a per-platform view.
971
+ realizationNote: "web time-slot listbox (step via node.timePicker) \xB7 iOS wheel \xB7 Android dialog"
962
972
  },
963
973
  {
964
974
  kind: "archetype",
@@ -981,11 +991,25 @@ var archetypes = [
981
991
  children: [
982
992
  { name: "area", role: "container" },
983
993
  { name: "hue", role: "container" },
984
- { name: "input", role: "input" }
994
+ { name: "alpha", role: "container" },
995
+ { name: "eyedropper", role: "control" },
996
+ { name: "input", role: "input" },
997
+ {
998
+ name: "swatches",
999
+ role: "container",
1000
+ children: [{ name: "swatch", role: "item" }]
1001
+ }
985
1002
  ]
986
1003
  }
987
1004
  ],
988
- composes: ["range", "value-entry", "presentation", "dismiss"]
1005
+ composes: ["range", "value-entry", "presentation", "dismiss"],
1006
+ // Behavior is the shared framework-agnostic color core (proposal 0029/0035,
1007
+ // targets/behavior/color.ts): HSV<->RGB<->hex + OKLCH math and the area/hue/alpha
1008
+ // geometry, so every target computes an identical picker. node.colorPicker (0035)
1009
+ // picks the working space (srgb|oklch), the editable format tabs (hex/rgb/hsl/hsb),
1010
+ // opacity, eyedropper, preset swatches, and popover-vs-inline layout. The `alpha`,
1011
+ // `eyedropper` and `swatches` parts render only when their config flag is on.
1012
+ realizationNote: "web: SV/OKLCH area + hue + optional alpha, format tabs, eyedropper, swatches, popover|inline \xB7 native color input fallback"
989
1013
  },
990
1014
  {
991
1015
  kind: "archetype",
@@ -1462,10 +1486,11 @@ var archetypes = [
1462
1486
  },
1463
1487
  {
1464
1488
  kind: "archetype",
1465
- name: "Image",
1489
+ name: "Media",
1466
1490
  group: "Media",
1467
- contract: "bitmap w/ alt + fit",
1491
+ contract: "image/video/audio/embed w/ alt + fit (proposal 0028)",
1468
1492
  produces: { level: "phrase", interactive: false },
1493
+ anatomy: [{ name: "media", role: "content" }],
1469
1494
  composes: []
1470
1495
  },
1471
1496
  {
@@ -1495,6 +1520,14 @@ var facets = [
1495
1520
  covers: "edge: color + width (hairline|thin|thick) + style",
1496
1521
  divergence: "iOS often overlay-stroke"
1497
1522
  },
1523
+ { name: "border.top", covers: "top edge (physical side)" },
1524
+ { name: "border.right", covers: "right edge (physical side)" },
1525
+ { name: "border.bottom", covers: "bottom edge (physical side)" },
1526
+ { name: "border.left", covers: "left edge (physical side)" },
1527
+ { name: "border.inline-start", covers: "leading edge (logical, writing-direction)" },
1528
+ { name: "border.inline-end", covers: "trailing edge (logical, writing-direction)" },
1529
+ { name: "border.block-start", covers: "block-start edge (logical)" },
1530
+ { name: "border.block-end", covers: "block-end edge (logical)" },
1498
1531
  { name: "corner", covers: "corner radius (token role)" },
1499
1532
  { name: "inset", covers: "internal padding (block/inline)" },
1500
1533
  { name: "gap", covers: "spacing between children" },
@@ -1509,6 +1542,17 @@ var facets = [
1509
1542
  divergence: "web -webkit-line-clamp \xB7 .lineLimit \xB7 maxLines+overflow"
1510
1543
  },
1511
1544
  { name: "textWrap", covers: "wrap | nowrap | break" },
1545
+ {
1546
+ name: "weight",
1547
+ covers: "font-weight (typography axis \xA73, split out of the `text` role bundle)",
1548
+ divergence: "web font-weight \xB7 .fontWeight \xB7 TextStyle.fontWeight"
1549
+ },
1550
+ {
1551
+ name: "font",
1552
+ covers: "font-family (typography axis \xA73)",
1553
+ divergence: "web font-family \xB7 UIFont \xB7 Typeface"
1554
+ },
1555
+ { name: "slant", covers: "font-style: normal | italic | oblique (typography axis \xA73)" },
1512
1556
  {
1513
1557
  name: "elevation",
1514
1558
  covers: "depth",
@@ -1619,6 +1663,9 @@ var preludeV0 = {
1619
1663
  nonBehaviors
1620
1664
  };
1621
1665
  var atomsByName = Object.fromEntries(atoms.map((a) => [a.name, a]));
1666
+ var compositesByName = Object.fromEntries(
1667
+ composites.map((c) => [c.name, c])
1668
+ );
1622
1669
  var archetypesByName = Object.fromEntries(
1623
1670
  archetypes.map((a) => [a.name, a])
1624
1671
  );
@@ -1694,6 +1741,8 @@ function facetTokenRefs(facets2) {
1694
1741
  if ("token" in v) ids.push(v.token);
1695
1742
  else if ("inset" in v) {
1696
1743
  for (const edge of Object.values(v.inset)) if (typeof edge === "string") ids.push(edge);
1744
+ } else if ("border" in v) {
1745
+ if (v.border.color) ids.push(v.border.color);
1697
1746
  }
1698
1747
  }
1699
1748
  return ids;
@@ -1726,12 +1775,13 @@ function validate(doc, prelude) {
1726
1775
  for (const tok of layer.tokens) {
1727
1776
  if (tok.value.kind === "alias") {
1728
1777
  const where = `token ${tok.name} (${tok.id})`;
1729
- if (!groupIds.has(tok.value.by)) {
1778
+ if (tok.value.by !== "" && !groupIds.has(tok.value.by)) {
1730
1779
  issues.push({
1731
1780
  severity: "error",
1732
1781
  code: "alias.unknownGroup",
1733
1782
  message: `alias by unknown context group "${tok.value.by}"`,
1734
- where
1783
+ where,
1784
+ id: tok.id
1735
1785
  });
1736
1786
  }
1737
1787
  for (const refId of Object.values(tok.value.refs)) {
@@ -1740,7 +1790,8 @@ function validate(doc, prelude) {
1740
1790
  severity: "error",
1741
1791
  code: "alias.danglingRef",
1742
1792
  message: `alias ref points at missing token "${refId}"`,
1743
- where
1793
+ where,
1794
+ id: tok.id
1744
1795
  });
1745
1796
  }
1746
1797
  }
@@ -1749,12 +1800,13 @@ function validate(doc, prelude) {
1749
1800
  }
1750
1801
  for (const comp of doc.components) {
1751
1802
  const where = `component ${comp.name} (${comp.id})`;
1752
- if (!archetypeNames.has(comp.archetype)) {
1803
+ if (comp.archetype !== "" && !archetypeNames.has(comp.archetype)) {
1753
1804
  issues.push({
1754
1805
  severity: "error",
1755
1806
  code: "component.unknownArchetype",
1756
1807
  message: `unknown archetype "${comp.archetype}"`,
1757
- where
1808
+ where,
1809
+ id: comp.id
1758
1810
  });
1759
1811
  }
1760
1812
  const propIds = new Set(comp.contract.props.map((p) => p.id));
@@ -1764,7 +1816,8 @@ function validate(doc, prelude) {
1764
1816
  severity: "error",
1765
1817
  code: "param.danglingProp",
1766
1818
  message: `param "${param}" binds missing prop "${binding.prop}"`,
1767
- where
1819
+ where,
1820
+ id: comp.id
1768
1821
  });
1769
1822
  }
1770
1823
  }
@@ -1785,7 +1838,8 @@ function checkPart(comp, partName, part, ctx) {
1785
1838
  severity: "error",
1786
1839
  code: "facet.unknown",
1787
1840
  message: `unknown facet "${name}"`,
1788
- where: scope
1841
+ where: scope,
1842
+ id: comp.id
1789
1843
  });
1790
1844
  }
1791
1845
  }
@@ -1795,7 +1849,8 @@ function checkPart(comp, partName, part, ctx) {
1795
1849
  severity: "error",
1796
1850
  code: "facet.danglingToken",
1797
1851
  message: `facet references missing token "${id}"`,
1798
- where: scope
1852
+ where: scope,
1853
+ id: comp.id
1799
1854
  });
1800
1855
  }
1801
1856
  }
@@ -1807,7 +1862,8 @@ function checkPart(comp, partName, part, ctx) {
1807
1862
  severity: "error",
1808
1863
  code: "layout.danglingToken",
1809
1864
  message: `layout references missing token "${id}"`,
1810
- where: scope
1865
+ where: scope,
1866
+ id: comp.id
1811
1867
  });
1812
1868
  }
1813
1869
  }
@@ -1821,7 +1877,8 @@ function checkPart(comp, partName, part, ctx) {
1821
1877
  severity: "error",
1822
1878
  code: "state.unknown",
1823
1879
  message: `unknown state "${stateName}"`,
1824
- where: scope
1880
+ where: scope,
1881
+ id: comp.id
1825
1882
  });
1826
1883
  }
1827
1884
  checkFacets(override?.facets, scope);
@@ -1899,13 +1956,16 @@ var PUNCT = {
1899
1956
  "}": "rbrace",
1900
1957
  "(": "lparen",
1901
1958
  ")": "rparen",
1959
+ "[": "lbracket",
1960
+ "]": "rbracket",
1902
1961
  "<": "lt",
1903
1962
  ">": "gt",
1904
1963
  ":": "colon",
1905
1964
  "=": "equals",
1906
1965
  ",": "comma",
1907
1966
  ";": "semi",
1908
- "?": "question"
1967
+ "?": "question",
1968
+ "@": "at"
1909
1969
  };
1910
1970
  function lex(src) {
1911
1971
  const tokens = [];
@@ -1976,6 +2036,21 @@ function lex(src) {
1976
2036
  push("arrow", "->", startPos, startLine, startCol);
1977
2037
  continue;
1978
2038
  }
2039
+ if (c === "!" && src[i + 1] === "=") {
2040
+ advance(2);
2041
+ push("ne", "!=", startPos, startLine, startCol);
2042
+ continue;
2043
+ }
2044
+ if (c === "<" && src[i + 1] === "=") {
2045
+ advance(2);
2046
+ push("lte", "<=", startPos, startLine, startCol);
2047
+ continue;
2048
+ }
2049
+ if (c === ">" && src[i + 1] === "=") {
2050
+ advance(2);
2051
+ push("gte", ">=", startPos, startLine, startCol);
2052
+ continue;
2053
+ }
1979
2054
  if (PUNCT[c]) {
1980
2055
  advance();
1981
2056
  push(PUNCT[c], c, startPos, startLine, startCol);
@@ -2003,10 +2078,24 @@ var ctxId = (groupName, ctxName) => `ctx.${seg(groupName)}.${seg(ctxName)}`;
2003
2078
  var tokId = (path) => `tok.${path.replace(/\//g, ".")}`;
2004
2079
  var stId = (name) => `st.${seg(name)}`;
2005
2080
  var cmpId = (name) => `cmp.${seg(name)}`;
2081
+ function metaValueType(v) {
2082
+ if (typeof v === "number") return { kind: "number" };
2083
+ if (typeof v === "boolean") return { kind: "boolean" };
2084
+ if (Array.isArray(v)) return { kind: "array", item: { kind: "string" } };
2085
+ return { kind: "string" };
2086
+ }
2006
2087
  var FACET_MAP = {
2007
2088
  fill: "fill",
2008
2089
  ink: "ink",
2009
2090
  border: "border",
2091
+ "border.top": "border.top",
2092
+ "border.right": "border.right",
2093
+ "border.bottom": "border.bottom",
2094
+ "border.left": "border.left",
2095
+ "border.inline-start": "border.inline-start",
2096
+ "border.inline-end": "border.inline-end",
2097
+ "border.block-start": "border.block-start",
2098
+ "border.block-end": "border.block-end",
2010
2099
  corner: "corner",
2011
2100
  inset: "inset",
2012
2101
  gap: "gap",
@@ -2018,9 +2107,9 @@ var FACET_MAP = {
2018
2107
  opacity: "opacity",
2019
2108
  ring: "ring",
2020
2109
  backdrop: "backdrop",
2021
- weight: null,
2022
- font: null,
2023
- slant: null
2110
+ weight: "weight",
2111
+ font: "font",
2112
+ slant: "slant"
2024
2113
  };
2025
2114
  var TYPE_MAP = {
2026
2115
  color: "color",
@@ -2032,6 +2121,8 @@ var TYPE_MAP = {
2032
2121
  typography: "typography",
2033
2122
  "font-family": "fontFamily",
2034
2123
  fontFamily: "fontFamily",
2124
+ "font-weight": "fontWeight",
2125
+ fontWeight: "fontWeight",
2035
2126
  duration: "duration",
2036
2127
  opacity: "opacity",
2037
2128
  blur: "blur",
@@ -2055,10 +2146,34 @@ function hexToColor(hex) {
2055
2146
  if (h.length === 8) value.alpha = byte(3);
2056
2147
  return value;
2057
2148
  }
2149
+ var COLOR_FNS = /* @__PURE__ */ new Set(["hex", "srgb", "rgb", "p3", "oklch", "hsl"]);
2150
+ var UNIT_RE = /^(-?\d*\.?\d+)(px|rem|em|%|ms|s)$/;
2151
+ function unitToValue(n, unit) {
2152
+ switch (unit) {
2153
+ case "px":
2154
+ return { value: n, unit: "fixed" };
2155
+ case "em":
2156
+ return { value: n, unit: "em" };
2157
+ case "%":
2158
+ return { value: n, unit: "percent" };
2159
+ case "ms":
2160
+ return { ms: n };
2161
+ case "s":
2162
+ return { ms: n * 1e3 };
2163
+ default:
2164
+ return { value: n, unit: "scaled" };
2165
+ }
2166
+ }
2058
2167
  var Parser = class {
2059
2168
  constructor(toks) {
2060
2169
  this.pos = 0;
2061
2170
  this.issues = [];
2171
+ // Source spans for declared entities (see NodeSpan). Populated as declarations are
2172
+ // parsed; the AST itself never gains position fields.
2173
+ this.spans = [];
2174
+ // The component currently being parsed, so nested part/prop/event spans can point
2175
+ // back at it via `parentId`.
2176
+ this.currentComponentId = null;
2062
2177
  // Tokens synthesized while parsing (inline literals hoisted out of alias slots —
2063
2178
  // the canonical AST's `refs` holds only token ids, so an inline value must become
2064
2179
  // its own token). Deduped by (type, serialized value). Flushed into the layer.
@@ -2078,6 +2193,73 @@ var Parser = class {
2078
2193
  this.issue("warning", "inline-literal", `inline ${astType} literal in an alias slot hoisted to token ${id}`, tok);
2079
2194
  return id;
2080
2195
  }
2196
+ // Hoist an already-typed token value (an inline literal in an alias slot) into a
2197
+ // synthesized token and return its id.
2198
+ hoistValue(astType, value, tok) {
2199
+ const key = `${astType} ${JSON.stringify(value)}`;
2200
+ const existing = this.synthKeys.get(key);
2201
+ if (existing) return existing;
2202
+ const id = `tok.lit-${seg(String(astType))}-${this.synth.length + 1}`;
2203
+ this.synth.push({ id, name: `(inline ${astType})`, type: astType, value });
2204
+ this.synthKeys.set(key, id);
2205
+ this.issue("warning", "inline-literal", `inline ${astType} literal in an alias slot hoisted to token ${id}`, tok);
2206
+ return id;
2207
+ }
2208
+ // A self-typing value literal: a color-space function (`oklch(…)`, `hex(rrggbb)`, …)
2209
+ // or a unit-suffixed number (`8px`, `150ms`). Returns a raw TokenValue, or null if the
2210
+ // cursor isn't at such a literal (then it consumes nothing).
2211
+ tryParseLiteralValue() {
2212
+ const t = this.peek();
2213
+ if (t.type !== "word") return null;
2214
+ if (COLOR_FNS.has(t.value) && this.toks[this.pos + 1]?.type === "lparen") {
2215
+ return { kind: "raw", value: this.parseColorFn(t.value) };
2216
+ }
2217
+ const m = UNIT_RE.exec(t.value);
2218
+ if (m) {
2219
+ this.next();
2220
+ return { kind: "raw", value: unitToValue(Number(m[1]), m[2]) };
2221
+ }
2222
+ return null;
2223
+ }
2224
+ // `fn( a b c [alpha] )` — space-separated numeric channels (hex takes raw digits).
2225
+ parseColorFn(fn) {
2226
+ this.next();
2227
+ this.expect("lparen", "color-lparen");
2228
+ const nums = [];
2229
+ let hex = "";
2230
+ while (!this.at("rparen") && !this.at("eof")) {
2231
+ const w = this.next().value;
2232
+ if (fn === "hex") hex += w;
2233
+ else {
2234
+ const n = Number(w);
2235
+ if (!Number.isNaN(n)) nums.push(n);
2236
+ }
2237
+ }
2238
+ this.expect("rparen", "color-rparen");
2239
+ if (fn === "hex") {
2240
+ return hexToColor(hex.startsWith("#") ? hex : `#${hex}`) ?? { space: "srgb", channels: [0, 0, 0] };
2241
+ }
2242
+ const c = (i) => nums[i] ?? 0;
2243
+ let cv;
2244
+ if (fn === "rgb") cv = { space: "srgb", channels: [c(0) / 255, c(1) / 255, c(2) / 255] };
2245
+ else if (fn === "p3") cv = { space: "display-p3", channels: [c(0), c(1), c(2)] };
2246
+ else cv = { space: fn, channels: [c(0), c(1), c(2)] };
2247
+ if (nums.length >= 4) cv.alpha = nums[3];
2248
+ return cv;
2249
+ }
2250
+ // Self-typing literals let us check the value's inherent kind against the token's
2251
+ // declared type (e.g. `token x : color = 8px` is a dimension in a color slot).
2252
+ checkLiteralType(astType, lit, tok) {
2253
+ if (lit.kind !== "raw") return;
2254
+ const v = lit.value;
2255
+ if (typeof v !== "object" || v === null) return;
2256
+ const cat = "space" in v ? "color" : "ms" in v ? "duration" : "unit" in v ? "dimension" : null;
2257
+ if (!cat) return;
2258
+ const expected = astType === "color" ? "color" : astType === "duration" ? "duration" : astType === "spacing" || astType === "radius" || astType === "size" ? "dimension" : null;
2259
+ if (expected && cat !== expected) {
2260
+ this.issue("error", "value-type", `${cat} literal in a "${astType}" token`, tok);
2261
+ }
2262
+ }
2081
2263
  peek() {
2082
2264
  return this.toks[this.pos];
2083
2265
  }
@@ -2103,6 +2285,28 @@ var Parser = class {
2103
2285
  if (!this.at("eof")) this.next();
2104
2286
  return true;
2105
2287
  }
2288
+ // ─── span recording (source positions for tooling) ─────────────────────────
2289
+ recordSpan(span) {
2290
+ this.spans.push(span);
2291
+ }
2292
+ startPos(tok) {
2293
+ return { line: tok.line, col: tok.col };
2294
+ }
2295
+ // End position of a token = its start advanced past its source text. Strings add 2
2296
+ // for the surrounding quotes (escape sequences make this a slight over-estimate,
2297
+ // affecting only a rarely-used string span — never an identifier or brace).
2298
+ endPos(tok) {
2299
+ const len = tok.type === "newline" || tok.type === "eof" ? 0 : tok.type === "string" ? tok.value.length + 2 : tok.value.length;
2300
+ return { line: tok.line, col: tok.col + len };
2301
+ }
2302
+ rangeOf(start, end) {
2303
+ return { start: this.startPos(start), end: this.endPos(end) };
2304
+ }
2305
+ // The most-recently-consumed token — closes a span that has no trailing brace
2306
+ // (a single-line `token`/`type` declaration ends at its value).
2307
+ lastTok() {
2308
+ return this.toks[Math.max(0, this.pos - 1)];
2309
+ }
2106
2310
  issue(severity, code, message, tok = this.peek()) {
2107
2311
  this.issues.push({ severity, code, message, line: tok.line, col: tok.col });
2108
2312
  }
@@ -2133,7 +2337,8 @@ var Parser = class {
2133
2337
  }
2134
2338
  // ── context-group NAME { ctx ctx … } ──
2135
2339
  parseContextGroup() {
2136
- this.next();
2340
+ const kw = this.next();
2341
+ const nameTok = this.peek();
2137
2342
  const name = this.expectWord("cg-name");
2138
2343
  this.expect("lbrace", "cg-lbrace");
2139
2344
  const contexts = [];
@@ -2142,11 +2347,27 @@ var Parser = class {
2142
2347
  this.pos++;
2143
2348
  continue;
2144
2349
  }
2350
+ const ctxTok = this.peek();
2145
2351
  const ctxName = this.expectWord("cg-ctx");
2146
2352
  if (!ctxName) break;
2147
2353
  contexts.push({ id: ctxId(name, ctxName), name: ctxName });
2354
+ this.recordSpan({
2355
+ id: ctxId(name, ctxName),
2356
+ kind: "context",
2357
+ name: ctxName,
2358
+ nameRange: this.rangeOf(ctxTok, ctxTok),
2359
+ fullRange: this.rangeOf(ctxTok, ctxTok),
2360
+ parentId: cgId(name)
2361
+ });
2148
2362
  }
2149
- this.expect("rbrace", "cg-rbrace");
2363
+ const close = this.expect("rbrace", "cg-rbrace");
2364
+ this.recordSpan({
2365
+ id: cgId(name),
2366
+ kind: "contextGroup",
2367
+ name,
2368
+ nameRange: this.rangeOf(nameTok, nameTok),
2369
+ fullRange: this.rangeOf(kw, close)
2370
+ });
2150
2371
  return { id: cgId(name), name, contexts };
2151
2372
  }
2152
2373
  // ── raw value literal → a JS value (string | number | nested object) ──
@@ -2155,6 +2376,12 @@ var Parser = class {
2155
2376
  if (t.type === "string") return this.next().value;
2156
2377
  if (t.type === "lbrace") return this.parseStructured();
2157
2378
  if (t.type === "word") {
2379
+ if (COLOR_FNS.has(t.value) && this.toks[this.pos + 1]?.type === "lparen") return this.parseColorFn(t.value);
2380
+ const m = UNIT_RE.exec(t.value);
2381
+ if (m) {
2382
+ this.next();
2383
+ return unitToValue(Number(m[1]), m[2]);
2384
+ }
2158
2385
  const w = this.next().value;
2159
2386
  return NUMBER_RE.test(w) ? Number(w) : w;
2160
2387
  }
@@ -2196,6 +2423,7 @@ var Parser = class {
2196
2423
  }
2197
2424
  }
2198
2425
  if (astType === "opacity" && typeof raw === "number") return { kind: "raw", value: { value: raw } };
2426
+ if (astType === "fontWeight" && typeof raw === "number") return { kind: "raw", value: { weight: raw } };
2199
2427
  if (astType === "duration" && typeof raw === "number") return { kind: "raw", value: { ms: raw } };
2200
2428
  if (astType === "tonal" && typeof raw === "number") return { kind: "raw", value: { level: raw } };
2201
2429
  return { kind: "raw", value: raw };
@@ -2221,7 +2449,11 @@ var Parser = class {
2221
2449
  this.expect("arrow", "alias-arrow");
2222
2450
  const slot = this.peek();
2223
2451
  let refId = "";
2224
- if (slot.type === "string" || slot.type === "lbrace" || slot.type === "word" && NUMBER_RE.test(slot.value)) {
2452
+ const lit = this.tryParseLiteralValue();
2453
+ if (lit) {
2454
+ this.checkLiteralType(astType, lit, slot);
2455
+ refId = this.hoistValue(astType, lit, slot);
2456
+ } else if (slot.type === "string" || slot.type === "lbrace" || slot.type === "word" && NUMBER_RE.test(slot.value)) {
2225
2457
  refId = this.hoistLiteral(astType, this.parseRawLiteral(), slot);
2226
2458
  } else {
2227
2459
  const refPath = this.expectWord("alias-ref");
@@ -2263,6 +2495,15 @@ var Parser = class {
2263
2495
  return this.typedRaw(astType, raw, t);
2264
2496
  }
2265
2497
  if (t.type === "word") {
2498
+ const lit = this.tryParseLiteralValue();
2499
+ if (lit) {
2500
+ this.checkLiteralType(astType, lit, t);
2501
+ return lit;
2502
+ }
2503
+ if (NUMBER_RE.test(t.value)) {
2504
+ this.next();
2505
+ return this.typedRaw(astType, Number(t.value), t);
2506
+ }
2266
2507
  const refPath = this.next().value;
2267
2508
  this.issue("warning", "ref-sugar", `unconditional ref "${refPath}" lowered to a default-only alias`, t);
2268
2509
  return { kind: "alias", by: "", refs: { default: tokId(refPath) } };
@@ -2302,6 +2543,7 @@ var Parser = class {
2302
2543
  }
2303
2544
  parseTokenDecl(_order) {
2304
2545
  const startTok = this.next();
2546
+ const nameTok = this.peek();
2305
2547
  const path = this.expectWord("token-path");
2306
2548
  if (!path) return null;
2307
2549
  this.expect("colon", "token-colon");
@@ -2310,6 +2552,13 @@ var Parser = class {
2310
2552
  if (!astType) this.issue("warning", "token-type", `non-canonical token type "${typeName}" kept as-is`, startTok);
2311
2553
  this.expect("equals", "token-equals");
2312
2554
  const value = this.parseTokenValue(astType ?? typeName);
2555
+ this.recordSpan({
2556
+ id: tokId(path),
2557
+ kind: "token",
2558
+ name: path,
2559
+ nameRange: this.rangeOf(nameTok, nameTok),
2560
+ fullRange: this.rangeOf(startTok, this.lastTok())
2561
+ });
2313
2562
  return {
2314
2563
  id: tokId(path),
2315
2564
  name: path,
@@ -2421,11 +2670,19 @@ var Parser = class {
2421
2670
  return { kind: "ref", ref: stId(w) };
2422
2671
  }
2423
2672
  parseSharedType() {
2424
- this.next();
2673
+ const kw = this.next();
2674
+ const nameTok = this.peek();
2425
2675
  const name = this.expectWord("type-name");
2426
2676
  if (!name) return null;
2427
2677
  this.expect("equals", "type-equals");
2428
2678
  const definition = this.parseTypeExpr();
2679
+ this.recordSpan({
2680
+ id: stId(name),
2681
+ kind: "sharedType",
2682
+ name,
2683
+ nameRange: this.rangeOf(nameTok, nameTok),
2684
+ fullRange: this.rangeOf(kw, this.lastTok())
2685
+ });
2429
2686
  return { id: stId(name), name, definition };
2430
2687
  }
2431
2688
  // ════════════════════════════════════════════════════════════════════════
@@ -2455,6 +2712,125 @@ var Parser = class {
2455
2712
  }
2456
2713
  return { prop, eq };
2457
2714
  }
2715
+ // ── Predicate expression parser (proposal 0032) ─────────────────────────────
2716
+ // Recursive-descent over the surface predicate grammar emitted by @mildastudio/
2717
+ // generate's `predToDsl` (targets/dsl/emit.ts). Precedence, lowest → highest:
2718
+ // or → and → unary(`not`) → primary(`( … )` | leaf)
2719
+ // A leaf names a source (`state s` / `context g` / `ancestor id s` / a bare prop path)
2720
+ // then an optional operator + value: a bare leaf is `set`, `not <leaf>` is `unset`, and
2721
+ // a parenthesized `not ( … )` negates a whole group. Values are bare idents/numbers/
2722
+ // bools or quoted strings (mirroring the emitter's `predValue`), coerced to a JS type.
2723
+ parsePredicate() {
2724
+ return this.parsePredOr();
2725
+ }
2726
+ parsePredOr() {
2727
+ const first = this.parsePredAnd();
2728
+ if (!this.atWord("or")) return first;
2729
+ const items = [first];
2730
+ while (this.atWord("or")) {
2731
+ this.next();
2732
+ items.push(this.parsePredAnd());
2733
+ }
2734
+ return { kind: "any", items };
2735
+ }
2736
+ parsePredAnd() {
2737
+ const first = this.parsePredUnary();
2738
+ if (!this.atWord("and")) return first;
2739
+ const items = [first];
2740
+ while (this.atWord("and")) {
2741
+ this.next();
2742
+ items.push(this.parsePredUnary());
2743
+ }
2744
+ return { kind: "all", items };
2745
+ }
2746
+ parsePredUnary() {
2747
+ if (this.atWord("not")) {
2748
+ this.next();
2749
+ if (this.at("lparen")) {
2750
+ this.next();
2751
+ const item = this.parsePredOr();
2752
+ this.expect("rparen", "pred-not-rparen");
2753
+ return { kind: "not", item };
2754
+ }
2755
+ return { kind: "cmp", ref: this.parsePredRef(), op: "unset" };
2756
+ }
2757
+ return this.parsePredPrimary();
2758
+ }
2759
+ parsePredPrimary() {
2760
+ if (this.at("lparen")) {
2761
+ this.next();
2762
+ const inner = this.parsePredOr();
2763
+ this.expect("rparen", "pred-rparen");
2764
+ return inner;
2765
+ }
2766
+ return this.parsePredLeaf();
2767
+ }
2768
+ // A comparison leaf: a source, then an optional operator + value. No operator ⇒ `set`.
2769
+ parsePredLeaf() {
2770
+ const ref = this.parsePredRef();
2771
+ const op = this.predOp();
2772
+ if (!op) return { kind: "cmp", ref, op: "set" };
2773
+ return { kind: "cmp", ref, op, value: this.parsePredValue() };
2774
+ }
2775
+ // The source of a leaf: `state <s>` / `context <g>` / `ancestor <id> <s>` / bare prop path.
2776
+ parsePredRef() {
2777
+ if (this.atWord("state")) {
2778
+ this.next();
2779
+ return { kind: "state", state: this.expectWord("pred-state") };
2780
+ }
2781
+ if (this.atWord("context")) {
2782
+ this.next();
2783
+ return { kind: "context", group: this.expectWord("pred-context") };
2784
+ }
2785
+ if (this.atWord("ancestor")) {
2786
+ this.next();
2787
+ const nodeId = this.expectWord("pred-ancestor-id");
2788
+ const state = this.expectWord("pred-ancestor-state");
2789
+ return { kind: "ancestor", nodeId, state };
2790
+ }
2791
+ return { kind: "prop", path: this.expectWord("pred-prop") };
2792
+ }
2793
+ // A comparison operator token → CompareOp, or null when the next token isn't one.
2794
+ predOp() {
2795
+ switch (this.peek().type) {
2796
+ case "equals":
2797
+ this.next();
2798
+ return "eq";
2799
+ case "ne":
2800
+ this.next();
2801
+ return "ne";
2802
+ case "lt":
2803
+ this.next();
2804
+ return "lt";
2805
+ case "lte":
2806
+ this.next();
2807
+ return "lte";
2808
+ case "gt":
2809
+ this.next();
2810
+ return "gt";
2811
+ case "gte":
2812
+ this.next();
2813
+ return "gte";
2814
+ default:
2815
+ if (this.atWord("contains")) {
2816
+ this.next();
2817
+ return "contains";
2818
+ }
2819
+ return null;
2820
+ }
2821
+ }
2822
+ // A leaf value: a quoted string, or a bare word coerced to number / boolean / string
2823
+ // (mirrors `parseRealizeCond` and the emitter's bare-vs-quoted `predValue`).
2824
+ parsePredValue() {
2825
+ const t = this.peek();
2826
+ if (t.type === "string") return this.next().value;
2827
+ if (t.type === "word") {
2828
+ const w = this.next().value;
2829
+ return NUMBER_RE.test(w) ? Number(w) : w === "true" ? true : w === "false" ? false : w;
2830
+ }
2831
+ this.issue("error", "pred-value", `expected a comparison value, got ${t.type}`);
2832
+ return "";
2833
+ }
2458
2834
  parsePropDefault() {
2459
2835
  const t = this.peek();
2460
2836
  if (t.type === "string") return this.next().value;
@@ -2477,6 +2853,7 @@ var Parser = class {
2477
2853
  return void 0;
2478
2854
  }
2479
2855
  parsePropDecl() {
2856
+ const nameTok = this.peek();
2480
2857
  const name = this.expectWord("prop-name");
2481
2858
  if (!name) return null;
2482
2859
  const type = this.parseTypeExpr();
@@ -2490,6 +2867,28 @@ var Parser = class {
2490
2867
  this.next();
2491
2868
  prop.binding = this.expectWord("bind-target");
2492
2869
  }
2870
+ const meta = {};
2871
+ while (this.at("at")) {
2872
+ this.next();
2873
+ const key = this.expectWord("annot-key");
2874
+ const t = this.peek();
2875
+ let value = true;
2876
+ if (t.type === "string") value = this.next().value;
2877
+ else if (t.type === "word") {
2878
+ const w = this.next().value;
2879
+ value = NUMBER_RE.test(w) ? Number(w) : w === "true" ? true : w === "false" ? false : w;
2880
+ }
2881
+ if (key) meta[key] = value;
2882
+ }
2883
+ if (Object.keys(meta).length) prop.meta = meta;
2884
+ this.recordSpan({
2885
+ id: `${this.currentComponentId ?? ""}#prop.${seg(name)}`,
2886
+ kind: "prop",
2887
+ name,
2888
+ nameRange: this.rangeOf(nameTok, nameTok),
2889
+ fullRange: this.rangeOf(nameTok, this.lastTok()),
2890
+ parentId: this.currentComponentId ?? void 0
2891
+ });
2493
2892
  return prop;
2494
2893
  }
2495
2894
  parseProps(props) {
@@ -2517,6 +2916,7 @@ var Parser = class {
2517
2916
  this.pos++;
2518
2917
  continue;
2519
2918
  }
2919
+ const nameTok = this.peek();
2520
2920
  const name = this.expectWord("event-name");
2521
2921
  if (name) {
2522
2922
  const e = { name };
@@ -2525,6 +2925,14 @@ var Parser = class {
2525
2925
  e.from = this.expectWord("event-from");
2526
2926
  }
2527
2927
  events.push(e);
2928
+ this.recordSpan({
2929
+ id: `${this.currentComponentId ?? ""}#event.${seg(name)}`,
2930
+ kind: "event",
2931
+ name,
2932
+ nameRange: this.rangeOf(nameTok, nameTok),
2933
+ fullRange: this.rangeOf(nameTok, this.lastTok()),
2934
+ parentId: this.currentComponentId ?? void 0
2935
+ });
2528
2936
  }
2529
2937
  if (this.at("semi") || this.at("newline")) this.pos++;
2530
2938
  this.ensureProgress(mark);
@@ -2697,6 +3105,16 @@ var Parser = class {
2697
3105
  if (mapped === "inset") {
2698
3106
  return { name: "inset", value: { inset: this.parseInsetArgs() } };
2699
3107
  }
3108
+ if (mapped === "border" || mapped.startsWith("border.")) {
3109
+ const border = {};
3110
+ while (this.at("word")) {
3111
+ const w = this.next().value;
3112
+ if (w === "solid" || w === "dashed" || w === "dotted") border.style = w;
3113
+ else if (w.includes(".")) border.color = tokId(w);
3114
+ else border.width = tokId(w);
3115
+ }
3116
+ return { name: mapped, value: { border } };
3117
+ }
2700
3118
  const t = this.peek();
2701
3119
  let value;
2702
3120
  if (t.type === "string") {
@@ -2705,6 +3123,7 @@ var Parser = class {
2705
3123
  const v = this.next().value;
2706
3124
  if (NUMBER_RE.test(v)) value = { literal: Number(v) };
2707
3125
  else if (isRawValue(v)) value = { raw: v };
3126
+ else if (!v.includes(".")) value = { raw: v };
2708
3127
  else value = { token: tokId(v) };
2709
3128
  } else {
2710
3129
  this.issue("error", "facet-value", `expected a facet value, got ${t.type}`);
@@ -2726,11 +3145,13 @@ var Parser = class {
2726
3145
  this.applyStyleBody(variant);
2727
3146
  return variant;
2728
3147
  }
2729
- // `when COND { … }` → a state-less prop-conditioned PartVariant (proposal 0025).
3148
+ // `when <predicate> { … }` → a state-less predicate-conditioned PartVariant. The rich
3149
+ // predicate grammar (proposal 0032) supersedes the flat single-condition form: it carries
3150
+ // OR / NOT / operators / state·context·ancestor leaves. A plain `when prop = value` parses
3151
+ // to the degenerate single-`eq` predicate, so the legacy one-condition case still works.
2730
3152
  parseWhenBlock() {
2731
3153
  this.next();
2732
- const when = this.parseRealizeCond();
2733
- const variant = { when };
3154
+ const variant = { whenPredicate: this.parsePredicate() };
2734
3155
  this.applyStyleBody(variant);
2735
3156
  return variant;
2736
3157
  }
@@ -2757,9 +3178,26 @@ var Parser = class {
2757
3178
  this.expect("rbrace", "style-rbrace");
2758
3179
  if (Object.keys(facets2).length) target.facets = facets2;
2759
3180
  }
3181
+ // `slot [single|optional|many] [of content | of TYPE]` — a content slot on a part.
3182
+ // Bare `slot` defaults to a single content slot.
3183
+ parseSlot() {
3184
+ let arity = "single";
3185
+ if (this.atWord("single") || this.atWord("optional") || this.atWord("many")) {
3186
+ arity = this.next().value;
3187
+ }
3188
+ let accepts = { kind: "content" };
3189
+ if (this.atWord("of")) {
3190
+ this.next();
3191
+ const w = this.expectWord("slot-accepts");
3192
+ if (w && w !== "content") accepts = { kind: "type", ref: w };
3193
+ }
3194
+ this.skipToTerminator();
3195
+ return { arity, accepts };
3196
+ }
2760
3197
  // `part NAME { pstmt* }` → [name, Part]
2761
3198
  parsePart() {
2762
- this.next();
3199
+ const kw = this.next();
3200
+ const nameTok = this.peek();
2763
3201
  const name = this.expectWord("part-name");
2764
3202
  if (!name) return null;
2765
3203
  this.expect("lbrace", "part-lbrace");
@@ -2781,13 +3219,18 @@ var Parser = class {
2781
3219
  } else {
2782
3220
  part.layout = this.parseLayoutBody();
2783
3221
  }
3222
+ } else if (this.atWord("present")) {
3223
+ this.next();
3224
+ if (this.atWord("when")) this.next();
3225
+ else this.issue("warning", "present-when", "expected 'when' after 'present'");
3226
+ part.presence = this.parsePredicate();
2784
3227
  } else if (this.atWord("state")) {
2785
3228
  variants.push(this.parseStateBlock());
2786
3229
  } else if (this.atWord("when")) {
2787
3230
  variants.push(this.parseWhenBlock());
2788
3231
  } else if (this.atWord("slot")) {
2789
- this.issue("warning", "slot", "part `slot` has no AST Part field yet \u2014 skipped");
2790
- this.skipToTerminator();
3232
+ this.next();
3233
+ part.slot = this.parseSlot();
2791
3234
  } else {
2792
3235
  const f = this.parseFacetStmt();
2793
3236
  if (f) facets2[f.name] = f.value;
@@ -2795,9 +3238,17 @@ var Parser = class {
2795
3238
  if (this.at("semi") || this.at("newline")) this.pos++;
2796
3239
  this.ensureProgress(mark);
2797
3240
  }
2798
- this.expect("rbrace", "part-rbrace");
3241
+ const close = this.expect("rbrace", "part-rbrace");
2799
3242
  if (Object.keys(facets2).length) part.facets = facets2;
2800
3243
  if (variants.length) part.variants = variants;
3244
+ this.recordSpan({
3245
+ id: `${this.currentComponentId ?? ""}#part.${seg(name)}`,
3246
+ kind: "part",
3247
+ name,
3248
+ nameRange: this.rangeOf(nameTok, nameTok),
3249
+ fullRange: this.rangeOf(kw, close),
3250
+ parentId: this.currentComponentId ?? void 0
3251
+ });
2801
3252
  return [name, part];
2802
3253
  }
2803
3254
  // `realize TARGET [when COND] { rstmt* }`
@@ -2841,6 +3292,14 @@ var Parser = class {
2841
3292
  } else if (this.atWord("note")) {
2842
3293
  this.next();
2843
3294
  r.note = this.at("string") ? this.next().value : this.expectWord("note");
3295
+ } else if (this.atWord("dismiss")) {
3296
+ this.next();
3297
+ const words = this.parseIdentListWords();
3298
+ r.dismiss = words.length === 1 && words[0] === "none" ? [] : words;
3299
+ } else if (this.atWord("backdrop")) {
3300
+ this.next();
3301
+ r.backdrop = this.at("word") ? this.next().value : void 0;
3302
+ this.skipToTerminator();
2844
3303
  } else if (this.atWord("wire")) {
2845
3304
  const w = this.parseWire();
2846
3305
  if (w && w.kind === "wire") wires.push({ from: w.from, to: w.to, ...w.arg ? { arg: w.arg } : {} });
@@ -2882,6 +3341,21 @@ var Parser = class {
2882
3341
  this.next();
2883
3342
  commit.on = this.parseIdentListWords();
2884
3343
  }
3344
+ if (this.at("lbrace")) {
3345
+ this.next();
3346
+ while (!this.at("rbrace") && !this.at("eof")) {
3347
+ if (this.at("newline") || this.at("semi")) {
3348
+ this.pos++;
3349
+ continue;
3350
+ }
3351
+ const key = this.expectWord("commit-key");
3352
+ const val = this.at("word") ? this.next().value : void 0;
3353
+ if (key === "live" && val) commit.live = val;
3354
+ else if (key === "settled" && val) commit.settled = val;
3355
+ else this.skipToTerminator();
3356
+ }
3357
+ this.expect("rbrace", "commit-rbrace");
3358
+ }
2885
3359
  return commit;
2886
3360
  }
2887
3361
  // A run of bare words up to a terminator (for `drop a b c`, `commit … on click enter`).
@@ -2891,13 +3365,18 @@ var Parser = class {
2891
3365
  return out;
2892
3366
  }
2893
3367
  parseComponent() {
2894
- this.next();
3368
+ const kw = this.next();
3369
+ const nameTok = this.peek();
2895
3370
  const name = this.expectWord("component-name");
2896
3371
  if (!name) return null;
3372
+ const componentId = cmpId(name);
3373
+ const prevComponentId = this.currentComponentId;
3374
+ this.currentComponentId = componentId;
2897
3375
  let description;
2898
3376
  if (this.at("string")) description = this.next().value;
2899
3377
  this.expect("lbrace", "component-lbrace");
2900
3378
  let archetype = "";
3379
+ let meta;
2901
3380
  const props = [];
2902
3381
  const events = [];
2903
3382
  const uses = [];
@@ -2913,6 +3392,8 @@ var Parser = class {
2913
3392
  if (this.atWord("archetype")) {
2914
3393
  this.next();
2915
3394
  archetype = this.expectWord("archetype");
3395
+ } else if (this.atWord("meta")) {
3396
+ meta = this.parseMetaBlock();
2916
3397
  } else if (this.atWord("props")) {
2917
3398
  this.parseProps(props);
2918
3399
  } else if (this.atWord("events")) {
@@ -2946,9 +3427,17 @@ var Parser = class {
2946
3427
  }
2947
3428
  this.ensureProgress(mark);
2948
3429
  }
2949
- this.expect("rbrace", "component-rbrace");
3430
+ const close = this.expect("rbrace", "component-rbrace");
3431
+ this.currentComponentId = prevComponentId;
3432
+ this.recordSpan({
3433
+ id: componentId,
3434
+ kind: "component",
3435
+ name,
3436
+ nameRange: this.rangeOf(nameTok, nameTok),
3437
+ fullRange: this.rangeOf(kw, close)
3438
+ });
2950
3439
  const component = {
2951
- id: cmpId(name),
3440
+ id: componentId,
2952
3441
  name,
2953
3442
  archetype,
2954
3443
  contract: { params: {}, props, events },
@@ -2960,18 +3449,91 @@ var Parser = class {
2960
3449
  const composition = { uses, coordination };
2961
3450
  component.composition = composition;
2962
3451
  }
3452
+ if (meta && Object.keys(meta).length) component.meta = meta;
2963
3453
  return component;
2964
3454
  }
3455
+ // ════════════════════════════════════════════════════════════════════════
3456
+ // Header & metadata (proposal 0026)
3457
+ // ════════════════════════════════════════════════════════════════════════
3458
+ parseMetaValue() {
3459
+ const t = this.peek();
3460
+ if (t.type === "string") return this.next().value;
3461
+ if (t.type === "lbracket") {
3462
+ this.next();
3463
+ const arr = [];
3464
+ while (!this.at("rbracket") && !this.at("eof")) {
3465
+ const mark = this.pos;
3466
+ if (this.at("comma") || this.at("newline") || this.at("semi")) {
3467
+ this.pos++;
3468
+ continue;
3469
+ }
3470
+ const it = this.peek();
3471
+ if (it.type === "string") arr.push(this.next().value);
3472
+ else if (it.type === "word") {
3473
+ const w = this.next().value;
3474
+ arr.push(NUMBER_RE.test(w) ? Number(w) : w === "true" ? true : w === "false" ? false : w);
3475
+ } else this.next();
3476
+ this.ensureProgress(mark);
3477
+ }
3478
+ if (this.at("rbracket")) this.next();
3479
+ return arr;
3480
+ }
3481
+ if (t.type === "word") {
3482
+ this.next();
3483
+ return NUMBER_RE.test(t.value) ? Number(t.value) : t.value === "true" ? true : t.value === "false" ? false : t.value;
3484
+ }
3485
+ this.issue("error", "meta-value", `expected a metadata value, got ${t.type}`);
3486
+ return "";
3487
+ }
3488
+ parseMetaBlock() {
3489
+ this.next();
3490
+ this.expect("lbrace", "meta-lbrace");
3491
+ const out = {};
3492
+ while (!this.at("rbrace") && !this.at("eof")) {
3493
+ const mark = this.pos;
3494
+ if (this.at("newline") || this.at("semi")) {
3495
+ this.pos++;
3496
+ continue;
3497
+ }
3498
+ const key = this.expectWord("meta-key");
3499
+ const val = this.parseMetaValue();
3500
+ if (key) out[key] = val;
3501
+ if (this.at("semi") || this.at("newline")) this.pos++;
3502
+ this.ensureProgress(mark);
3503
+ }
3504
+ this.expect("rbrace", "meta-rbrace");
3505
+ return out;
3506
+ }
2965
3507
  // ── top-level document driver ──
2966
3508
  parseDocument(opts) {
2967
3509
  const contextGroups = [];
2968
3510
  const layer = { id: "layer.foundations", name: "Foundations", order: 0, tokens: [] };
2969
3511
  const sharedTypes = [];
2970
3512
  const components = [];
3513
+ const metadata = [];
3514
+ let mildaVer = opts.milda ?? "0.1";
3515
+ let preludeVer = opts.prelude ?? "0.0.2";
3516
+ let docName = opts.name ?? "Design system";
3517
+ let docVersion = "0.0.0";
3518
+ let docDescription;
2971
3519
  this.skipNewlines();
2972
3520
  while (!this.at("eof")) {
2973
3521
  const mark = this.pos;
2974
- if (this.atWord("context-group")) {
3522
+ if (this.atWord("milda")) {
3523
+ this.next();
3524
+ mildaVer = this.expectWord("milda-version") || mildaVer;
3525
+ } else if (this.atWord("prelude")) {
3526
+ this.next();
3527
+ preludeVer = this.expectWord("prelude-version") || preludeVer;
3528
+ } else if (this.atWord("meta")) {
3529
+ const m = this.parseMetaBlock();
3530
+ for (const [k, v] of Object.entries(m)) {
3531
+ if (k === "name") docName = String(v);
3532
+ else if (k === "version") docVersion = String(v);
3533
+ else if (k === "description") docDescription = String(v);
3534
+ else metadata.push({ id: `md.${seg(k)}`, key: k, type: metaValueType(v), value: v });
3535
+ }
3536
+ } else if (this.atWord("context-group")) {
2975
3537
  const cg = this.parseContextGroup();
2976
3538
  if (cg) contextGroups.push(cg);
2977
3539
  } else if (this.atWord("foundations")) {
@@ -2991,19 +3553,20 @@ var Parser = class {
2991
3553
  this.skipNewlines();
2992
3554
  }
2993
3555
  return {
2994
- milda: opts.milda ?? "0.1",
3556
+ milda: mildaVer,
2995
3557
  kind: "design-system",
2996
- prelude: opts.prelude ?? "0.0.2",
3558
+ prelude: preludeVer,
2997
3559
  meta: {
2998
- id: `ds.${seg(opts.name ?? "design-system")}`,
2999
- name: opts.name ?? "Design system",
3000
- version: "0.0.0"
3560
+ id: `ds.${seg(docName)}`,
3561
+ name: docName,
3562
+ version: docVersion,
3563
+ ...docDescription ? { description: docDescription } : {}
3001
3564
  },
3002
3565
  foundations: {
3003
3566
  contextGroups,
3004
3567
  layers: layer.tokens.length ? [layer] : [],
3005
3568
  sharedTypes,
3006
- metadata: []
3569
+ metadata
3007
3570
  },
3008
3571
  components
3009
3572
  };
@@ -3017,12 +3580,13 @@ function parseMildaDocument(src, opts = {}) {
3017
3580
  const le = e;
3018
3581
  return {
3019
3582
  document: emptyDocument(opts),
3020
- issues: [{ severity: "error", code: "lex", message: le.message, line: le.line ?? 1, col: le.col ?? 1 }]
3583
+ issues: [{ severity: "error", code: "lex", message: le.message, line: le.line ?? 1, col: le.col ?? 1 }],
3584
+ spans: []
3021
3585
  };
3022
3586
  }
3023
3587
  const p = new Parser(toks);
3024
3588
  const document = p.parseDocument(opts);
3025
- return { document, issues: p.issues };
3589
+ return { document, issues: p.issues, spans: p.spans };
3026
3590
  }
3027
3591
  function emptyDocument(opts) {
3028
3592
  return {
@@ -3041,6 +3605,7 @@ export {
3041
3605
  archetypesByName,
3042
3606
  atomsByName,
3043
3607
  collectActiveStates,
3608
+ compositesByName,
3044
3609
  conformance,
3045
3610
  findToken,
3046
3611
  lex,