spec-layer 0.8.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -17
- package/dist/cli.js +3182 -319
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -262,7 +262,7 @@ var require_sha256 = __commonJS({
|
|
|
262
262
|
}
|
|
263
263
|
notString = true;
|
|
264
264
|
}
|
|
265
|
-
var
|
|
265
|
+
var code3, index = 0, i, length = message.length, blocks2 = this.blocks;
|
|
266
266
|
while (index < length) {
|
|
267
267
|
if (this.hashed) {
|
|
268
268
|
this.hashed = false;
|
|
@@ -275,22 +275,22 @@ var require_sha256 = __commonJS({
|
|
|
275
275
|
}
|
|
276
276
|
} else {
|
|
277
277
|
for (i = this.start; index < length && i < 64; ++index) {
|
|
278
|
-
|
|
279
|
-
if (
|
|
280
|
-
blocks2[i >>> 2] |=
|
|
281
|
-
} else if (
|
|
282
|
-
blocks2[i >>> 2] |= (192 |
|
|
283
|
-
blocks2[i >>> 2] |= (128 |
|
|
284
|
-
} else if (
|
|
285
|
-
blocks2[i >>> 2] |= (224 |
|
|
286
|
-
blocks2[i >>> 2] |= (128 |
|
|
287
|
-
blocks2[i >>> 2] |= (128 |
|
|
278
|
+
code3 = message.charCodeAt(index);
|
|
279
|
+
if (code3 < 128) {
|
|
280
|
+
blocks2[i >>> 2] |= code3 << SHIFT[i++ & 3];
|
|
281
|
+
} else if (code3 < 2048) {
|
|
282
|
+
blocks2[i >>> 2] |= (192 | code3 >>> 6) << SHIFT[i++ & 3];
|
|
283
|
+
blocks2[i >>> 2] |= (128 | code3 & 63) << SHIFT[i++ & 3];
|
|
284
|
+
} else if (code3 < 55296 || code3 >= 57344) {
|
|
285
|
+
blocks2[i >>> 2] |= (224 | code3 >>> 12) << SHIFT[i++ & 3];
|
|
286
|
+
blocks2[i >>> 2] |= (128 | code3 >>> 6 & 63) << SHIFT[i++ & 3];
|
|
287
|
+
blocks2[i >>> 2] |= (128 | code3 & 63) << SHIFT[i++ & 3];
|
|
288
288
|
} else {
|
|
289
|
-
|
|
290
|
-
blocks2[i >>> 2] |= (240 |
|
|
291
|
-
blocks2[i >>> 2] |= (128 |
|
|
292
|
-
blocks2[i >>> 2] |= (128 |
|
|
293
|
-
blocks2[i >>> 2] |= (128 |
|
|
289
|
+
code3 = 65536 + ((code3 & 1023) << 10 | message.charCodeAt(++index) & 1023);
|
|
290
|
+
blocks2[i >>> 2] |= (240 | code3 >>> 18) << SHIFT[i++ & 3];
|
|
291
|
+
blocks2[i >>> 2] |= (128 | code3 >>> 12 & 63) << SHIFT[i++ & 3];
|
|
292
|
+
blocks2[i >>> 2] |= (128 | code3 >>> 6 & 63) << SHIFT[i++ & 3];
|
|
293
|
+
blocks2[i >>> 2] |= (128 | code3 & 63) << SHIFT[i++ & 3];
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
}
|
|
@@ -472,24 +472,24 @@ var require_sha256 = __commonJS({
|
|
|
472
472
|
function HmacSha256(key, is224, sharedMemory) {
|
|
473
473
|
var i, type = typeof key;
|
|
474
474
|
if (type === "string") {
|
|
475
|
-
var bytes = [], length = key.length, index = 0,
|
|
475
|
+
var bytes = [], length = key.length, index = 0, code3;
|
|
476
476
|
for (i = 0; i < length; ++i) {
|
|
477
|
-
|
|
478
|
-
if (
|
|
479
|
-
bytes[index++] =
|
|
480
|
-
} else if (
|
|
481
|
-
bytes[index++] = 192 |
|
|
482
|
-
bytes[index++] = 128 |
|
|
483
|
-
} else if (
|
|
484
|
-
bytes[index++] = 224 |
|
|
485
|
-
bytes[index++] = 128 |
|
|
486
|
-
bytes[index++] = 128 |
|
|
477
|
+
code3 = key.charCodeAt(i);
|
|
478
|
+
if (code3 < 128) {
|
|
479
|
+
bytes[index++] = code3;
|
|
480
|
+
} else if (code3 < 2048) {
|
|
481
|
+
bytes[index++] = 192 | code3 >>> 6;
|
|
482
|
+
bytes[index++] = 128 | code3 & 63;
|
|
483
|
+
} else if (code3 < 55296 || code3 >= 57344) {
|
|
484
|
+
bytes[index++] = 224 | code3 >>> 12;
|
|
485
|
+
bytes[index++] = 128 | code3 >>> 6 & 63;
|
|
486
|
+
bytes[index++] = 128 | code3 & 63;
|
|
487
487
|
} else {
|
|
488
|
-
|
|
489
|
-
bytes[index++] = 240 |
|
|
490
|
-
bytes[index++] = 128 |
|
|
491
|
-
bytes[index++] = 128 |
|
|
492
|
-
bytes[index++] = 128 |
|
|
488
|
+
code3 = 65536 + ((code3 & 1023) << 10 | key.charCodeAt(++i) & 1023);
|
|
489
|
+
bytes[index++] = 240 | code3 >>> 18;
|
|
490
|
+
bytes[index++] = 128 | code3 >>> 12 & 63;
|
|
491
|
+
bytes[index++] = 128 | code3 >>> 6 & 63;
|
|
492
|
+
bytes[index++] = 128 | code3 & 63;
|
|
493
493
|
}
|
|
494
494
|
}
|
|
495
495
|
key = bytes;
|
|
@@ -559,9 +559,612 @@ var require_sha256 = __commonJS({
|
|
|
559
559
|
import { parseArgs } from "node:util";
|
|
560
560
|
|
|
561
561
|
// src/commands.ts
|
|
562
|
-
import { existsSync as existsSync8 } from "node:fs";
|
|
562
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
|
|
563
563
|
import { join as join8, resolve as resolve5 } from "node:path";
|
|
564
564
|
|
|
565
|
+
// ../extractor/src/naming.ts
|
|
566
|
+
function parseVariantName(name) {
|
|
567
|
+
const out = {};
|
|
568
|
+
for (const segment of name.split(",")) {
|
|
569
|
+
const [axis, ...rest] = segment.split("=");
|
|
570
|
+
if (!rest.length) return null;
|
|
571
|
+
out[axis.trim()] = rest.join("=").trim();
|
|
572
|
+
}
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
575
|
+
var WHITESPACE = /\s/;
|
|
576
|
+
function cleanPartName(name) {
|
|
577
|
+
let end = name.length;
|
|
578
|
+
while (end > 0 && WHITESPACE.test(name[end - 1])) end--;
|
|
579
|
+
const afterHashes = end;
|
|
580
|
+
while (end > 0 && name[end - 1] === "#") end--;
|
|
581
|
+
return afterHashes === end ? name.trim() : name.slice(0, end).trim();
|
|
582
|
+
}
|
|
583
|
+
var cleanPropName = (raw) => raw.split("#")[0];
|
|
584
|
+
function siblingPartNames(children) {
|
|
585
|
+
const counts = /* @__PURE__ */ new Map();
|
|
586
|
+
const out = /* @__PURE__ */ new Map();
|
|
587
|
+
for (const child of children) {
|
|
588
|
+
const base = cleanPartName(child.name);
|
|
589
|
+
const n = (counts.get(base) ?? 0) + 1;
|
|
590
|
+
counts.set(base, n);
|
|
591
|
+
out.set(child, n === 1 ? base : `${base} (${n})`);
|
|
592
|
+
}
|
|
593
|
+
return out;
|
|
594
|
+
}
|
|
595
|
+
function joinPath(parentPath, part) {
|
|
596
|
+
const escaped = part.replace(/\//g, "\\/");
|
|
597
|
+
return parentPath ? `${parentPath}/${escaped}` : escaped;
|
|
598
|
+
}
|
|
599
|
+
function walkParts(root, rootName, visit, skipInvisible = false, parentPath = "") {
|
|
600
|
+
if (typeof skipInvisible === "function" ? skipInvisible(root) : skipInvisible && root.visible === false) return;
|
|
601
|
+
const path = joinPath(parentPath, rootName);
|
|
602
|
+
visit(root, rootName, path);
|
|
603
|
+
const kids = root.children ?? [];
|
|
604
|
+
const names = siblingPartNames(kids);
|
|
605
|
+
for (const child of kids) {
|
|
606
|
+
walkParts(child, names.get(child), visit, skipInvisible, path);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ../extractor/src/anatomy.ts
|
|
611
|
+
var MAX_DEPTH = 3;
|
|
612
|
+
function defaultVariant(root) {
|
|
613
|
+
if (root.type !== "COMPONENT_SET" || !root.children?.length) return root;
|
|
614
|
+
const variants = root.children.filter((c) => c.type === "COMPONENT");
|
|
615
|
+
if (!variants.length) return root.children[0];
|
|
616
|
+
const declared = Object.entries(root.propertyDefinitions ?? {}).filter(([, d]) => d.type === "VARIANT" && typeof d.defaultValue === "string").map(([axis, d]) => [axis, d.defaultValue]);
|
|
617
|
+
if (declared.length) {
|
|
618
|
+
const match = variants.find((v) => {
|
|
619
|
+
const combo = parseVariantName(v.name);
|
|
620
|
+
return combo != null && declared.every(([axis, value]) => combo[axis] === value);
|
|
621
|
+
});
|
|
622
|
+
if (match) return match;
|
|
623
|
+
}
|
|
624
|
+
return variants[0];
|
|
625
|
+
}
|
|
626
|
+
function booleanPropertyKeys(root) {
|
|
627
|
+
return new Set(
|
|
628
|
+
Object.entries(root.propertyDefinitions ?? {}).filter(([, def]) => def.type === "BOOLEAN").map(([key]) => key)
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
function hiddenBoundTo(node, booleans) {
|
|
632
|
+
if (node.visible) return void 0;
|
|
633
|
+
if (node.visibleProperty === void 0 || !booleans.has(node.visibleProperty)) return void 0;
|
|
634
|
+
return cleanPropName(node.visibleProperty);
|
|
635
|
+
}
|
|
636
|
+
function isDocumentable(node, booleans) {
|
|
637
|
+
return node.visible || hiddenBoundTo(node, booleans) !== void 0;
|
|
638
|
+
}
|
|
639
|
+
function hiddenPartRules(root) {
|
|
640
|
+
const booleans = booleanPropertyKeys(root);
|
|
641
|
+
return {
|
|
642
|
+
prune: (node) => !isDocumentable(node, booleans),
|
|
643
|
+
shownBy: (node) => hiddenBoundTo(node, booleans)
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function extractAnatomy(root) {
|
|
647
|
+
const parts = [];
|
|
648
|
+
const related = /* @__PURE__ */ new Set();
|
|
649
|
+
const isInSet = root.type === "COMPONENT_SET";
|
|
650
|
+
const def = defaultVariant(root);
|
|
651
|
+
const rootPath = isInSet ? "Container" : cleanPartName(def.name);
|
|
652
|
+
const booleans = booleanPropertyKeys(root);
|
|
653
|
+
let siblingSet = def.children ?? [];
|
|
654
|
+
let children = siblingSet.filter((c) => c.visible);
|
|
655
|
+
let parentPath = rootPath;
|
|
656
|
+
const skippedHidden = [];
|
|
657
|
+
while (children.length === 1 && (children[0].type === "FRAME" || children[0].type === "GROUP") && (children[0].children ?? []).filter((c) => c.visible).length > 0) {
|
|
658
|
+
for (const node of siblingSet) {
|
|
659
|
+
if (node !== children[0] && hiddenBoundTo(node, booleans) !== void 0) {
|
|
660
|
+
skippedHidden.push({ node, parentPath });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const names = siblingPartNames(siblingSet);
|
|
664
|
+
parentPath = joinPath(parentPath, names.get(children[0]));
|
|
665
|
+
siblingSet = children[0].children ?? [];
|
|
666
|
+
children = siblingSet.filter((c) => c.visible);
|
|
667
|
+
}
|
|
668
|
+
const namingOrder = [
|
|
669
|
+
...children,
|
|
670
|
+
...siblingSet.filter((c) => hiddenBoundTo(c, booleans) !== void 0),
|
|
671
|
+
...skippedHidden.map((e) => e.node)
|
|
672
|
+
];
|
|
673
|
+
const topNames = siblingPartNames(namingOrder);
|
|
674
|
+
const topLevel = [
|
|
675
|
+
...siblingSet.filter((c) => isDocumentable(c, booleans)).map((node) => ({ node, parentPath })),
|
|
676
|
+
...skippedHidden
|
|
677
|
+
];
|
|
678
|
+
function pushPart(child, depth, childParentPath, name, inheritedShownBy) {
|
|
679
|
+
const shownBy = hiddenBoundTo(child, booleans) ?? inheritedShownBy;
|
|
680
|
+
const nested = child.type === "INSTANCE";
|
|
681
|
+
if (nested && child.mainComponent && shownBy === void 0) related.add(child.mainComponent.name);
|
|
682
|
+
const path = joinPath(childParentPath, name);
|
|
683
|
+
parts.push({
|
|
684
|
+
id: child.id,
|
|
685
|
+
name,
|
|
686
|
+
type: child.type,
|
|
687
|
+
nested,
|
|
688
|
+
depth,
|
|
689
|
+
path,
|
|
690
|
+
...nested && child.mainComponent ? { component: child.mainComponent.name } : {},
|
|
691
|
+
...child.text ? { text: child.text } : {},
|
|
692
|
+
...shownBy !== void 0 ? { hiddenByDefault: true, shownBy } : {}
|
|
693
|
+
});
|
|
694
|
+
if (!nested && depth + 1 < MAX_DEPTH && child.children?.length) {
|
|
695
|
+
addParts(child.children, depth + 1, path, shownBy);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function addParts(nodes, depth, nodesParentPath, inheritedShownBy) {
|
|
699
|
+
const names = siblingPartNames(nodes);
|
|
700
|
+
for (const child of nodes) {
|
|
701
|
+
if (!isDocumentable(child, booleans)) continue;
|
|
702
|
+
pushPart(child, depth, nodesParentPath, names.get(child), inheritedShownBy);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
for (const entry2 of topLevel) {
|
|
706
|
+
pushPart(entry2.node, 0, entry2.parentPath, topNames.get(entry2.node), void 0);
|
|
707
|
+
}
|
|
708
|
+
return { parts, related: [...related], componentId: def.id };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ../extractor/src/effects.ts
|
|
712
|
+
function extractNodeEffects(root) {
|
|
713
|
+
const out = [];
|
|
714
|
+
const def = defaultVariant(root);
|
|
715
|
+
walkParts(
|
|
716
|
+
def,
|
|
717
|
+
root.type === "COMPONENT_SET" ? "Container" : cleanPartName(def.name),
|
|
718
|
+
(n, part, path) => {
|
|
719
|
+
if (n.effects && n.effects.length > 0) out.push({ part, path, effects: n.effects });
|
|
720
|
+
}
|
|
721
|
+
);
|
|
722
|
+
return out;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// ../extractor/src/tokens.ts
|
|
726
|
+
function variantAxisModel(root) {
|
|
727
|
+
const isInSet = root.type === "COMPONENT_SET";
|
|
728
|
+
const variants = isInSet ? (root.children ?? []).filter((c) => c.type === "COMPONENT") : [root];
|
|
729
|
+
if (!isInSet) return { variants, combos: variants.map(() => ({})) };
|
|
730
|
+
const parsed = variants.map((v) => parseVariantName(v.name));
|
|
731
|
+
const first = parsed[0];
|
|
732
|
+
const consistent = first != null && parsed.every(
|
|
733
|
+
(p) => p !== null && Object.keys(p).length === Object.keys(first).length && Object.keys(first).every((k) => k in p)
|
|
734
|
+
);
|
|
735
|
+
return {
|
|
736
|
+
variants,
|
|
737
|
+
combos: consistent ? parsed : variants.map((v) => ({ Variant: v.name }))
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function formatConditions(conditions) {
|
|
741
|
+
const entries = Object.entries(conditions);
|
|
742
|
+
if (!entries.length) return "\u2014";
|
|
743
|
+
return entries.map(([axis, values]) => `${axis}=${values.join(" \xB7 ")}`).join(", ");
|
|
744
|
+
}
|
|
745
|
+
var SIMPLE_PROPERTY_MAP = {
|
|
746
|
+
fills: "fill",
|
|
747
|
+
strokes: "border",
|
|
748
|
+
cornerRadius: "border-radius",
|
|
749
|
+
itemSpacing: "gap",
|
|
750
|
+
fontSize: "font-size",
|
|
751
|
+
fontFamily: "font-family",
|
|
752
|
+
fontWeight: "font-weight",
|
|
753
|
+
fontStyle: "font-style",
|
|
754
|
+
lineHeight: "line-height",
|
|
755
|
+
letterSpacing: "letter-spacing",
|
|
756
|
+
strokeWeight: "border-width",
|
|
757
|
+
strokeTopWeight: "border-top-width",
|
|
758
|
+
strokeRightWeight: "border-right-width",
|
|
759
|
+
strokeBottomWeight: "border-bottom-width",
|
|
760
|
+
strokeLeftWeight: "border-left-width",
|
|
761
|
+
maxWidth: "max-width",
|
|
762
|
+
minWidth: "min-width",
|
|
763
|
+
maxHeight: "max-height",
|
|
764
|
+
minHeight: "min-height"
|
|
765
|
+
};
|
|
766
|
+
var RADIUS_PROPS = ["topLeftRadius", "topRightRadius", "bottomLeftRadius", "bottomRightRadius"];
|
|
767
|
+
var RADIUS_INDIVIDUAL_MAP = {
|
|
768
|
+
topLeftRadius: "border-top-left-radius",
|
|
769
|
+
topRightRadius: "border-top-right-radius",
|
|
770
|
+
bottomLeftRadius: "border-bottom-left-radius",
|
|
771
|
+
bottomRightRadius: "border-bottom-right-radius"
|
|
772
|
+
};
|
|
773
|
+
var RADIUS_BINDINGS = /* @__PURE__ */ new Set([
|
|
774
|
+
"cornerRadius",
|
|
775
|
+
"topLeftRadius",
|
|
776
|
+
"topRightRadius",
|
|
777
|
+
"bottomLeftRadius",
|
|
778
|
+
"bottomRightRadius"
|
|
779
|
+
]);
|
|
780
|
+
var PADDING_RAW_PROPS = /* @__PURE__ */ new Set([
|
|
781
|
+
"paddingTop",
|
|
782
|
+
"paddingRight",
|
|
783
|
+
"paddingBottom",
|
|
784
|
+
"paddingLeft",
|
|
785
|
+
"verticalPadding",
|
|
786
|
+
"horizontalPadding"
|
|
787
|
+
]);
|
|
788
|
+
var TYPOGRAPHY_SUBPROPS = /* @__PURE__ */ new Set(["fontSize", "fontFamily", "fontWeight", "fontStyle", "lineHeight", "letterSpacing"]);
|
|
789
|
+
var simpleProperty = (raw) => SIMPLE_PROPERTY_MAP[raw] ?? raw;
|
|
790
|
+
function paddingSides(top, right, bottom, left, key = String) {
|
|
791
|
+
const single = (xs) => xs.length === 1 ? xs[0] : null;
|
|
792
|
+
const sameKey = (a, b) => {
|
|
793
|
+
const x = single(a), y = single(b);
|
|
794
|
+
return x !== null && y !== null && key(x) === key(y);
|
|
795
|
+
};
|
|
796
|
+
const sides = [top, right, bottom, left];
|
|
797
|
+
const out = [];
|
|
798
|
+
if (sides.every((s) => single(s) !== null) && new Set(sides.map((s) => key(single(s)))).size === 1) {
|
|
799
|
+
out.push({ property: "padding", value: single(top) });
|
|
800
|
+
return out;
|
|
801
|
+
}
|
|
802
|
+
if (left.length && sameKey(left, right)) {
|
|
803
|
+
out.push({ property: "padding-x", value: single(left) });
|
|
804
|
+
} else {
|
|
805
|
+
for (const t of left) out.push({ property: "padding-left", value: t });
|
|
806
|
+
for (const t of right) out.push({ property: "padding-right", value: t });
|
|
807
|
+
}
|
|
808
|
+
if (top.length && sameKey(top, bottom)) {
|
|
809
|
+
out.push({ property: "padding-y", value: single(top) });
|
|
810
|
+
} else {
|
|
811
|
+
for (const t of top) out.push({ property: "padding-top", value: t });
|
|
812
|
+
for (const t of bottom) out.push({ property: "padding-bottom", value: t });
|
|
813
|
+
}
|
|
814
|
+
return out;
|
|
815
|
+
}
|
|
816
|
+
function normalizeBindings(raw) {
|
|
817
|
+
const byProp = /* @__PURE__ */ new Map();
|
|
818
|
+
for (const b of raw) {
|
|
819
|
+
const refs = byProp.get(b.property) ?? [];
|
|
820
|
+
if (!refs.some((r) => r.kind === b.kind && r.id === b.id)) refs.push(b);
|
|
821
|
+
byProp.set(b.property, refs);
|
|
822
|
+
}
|
|
823
|
+
const out = [];
|
|
824
|
+
const emit = (property, ref) => {
|
|
825
|
+
if (out.some((o) => o.property === property && o.kind === ref.kind && o.id === ref.id)) return;
|
|
826
|
+
out.push({ ...ref, property });
|
|
827
|
+
};
|
|
828
|
+
const radii = RADIUS_PROPS.filter((p) => byProp.has(p));
|
|
829
|
+
const radiusRefs = radii.flatMap((p) => byProp.get(p));
|
|
830
|
+
const distinctRadius = new Set(radiusRefs.map((r) => `${r.kind}|${r.id}`));
|
|
831
|
+
if (radii.length === RADIUS_PROPS.length && distinctRadius.size === 1) {
|
|
832
|
+
emit("border-radius", radiusRefs[0]);
|
|
833
|
+
} else {
|
|
834
|
+
for (const p of radii) for (const r of byProp.get(p)) emit(RADIUS_INDIVIDUAL_MAP[p], r);
|
|
835
|
+
}
|
|
836
|
+
const sideRefs = (...props) => props.flatMap((p) => byProp.get(p) ?? []);
|
|
837
|
+
for (const { property, value } of paddingSides(
|
|
838
|
+
sideRefs("paddingTop", "verticalPadding"),
|
|
839
|
+
sideRefs("paddingRight", "horizontalPadding"),
|
|
840
|
+
sideRefs("paddingBottom", "verticalPadding"),
|
|
841
|
+
sideRefs("paddingLeft", "horizontalPadding"),
|
|
842
|
+
(r) => `${r.kind}|${r.id}`
|
|
843
|
+
)) {
|
|
844
|
+
emit(property, value);
|
|
845
|
+
}
|
|
846
|
+
const hasTypography = byProp.has("typography");
|
|
847
|
+
for (const [prop, refs] of byProp) {
|
|
848
|
+
if (RADIUS_PROPS.includes(prop) || PADDING_RAW_PROPS.has(prop)) continue;
|
|
849
|
+
if (hasTypography && TYPOGRAPHY_SUBPROPS.has(prop)) continue;
|
|
850
|
+
const mapped = simpleProperty(prop);
|
|
851
|
+
for (const r of refs) emit(mapped, r);
|
|
852
|
+
}
|
|
853
|
+
return out;
|
|
854
|
+
}
|
|
855
|
+
var refKey = (r) => `${r.kind}|${r.id}`;
|
|
856
|
+
var ABSENT_KEY = "absent";
|
|
857
|
+
function extractTokens(root, model) {
|
|
858
|
+
const isInSet = root.type === "COMPONENT_SET";
|
|
859
|
+
const { variants, combos } = model ?? variantAxisModel(root);
|
|
860
|
+
if (!variants.length) return [];
|
|
861
|
+
const axisOrder = [];
|
|
862
|
+
const observedValues = /* @__PURE__ */ new Map();
|
|
863
|
+
for (const combo of combos) {
|
|
864
|
+
for (const [axis, value] of Object.entries(combo)) {
|
|
865
|
+
let vals = observedValues.get(axis);
|
|
866
|
+
if (!vals) {
|
|
867
|
+
axisOrder.push(axis);
|
|
868
|
+
observedValues.set(axis, vals = []);
|
|
869
|
+
}
|
|
870
|
+
if (!vals.includes(value)) vals.push(value);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
const axisValues = /* @__PURE__ */ new Map();
|
|
874
|
+
for (const axis of axisOrder) {
|
|
875
|
+
const obs = observedValues.get(axis);
|
|
876
|
+
const declared = root.propertyDefinitions?.[axis]?.variantOptions;
|
|
877
|
+
axisValues.set(
|
|
878
|
+
axis,
|
|
879
|
+
declared ? [...declared.filter((v) => obs.includes(v)), ...obs.filter((v) => !declared.includes(v))] : obs
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
const gridKey = (path, property) => JSON.stringify([path, property]);
|
|
883
|
+
const cellsByPathProp = /* @__PURE__ */ new Map();
|
|
884
|
+
const pathOrder = [];
|
|
885
|
+
const propOrder = /* @__PURE__ */ new Map();
|
|
886
|
+
const partByPath = /* @__PURE__ */ new Map();
|
|
887
|
+
const shownByPath = /* @__PURE__ */ new Map();
|
|
888
|
+
const hidden = hiddenPartRules(root);
|
|
889
|
+
const refsByKey = /* @__PURE__ */ new Map();
|
|
890
|
+
variants.forEach((variant, idx) => {
|
|
891
|
+
const combo = combos[idx];
|
|
892
|
+
const variantRefs = /* @__PURE__ */ new Map();
|
|
893
|
+
const shownByNode = /* @__PURE__ */ new Map();
|
|
894
|
+
const markHidden = (n, inherited) => {
|
|
895
|
+
const own = hidden.shownBy(n) ?? inherited;
|
|
896
|
+
if (own !== void 0) shownByNode.set(n, own);
|
|
897
|
+
for (const child of n.children ?? []) markHidden(child, own);
|
|
898
|
+
};
|
|
899
|
+
for (const child of variant.children ?? []) markHidden(child, void 0);
|
|
900
|
+
walkParts(variant, isInSet ? "Container" : cleanPartName(variant.name), (n, part, path) => {
|
|
901
|
+
const shownBy = shownByNode.get(n);
|
|
902
|
+
if (shownBy !== void 0 && !shownByPath.has(path)) shownByPath.set(path, shownBy);
|
|
903
|
+
for (const ref of normalizeBindings(n.bindings ?? [])) {
|
|
904
|
+
const key = gridKey(path, ref.property);
|
|
905
|
+
partByPath.set(path, part);
|
|
906
|
+
const { property: _property, ...identity } = ref;
|
|
907
|
+
let inner = variantRefs.get(key);
|
|
908
|
+
if (!inner) variantRefs.set(key, inner = /* @__PURE__ */ new Map());
|
|
909
|
+
const rk = refKey(ref);
|
|
910
|
+
inner.set(rk, identity);
|
|
911
|
+
refsByKey.set(rk, identity);
|
|
912
|
+
}
|
|
913
|
+
}, hidden.prune);
|
|
914
|
+
for (const [key, inner] of variantRefs) {
|
|
915
|
+
let cells = cellsByPathProp.get(key);
|
|
916
|
+
if (!cells) {
|
|
917
|
+
cellsByPathProp.set(key, cells = []);
|
|
918
|
+
const [path, prop] = JSON.parse(key);
|
|
919
|
+
if (!propOrder.has(path)) {
|
|
920
|
+
pathOrder.push(path);
|
|
921
|
+
propOrder.set(path, []);
|
|
922
|
+
}
|
|
923
|
+
propOrder.get(path).push(prop);
|
|
924
|
+
}
|
|
925
|
+
cells.push({ combo, keys: [...inner.keys()].sort() });
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
for (const cells of cellsByPathProp.values()) {
|
|
929
|
+
const present = new Set(cells.map((c) => c.combo));
|
|
930
|
+
for (const combo of combos) {
|
|
931
|
+
if (!present.has(combo)) cells.push({ combo, keys: [ABSENT_KEY] });
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
const cellKey = (c) => JSON.stringify(c.keys);
|
|
935
|
+
const projKey = (combo, axes) => JSON.stringify(axes.map((a) => combo[a]));
|
|
936
|
+
const relevantAxes = (cells) => {
|
|
937
|
+
const relevant = [];
|
|
938
|
+
for (const axis of axisOrder) {
|
|
939
|
+
const present = new Set(cells.map((c) => c.combo[axis]));
|
|
940
|
+
if (present.size < axisValues.get(axis).length) {
|
|
941
|
+
relevant.push(axis);
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const others = axisOrder.filter((a) => a !== axis);
|
|
945
|
+
const groups = /* @__PURE__ */ new Map();
|
|
946
|
+
for (const c of cells) {
|
|
947
|
+
const gk = projKey(c.combo, others);
|
|
948
|
+
const tk = cellKey(c);
|
|
949
|
+
const prev = groups.get(gk);
|
|
950
|
+
if (prev === void 0) groups.set(gk, tk);
|
|
951
|
+
else if (prev !== tk) {
|
|
952
|
+
relevant.push(axis);
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return relevant;
|
|
958
|
+
};
|
|
959
|
+
const hasConflict = (cells, axes) => {
|
|
960
|
+
const m = /* @__PURE__ */ new Map();
|
|
961
|
+
for (const c of cells) {
|
|
962
|
+
const k = projKey(c.combo, axes);
|
|
963
|
+
const tk = cellKey(c);
|
|
964
|
+
const prev = m.get(k);
|
|
965
|
+
if (prev === void 0) m.set(k, tk);
|
|
966
|
+
else if (prev !== tk) return true;
|
|
967
|
+
}
|
|
968
|
+
return false;
|
|
969
|
+
};
|
|
970
|
+
const buildRules = (cellsIn) => {
|
|
971
|
+
let cells = cellsIn;
|
|
972
|
+
let relevant = relevantAxes(cells);
|
|
973
|
+
for (const axis of axisOrder) {
|
|
974
|
+
if (!hasConflict(cells, relevant)) break;
|
|
975
|
+
if (!relevant.includes(axis)) relevant = axisOrder.filter((a) => relevant.includes(a) || a === axis);
|
|
976
|
+
}
|
|
977
|
+
if (hasConflict(cells, relevant)) {
|
|
978
|
+
relevant = [...axisOrder];
|
|
979
|
+
const byCombo = /* @__PURE__ */ new Map();
|
|
980
|
+
for (const c of cells) {
|
|
981
|
+
const k = projKey(c.combo, axisOrder);
|
|
982
|
+
if (!byCombo.has(k)) byCombo.set(k, c);
|
|
983
|
+
}
|
|
984
|
+
cells = [...byCombo.values()];
|
|
985
|
+
}
|
|
986
|
+
const groups = /* @__PURE__ */ new Map();
|
|
987
|
+
for (const c of cells) {
|
|
988
|
+
const k = projKey(c.combo, relevant);
|
|
989
|
+
let g = groups.get(k);
|
|
990
|
+
if (!g) groups.set(k, g = { combo: c.combo, keys: /* @__PURE__ */ new Set() });
|
|
991
|
+
c.keys.forEach((t) => g.keys.add(t));
|
|
992
|
+
}
|
|
993
|
+
let rules = [];
|
|
994
|
+
for (const g of groups.values()) {
|
|
995
|
+
for (const key of [...g.keys].sort()) {
|
|
996
|
+
rules.push({ key, values: new Map(relevant.map((a) => [a, /* @__PURE__ */ new Set([g.combo[a]])])) });
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const conditionKey = (r, excludeAxis) => JSON.stringify(axisOrder.filter((a) => a !== excludeAxis).map((a) => r.values.has(a) ? [...r.values.get(a)].sort() : null));
|
|
1000
|
+
for (const axis of relevant) {
|
|
1001
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1002
|
+
for (const r of rules) {
|
|
1003
|
+
const k = JSON.stringify([r.key, conditionKey(r, axis)]);
|
|
1004
|
+
const prev = merged.get(k);
|
|
1005
|
+
if (prev && prev.values.has(axis) && r.values.has(axis)) {
|
|
1006
|
+
r.values.get(axis).forEach((v) => prev.values.get(axis).add(v));
|
|
1007
|
+
} else if (!merged.has(k)) {
|
|
1008
|
+
merged.set(k, r);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
rules = [...merged.values()];
|
|
1012
|
+
}
|
|
1013
|
+
for (const r of rules) {
|
|
1014
|
+
for (const axis of [...r.values.keys()]) {
|
|
1015
|
+
const vals = r.values.get(axis);
|
|
1016
|
+
const observed = /* @__PURE__ */ new Set();
|
|
1017
|
+
for (const combo of combos) {
|
|
1018
|
+
const matchesOthers = [...r.values.entries()].every(
|
|
1019
|
+
([a, vs]) => a === axis || vs.has(combo[a])
|
|
1020
|
+
);
|
|
1021
|
+
if (matchesOthers) observed.add(combo[axis]);
|
|
1022
|
+
}
|
|
1023
|
+
if ([...observed].every((v) => vals.has(v))) r.values.delete(axis);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1027
|
+
for (const r of rules) {
|
|
1028
|
+
const k = JSON.stringify([r.key, conditionKey(r, null)]);
|
|
1029
|
+
if (!seen.has(k)) seen.set(k, r);
|
|
1030
|
+
}
|
|
1031
|
+
rules = [...seen.values()];
|
|
1032
|
+
rules = rules.filter(
|
|
1033
|
+
(r) => !rules.some(
|
|
1034
|
+
(other) => other !== r && other.key === r.key && other.values.size < r.values.size && [...other.values.entries()].every(
|
|
1035
|
+
([a, vs]) => r.values.has(a) && [...r.values.get(a)].every((v) => vs.has(v))
|
|
1036
|
+
)
|
|
1037
|
+
)
|
|
1038
|
+
);
|
|
1039
|
+
return rules;
|
|
1040
|
+
};
|
|
1041
|
+
const defaultCombo = combos[0];
|
|
1042
|
+
const toTokenRule = (path, property, r) => {
|
|
1043
|
+
const conditions = {};
|
|
1044
|
+
for (const axis of axisOrder) {
|
|
1045
|
+
const vs = r.values.get(axis);
|
|
1046
|
+
if (!vs) continue;
|
|
1047
|
+
conditions[axis] = axisValues.get(axis).filter((v) => vs.has(v));
|
|
1048
|
+
}
|
|
1049
|
+
const shownBy = shownByPath.get(path);
|
|
1050
|
+
return {
|
|
1051
|
+
part: partByPath.get(path),
|
|
1052
|
+
path,
|
|
1053
|
+
property,
|
|
1054
|
+
conditions,
|
|
1055
|
+
...refsByKey.get(r.key),
|
|
1056
|
+
// Absent, never undefined, on a rule that applies unconditionally: the
|
|
1057
|
+
// same contract AnatomyPart.shownBy keeps, so `'shownBy' in rule` is a
|
|
1058
|
+
// reliable test and the key never appears in output it does not apply to.
|
|
1059
|
+
...shownBy !== void 0 ? { shownBy } : {}
|
|
1060
|
+
};
|
|
1061
|
+
};
|
|
1062
|
+
const ruleSortKey = (r) => {
|
|
1063
|
+
const matchesDefault = [...r.values.entries()].every(([a, vs]) => vs.has(defaultCombo[a]));
|
|
1064
|
+
const axisBits = axisOrder.map((a, i) => {
|
|
1065
|
+
const vs = r.values.get(a);
|
|
1066
|
+
if (!vs) return "";
|
|
1067
|
+
const indices = axisValues.get(a).map((v, vi) => vs.has(v) ? String(vi).padStart(3, "0") : "").filter(Boolean).join(".");
|
|
1068
|
+
return `${i}:${indices}`;
|
|
1069
|
+
}).filter(Boolean).join("|");
|
|
1070
|
+
return [
|
|
1071
|
+
matchesDefault ? "0" : "1",
|
|
1072
|
+
String(r.values.size).padStart(3, "0"),
|
|
1073
|
+
axisBits,
|
|
1074
|
+
// An absent rule has no reference and sorts first, exactly as the old
|
|
1075
|
+
// control-character sentinel did. It is dropped below either way.
|
|
1076
|
+
refsByKey.get(r.key)?.name ?? "",
|
|
1077
|
+
r.key
|
|
1078
|
+
];
|
|
1079
|
+
};
|
|
1080
|
+
const compareKeys = (a, b) => {
|
|
1081
|
+
for (let i = 0; i < a.length; i++) {
|
|
1082
|
+
if (a[i] < b[i]) return -1;
|
|
1083
|
+
if (a[i] > b[i]) return 1;
|
|
1084
|
+
}
|
|
1085
|
+
return 0;
|
|
1086
|
+
};
|
|
1087
|
+
const out = [];
|
|
1088
|
+
for (const path of pathOrder) {
|
|
1089
|
+
for (const prop of propOrder.get(path)) {
|
|
1090
|
+
const cells = cellsByPathProp.get(gridKey(path, prop));
|
|
1091
|
+
const rules = buildRules(cells);
|
|
1092
|
+
rules.sort((a, b) => compareKeys(ruleSortKey(a), ruleSortKey(b)));
|
|
1093
|
+
for (const r of rules) {
|
|
1094
|
+
if (r.key === ABSENT_KEY) continue;
|
|
1095
|
+
out.push(toTokenRule(path, prop, r));
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return out;
|
|
1100
|
+
}
|
|
1101
|
+
var TYPOGRAPHY_PROPS = ["typography", "fontSize", "fontFamily", "fontStyle", "fontWeight", "lineHeight", "letterSpacing"];
|
|
1102
|
+
var PADDING_PROPS = ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "verticalPadding", "horizontalPadding"];
|
|
1103
|
+
function extractGaps(root) {
|
|
1104
|
+
const out = [];
|
|
1105
|
+
const seenGaps = /* @__PURE__ */ new Set();
|
|
1106
|
+
const pushGap = (part, path, property, issue, value) => {
|
|
1107
|
+
const key = `${path} ${property} ${issue}`;
|
|
1108
|
+
if (seenGaps.has(key)) return;
|
|
1109
|
+
seenGaps.add(key);
|
|
1110
|
+
out.push({ part, path, property, issue, ...value !== void 0 ? { value } : {} });
|
|
1111
|
+
};
|
|
1112
|
+
const isInSet = root.type === "COMPONENT_SET";
|
|
1113
|
+
const def = defaultVariant(root);
|
|
1114
|
+
walkParts(def, isInSet ? "Container" : cleanPartName(def.name), (n, part, path) => {
|
|
1115
|
+
const bound = new Set((n.bindings ?? []).map((b) => b.property));
|
|
1116
|
+
if (n.hasUnboundPaint) {
|
|
1117
|
+
pushGap(part, path, simpleProperty("fills"), "hardcoded-color", n.unboundFill);
|
|
1118
|
+
}
|
|
1119
|
+
if (n.hasUnboundStroke) {
|
|
1120
|
+
pushGap(part, path, simpleProperty("strokes"), "hardcoded-color", n.unboundStroke);
|
|
1121
|
+
}
|
|
1122
|
+
if (n.hasUnboundGradient) {
|
|
1123
|
+
pushGap(part, path, simpleProperty("fills"), "missing-token-binding");
|
|
1124
|
+
}
|
|
1125
|
+
if (n.hasUnboundEffect) {
|
|
1126
|
+
pushGap(part, path, simpleProperty("effects"), "missing-token-binding");
|
|
1127
|
+
}
|
|
1128
|
+
if (n.opacity !== void 0 && n.opacity !== 1 && !bound.has("opacity")) {
|
|
1129
|
+
pushGap(part, path, simpleProperty("opacity"), "hardcoded-value", Math.round(n.opacity * 1e4) / 1e4);
|
|
1130
|
+
}
|
|
1131
|
+
if (n.type === "TEXT" && !TYPOGRAPHY_PROPS.some((p) => bound.has(p))) {
|
|
1132
|
+
pushGap(part, path, simpleProperty("typography"), "missing-token-binding");
|
|
1133
|
+
}
|
|
1134
|
+
const l = n.layout;
|
|
1135
|
+
if (!l) return;
|
|
1136
|
+
if (l.itemSpacing !== void 0 && !bound.has("itemSpacing")) {
|
|
1137
|
+
pushGap(part, path, simpleProperty("itemSpacing"), "hardcoded-value", l.itemSpacing);
|
|
1138
|
+
}
|
|
1139
|
+
if (l.cornerRadius !== void 0 && ![...RADIUS_BINDINGS].some((p) => bound.has(p))) {
|
|
1140
|
+
pushGap(part, path, simpleProperty("cornerRadius"), "hardcoded-value", l.cornerRadius);
|
|
1141
|
+
}
|
|
1142
|
+
if (!PADDING_PROPS.some((p) => bound.has(p))) {
|
|
1143
|
+
const side = (v) => v !== void 0 ? [v] : [];
|
|
1144
|
+
for (const { property, value } of paddingSides(
|
|
1145
|
+
side(l.paddingTop),
|
|
1146
|
+
side(l.paddingRight),
|
|
1147
|
+
side(l.paddingBottom),
|
|
1148
|
+
side(l.paddingLeft)
|
|
1149
|
+
)) {
|
|
1150
|
+
pushGap(part, path, property, "hardcoded-value", value);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
return out;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// ../extractor/src/pivot.ts
|
|
1158
|
+
function isModifierAxis(axis) {
|
|
1159
|
+
if (axis.values.length !== 2) return false;
|
|
1160
|
+
const lower = axis.values.map((v) => v.toLowerCase());
|
|
1161
|
+
return lower.includes("true") && lower.includes("false");
|
|
1162
|
+
}
|
|
1163
|
+
function isStateAxisName(prop) {
|
|
1164
|
+
const n = prop.trim().toLowerCase();
|
|
1165
|
+
return n === "state" || n === "states";
|
|
1166
|
+
}
|
|
1167
|
+
|
|
565
1168
|
// ../extractor/src/statesMatrix.ts
|
|
566
1169
|
var STATE_ORDER = [
|
|
567
1170
|
"default",
|
|
@@ -588,6 +1191,200 @@ var STATE_ORDER = [
|
|
|
588
1191
|
"visited"
|
|
589
1192
|
];
|
|
590
1193
|
var STATE_VOCAB = new Set(STATE_ORDER);
|
|
1194
|
+
var WHITESPACE2 = /\s/;
|
|
1195
|
+
function stateBaseName(v) {
|
|
1196
|
+
const lowered = v.trim().toLowerCase();
|
|
1197
|
+
let end = lowered.length;
|
|
1198
|
+
while (end > 0 && WHITESPACE2.test(lowered[end - 1])) end--;
|
|
1199
|
+
if (end === 0 || lowered[end - 1] !== ")") return lowered.trim();
|
|
1200
|
+
let open = -1;
|
|
1201
|
+
for (let i = end - 2; i >= 0; i--) {
|
|
1202
|
+
const ch = lowered[i];
|
|
1203
|
+
if (ch === ")") break;
|
|
1204
|
+
if (ch === "(") open = i;
|
|
1205
|
+
}
|
|
1206
|
+
if (open === -1) return lowered.trim();
|
|
1207
|
+
let cut = open;
|
|
1208
|
+
while (cut > 0 && WHITESPACE2.test(lowered[cut - 1])) cut--;
|
|
1209
|
+
return lowered.slice(0, cut).trim();
|
|
1210
|
+
}
|
|
1211
|
+
function stateRank(v) {
|
|
1212
|
+
const i = STATE_ORDER.indexOf(stateBaseName(v));
|
|
1213
|
+
return i === -1 ? STATE_ORDER.length : i;
|
|
1214
|
+
}
|
|
1215
|
+
function orderStates(values) {
|
|
1216
|
+
return values.map((v, i) => ({ v, i })).sort((a, b) => stateRank(a.v) - stateRank(b.v) || a.i - b.i).map((x) => x.v);
|
|
1217
|
+
}
|
|
1218
|
+
function isStateLike(axis) {
|
|
1219
|
+
const n = axis.prop.trim().toLowerCase();
|
|
1220
|
+
if (isStateAxisName(axis.prop) || n === "status") return true;
|
|
1221
|
+
const hits = axis.values.filter((v) => STATE_VOCAB.has(v.trim().toLowerCase())).length;
|
|
1222
|
+
return hits >= 2;
|
|
1223
|
+
}
|
|
1224
|
+
function isStateVocabName(prop) {
|
|
1225
|
+
const n = prop.trim().toLowerCase();
|
|
1226
|
+
return isStateAxisName(prop) || n === "status" || STATE_VOCAB.has(stateBaseName(prop));
|
|
1227
|
+
}
|
|
1228
|
+
function trueValueOf(axis) {
|
|
1229
|
+
return axis.values.find((v) => v.toLowerCase() === "true") ?? axis.values[axis.values.length - 1];
|
|
1230
|
+
}
|
|
1231
|
+
function detectStateMatrix(variants) {
|
|
1232
|
+
const stateAxis = variants.find(isStateLike) ?? null;
|
|
1233
|
+
if (stateAxis) {
|
|
1234
|
+
const rowAxis2 = variants.find((v) => v.prop !== stateAxis.prop && !isModifierAxis(v) && !isStateLike(v)) ?? null;
|
|
1235
|
+
const columns2 = orderStates(stateAxis.values).map((v) => ({
|
|
1236
|
+
label: v,
|
|
1237
|
+
override: { [stateAxis.prop]: v }
|
|
1238
|
+
}));
|
|
1239
|
+
return { encoding: "enum", columns: columns2, rowAxis: rowAxis2?.prop ?? null, axis: stateAxis.prop };
|
|
1240
|
+
}
|
|
1241
|
+
const flags = variants.filter((v) => isModifierAxis(v) && isStateVocabName(v.prop));
|
|
1242
|
+
if (flags.length === 0) return null;
|
|
1243
|
+
const orderedFlagProps = orderStates(flags.map((f) => f.prop));
|
|
1244
|
+
const orderedFlags = orderedFlagProps.map((p) => flags.find((f) => f.prop === p));
|
|
1245
|
+
const columns = [
|
|
1246
|
+
{ label: "Default", override: {} },
|
|
1247
|
+
...orderedFlags.map((f) => ({ label: f.prop, override: { [f.prop]: trueValueOf(f) } }))
|
|
1248
|
+
];
|
|
1249
|
+
const rowAxis = variants.find((v) => !isModifierAxis(v) && !isStateLike(v)) ?? null;
|
|
1250
|
+
return { encoding: "flags", columns, rowAxis: rowAxis?.prop ?? null, axis: null };
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// ../extractor/src/props.ts
|
|
1254
|
+
var KIND_MAP = {
|
|
1255
|
+
VARIANT: "variant",
|
|
1256
|
+
BOOLEAN: "boolean",
|
|
1257
|
+
TEXT: "text",
|
|
1258
|
+
INSTANCE_SWAP: "instanceSwap"
|
|
1259
|
+
};
|
|
1260
|
+
function extractProps(root) {
|
|
1261
|
+
return Object.entries(root.propertyDefinitions ?? {}).map(([raw, def]) => ({
|
|
1262
|
+
name: cleanPropName(raw),
|
|
1263
|
+
kind: KIND_MAP[def.type],
|
|
1264
|
+
...def.variantOptions !== void 0 ? { options: def.variantOptions } : {},
|
|
1265
|
+
default: def.defaultValue
|
|
1266
|
+
}));
|
|
1267
|
+
}
|
|
1268
|
+
function extractVariants(root) {
|
|
1269
|
+
return extractProps(root).filter((p) => p.kind === "variant").map((p) => ({ prop: p.name, values: p.options ?? [] }));
|
|
1270
|
+
}
|
|
1271
|
+
function extractStates(root) {
|
|
1272
|
+
const info = detectStateMatrix(extractVariants(root));
|
|
1273
|
+
if (!info) return ["Default"];
|
|
1274
|
+
const labels = info.columns.map((c) => c.label);
|
|
1275
|
+
return labels.length ? labels : ["Default"];
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// ../extractor/src/layout.ts
|
|
1279
|
+
function valuesOf(l) {
|
|
1280
|
+
return {
|
|
1281
|
+
...l.cornerRadius !== void 0 ? { radius: l.cornerRadius } : {},
|
|
1282
|
+
...l.itemSpacing !== void 0 ? { gap: l.itemSpacing } : {}
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
function fmt(l) {
|
|
1286
|
+
const bits = [];
|
|
1287
|
+
if (l.mode) bits.push(l.mode.toLowerCase());
|
|
1288
|
+
const pads = [l.paddingTop ?? 0, l.paddingRight ?? 0, l.paddingBottom ?? 0, l.paddingLeft ?? 0];
|
|
1289
|
+
if (pads.some((p) => p > 0)) bits.push(`padding ${pads.join("/")}`);
|
|
1290
|
+
if (l.itemSpacing !== void 0) bits.push(`gap ${l.itemSpacing}`);
|
|
1291
|
+
if (l.cornerRadius !== void 0) bits.push(`radius ${l.cornerRadius}`);
|
|
1292
|
+
return bits.join(", ");
|
|
1293
|
+
}
|
|
1294
|
+
function extractLayout(root) {
|
|
1295
|
+
const out = [];
|
|
1296
|
+
const isInSet = root.type === "COMPONENT_SET";
|
|
1297
|
+
walkParts(defaultVariant(root), isInSet ? "Container" : cleanPartName(root.name), (n, _part, path) => {
|
|
1298
|
+
if (!n.layout) return;
|
|
1299
|
+
const summary = fmt(n.layout);
|
|
1300
|
+
if (summary) out.push({ part: n.name, path, summary, values: valuesOf(n.layout) });
|
|
1301
|
+
}, false);
|
|
1302
|
+
return out;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// ../extractor/src/rawValues.ts
|
|
1306
|
+
var PADDING_BINDINGS = /* @__PURE__ */ new Set([
|
|
1307
|
+
"paddingTop",
|
|
1308
|
+
"paddingRight",
|
|
1309
|
+
"paddingBottom",
|
|
1310
|
+
"paddingLeft",
|
|
1311
|
+
"verticalPadding",
|
|
1312
|
+
"horizontalPadding"
|
|
1313
|
+
]);
|
|
1314
|
+
function extractRawValues(root) {
|
|
1315
|
+
const out = [];
|
|
1316
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1317
|
+
const push = (part, property, value) => {
|
|
1318
|
+
const k = JSON.stringify([part, property]);
|
|
1319
|
+
if (seen.has(k)) return;
|
|
1320
|
+
seen.add(k);
|
|
1321
|
+
out.push({ part, property, value });
|
|
1322
|
+
};
|
|
1323
|
+
const def = defaultVariant(root);
|
|
1324
|
+
walkParts(def, root.type === "COMPONENT_SET" ? "Container" : cleanPartName(def.name), (n, part) => {
|
|
1325
|
+
const bound = new Set((n.bindings ?? []).map((b) => b.property));
|
|
1326
|
+
if (n.unboundFill) push(part, "fill", n.unboundFill);
|
|
1327
|
+
const l = n.layout;
|
|
1328
|
+
if (l) {
|
|
1329
|
+
const t = l.paddingTop ?? 0, r = l.paddingRight ?? 0, b = l.paddingBottom ?? 0, lf = l.paddingLeft ?? 0;
|
|
1330
|
+
const hasPad = t > 0 || r > 0 || b > 0 || lf > 0;
|
|
1331
|
+
if (hasPad && ![...PADDING_BINDINGS].some((p) => bound.has(p))) {
|
|
1332
|
+
if (t === r && r === b && b === lf) push(part, "padding", String(t));
|
|
1333
|
+
else {
|
|
1334
|
+
if (lf === r && lf > 0) push(part, "padding-x", String(lf));
|
|
1335
|
+
else {
|
|
1336
|
+
if (lf > 0) push(part, "padding-left", String(lf));
|
|
1337
|
+
if (r > 0) push(part, "padding-right", String(r));
|
|
1338
|
+
}
|
|
1339
|
+
if (t === b && t > 0) push(part, "padding-y", String(t));
|
|
1340
|
+
else {
|
|
1341
|
+
if (t > 0) push(part, "padding-top", String(t));
|
|
1342
|
+
if (b > 0) push(part, "padding-bottom", String(b));
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (l.itemSpacing !== void 0 && l.itemSpacing > 0 && !bound.has("itemSpacing")) {
|
|
1347
|
+
push(part, "gap", String(l.itemSpacing));
|
|
1348
|
+
}
|
|
1349
|
+
if (l.cornerRadius !== void 0 && l.cornerRadius > 0 && ![...RADIUS_BINDINGS].some((p) => bound.has(p))) {
|
|
1350
|
+
push(part, "border-radius", String(l.cornerRadius));
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}, true);
|
|
1354
|
+
return out;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// ../extractor/src/extract.ts
|
|
1358
|
+
function toVariantInstances(model) {
|
|
1359
|
+
return model.variants.map((v, i) => ({ nodeId: v.id, name: v.name, values: model.combos[i] }));
|
|
1360
|
+
}
|
|
1361
|
+
function extract(root, meta) {
|
|
1362
|
+
const { parts, related, componentId } = extractAnatomy(root);
|
|
1363
|
+
const model = variantAxisModel(root);
|
|
1364
|
+
return {
|
|
1365
|
+
name: root.name,
|
|
1366
|
+
figmaKey: root.key ?? "",
|
|
1367
|
+
figmaFile: meta.figmaFile,
|
|
1368
|
+
// Spread in only when a name was actually supplied, so an absent name is
|
|
1369
|
+
// an absent key rather than a key holding undefined.
|
|
1370
|
+
...meta.figmaFileName ? { figmaFileName: meta.figmaFileName } : {},
|
|
1371
|
+
figmaNode: root.id,
|
|
1372
|
+
description: root.description ?? "",
|
|
1373
|
+
documentationLinks: root.documentationLinks ?? [],
|
|
1374
|
+
anatomy: parts,
|
|
1375
|
+
anatomyComponentId: componentId,
|
|
1376
|
+
props: extractProps(root),
|
|
1377
|
+
variants: extractVariants(root),
|
|
1378
|
+
variantInstances: toVariantInstances(model),
|
|
1379
|
+
states: extractStates(root),
|
|
1380
|
+
tokens: extractTokens(root, model),
|
|
1381
|
+
related,
|
|
1382
|
+
gaps: extractGaps(root),
|
|
1383
|
+
layout: extractLayout(root),
|
|
1384
|
+
rawValues: extractRawValues(root),
|
|
1385
|
+
nodeEffects: extractNodeEffects(root)
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
591
1388
|
|
|
592
1389
|
// ../extractor/src/v5/precision.ts
|
|
593
1390
|
var SIGNIFICANT_DIGITS = 7;
|
|
@@ -643,10 +1440,10 @@ var DEFAULT_SEVERITY = {
|
|
|
643
1440
|
EXPORT_SCOPED: "info"
|
|
644
1441
|
};
|
|
645
1442
|
var compareCodeUnits = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
646
|
-
function diagnostic(
|
|
1443
|
+
function diagnostic(code3, fields) {
|
|
647
1444
|
return {
|
|
648
|
-
code:
|
|
649
|
-
severity: DEFAULT_SEVERITY[
|
|
1445
|
+
code: code3,
|
|
1446
|
+
severity: DEFAULT_SEVERITY[code3],
|
|
650
1447
|
entity_id: fields.entity_id,
|
|
651
1448
|
...fields.mode_id !== void 0 ? { mode_id: fields.mode_id } : {},
|
|
652
1449
|
message: fields.message,
|
|
@@ -657,8 +1454,16 @@ function diagnostic(code2, fields) {
|
|
|
657
1454
|
// ../extractor/src/hash.ts
|
|
658
1455
|
var import_js_sha256 = __toESM(require_sha256(), 1);
|
|
659
1456
|
|
|
1457
|
+
// ../extractor/src/displayNames.ts
|
|
1458
|
+
function displayComponentName(raw) {
|
|
1459
|
+
const name = raw.replace(/^[._]+/, "").trim();
|
|
1460
|
+
if (!name) return "";
|
|
1461
|
+
if (/[A-Z]/.test(name)) return name;
|
|
1462
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
660
1465
|
// ../extractor/src/prose/prompt.ts
|
|
661
|
-
var
|
|
1466
|
+
var LEGACY_PROSE_SYSTEM_PROMPT = [
|
|
662
1467
|
"You write component guideline prose for a design-system specification tool.",
|
|
663
1468
|
"Your output fills three spec sections: Definition, Accessibility, and Do's & Don'ts,",
|
|
664
1469
|
"in the voice of best-in-class design systems (Atlassian, Material, Polaris, Carbon).",
|
|
@@ -707,7 +1512,7 @@ var PROSE_SYSTEM_PROMPT = [
|
|
|
707
1512
|
"Return only the JSON object requested in the user message. No preamble and no prose outside the",
|
|
708
1513
|
"JSON."
|
|
709
1514
|
].join("\n");
|
|
710
|
-
var
|
|
1515
|
+
var LEGACY_FEW_SHOT_PROMPT = [
|
|
711
1516
|
"Component: Button",
|
|
712
1517
|
"",
|
|
713
1518
|
"Anatomy: Container, Label, Leading icon (component)",
|
|
@@ -719,7 +1524,7 @@ var FEW_SHOT_PROMPT = [
|
|
|
719
1524
|
"",
|
|
720
1525
|
`Return ONLY a JSON object with keys: definition (one sentence defining what it is, then a short benefit-led overview: where it is used, the value it gives people, its role, and a guiding principle; no style names and no when-to-use guide), variantsSummary (1-2 sentences on what varies across the options, then a bulleted "when to use which type" guide with bold type names when it has several types), anatomySummary (1-2 sentences orienting the reader to the component structure and the role of its key parts), anatomyParts (array of { name, description } where each name EXACTLY matches one of the Anatomy part names above and description is one concise sentence naming that part's role), accessibility (a bulleted list; give each bullet a short bold lead-in then the guidance; include one bullet flagging what cannot be known from the design file), interactions (Markdown under "### Mouse", "### Keyboard", "### Other" subheadings, 2-3 bullets each, anchored to the States above), designConsiderations (3-4 designer-facing bullets on contrast, state distinguishability, and missing-state flags), contentConsiderations (3-4 bullets on label writing, truncation, and internationalization), dos (string[], 3 to 5 items, each starting with a bold rule summary then the reason), donts (string[], 3 to 5 items, same shape). Use Markdown (bold lead-ins, lists, at most "###" subheadings); never "#" or "##" headings. Do not use em dashes. Do not include any prose outside the JSON.`
|
|
721
1526
|
].join("\n");
|
|
722
|
-
var
|
|
1527
|
+
var LEGACY_FEW_SHOT_RESPONSE = {
|
|
723
1528
|
definition: "A Button triggers an action when activated. Used across products to perform common actions, it gives people a familiar, accessible way to engage with the interface and keeps frequent tasks fast and predictable. It is essential for guiding people through workflows and performing the key actions on a screen. Create buttons that are clear, easy to identify, and accessible.",
|
|
724
1529
|
variantsSummary: [
|
|
725
1530
|
"Style sets the visual weight and states cover the interactive feedback; all styles share the same anatomy.",
|
|
@@ -773,6 +1578,315 @@ var FEW_SHOT_RESPONSE = {
|
|
|
773
1578
|
"**Don't disable a button without explaining why.** A disabled control gives no reason and drops out of the tab order, so use inline validation instead."
|
|
774
1579
|
]
|
|
775
1580
|
};
|
|
1581
|
+
var LEGACY_FOUNDATION_SYSTEM_PROMPT = [
|
|
1582
|
+
"You write short descriptions of design-token groups for a design-system reference.",
|
|
1583
|
+
"Each description sits under a group heading in a generated documentation frame.",
|
|
1584
|
+
"",
|
|
1585
|
+
"You are given the token names in the group and their resolved values. That is ALL you know.",
|
|
1586
|
+
"Describe what the group is for, as its names and values actually show.",
|
|
1587
|
+
"",
|
|
1588
|
+
"Never invent: no component names the tokens do not mention, no counts, no accessibility",
|
|
1589
|
+
"claims, no history, no rules the names do not support. If the names are too generic to",
|
|
1590
|
+
"support a purpose, describe the shape of the set plainly instead and stop. A vague but true",
|
|
1591
|
+
"sentence is correct; a specific but invented one is a defect.",
|
|
1592
|
+
"",
|
|
1593
|
+
"Voice:",
|
|
1594
|
+
"- One or two sentences. Under 220 characters. No heading, no list, no markdown.",
|
|
1595
|
+
"- Plain and factual, the tone of a peer explaining their own file.",
|
|
1596
|
+
'- Say what the group is for and when to reach for it. Lead with the purpose, not "This group".',
|
|
1597
|
+
'- Write for people, not "the user".',
|
|
1598
|
+
"- Never use em dashes or en dashes. Use a period, comma, colon, or parentheses.",
|
|
1599
|
+
'- Do not restate the heading as a sentence ("Surface colours are colours for surfaces").',
|
|
1600
|
+
"",
|
|
1601
|
+
"Return ONLY a JSON object mapping each group key to its description string.",
|
|
1602
|
+
"No prose outside the JSON, no code fence."
|
|
1603
|
+
].join("\n");
|
|
1604
|
+
|
|
1605
|
+
// ../extractor/src/prose/v2.ts
|
|
1606
|
+
var PROSE_V2_KEYS = [
|
|
1607
|
+
"overview",
|
|
1608
|
+
"whenToUse",
|
|
1609
|
+
"whenNotToUse",
|
|
1610
|
+
"variantsIntro",
|
|
1611
|
+
"variantsGuide",
|
|
1612
|
+
"anatomySummary",
|
|
1613
|
+
"anatomyParts",
|
|
1614
|
+
"properties",
|
|
1615
|
+
"states",
|
|
1616
|
+
"keyboard",
|
|
1617
|
+
"pointer",
|
|
1618
|
+
"semantics",
|
|
1619
|
+
"content",
|
|
1620
|
+
"guidelines"
|
|
1621
|
+
];
|
|
1622
|
+
var KEYBOARD_KEYS = [
|
|
1623
|
+
"Tab",
|
|
1624
|
+
"Shift+Tab",
|
|
1625
|
+
"Enter",
|
|
1626
|
+
"Space",
|
|
1627
|
+
"Escape",
|
|
1628
|
+
"Arrow Up",
|
|
1629
|
+
"Arrow Down",
|
|
1630
|
+
"Arrow Left",
|
|
1631
|
+
"Arrow Right",
|
|
1632
|
+
"Home",
|
|
1633
|
+
"End",
|
|
1634
|
+
"Page Up",
|
|
1635
|
+
"Page Down",
|
|
1636
|
+
"Delete",
|
|
1637
|
+
"Backspace"
|
|
1638
|
+
];
|
|
1639
|
+
|
|
1640
|
+
// ../extractor/src/prose/promptV2.ts
|
|
1641
|
+
var PROMPT_RETURN_ANCHOR = "\nReturn ONLY a JSON object with these keys: ";
|
|
1642
|
+
function partKind(part) {
|
|
1643
|
+
if (part.nested) return `nested component ${part.component ?? "component"}`;
|
|
1644
|
+
switch (part.type) {
|
|
1645
|
+
case "TEXT":
|
|
1646
|
+
return "text";
|
|
1647
|
+
case "VECTOR":
|
|
1648
|
+
case "BOOLEAN_OPERATION":
|
|
1649
|
+
case "STAR":
|
|
1650
|
+
case "POLYGON":
|
|
1651
|
+
case "LINE":
|
|
1652
|
+
return "vector";
|
|
1653
|
+
case "RECTANGLE":
|
|
1654
|
+
case "ELLIPSE":
|
|
1655
|
+
return "shape";
|
|
1656
|
+
case "FRAME":
|
|
1657
|
+
case "GROUP":
|
|
1658
|
+
case "SECTION":
|
|
1659
|
+
case "COMPONENT":
|
|
1660
|
+
case "INSTANCE":
|
|
1661
|
+
return "container";
|
|
1662
|
+
default:
|
|
1663
|
+
return part.type.toLowerCase();
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
var PROSE_KEY_INSTRUCTIONS = {
|
|
1667
|
+
overview: "overview ({ lede, body }: lede is one sentence saying what the component is and what a person does with it; body is at most one short paragraph, one or two sentences, on where it appears and what it holds; describe, never explain or justify; no option names)",
|
|
1668
|
+
whenToUse: "whenToUse (string[], 2 to 4 concrete situations where this component is the right choice; each names a task or context, never a rule about how to use it)",
|
|
1669
|
+
whenNotToUse: 'whenNotToUse (string[], 2 to 3 situations where another control fits better, each phrased "For <situation>, use <alternative> instead" with the reason after a semicolon; never start with "Do not"; name another component only if it is listed under Related above)',
|
|
1670
|
+
variantsIntro: "variantsIntro (one or two sentences on what the option axes change; never mention states)",
|
|
1671
|
+
variantsGuide: "variantsGuide ({ name, guidance }[] with one entry per option value listed under Options above, name spelled exactly as listed, guidance one sentence on when to choose it; omit state values)",
|
|
1672
|
+
anatomySummary: "anatomySummary (one or two sentences on how the parts fit together)",
|
|
1673
|
+
anatomyParts: "anatomyParts ({ name, role }[] with name exactly as listed under Anatomy above and role one sentence on what the part does, not how it looks; skip a part you cannot describe without guessing)",
|
|
1674
|
+
properties: "properties ({ name, description }[] with name exactly as listed under Options, State axis or Properties above and description one sentence on what the property controls and when to change it)",
|
|
1675
|
+
states: "states ({ name, whenItApplies }[] with name exactly as listed under States above and whenItApplies one sentence on when the state applies)",
|
|
1676
|
+
// The vocabulary is read from KEYBOARD_KEYS rather than written out again:
|
|
1677
|
+
// `validateProseV2` drops a row whose key is not in that list, so a literal
|
|
1678
|
+
// here could ask for a key the validator then throws away, or leave out one
|
|
1679
|
+
// it would have accepted.
|
|
1680
|
+
keyboard: `keyboard ({ keys: string[], action }[] with each key one of ${KEYBOARD_KEYS.join(", ")}, and action one sentence; include only bindings this component really has, and leave the key out for a non-interactive component)`,
|
|
1681
|
+
pointer: "pointer (string[], 2 to 3 sentences on mouse and touch behaviour, including target size)",
|
|
1682
|
+
semantics: "semantics (string[], 3 to 4 sentences on roles, accessible names, and announcements, plus one on what the design file cannot encode)",
|
|
1683
|
+
content: "content (string[], 3 to 4 sentences on writing the text parts listed under Anatomy, on truncation, and on translation)",
|
|
1684
|
+
guidelines: "guidelines ({ do: { rule, reason }, dont: { rule, reason } }[], 3 pairs about using this component once chosen; each pair covers one topic drawn from its options, states or text parts, the dont mirrors the do, and no pair repeats a When to use or When not to use bullet; each rule one sentence, each reason one sentence)"
|
|
1685
|
+
};
|
|
1686
|
+
function axisDefault(spec, axis) {
|
|
1687
|
+
const prop = spec.props.find((p) => p.name === axis && p.kind === "variant");
|
|
1688
|
+
return typeof prop?.default === "string" ? prop.default : void 0;
|
|
1689
|
+
}
|
|
1690
|
+
var WHITESPACE3 = /\s/;
|
|
1691
|
+
function collapseLineBreaks(text) {
|
|
1692
|
+
let out = "";
|
|
1693
|
+
let i = 0;
|
|
1694
|
+
while (i < text.length) {
|
|
1695
|
+
if (!WHITESPACE3.test(text[i])) {
|
|
1696
|
+
out += text[i];
|
|
1697
|
+
i += 1;
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
let j = i;
|
|
1701
|
+
let hasBreak = false;
|
|
1702
|
+
while (j < text.length && WHITESPACE3.test(text[j])) {
|
|
1703
|
+
if (text[j] === "\n") hasBreak = true;
|
|
1704
|
+
j += 1;
|
|
1705
|
+
}
|
|
1706
|
+
out += hasBreak ? " " : text.slice(i, j);
|
|
1707
|
+
i = j;
|
|
1708
|
+
}
|
|
1709
|
+
return out;
|
|
1710
|
+
}
|
|
1711
|
+
function buildProsePrompt(spec, requested) {
|
|
1712
|
+
const lines = [];
|
|
1713
|
+
const display = displayComponentName(spec.name);
|
|
1714
|
+
lines.push(display === spec.name ? `Component: ${spec.name}` : `Component: ${display} (layer name: ${spec.name})`);
|
|
1715
|
+
const description = spec.description.trim();
|
|
1716
|
+
if (description) {
|
|
1717
|
+
lines.push("");
|
|
1718
|
+
lines.push("Designer's description (authoritative; build on it, never contradict or restate it):");
|
|
1719
|
+
lines.push(` ${collapseLineBreaks(description)}`);
|
|
1720
|
+
}
|
|
1721
|
+
if (spec.anatomy.length) {
|
|
1722
|
+
lines.push("");
|
|
1723
|
+
lines.push("Anatomy (depth-first; indent marks nesting):");
|
|
1724
|
+
for (const part of spec.anatomy) {
|
|
1725
|
+
const indent = " ".repeat(part.depth + 1);
|
|
1726
|
+
const shown = part.shownBy ? `; shown by ${part.shownBy}` : "";
|
|
1727
|
+
lines.push(`${indent}${part.name}: ${partKind(part)}${shown}`);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
const matrix = detectStateMatrix(spec.variants);
|
|
1731
|
+
const stateAxis = matrix?.axis ?? null;
|
|
1732
|
+
const optionAxes = spec.variants.filter((v) => v.prop !== stateAxis);
|
|
1733
|
+
if (optionAxes.length) {
|
|
1734
|
+
lines.push("");
|
|
1735
|
+
lines.push("Options:");
|
|
1736
|
+
for (const v of optionAxes) {
|
|
1737
|
+
const def = axisDefault(spec, v.prop);
|
|
1738
|
+
lines.push(` ${v.prop}: ${v.values.join(" \xB7 ")}${def ? ` (default ${def})` : ""}`);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
if (stateAxis) {
|
|
1742
|
+
const axis = spec.variants.find((v) => v.prop === stateAxis);
|
|
1743
|
+
if (axis) {
|
|
1744
|
+
lines.push("");
|
|
1745
|
+
lines.push("State axis:");
|
|
1746
|
+
lines.push(` ${axis.prop}: ${axis.values.join(" \xB7 ")}`);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
const otherProps = spec.props.filter((p) => p.kind !== "variant");
|
|
1750
|
+
if (otherProps.length) {
|
|
1751
|
+
lines.push("");
|
|
1752
|
+
lines.push("Properties:");
|
|
1753
|
+
for (const p of otherProps) {
|
|
1754
|
+
const def = p.default !== void 0 ? ` (default: ${p.default})` : "";
|
|
1755
|
+
lines.push(` ${p.name} [${p.kind}]${def}`);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
if (spec.states.length) {
|
|
1759
|
+
lines.push("");
|
|
1760
|
+
lines.push(`States: ${spec.states.join(", ")}`);
|
|
1761
|
+
}
|
|
1762
|
+
if (spec.tokens.length) {
|
|
1763
|
+
lines.push("");
|
|
1764
|
+
lines.push("Design tokens:");
|
|
1765
|
+
for (const t of spec.tokens) {
|
|
1766
|
+
const condition = formatConditions(t.conditions);
|
|
1767
|
+
const qualifier = condition === "\u2014" ? "" : ` [${condition}]`;
|
|
1768
|
+
lines.push(` ${t.part}.${t.property}${qualifier} \u2192 ${t.name}`);
|
|
1769
|
+
}
|
|
1770
|
+
if (spec.tokens.some((t) => Object.keys(t.conditions).length)) {
|
|
1771
|
+
lines.push(" Note: a bracketed condition like [State=Hover] means the token applies only to variants matching those axis values; unbracketed lines apply to all variants.");
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
if (spec.layout.length) {
|
|
1775
|
+
lines.push("");
|
|
1776
|
+
lines.push("Layout (default variant):");
|
|
1777
|
+
for (const l of spec.layout) lines.push(` ${l.part}: ${l.summary}`);
|
|
1778
|
+
}
|
|
1779
|
+
if (spec.related.length) {
|
|
1780
|
+
lines.push("");
|
|
1781
|
+
lines.push(`Related: ${spec.related.join(", ")}`);
|
|
1782
|
+
}
|
|
1783
|
+
const keys = requested ? PROSE_V2_KEYS.filter((k) => requested.has(k)) : [...PROSE_V2_KEYS];
|
|
1784
|
+
lines.push("");
|
|
1785
|
+
lines.push(
|
|
1786
|
+
PROMPT_RETURN_ANCHOR.trimStart() + keys.map((k) => PROSE_KEY_INSTRUCTIONS[k]).join("; ") + ". Leave out any key you cannot fill honestly. Markdown only as **bold** or `code` inside a sentence; no headings. No em dashes; keep sentences short. Return only the JSON object."
|
|
1787
|
+
);
|
|
1788
|
+
return lines.join("\n");
|
|
1789
|
+
}
|
|
1790
|
+
var BANNED_PHRASES = [
|
|
1791
|
+
"familiar",
|
|
1792
|
+
"essential",
|
|
1793
|
+
"intuitive",
|
|
1794
|
+
"seamless",
|
|
1795
|
+
"engage with the interface",
|
|
1796
|
+
"clear, easy to identify",
|
|
1797
|
+
"gives people a way to",
|
|
1798
|
+
"plays a key role"
|
|
1799
|
+
];
|
|
1800
|
+
var PROSE_SYSTEM_PROMPT = [
|
|
1801
|
+
"You write component documentation for a design system, in the voice of a senior designer explaining their own component to a colleague.",
|
|
1802
|
+
"",
|
|
1803
|
+
"Voice:",
|
|
1804
|
+
"- Second person, verb first, one idea per sentence. Every rule carries its reason.",
|
|
1805
|
+
"- Anchor guidance in concrete situations: forms, dialogs, toolbars, lists, filters.",
|
|
1806
|
+
'- Write for people, not "the user".',
|
|
1807
|
+
'- Describe, do not argue. No sentence explains why the component reads or feels a certain way, and nothing is "rather than" something else.',
|
|
1808
|
+
"",
|
|
1809
|
+
"Facts:",
|
|
1810
|
+
"- Name only what the prompt lists: this component's parts, properties, option values, states, and related components. Never invent an option, a part, a state, or a component.",
|
|
1811
|
+
"- The designer's description, when given, is authoritative. Build on it. Never contradict it and never restate it.",
|
|
1812
|
+
"- States are not variants. The variants guide covers option axes only; states go in the states list.",
|
|
1813
|
+
"- Say each fact once. When to use and When not to use are about choosing this component over another; the guidelines are about using it well once chosen.",
|
|
1814
|
+
`- Keyboard rows use only these keys: ${KEYBOARD_KEYS.join(", ")}.`,
|
|
1815
|
+
"",
|
|
1816
|
+
"Words to avoid:",
|
|
1817
|
+
`- Do not write ${BANNED_PHRASES.map((p) => `"${p}"`).join(", ")}.`,
|
|
1818
|
+
"- Do not restate a heading as a sentence.",
|
|
1819
|
+
"",
|
|
1820
|
+
"Format:",
|
|
1821
|
+
"- No em dashes and no spaced en dashes. Use a comma, a colon, or a full stop.",
|
|
1822
|
+
"- No headings inside strings. Markdown only as **bold** or `code` inside a sentence.",
|
|
1823
|
+
"- Return only the JSON object the message asks for, with only the keys it lists. Leave out a key you cannot fill honestly."
|
|
1824
|
+
].join("\n");
|
|
1825
|
+
function exemplarVariant(state) {
|
|
1826
|
+
const bind = (property, name) => ({
|
|
1827
|
+
property,
|
|
1828
|
+
id: `VariableID:${name}`,
|
|
1829
|
+
name,
|
|
1830
|
+
kind: "variable",
|
|
1831
|
+
remote: false,
|
|
1832
|
+
collectionId: "VariableCollectionId:exemplar"
|
|
1833
|
+
});
|
|
1834
|
+
const border = state === "Focused" ? "color/border/focus" : state === "Error" ? "color/border/error" : "color/field/border";
|
|
1835
|
+
return {
|
|
1836
|
+
id: `x:${state}`,
|
|
1837
|
+
name: `Size=Medium, Style=Filled, State=${state}`,
|
|
1838
|
+
type: "COMPONENT",
|
|
1839
|
+
visible: true,
|
|
1840
|
+
layout: { mode: "VERTICAL", itemSpacing: 4 },
|
|
1841
|
+
children: [
|
|
1842
|
+
{ id: `x:${state}:label`, name: "Label", type: "TEXT", visible: true, bindings: [bind("fills", "color/text/secondary")] },
|
|
1843
|
+
{
|
|
1844
|
+
id: `x:${state}:input`,
|
|
1845
|
+
name: "Input",
|
|
1846
|
+
type: "FRAME",
|
|
1847
|
+
visible: true,
|
|
1848
|
+
layout: { mode: "HORIZONTAL", paddingLeft: 12, paddingRight: 12, itemSpacing: 8 },
|
|
1849
|
+
bindings: [bind("fills", "color/field/bg"), bind("strokes", border)],
|
|
1850
|
+
children: [
|
|
1851
|
+
{
|
|
1852
|
+
id: `x:${state}:icon`,
|
|
1853
|
+
name: "Leading icon",
|
|
1854
|
+
type: "INSTANCE",
|
|
1855
|
+
visible: false,
|
|
1856
|
+
visibleProperty: "Show leading icon",
|
|
1857
|
+
mainComponent: { name: "Icon", key: "exemplar-icon" }
|
|
1858
|
+
},
|
|
1859
|
+
{ id: `x:${state}:placeholder`, name: "Placeholder", type: "TEXT", visible: true, bindings: [bind("fills", "color/text/placeholder")] }
|
|
1860
|
+
]
|
|
1861
|
+
},
|
|
1862
|
+
{ id: `x:${state}:helper`, name: "Helper text", type: "TEXT", visible: true, bindings: [bind("fills", "color/text/secondary")] }
|
|
1863
|
+
]
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
function exemplarNode() {
|
|
1867
|
+
return {
|
|
1868
|
+
id: "x:0",
|
|
1869
|
+
name: "Text field",
|
|
1870
|
+
type: "COMPONENT_SET",
|
|
1871
|
+
visible: true,
|
|
1872
|
+
key: "exemplar-text-field",
|
|
1873
|
+
description: "A single-line field where people type short, free-form text.",
|
|
1874
|
+
propertyDefinitions: {
|
|
1875
|
+
Size: { type: "VARIANT", defaultValue: "Medium", variantOptions: ["Small", "Medium"] },
|
|
1876
|
+
Style: { type: "VARIANT", defaultValue: "Filled", variantOptions: ["Filled", "Outlined"] },
|
|
1877
|
+
State: { type: "VARIANT", defaultValue: "Enabled", variantOptions: ["Enabled", "Hover", "Focused", "Error", "Disabled"] },
|
|
1878
|
+
"Show leading icon": { type: "BOOLEAN", defaultValue: false },
|
|
1879
|
+
"Show helper text": { type: "BOOLEAN", defaultValue: true },
|
|
1880
|
+
Label: { type: "TEXT", defaultValue: "Label" },
|
|
1881
|
+
Placeholder: { type: "TEXT", defaultValue: "Placeholder" }
|
|
1882
|
+
},
|
|
1883
|
+
children: ["Enabled", "Hover", "Focused", "Error", "Disabled"].map(exemplarVariant)
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
function exemplarSpec() {
|
|
1887
|
+
return extract(exemplarNode(), { figmaFile: "exemplar" });
|
|
1888
|
+
}
|
|
1889
|
+
var EXEMPLAR_PROMPT = buildProsePrompt(exemplarSpec());
|
|
776
1890
|
|
|
777
1891
|
// ../extractor/src/prose/foundationPrompt.ts
|
|
778
1892
|
var FOUNDATION_SYSTEM_PROMPT = [
|
|
@@ -795,10 +1909,160 @@ var FOUNDATION_SYSTEM_PROMPT = [
|
|
|
795
1909
|
"- Never use em dashes or en dashes. Use a period, comma, colon, or parentheses.",
|
|
796
1910
|
'- Do not restate the heading as a sentence ("Surface colours are colours for surfaces").',
|
|
797
1911
|
"",
|
|
798
|
-
"
|
|
1912
|
+
'Each "<collection key>|overview" entry describes that collection in one paragraph: what it holds,',
|
|
1913
|
+
"how its modes differ, and which collections it draws from, exactly as the names, modes and alias",
|
|
1914
|
+
"counts show. Under 400 characters, no markdown, no invented usage. Leave it out if the names",
|
|
1915
|
+
"support nothing.",
|
|
1916
|
+
"",
|
|
1917
|
+
"Return ONLY a JSON object: each group key mapped to its description, and each",
|
|
1918
|
+
"collection overview key mapped to its overview.",
|
|
799
1919
|
"No prose outside the JSON, no code fence."
|
|
800
1920
|
].join("\n");
|
|
801
1921
|
|
|
1922
|
+
// ../extractor/src/yaml.ts
|
|
1923
|
+
var RESERVED_WORD = /^(y|n|yes|no|true|false|on|off|null|~)$/i;
|
|
1924
|
+
var NUMERIC = /^[-+]?(\d[\d_]*(\.\d*)?([eE][-+]?\d+)?|\.\d+|0[xob][0-9a-fA-F_]+)$/;
|
|
1925
|
+
var SPECIAL_FLOAT = /^[-+]?\.(inf|nan)$/i;
|
|
1926
|
+
var LEADING_INDICATOR = /^[-?:,[\]{}#&*!|>'"%@`]/;
|
|
1927
|
+
var CONTROL_CHAR = /[\x00-\x1f]/;
|
|
1928
|
+
function needsQuote(s) {
|
|
1929
|
+
if (s === "") return true;
|
|
1930
|
+
if (LEADING_INDICATOR.test(s)) return true;
|
|
1931
|
+
if (/^\s|\s$/.test(s)) return true;
|
|
1932
|
+
if (s.includes(": ") || s.endsWith(":")) return true;
|
|
1933
|
+
if (s.includes(" #")) return true;
|
|
1934
|
+
if (RESERVED_WORD.test(s)) return true;
|
|
1935
|
+
if (SPECIAL_FLOAT.test(s)) return true;
|
|
1936
|
+
if (NUMERIC.test(s)) return true;
|
|
1937
|
+
if (CONTROL_CHAR.test(s)) return true;
|
|
1938
|
+
return false;
|
|
1939
|
+
}
|
|
1940
|
+
function unicodeEscape(ch) {
|
|
1941
|
+
return "\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0");
|
|
1942
|
+
}
|
|
1943
|
+
function doubleQuote(s) {
|
|
1944
|
+
const body = s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/[\x00-\x1f]/g, (ch) => unicodeEscape(ch));
|
|
1945
|
+
return `"${body}"`;
|
|
1946
|
+
}
|
|
1947
|
+
function inlineScalar(s) {
|
|
1948
|
+
return needsQuote(s) ? doubleQuote(s) : s;
|
|
1949
|
+
}
|
|
1950
|
+
function isInline(value) {
|
|
1951
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") return true;
|
|
1952
|
+
if (typeof value === "string") return value.includes("\r") || !value.includes("\n");
|
|
1953
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
1954
|
+
return Object.values(value).filter((v) => v !== void 0).length === 0;
|
|
1955
|
+
}
|
|
1956
|
+
function inlineText(value) {
|
|
1957
|
+
if (value === null) return "null";
|
|
1958
|
+
if (typeof value === "boolean") return String(value);
|
|
1959
|
+
if (typeof value === "number") {
|
|
1960
|
+
if (!Number.isFinite(value)) throw new Error(`yaml: cannot emit ${String(value)}`);
|
|
1961
|
+
return String(value);
|
|
1962
|
+
}
|
|
1963
|
+
if (typeof value === "string") return inlineScalar(value);
|
|
1964
|
+
if (Array.isArray(value)) return "[]";
|
|
1965
|
+
return "{}";
|
|
1966
|
+
}
|
|
1967
|
+
var FLOW_MAX = 72;
|
|
1968
|
+
function flowEligible(value, depth = 0) {
|
|
1969
|
+
if (isInline(value)) return true;
|
|
1970
|
+
if (typeof value === "string") return false;
|
|
1971
|
+
if (depth >= 2) return false;
|
|
1972
|
+
if (Array.isArray(value)) return value.every((m) => flowEligible(m, depth + 1));
|
|
1973
|
+
if (value === null || typeof value !== "object") {
|
|
1974
|
+
throw new Error(`yaml: flowEligible() called on a non-collection value: ${JSON.stringify(value)}`);
|
|
1975
|
+
}
|
|
1976
|
+
const members = Object.values(value).filter((v) => v !== void 0);
|
|
1977
|
+
if (members.length === 0) return true;
|
|
1978
|
+
return members.every((m) => flowEligible(m, depth + 1));
|
|
1979
|
+
}
|
|
1980
|
+
var FLOW_UNSAFE = /[,{}[\]]/;
|
|
1981
|
+
function flowScalar(value) {
|
|
1982
|
+
if (typeof value !== "string") return inlineText(value);
|
|
1983
|
+
return needsQuote(value) || FLOW_UNSAFE.test(value) ? doubleQuote(value) : value;
|
|
1984
|
+
}
|
|
1985
|
+
function flowText(value) {
|
|
1986
|
+
if (isInline(value)) return flowScalar(value);
|
|
1987
|
+
if (Array.isArray(value)) {
|
|
1988
|
+
return `[${value.map((v) => flowText(v)).join(", ")}]`;
|
|
1989
|
+
}
|
|
1990
|
+
if (value === null || typeof value !== "object") {
|
|
1991
|
+
throw new Error(`yaml: flowText() called on a non-collection value: ${JSON.stringify(value)}`);
|
|
1992
|
+
}
|
|
1993
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0);
|
|
1994
|
+
return `{ ${entries.map(([k, v]) => `${flowScalar(k)}: ${flowText(v)}`).join(", ")} }`;
|
|
1995
|
+
}
|
|
1996
|
+
function asFlow(value) {
|
|
1997
|
+
if (isInline(value)) return null;
|
|
1998
|
+
if (!flowEligible(value)) return null;
|
|
1999
|
+
const text = flowText(value);
|
|
2000
|
+
return text.length <= FLOW_MAX ? text : null;
|
|
2001
|
+
}
|
|
2002
|
+
function blockScalarLines(s, indent) {
|
|
2003
|
+
const pad = " ".repeat(indent);
|
|
2004
|
+
let trailingNewlines = 0;
|
|
2005
|
+
while (trailingNewlines < s.length && s[s.length - 1 - trailingNewlines] === "\n") {
|
|
2006
|
+
trailingNewlines++;
|
|
2007
|
+
}
|
|
2008
|
+
const core = trailingNewlines === 0 ? s : s.slice(0, s.length - trailingNewlines);
|
|
2009
|
+
const indicator = trailingNewlines === 0 ? "|-" : "|+";
|
|
2010
|
+
const extraBlankLines = trailingNewlines >= 1 ? trailingNewlines - 1 : 0;
|
|
2011
|
+
const contentLines = core.split("\n").concat(Array(extraBlankLines).fill(""));
|
|
2012
|
+
return [indicator, ...contentLines.map((l) => l === "" ? "" : pad + l)];
|
|
2013
|
+
}
|
|
2014
|
+
function emitMapEntry(key, value, indent) {
|
|
2015
|
+
const pad = " ".repeat(indent);
|
|
2016
|
+
const k = inlineScalar(key);
|
|
2017
|
+
if (isInline(value)) {
|
|
2018
|
+
return [`${pad}${k}: ${inlineText(value)}`];
|
|
2019
|
+
}
|
|
2020
|
+
const flow = asFlow(value);
|
|
2021
|
+
if (flow !== null) {
|
|
2022
|
+
return [`${pad}${k}: ${flow}`];
|
|
2023
|
+
}
|
|
2024
|
+
if (typeof value === "string") {
|
|
2025
|
+
const [indicator, ...lines] = blockScalarLines(value, indent + 2);
|
|
2026
|
+
return [`${pad}${k}: ${indicator}`, ...lines];
|
|
2027
|
+
}
|
|
2028
|
+
return [`${pad}${k}:`, ...blockLines(value, indent + 2)];
|
|
2029
|
+
}
|
|
2030
|
+
function emitListItem(value, indent) {
|
|
2031
|
+
const pad = " ".repeat(indent);
|
|
2032
|
+
if (isInline(value)) {
|
|
2033
|
+
return [`${pad}- ${inlineText(value)}`];
|
|
2034
|
+
}
|
|
2035
|
+
const flow = asFlow(value);
|
|
2036
|
+
if (flow !== null) {
|
|
2037
|
+
return [`${pad}- ${flow}`];
|
|
2038
|
+
}
|
|
2039
|
+
if (typeof value === "string") {
|
|
2040
|
+
const [indicator, ...lines2] = blockScalarLines(value, indent + 2);
|
|
2041
|
+
return [`${pad}- ${indicator}`, ...lines2];
|
|
2042
|
+
}
|
|
2043
|
+
const lines = blockLines(value, indent + 2);
|
|
2044
|
+
const first = lines[0].slice(indent + 2);
|
|
2045
|
+
return [`${pad}- ${first}`, ...lines.slice(1)];
|
|
2046
|
+
}
|
|
2047
|
+
function blockLines(value, indent) {
|
|
2048
|
+
if (Array.isArray(value)) {
|
|
2049
|
+
return value.flatMap((item) => emitListItem(item, indent));
|
|
2050
|
+
}
|
|
2051
|
+
if (value === null || typeof value !== "object") {
|
|
2052
|
+
throw new Error(`yaml: blockLines() called on a non-collection value: ${JSON.stringify(value)}`);
|
|
2053
|
+
}
|
|
2054
|
+
const entries = Object.entries(value).filter((e) => e[1] !== void 0);
|
|
2055
|
+
return entries.flatMap(([k, v]) => emitMapEntry(k, v, indent));
|
|
2056
|
+
}
|
|
2057
|
+
function toYaml(value) {
|
|
2058
|
+
if (isInline(value)) return inlineText(value) + "\n";
|
|
2059
|
+
if (typeof value === "string") {
|
|
2060
|
+
const [indicator, ...lines] = blockScalarLines(value, 2);
|
|
2061
|
+
return [indicator, ...lines].join("\n") + "\n";
|
|
2062
|
+
}
|
|
2063
|
+
return blockLines(value, 0).join("\n") + "\n";
|
|
2064
|
+
}
|
|
2065
|
+
|
|
802
2066
|
// ../extractor/src/v5/value.ts
|
|
803
2067
|
var SUPPORTED_UNITS = ["px", "rem", "em", "%", "deg", "ms", "s"];
|
|
804
2068
|
var SUPPORTED_TOKEN_TYPES = [
|
|
@@ -814,6 +2078,26 @@ var SUPPORTED_TOKEN_TYPES = [
|
|
|
814
2078
|
var SUPPORTED_VALUE_KINDS = ["literal", "alias", "missing"];
|
|
815
2079
|
var SUPPORTED_DURATION_UNITS = ["ms", "s"];
|
|
816
2080
|
|
|
2081
|
+
// ../extractor/src/v5/units.ts
|
|
2082
|
+
var UNIT_BY_SCOPE = {
|
|
2083
|
+
WIDTH_HEIGHT: "px",
|
|
2084
|
+
CORNER_RADIUS: "px",
|
|
2085
|
+
GAP: "px",
|
|
2086
|
+
FONT_SIZE: "px",
|
|
2087
|
+
STROKE_FLOAT: "px",
|
|
2088
|
+
PARAGRAPH_SPACING: "px",
|
|
2089
|
+
PARAGRAPH_INDENT: "px",
|
|
2090
|
+
EFFECT_FLOAT: "px",
|
|
2091
|
+
FONT_WEIGHT: "number",
|
|
2092
|
+
OPACITY: "number"
|
|
2093
|
+
};
|
|
2094
|
+
function scopesStateUnit(scopes) {
|
|
2095
|
+
return (scopes ?? []).some((s) => UNIT_BY_SCOPE[s] !== void 0);
|
|
2096
|
+
}
|
|
2097
|
+
function scopesStateNumber(scopes) {
|
|
2098
|
+
return (scopes ?? []).some((s) => UNIT_BY_SCOPE[s] === "number");
|
|
2099
|
+
}
|
|
2100
|
+
|
|
817
2101
|
// ../extractor/src/v5/canonical.ts
|
|
818
2102
|
var import_js_sha2562 = __toESM(require_sha256(), 1);
|
|
819
2103
|
var SCHEMA_VERSION = "5.1.0";
|
|
@@ -825,6 +2109,14 @@ function canonicalJson(value) {
|
|
|
825
2109
|
}
|
|
826
2110
|
return JSON.stringify(value);
|
|
827
2111
|
}
|
|
2112
|
+
function semanticContentHash(payload) {
|
|
2113
|
+
return `sha256:${(0, import_js_sha2562.sha256)(canonicalJson({
|
|
2114
|
+
completeness: payload.completeness,
|
|
2115
|
+
collections: payload.collections,
|
|
2116
|
+
tokens: payload.tokens,
|
|
2117
|
+
styles: payload.styles
|
|
2118
|
+
}))}`;
|
|
2119
|
+
}
|
|
828
2120
|
|
|
829
2121
|
// ../extractor/src/v5/validate.ts
|
|
830
2122
|
var ROOT = "<artifact>";
|
|
@@ -1277,52 +2569,417 @@ function validateRootSections(artifact, out) {
|
|
|
1277
2569
|
artifact.styles.typography.forEach((style, index) => validateTypographyStyle(style, index, out));
|
|
1278
2570
|
artifact.styles.effects.forEach((style, index) => validateEffectStyle(style, index, out));
|
|
1279
2571
|
}
|
|
1280
|
-
if (!Array.isArray(artifact.diagnostics)) {
|
|
1281
|
-
out.push(shape(ROOT, "`diagnostics` must be an array."));
|
|
2572
|
+
if (!Array.isArray(artifact.diagnostics)) {
|
|
2573
|
+
out.push(shape(ROOT, "`diagnostics` must be an array."));
|
|
2574
|
+
}
|
|
2575
|
+
if (!isRecord(artifact.statistics)) {
|
|
2576
|
+
out.push(shape(ROOT, "`statistics` must be an object."));
|
|
2577
|
+
}
|
|
2578
|
+
if (artifact.guidelines !== void 0) {
|
|
2579
|
+
const guidelines = artifact.guidelines;
|
|
2580
|
+
if (!isRecord(guidelines) || guidelines.origin !== "generated" || !isRecord(guidelines.group_descriptions)) {
|
|
2581
|
+
out.push(shape(
|
|
2582
|
+
ROOT,
|
|
2583
|
+
'`guidelines` must state origin "generated" and a group_descriptions object.'
|
|
2584
|
+
));
|
|
2585
|
+
} else {
|
|
2586
|
+
for (const [collectionName, folders] of Object.entries(guidelines.group_descriptions)) {
|
|
2587
|
+
if (!isRecord(folders) || Object.values(folders).some((description) => typeof description !== "string")) {
|
|
2588
|
+
out.push(shape(
|
|
2589
|
+
ROOT,
|
|
2590
|
+
`guidelines.group_descriptions[${JSON.stringify(collectionName)}] must map folder names to strings.`
|
|
2591
|
+
));
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
function validateLevel1(artifact) {
|
|
2598
|
+
try {
|
|
2599
|
+
const out = [];
|
|
2600
|
+
if (!isRecord(artifact)) {
|
|
2601
|
+
out.push(shape(ROOT, "The artifact root must be an object."));
|
|
2602
|
+
return out;
|
|
2603
|
+
}
|
|
2604
|
+
validateRootSections(artifact, out);
|
|
2605
|
+
if (!Array.isArray(artifact.tokens)) {
|
|
2606
|
+
out.push(shape(ROOT, "`tokens` must be an array."));
|
|
2607
|
+
} else {
|
|
2608
|
+
artifact.tokens.forEach((token, index) => validateToken(token, index, out));
|
|
2609
|
+
}
|
|
2610
|
+
return out;
|
|
2611
|
+
} catch (err) {
|
|
2612
|
+
const message = err instanceof Error && err.message ? `The artifact could not be read; accessing its properties threw: ${err.message}` : "The artifact could not be read; accessing its properties threw.";
|
|
2613
|
+
return [diagnostic("INCONSISTENT_VALUE_SHAPE", {
|
|
2614
|
+
entity_id: "artifact",
|
|
2615
|
+
message
|
|
2616
|
+
})];
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
// ../extractor/src/v5/fonts.ts
|
|
2621
|
+
function fontRequirements(artifact) {
|
|
2622
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
2623
|
+
for (const style of artifact.styles.typography) {
|
|
2624
|
+
const family = style.properties.font_family.resolved;
|
|
2625
|
+
if (family === null || family.type !== "font_family") continue;
|
|
2626
|
+
const bucket = byFamily.get(family.value) ?? { weights: /* @__PURE__ */ new Set(), used: /* @__PURE__ */ new Set() };
|
|
2627
|
+
const weight = style.properties.font_weight.resolved;
|
|
2628
|
+
if (weight !== null && weight.type === "number") bucket.weights.add(weight.value);
|
|
2629
|
+
bucket.used.add(style.name);
|
|
2630
|
+
byFamily.set(family.value, bucket);
|
|
2631
|
+
}
|
|
2632
|
+
return [...byFamily.entries()].sort((a, b) => compareCodeUnits(a[0], b[0])).map(([family, bucket]) => ({
|
|
2633
|
+
family,
|
|
2634
|
+
weights: [...bucket.weights].sort((a, b) => a - b),
|
|
2635
|
+
used_by: [...bucket.used].sort(compareCodeUnits)
|
|
2636
|
+
}));
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// ../extractor/src/v5/aiContext.ts
|
|
2640
|
+
function readableLabels(items, idOf, nameOf) {
|
|
2641
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2642
|
+
for (const item of items) {
|
|
2643
|
+
const name = nameOf(item);
|
|
2644
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
2645
|
+
}
|
|
2646
|
+
const preferred = items.map((item) => {
|
|
2647
|
+
const id = idOf(item);
|
|
2648
|
+
const name = nameOf(item);
|
|
2649
|
+
return [id, counts.get(name) === 1 ? name : `${name} [${id}]`];
|
|
2650
|
+
});
|
|
2651
|
+
if (new Set(preferred.map(([, label]) => label)).size === preferred.length) {
|
|
2652
|
+
return new Map(preferred);
|
|
2653
|
+
}
|
|
2654
|
+
return new Map(items.map((item) => [
|
|
2655
|
+
idOf(item),
|
|
2656
|
+
`[${idOf(item)}] ${nameOf(item)}`
|
|
2657
|
+
]));
|
|
2658
|
+
}
|
|
2659
|
+
function buildIndex(artifact) {
|
|
2660
|
+
const collectionById = new Map(artifact.collections.map((collection) => [collection.id, collection]));
|
|
2661
|
+
const tokenById = new Map(artifact.tokens.map((token) => [token.id, token]));
|
|
2662
|
+
const collectionLabelById = readableLabels(
|
|
2663
|
+
artifact.collections,
|
|
2664
|
+
({ id }) => id,
|
|
2665
|
+
({ name }) => name
|
|
2666
|
+
);
|
|
2667
|
+
const ambiguousEntityIds = /* @__PURE__ */ new Set();
|
|
2668
|
+
for (const collection of artifact.collections) {
|
|
2669
|
+
if (collectionLabelById.get(collection.id) !== collection.name) {
|
|
2670
|
+
ambiguousEntityIds.add(collection.id);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
const tokenLabelById = /* @__PURE__ */ new Map();
|
|
2674
|
+
for (const collection of artifact.collections) {
|
|
2675
|
+
const tokens = artifact.tokens.filter((token) => token.collection_id === collection.id);
|
|
2676
|
+
const localLabels = readableLabels(tokens, ({ id }) => id, ({ name }) => name);
|
|
2677
|
+
const collectionLabel = collectionLabelById.get(collection.id) ?? collection.name;
|
|
2678
|
+
for (const token of tokens) {
|
|
2679
|
+
const tokenLabel = localLabels.get(token.id) ?? token.name;
|
|
2680
|
+
if (tokenLabel !== token.name) ambiguousEntityIds.add(token.id);
|
|
2681
|
+
tokenLabelById.set(token.id, `${collectionLabel}/${tokenLabel}`);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
const entityLabelById = new Map(tokenLabelById);
|
|
2685
|
+
const addStyleLabels = (kind, styles) => {
|
|
2686
|
+
const labels = readableLabels(styles, ({ id }) => id, ({ name }) => name);
|
|
2687
|
+
for (const style of styles) {
|
|
2688
|
+
const label = labels.get(style.id) ?? style.name;
|
|
2689
|
+
if (label !== style.name) ambiguousEntityIds.add(style.id);
|
|
2690
|
+
entityLabelById.set(style.id, `${kind}/${label}`);
|
|
2691
|
+
}
|
|
2692
|
+
};
|
|
2693
|
+
addStyleLabels("Typography", artifact.styles.typography);
|
|
2694
|
+
addStyleLabels("Effects", artifact.styles.effects);
|
|
2695
|
+
const modeLabelByCollectionAndId = /* @__PURE__ */ new Map();
|
|
2696
|
+
for (const collection of artifact.collections) {
|
|
2697
|
+
modeLabelByCollectionAndId.set(collection.id, readableLabels(
|
|
2698
|
+
collection.modes,
|
|
2699
|
+
({ id }) => id,
|
|
2700
|
+
({ name }) => name
|
|
2701
|
+
));
|
|
2702
|
+
}
|
|
2703
|
+
return {
|
|
2704
|
+
collectionById,
|
|
2705
|
+
tokenById,
|
|
2706
|
+
collectionLabelById,
|
|
2707
|
+
tokenLabelById,
|
|
2708
|
+
entityLabelById,
|
|
2709
|
+
modeLabelByCollectionAndId,
|
|
2710
|
+
ambiguousEntityIds
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
function compactTypedValue(value, expectedType) {
|
|
2714
|
+
let compact;
|
|
2715
|
+
switch (value.type) {
|
|
2716
|
+
case "color":
|
|
2717
|
+
compact = value.alpha === 1 && value.channels === void 0 ? value.hex : {
|
|
2718
|
+
hex: value.hex,
|
|
2719
|
+
...value.alpha !== 1 ? { alpha: value.alpha } : {},
|
|
2720
|
+
...value.channels ? { channels: [...value.channels] } : {}
|
|
2721
|
+
};
|
|
2722
|
+
break;
|
|
2723
|
+
case "dimension":
|
|
2724
|
+
case "duration":
|
|
2725
|
+
compact = { number: value.number, unit: value.unit };
|
|
2726
|
+
break;
|
|
2727
|
+
case "cubic_bezier":
|
|
2728
|
+
compact = [...value.value];
|
|
2729
|
+
break;
|
|
2730
|
+
case "number":
|
|
2731
|
+
case "string":
|
|
2732
|
+
case "boolean":
|
|
2733
|
+
case "font_family":
|
|
2734
|
+
compact = value.value;
|
|
2735
|
+
break;
|
|
2736
|
+
default: {
|
|
2737
|
+
const exhaustive = value;
|
|
2738
|
+
return exhaustive;
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
return expectedType !== void 0 && value.type !== expectedType ? { type: value.type, value: compact } : compact;
|
|
2742
|
+
}
|
|
2743
|
+
function referenceLabel(reference, index) {
|
|
2744
|
+
if (reference.target_id !== null) {
|
|
2745
|
+
const local = index.tokenLabelById.get(reference.target_id);
|
|
2746
|
+
if (local !== void 0) return local;
|
|
2747
|
+
}
|
|
2748
|
+
const path = reference.target_path.length > 0 ? reference.target_path.join("/") : reference.target_id ?? "<unknown target>";
|
|
2749
|
+
return reference.source_library_name ? `${reference.source_library_name}/${path}` : path;
|
|
2750
|
+
}
|
|
2751
|
+
function stepLabel(step, index) {
|
|
2752
|
+
const token = index.tokenById.get(step.token_id);
|
|
2753
|
+
if (token === void 0) return `${step.token_id} @ ${step.mode_id}`;
|
|
2754
|
+
const tokenLabel = index.tokenLabelById.get(token.id) ?? token.name;
|
|
2755
|
+
const modeLabel = index.modeLabelByCollectionAndId.get(token.collection_id)?.get(step.mode_id) ?? step.mode_id;
|
|
2756
|
+
return `${tokenLabel} @ ${modeLabel}`;
|
|
2757
|
+
}
|
|
2758
|
+
function compactCanonicalValue(value, expectedType, index) {
|
|
2759
|
+
if (value.kind === "literal") return compactTypedValue(value.value, expectedType);
|
|
2760
|
+
if (value.kind === "missing") return { missing: value.reason };
|
|
2761
|
+
const chain = value.resolved.chain.map((step) => stepLabel(step, index));
|
|
2762
|
+
const firstStep = value.resolved.chain[0];
|
|
2763
|
+
const alias = firstStep !== void 0 && firstStep.token_id === value.reference.target_id ? stepLabel(firstStep, index) : referenceLabel(value.reference, index);
|
|
2764
|
+
return {
|
|
2765
|
+
alias,
|
|
2766
|
+
...value.resolved.status === "resolved" ? { resolved: compactTypedValue(value.resolved.value, expectedType) } : { unresolved: value.resolved.reason },
|
|
2767
|
+
...chain.length > 1 || value.resolved.status === "unresolved" && chain.length > 0 ? { chain } : {}
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2770
|
+
function compactLifecycle(lifecycle, index) {
|
|
2771
|
+
if (lifecycle === void 0) return void 0;
|
|
2772
|
+
const replacement = lifecycle.replacement_id === null ? void 0 : index.entityLabelById.get(lifecycle.replacement_id) ?? lifecycle.replacement_id;
|
|
2773
|
+
return { status: lifecycle.status, ...replacement ? { replacement } : {} };
|
|
2774
|
+
}
|
|
2775
|
+
function compactToken(token, collection, index, includeSourceIds) {
|
|
2776
|
+
const modeLabels2 = index.modeLabelByCollectionAndId.get(collection.id) ?? /* @__PURE__ */ new Map();
|
|
2777
|
+
const values = {};
|
|
2778
|
+
for (const [modeId, value] of Object.entries(token.values)) {
|
|
2779
|
+
values[modeLabels2.get(modeId) ?? modeId] = compactCanonicalValue(value, token.type, index);
|
|
1282
2780
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
2781
|
+
const lifecycle = compactLifecycle(token.lifecycle, index);
|
|
2782
|
+
return {
|
|
2783
|
+
name: token.name,
|
|
2784
|
+
...includeSourceIds || index.ambiguousEntityIds.has(token.id) ? { source_id: token.id } : {},
|
|
2785
|
+
...token.suggested_code_name ? { suggested_code_name: token.suggested_code_name } : {},
|
|
2786
|
+
type: token.type,
|
|
2787
|
+
...token.description.length > 0 ? { description: token.description } : {},
|
|
2788
|
+
...token.scopes.length > 0 ? { scopes: token.scopes } : {},
|
|
2789
|
+
...token.code_syntax ? { code_syntax: token.code_syntax } : {},
|
|
2790
|
+
...token.publication ? { publication: token.publication } : {},
|
|
2791
|
+
...lifecycle ? { lifecycle } : {},
|
|
2792
|
+
values
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
function compactStyleProperty(property, index) {
|
|
2796
|
+
if (property.source.kind === "literal") {
|
|
2797
|
+
return property.resolved === null ? { missing: "source_unavailable" } : { type: property.resolved.type, value: compactTypedValue(property.resolved) };
|
|
1285
2798
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
2799
|
+
const path = property.source.target_path.join("/");
|
|
2800
|
+
const target = property.source.target_id === null ? path || "<unknown target>" : (index.tokenLabelById.get(property.source.target_id) ?? path) || property.source.target_id;
|
|
2801
|
+
return {
|
|
2802
|
+
alias: target,
|
|
2803
|
+
...property.resolved === null ? { unresolved: "target_not_found" } : { resolved: { type: property.resolved.type, value: compactTypedValue(property.resolved) } }
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
function styleIdentity(style, index, includeSourceIds) {
|
|
2807
|
+
const lifecycle = compactLifecycle(style.lifecycle, index);
|
|
2808
|
+
return {
|
|
2809
|
+
name: style.name,
|
|
2810
|
+
...includeSourceIds || index.ambiguousEntityIds.has(style.id) ? { source_id: style.id } : {},
|
|
2811
|
+
...style.suggested_code_name ? { suggested_code_name: style.suggested_code_name } : {},
|
|
2812
|
+
...style.publication ? { publication: {
|
|
2813
|
+
published: style.publication.published,
|
|
2814
|
+
hidden_from_publishing: style.publication.hidden_from_publishing
|
|
2815
|
+
} } : {},
|
|
2816
|
+
...style.source ? { source: {
|
|
2817
|
+
remote: style.source.remote,
|
|
2818
|
+
...style.source.library_name === null ? {} : { library_name: style.source.library_name }
|
|
2819
|
+
} } : {},
|
|
2820
|
+
...lifecycle ? { lifecycle } : {}
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
function compactTypography(style, index, includeSourceIds) {
|
|
2824
|
+
return {
|
|
2825
|
+
...styleIdentity(style, index, includeSourceIds),
|
|
2826
|
+
...style.description.length > 0 ? { description: style.description } : {},
|
|
2827
|
+
properties: {
|
|
2828
|
+
font_family: compactStyleProperty(style.properties.font_family, index),
|
|
2829
|
+
font_weight: compactStyleProperty(style.properties.font_weight, index),
|
|
2830
|
+
font_size: compactStyleProperty(style.properties.font_size, index),
|
|
2831
|
+
line_height: compactStyleProperty(style.properties.line_height, index),
|
|
2832
|
+
letter_spacing: compactStyleProperty(style.properties.letter_spacing, index),
|
|
2833
|
+
paragraph_spacing: compactStyleProperty(style.properties.paragraph_spacing, index),
|
|
2834
|
+
paragraph_indent: compactStyleProperty(style.properties.paragraph_indent, index),
|
|
2835
|
+
text_case: style.properties.text_case,
|
|
2836
|
+
text_decoration: style.properties.text_decoration
|
|
1302
2837
|
}
|
|
1303
|
-
}
|
|
2838
|
+
};
|
|
1304
2839
|
}
|
|
1305
|
-
function
|
|
1306
|
-
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
2840
|
+
function compactEffect(style, index, includeSourceIds) {
|
|
2841
|
+
const modeMatches = [...index.collectionById.values()].flatMap((collection) => {
|
|
2842
|
+
const label = index.modeLabelByCollectionAndId.get(collection.id)?.get(style.mode_id ?? "");
|
|
2843
|
+
return label === void 0 ? [] : [label];
|
|
2844
|
+
});
|
|
2845
|
+
const mode = style.mode_id === null ? null : modeMatches.length === 1 ? modeMatches[0] : style.mode_id;
|
|
2846
|
+
return {
|
|
2847
|
+
...styleIdentity(style, index, includeSourceIds),
|
|
2848
|
+
mode,
|
|
2849
|
+
effects: style.effects.map((effect) => ({
|
|
2850
|
+
type: effect.type,
|
|
2851
|
+
visible: effect.visible,
|
|
2852
|
+
...effect.blend_mode !== void 0 ? { blend_mode: effect.blend_mode } : {},
|
|
2853
|
+
...effect.color ? { color: compactTypedValue(effect.color, "color") } : {},
|
|
2854
|
+
...effect.offset_x ? { offset_x: compactTypedValue(effect.offset_x, "dimension") } : {},
|
|
2855
|
+
...effect.offset_y ? { offset_y: compactTypedValue(effect.offset_y, "dimension") } : {},
|
|
2856
|
+
...effect.blur ? { blur: compactTypedValue(effect.blur, "dimension") } : {},
|
|
2857
|
+
...effect.spread ? { spread: compactTypedValue(effect.spread, "dimension") } : {},
|
|
2858
|
+
...effect.show_behind_node !== void 0 ? { show_behind_node: effect.show_behind_node } : {}
|
|
2859
|
+
})),
|
|
2860
|
+
...style.bindings && style.bindings.length > 0 ? { bindings: Object.fromEntries(style.bindings.map((binding) => [
|
|
2861
|
+
binding.property,
|
|
2862
|
+
index.tokenLabelById.get(binding.token_id) ?? binding.token_id
|
|
2863
|
+
])) } : {}
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
function kebab(code3) {
|
|
2867
|
+
return code3.toLowerCase().replace(/_/g, "-");
|
|
2868
|
+
}
|
|
2869
|
+
function valueText(value) {
|
|
2870
|
+
if (value === null || value === void 0) return "unknown";
|
|
2871
|
+
if (typeof value === "object") {
|
|
2872
|
+
const record = value;
|
|
2873
|
+
if ("value" in record) return String(record.value);
|
|
2874
|
+
if ("number" in record) {
|
|
2875
|
+
const unit = typeof record.unit === "string" ? record.unit : "";
|
|
2876
|
+
return `${String(record.number)}${unit}`;
|
|
1311
2877
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
artifact.tokens.forEach((token, index) => validateToken(token, index, out));
|
|
2878
|
+
if (typeof record.hex === "string") {
|
|
2879
|
+
const alpha = typeof record.alpha === "number" ? ` alpha ${record.alpha}` : "";
|
|
2880
|
+
const channels = Array.isArray(record.channels) ? ` channels ${JSON.stringify(record.channels)}` : "";
|
|
2881
|
+
return `${record.hex}${alpha}${channels}`;
|
|
1317
2882
|
}
|
|
1318
|
-
return out;
|
|
1319
|
-
} catch (err) {
|
|
1320
|
-
const message = err instanceof Error && err.message ? `The artifact could not be read; accessing its properties threw: ${err.message}` : "The artifact could not be read; accessing its properties threw.";
|
|
1321
|
-
return [diagnostic("INCONSISTENT_VALUE_SHAPE", {
|
|
1322
|
-
entity_id: "artifact",
|
|
1323
|
-
message
|
|
1324
|
-
})];
|
|
1325
2883
|
}
|
|
2884
|
+
return String(value);
|
|
2885
|
+
}
|
|
2886
|
+
var DIAGNOSTIC_MESSAGE = {
|
|
2887
|
+
STYLE_BINDING_DRIFT: (d) => `${String(d.details?.property)} is ${valueText(d.details?.style_value)} in the style but ${valueText(d.details?.token_value)} in the token it is bound to; the two disagree.`,
|
|
2888
|
+
UNIT_METADATA_UNAVAILABLE: (d) => `The numeric value is kept, but scopes ${JSON.stringify(d.details?.scopes ?? [])} state no unit, so a consumer cannot use it as a length.`,
|
|
2889
|
+
UNRESOLVED_REFERENCE: (d) => d.message
|
|
2890
|
+
};
|
|
2891
|
+
function diagnosticRows(diagnostics, index) {
|
|
2892
|
+
const rows = diagnostics.flatMap((d) => {
|
|
2893
|
+
const render = DIAGNOSTIC_MESSAGE[d.code];
|
|
2894
|
+
if (!render) return [];
|
|
2895
|
+
const property = d.details?.property;
|
|
2896
|
+
const path = index.entityLabelById.get(d.entity_id) ?? d.entity_id;
|
|
2897
|
+
return [{
|
|
2898
|
+
id: kebab(d.code),
|
|
2899
|
+
severity: d.severity,
|
|
2900
|
+
path,
|
|
2901
|
+
...typeof property === "string" ? { property } : {},
|
|
2902
|
+
message: render(d)
|
|
2903
|
+
}];
|
|
2904
|
+
});
|
|
2905
|
+
return rows.sort((a, b) => compareCodeUnits(a.id, b.id) || compareCodeUnits(a.path ?? "", b.path ?? "") || compareCodeUnits(a.property ?? "", b.property ?? "") || compareCodeUnits(a.message, b.message));
|
|
2906
|
+
}
|
|
2907
|
+
function issueCounts(artifact) {
|
|
2908
|
+
const bySeverity = /* @__PURE__ */ new Map();
|
|
2909
|
+
for (const finding of artifact.diagnostics) {
|
|
2910
|
+
const byCode = bySeverity.get(finding.severity) ?? /* @__PURE__ */ new Map();
|
|
2911
|
+
byCode.set(finding.code, (byCode.get(finding.code) ?? 0) + 1);
|
|
2912
|
+
bySeverity.set(finding.severity, byCode);
|
|
2913
|
+
}
|
|
2914
|
+
if (bySeverity.size === 0) return void 0;
|
|
2915
|
+
return Object.fromEntries([...bySeverity].sort(([a], [b]) => compareCodeUnits(a, b)).map(
|
|
2916
|
+
([severity, byCode]) => [severity, Object.fromEntries(
|
|
2917
|
+
[...byCode].sort(([a], [b]) => compareCodeUnits(a, b))
|
|
2918
|
+
)]
|
|
2919
|
+
));
|
|
2920
|
+
}
|
|
2921
|
+
function foundationAiContext(artifact, options = {}) {
|
|
2922
|
+
const includeSourceIds = options.includeSourceIds === true;
|
|
2923
|
+
const index = buildIndex(artifact);
|
|
2924
|
+
const tokensByCollection = /* @__PURE__ */ new Map();
|
|
2925
|
+
for (const token of artifact.tokens) {
|
|
2926
|
+
const tokens = tokensByCollection.get(token.collection_id) ?? [];
|
|
2927
|
+
tokens.push(token);
|
|
2928
|
+
tokensByCollection.set(token.collection_id, tokens);
|
|
2929
|
+
}
|
|
2930
|
+
const collections = artifact.collections.map((collection) => {
|
|
2931
|
+
const tokens = tokensByCollection.get(collection.id) ?? [];
|
|
2932
|
+
const modeLabels2 = index.modeLabelByCollectionAndId.get(collection.id) ?? /* @__PURE__ */ new Map();
|
|
2933
|
+
return {
|
|
2934
|
+
name: collection.name,
|
|
2935
|
+
...includeSourceIds || index.ambiguousEntityIds.has(collection.id) ? { source_id: collection.id } : {},
|
|
2936
|
+
...collection.suggested_code_name ? { suggested_code_name: collection.suggested_code_name } : {},
|
|
2937
|
+
...collection.publication ? { publication: collection.publication } : {},
|
|
2938
|
+
...collection.source ? { source: {
|
|
2939
|
+
remote: collection.source.remote,
|
|
2940
|
+
...collection.source.library_name === null ? {} : { library_name: collection.source.library_name }
|
|
2941
|
+
} } : {},
|
|
2942
|
+
default_mode: modeLabels2.get(collection.default_mode_id) ?? collection.default_mode_id,
|
|
2943
|
+
modes: collection.modes.map((mode) => modeLabels2.get(mode.id) ?? mode.name),
|
|
2944
|
+
tokens: tokens.map((token) => compactToken(
|
|
2945
|
+
token,
|
|
2946
|
+
collection,
|
|
2947
|
+
index,
|
|
2948
|
+
includeSourceIds
|
|
2949
|
+
))
|
|
2950
|
+
};
|
|
2951
|
+
});
|
|
2952
|
+
const counts = issueCounts(artifact);
|
|
2953
|
+
const validation = diagnosticRows(artifact.diagnostics, index);
|
|
2954
|
+
return {
|
|
2955
|
+
spec_layer: {
|
|
2956
|
+
kind: "foundation",
|
|
2957
|
+
version: 5,
|
|
2958
|
+
profile: "ai",
|
|
2959
|
+
content_hash: artifact.spec_layer.export.content_hash,
|
|
2960
|
+
source: {
|
|
2961
|
+
provider: "figma",
|
|
2962
|
+
...artifact.spec_layer.source.file_name === null ? {} : { file_name: artifact.spec_layer.source.file_name }
|
|
2963
|
+
}
|
|
2964
|
+
},
|
|
2965
|
+
completeness: artifact.completeness,
|
|
2966
|
+
collections,
|
|
2967
|
+
styles: {
|
|
2968
|
+
typography: artifact.styles.typography.map((style) => compactTypography(
|
|
2969
|
+
style,
|
|
2970
|
+
index,
|
|
2971
|
+
includeSourceIds
|
|
2972
|
+
)),
|
|
2973
|
+
effects: artifact.styles.effects.map((style) => compactEffect(
|
|
2974
|
+
style,
|
|
2975
|
+
index,
|
|
2976
|
+
includeSourceIds
|
|
2977
|
+
))
|
|
2978
|
+
},
|
|
2979
|
+
...validation.length > 0 ? { validation } : {},
|
|
2980
|
+
...counts ? { issue_counts: counts } : {},
|
|
2981
|
+
...artifact.guidelines ? { guidelines: artifact.guidelines.group_descriptions } : {}
|
|
2982
|
+
};
|
|
1326
2983
|
}
|
|
1327
2984
|
|
|
1328
2985
|
// ../extractor/src/v5/dtcg.ts
|
|
@@ -1344,6 +3001,9 @@ function dtcgSegments(name) {
|
|
|
1344
3001
|
}
|
|
1345
3002
|
return { segments, notes };
|
|
1346
3003
|
}
|
|
3004
|
+
function dtcgPathOf(collectionName, tokenName) {
|
|
3005
|
+
return [...dtcgSegments(collectionName).segments, ...dtcgSegments(tokenName).segments].join(".");
|
|
3006
|
+
}
|
|
1347
3007
|
function trimDashes(s) {
|
|
1348
3008
|
let start = 0;
|
|
1349
3009
|
let end = s.length;
|
|
@@ -1514,25 +3174,73 @@ function unitOverrideFor(p, token, collection) {
|
|
|
1514
3174
|
}
|
|
1515
3175
|
return void 0;
|
|
1516
3176
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
3177
|
+
function reportPathOf(p, token) {
|
|
3178
|
+
const path = p.pathById.get(token.id) ?? p.segmentsById.get(token.id)?.join(".") ?? token.name;
|
|
3179
|
+
return p.collidedIds.has(token.id) ? `${path} [${token.id}]` : path;
|
|
3180
|
+
}
|
|
3181
|
+
function ownerFor(p, token) {
|
|
3182
|
+
const path = reportPathOf(p, token);
|
|
3183
|
+
return {
|
|
3184
|
+
overrideConflict: (override) => {
|
|
3185
|
+
reportOnce(p, {
|
|
3186
|
+
code: "unit_override_conflicts_with_scope",
|
|
3187
|
+
severity: "warning",
|
|
3188
|
+
path,
|
|
3189
|
+
message: "A unit override names this token but its scopes state a unitless number; the override was ignored.",
|
|
3190
|
+
details: { id: token.id, override, scopes: [...token.scopes] }
|
|
3191
|
+
});
|
|
3192
|
+
},
|
|
3193
|
+
// Reported without a mode, like the override conflict above: the evidence
|
|
3194
|
+
// is a fact about the token, not about one of its values, so a token in
|
|
3195
|
+
// three modes earns one entry rather than three.
|
|
3196
|
+
derivedUnit: (evidence) => {
|
|
3197
|
+
reportOnce(p, {
|
|
3198
|
+
code: "unit_derived_from_usage",
|
|
3199
|
+
severity: "info",
|
|
3200
|
+
path,
|
|
3201
|
+
// "its own variable", not "no scope": for `via: 'alias-scope'` a scope
|
|
3202
|
+
// is exactly what stated the unit, and this same sentence goes on to
|
|
3203
|
+
// name it. What is true of both kinds of evidence is that the token's
|
|
3204
|
+
// OWN variable states nothing. The CSS header that points a reader at
|
|
3205
|
+
// this entry says it the same way, for the same reason.
|
|
3206
|
+
message: `This token's own variable states no unit, so ${evidence.unit} was taken from how the library uses it: ${evidence.source} ${evidence.via === "binding" ? "binds it to" : "is scoped"} ${evidence.reason}.`,
|
|
3207
|
+
details: {
|
|
3208
|
+
id: token.id,
|
|
3209
|
+
unit: evidence.unit,
|
|
3210
|
+
via: evidence.via,
|
|
3211
|
+
source: evidence.source,
|
|
3212
|
+
reason: evidence.reason
|
|
3213
|
+
}
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
};
|
|
3217
|
+
}
|
|
3218
|
+
function projectedLiteral(p, token, resolved2, owner) {
|
|
1519
3219
|
const collection = p.collectionById.get(token.collection_id);
|
|
1520
3220
|
const override = collection ? unitOverrideFor(p, token, collection) : void 0;
|
|
1521
3221
|
let literal = resolved2;
|
|
1522
3222
|
let overrode = false;
|
|
3223
|
+
let derived;
|
|
1523
3224
|
if (override !== void 0 && literal.type === "number") {
|
|
1524
|
-
if (token.scopes
|
|
3225
|
+
if (scopesStateNumber(token.scopes)) owner?.overrideConflict(override);
|
|
1525
3226
|
else {
|
|
1526
3227
|
literal = { type: "dimension", number: literal.value, unit: override };
|
|
1527
3228
|
overrode = true;
|
|
1528
3229
|
}
|
|
3230
|
+
} else if (literal.type === "number" && !scopesStateUnit(token.scopes)) {
|
|
3231
|
+
const evidence = p.derivedUnits.get(token.id);
|
|
3232
|
+
if (evidence !== void 0) {
|
|
3233
|
+
literal = { type: "dimension", number: literal.value, unit: evidence.unit };
|
|
3234
|
+
derived = evidence;
|
|
3235
|
+
owner?.derivedUnit(evidence);
|
|
3236
|
+
}
|
|
1529
3237
|
}
|
|
1530
3238
|
const converted2 = dtcgLiteral(literal, token.scopes, p.options.values);
|
|
1531
3239
|
if ("omit" in converted2) return { converted: converted2, transform: null };
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
};
|
|
3240
|
+
let transform = literalTransform(literal, token.scopes);
|
|
3241
|
+
if (overrode) transform = "number-unit-override";
|
|
3242
|
+
else if (derived !== void 0) transform = "number-unit-usage";
|
|
3243
|
+
return { converted: converted2, transform };
|
|
1536
3244
|
}
|
|
1537
3245
|
function literalTransform(value, scopes) {
|
|
1538
3246
|
switch (value.type) {
|
|
@@ -1559,7 +3267,24 @@ function literalTransform(value, scopes) {
|
|
|
1559
3267
|
}
|
|
1560
3268
|
function aliasLeafType(p, token, chain, resolved2) {
|
|
1561
3269
|
const terminal = chain.length > 0 ? p.tokenById.get(chain[chain.length - 1].token_id) : void 0;
|
|
1562
|
-
|
|
3270
|
+
const subject = terminal ?? token;
|
|
3271
|
+
return projectedLiteral(p, subject, resolved2, ownerFor(p, subject)).converted;
|
|
3272
|
+
}
|
|
3273
|
+
function terminalOwnType(p, chain) {
|
|
3274
|
+
const hop = chain.length > 0 ? chain[chain.length - 1] : void 0;
|
|
3275
|
+
const terminal = hop ? p.tokenById.get(hop.token_id) : void 0;
|
|
3276
|
+
const value = terminal && hop ? terminal.values[hop.mode_id] : void 0;
|
|
3277
|
+
if (!terminal || !value || value.kind !== "literal") return void 0;
|
|
3278
|
+
return projectedLiteral(p, terminal, value.value, ownerFor(p, terminal)).converted;
|
|
3279
|
+
}
|
|
3280
|
+
function reportAliasTypeMismatch(p, path, targetPath, ownType, targetType) {
|
|
3281
|
+
reportOnce(p, {
|
|
3282
|
+
code: "alias_type_mismatch",
|
|
3283
|
+
severity: "error",
|
|
3284
|
+
path,
|
|
3285
|
+
message: `This token is "${ownType}" but its alias target ${targetPath} is "${targetType}"; a consumer reading the declared type gets a value the target cannot carry.`,
|
|
3286
|
+
details: { target: targetPath, own_type: ownType, target_type: targetType }
|
|
3287
|
+
});
|
|
1563
3288
|
}
|
|
1564
3289
|
function modeLabels(collection) {
|
|
1565
3290
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -1978,10 +3703,11 @@ function styleCensus(tree) {
|
|
|
1978
3703
|
walk(tree);
|
|
1979
3704
|
return { tokens: a.tokens, types: histogram(a.types), descriptions: { present: a.present, missing: a.missing } };
|
|
1980
3705
|
}
|
|
1981
|
-
function foundationDtcg(artifact, options = {}) {
|
|
3706
|
+
function foundationDtcg(artifact, options = {}, derivedUnits) {
|
|
1982
3707
|
const p = {
|
|
1983
3708
|
artifact,
|
|
1984
3709
|
options: { values: options.values ?? "standard", ...options.units ? { units: options.units } : {} },
|
|
3710
|
+
derivedUnits: derivedUnits ?? /* @__PURE__ */ new Map(),
|
|
1985
3711
|
tokenById: new Map(artifact.tokens.map((t) => [t.id, t])),
|
|
1986
3712
|
tokenIds: new Set(artifact.tokens.map((t) => t.id)),
|
|
1987
3713
|
collectionById: new Map(artifact.collections.map((c) => [c.id, c])),
|
|
@@ -2170,45 +3896,754 @@ function tokenLeaf(p, token, collection, modeId) {
|
|
|
2170
3896
|
});
|
|
2171
3897
|
return null;
|
|
2172
3898
|
}
|
|
2173
|
-
|
|
2174
|
-
|
|
3899
|
+
const terminalType = terminalOwnType(p, value.resolved.chain);
|
|
3900
|
+
if (terminalType && !("omit" in terminalType) && terminalType.$type !== typed.$type) {
|
|
3901
|
+
reportAliasTypeMismatch(p, path, targetPath, typed.$type, terminalType.$type);
|
|
3902
|
+
const transform = literalTransform(value.resolved.value, token.scopes);
|
|
3903
|
+
if (transform !== null) recordFact(p, token.id, mode, transform);
|
|
3904
|
+
return { $type: typed.$type, $value: typed.$value, ...description };
|
|
3905
|
+
}
|
|
3906
|
+
recordFact(p, token.id, mode, "alias", typed.$value);
|
|
3907
|
+
return { $type: typed.$type, $value: `{${targetPath}}`, ...description };
|
|
3908
|
+
}
|
|
3909
|
+
const projected = projectedLiteral(p, token, value.value, ownerFor(p, token));
|
|
3910
|
+
const converted2 = projected.converted;
|
|
3911
|
+
if ("omit" in converted2) {
|
|
3912
|
+
reportOnce(p, {
|
|
3913
|
+
code: converted2.omit,
|
|
3914
|
+
severity: "warning",
|
|
3915
|
+
path,
|
|
3916
|
+
mode,
|
|
3917
|
+
message: converted2.omit === "type_not_expressible" ? `DTCG has no ${String(converted2.details.type)} type; the value was omitted.` : `DTCG dimensions take px or rem; a ${String(converted2.details.unit)} value was omitted.`,
|
|
3918
|
+
details: { id: token.id, ...converted2.details }
|
|
3919
|
+
});
|
|
3920
|
+
return null;
|
|
3921
|
+
}
|
|
3922
|
+
if (projected.transform !== null) recordFact(p, token.id, mode, projected.transform);
|
|
3923
|
+
return { $type: converted2.$type, $value: converted2.$value, ...description };
|
|
3924
|
+
}
|
|
3925
|
+
function dtcgExportFiles(out) {
|
|
3926
|
+
const text = (v) => `${JSON.stringify(v, null, 2)}
|
|
3927
|
+
`;
|
|
3928
|
+
const files = {};
|
|
3929
|
+
for (const name of Object.keys(out.files).sort(compareCodeUnits)) files[name] = text(out.files[name]);
|
|
3930
|
+
files["resolver.json"] = text({
|
|
3931
|
+
...out.resolver,
|
|
3932
|
+
$extensions: { "com.spec-layer": out.extension }
|
|
3933
|
+
});
|
|
3934
|
+
files["spec-layer.meta.json"] = text(out.meta);
|
|
3935
|
+
files["report.json"] = text(out.report);
|
|
3936
|
+
return files;
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
// ../extractor/src/v5/componentContext.ts
|
|
3940
|
+
var import_js_sha2564 = __toESM(require_sha256(), 1);
|
|
3941
|
+
function componentFoundationAiSlice(artifact) {
|
|
3942
|
+
const payload = artifact.references.foundation;
|
|
3943
|
+
if (!payload) return null;
|
|
3944
|
+
const dependencyArtifact = {
|
|
3945
|
+
...payload,
|
|
3946
|
+
spec_layer: {
|
|
3947
|
+
kind: "foundation",
|
|
3948
|
+
// The Foundation schema's own version, since this slice is a Foundation
|
|
3949
|
+
// artifact: the two happen to agree today and need not tomorrow.
|
|
3950
|
+
schema_version: SCHEMA_VERSION,
|
|
3951
|
+
schema_uri: "https://spec-layer.com/schemas/foundation-context/v5.json",
|
|
3952
|
+
extractor: artifact.spec_layer.extractor,
|
|
3953
|
+
export: {
|
|
3954
|
+
id: `${artifact.spec_layer.export.id}:foundation-dependencies`,
|
|
3955
|
+
generated_at: artifact.spec_layer.export.generated_at,
|
|
3956
|
+
deterministic: true,
|
|
3957
|
+
content_hash: artifact.foundation_dependency_hash ?? semanticContentHash(payload)
|
|
3958
|
+
},
|
|
3959
|
+
source: {
|
|
3960
|
+
provider: "figma",
|
|
3961
|
+
file_id: null,
|
|
3962
|
+
file_name: artifact.spec_layer.source.file_name,
|
|
3963
|
+
file_version: null,
|
|
3964
|
+
library_enabled: null
|
|
3965
|
+
}
|
|
3966
|
+
},
|
|
3967
|
+
diagnostics: artifact.foundation_diagnostics ?? [],
|
|
3968
|
+
statistics: {}
|
|
3969
|
+
};
|
|
3970
|
+
return {
|
|
3971
|
+
dependency_hash: dependencyArtifact.spec_layer.export.content_hash,
|
|
3972
|
+
compact: foundationAiContext(dependencyArtifact, { includeSourceIds: true })
|
|
3973
|
+
};
|
|
3974
|
+
}
|
|
3975
|
+
function componentEnvelope(artifact, profile) {
|
|
3976
|
+
return {
|
|
3977
|
+
spec_layer: {
|
|
3978
|
+
kind: "component",
|
|
3979
|
+
version: 5,
|
|
3980
|
+
profile,
|
|
3981
|
+
content_hash: artifact.spec_layer.export.content_hash,
|
|
3982
|
+
...artifact.foundation_content_hash ? { foundation_hash: artifact.foundation_content_hash } : {},
|
|
3983
|
+
source: {
|
|
3984
|
+
provider: "figma",
|
|
3985
|
+
...artifact.spec_layer.source.file_name ? { file_name: artifact.spec_layer.source.file_name } : {}
|
|
3986
|
+
}
|
|
3987
|
+
},
|
|
3988
|
+
source: {
|
|
3989
|
+
node_id: artifact.spec_layer.source.node_id,
|
|
3990
|
+
node_name: artifact.spec_layer.source.node_name,
|
|
3991
|
+
...artifact.spec_layer.source.component_key ? { component_key: artifact.spec_layer.source.component_key } : {}
|
|
3992
|
+
}
|
|
3993
|
+
};
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
// ../extractor/src/v5/markdown.ts
|
|
3997
|
+
var COMPONENT_YAML_MARKER = "spec_layer:\n kind: component";
|
|
3998
|
+
var COMPONENT_MARKDOWN_MARKER = "---\nspec_layer:\n kind: component";
|
|
3999
|
+
function escapeInline(text) {
|
|
4000
|
+
return text.replace(/\r?\n/g, " ").replace(/([\\*_<>[\]])/g, "\\$1").trim();
|
|
4001
|
+
}
|
|
4002
|
+
function escapeCell(text) {
|
|
4003
|
+
return text.replace(/\r?\n/g, " ").replace(/([\\*_<>[\]|])/g, "\\$1").trim();
|
|
4004
|
+
}
|
|
4005
|
+
function escapeHeading(text) {
|
|
4006
|
+
return escapeCell(text).replace(/^#/, "\\#");
|
|
4007
|
+
}
|
|
4008
|
+
function escapeBlock(text) {
|
|
4009
|
+
return escapeInline(text).replace(/^(`{3,}|~{3,}|[#=+-])/, "\\$1").replace(/^(\d{1,9})([.)])/, "$1\\$2");
|
|
4010
|
+
}
|
|
4011
|
+
function code(text) {
|
|
4012
|
+
const flat = text.replace(/\r?\n/g, " ");
|
|
4013
|
+
const longest = (flat.match(/`+/g) ?? []).reduce((n, run) => Math.max(n, run.length), 0);
|
|
4014
|
+
const fence = "`".repeat(longest + 1);
|
|
4015
|
+
return longest === 0 ? `${fence}${flat}${fence}` : `${fence} ${flat} ${fence}`;
|
|
4016
|
+
}
|
|
4017
|
+
function codeCell(text) {
|
|
4018
|
+
if (text.includes("\\")) return escapeCell(text);
|
|
4019
|
+
return code(text).replace(/\|/g, "\\|");
|
|
4020
|
+
}
|
|
4021
|
+
function table(headers, rows) {
|
|
4022
|
+
const line = (cells) => `| ${cells.join(" | ")} |
|
|
4023
|
+
`;
|
|
4024
|
+
return line(headers) + `|${headers.map(() => "---").join("|")}|
|
|
4025
|
+
` + rows.map(line).join("");
|
|
4026
|
+
}
|
|
4027
|
+
var asRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
4028
|
+
var str = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
4029
|
+
var SCOPE_SENTENCE = {
|
|
4030
|
+
default_variant: "Default variant."
|
|
4031
|
+
};
|
|
4032
|
+
function bindingsSection(references) {
|
|
4033
|
+
const bindings = Array.isArray(references.bindings) ? references.bindings : [];
|
|
4034
|
+
if (bindings.length === 0) return void 0;
|
|
4035
|
+
const byId = new Map(
|
|
4036
|
+
(Array.isArray(references.used) ? references.used : []).map((r) => [
|
|
4037
|
+
asRecord(r).source_id,
|
|
4038
|
+
r
|
|
4039
|
+
])
|
|
4040
|
+
);
|
|
4041
|
+
const rows = bindings.map((raw) => {
|
|
4042
|
+
const binding = asRecord(raw);
|
|
4043
|
+
const source_id = str(binding.source_id);
|
|
4044
|
+
const reference = source_id ? byId.get(source_id) : void 0;
|
|
4045
|
+
const ref = asRecord(reference);
|
|
4046
|
+
const name = str(ref.name) ?? source_id;
|
|
4047
|
+
const status = ref.status && ref.status !== "resolved" ? ` (${ref.status})` : "";
|
|
4048
|
+
const when = asRecord(binding.when);
|
|
4049
|
+
const whenStr = Object.entries(when).map(([axis, raw_values]) => {
|
|
4050
|
+
const values = Array.isArray(raw_values) ? raw_values : [];
|
|
4051
|
+
return `${escapeCell(axis)}: ${values.map((v) => escapeCell(String(v))).join(", ")}`;
|
|
4052
|
+
}).join("; ");
|
|
4053
|
+
const path = str(binding.path);
|
|
4054
|
+
const property = str(binding.property);
|
|
4055
|
+
return [
|
|
4056
|
+
path ? codeCell(path) : "",
|
|
4057
|
+
property ? escapeCell(property) : "",
|
|
4058
|
+
`${escapeCell(name ?? "")}${status}`,
|
|
4059
|
+
whenStr
|
|
4060
|
+
];
|
|
4061
|
+
});
|
|
4062
|
+
return `## Token bindings
|
|
4063
|
+
|
|
4064
|
+
${table(["Part", "Property", "Token", "When"], rows).trimEnd()}`;
|
|
4065
|
+
}
|
|
4066
|
+
function layoutSection(layout) {
|
|
4067
|
+
const items = Array.isArray(layout.items) ? layout.items : [];
|
|
4068
|
+
if (items.length === 0) return void 0;
|
|
4069
|
+
const rows = items.map((raw) => {
|
|
4070
|
+
const item = asRecord(raw);
|
|
4071
|
+
const path = str(item.path);
|
|
4072
|
+
return [path ? codeCell(path) : "", escapeCell(str(item.summary) ?? "")];
|
|
4073
|
+
});
|
|
4074
|
+
const scope = str(layout.scope);
|
|
4075
|
+
const sentence = scope ? SCOPE_SENTENCE[scope] : void 0;
|
|
4076
|
+
const parts = ["## Layout"];
|
|
4077
|
+
if (sentence) parts.push(sentence);
|
|
4078
|
+
parts.push(table(["Part", "Layout"], rows).trimEnd());
|
|
4079
|
+
return parts.join("\n\n");
|
|
4080
|
+
}
|
|
4081
|
+
function propertiesSection(api) {
|
|
4082
|
+
const rows = [];
|
|
4083
|
+
for (const [name, raw] of Object.entries(asRecord(api.variants))) {
|
|
4084
|
+
const axis = asRecord(raw);
|
|
4085
|
+
const options = Array.isArray(axis.options) ? axis.options.map((o) => escapeCell(String(o))).join(", ") : "";
|
|
4086
|
+
rows.push([escapeCell(name), "Variant", options, escapeCell(String(axis.default ?? ""))]);
|
|
4087
|
+
}
|
|
4088
|
+
for (const [name, raw] of Object.entries(asRecord(api.booleans))) {
|
|
4089
|
+
const b = asRecord(raw);
|
|
4090
|
+
rows.push([
|
|
4091
|
+
escapeCell(name),
|
|
4092
|
+
"Boolean",
|
|
4093
|
+
"",
|
|
4094
|
+
b.default === void 0 ? "" : escapeCell(String(b.default))
|
|
4095
|
+
]);
|
|
4096
|
+
}
|
|
4097
|
+
for (const [name, raw] of Object.entries(asRecord(api.slots))) {
|
|
4098
|
+
const slot = asRecord(raw);
|
|
4099
|
+
const type = str(slot.type);
|
|
4100
|
+
const label = type ? type.charAt(0).toUpperCase() + type.slice(1) : "Slot";
|
|
4101
|
+
rows.push([
|
|
4102
|
+
escapeCell(name),
|
|
4103
|
+
label,
|
|
4104
|
+
"",
|
|
4105
|
+
slot.default === void 0 ? "" : escapeCell(String(slot.default))
|
|
4106
|
+
]);
|
|
4107
|
+
}
|
|
4108
|
+
const states = Array.isArray(api.states) ? api.states.map((s) => escapeInline(String(s))) : [];
|
|
4109
|
+
if (rows.length === 0 && states.length === 0) return void 0;
|
|
4110
|
+
const parts = ["## Properties"];
|
|
4111
|
+
if (rows.length > 0) {
|
|
4112
|
+
parts.push(table(["Property", "Type", "Options", "Default"], rows).trimEnd());
|
|
4113
|
+
}
|
|
4114
|
+
if (states.length > 0) parts.push(`States: ${states.join(", ")}`);
|
|
4115
|
+
return parts.join("\n\n");
|
|
4116
|
+
}
|
|
4117
|
+
function anatomyBullets(nodes, depth) {
|
|
4118
|
+
const lines = [];
|
|
4119
|
+
for (const raw of nodes) {
|
|
4120
|
+
const node = asRecord(raw);
|
|
4121
|
+
const part = escapeInline(str(node.part) ?? "");
|
|
4122
|
+
const path = str(node.path);
|
|
4123
|
+
const type = str(node.type);
|
|
4124
|
+
const parts = [];
|
|
4125
|
+
if (path) parts.push(code(path));
|
|
4126
|
+
const component = str(node.component);
|
|
4127
|
+
if (component) parts.push(`instance of ${escapeInline(component)}`);
|
|
4128
|
+
else if (type) parts.push(escapeInline(type.toLowerCase()));
|
|
4129
|
+
const shownBy = str(node.shown_by);
|
|
4130
|
+
if (shownBy) parts.push(`shown when ${code(shownBy)} is true`);
|
|
4131
|
+
lines.push(`${" ".repeat(depth)}- ${part}: ${parts.join(", ")}`);
|
|
4132
|
+
if (Array.isArray(node.children)) {
|
|
4133
|
+
lines.push(...anatomyBullets(node.children, depth + 1));
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
return lines;
|
|
4137
|
+
}
|
|
4138
|
+
var NOT_READ_SENTENCE = "Token values are not included: the foundations had not been read when this was exported.";
|
|
4139
|
+
function styleValueText(value) {
|
|
4140
|
+
if (value !== null && typeof value === "object") {
|
|
4141
|
+
const record = value;
|
|
4142
|
+
if (typeof record.missing === "string") return `missing: ${record.missing}`;
|
|
4143
|
+
if (typeof record.alias === "string") {
|
|
4144
|
+
if (record.resolved !== void 0) {
|
|
4145
|
+
const resolved2 = record.resolved;
|
|
4146
|
+
return `${record.alias} (resolved: ${valueText(resolved2.value)})`;
|
|
4147
|
+
}
|
|
4148
|
+
if (typeof record.unresolved === "string") {
|
|
4149
|
+
return `${record.alias} (unresolved: ${record.unresolved})`;
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
if ("value" in record) return valueText(record.value);
|
|
4153
|
+
}
|
|
4154
|
+
return valueText(value);
|
|
4155
|
+
}
|
|
4156
|
+
function typographySection(items) {
|
|
4157
|
+
if (items.length === 0) return void 0;
|
|
4158
|
+
const rows = items.map((raw) => {
|
|
4159
|
+
const style = asRecord(raw);
|
|
4160
|
+
const properties = asRecord(style.properties);
|
|
4161
|
+
return [
|
|
4162
|
+
escapeCell(str(style.name) ?? ""),
|
|
4163
|
+
escapeCell(styleValueText(properties.font_family)),
|
|
4164
|
+
escapeCell(styleValueText(properties.font_weight)),
|
|
4165
|
+
escapeCell(styleValueText(properties.font_size)),
|
|
4166
|
+
escapeCell(styleValueText(properties.line_height)),
|
|
4167
|
+
escapeCell(styleValueText(properties.letter_spacing)),
|
|
4168
|
+
escapeCell(styleValueText(properties.paragraph_indent))
|
|
4169
|
+
];
|
|
4170
|
+
});
|
|
4171
|
+
return [
|
|
4172
|
+
"### Typography styles",
|
|
4173
|
+
table(
|
|
4174
|
+
["Style", "Font family", "Weight", "Size", "Line height", "Letter spacing", "Paragraph indent"],
|
|
4175
|
+
rows
|
|
4176
|
+
).trimEnd()
|
|
4177
|
+
].join("\n\n");
|
|
4178
|
+
}
|
|
4179
|
+
function effectSummary(effect) {
|
|
4180
|
+
const parts = [];
|
|
4181
|
+
const type = str(effect.type);
|
|
4182
|
+
if (type) parts.push(type);
|
|
4183
|
+
if (effect.offset_x !== void 0 || effect.offset_y !== void 0) {
|
|
4184
|
+
parts.push(`offset ${valueText(effect.offset_x)}/${valueText(effect.offset_y)}`);
|
|
4185
|
+
}
|
|
4186
|
+
if (effect.blur !== void 0) parts.push(`blur ${valueText(effect.blur)}`);
|
|
4187
|
+
if (effect.spread !== void 0) parts.push(`spread ${valueText(effect.spread)}`);
|
|
4188
|
+
if (effect.color !== void 0) parts.push(valueText(effect.color));
|
|
4189
|
+
const summary = parts.join(", ");
|
|
4190
|
+
return effect.visible === false ? `${summary} (hidden)` : summary;
|
|
4191
|
+
}
|
|
4192
|
+
function effectsSection(items) {
|
|
4193
|
+
if (items.length === 0) return void 0;
|
|
4194
|
+
const rows = items.map((raw) => {
|
|
4195
|
+
const style = asRecord(raw);
|
|
4196
|
+
const mode = str(style.mode);
|
|
4197
|
+
const effects = Array.isArray(style.effects) ? style.effects : [];
|
|
4198
|
+
const summary = effects.map((effect) => effectSummary(asRecord(effect))).join("; ");
|
|
4199
|
+
return [escapeCell(str(style.name) ?? ""), mode ? escapeCell(mode) : "", escapeCell(summary)];
|
|
4200
|
+
});
|
|
4201
|
+
return [
|
|
4202
|
+
"### Effect styles",
|
|
4203
|
+
table(["Style", "Mode", "Effects"], rows).trimEnd()
|
|
4204
|
+
].join("\n\n");
|
|
4205
|
+
}
|
|
4206
|
+
function inlineEffectLayerText(raw) {
|
|
4207
|
+
const layer = asRecord(raw);
|
|
4208
|
+
const type = str(layer.type) ?? "unknown";
|
|
4209
|
+
const num = (value) => typeof value === "number" ? String(value) : "";
|
|
4210
|
+
let summary;
|
|
4211
|
+
switch (type) {
|
|
4212
|
+
case "drop-shadow":
|
|
4213
|
+
case "inner-shadow": {
|
|
4214
|
+
const offset = asRecord(layer.offset);
|
|
4215
|
+
const color = asRecord(layer.color);
|
|
4216
|
+
const parts = [type, `offset ${num(offset.x)}/${num(offset.y)}`, `radius ${num(layer.radius)}`];
|
|
4217
|
+
if (layer.spread !== void 0) parts.push(`spread ${num(layer.spread)}`);
|
|
4218
|
+
if (str(color.hex)) parts.push(`${str(color.hex)} alpha ${num(color.alpha)}`);
|
|
4219
|
+
summary = parts.join(", ");
|
|
4220
|
+
break;
|
|
4221
|
+
}
|
|
4222
|
+
case "layer-blur":
|
|
4223
|
+
case "background-blur": {
|
|
4224
|
+
const progressive = layer.blurType === "progressive";
|
|
4225
|
+
const parts = [progressive ? `${type} (progressive)` : type, `radius ${num(layer.radius)}`];
|
|
4226
|
+
if (progressive) {
|
|
4227
|
+
const startOffset = asRecord(layer.startOffset);
|
|
4228
|
+
const endOffset = asRecord(layer.endOffset);
|
|
4229
|
+
parts.push(`start radius ${num(layer.startRadius)}`);
|
|
4230
|
+
parts.push(`start offset ${num(startOffset.x)}/${num(startOffset.y)}`);
|
|
4231
|
+
parts.push(`end offset ${num(endOffset.x)}/${num(endOffset.y)}`);
|
|
4232
|
+
}
|
|
4233
|
+
summary = parts.join(", ");
|
|
4234
|
+
break;
|
|
4235
|
+
}
|
|
4236
|
+
case "noise": {
|
|
4237
|
+
const color = asRecord(layer.color);
|
|
4238
|
+
const noiseType = str(layer.noiseType);
|
|
4239
|
+
const parts = [noiseType ? `noise (${noiseType})` : "noise"];
|
|
4240
|
+
if (str(color.hex)) parts.push(`${str(color.hex)} alpha ${num(color.alpha)}`);
|
|
4241
|
+
parts.push(`size ${num(layer.noiseSize)}`);
|
|
4242
|
+
parts.push(`density ${num(layer.density)}`);
|
|
4243
|
+
const secondary = asRecord(layer.secondaryColor);
|
|
4244
|
+
if (str(secondary.hex)) parts.push(`secondary ${str(secondary.hex)} alpha ${num(secondary.alpha)}`);
|
|
4245
|
+
if (layer.opacity !== void 0) parts.push(`opacity ${num(layer.opacity)}`);
|
|
4246
|
+
summary = parts.join(", ");
|
|
4247
|
+
break;
|
|
4248
|
+
}
|
|
4249
|
+
case "texture": {
|
|
4250
|
+
const parts = [
|
|
4251
|
+
"texture",
|
|
4252
|
+
`size ${num(layer.noiseSize)}`,
|
|
4253
|
+
`radius ${num(layer.radius)}`,
|
|
4254
|
+
`clip ${layer.clipToShape === true ? "true" : "false"}`
|
|
4255
|
+
];
|
|
4256
|
+
const vector = asRecord(layer.noiseSizeVector);
|
|
4257
|
+
if (vector.x !== void 0) parts.push(`vector ${num(vector.x)}/${num(vector.y)}`);
|
|
4258
|
+
summary = parts.join(", ");
|
|
4259
|
+
break;
|
|
4260
|
+
}
|
|
4261
|
+
case "glass": {
|
|
4262
|
+
summary = [
|
|
4263
|
+
"glass",
|
|
4264
|
+
`radius ${num(layer.radius)}`,
|
|
4265
|
+
`light intensity ${num(layer.lightIntensity)}`,
|
|
4266
|
+
`light angle ${num(layer.lightAngle)}`,
|
|
4267
|
+
`refraction ${num(layer.refraction)}`,
|
|
4268
|
+
`depth ${num(layer.depth)}`,
|
|
4269
|
+
`dispersion ${num(layer.dispersion)}`
|
|
4270
|
+
].join(", ");
|
|
4271
|
+
break;
|
|
4272
|
+
}
|
|
4273
|
+
default: {
|
|
4274
|
+
const figmaType = str(layer.figma_type);
|
|
4275
|
+
summary = figmaType ? `unknown (${figmaType})` : "unknown";
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
const bindings = layer.bindings !== void 0 ? asRecord(layer.bindings) : void 0;
|
|
4279
|
+
const boundText = bindings ? Object.entries(bindings).map(([field, reference]) => `${field} bound to ${str(asRecord(reference).name) ?? ""}`).join(", ") : "";
|
|
4280
|
+
const hidden = layer.visible === false ? " (hidden)" : "";
|
|
4281
|
+
return `${summary}${boundText ? `, ${boundText}` : ""}${hidden}`;
|
|
4282
|
+
}
|
|
4283
|
+
function effectsInlineSection(items) {
|
|
4284
|
+
if (items.length === 0) return void 0;
|
|
4285
|
+
const rows = items.map((raw) => {
|
|
4286
|
+
const item = asRecord(raw);
|
|
4287
|
+
const path = str(item.path);
|
|
4288
|
+
const layers = Array.isArray(item.layers) ? item.layers : [];
|
|
4289
|
+
const summary = layers.map((layer) => inlineEffectLayerText(layer)).join("; ");
|
|
4290
|
+
return [path ? codeCell(path) : "", escapeCell(summary)];
|
|
4291
|
+
});
|
|
4292
|
+
return `## Effects
|
|
4293
|
+
|
|
4294
|
+
${table(["Part", "Effects"], rows).trimEnd()}`;
|
|
4295
|
+
}
|
|
4296
|
+
function unboundSection(unbound) {
|
|
4297
|
+
const entries = Array.isArray(unbound) ? unbound : [];
|
|
4298
|
+
if (entries.length === 0) return void 0;
|
|
4299
|
+
const rows = entries.map((raw) => {
|
|
4300
|
+
const entry2 = asRecord(raw);
|
|
4301
|
+
return [
|
|
4302
|
+
str(entry2.path) ? codeCell(str(entry2.path)) : "",
|
|
4303
|
+
escapeCell(str(entry2.property) ?? ""),
|
|
4304
|
+
escapeCell(str(entry2.issue) ?? ""),
|
|
4305
|
+
entry2.value === void 0 ? "" : escapeCell(String(entry2.value))
|
|
4306
|
+
];
|
|
4307
|
+
});
|
|
4308
|
+
return `## Unbound values
|
|
4309
|
+
|
|
4310
|
+
${table(["Part", "Property", "Issue", "Value"], rows).trimEnd()}`;
|
|
4311
|
+
}
|
|
4312
|
+
function issuesSection(artifact) {
|
|
4313
|
+
const validation = Array.isArray(artifact.validation) ? artifact.validation : [];
|
|
4314
|
+
const lines = [];
|
|
4315
|
+
for (const raw of validation) {
|
|
4316
|
+
const row = asRecord(raw);
|
|
4317
|
+
if (str(row.id) === "unbound-value") continue;
|
|
4318
|
+
lines.push(issueLine(row));
|
|
4319
|
+
}
|
|
4320
|
+
return lines.length === 0 ? void 0 : `## Issues
|
|
4321
|
+
|
|
4322
|
+
${lines.join("\n")}`;
|
|
4323
|
+
}
|
|
4324
|
+
function issueLine(row) {
|
|
4325
|
+
const severity = escapeInline(str(row.severity) ?? "info");
|
|
4326
|
+
const message = escapeInline(str(row.message) ?? "");
|
|
4327
|
+
const where = [
|
|
4328
|
+
str(row.path) ? code(str(row.path)) : void 0,
|
|
4329
|
+
str(row.property) ? escapeInline(str(row.property)) : void 0
|
|
4330
|
+
].filter((v) => v !== void 0);
|
|
4331
|
+
return `- ${severity}: ${message}${where.length > 0 ? ` (${where.join(", ")})` : ""}`;
|
|
4332
|
+
}
|
|
4333
|
+
function foundationIssuesSection(rows) {
|
|
4334
|
+
if (!rows || rows.length === 0) return void 0;
|
|
4335
|
+
return `### Foundation issues
|
|
4336
|
+
|
|
4337
|
+
${rows.map((row) => issueLine(asRecord(row))).join("\n")}`;
|
|
4338
|
+
}
|
|
4339
|
+
function tokenValueText(value) {
|
|
4340
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
4341
|
+
const record = value;
|
|
4342
|
+
if (typeof record.missing === "string") return `missing: ${record.missing}`;
|
|
4343
|
+
if (typeof record.alias === "string") {
|
|
4344
|
+
const chain = Array.isArray(record.chain) ? record.chain.filter((step) => typeof step === "string") : [];
|
|
4345
|
+
const steps = chain.length === 0 ? [record.alias] : chain[0] === record.alias ? chain : [record.alias, ...chain];
|
|
4346
|
+
const target = steps.join(" \u2192 ");
|
|
4347
|
+
if (record.resolved !== void 0) return `${target} (resolved: ${valueText(record.resolved)})`;
|
|
4348
|
+
if (typeof record.unresolved === "string") return `${target} (unresolved: ${record.unresolved})`;
|
|
4349
|
+
return target;
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
return valueText(value);
|
|
4353
|
+
}
|
|
4354
|
+
function tokensUsedSection(artifact) {
|
|
4355
|
+
const slice = componentFoundationAiSlice(artifact);
|
|
4356
|
+
if (!slice) return `## Tokens used
|
|
4357
|
+
|
|
4358
|
+
${NOT_READ_SENTENCE}`;
|
|
4359
|
+
const { compact } = slice;
|
|
4360
|
+
const parts = ["## Tokens used"];
|
|
4361
|
+
parts.push(
|
|
4362
|
+
`Foundation: collections ${compact.completeness.collections}, styles ${compact.completeness.styles}.`
|
|
4363
|
+
);
|
|
4364
|
+
if (compact.completeness.unavailable_sources.length > 0) {
|
|
4365
|
+
parts.push(`Unavailable sources: ${compact.completeness.unavailable_sources.map((source) => escapeInline(source)).join(", ")}.`);
|
|
4366
|
+
}
|
|
4367
|
+
for (const collection of compact.collections) {
|
|
4368
|
+
parts.push(`### ${escapeInline(collection.name)}`);
|
|
4369
|
+
const modes = collection.modes.map((mode) => mode === collection.default_mode ? `${escapeInline(mode)} (default)` : escapeInline(mode));
|
|
4370
|
+
parts.push(`Modes: ${modes.join(", ")}.`);
|
|
4371
|
+
const rows = collection.tokens.map((token) => [
|
|
4372
|
+
escapeCell(token.name),
|
|
4373
|
+
escapeCell(token.type),
|
|
4374
|
+
...collection.modes.map((mode) => escapeCell(tokenValueText(token.values[mode]))),
|
|
4375
|
+
token.code_syntax ? Object.entries(token.code_syntax).map(([platform, id]) => id ? `${escapeCell(platform)} ${codeCell(id)}` : escapeCell(platform)).join(", ") : ""
|
|
4376
|
+
]);
|
|
4377
|
+
parts.push(table(
|
|
4378
|
+
["Token", "Type", ...collection.modes.map((mode) => escapeCell(mode)), "Code syntax"],
|
|
4379
|
+
rows
|
|
4380
|
+
).trimEnd());
|
|
4381
|
+
}
|
|
4382
|
+
const typography = typographySection(compact.styles.typography);
|
|
4383
|
+
if (typography) parts.push(typography);
|
|
4384
|
+
const effects = effectsSection(compact.styles.effects);
|
|
4385
|
+
if (effects) parts.push(effects);
|
|
4386
|
+
const issues = foundationIssuesSection(compact.validation);
|
|
4387
|
+
if (issues) parts.push(issues);
|
|
4388
|
+
return parts.join("\n\n");
|
|
4389
|
+
}
|
|
4390
|
+
var AI_MARKER = "*Written by AI from the extracted facts, not read from Figma.*";
|
|
4391
|
+
var ATX_H1_H2 = /^ {0,3}(#{1,2})(?= |$)/;
|
|
4392
|
+
var FENCE_MARKER = /^(`{3,}|~{3,})/;
|
|
4393
|
+
var SETEXT_UNDERLINE = /^(?:=+|-+)$/;
|
|
4394
|
+
function demote(blob) {
|
|
4395
|
+
const lines = blob.split("\n");
|
|
4396
|
+
const out = [];
|
|
4397
|
+
let inFence = false;
|
|
4398
|
+
const isHeadingOrFence = (line) => {
|
|
4399
|
+
if (line === void 0) return false;
|
|
4400
|
+
const trimmed = line.trim();
|
|
4401
|
+
return /^#{1,6}(?= |$)/.test(trimmed) || FENCE_MARKER.test(trimmed);
|
|
4402
|
+
};
|
|
4403
|
+
for (const rawLine of lines) {
|
|
4404
|
+
const trimmed = rawLine.trim();
|
|
4405
|
+
if (FENCE_MARKER.test(trimmed)) {
|
|
4406
|
+
inFence = !inFence;
|
|
4407
|
+
out.push(rawLine);
|
|
4408
|
+
continue;
|
|
4409
|
+
}
|
|
4410
|
+
if (inFence) {
|
|
4411
|
+
out.push(rawLine);
|
|
4412
|
+
continue;
|
|
4413
|
+
}
|
|
4414
|
+
if (SETEXT_UNDERLINE.test(trimmed)) {
|
|
4415
|
+
const previous = out[out.length - 1];
|
|
4416
|
+
const previousIsBlank = previous === void 0 || previous.trim() === "";
|
|
4417
|
+
if (!previousIsBlank && !isHeadingOrFence(previous)) {
|
|
4418
|
+
out[out.length - 1] = `### ${previous.trim()}`;
|
|
4419
|
+
continue;
|
|
4420
|
+
}
|
|
4421
|
+
out.push(rawLine);
|
|
4422
|
+
continue;
|
|
4423
|
+
}
|
|
4424
|
+
out.push(rawLine.replace(ATX_H1_H2, "###"));
|
|
4425
|
+
}
|
|
4426
|
+
return out.join("\n").trimEnd();
|
|
4427
|
+
}
|
|
4428
|
+
function proseSection(heading, blob) {
|
|
4429
|
+
return `## ${heading}
|
|
4430
|
+
|
|
4431
|
+
${AI_MARKER}
|
|
4432
|
+
|
|
4433
|
+
${demote(blob)}`;
|
|
4434
|
+
}
|
|
4435
|
+
function overviewBlock(guidelines) {
|
|
4436
|
+
const blob = str(guidelines.definition);
|
|
4437
|
+
return blob ? proseSection("Overview", blob) : void 0;
|
|
4438
|
+
}
|
|
4439
|
+
function anatomyProseParagraph(guidelines) {
|
|
4440
|
+
const blob = str(guidelines.anatomy_summary);
|
|
4441
|
+
return blob ? `${AI_MARKER}
|
|
4442
|
+
|
|
4443
|
+
${demote(blob)}` : void 0;
|
|
4444
|
+
}
|
|
4445
|
+
function ruleList(items) {
|
|
4446
|
+
const indent = (blob) => blob.split("\n").map((line, index) => index === 0 || line === "" ? line : ` ${line}`).join("\n");
|
|
4447
|
+
return items.map((item) => `- ${indent(demote(String(item)))}`).join("\n");
|
|
4448
|
+
}
|
|
4449
|
+
function restProseBlocks(guidelines) {
|
|
4450
|
+
const blocks = [];
|
|
4451
|
+
const add = (heading, key) => {
|
|
4452
|
+
const blob = str(guidelines[key]);
|
|
4453
|
+
if (blob) blocks.push(proseSection(heading, blob));
|
|
4454
|
+
};
|
|
4455
|
+
add("Variants", "variants_summary");
|
|
4456
|
+
const dos = Array.isArray(guidelines.dos) ? guidelines.dos : [];
|
|
4457
|
+
const donts = Array.isArray(guidelines.donts) ? guidelines.donts : [];
|
|
4458
|
+
if (dos.length > 0 || donts.length > 0) {
|
|
4459
|
+
const parts = [`## Do and don't`, AI_MARKER];
|
|
4460
|
+
if (dos.length > 0) parts.push("### Do", ruleList(dos));
|
|
4461
|
+
if (donts.length > 0) parts.push(`### Don't`, ruleList(donts));
|
|
4462
|
+
blocks.push(parts.join("\n\n"));
|
|
4463
|
+
}
|
|
4464
|
+
add("Accessibility", "accessibility");
|
|
4465
|
+
add("Interactions", "interactions");
|
|
4466
|
+
add("Content considerations", "content_considerations");
|
|
4467
|
+
add("Design considerations", "design_considerations");
|
|
4468
|
+
return blocks;
|
|
4469
|
+
}
|
|
4470
|
+
function frontMatter(artifact) {
|
|
4471
|
+
const envelope = componentEnvelope(artifact, "markdown");
|
|
4472
|
+
return `---
|
|
4473
|
+
${toYaml(envelope)}---
|
|
4474
|
+
`;
|
|
4475
|
+
}
|
|
4476
|
+
function componentMarkdown(artifact) {
|
|
4477
|
+
const component = asRecord(artifact.component);
|
|
4478
|
+
const guidelines = asRecord(artifact.guidelines);
|
|
4479
|
+
const blocks = [];
|
|
4480
|
+
const name = str(component.name);
|
|
4481
|
+
if (name === void 0) throw new Error("componentMarkdown needs component.name; the artifact has none.");
|
|
4482
|
+
if (!Array.isArray(artifact.anatomy)) {
|
|
4483
|
+
throw new Error("componentMarkdown needs anatomy as an array; the artifact has none.");
|
|
4484
|
+
}
|
|
4485
|
+
blocks.push(`# ${escapeHeading(name)}`);
|
|
4486
|
+
const description = str(component.description);
|
|
4487
|
+
if (description) blocks.push(escapeBlock(description));
|
|
4488
|
+
const related = Array.isArray(component.related) ? component.related.filter((r) => typeof r === "string") : [];
|
|
4489
|
+
if (related.length > 0) {
|
|
4490
|
+
blocks.push(`Related: ${related.map((r) => escapeInline(r)).join(", ")}`);
|
|
4491
|
+
}
|
|
4492
|
+
const overview = overviewBlock(guidelines);
|
|
4493
|
+
if (overview) blocks.push(overview);
|
|
4494
|
+
if (artifact.api !== void 0) {
|
|
4495
|
+
const section = propertiesSection(asRecord(artifact.api));
|
|
4496
|
+
if (section) blocks.push(section);
|
|
4497
|
+
}
|
|
4498
|
+
if (artifact.anatomy.length > 0) {
|
|
4499
|
+
const anatomyProse = anatomyProseParagraph(guidelines);
|
|
4500
|
+
const bullets = anatomyBullets(artifact.anatomy, 0).join("\n");
|
|
4501
|
+
const body = anatomyProse ? `${anatomyProse}
|
|
4502
|
+
|
|
4503
|
+
${bullets}` : bullets;
|
|
4504
|
+
blocks.push(`## Anatomy
|
|
4505
|
+
|
|
4506
|
+
${body}`);
|
|
4507
|
+
}
|
|
4508
|
+
if (artifact.layout !== void 0) {
|
|
4509
|
+
const section = layoutSection(asRecord(artifact.layout));
|
|
4510
|
+
if (section) blocks.push(section);
|
|
4511
|
+
}
|
|
4512
|
+
const bindings = bindingsSection(asRecord(artifact.references));
|
|
4513
|
+
if (bindings) blocks.push(bindings);
|
|
4514
|
+
blocks.push(tokensUsedSection(artifact));
|
|
4515
|
+
if (artifact.effects_inline !== void 0) {
|
|
4516
|
+
const section = effectsInlineSection(
|
|
4517
|
+
Array.isArray(artifact.effects_inline) ? artifact.effects_inline : []
|
|
4518
|
+
);
|
|
4519
|
+
if (section) blocks.push(section);
|
|
4520
|
+
}
|
|
4521
|
+
const unbound = unboundSection(artifact.unbound);
|
|
4522
|
+
if (unbound) blocks.push(unbound);
|
|
4523
|
+
const issues = issuesSection(artifact);
|
|
4524
|
+
if (issues) blocks.push(issues);
|
|
4525
|
+
blocks.push(...restProseBlocks(guidelines));
|
|
4526
|
+
return `${frontMatter(artifact)}
|
|
4527
|
+
${blocks.join("\n\n")}
|
|
4528
|
+
`;
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
// ../extractor/src/v5/usageUnits.ts
|
|
4532
|
+
var LENGTH_SCOPES = ["CORNER_RADIUS", "WIDTH_HEIGHT", "GAP", "FONT_SIZE", "STROKE_FLOAT"];
|
|
4533
|
+
var LENGTH_PROPERTIES = [
|
|
4534
|
+
"border-radius",
|
|
4535
|
+
"border-top-left-radius",
|
|
4536
|
+
"border-top-right-radius",
|
|
4537
|
+
"border-bottom-left-radius",
|
|
4538
|
+
"border-bottom-right-radius",
|
|
4539
|
+
"gap",
|
|
4540
|
+
"height",
|
|
4541
|
+
"padding-x",
|
|
4542
|
+
"padding-y",
|
|
4543
|
+
"width"
|
|
4544
|
+
];
|
|
4545
|
+
var MAX_ALIAS_DEPTH = 16;
|
|
4546
|
+
var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4547
|
+
function foundationOf(bundle) {
|
|
4548
|
+
const artifact = bundle.foundation?.artifact;
|
|
4549
|
+
if (!isRecord2(artifact)) return null;
|
|
4550
|
+
if (!Array.isArray(artifact.tokens) || !Array.isArray(artifact.collections)) return null;
|
|
4551
|
+
return artifact;
|
|
4552
|
+
}
|
|
4553
|
+
function precedes(a, b) {
|
|
4554
|
+
return (compareCodeUnits(a.via, b.via) || compareCodeUnits(a.source, b.source) || compareCodeUnits(a.reason, b.reason)) < 0;
|
|
4555
|
+
}
|
|
4556
|
+
function bindingsOf(artifact) {
|
|
4557
|
+
if (!isRecord2(artifact) || !isRecord2(artifact.references)) return [];
|
|
4558
|
+
const bindings = artifact.references.bindings;
|
|
4559
|
+
if (!Array.isArray(bindings)) return [];
|
|
4560
|
+
return bindings.filter((b) => isRecord2(b) && b.kind === "variable" && typeof b.source_id === "string" && typeof b.property === "string");
|
|
4561
|
+
}
|
|
4562
|
+
function aliasTargets(token) {
|
|
4563
|
+
const out = [];
|
|
4564
|
+
for (const value of Object.values(token.values)) {
|
|
4565
|
+
if (value.kind !== "alias" || value.reference.external) continue;
|
|
4566
|
+
const id = value.reference.target_id;
|
|
4567
|
+
if (id !== null && !out.includes(id)) out.push(id);
|
|
4568
|
+
}
|
|
4569
|
+
return out;
|
|
4570
|
+
}
|
|
4571
|
+
function usageUnits(bundle) {
|
|
4572
|
+
const evidence = /* @__PURE__ */ new Map();
|
|
4573
|
+
const artifact = foundationOf(bundle);
|
|
4574
|
+
if (artifact === null) return evidence;
|
|
4575
|
+
const tokenById = new Map(artifact.tokens.map((t) => [t.id, t]));
|
|
4576
|
+
const collectionById = new Map(artifact.collections.map((c) => [c.id, c]));
|
|
4577
|
+
const lengthUse = /* @__PURE__ */ new Map();
|
|
4578
|
+
const nonLengthUse = [];
|
|
4579
|
+
for (const component of bundle.components) {
|
|
4580
|
+
for (const binding of bindingsOf(component.artifact)) {
|
|
4581
|
+
if (!LENGTH_PROPERTIES.includes(binding.property)) {
|
|
4582
|
+
nonLengthUse.push(binding.source_id);
|
|
4583
|
+
continue;
|
|
4584
|
+
}
|
|
4585
|
+
const found = {
|
|
4586
|
+
unit: "px",
|
|
4587
|
+
via: "binding",
|
|
4588
|
+
source: component.name,
|
|
4589
|
+
reason: binding.property
|
|
4590
|
+
};
|
|
4591
|
+
const prior = lengthUse.get(binding.source_id);
|
|
4592
|
+
if (prior === void 0 || precedes(found, prior)) lengthUse.set(binding.source_id, found);
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
const vetoedIds = /* @__PURE__ */ new Set();
|
|
4596
|
+
const walkChain = (startId, includeStart, visit) => {
|
|
4597
|
+
const seen = /* @__PURE__ */ new Set([startId]);
|
|
4598
|
+
let frontier = [startId];
|
|
4599
|
+
for (let depth = 0; depth <= MAX_ALIAS_DEPTH && frontier.length > 0; depth += 1) {
|
|
4600
|
+
const next = [];
|
|
4601
|
+
for (const id of frontier) {
|
|
4602
|
+
const token = tokenById.get(id);
|
|
4603
|
+
if (id !== startId && token !== void 0 && scopesStateUnit(token.scopes)) continue;
|
|
4604
|
+
if (id !== startId || includeStart) visit(id);
|
|
4605
|
+
if (token === void 0) continue;
|
|
4606
|
+
for (const target of aliasTargets(token)) {
|
|
4607
|
+
if (seen.has(target)) continue;
|
|
4608
|
+
seen.add(target);
|
|
4609
|
+
next.push(target);
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
frontier = next;
|
|
4613
|
+
}
|
|
4614
|
+
};
|
|
4615
|
+
for (const id of nonLengthUse) walkChain(id, true, (reached) => vetoedIds.add(reached));
|
|
4616
|
+
for (const token of artifact.tokens) {
|
|
4617
|
+
if (!scopesStateNumber(token.scopes)) continue;
|
|
4618
|
+
walkChain(token.id, false, (reached) => vetoedIds.add(reached));
|
|
2175
4619
|
}
|
|
2176
|
-
const
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
}
|
|
2185
|
-
const
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
4620
|
+
const isCandidate = (id) => {
|
|
4621
|
+
const token = tokenById.get(id);
|
|
4622
|
+
return token !== void 0 && token.type === "number" && !scopesStateUnit(token.scopes);
|
|
4623
|
+
};
|
|
4624
|
+
const record = (id, found) => {
|
|
4625
|
+
if (!isCandidate(id) || vetoedIds.has(id)) return;
|
|
4626
|
+
const prior = evidence.get(id);
|
|
4627
|
+
if (prior === void 0 || precedes(found, prior)) evidence.set(id, found);
|
|
4628
|
+
};
|
|
4629
|
+
const pin = (startId, found, includeStart) => walkChain(startId, includeStart, (id) => record(id, found));
|
|
4630
|
+
for (const token of artifact.tokens) {
|
|
4631
|
+
const scopes = token.scopes.filter((s) => LENGTH_SCOPES.includes(s)).sort(compareCodeUnits);
|
|
4632
|
+
if (scopes.length === 0) continue;
|
|
4633
|
+
const collection = collectionById.get(token.collection_id);
|
|
4634
|
+
if (collection === void 0) continue;
|
|
4635
|
+
pin(token.id, {
|
|
4636
|
+
unit: "px",
|
|
4637
|
+
via: "alias-scope",
|
|
4638
|
+
source: dtcgPathOf(collection.name, token.name),
|
|
4639
|
+
reason: scopes[0]
|
|
4640
|
+
}, false);
|
|
2196
4641
|
}
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
`;
|
|
2203
|
-
const files = {};
|
|
2204
|
-
for (const name of Object.keys(out.files).sort(compareCodeUnits)) files[name] = text(out.files[name]);
|
|
2205
|
-
files["resolver.json"] = text({
|
|
2206
|
-
...out.resolver,
|
|
2207
|
-
$extensions: { "com.spec-layer": out.extension }
|
|
2208
|
-
});
|
|
2209
|
-
files["spec-layer.meta.json"] = text(out.meta);
|
|
2210
|
-
files["report.json"] = text(out.report);
|
|
2211
|
-
return files;
|
|
4642
|
+
for (const token of artifact.tokens) {
|
|
4643
|
+
const found = lengthUse.get(token.id);
|
|
4644
|
+
if (found !== void 0) pin(token.id, found, true);
|
|
4645
|
+
}
|
|
4646
|
+
return evidence;
|
|
2212
4647
|
}
|
|
2213
4648
|
|
|
2214
4649
|
// ../extractor/src/v5/outputs/naming.ts
|
|
@@ -2313,12 +4748,12 @@ function acceptCssDeclared(declared) {
|
|
|
2313
4748
|
if (BARE_IDENT.test(declared)) return `--${declared}`;
|
|
2314
4749
|
return null;
|
|
2315
4750
|
}
|
|
2316
|
-
var
|
|
4751
|
+
var asRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
|
|
2317
4752
|
function collectLeaves(tree, prefix, out) {
|
|
2318
|
-
const node =
|
|
4753
|
+
const node = asRecord2(tree);
|
|
2319
4754
|
if (!node) return;
|
|
2320
4755
|
if ("$value" in node) {
|
|
2321
|
-
const ours =
|
|
4756
|
+
const ours = asRecord2(asRecord2(node.$extensions)?.[EXT]);
|
|
2322
4757
|
out.push({
|
|
2323
4758
|
path: prefix.join("."),
|
|
2324
4759
|
type: typeof node.$type === "string" ? node.$type : "",
|
|
@@ -2334,7 +4769,7 @@ function collectLeaves(tree, prefix, out) {
|
|
|
2334
4769
|
}
|
|
2335
4770
|
var unpointer = (s) => s.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
2336
4771
|
var refFile = (src) => {
|
|
2337
|
-
const r =
|
|
4772
|
+
const r = asRecord2(src);
|
|
2338
4773
|
return r && typeof r.$ref === "string" ? r.$ref : null;
|
|
2339
4774
|
};
|
|
2340
4775
|
function sourcesOf(resolver) {
|
|
@@ -2409,7 +4844,7 @@ function cssValue(ctx, type, value, property) {
|
|
|
2409
4844
|
switch (type) {
|
|
2410
4845
|
case "color": {
|
|
2411
4846
|
if (typeof value === "string") return value;
|
|
2412
|
-
const c =
|
|
4847
|
+
const c = asRecord2(value);
|
|
2413
4848
|
if (c && typeof c.hex === "string" && typeof c.alpha === "number") {
|
|
2414
4849
|
if (c.alpha === 1) return c.hex;
|
|
2415
4850
|
const [r, g, b] = hexChannels(c.hex);
|
|
@@ -2420,14 +4855,26 @@ function cssValue(ctx, type, value, property) {
|
|
|
2420
4855
|
case "dimension":
|
|
2421
4856
|
case "duration": {
|
|
2422
4857
|
if (typeof value === "string") return value;
|
|
2423
|
-
const d =
|
|
4858
|
+
const d = asRecord2(value);
|
|
2424
4859
|
if (d && typeof d.value === "number" && typeof d.unit === "string") return `${d.value}${d.unit}`;
|
|
2425
4860
|
break;
|
|
2426
4861
|
}
|
|
2427
|
-
case "number":
|
|
2428
4862
|
case "fontWeight":
|
|
2429
4863
|
if (typeof value === "number") return String(value);
|
|
2430
4864
|
break;
|
|
4865
|
+
case "number":
|
|
4866
|
+
if (typeof value === "number") {
|
|
4867
|
+
if (property !== "lineHeight") {
|
|
4868
|
+
report(ctx, {
|
|
4869
|
+
code: "unitless_number",
|
|
4870
|
+
severity: "warning",
|
|
4871
|
+
message: ctx.statesNumber ? "This token has no unit, because its Figma scopes state it is a unitless number. CSS reads it as a number, not a length, which is what those scopes ask for. Do not use it where a length is expected." : "This token has no unit, because its Figma variable states none. CSS reads it as a number, not a length. Narrow the variable's scopes in Figma and pull again.",
|
|
4872
|
+
details: { value, ...member }
|
|
4873
|
+
});
|
|
4874
|
+
}
|
|
4875
|
+
return String(value);
|
|
4876
|
+
}
|
|
4877
|
+
break;
|
|
2431
4878
|
case "cubicBezier":
|
|
2432
4879
|
if (Array.isArray(value) && value.length === 4 && value.every((n) => typeof n === "number")) {
|
|
2433
4880
|
return `cubic-bezier(${value.join(", ")})`;
|
|
@@ -2470,7 +4917,7 @@ function typographyMemberKeys(value, ext) {
|
|
|
2470
4917
|
}
|
|
2471
4918
|
for (const key of ["lineHeight", "letterSpacing"]) {
|
|
2472
4919
|
if (key in value) continue;
|
|
2473
|
-
const d =
|
|
4920
|
+
const d = asRecord2(ext[key]);
|
|
2474
4921
|
if (d && typeof d.value === "number" && typeof d.unit === "string") keys.push(key);
|
|
2475
4922
|
}
|
|
2476
4923
|
for (const [key, extKey] of TEXT_MEMBERS) {
|
|
@@ -2487,7 +4934,7 @@ function converted(ctx, property, from, to) {
|
|
|
2487
4934
|
});
|
|
2488
4935
|
}
|
|
2489
4936
|
function extensionDimension(ctx, ext, key, name, percentTo) {
|
|
2490
|
-
const d =
|
|
4937
|
+
const d = asRecord2(ext[key]);
|
|
2491
4938
|
if (!d || typeof d.value !== "number" || typeof d.unit !== "string") return null;
|
|
2492
4939
|
if (d.unit === "%") {
|
|
2493
4940
|
const to = percentTo(canonicalNumber(d.value / 100));
|
|
@@ -2499,7 +4946,7 @@ function extensionDimension(ctx, ext, key, name, percentTo) {
|
|
|
2499
4946
|
function typographyDecls(ctx, leaf, names) {
|
|
2500
4947
|
const decls = [];
|
|
2501
4948
|
const declaredPaths = [];
|
|
2502
|
-
const value =
|
|
4949
|
+
const value = asRecord2(leaf.value) ?? {};
|
|
2503
4950
|
const ext = leaf.ext ?? {};
|
|
2504
4951
|
const nameFor = (key) => names.get(`${leaf.path}.${key}`);
|
|
2505
4952
|
for (const [key, type] of TYPOGRAPHY_MEMBERS2) {
|
|
@@ -2532,12 +4979,12 @@ function typographyDecls(ctx, leaf, names) {
|
|
|
2532
4979
|
}
|
|
2533
4980
|
}
|
|
2534
4981
|
}
|
|
2535
|
-
for (const [key, extKey,
|
|
4982
|
+
for (const [key, extKey, table2] of TEXT_MEMBERS) {
|
|
2536
4983
|
const raw = ext[extKey];
|
|
2537
4984
|
if (typeof raw !== "string") continue;
|
|
2538
4985
|
const name = nameFor(key);
|
|
2539
4986
|
if (name === void 0) continue;
|
|
2540
|
-
const css =
|
|
4987
|
+
const css = table2[raw];
|
|
2541
4988
|
if (css !== void 0) {
|
|
2542
4989
|
decls.push(`${name}: ${css};`);
|
|
2543
4990
|
declaredPaths.push(`${leaf.path}.${key}`);
|
|
@@ -2568,7 +5015,7 @@ function shadowDecl(ctx, leaf, name) {
|
|
|
2568
5015
|
if (layers.length === 0) return omit("no_visible_shadow", "The style has no visible shadow, so no box-shadow was written.");
|
|
2569
5016
|
const parts = [];
|
|
2570
5017
|
for (const layer of layers) {
|
|
2571
|
-
const l =
|
|
5018
|
+
const l = asRecord2(layer);
|
|
2572
5019
|
if (!l) return omit("layer_not_an_object", "A shadow layer is not an object; the style was omitted.");
|
|
2573
5020
|
const members = [];
|
|
2574
5021
|
for (const [key, type] of SHADOW_MEMBERS) {
|
|
@@ -2582,22 +5029,59 @@ function shadowDecl(ctx, leaf, name) {
|
|
|
2582
5029
|
return `${name}: ${parts.join(", ")};`;
|
|
2583
5030
|
}
|
|
2584
5031
|
var commentSafe = (text) => text.replace(/\*\//g, "* /").replace(/[\r\n]+/g, " ");
|
|
2585
|
-
function headerText(header, nameCase) {
|
|
2586
|
-
|
|
2587
|
-
|
|
5032
|
+
function headerText(header, nameCase, unitlessCount = 0, derivedCount = 0) {
|
|
5033
|
+
const lines = [
|
|
5034
|
+
`${CSS_HEADER_PREFIX} from library ${commentSafe(header.libraryId)}, foundation ${header.contentHash}, ${header.platform}/${header.format}/${nameCase}.`,
|
|
5035
|
+
" Do not edit. Change the design in Figma, republish, and run spec-layer pull."
|
|
5036
|
+
];
|
|
5037
|
+
if (unitlessCount > 0) {
|
|
5038
|
+
const reportFile = `${header.platform}-${header.format}.report.json`;
|
|
5039
|
+
if (unitlessCount === 1) {
|
|
5040
|
+
lines.push(
|
|
5041
|
+
" 1 property in this file has no unit, because its Figma variable states none.",
|
|
5042
|
+
` CSS reads it as a number, not a length. See outputs/${reportFile} under your pull's output directory, or narrow the variable's scopes in Figma.`
|
|
5043
|
+
);
|
|
5044
|
+
} else {
|
|
5045
|
+
lines.push(
|
|
5046
|
+
` ${unitlessCount} properties in this file have no unit, because their Figma variables state none.`,
|
|
5047
|
+
` CSS reads them as numbers, not lengths. See outputs/${reportFile} under your pull's output directory, or narrow the variables' scopes in Figma.`
|
|
5048
|
+
);
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5051
|
+
if (derivedCount > 0) {
|
|
5052
|
+
lines.push(
|
|
5053
|
+
derivedCount === 1 ? " 1 property in this file has a unit its own Figma variable does not state, taken from how the library uses the token." : ` ${derivedCount} properties in this file have a unit their own Figma variables do not state, taken from how the library uses those tokens.`,
|
|
5054
|
+
` See tokens/report.json under your pull's output directory for what pinned ${derivedCount === 1 ? "it" : "each one"}.`
|
|
5055
|
+
);
|
|
5056
|
+
}
|
|
5057
|
+
return `${lines.join("\n")} */`;
|
|
2588
5058
|
}
|
|
2589
|
-
function emitPass(sources, leavesByFile, names, alive, root, template, modes) {
|
|
5059
|
+
function emitPass(sources, leavesByFile, names, alive, root, template, modes, facts) {
|
|
2590
5060
|
const entries = [];
|
|
2591
5061
|
const declared = /* @__PURE__ */ new Set();
|
|
2592
5062
|
const firstFile = /* @__PURE__ */ new Map();
|
|
2593
5063
|
const blocks = /* @__PURE__ */ new Map();
|
|
5064
|
+
const unitlessByFile = /* @__PURE__ */ new Map();
|
|
5065
|
+
const derivedByFile = /* @__PURE__ */ new Map();
|
|
5066
|
+
const note = (by, file, path) => {
|
|
5067
|
+
const set = by.get(file) ?? /* @__PURE__ */ new Set();
|
|
5068
|
+
set.add(path);
|
|
5069
|
+
by.set(file, set);
|
|
5070
|
+
};
|
|
2594
5071
|
for (const s of sources) {
|
|
2595
5072
|
const perCollection = modes?.[s.collection];
|
|
2596
5073
|
const selector = s.isDefault ? root : (perCollection ?? template).replace(/\{mode\}/g, modeSlug(s.file)).replace(/\{collection\}/g, collectionSlug(s.file));
|
|
2597
5074
|
const decls = [];
|
|
2598
5075
|
const declaredHere = [];
|
|
2599
5076
|
for (const leaf of leavesByFile.get(s.file) ?? []) {
|
|
2600
|
-
const ctx = {
|
|
5077
|
+
const ctx = {
|
|
5078
|
+
names,
|
|
5079
|
+
alive,
|
|
5080
|
+
report: entries,
|
|
5081
|
+
path: leaf.path,
|
|
5082
|
+
statesNumber: facts.statesNumber.has(leaf.path),
|
|
5083
|
+
...s.mode !== null ? { mode: s.mode } : {}
|
|
5084
|
+
};
|
|
2601
5085
|
if (leaf.type === "typography") {
|
|
2602
5086
|
const t = typographyDecls(ctx, leaf, names);
|
|
2603
5087
|
decls.push(...t.decls);
|
|
@@ -2616,12 +5100,17 @@ function emitPass(sources, leavesByFile, names, alive, root, template, modes) {
|
|
|
2616
5100
|
declaredHere.push(leaf.path);
|
|
2617
5101
|
}
|
|
2618
5102
|
} else {
|
|
5103
|
+
const before = entries.length;
|
|
2619
5104
|
const v = cssValue(ctx, leaf.type, leaf.value);
|
|
2620
5105
|
if (v !== null) {
|
|
2621
5106
|
decls.push(`${name}: ${v};`);
|
|
2622
5107
|
declared.add(leaf.path);
|
|
2623
5108
|
declaredHere.push(leaf.path);
|
|
2624
5109
|
}
|
|
5110
|
+
if (entries.length > before && entries[entries.length - 1].code === "unitless_number" && !ctx.statesNumber) {
|
|
5111
|
+
note(unitlessByFile, s.file, leaf.path);
|
|
5112
|
+
}
|
|
5113
|
+
if (v !== null && facts.derived.has(leaf.path)) note(derivedByFile, s.file, leaf.path);
|
|
2625
5114
|
}
|
|
2626
5115
|
}
|
|
2627
5116
|
}
|
|
@@ -2632,7 +5121,7 @@ function emitPass(sources, leavesByFile, names, alive, root, template, modes) {
|
|
|
2632
5121
|
if (existing) existing.decls.push(...decls);
|
|
2633
5122
|
else blocks.set(s.file, { selector, comment, decls });
|
|
2634
5123
|
}
|
|
2635
|
-
return { blocks, entries, declared, firstFile };
|
|
5124
|
+
return { blocks, entries, declared, firstFile, unitlessByFile, derivedByFile };
|
|
2636
5125
|
}
|
|
2637
5126
|
function cssOutput(exp, header, options = {}) {
|
|
2638
5127
|
const nameCase = options.case ?? CSS_DEFAULTS.case;
|
|
@@ -2651,7 +5140,7 @@ function cssOutput(exp, header, options = {}) {
|
|
|
2651
5140
|
for (const leaves of leavesByFile.values()) {
|
|
2652
5141
|
for (const leaf of leaves) {
|
|
2653
5142
|
if (leaf.type === "typography") {
|
|
2654
|
-
const value =
|
|
5143
|
+
const value = asRecord2(leaf.value) ?? {};
|
|
2655
5144
|
const ext = leaf.ext ?? {};
|
|
2656
5145
|
for (const key of typographyMemberKeys(value, ext)) candidatePaths.add(`${leaf.path}.${key}`);
|
|
2657
5146
|
} else {
|
|
@@ -2666,7 +5155,11 @@ function cssOutput(exp, header, options = {}) {
|
|
|
2666
5155
|
nameCase
|
|
2667
5156
|
});
|
|
2668
5157
|
const names = resolved2.names;
|
|
2669
|
-
const
|
|
5158
|
+
const facts = {
|
|
5159
|
+
statesNumber: new Set(Object.entries(exp.meta).filter(([, entry2]) => scopesStateNumber(entry2.scopes)).map(([path]) => path)),
|
|
5160
|
+
derived: new Set(exp.report.filter((entry2) => entry2.code === "unit_derived_from_usage").map((entry2) => entry2.path))
|
|
5161
|
+
};
|
|
5162
|
+
const emit = (alive2) => emitPass(sources, leavesByFile, names, alive2, root, template, options.modes, facts);
|
|
2670
5163
|
let alive = new Set(names.keys());
|
|
2671
5164
|
let pass = emit(alive);
|
|
2672
5165
|
for (; ; ) {
|
|
@@ -2693,11 +5186,16 @@ function cssOutput(exp, header, options = {}) {
|
|
|
2693
5186
|
});
|
|
2694
5187
|
}
|
|
2695
5188
|
}
|
|
2696
|
-
const head = headerText(header, nameCase);
|
|
2697
5189
|
const files = {};
|
|
2698
5190
|
const imports = [];
|
|
2699
5191
|
for (const [source, block] of pass.blocks) {
|
|
2700
5192
|
const name = fileNames.get(source);
|
|
5193
|
+
const head = headerText(
|
|
5194
|
+
header,
|
|
5195
|
+
nameCase,
|
|
5196
|
+
pass.unitlessByFile.get(source)?.size ?? 0,
|
|
5197
|
+
pass.derivedByFile.get(source)?.size ?? 0
|
|
5198
|
+
);
|
|
2701
5199
|
files[name] = `${head}
|
|
2702
5200
|
|
|
2703
5201
|
${block.selector} {
|
|
@@ -2707,36 +5205,48 @@ ${block.decls.map((d) => ` ${d}`).join("\n")}
|
|
|
2707
5205
|
`;
|
|
2708
5206
|
imports.push(block.comment, `@import "./${name}";`);
|
|
2709
5207
|
}
|
|
2710
|
-
if (imports.length > 0) files[CSS_INDEX_FILE] = `${
|
|
5208
|
+
if (imports.length > 0) files[CSS_INDEX_FILE] = `${headerText(header, nameCase)}
|
|
2711
5209
|
|
|
2712
5210
|
${imports.join("\n")}
|
|
2713
5211
|
`;
|
|
2714
5212
|
return { files, map, report: sortReport(entries) };
|
|
2715
5213
|
}
|
|
2716
5214
|
|
|
2717
|
-
// ../extractor/src/v5/componentContext.ts
|
|
2718
|
-
var import_js_sha2564 = __toESM(require_sha256(), 1);
|
|
2719
|
-
|
|
2720
5215
|
// ../extractor/src/libraryBundle.ts
|
|
2721
5216
|
var LIBRARY_BUNDLE_SCHEMA = "spec-layer-library-bundle";
|
|
2722
5217
|
var LibraryBundleError = class extends Error {
|
|
2723
|
-
constructor(
|
|
5218
|
+
constructor(code3, message) {
|
|
2724
5219
|
super(message);
|
|
2725
5220
|
this.name = "LibraryBundleError";
|
|
2726
|
-
this.code =
|
|
5221
|
+
this.code = code3;
|
|
2727
5222
|
}
|
|
2728
5223
|
};
|
|
2729
|
-
var
|
|
5224
|
+
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2730
5225
|
function hasContentHash(artifact) {
|
|
2731
|
-
if (!
|
|
5226
|
+
if (!isRecord3(artifact) || !isRecord3(artifact.spec_layer)) return false;
|
|
2732
5227
|
const exp = artifact.spec_layer.export;
|
|
2733
|
-
return
|
|
5228
|
+
return isRecord3(exp) && typeof exp.content_hash === "string";
|
|
5229
|
+
}
|
|
5230
|
+
function readVariants(value) {
|
|
5231
|
+
if (!Array.isArray(value)) return void 0;
|
|
5232
|
+
const out = [];
|
|
5233
|
+
for (const item of value) {
|
|
5234
|
+
if (!isRecord3(item) || typeof item.name !== "string" || !isRecord3(item.values)) return void 0;
|
|
5235
|
+
const values = {};
|
|
5236
|
+
for (const [axis, v] of Object.entries(item.values)) {
|
|
5237
|
+
if (typeof v !== "string") return void 0;
|
|
5238
|
+
values[axis] = v;
|
|
5239
|
+
}
|
|
5240
|
+
out.push({ name: item.name, values });
|
|
5241
|
+
}
|
|
5242
|
+
return out;
|
|
2734
5243
|
}
|
|
2735
5244
|
function entry(v, where) {
|
|
2736
|
-
if (!
|
|
5245
|
+
if (!isRecord3(v) || typeof v.name !== "string" || typeof v.ai !== "string" || !hasContentHash(v.artifact)) {
|
|
2737
5246
|
throw new LibraryBundleError("malformed", `The ${where} entry in this bundle is malformed.`);
|
|
2738
5247
|
}
|
|
2739
|
-
|
|
5248
|
+
const variants = readVariants(v.variants);
|
|
5249
|
+
return { name: v.name, ai: v.ai, artifact: v.artifact, ...variants ? { variants } : {} };
|
|
2740
5250
|
}
|
|
2741
5251
|
function supportedVersion(version) {
|
|
2742
5252
|
return typeof version === "string" && /^1\.\d+\.\d+$/.test(version);
|
|
@@ -2750,7 +5260,7 @@ function parseLibraryBundle(input) {
|
|
|
2750
5260
|
throw new LibraryBundleError("not_json", "This is not valid JSON.");
|
|
2751
5261
|
}
|
|
2752
5262
|
}
|
|
2753
|
-
if (!
|
|
5263
|
+
if (!isRecord3(parsed) || parsed.schema !== LIBRARY_BUNDLE_SCHEMA) {
|
|
2754
5264
|
throw new LibraryBundleError("not_bundle", "This is not a Spec Layer library bundle.");
|
|
2755
5265
|
}
|
|
2756
5266
|
if (!supportedVersion(parsed.version)) {
|
|
@@ -2766,7 +5276,7 @@ function parseLibraryBundle(input) {
|
|
|
2766
5276
|
const foundationRaw = parsed.foundation ?? null;
|
|
2767
5277
|
let foundation = null;
|
|
2768
5278
|
if (foundationRaw !== null) {
|
|
2769
|
-
if (!
|
|
5279
|
+
if (!isRecord3(foundationRaw) || typeof foundationRaw.ai !== "string" || !hasContentHash(foundationRaw.artifact)) {
|
|
2770
5280
|
throw new LibraryBundleError("malformed", "The foundation entry in this bundle is malformed.");
|
|
2771
5281
|
}
|
|
2772
5282
|
foundation = { ai: foundationRaw.ai, artifact: foundationRaw.artifact };
|
|
@@ -2785,6 +5295,39 @@ function parseLibraryBundle(input) {
|
|
|
2785
5295
|
// ../extractor/src/libraryBundleHash.ts
|
|
2786
5296
|
var import_js_sha2565 = __toESM(require_sha256(), 1);
|
|
2787
5297
|
|
|
5298
|
+
// ../extractor/src/componentSlugs.ts
|
|
5299
|
+
var DASH = 45;
|
|
5300
|
+
function slugify(name) {
|
|
5301
|
+
const collapsed = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
5302
|
+
let start = 0;
|
|
5303
|
+
let end = collapsed.length;
|
|
5304
|
+
while (start < end && collapsed.charCodeAt(start) === DASH) start += 1;
|
|
5305
|
+
while (end > start && collapsed.charCodeAt(end - 1) === DASH) end -= 1;
|
|
5306
|
+
const slug2 = collapsed.slice(start, end);
|
|
5307
|
+
return slug2 || "component";
|
|
5308
|
+
}
|
|
5309
|
+
function componentSlugs(names) {
|
|
5310
|
+
const usedSlugs = /* @__PURE__ */ new Set();
|
|
5311
|
+
const nextSuffix = /* @__PURE__ */ new Map();
|
|
5312
|
+
return names.map((name) => {
|
|
5313
|
+
const base = slugify(name);
|
|
5314
|
+
let slug2 = base;
|
|
5315
|
+
if (usedSlugs.has(slug2)) {
|
|
5316
|
+
let n = (nextSuffix.get(base) ?? 1) + 1;
|
|
5317
|
+
slug2 = `${base}-${n}`;
|
|
5318
|
+
while (usedSlugs.has(slug2)) {
|
|
5319
|
+
n += 1;
|
|
5320
|
+
slug2 = `${base}-${n}`;
|
|
5321
|
+
}
|
|
5322
|
+
nextSuffix.set(base, n);
|
|
5323
|
+
} else {
|
|
5324
|
+
nextSuffix.set(base, 1);
|
|
5325
|
+
}
|
|
5326
|
+
usedSlugs.add(slug2);
|
|
5327
|
+
return slug2;
|
|
5328
|
+
});
|
|
5329
|
+
}
|
|
5330
|
+
|
|
2788
5331
|
// src/bundle.ts
|
|
2789
5332
|
function parseBundle(raw) {
|
|
2790
5333
|
try {
|
|
@@ -2851,8 +5394,7 @@ var CODE_SYNTAX_KEY = {
|
|
|
2851
5394
|
var PLATFORMS = ["web", "ios", "android", "flutter"];
|
|
2852
5395
|
var AGENT_HOSTS = ["claude", "cursor", "copilot", "windsurf", "gemini", "agents-md"];
|
|
2853
5396
|
var uniq = (xs) => [...new Set(xs)];
|
|
2854
|
-
function
|
|
2855
|
-
const path = join2(cwd, "package.json");
|
|
5397
|
+
function readJsonObject(path) {
|
|
2856
5398
|
if (!existsSync2(path)) return null;
|
|
2857
5399
|
let parsed;
|
|
2858
5400
|
try {
|
|
@@ -2861,7 +5403,11 @@ function readPackageJson(cwd) {
|
|
|
2861
5403
|
return null;
|
|
2862
5404
|
}
|
|
2863
5405
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
2864
|
-
|
|
5406
|
+
return parsed;
|
|
5407
|
+
}
|
|
5408
|
+
function readPackageJson(cwd) {
|
|
5409
|
+
const record = readJsonObject(join2(cwd, "package.json"));
|
|
5410
|
+
if (!record) return null;
|
|
2865
5411
|
const deps = {};
|
|
2866
5412
|
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
2867
5413
|
const block = record[field];
|
|
@@ -2987,6 +5533,97 @@ function isPlatform(value) {
|
|
|
2987
5533
|
function isAgentHost(value) {
|
|
2988
5534
|
return AGENT_HOSTS.includes(value);
|
|
2989
5535
|
}
|
|
5536
|
+
function fontPackageSlug(family) {
|
|
5537
|
+
return family.toLowerCase().split(" ").join("-");
|
|
5538
|
+
}
|
|
5539
|
+
function dependencyNamesFamily(dependencyName, slug2) {
|
|
5540
|
+
const lower = dependencyName.toLowerCase();
|
|
5541
|
+
return lower === slug2 || lower.endsWith(`/${slug2}`);
|
|
5542
|
+
}
|
|
5543
|
+
function cssNamesFamily(cssText, family) {
|
|
5544
|
+
if (!cssText.includes("@font-face")) return false;
|
|
5545
|
+
return cssText.includes(`"${family}"`) || cssText.includes(`'${family}'`);
|
|
5546
|
+
}
|
|
5547
|
+
function googleFontsLinkNamesFamily(htmlText, family) {
|
|
5548
|
+
for (const quoted of htmlText.split('"')) {
|
|
5549
|
+
for (const fragment of quoted.split("'")) {
|
|
5550
|
+
const trimmed = fragment.trim();
|
|
5551
|
+
const candidate = trimmed.startsWith("//") ? `https:${trimmed}` : trimmed;
|
|
5552
|
+
if (!candidate.startsWith("http")) continue;
|
|
5553
|
+
let url;
|
|
5554
|
+
try {
|
|
5555
|
+
url = new URL(candidate);
|
|
5556
|
+
} catch {
|
|
5557
|
+
continue;
|
|
5558
|
+
}
|
|
5559
|
+
if (url.hostname !== "fonts.googleapis.com") continue;
|
|
5560
|
+
for (const value of url.searchParams.getAll("family")) {
|
|
5561
|
+
if (value === family || value.startsWith(`${family}:`)) return true;
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
return false;
|
|
5566
|
+
}
|
|
5567
|
+
function missingFontSources(families, repo) {
|
|
5568
|
+
const dependencyNames = Object.keys({ ...repo.packageJson.dependencies, ...repo.packageJson.devDependencies });
|
|
5569
|
+
return families.filter((family) => {
|
|
5570
|
+
const slug2 = fontPackageSlug(family);
|
|
5571
|
+
if (dependencyNames.some((name) => dependencyNamesFamily(name, slug2))) return false;
|
|
5572
|
+
if (cssNamesFamily(repo.cssText, family)) return false;
|
|
5573
|
+
if (googleFontsLinkNamesFamily(repo.htmlText, family)) return false;
|
|
5574
|
+
return true;
|
|
5575
|
+
});
|
|
5576
|
+
}
|
|
5577
|
+
function dependencyField(record, field) {
|
|
5578
|
+
const block = record?.[field];
|
|
5579
|
+
if (typeof block !== "object" || block === null) return {};
|
|
5580
|
+
const out = {};
|
|
5581
|
+
for (const [name, range] of Object.entries(block)) {
|
|
5582
|
+
if (typeof range === "string") out[name] = range;
|
|
5583
|
+
}
|
|
5584
|
+
return out;
|
|
5585
|
+
}
|
|
5586
|
+
function readRootCssText(cwd) {
|
|
5587
|
+
let names = [];
|
|
5588
|
+
try {
|
|
5589
|
+
names = readdirSync(cwd);
|
|
5590
|
+
} catch {
|
|
5591
|
+
names = [];
|
|
5592
|
+
}
|
|
5593
|
+
const chunks = [];
|
|
5594
|
+
for (const name of names) {
|
|
5595
|
+
if (!name.endsWith(".css")) continue;
|
|
5596
|
+
try {
|
|
5597
|
+
chunks.push(readFileSync2(join2(cwd, name), "utf8"));
|
|
5598
|
+
} catch {
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
return chunks.join("\n");
|
|
5602
|
+
}
|
|
5603
|
+
function readEntryHtmlText(cwd) {
|
|
5604
|
+
const chunks = [];
|
|
5605
|
+
for (const path of [join2(cwd, "index.html"), join2(cwd, "public", "index.html")]) {
|
|
5606
|
+
try {
|
|
5607
|
+
if (existsSync2(path)) chunks.push(readFileSync2(path, "utf8"));
|
|
5608
|
+
} catch {
|
|
5609
|
+
}
|
|
5610
|
+
}
|
|
5611
|
+
return chunks.join("\n");
|
|
5612
|
+
}
|
|
5613
|
+
function readFontRepoSignals(cwd) {
|
|
5614
|
+
const record = readJsonObject(join2(cwd, "package.json"));
|
|
5615
|
+
return {
|
|
5616
|
+
packageJson: {
|
|
5617
|
+
dependencies: dependencyField(record, "dependencies"),
|
|
5618
|
+
devDependencies: dependencyField(record, "devDependencies")
|
|
5619
|
+
},
|
|
5620
|
+
cssText: readRootCssText(cwd),
|
|
5621
|
+
htmlText: readEntryHtmlText(cwd)
|
|
5622
|
+
};
|
|
5623
|
+
}
|
|
5624
|
+
function missingFontSourcesInRepo(families, cwd) {
|
|
5625
|
+
return missingFontSources(families, readFontRepoSignals(cwd));
|
|
5626
|
+
}
|
|
2990
5627
|
|
|
2991
5628
|
// src/outputs.ts
|
|
2992
5629
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -3011,15 +5648,17 @@ var inside = (parent, child) => {
|
|
|
3011
5648
|
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
3012
5649
|
};
|
|
3013
5650
|
var isDotfile = (name) => name.startsWith(".");
|
|
5651
|
+
var markerList = (marker) => typeof marker === "string" ? [marker] : marker;
|
|
3014
5652
|
function carriesMarker(abs, marker) {
|
|
3015
|
-
const
|
|
3016
|
-
|
|
5653
|
+
const markers = markerList(marker);
|
|
5654
|
+
if (markers.length === 0) return false;
|
|
5655
|
+
const wantBytes = Math.max(...markers.map((m) => Buffer.byteLength(m, "utf8") + (m.match(/\n/g) ?? []).length));
|
|
3017
5656
|
const fd = openSync(abs, "r");
|
|
3018
5657
|
try {
|
|
3019
5658
|
const buf = Buffer.alloc(wantBytes);
|
|
3020
5659
|
const read = readSync(fd, buf, 0, wantBytes, 0);
|
|
3021
5660
|
const text = buf.subarray(0, read).toString("utf8").replace(/\r\n/g, "\n");
|
|
3022
|
-
return text.startsWith(
|
|
5661
|
+
return markers.some((m) => text.startsWith(m));
|
|
3023
5662
|
} finally {
|
|
3024
5663
|
closeSync(fd);
|
|
3025
5664
|
}
|
|
@@ -3096,18 +5735,18 @@ function parseOutput(value, index) {
|
|
|
3096
5735
|
}
|
|
3097
5736
|
const spec = specOf(r.platform, r.format);
|
|
3098
5737
|
if (!spec) throw new Error(`${at}: unknown platform/format "${r.platform}/${r.format}". Known: ${knownFormats()}.`);
|
|
3099
|
-
const
|
|
5738
|
+
const str2 = (key) => {
|
|
3100
5739
|
if (r[key] !== void 0 && typeof r[key] !== "string") throw new Error(`${at} "${key}" must be a string.`);
|
|
3101
5740
|
return r[key];
|
|
3102
5741
|
};
|
|
3103
|
-
const path =
|
|
5742
|
+
const path = str2("path") ?? spec.defaultPath;
|
|
3104
5743
|
if (path === null) throw new Error(`${at} needs "path": ${spec.platform} has no default location.`);
|
|
3105
|
-
const nameCase =
|
|
5744
|
+
const nameCase = str2("case");
|
|
3106
5745
|
if (nameCase !== void 0 && !NAME_CASES.includes(nameCase)) {
|
|
3107
5746
|
throw new Error(`${at} "case" takes ${NAME_CASES.join(", ")}.`);
|
|
3108
5747
|
}
|
|
3109
|
-
const root =
|
|
3110
|
-
const modeSelector =
|
|
5748
|
+
const root = str2("root");
|
|
5749
|
+
const modeSelector = str2("modeSelector");
|
|
3111
5750
|
let modes;
|
|
3112
5751
|
if (r.modes !== void 0) {
|
|
3113
5752
|
const m = r.modes;
|
|
@@ -3163,6 +5802,11 @@ var DEFAULT_API = "https://api.spec-layer.com";
|
|
|
3163
5802
|
var DEFAULT_OUT_DIR = ".speclayer";
|
|
3164
5803
|
var DEFAULT_COMPONENT_SPECS_DIR = "component-specs";
|
|
3165
5804
|
var CONFIG_NAME = "speclayer.json";
|
|
5805
|
+
var COMPONENT_FORMATS = ["yaml", "md"];
|
|
5806
|
+
var DEFAULT_COMPONENT_FORMAT = "yaml";
|
|
5807
|
+
function isComponentFormat(value) {
|
|
5808
|
+
return typeof value === "string" && COMPONENT_FORMATS.includes(value);
|
|
5809
|
+
}
|
|
3166
5810
|
var invalidConfig = () => new Error(`${CONFIG_NAME} is not valid JSON. Fix or delete it, then retry.`);
|
|
3167
5811
|
function parseInclude(value) {
|
|
3168
5812
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalidConfig();
|
|
@@ -3201,6 +5845,10 @@ function parseComponentSpecsDir(value) {
|
|
|
3201
5845
|
if (dir.length === 0) throw new Error('speclayer.json "componentSpecsDir" must be a non-empty string.');
|
|
3202
5846
|
return dir;
|
|
3203
5847
|
}
|
|
5848
|
+
function parseComponentSpecsFormat(value) {
|
|
5849
|
+
if (!isComponentFormat(value)) throw new Error('speclayer.json "componentSpecsFormat" must be "yaml" or "md".');
|
|
5850
|
+
return value;
|
|
5851
|
+
}
|
|
3204
5852
|
function parsePlatforms(value) {
|
|
3205
5853
|
if (!Array.isArray(value) || !value.every((p) => typeof p === "string" && isPlatform(p))) {
|
|
3206
5854
|
throw new Error(`speclayer.json "platforms" must be an array of ${PLATFORMS.join(", ")}.`);
|
|
@@ -3235,6 +5883,7 @@ function readConfig(cwd) {
|
|
|
3235
5883
|
...typeof record.libraryId === "string" ? { libraryId: record.libraryId } : {},
|
|
3236
5884
|
...typeof record.outDir === "string" ? { outDir: record.outDir } : {},
|
|
3237
5885
|
...record.componentSpecsDir !== void 0 ? { componentSpecsDir: parseComponentSpecsDir(record.componentSpecsDir) } : {},
|
|
5886
|
+
...record.componentSpecsFormat !== void 0 ? { componentSpecsFormat: parseComponentSpecsFormat(record.componentSpecsFormat) } : {},
|
|
3238
5887
|
...record.include !== void 0 ? { include: parseInclude(record.include) } : {},
|
|
3239
5888
|
...record.dtcg !== void 0 ? { dtcg: parseDtcg(record.dtcg) } : {},
|
|
3240
5889
|
...record.platforms !== void 0 ? { platforms: parsePlatforms(record.platforms) } : {},
|
|
@@ -3246,6 +5895,7 @@ function writeConfig(cwd, config) {
|
|
|
3246
5895
|
libraryId: config.libraryId,
|
|
3247
5896
|
outDir: config.outDir,
|
|
3248
5897
|
...config.componentSpecsDir ? { componentSpecsDir: config.componentSpecsDir } : {},
|
|
5898
|
+
...config.componentSpecsFormat ? { componentSpecsFormat: config.componentSpecsFormat } : {},
|
|
3249
5899
|
...config.include ? { include: config.include } : {},
|
|
3250
5900
|
...config.dtcg ? { dtcg: config.dtcg } : {},
|
|
3251
5901
|
...config.platforms && config.platforms.length > 0 ? { platforms: config.platforms } : {},
|
|
@@ -3275,6 +5925,7 @@ function resolveOptions(cwd, flags, env, manifestLibraryId) {
|
|
|
3275
5925
|
// A trailing slash would build "//v1/..." paths the proxy router 404s on.
|
|
3276
5926
|
api: (flags.api ?? env.SPEC_LAYER_API ?? DEFAULT_API).replace(/\/+$/, ""),
|
|
3277
5927
|
key: supplied ?? storedKey,
|
|
5928
|
+
...config?.componentSpecsFormat ? { componentSpecsFormat: config.componentSpecsFormat } : {},
|
|
3278
5929
|
...config?.include ? { include: config.include } : {},
|
|
3279
5930
|
...config?.dtcg ? { dtcg: config.dtcg } : {},
|
|
3280
5931
|
...config?.platforms ? { platforms: config.platforms } : {},
|
|
@@ -3298,7 +5949,8 @@ async function fetchBundle(opts) {
|
|
|
3298
5949
|
} catch {
|
|
3299
5950
|
return { kind: "error", message: `Could not reach ${opts.api}.` };
|
|
3300
5951
|
}
|
|
3301
|
-
|
|
5952
|
+
const version = res.headers.get("X-Library-Version");
|
|
5953
|
+
if (res.status === 304) return { kind: "not_modified", version };
|
|
3302
5954
|
if (res.status === 401) {
|
|
3303
5955
|
return {
|
|
3304
5956
|
kind: "error",
|
|
@@ -3312,12 +5964,13 @@ async function fetchBundle(opts) {
|
|
|
3312
5964
|
kind: "ok",
|
|
3313
5965
|
raw,
|
|
3314
5966
|
publishedAt: res.headers.get("X-Published-At") ?? "unknown",
|
|
3315
|
-
bundleHash: createHash("sha256").update(raw).digest("hex")
|
|
5967
|
+
bundleHash: createHash("sha256").update(raw).digest("hex"),
|
|
5968
|
+
version
|
|
3316
5969
|
};
|
|
3317
5970
|
}
|
|
3318
5971
|
|
|
3319
5972
|
// src/files.ts
|
|
3320
|
-
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4, readFileSync as
|
|
5973
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4, readFileSync as readFileSync6, readdirSync as readdirSync3, rmSync as rmSync2, renameSync as renameSync2, existsSync as existsSync5 } from "node:fs";
|
|
3321
5974
|
import { join as join5, dirname, relative as relative2, resolve as resolve3, isAbsolute as isAbsolute2, sep } from "node:path";
|
|
3322
5975
|
|
|
3323
5976
|
// src/selection.ts
|
|
@@ -3351,17 +6004,38 @@ Available: ${available || "none"}.`
|
|
|
3351
6004
|
return bundle.components.map((c) => wanted.some((name) => matchesName(name, c.name)));
|
|
3352
6005
|
}
|
|
3353
6006
|
|
|
6007
|
+
// src/version.ts
|
|
6008
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
6009
|
+
function cliVersion() {
|
|
6010
|
+
try {
|
|
6011
|
+
const parsed = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
|
|
6012
|
+
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
6013
|
+
} catch {
|
|
6014
|
+
return "unknown";
|
|
6015
|
+
}
|
|
6016
|
+
}
|
|
6017
|
+
|
|
3354
6018
|
// src/files.ts
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
6019
|
+
var COMPONENT_SPEC_MARKER = COMPONENT_YAML_MARKER;
|
|
6020
|
+
var COMPONENT_SPEC_MARKERS = [COMPONENT_YAML_MARKER, COMPONENT_MARKDOWN_MARKER];
|
|
6021
|
+
function componentMarkdownPage(component) {
|
|
6022
|
+
const failed = () => new Error(
|
|
6023
|
+
`The published component context for ${component.name} could not be rendered as Markdown. Republish from the plugin, then pull again.`
|
|
6024
|
+
);
|
|
6025
|
+
let page;
|
|
6026
|
+
try {
|
|
6027
|
+
page = componentMarkdown(component.artifact);
|
|
6028
|
+
} catch {
|
|
6029
|
+
throw failed();
|
|
6030
|
+
}
|
|
6031
|
+
if (!page.startsWith(COMPONENT_MARKDOWN_MARKER)) throw failed();
|
|
6032
|
+
return page;
|
|
3358
6033
|
}
|
|
3359
|
-
var COMPONENT_SPEC_MARKER = "spec_layer:\n kind: component";
|
|
3360
6034
|
function readManifest(outDir) {
|
|
3361
6035
|
const path = join5(outDir, "manifest.json");
|
|
3362
6036
|
if (!existsSync5(path)) return null;
|
|
3363
6037
|
try {
|
|
3364
|
-
const parsed = JSON.parse(
|
|
6038
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
3365
6039
|
parsed.artifacts = parsed.artifacts.map((artifact) => {
|
|
3366
6040
|
const { aiPath, ...rest } = artifact;
|
|
3367
6041
|
return {
|
|
@@ -3378,32 +6052,11 @@ function readLocalBundle(outDir) {
|
|
|
3378
6052
|
const path = join5(outDir, "bundle.json");
|
|
3379
6053
|
if (!existsSync5(path)) return null;
|
|
3380
6054
|
try {
|
|
3381
|
-
return parseBundle(
|
|
6055
|
+
return parseBundle(readFileSync6(path, "utf8"));
|
|
3382
6056
|
} catch {
|
|
3383
6057
|
throw new Error(`${path} could not be read as a library bundle. Run spec-layer pull again.`);
|
|
3384
6058
|
}
|
|
3385
6059
|
}
|
|
3386
|
-
function componentSlugs(bundle) {
|
|
3387
|
-
const usedSlugs = /* @__PURE__ */ new Set();
|
|
3388
|
-
const nextSuffix = /* @__PURE__ */ new Map();
|
|
3389
|
-
return bundle.components.map((component) => {
|
|
3390
|
-
const base = slugify(component.name);
|
|
3391
|
-
let slug2 = base;
|
|
3392
|
-
if (usedSlugs.has(slug2)) {
|
|
3393
|
-
let n = (nextSuffix.get(base) ?? 1) + 1;
|
|
3394
|
-
slug2 = `${base}-${n}`;
|
|
3395
|
-
while (usedSlugs.has(slug2)) {
|
|
3396
|
-
n += 1;
|
|
3397
|
-
slug2 = `${base}-${n}`;
|
|
3398
|
-
}
|
|
3399
|
-
nextSuffix.set(base, n);
|
|
3400
|
-
} else {
|
|
3401
|
-
nextSuffix.set(base, 1);
|
|
3402
|
-
}
|
|
3403
|
-
usedSlugs.add(slug2);
|
|
3404
|
-
return slug2;
|
|
3405
|
-
});
|
|
3406
|
-
}
|
|
3407
6060
|
function assertReplaceable(outDir, cwd) {
|
|
3408
6061
|
const rel = relative2(resolve3(cwd), resolve3(outDir));
|
|
3409
6062
|
if (rel === "" || rel.startsWith("..") || isAbsolute2(rel)) {
|
|
@@ -3417,12 +6070,13 @@ function writeBundleFiles(opts) {
|
|
|
3417
6070
|
assertReplaceable(opts.outDir, opts.cwd);
|
|
3418
6071
|
const selection = opts.selection ?? DEFAULT_SELECTION;
|
|
3419
6072
|
const selected = selectComponents(opts.bundle, selection);
|
|
3420
|
-
const slugs = componentSlugs(opts.bundle);
|
|
6073
|
+
const slugs = componentSlugs(opts.bundle.components.map((c) => c.name));
|
|
3421
6074
|
const outputs = opts.outputs ?? [];
|
|
3422
6075
|
const componentSpecsDir = opts.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
|
|
6076
|
+
const componentSpecsFormat = opts.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT;
|
|
3423
6077
|
const outDirRel = relative2(resolve3(opts.cwd), resolve3(opts.outDir)).split(sep).join("/");
|
|
3424
6078
|
const outputPaths = outputs.map((o) => o.path);
|
|
3425
|
-
const specsProblem = visibleDirProblem(opts.cwd, outDirRel, componentSpecsDir,
|
|
6079
|
+
const specsProblem = visibleDirProblem(opts.cwd, outDirRel, componentSpecsDir, COMPONENT_SPEC_MARKERS, outputPaths, "componentSpecsDir");
|
|
3426
6080
|
if (specsProblem) throw new Error(specsProblem);
|
|
3427
6081
|
for (const o of outputs) {
|
|
3428
6082
|
const problem = outputPathProblem(opts.cwd, outDirRel, o, [componentSpecsDir, ...outputPaths.filter((p) => p !== o.path)]);
|
|
@@ -3431,6 +6085,10 @@ function writeBundleFiles(opts) {
|
|
|
3431
6085
|
const briefs = {};
|
|
3432
6086
|
opts.bundle.components.forEach((component, i) => {
|
|
3433
6087
|
if (!selected[i]) return;
|
|
6088
|
+
if (componentSpecsFormat === "md") {
|
|
6089
|
+
briefs[`${slugs[i]}.md`] = componentMarkdownPage(component);
|
|
6090
|
+
return;
|
|
6091
|
+
}
|
|
3434
6092
|
if (!component.ai.startsWith(COMPONENT_SPEC_MARKER)) {
|
|
3435
6093
|
throw new Error(`The published brief for ${component.name} does not begin with the Spec Layer marker. Republish from the plugin, then pull again.`);
|
|
3436
6094
|
}
|
|
@@ -3458,7 +6116,12 @@ function writeBundleFiles(opts) {
|
|
|
3458
6116
|
if (validateLevel1(artifact).some((d) => d.severity === "error")) {
|
|
3459
6117
|
throw new Error("The published Foundation context did not pass schema validation. Republish from the plugin, then pull again.");
|
|
3460
6118
|
}
|
|
3461
|
-
|
|
6119
|
+
put("fonts.json", json(fontRequirements(artifact)));
|
|
6120
|
+
const exp = foundationDtcg(
|
|
6121
|
+
artifact,
|
|
6122
|
+
opts.dtcg ?? {},
|
|
6123
|
+
usageUnits(opts.bundle)
|
|
6124
|
+
);
|
|
3462
6125
|
for (const [name, text] of Object.entries(dtcgExportFiles(exp))) put(`tokens/${name}`, text);
|
|
3463
6126
|
path = `${outDirRel}/tokens/resolver.json`;
|
|
3464
6127
|
const header = { libraryId: opts.libraryId, contentHash: opts.bundle.foundation.artifact.spec_layer.export.content_hash };
|
|
@@ -3481,7 +6144,7 @@ function writeBundleFiles(opts) {
|
|
|
3481
6144
|
kind: "component",
|
|
3482
6145
|
name: component.name,
|
|
3483
6146
|
contentHash: component.artifact.spec_layer.export.content_hash,
|
|
3484
|
-
path: selected[i] ? `${componentSpecsDir}/${slugs[i]}
|
|
6147
|
+
path: selected[i] ? `${componentSpecsDir}/${slugs[i]}.${componentSpecsFormat}` : null
|
|
3485
6148
|
});
|
|
3486
6149
|
});
|
|
3487
6150
|
const manifest = {
|
|
@@ -3490,9 +6153,12 @@ function writeBundleFiles(opts) {
|
|
|
3490
6153
|
bundleHash: opts.bundleHash,
|
|
3491
6154
|
pluginVersion: opts.bundle.pluginVersion,
|
|
3492
6155
|
extractorVersion: opts.bundle.extractorVersion,
|
|
6156
|
+
cliVersion: cliVersion(),
|
|
3493
6157
|
selection,
|
|
3494
6158
|
componentSpecsDir,
|
|
6159
|
+
componentSpecsFormat,
|
|
3495
6160
|
artifacts,
|
|
6161
|
+
...opts.version ? { version: opts.version } : {},
|
|
3496
6162
|
...opts.dtcg && Object.keys(opts.dtcg).length > 0 ? { dtcg: opts.dtcg } : {},
|
|
3497
6163
|
...opts.platforms && opts.platforms.length > 0 ? { platforms: opts.platforms } : {},
|
|
3498
6164
|
...opts.outputs ? { outputs: opts.outputs } : {}
|
|
@@ -3504,7 +6170,7 @@ function writeBundleFiles(opts) {
|
|
|
3504
6170
|
}
|
|
3505
6171
|
rmSync2(opts.outDir, { recursive: true, force: true });
|
|
3506
6172
|
renameSync2(staging, opts.outDir);
|
|
3507
|
-
const componentSpecs = { path: componentSpecsDir, files: writeVisibleDir(opts.cwd, componentSpecsDir,
|
|
6173
|
+
const componentSpecs = { path: componentSpecsDir, files: writeVisibleDir(opts.cwd, componentSpecsDir, COMPONENT_SPEC_MARKERS, briefs) };
|
|
3508
6174
|
const outputResults = [];
|
|
3509
6175
|
for (const d of deliverables) {
|
|
3510
6176
|
outputResults.push({ path: d.output.path, files: writeVisibleDir(opts.cwd, d.output.path, CSS_HEADER_PREFIX, d.files, CSS_INDEX_FILE) });
|
|
@@ -3513,7 +6179,7 @@ function writeBundleFiles(opts) {
|
|
|
3513
6179
|
}
|
|
3514
6180
|
|
|
3515
6181
|
// src/gitignore.ts
|
|
3516
|
-
import { readFileSync as
|
|
6182
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "node:fs";
|
|
3517
6183
|
import { spawnSync } from "node:child_process";
|
|
3518
6184
|
import { join as join6, dirname as dirname2, resolve as resolve4 } from "node:path";
|
|
3519
6185
|
var COMMENT = "# Spec Layer pull key, not for committing";
|
|
@@ -3550,7 +6216,7 @@ function ensureIgnored(cwd, fileName) {
|
|
|
3550
6216
|
${fileName}
|
|
3551
6217
|
`);
|
|
3552
6218
|
} else {
|
|
3553
|
-
const body =
|
|
6219
|
+
const body = readFileSync7(path, "utf8");
|
|
3554
6220
|
if (!hasEntryLine(body, fileName)) {
|
|
3555
6221
|
const lead = body.length === 0 || body.endsWith("\n") ? "" : "\n";
|
|
3556
6222
|
writeFileSync5(path, `${body}${lead}${COMMENT}
|
|
@@ -3567,16 +6233,20 @@ ${fileName}
|
|
|
3567
6233
|
}
|
|
3568
6234
|
|
|
3569
6235
|
// src/skill.ts
|
|
3570
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as
|
|
6236
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3571
6237
|
import { dirname as dirname3, join as join7 } from "node:path";
|
|
3572
6238
|
|
|
3573
6239
|
// src/tools.ts
|
|
3574
6240
|
var OK_OR_ERROR = { "0": "success", "1": "usage error, bad key or id, or a network or server failure" };
|
|
3575
6241
|
var LOCAL_ONLY = { "0": "success", "1": "no local pull, or a usage error" };
|
|
6242
|
+
var PULL_EXITS = {
|
|
6243
|
+
"0": "success, even when tokens/report.json or an outputs/*.report.json holds an error-severity entry (pass --strict to fail on that instead)",
|
|
6244
|
+
"1": "usage error, bad key or id, a network or server failure, or --strict with an error-severity entry in tokens/report.json or an outputs/*.report.json, including on a cached (304) pull"
|
|
6245
|
+
};
|
|
3576
6246
|
var TOOLS = [
|
|
3577
6247
|
{
|
|
3578
6248
|
name: "setup",
|
|
3579
|
-
usage: "spec-layer setup --id lib_... --key sl_... [--out DIR] [--platform web|ios|android|flutter]... [--only foundation|components] [--component NAME]...",
|
|
6249
|
+
usage: "spec-layer setup --id lib_... --key sl_... [--out DIR] [--platform web|ios|android|flutter]... [--component-format yaml|md] [--only foundation|components] [--component NAME]...",
|
|
3580
6250
|
summary: "Records the library id, stores the pull key in a gitignored speclayer.local.json, then pulls.",
|
|
3581
6251
|
when: "Once, with the command the plugin's Publish screen hands out. Re-run it after the key is rotated.",
|
|
3582
6252
|
network: true,
|
|
@@ -3593,7 +6263,7 @@ var TOOLS = [
|
|
|
3593
6263
|
},
|
|
3594
6264
|
{
|
|
3595
6265
|
name: "init",
|
|
3596
|
-
usage: "spec-layer init --id lib_... [--out DIR] [--platform web|ios|android|flutter]... [--only foundation|components] [--component NAME]...",
|
|
6266
|
+
usage: "spec-layer init --id lib_... [--out DIR] [--platform web|ios|android|flutter]... [--component-format yaml|md] [--only foundation|components] [--component NAME]...",
|
|
3597
6267
|
summary: "Writes speclayer.json, with the platforms and default outputs, so later commands need no flags. Stores no key and reaches no server.",
|
|
3598
6268
|
when: "A repo that supplies the key from SPEC_LAYER_KEY instead of a stored file.",
|
|
3599
6269
|
network: false,
|
|
@@ -3603,9 +6273,9 @@ var TOOLS = [
|
|
|
3603
6273
|
},
|
|
3604
6274
|
{
|
|
3605
6275
|
name: "pull",
|
|
3606
|
-
usage: "spec-layer pull [--id lib_...] [--key sl_...] [--out DIR] [--platform web|ios|android|flutter]... [--only foundation|components] [--component NAME]...",
|
|
3607
|
-
summary: "Fetches the published library and writes the record under the output directory (default .speclayer/), the briefs under componentSpecsDir, and the token files under outputs[].path.",
|
|
3608
|
-
when: "After setup, whenever status says the local copy is behind, or after changing the include, dtcg, outputs, or
|
|
6276
|
+
usage: "spec-layer pull [--id lib_...] [--key sl_...] [--out DIR] [--platform web|ios|android|flutter]... [--component-format yaml|md] [--only foundation|components] [--component NAME]... [--strict]",
|
|
6277
|
+
summary: "Fetches the published library and writes the record under the output directory (default .speclayer/), the briefs under componentSpecsDir (YAML, or Markdown when componentSpecsFormat or --component-format says md), and the token files under outputs[].path. Prints a severity summary to stderr, even on a cached pull, when tokens/report.json or an outputs/*.report.json holds an error or warning.",
|
|
6278
|
+
when: "After setup, whenever status says the local copy is behind, or after changing the include, dtcg, outputs, componentSpecsDir, or componentSpecsFormat settings. Add --strict in CI to fail the build on an error-severity report entry.",
|
|
3609
6279
|
network: true,
|
|
3610
6280
|
needsKey: true,
|
|
3611
6281
|
writes: [
|
|
@@ -3613,7 +6283,7 @@ var TOOLS = [
|
|
|
3613
6283
|
"outputs[].path from speclayer.json (default tokens/ for web), a directory written in place",
|
|
3614
6284
|
"componentSpecsDir from speclayer.json (default component-specs/), written in place"
|
|
3615
6285
|
],
|
|
3616
|
-
exits:
|
|
6286
|
+
exits: PULL_EXITS
|
|
3617
6287
|
},
|
|
3618
6288
|
{
|
|
3619
6289
|
name: "status",
|
|
@@ -3637,8 +6307,8 @@ var TOOLS = [
|
|
|
3637
6307
|
},
|
|
3638
6308
|
{
|
|
3639
6309
|
name: "show",
|
|
3640
|
-
usage: "spec-layer show foundation | component NAME [--canonical] [--out DIR]",
|
|
3641
|
-
summary: "Prints one artifact to stdout: the Foundation DTCG document, or one component's AI YAML; --canonical prints the v5 JSON.",
|
|
6310
|
+
usage: "spec-layer show foundation | component NAME [--component-format yaml|md] [--canonical] [--out DIR]",
|
|
6311
|
+
summary: "Prints one artifact to stdout: the Foundation DTCG document, or one component's AI YAML or Markdown (by --component-format, then componentSpecsFormat, then the last pull); --canonical prints the v5 JSON.",
|
|
3642
6312
|
when: "To read one component or the token document without opening files; it pipes cleanly.",
|
|
3643
6313
|
network: false,
|
|
3644
6314
|
needsKey: false,
|
|
@@ -3678,7 +6348,7 @@ function toolsText() {
|
|
|
3678
6348
|
lines.push(` ${tool.summary}`);
|
|
3679
6349
|
lines.push(` When: ${tool.when}`);
|
|
3680
6350
|
lines.push(` Network: ${tool.network ? "yes" : "no"}. Key: ${tool.needsKey ? "required" : "not needed"}. Writes: ${tool.writes.length ? tool.writes.join(", ") : "nothing"}.`);
|
|
3681
|
-
lines.push(` Exits: ${Object.entries(tool.exits).map(([
|
|
6351
|
+
lines.push(` Exits: ${Object.entries(tool.exits).map(([code3, meaning]) => `${code3} ${meaning}`).join("; ")}.`);
|
|
3682
6352
|
lines.push("");
|
|
3683
6353
|
}
|
|
3684
6354
|
lines.push("Flags every command accepts where they apply:");
|
|
@@ -3700,25 +6370,43 @@ function toolsJson(version) {
|
|
|
3700
6370
|
|
|
3701
6371
|
// src/skill.ts
|
|
3702
6372
|
var RESERVED = /* @__PURE__ */ new Set(["resolver.json", "spec-layer.meta.json", "report.json"]);
|
|
3703
|
-
function
|
|
3704
|
-
if (typeof tree !== "object" || tree === null || Array.isArray(tree)) return
|
|
6373
|
+
function collectNumberTokenPaths(tree, path, legitimatelyUnitless, out) {
|
|
6374
|
+
if (typeof tree !== "object" || tree === null || Array.isArray(tree)) return;
|
|
3705
6375
|
const record = tree;
|
|
3706
|
-
if (record.$type === "number" && "$value" in record)
|
|
3707
|
-
|
|
6376
|
+
if (record.$type === "number" && "$value" in record) {
|
|
6377
|
+
const dotted = path.join(".");
|
|
6378
|
+
if (!legitimatelyUnitless.has(dotted)) out.add(dotted);
|
|
6379
|
+
return;
|
|
6380
|
+
}
|
|
3708
6381
|
for (const [key, value] of Object.entries(record)) {
|
|
3709
6382
|
if (key.startsWith("$")) continue;
|
|
3710
|
-
|
|
6383
|
+
collectNumberTokenPaths(value, [...path, key], legitimatelyUnitless, out);
|
|
3711
6384
|
}
|
|
3712
|
-
return n;
|
|
3713
6385
|
}
|
|
3714
6386
|
function readJson(path) {
|
|
3715
6387
|
if (!existsSync7(path)) return null;
|
|
3716
6388
|
try {
|
|
3717
|
-
return JSON.parse(
|
|
6389
|
+
return JSON.parse(readFileSync8(path, "utf8"));
|
|
3718
6390
|
} catch {
|
|
3719
6391
|
return null;
|
|
3720
6392
|
}
|
|
3721
6393
|
}
|
|
6394
|
+
function unitlessScopedPaths(tokensDir) {
|
|
6395
|
+
const meta = readJson(join7(tokensDir, "spec-layer.meta.json"));
|
|
6396
|
+
const out = /* @__PURE__ */ new Set();
|
|
6397
|
+
if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return out;
|
|
6398
|
+
for (const [path, entry2] of Object.entries(meta)) {
|
|
6399
|
+
if (typeof entry2 !== "object" || entry2 === null) continue;
|
|
6400
|
+
const { type, scopes } = entry2;
|
|
6401
|
+
if (type === "number" && Array.isArray(scopes) && scopesStateNumber(scopes)) out.add(path);
|
|
6402
|
+
}
|
|
6403
|
+
return out;
|
|
6404
|
+
}
|
|
6405
|
+
function isFontRequirement(v) {
|
|
6406
|
+
if (typeof v !== "object" || v === null) return false;
|
|
6407
|
+
const r = v;
|
|
6408
|
+
return typeof r.family === "string" && Array.isArray(r.weights) && r.weights.every((w) => typeof w === "number") && Array.isArray(r.used_by) && r.used_by.every((u) => typeof u === "string");
|
|
6409
|
+
}
|
|
3722
6410
|
function summarizePull(cwd, outDir, manifest) {
|
|
3723
6411
|
if (!manifest) return null;
|
|
3724
6412
|
const absOut = join7(cwd, outDir);
|
|
@@ -3735,17 +6423,37 @@ function summarizePull(cwd, outDir, manifest) {
|
|
|
3735
6423
|
} catch {
|
|
3736
6424
|
tokenFiles = [];
|
|
3737
6425
|
}
|
|
3738
|
-
|
|
6426
|
+
const legitimatelyUnitless = unitlessScopedPaths(tokensDir);
|
|
6427
|
+
const unitlessPaths = /* @__PURE__ */ new Set();
|
|
3739
6428
|
for (const file of tokenFiles) {
|
|
3740
6429
|
if (file.startsWith("styles.")) continue;
|
|
3741
|
-
|
|
6430
|
+
collectNumberTokenPaths(readJson(join7(tokensDir, file)), [], legitimatelyUnitless, unitlessPaths);
|
|
3742
6431
|
}
|
|
6432
|
+
const unitlessNumbers = unitlessPaths.size;
|
|
3743
6433
|
const reportCounts = {};
|
|
3744
6434
|
if (Array.isArray(report2)) {
|
|
3745
6435
|
for (const entry2 of report2) {
|
|
3746
6436
|
if (typeof entry2?.code === "string") reportCounts[entry2.code] = (reportCounts[entry2.code] ?? 0) + 1;
|
|
3747
6437
|
}
|
|
3748
6438
|
}
|
|
6439
|
+
const fontsPath = join7(absOut, "fonts.json");
|
|
6440
|
+
let fontsStatus = "missing";
|
|
6441
|
+
let fonts = [];
|
|
6442
|
+
if (existsSync7(fontsPath)) {
|
|
6443
|
+
let parsed;
|
|
6444
|
+
try {
|
|
6445
|
+
parsed = JSON.parse(readFileSync8(fontsPath, "utf8"));
|
|
6446
|
+
} catch {
|
|
6447
|
+
parsed = void 0;
|
|
6448
|
+
}
|
|
6449
|
+
if (Array.isArray(parsed)) {
|
|
6450
|
+
fontsStatus = "ok";
|
|
6451
|
+
fonts = parsed.filter(isFontRequirement);
|
|
6452
|
+
} else {
|
|
6453
|
+
fontsStatus = "unreadable";
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
const missingFontFamilies = fontsStatus === "ok" && fonts.length > 0 ? missingFontSourcesInRepo(fonts.map((f) => f.family), cwd) : [];
|
|
3749
6457
|
foundation = {
|
|
3750
6458
|
written: foundationEntry.path !== null && resolver !== null,
|
|
3751
6459
|
sets: resolver ? Object.keys(resolver.sets ?? {}) : [],
|
|
@@ -3756,7 +6464,10 @@ function summarizePull(cwd, outDir, manifest) {
|
|
|
3756
6464
|
})) : [],
|
|
3757
6465
|
tokenFiles,
|
|
3758
6466
|
unitlessNumbers,
|
|
3759
|
-
reportCounts
|
|
6467
|
+
reportCounts,
|
|
6468
|
+
fontsStatus,
|
|
6469
|
+
fonts,
|
|
6470
|
+
missingFontFamilies
|
|
3760
6471
|
};
|
|
3761
6472
|
}
|
|
3762
6473
|
return {
|
|
@@ -3765,6 +6476,7 @@ function summarizePull(cwd, outDir, manifest) {
|
|
|
3765
6476
|
publishedAt: manifest.publishedAt,
|
|
3766
6477
|
pluginVersion: manifest.pluginVersion,
|
|
3767
6478
|
componentSpecsDir: manifest.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR,
|
|
6479
|
+
componentSpecsFormat: manifest.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT,
|
|
3768
6480
|
components,
|
|
3769
6481
|
foundation,
|
|
3770
6482
|
outputs: (manifest.outputs ?? []).map((o) => {
|
|
@@ -3793,7 +6505,7 @@ function summarizePull(cwd, outDir, manifest) {
|
|
|
3793
6505
|
})
|
|
3794
6506
|
};
|
|
3795
6507
|
}
|
|
3796
|
-
var
|
|
6508
|
+
var code2 = (s) => `\`${s}\``;
|
|
3797
6509
|
function stackSection(input) {
|
|
3798
6510
|
const { profile, platforms, platformSource, pull } = input;
|
|
3799
6511
|
const lines = ["## This codebase", ""];
|
|
@@ -3803,7 +6515,7 @@ function stackSection(input) {
|
|
|
3803
6515
|
);
|
|
3804
6516
|
} else {
|
|
3805
6517
|
lines.push("Detected from the repository root (the file that carries each signal is named, and nothing deeper was read):", "");
|
|
3806
|
-
for (const e of profile.evidence) lines.push(`- ${e.signal} (${
|
|
6518
|
+
for (const e of profile.evidence) lines.push(`- ${e.signal} (${code2(e.file)})`);
|
|
3807
6519
|
lines.push("");
|
|
3808
6520
|
const facts = [];
|
|
3809
6521
|
if (profile.languages.length) facts.push(`Languages: ${profile.languages.join(", ")}.`);
|
|
@@ -3813,74 +6525,111 @@ function stackSection(input) {
|
|
|
3813
6525
|
}
|
|
3814
6526
|
if (platformSource === "none") {
|
|
3815
6527
|
lines.push(
|
|
3816
|
-
`No target platform was detected, so the token advice below is generic. Re-run ${
|
|
6528
|
+
`No target platform was detected, so the token advice below is generic. Re-run ${code2("spec-layer skill --platform web|ios|android|flutter")} to write it for a platform, or pass ` + code2("--install") + " with the same flag to update the installed copy.",
|
|
3817
6529
|
""
|
|
3818
6530
|
);
|
|
3819
6531
|
} else {
|
|
3820
6532
|
const label = platformSource === "flag" ? "chosen with --platform" : platformSource === "config" ? "set in speclayer.json" : "detected";
|
|
3821
6533
|
lines.push(`Target platform${platforms.length > 1 ? "s" : ""} (${label}): ${platforms.join(", ")}.`, "");
|
|
3822
6534
|
}
|
|
6535
|
+
const tokensDir = `${input.outDir}/tokens/`;
|
|
6536
|
+
if (pull?.foundation && pull.foundation.unitlessNumbers > 0) {
|
|
6537
|
+
const n = pull.foundation.unitlessNumbers;
|
|
6538
|
+
const cssReport = pull.outputs.find((o) => o.platform === "web" && o.format === "css" && o.written) ?? null;
|
|
6539
|
+
lines.push(
|
|
6540
|
+
`**${n} token${n === 1 ? " has" : "s have"} no unit.** ${n === 1 ? "Its own Figma variable states" : "Their own Figma variables state"} none at all, not even that ${n === 1 ? "it is" : "they are"} a unitless number the way an opacity or a font weight would. Those are already excluded from this count, because Figma states that for them. So ${n === 1 ? "it is" : "they are"} written as ${code2('$type: "number"')}: a bare number, not usable as a CSS length. ${code2("height: 36")} is invalid CSS and the browser drops the declaration. ${n === 1 ? "Narrow the variable's" : "Narrow each variable's"} scope in Figma to a length (${code2("CORNER_RADIUS")}, ${code2("GAP")}, ${code2("WIDTH_HEIGHT")}, ${code2("FONT_SIZE")}, or ${code2("STROKE_FLOAT")}), or to ${code2("OPACITY")}/${code2("FONT_WEIGHT")} if it genuinely carries none, and pull again. Only add ${code2('"dtcg": { "units": { "<Collection>/<name glob>": "px" } }')} in ${code2("speclayer.json")} once you already know the token is a length: the override applies to anything the glob matches that Figma has not already scoped as unitless, so a glob that is too broad can turn a genuine opacity or font weight into a fake length. Nothing is inferred from a name; ${code2(`${tokensDir}spec-layer.meta.json`)} names each token's own Figma scopes` + (cssReport ? `, and ${code2(`${input.outDir}/outputs/${cssReport.platform}-${cssReport.format}.report.json`)} names every one under ${code2("unitless_number")}. Do not read that file's entry count as this number: it carries one entry per mode a token appears in, and it also names the tokens Figma does scope as a unitless number, which this count leaves out.` : "."),
|
|
6541
|
+
""
|
|
6542
|
+
);
|
|
6543
|
+
}
|
|
3823
6544
|
for (const platform of platforms) {
|
|
3824
6545
|
const key = CODE_SYNTAX_KEY[platform];
|
|
3825
|
-
const tokensDir = `${input.outDir}/tokens/`;
|
|
3826
6546
|
if (platform === "web") {
|
|
3827
6547
|
lines.push("### Web", "");
|
|
3828
6548
|
const cssOut = pull?.outputs.find((o) => o.platform === "web" && o.format === "css" && o.written) ?? null;
|
|
6549
|
+
if (pull?.foundation?.written) {
|
|
6550
|
+
const { fontsStatus, fonts, missingFontFamilies } = pull.foundation;
|
|
6551
|
+
if (fontsStatus === "ok" && fonts.length > 0) {
|
|
6552
|
+
const fontList = fonts.map((f) => `${f.family} at ${f.weights.join(", ")}`).join("; ");
|
|
6553
|
+
lines.push(
|
|
6554
|
+
`**Fonts.** This library's type is ${fontList}. Load every weight listed; a missing weight renders as a synthesised bold that matches nothing in the design.`
|
|
6555
|
+
);
|
|
6556
|
+
for (const family of missingFontFamilies) {
|
|
6557
|
+
lines.push(
|
|
6558
|
+
`${family}: nothing in this repository loads it. Add a font source before building UI, or every component that uses it renders in the browser default.`
|
|
6559
|
+
);
|
|
6560
|
+
}
|
|
6561
|
+
const fallbackTarget = cssOut ? `${cssOut.path}/` : `${input.outDir}/`;
|
|
6562
|
+
lines.push(
|
|
6563
|
+
`The token holds a family name and no fallback stack. Never write ${code2("font-family")} from a token without appending a generic fallback, and never keep that fallback inside ${code2(fallbackTarget)}: the next pull replaces it.`,
|
|
6564
|
+
""
|
|
6565
|
+
);
|
|
6566
|
+
} else if (fontsStatus === "ok") {
|
|
6567
|
+
lines.push(
|
|
6568
|
+
`${code2("fonts.json")} names no font family. Either this library's typography styles reference none, or a reference failed to resolve one; ${code2("npx spec-layer show foundation")} prints the styles themselves.`,
|
|
6569
|
+
""
|
|
6570
|
+
);
|
|
6571
|
+
} else {
|
|
6572
|
+
lines.push(
|
|
6573
|
+
fontsStatus === "missing" ? `${code2("fonts.json")} is missing, so the font requirement for this library is unknown here. Run ${code2("npx spec-layer pull")} again: every pull that includes the Foundation writes this file, and a pull that finds it gone re-projects instead of reporting no change.` : `${code2("fonts.json")} is not valid JSON, so the font requirement for this library is unknown here. Delete ${code2(`${input.outDir}/fonts.json`)} and run ${code2("npx spec-layer pull")} again, which rewrites it.`,
|
|
6574
|
+
""
|
|
6575
|
+
);
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
3829
6578
|
if (cssOut) {
|
|
3830
6579
|
const mapPath = `${input.outDir}/outputs/web-css.map.json`;
|
|
3831
6580
|
lines.push(
|
|
3832
|
-
`The CSS custom property for every token is in ${
|
|
6581
|
+
`The CSS custom property for every token is in ${code2(mapPath)}: source "code_syntax" when the designer declared it in Figma, "derived" when the CLI built it from the DTCG path by the stated rule (${cssOut.case} case, collection root included). Each entry names the file that declares the property. Use those names; never invent a third. ${code2(`${tokensDir}spec-layer.meta.json`)} still holds the raw ${code2("code_syntax.WEB")} the designer declared.`,
|
|
3833
6582
|
""
|
|
3834
6583
|
);
|
|
3835
6584
|
const partFiles = cssOut.files.filter((f) => f !== CSS_INDEX_FILE);
|
|
3836
6585
|
lines.push(
|
|
3837
|
-
`Import ${
|
|
6586
|
+
`Import ${code2(`${cssOut.path}/index.css`)} from the root stylesheet. It imports one file per collection and mode: ${partFiles.join(", ")}. Sets and default modes are at ${code2(":root")}; every other mode is a block under ${code2(cssOut.modeSelector)} in its own file, so a mode can also be imported alone. To switch, set ${code2("data-theme")} on ${code2("<html>")} (or whatever the selector names). To let the OS choose, set that collection's selector to ${code2(":root")} under ${code2("outputs[].modes")} and import the mode's file yourself under ${code2("@media (prefers-color-scheme: dark)")}; the CLI never assumes that.`,
|
|
3838
6587
|
""
|
|
3839
6588
|
);
|
|
3840
6589
|
if (profile.tokenTools.includes("style-dictionary") || profile.tokenTools.includes("tokens-studio")) {
|
|
3841
6590
|
lines.push(
|
|
3842
|
-
`${
|
|
6591
|
+
`${code2(`${cssOut.path}/`)} is a projection of the same ${code2(tokensDir)} files, not a second source. Import one or the other.`,
|
|
3843
6592
|
""
|
|
3844
6593
|
);
|
|
3845
6594
|
}
|
|
3846
6595
|
} else {
|
|
3847
6596
|
lines.push(
|
|
3848
|
-
`Token identifiers for code live in ${
|
|
6597
|
+
`Token identifiers for code live in ${code2(`${tokensDir}spec-layer.meta.json`)} under each token's ${code2(`code_syntax.${key}`)}, when the designer declared one in Figma. When a token has no WEB entry, use the DTCG path as it appears in the token file and say in your change that the code name is not declared in Figma.`,
|
|
3849
6598
|
""
|
|
3850
6599
|
);
|
|
3851
6600
|
const configuredNotWritten = pull?.outputs.find((o) => o.platform === "web" && !o.written) ?? null;
|
|
3852
6601
|
if (configuredNotWritten?.indexMissing) {
|
|
3853
6602
|
lines.push(
|
|
3854
|
-
`A web/css output is configured at ${
|
|
6603
|
+
`A web/css output is configured at ${code2(`${configuredNotWritten.path}/`)} but its ${code2("index.css")} is missing, so the file list is unknown. Run ${code2("npx spec-layer pull")} to write it again.`,
|
|
3855
6604
|
""
|
|
3856
6605
|
);
|
|
3857
6606
|
} else if (configuredNotWritten) {
|
|
3858
6607
|
lines.push(
|
|
3859
|
-
`A web/css output is configured at ${
|
|
6608
|
+
`A web/css output is configured at ${code2(`${configuredNotWritten.path}/`)} but was not written, because the last pull did not write the Foundation. Pull with the Foundation selected to write it.`,
|
|
3860
6609
|
""
|
|
3861
6610
|
);
|
|
3862
6611
|
} else if (pull?.foundation?.written) {
|
|
3863
6612
|
lines.push(
|
|
3864
|
-
`No token file was written for web. Add \`"outputs"\` in \`speclayer.json\` (or run \`spec-layer pull --platform web\` once) and pull again; the default lands at ${
|
|
6613
|
+
`No token file was written for web. Add \`"outputs"\` in \`speclayer.json\` (or run \`spec-layer pull --platform web\` once) and pull again; the default lands at ${code2("tokens/")}.`,
|
|
3865
6614
|
""
|
|
3866
6615
|
);
|
|
3867
6616
|
}
|
|
3868
6617
|
}
|
|
3869
6618
|
if (profile.tokenTools.includes("tailwind")) {
|
|
3870
6619
|
lines.push(
|
|
3871
|
-
`Tailwind is present (${
|
|
6620
|
+
`Tailwind is present (${code2("tailwindcss")}). Map DTCG ${code2("color")} tokens to the theme's color scale and ${code2("dimension")} tokens to spacing, radius, or font size by the collection and group they sit in. Keep the mapping in one place and reference token paths, not copied values, so a republish moves the code with it.`,
|
|
3872
6621
|
""
|
|
3873
6622
|
);
|
|
3874
6623
|
}
|
|
3875
6624
|
if (profile.tokenTools.includes("style-dictionary")) {
|
|
3876
6625
|
const major = profile.styleDictionaryMajor;
|
|
3877
6626
|
lines.push(
|
|
3878
|
-
`Style Dictionary is present${major !== null ? ` (major version ${major} in package.json)` : ""}. Point it at ${
|
|
6627
|
+
`Style Dictionary is present${major !== null ? ` (major version ${major} in package.json)` : ""}. Point it at ${code2(tokensDir)} and load the files ${code2("resolver.json")} names for the mode you build. Exclude ${code2("spec-layer.meta.json")} and ${code2("report.json")} from token globs; they are not token files.`
|
|
3879
6628
|
);
|
|
3880
6629
|
if (major !== null && major < 5) {
|
|
3881
6630
|
lines.push(
|
|
3882
6631
|
"",
|
|
3883
|
-
`Style Dictionary ${major} reads the string value forms, not the 2025.10 object forms. Set ${
|
|
6632
|
+
`Style Dictionary ${major} reads the string value forms, not the 2025.10 object forms. Set ${code2('"dtcg": { "values": "legacy" }')} in ${code2("speclayer.json")} and run ${code2("spec-layer pull")}; the change re-projects tokens/ without a republish.`
|
|
3884
6633
|
);
|
|
3885
6634
|
}
|
|
3886
6635
|
lines.push("");
|
|
@@ -3888,30 +6637,23 @@ function stackSection(input) {
|
|
|
3888
6637
|
} else if (platform === "ios") {
|
|
3889
6638
|
lines.push("### iOS", "");
|
|
3890
6639
|
lines.push(
|
|
3891
|
-
`Token identifiers for code live in ${
|
|
6640
|
+
`Token identifiers for code live in ${code2(`${tokensDir}spec-layer.meta.json`)} under each token's ${code2(`code_syntax.${key}`)}, when the designer declared one in Figma. Use that as the Swift symbol. Colors arrive as hex strings or DTCG color objects with RGBA components; dimensions carry an explicit px or rem unit. A modifier with more than one context (for example a light and a dark mode) maps to a color-scheme-dependent value; a set without modes is a constant. Do not invent a dark variant for a collection that has one mode.`,
|
|
3892
6641
|
""
|
|
3893
6642
|
);
|
|
3894
6643
|
} else if (platform === "android") {
|
|
3895
6644
|
lines.push("### Android", "");
|
|
3896
6645
|
lines.push(
|
|
3897
|
-
`Token identifiers for code live in ${
|
|
6646
|
+
`Token identifiers for code live in ${code2(`${tokensDir}spec-layer.meta.json`)} under each token's ${code2(`code_syntax.${key}`)}, when the designer declared one in Figma. Use that as the Kotlin or resource name. Dimensions carry an explicit px or rem unit and no density assumption; a value is only dp when your own convention says so, and that convention belongs in your code, not in the token file. Modes map to resource qualifiers or a Compose theme switch.`,
|
|
3898
6647
|
""
|
|
3899
6648
|
);
|
|
3900
6649
|
} else {
|
|
3901
6650
|
lines.push("### Flutter", "");
|
|
3902
6651
|
lines.push(
|
|
3903
|
-
`Figma declares no code syntax for Flutter, so no identifier is provided for Dart. Name symbols after the DTCG path (for example ${
|
|
6652
|
+
`Figma declares no code syntax for Flutter, so no identifier is provided for Dart. Name symbols after the DTCG path (for example ${code2("Collection.group.name")} becomes a nested class or a camelCase constant) and keep the path in a comment so the source token stays traceable. Modes map to theme variants.`,
|
|
3904
6653
|
""
|
|
3905
6654
|
);
|
|
3906
6655
|
}
|
|
3907
6656
|
}
|
|
3908
|
-
if (pull?.foundation && pull.foundation.unitlessNumbers > 0) {
|
|
3909
|
-
const n = pull.foundation.unitlessNumbers;
|
|
3910
|
-
lines.push(
|
|
3911
|
-
`${n} token${n === 1 ? " is" : "s are"} exported as ${code('$type: "number"')} because the Figma scopes state no unit. If your code needs them as px or rem, declare it in ${code("speclayer.json")}: ${code('"dtcg": { "units": { "<Collection>/<name glob>": "px" } }')}, then run ${code("spec-layer pull")}. Nothing is inferred from a name; an override that contradicts a stated scope is ignored and listed in ${code("report.json")}.`,
|
|
3912
|
-
""
|
|
3913
|
-
);
|
|
3914
|
-
}
|
|
3915
6657
|
return lines;
|
|
3916
6658
|
}
|
|
3917
6659
|
function pullSection(input) {
|
|
@@ -3919,46 +6661,46 @@ function pullSection(input) {
|
|
|
3919
6661
|
const lines = ["## What is on disk", ""];
|
|
3920
6662
|
if (!pull) {
|
|
3921
6663
|
lines.push(
|
|
3922
|
-
`No pull has been made in this directory yet, so nothing under ${
|
|
6664
|
+
`No pull has been made in this directory yet, so nothing under ${code2(outDir + "/")} can be described. Run ${code2("npx spec-layer pull")} (or the setup command from the plugin if there is no ${code2("speclayer.json")}), then ${code2("npx spec-layer skill --install")} again to list the components and token collections here.`,
|
|
3923
6665
|
""
|
|
3924
6666
|
);
|
|
3925
6667
|
return lines;
|
|
3926
6668
|
}
|
|
3927
6669
|
lines.push(
|
|
3928
|
-
`Library ${
|
|
6670
|
+
`Library ${code2(pull.libraryId)}, published ${pull.publishedAt}${pull.pluginVersion ? ` by plugin ${pull.pluginVersion}` : ""}. Run ${code2("npx spec-layer status")} first; exit code 2 means a newer publish exists and ${code2("npx spec-layer pull")} fetches it.`,
|
|
3929
6671
|
""
|
|
3930
6672
|
);
|
|
3931
|
-
lines.push(`- ${
|
|
3932
|
-
lines.push(`- ${
|
|
6673
|
+
lines.push(`- ${code2(`${outDir}/manifest.json`)}: every artifact with its content hash and file path.`);
|
|
6674
|
+
lines.push(`- ${code2(`${outDir}/bundle.json`)}: the whole published library, including the canonical v5 JSON of every artifact.`);
|
|
3933
6675
|
if (pull.foundation) {
|
|
3934
6676
|
if (pull.foundation.written) {
|
|
3935
|
-
lines.push(`- ${
|
|
3936
|
-
lines.push(` - ${
|
|
3937
|
-
lines.push(` - ${
|
|
3938
|
-
lines.push(` - ${
|
|
3939
|
-
for (const f of pull.foundation.tokenFiles) lines.push(` - ${
|
|
6677
|
+
lines.push(`- ${code2(`${outDir}/tokens/`)}: the Foundation as Design Tokens Format Module 2025.10 files.`);
|
|
6678
|
+
lines.push(` - ${code2("resolver.json")}: sets, modifiers, and resolution order. Start here.`);
|
|
6679
|
+
lines.push(` - ${code2("spec-layer.meta.json")}: Figma ids, scopes, publication, and ${code2("code_syntax")} per DTCG path.`);
|
|
6680
|
+
lines.push(` - ${code2("report.json")}: what the format could not express, with reasons. Never fill these gaps with a guess.`);
|
|
6681
|
+
for (const f of pull.foundation.tokenFiles) lines.push(` - ${code2(f)}`);
|
|
3940
6682
|
} else {
|
|
3941
|
-
lines.push(`- Foundation: present in the library but not written, because the selection excludes it. ${
|
|
6683
|
+
lines.push(`- Foundation: present in the library but not written, because the selection excludes it. ${code2("spec-layer show foundation")} still prints it.`);
|
|
3942
6684
|
}
|
|
3943
6685
|
} else {
|
|
3944
6686
|
lines.push("- This library has no Foundation, so there is no tokens/ directory.");
|
|
3945
6687
|
}
|
|
3946
|
-
lines.push(`- ${
|
|
6688
|
+
lines.push(`- ${code2(`${pull.componentSpecsDir}/`)}: one ${pull.componentSpecsFormat === "md" ? "Markdown page" : "YAML"} per component.`);
|
|
3947
6689
|
for (const o of pull.outputs) {
|
|
3948
|
-
lines.push(o.written ? `- ${
|
|
6690
|
+
lines.push(o.written ? `- ${code2(`${o.path}/`)}: ${o.platform}/${o.format} token files, ${o.case} names: ${o.files.join(", ")}. Non-default modes are under ${code2(o.modeSelector)}, each in its own file. Names and provenance: ${code2(`${outDir}/outputs/${o.platform}-${o.format}.map.json`)}; what it could not express: ${code2(`${outDir}/outputs/${o.platform}-${o.format}.report.json`)}.` : o.indexMissing ? `- ${code2(`${o.path}/`)}: ${o.platform}/${o.format} token files, but ${code2("index.css")} is missing; run ${code2("npx spec-layer pull")} to restore the directory.` : `- ${code2(`${o.path}/`)}: ${o.platform}/${o.format} token files, configured but not written by the last pull (the Foundation was not written). Nothing is on disk at that path from Spec Layer.`);
|
|
3949
6691
|
}
|
|
3950
6692
|
lines.push("");
|
|
3951
6693
|
if (pull.foundation && (pull.foundation.sets.length || pull.foundation.modifiers.length)) {
|
|
3952
6694
|
lines.push("### Token collections", "");
|
|
3953
|
-
for (const set of pull.foundation.sets) lines.push(`- ${
|
|
6695
|
+
for (const set of pull.foundation.sets) lines.push(`- ${code2(set)}: one mode, always applied.`);
|
|
3954
6696
|
for (const m of pull.foundation.modifiers) {
|
|
3955
|
-
lines.push(`- ${
|
|
6697
|
+
lines.push(`- ${code2(m.name)}: modes ${m.contexts.map(code2).join(", ")}${m.default ? `, default ${code2(m.default)}` : ""}.`);
|
|
3956
6698
|
}
|
|
3957
6699
|
lines.push("");
|
|
3958
6700
|
const counts = Object.entries(pull.foundation.reportCounts);
|
|
3959
6701
|
if (counts.length) {
|
|
3960
6702
|
lines.push(
|
|
3961
|
-
`${
|
|
6703
|
+
`${code2("report.json")} lists ${counts.map(([c, n]) => `${n} ${code2(c)}`).join(", ")}. Read it before assuming a token is missing.`,
|
|
3962
6704
|
""
|
|
3963
6705
|
);
|
|
3964
6706
|
}
|
|
@@ -3968,7 +6710,7 @@ function pullSection(input) {
|
|
|
3968
6710
|
lines.push("The library documents no components.", "");
|
|
3969
6711
|
} else {
|
|
3970
6712
|
for (const c of pull.components) {
|
|
3971
|
-
lines.push(c.path ? `- ${c.name}: ${
|
|
6713
|
+
lines.push(c.path ? `- ${c.name}: ${code2(c.path)}` : `- ${c.name}: not written (excluded by the selection). ${code2(`spec-layer show component "${c.name}"`)} prints it.`);
|
|
3972
6714
|
}
|
|
3973
6715
|
lines.push("");
|
|
3974
6716
|
}
|
|
@@ -3979,42 +6721,43 @@ function commandsSection() {
|
|
|
3979
6721
|
const cell = (s) => s.replace(/\|/g, "\\|");
|
|
3980
6722
|
lines.push("| Command | What it does | When | Network | Key | Writes |", "|---|---|---|---|---|---|");
|
|
3981
6723
|
for (const t of TOOLS) {
|
|
3982
|
-
lines.push(`| ${
|
|
6724
|
+
lines.push(`| ${code2(cell(t.usage))} | ${cell(t.summary)} | ${cell(t.when)} | ${t.network ? "yes" : "no"} | ${t.needsKey ? "required" : "no"} | ${t.writes.length ? t.writes.map((w) => code2(cell(w))).join(", ") : "nothing"} |`);
|
|
3983
6725
|
}
|
|
3984
6726
|
lines.push("");
|
|
3985
6727
|
lines.push("Exit codes:", "");
|
|
3986
6728
|
for (const t of TOOLS) {
|
|
3987
|
-
lines.push(`- ${
|
|
6729
|
+
lines.push(`- ${code2(t.name)}: ${Object.entries(t.exits).map(([c, m]) => `${c} = ${m}`).join("; ")}.`);
|
|
3988
6730
|
}
|
|
3989
6731
|
lines.push("");
|
|
3990
|
-
for (const f of GLOBAL_FLAGS) lines.push(`- ${
|
|
6732
|
+
for (const f of GLOBAL_FLAGS) lines.push(`- ${code2(f.flag)}: ${f.summary}`);
|
|
3991
6733
|
lines.push("", KEY_RESOLUTION, "");
|
|
3992
|
-
lines.push(`Run ${
|
|
6734
|
+
lines.push(`Run ${code2("npx --yes spec-layer <command>")} in an unattended session so npx does not stop to ask before downloading the package. ${code2("spec-layer tools --json")} prints this table for machines.`, "");
|
|
3993
6735
|
return lines;
|
|
3994
6736
|
}
|
|
3995
6737
|
function buildSkillGuide(input) {
|
|
3996
6738
|
const { outDir } = input;
|
|
3997
6739
|
const lines = [];
|
|
6740
|
+
const markdown = (input.pull?.componentSpecsFormat ?? input.config?.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT) === "md";
|
|
3998
6741
|
lines.push("# Spec Layer: design-system context for this repository", "");
|
|
3999
6742
|
lines.push(
|
|
4000
|
-
`The Spec Layer Figma plugin publishes a design system's components, variables, and styles as data. The ${
|
|
6743
|
+
`The Spec Layer Figma plugin publishes a design system's components, variables, and styles as data. The ${code2("spec-layer")} CLI (version ${input.version}) pulls that data into this repository under ${code2(outDir + "/")}. Everything in those files is extracted deterministically from Figma and validated against a published schema, ` + (markdown ? `with two exceptions that can carry model-written prose: a section of a component page that says it was written by AI, or a component's or the foundation's ${code2("guidelines")} block, marked ${code2("origin: generated")}; and a token group's ${code2("$description")}, which carries no marker ` : `with two exceptions that can carry model-written prose: a component's or the foundation's ${code2("guidelines")} block, marked ${code2("origin: generated")}, and a token group's ${code2("$description")}, which carries no marker `) + "and can be model-written even though it looks like an ordinary field. Treat the rest as the source of truth for what the design system contains, and treat anything it does not state as unknown rather than as something to infer.",
|
|
4001
6744
|
""
|
|
4002
6745
|
);
|
|
4003
6746
|
const componentSpecsDir = input.pull?.componentSpecsDir ?? input.config?.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
|
|
4004
6747
|
lines.push("## How to use it", "");
|
|
4005
|
-
lines.push(`1. Run ${
|
|
4006
|
-
lines.push(`2. Building or changing a component: read its YAML under ${
|
|
4007
|
-
lines.push(`3. Working with colors, spacing, type, or effects: start at ${
|
|
6748
|
+
lines.push(`1. Run ${code2("npx spec-layer status")}. Exit 0 means the local copy is current; exit 2 means run ${code2("npx spec-layer pull")} first.`);
|
|
6749
|
+
lines.push(markdown ? `2. Building or changing a component: read its page under ${code2(`${componentSpecsDir}/`)}, or ${code2("npx spec-layer show component NAME")}. **Properties** gives variants, states, booleans, and slots; **Anatomy** names the parts; **Token bindings** says which token each part's property uses and under which **When** conditions; **Unbound values** lists values that are hardcoded in Figma.` : `2. Building or changing a component: read its YAML under ${code2(`${componentSpecsDir}/`)}, or ${code2("npx spec-layer show component NAME")}. ${code2("api")} gives variants, states, booleans, and slots; ${code2("anatomy")} names the parts; ${code2("references.bindings")} says which token each part's property uses and under which ${code2("when")} conditions; ${code2("unbound")} lists values that are hardcoded in Figma.`);
|
|
6750
|
+
lines.push(`3. Working with colors, spacing, type, or effects: start at ${code2(`${outDir}/tokens/resolver.json`)}, load the set and mode files it names, and look up ${code2("code_syntax")} in ${code2("spec-layer.meta.json")} for the name the designer declared for your platform.`);
|
|
4008
6751
|
lines.push(`4. Reference tokens by name in code; never paste a resolved value where a token exists. A value the design system does not define is not a token: say so in your change rather than adding one.`);
|
|
4009
|
-
lines.push(`5. An ${
|
|
6752
|
+
lines.push(markdown ? "5. A row under **Unbound values** is design debt reported from Figma. Do not silently promote it to a token; keep the literal and note that Figma has no binding for it." : `5. An ${code2("unbound")} entry is design debt reported from Figma. Do not silently promote it to a token; keep the literal and note that Figma has no binding for it.`);
|
|
4010
6753
|
const writtenOutputs = input.pull?.outputs.filter((o) => o.written) ?? [];
|
|
4011
|
-
const outputNote = writtenOutputs.length ? ` Never edit ${writtenOutputs.map((o) =>
|
|
4012
|
-
lines.push(`6. Never edit files under ${
|
|
6754
|
+
const outputNote = writtenOutputs.length ? ` Never edit ${writtenOutputs.map((o) => code2(`${o.path}/`)).join(", ")} either: pull replaces or removes files there.` : "";
|
|
6755
|
+
lines.push(`6. Never edit files under ${code2(outDir + "/")} or ${code2(componentSpecsDir + "/")}: the next pull replaces or removes them.${outputNote} Configuration lives in ${code2("speclayer.json")}. Never commit ${code2(CREDENTIALS_NAME)}, and never print or copy the pull key.`);
|
|
4013
6756
|
lines.push("");
|
|
4014
6757
|
lines.push(...pullSection(input));
|
|
4015
6758
|
lines.push(...stackSection(input));
|
|
4016
6759
|
lines.push(...commandsSection());
|
|
4017
|
-
lines.push(`Generated by ${
|
|
6760
|
+
lines.push(`Generated by ${code2("spec-layer skill")}. Re-run ${code2("npx spec-layer skill --install")} after a pull that adds components, after changing ${code2("outputs")} or ${code2("componentSpecsDir")}, or when the codebase changes stack; the file is replaced, not appended.`);
|
|
4018
6761
|
return `${lines.join("\n")}
|
|
4019
6762
|
`;
|
|
4020
6763
|
}
|
|
@@ -4087,30 +6830,27 @@ ${BLOCK_END}
|
|
|
4087
6830
|
const sep2 = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
4088
6831
|
return `${existing}${sep2}${block}`;
|
|
4089
6832
|
}
|
|
6833
|
+
var SNAPSHOT_DIRS = ["components", "tokens"];
|
|
6834
|
+
function staleSnapshotDirs(cwd, target) {
|
|
6835
|
+
if (target.host !== "claude") return [];
|
|
6836
|
+
const dir = dirname3(target.path);
|
|
6837
|
+
return SNAPSHOT_DIRS.map((name) => `${dir}/${name}`).filter((rel) => existsSync7(join7(cwd, rel)));
|
|
6838
|
+
}
|
|
4090
6839
|
function installSkill(cwd, host, guide) {
|
|
4091
6840
|
const target = installTarget(host);
|
|
4092
6841
|
const abs = join7(cwd, target.path);
|
|
4093
|
-
const existing = existsSync7(abs) ?
|
|
6842
|
+
const existing = existsSync7(abs) ? readFileSync8(abs, "utf8") : null;
|
|
6843
|
+
const staleSnapshot = staleSnapshotDirs(cwd, target);
|
|
4094
6844
|
const next = target.mode === "file" ? renderForHost(host, guide) : upsertBlock(existing, renderForHost(host, guide));
|
|
4095
|
-
if (existing === next) return { path: target.path, result: "unchanged" };
|
|
6845
|
+
if (existing === next) return { path: target.path, result: "unchanged", staleSnapshot };
|
|
4096
6846
|
mkdirSync3(dirname3(abs), { recursive: true });
|
|
4097
6847
|
writeFileSync6(abs, next);
|
|
4098
|
-
return { path: target.path, result: existing === null ? "created" : "updated" };
|
|
4099
|
-
}
|
|
4100
|
-
|
|
4101
|
-
// src/version.ts
|
|
4102
|
-
import { readFileSync as readFileSync8 } from "node:fs";
|
|
4103
|
-
function cliVersion() {
|
|
4104
|
-
try {
|
|
4105
|
-
const parsed = JSON.parse(readFileSync8(new URL("../package.json", import.meta.url), "utf8"));
|
|
4106
|
-
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
4107
|
-
} catch {
|
|
4108
|
-
return "unknown";
|
|
4109
|
-
}
|
|
6848
|
+
return { path: target.path, result: existing === null ? "created" : "updated", staleSnapshot };
|
|
4110
6849
|
}
|
|
4111
6850
|
|
|
4112
6851
|
// src/commands.ts
|
|
4113
6852
|
var NO_LOCAL_PULL = "No local pull found. Run spec-layer pull.";
|
|
6853
|
+
var FORMAT_NAME = { yaml: "YAML", md: "Markdown" };
|
|
4114
6854
|
function manifestReader() {
|
|
4115
6855
|
const cache = /* @__PURE__ */ new Map();
|
|
4116
6856
|
return (outDir) => {
|
|
@@ -4121,7 +6861,7 @@ function manifestReader() {
|
|
|
4121
6861
|
function sameOutput(a, b) {
|
|
4122
6862
|
const selectionKey = (s) => JSON.stringify([s.foundation, s.components === null ? null : [...new Set(s.components.map(slugify))].sort()]);
|
|
4123
6863
|
const key = (v) => JSON.stringify(sortKeys(v ?? {}));
|
|
4124
|
-
return selectionKey(a.selection) === selectionKey(b.selection) && key(a.dtcg) === key(b.dtcg) && key(a.outputs ?? []) === key(b.outputs ?? []) && (a.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR) === (b.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR);
|
|
6864
|
+
return selectionKey(a.selection) === selectionKey(b.selection) && key(a.dtcg) === key(b.dtcg) && key(a.outputs ?? []) === key(b.outputs ?? []) && (a.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR) === (b.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR) && (a.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT) === (b.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT);
|
|
4125
6865
|
}
|
|
4126
6866
|
function sortKeys(value) {
|
|
4127
6867
|
if (Array.isArray(value)) return value.map(sortKeys);
|
|
@@ -4142,6 +6882,13 @@ function platformsFromFlags(flags, io2) {
|
|
|
4142
6882
|
}
|
|
4143
6883
|
return out;
|
|
4144
6884
|
}
|
|
6885
|
+
function componentFormatFromFlags(flags, io2) {
|
|
6886
|
+
const value = flags["component-format"];
|
|
6887
|
+
if (value === void 0) return void 0;
|
|
6888
|
+
if (isComponentFormat(value)) return value;
|
|
6889
|
+
io2.err(`--component-format takes ${COMPONENT_FORMATS.join(" or ")}, not "${value}".`);
|
|
6890
|
+
return null;
|
|
6891
|
+
}
|
|
4145
6892
|
function resolvePlatforms(cwd, fromFlags, config) {
|
|
4146
6893
|
if (fromFlags) return { platforms: fromFlags, source: "flag" };
|
|
4147
6894
|
if (config?.platforms && config.platforms.length > 0) return { platforms: config.platforms, source: "config" };
|
|
@@ -4174,6 +6921,8 @@ function runInit(cwd, flags, io2) {
|
|
|
4174
6921
|
}
|
|
4175
6922
|
const fromFlags = platformsFromFlags(flags, io2);
|
|
4176
6923
|
if (fromFlags === null) return 1;
|
|
6924
|
+
const format = componentFormatFromFlags(flags, io2);
|
|
6925
|
+
if (format === null) return 1;
|
|
4177
6926
|
const { platforms, source } = resolvePlatforms(cwd, fromFlags, null);
|
|
4178
6927
|
const outputs = defaultOutputs(platforms);
|
|
4179
6928
|
const outDir = flags.out ?? DEFAULT_OUT_DIR;
|
|
@@ -4181,6 +6930,7 @@ function runInit(cwd, flags, io2) {
|
|
|
4181
6930
|
libraryId: flags.id,
|
|
4182
6931
|
outDir,
|
|
4183
6932
|
componentSpecsDir: DEFAULT_COMPONENT_SPECS_DIR,
|
|
6933
|
+
...format ? { componentSpecsFormat: format } : {},
|
|
4184
6934
|
...include ? { include } : {},
|
|
4185
6935
|
...platforms.length > 0 ? { platforms } : {},
|
|
4186
6936
|
...outputs.length > 0 ? { outputs } : {}
|
|
@@ -4252,16 +7002,20 @@ async function runSetup(cwd, flags, env, io2, fetcher) {
|
|
|
4252
7002
|
}
|
|
4253
7003
|
const fromFlags = platformsFromFlags(flags, io2);
|
|
4254
7004
|
if (fromFlags === null) return 1;
|
|
7005
|
+
const format = componentFormatFromFlags(flags, io2);
|
|
7006
|
+
if (format === null) return 1;
|
|
4255
7007
|
const outDir = flags.out ?? existing?.outDir ?? DEFAULT_OUT_DIR;
|
|
4256
7008
|
const componentSpecsDir = existing?.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
|
|
4257
7009
|
const keptInclude = include ?? existing?.include ?? null;
|
|
4258
7010
|
const keptDtcg = existing?.dtcg ?? null;
|
|
7011
|
+
const keptFormat = format ?? existing?.componentSpecsFormat ?? null;
|
|
4259
7012
|
const { platforms } = resolvePlatforms(cwd, fromFlags, existing);
|
|
4260
7013
|
const outputs = withDefaults(existing?.outputs ?? [], platforms);
|
|
4261
7014
|
writeConfig(cwd, {
|
|
4262
7015
|
libraryId: flags.id,
|
|
4263
7016
|
outDir,
|
|
4264
7017
|
componentSpecsDir,
|
|
7018
|
+
...keptFormat ? { componentSpecsFormat: keptFormat } : {},
|
|
4265
7019
|
...keptInclude ? { include: keptInclude } : {},
|
|
4266
7020
|
...keptDtcg ? { dtcg: keptDtcg } : {},
|
|
4267
7021
|
...platforms.length > 0 ? { platforms } : {},
|
|
@@ -4305,8 +7059,8 @@ git rm --cached ${ignored.line}`);
|
|
|
4305
7059
|
}
|
|
4306
7060
|
const { replaced } = writeCredentials(cwd, { libraryId: flags.id, key });
|
|
4307
7061
|
io2.out(replaced ? `Replaced the stored key in ${CREDENTIALS_NAME}.` : `Stored the pull key in ${CREDENTIALS_NAME}.`);
|
|
4308
|
-
const
|
|
4309
|
-
if (
|
|
7062
|
+
const code3 = await runPull(cwd, { ...flags, key }, env, io2, fetcher);
|
|
7063
|
+
if (code3 !== 0) return code3;
|
|
4310
7064
|
const hosts = detectRepo(cwd).agents;
|
|
4311
7065
|
io2.out("");
|
|
4312
7066
|
io2.out("Next step for a coding agent: npx spec-layer skill --install");
|
|
@@ -4315,11 +7069,62 @@ git rm --cached ${ignored.line}`);
|
|
|
4315
7069
|
return 0;
|
|
4316
7070
|
}
|
|
4317
7071
|
function outputFilesOnDisk(cwd, outDir, o) {
|
|
4318
|
-
const
|
|
4319
|
-
|
|
7072
|
+
const id = outputId(o);
|
|
7073
|
+
for (const rel of [`${id}.map.json`, `${id}.report.json`]) {
|
|
7074
|
+
if (!existsSync8(join8(cwd, outDir, "outputs", rel))) return false;
|
|
7075
|
+
}
|
|
4320
7076
|
const imports = readIndexImports(cwd, o);
|
|
4321
7077
|
return imports !== null && imports.every((f) => existsSync8(resolve5(cwd, o.path, f)));
|
|
4322
7078
|
}
|
|
7079
|
+
function foundationFilesOnDisk(cwd, outDir) {
|
|
7080
|
+
return ["fonts.json", join8("tokens", "report.json"), join8("tokens", "resolver.json")].every((rel) => existsSync8(join8(cwd, outDir, rel)));
|
|
7081
|
+
}
|
|
7082
|
+
function readJsonArray(path) {
|
|
7083
|
+
if (!existsSync8(path)) return [];
|
|
7084
|
+
try {
|
|
7085
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
7086
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
7087
|
+
} catch {
|
|
7088
|
+
return [];
|
|
7089
|
+
}
|
|
7090
|
+
}
|
|
7091
|
+
function readReportSeverities(path) {
|
|
7092
|
+
return readJsonArray(path).map((entry2) => entry2.severity).filter((s) => s === "error" || s === "warning" || s === "info");
|
|
7093
|
+
}
|
|
7094
|
+
function readFontFamilies(cwd, outDir) {
|
|
7095
|
+
return readJsonArray(join8(cwd, outDir, "fonts.json")).map((entry2) => entry2.family).filter((f) => typeof f === "string");
|
|
7096
|
+
}
|
|
7097
|
+
function plural(n, word) {
|
|
7098
|
+
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
7099
|
+
}
|
|
7100
|
+
function printReportSummary(cwd, outDir, outputs, io2) {
|
|
7101
|
+
const reportPaths = [];
|
|
7102
|
+
let errors = 0;
|
|
7103
|
+
let warnings = 0;
|
|
7104
|
+
const add = (severities, path) => {
|
|
7105
|
+
if (severities.length === 0) return;
|
|
7106
|
+
errors += severities.filter((s) => s === "error").length;
|
|
7107
|
+
warnings += severities.filter((s) => s === "warning").length;
|
|
7108
|
+
reportPaths.push(path);
|
|
7109
|
+
};
|
|
7110
|
+
add(readReportSeverities(join8(cwd, outDir, "tokens", "report.json")), `${outDir}/tokens/report.json`);
|
|
7111
|
+
for (const o of outputs) {
|
|
7112
|
+
add(readReportSeverities(join8(cwd, outDir, "outputs", `${outputId(o)}.report.json`)), `${outDir}/outputs/${outputId(o)}.report.json`);
|
|
7113
|
+
}
|
|
7114
|
+
if (errors > 0 || warnings > 0) {
|
|
7115
|
+
io2.err(`${plural(errors, "error")}, ${plural(warnings, "warning")} in the token output. See ${reportPaths.join(", ")}.`);
|
|
7116
|
+
}
|
|
7117
|
+
const fontFamilies = readFontFamilies(cwd, outDir);
|
|
7118
|
+
if (fontFamilies.length > 0) {
|
|
7119
|
+
for (const family of missingFontSourcesInRepo(fontFamilies, cwd)) {
|
|
7120
|
+
io2.err(`This library needs ${family}, and nothing in this repository loads it. See fonts.json.`);
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
return errors;
|
|
7124
|
+
}
|
|
7125
|
+
function publishedPhrase(version, publishedAt) {
|
|
7126
|
+
return version ? `(v${version}, published ${publishedAt})` : `(published ${publishedAt})`;
|
|
7127
|
+
}
|
|
4323
7128
|
async function runPull(cwd, flags, env, io2, fetcher) {
|
|
4324
7129
|
const manifestAt = manifestReader();
|
|
4325
7130
|
const opts = resolved(cwd, flags, env, io2, manifestAt);
|
|
@@ -4333,16 +7138,25 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
4333
7138
|
}
|
|
4334
7139
|
const fromFlags = platformsFromFlags(flags, io2);
|
|
4335
7140
|
if (fromFlags === null) return 1;
|
|
7141
|
+
const flagFormat = componentFormatFromFlags(flags, io2);
|
|
7142
|
+
if (flagFormat === null) return 1;
|
|
7143
|
+
const componentSpecsFormat = flagFormat ?? opts.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT;
|
|
4336
7144
|
const { platforms, source } = resolvePlatforms(cwd, fromFlags, opts);
|
|
4337
7145
|
const outputs = outputsForRun(fromFlags, opts, platforms);
|
|
4338
7146
|
const manifest = manifestAt(join8(cwd, opts.outDir));
|
|
4339
7147
|
const foundationOnDisk = Boolean(manifest?.artifacts.find((a) => a.kind === "foundation")?.path);
|
|
4340
7148
|
const willWriteFoundation = selection.foundation && foundationOnDisk;
|
|
4341
7149
|
const briefsOnDisk = (manifest?.artifacts ?? []).filter((a) => a.kind === "component" && a.path !== null).every((a) => existsSync8(resolve5(cwd, a.path)));
|
|
4342
|
-
const etag = manifest && sameOutput(
|
|
4343
|
-
{
|
|
4344
|
-
|
|
4345
|
-
|
|
7150
|
+
const etag = manifest && manifest.cliVersion === cliVersion() && sameOutput(
|
|
7151
|
+
{
|
|
7152
|
+
selection: manifest.selection ?? DEFAULT_SELECTION,
|
|
7153
|
+
dtcg: manifest.dtcg,
|
|
7154
|
+
outputs: manifest.outputs,
|
|
7155
|
+
componentSpecsDir: manifest.componentSpecsDir,
|
|
7156
|
+
componentSpecsFormat: manifest.componentSpecsFormat
|
|
7157
|
+
},
|
|
7158
|
+
{ selection, dtcg: opts.dtcg, outputs, componentSpecsDir: opts.componentSpecsDir, componentSpecsFormat }
|
|
7159
|
+
) && briefsOnDisk && (!willWriteFoundation || foundationFilesOnDisk(cwd, opts.outDir)) && (!willWriteFoundation || outputs.every((o) => outputFilesOnDisk(cwd, opts.outDir, o))) ? manifest.bundleHash : void 0;
|
|
4346
7160
|
const result = await fetchBundle({
|
|
4347
7161
|
api: opts.api,
|
|
4348
7162
|
libraryId: opts.libraryId,
|
|
@@ -4355,7 +7169,9 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
4355
7169
|
return 1;
|
|
4356
7170
|
}
|
|
4357
7171
|
if (result.kind === "not_modified") {
|
|
4358
|
-
io2.out(`Already up to date (
|
|
7172
|
+
io2.out(`Already up to date ${publishedPhrase(result.version ?? manifest?.version, manifest?.publishedAt ?? "unknown")}.`);
|
|
7173
|
+
const cachedErrors = printReportSummary(cwd, opts.outDir, outputs, io2);
|
|
7174
|
+
if (flags.strict && cachedErrors > 0) return 1;
|
|
4359
7175
|
return 0;
|
|
4360
7176
|
}
|
|
4361
7177
|
let written;
|
|
@@ -4373,16 +7189,18 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
4373
7189
|
libraryId: opts.libraryId,
|
|
4374
7190
|
publishedAt: result.publishedAt,
|
|
4375
7191
|
bundleHash: result.bundleHash,
|
|
7192
|
+
version: result.version,
|
|
4376
7193
|
dtcg: opts.dtcg,
|
|
4377
7194
|
platforms,
|
|
4378
7195
|
outputs,
|
|
4379
|
-
componentSpecsDir: opts.componentSpecsDir
|
|
7196
|
+
componentSpecsDir: opts.componentSpecsDir,
|
|
7197
|
+
componentSpecsFormat
|
|
4380
7198
|
});
|
|
4381
7199
|
written = writeResult.written;
|
|
4382
7200
|
componentSpecs = writeResult.componentSpecs;
|
|
4383
7201
|
outputResults = writeResult.outputs;
|
|
4384
7202
|
io2.out(
|
|
4385
|
-
`Pulled ${bundle.fileName ?? opts.libraryId}: ${describePull(bundle, selection, selected)}
|
|
7203
|
+
`Pulled ${bundle.fileName ?? opts.libraryId}: ${describePull(bundle, selection, selected)} ${publishedPhrase(result.version, result.publishedAt)}.`
|
|
4386
7204
|
);
|
|
4387
7205
|
} catch (err) {
|
|
4388
7206
|
io2.err(errorText(err));
|
|
@@ -4390,7 +7208,10 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
4390
7208
|
}
|
|
4391
7209
|
const count = (n) => `${n} file${n === 1 ? "" : "s"}`;
|
|
4392
7210
|
io2.out(`Wrote ${written.length} files under ${opts.outDir}/.`);
|
|
4393
|
-
if (componentSpecs.files.length > 0)
|
|
7211
|
+
if (componentSpecs.files.length > 0) {
|
|
7212
|
+
const n = componentSpecs.files.length;
|
|
7213
|
+
io2.out(`Wrote ${componentSpecs.path}/ (${n} ${FORMAT_NAME[componentSpecsFormat]} file${n === 1 ? "" : "s"}).`);
|
|
7214
|
+
}
|
|
4394
7215
|
for (const r of outputResults) {
|
|
4395
7216
|
const o = outputs.find((x) => x.path === r.path);
|
|
4396
7217
|
if (o) io2.out(`Wrote ${r.path}/ (${count(r.files.length)}, ${o.platform}/${o.format}, ${o.case} names).`);
|
|
@@ -4410,6 +7231,8 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
4410
7231
|
const missing = platformsMissingFormat(platforms);
|
|
4411
7232
|
if (missing.length > 0) io2.out(missingFormatNote(missing));
|
|
4412
7233
|
}
|
|
7234
|
+
const errors = printReportSummary(cwd, opts.outDir, outputs, io2);
|
|
7235
|
+
if (flags.strict && errors > 0) return 1;
|
|
4413
7236
|
return 0;
|
|
4414
7237
|
}
|
|
4415
7238
|
async function runStatus(cwd, flags, env, io2, fetcher) {
|
|
@@ -4433,10 +7256,10 @@ async function runStatus(cwd, flags, env, io2, fetcher) {
|
|
|
4433
7256
|
return 1;
|
|
4434
7257
|
}
|
|
4435
7258
|
if (result.kind === "not_modified") {
|
|
4436
|
-
io2.out(`Up to date (
|
|
7259
|
+
io2.out(`Up to date ${publishedPhrase(result.version ?? manifest.version, manifest.publishedAt)}.`);
|
|
4437
7260
|
return 0;
|
|
4438
7261
|
}
|
|
4439
|
-
io2.out(`Behind: remote published ${result.publishedAt}. Run spec-layer pull.`);
|
|
7262
|
+
io2.out(result.version ? `Behind: remote is v${result.version}, published ${result.publishedAt}. Run spec-layer pull.` : `Behind: remote published ${result.publishedAt}. Run spec-layer pull.`);
|
|
4440
7263
|
return 2;
|
|
4441
7264
|
}
|
|
4442
7265
|
function runList(cwd, flags, io2) {
|
|
@@ -4447,7 +7270,7 @@ function runList(cwd, flags, io2) {
|
|
|
4447
7270
|
io2.err(NO_LOCAL_PULL);
|
|
4448
7271
|
return 1;
|
|
4449
7272
|
}
|
|
4450
|
-
io2.out(`Library ${manifest.libraryId}, published ${manifest.publishedAt}.`);
|
|
7273
|
+
io2.out(manifest.version ? `Library ${manifest.libraryId}, v${manifest.version}, published ${manifest.publishedAt}.` : `Library ${manifest.libraryId}, published ${manifest.publishedAt}.`);
|
|
4451
7274
|
const rows = manifest.artifacts.map((a) => [a.kind, a.name, a.path ?? "not written", a.contentHash]);
|
|
4452
7275
|
const widths = [0, 1, 2].map((i) => Math.max(...rows.map((r) => r[i].length)));
|
|
4453
7276
|
for (const row of rows) {
|
|
@@ -4468,6 +7291,12 @@ function runShow(cwd, flags, args, io2) {
|
|
|
4468
7291
|
io2.err(SHOW_USAGE);
|
|
4469
7292
|
return 1;
|
|
4470
7293
|
}
|
|
7294
|
+
const flagFormat = componentFormatFromFlags(flags, io2);
|
|
7295
|
+
if (flagFormat === null) return 1;
|
|
7296
|
+
if (wantsFoundation && flagFormat !== void 0) {
|
|
7297
|
+
io2.err("--component-format applies to components. The Foundation prints as its DTCG document.");
|
|
7298
|
+
return 1;
|
|
7299
|
+
}
|
|
4471
7300
|
const outDir = resolvedOutDir(cwd, flags, io2);
|
|
4472
7301
|
if (!outDir) return 1;
|
|
4473
7302
|
let bundle;
|
|
@@ -4482,6 +7311,7 @@ function runShow(cwd, flags, args, io2) {
|
|
|
4482
7311
|
return 1;
|
|
4483
7312
|
}
|
|
4484
7313
|
let entry2;
|
|
7314
|
+
let component = null;
|
|
4485
7315
|
if (wantsFoundation) {
|
|
4486
7316
|
if (!bundle.foundation) {
|
|
4487
7317
|
io2.err("This library has no Foundation. Run spec-layer list to see what it holds.");
|
|
@@ -4501,9 +7331,33 @@ Available: ${available || "none"}.`);
|
|
|
4501
7331
|
return 1;
|
|
4502
7332
|
}
|
|
4503
7333
|
entry2 = matches[0];
|
|
7334
|
+
component = matches[0];
|
|
7335
|
+
}
|
|
7336
|
+
if (flags.canonical) {
|
|
7337
|
+
io2.write(`${JSON.stringify(entry2.artifact, null, 2)}
|
|
7338
|
+
`);
|
|
7339
|
+
return 0;
|
|
7340
|
+
}
|
|
7341
|
+
if (component) {
|
|
7342
|
+
let configFormat;
|
|
7343
|
+
try {
|
|
7344
|
+
configFormat = readConfig(cwd)?.componentSpecsFormat;
|
|
7345
|
+
} catch (err) {
|
|
7346
|
+
io2.err(errorText(err));
|
|
7347
|
+
return 1;
|
|
7348
|
+
}
|
|
7349
|
+
const format = flagFormat ?? configFormat ?? readManifest(outDir)?.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT;
|
|
7350
|
+
if (format === "md") {
|
|
7351
|
+
try {
|
|
7352
|
+
io2.write(componentMarkdownPage(component));
|
|
7353
|
+
} catch (err) {
|
|
7354
|
+
io2.err(errorText(err));
|
|
7355
|
+
return 1;
|
|
7356
|
+
}
|
|
7357
|
+
return 0;
|
|
7358
|
+
}
|
|
4504
7359
|
}
|
|
4505
|
-
io2.write(
|
|
4506
|
-
` : entry2.ai);
|
|
7360
|
+
io2.write(entry2.ai);
|
|
4507
7361
|
return 0;
|
|
4508
7362
|
}
|
|
4509
7363
|
function runTools(flags, io2) {
|
|
@@ -4575,6 +7429,11 @@ function runSkill(cwd, flags, io2) {
|
|
|
4575
7429
|
}
|
|
4576
7430
|
const verb = outcome.result === "created" ? "Wrote" : outcome.result === "updated" ? "Updated" : "Unchanged:";
|
|
4577
7431
|
io2.out(`${verb} ${outcome.path} (${host}, ${chosen}).`);
|
|
7432
|
+
if (outcome.staleSnapshot.length > 0) {
|
|
7433
|
+
io2.out(
|
|
7434
|
+
`A downloaded snapshot is still in ${outcome.staleSnapshot.join(" and ")}. The guide above supersedes it, and those folders can be deleted.`
|
|
7435
|
+
);
|
|
7436
|
+
}
|
|
4578
7437
|
}
|
|
4579
7438
|
if (!input.pull) io2.out(`No local pull yet, so the guide lists no components. Run spec-layer pull, then spec-layer skill --install again.`);
|
|
4580
7439
|
if (input.platformSource === "none") io2.out(`No target platform detected. Pass --platform ${PLATFORMS.join("|")} to write platform-specific token advice.`);
|
|
@@ -4585,16 +7444,17 @@ function runSkill(cwd, flags, io2) {
|
|
|
4585
7444
|
var USAGE = `spec-layer <command>
|
|
4586
7445
|
|
|
4587
7446
|
Commands:
|
|
4588
|
-
setup --id lib_... --key sl_... [--out DIR] [selection] [--platform P]...
|
|
7447
|
+
setup --id lib_... --key sl_... [--out DIR] [selection] [--platform P]... [--component-format F]
|
|
4589
7448
|
store the key, then pull
|
|
4590
|
-
init --id lib_... [--out DIR] [selection] [--platform P]...
|
|
7449
|
+
init --id lib_... [--out DIR] [selection] [--platform P]... [--component-format F]
|
|
4591
7450
|
write speclayer.json
|
|
4592
|
-
pull [--id lib_...] [--key sl_...] [selection] [--platform P]...
|
|
4593
|
-
fetch the library into DIR (default .speclayer); the foundation lands as DTCG under DIR/tokens
|
|
7451
|
+
pull [--id lib_...] [--key sl_...] [selection] [--platform P]... [--component-format F] [--strict]
|
|
7452
|
+
fetch the library into DIR (default .speclayer); the foundation lands as DTCG under DIR/tokens/;
|
|
7453
|
+
--strict exits 1 when tokens/report.json or an outputs/*.report.json holds an error-severity entry, even on a cached pull (default exit stays 0)
|
|
4594
7454
|
status [--id lib_...] [--key sl_...] check freshness; exits 2 when behind
|
|
4595
7455
|
list list every artifact in the last pull
|
|
4596
|
-
show foundation | component NAME [--canonical]
|
|
4597
|
-
print one artifact (foundation: the DTCG document; component: its AI YAML; --canonical for JSON)
|
|
7456
|
+
show foundation | component NAME [--component-format F] [--canonical]
|
|
7457
|
+
print one artifact (foundation: the DTCG document; component: its AI YAML or Markdown; --canonical for JSON)
|
|
4598
7458
|
tools [--json] list every command with what it reaches and writes
|
|
4599
7459
|
skill [--install] [--agent HOST]... [--platform P]... [--json]
|
|
4600
7460
|
print a guide for a coding agent, adapted to this repo and the last pull;
|
|
@@ -4607,6 +7467,7 @@ Selection (setup, pull and init; flags replace the include block in speclayer.js
|
|
|
4607
7467
|
Options:
|
|
4608
7468
|
--api URL override the API origin (default https://api.spec-layer.com)
|
|
4609
7469
|
--platform web|ios|android|flutter the target this repo builds for (repeatable); applies to setup, init, pull, and skill; setup and init store it, pull uses it for the run
|
|
7470
|
+
--component-format yaml|md how component-specs/ is written and show prints a component (default yaml); setup and init store it, pull and show use it for the run
|
|
4610
7471
|
The pull key comes from --key, SPEC_LAYER_KEY, or speclayer.local.json written by setup.`;
|
|
4611
7472
|
var io = {
|
|
4612
7473
|
out: (l) => console.log(l),
|
|
@@ -4631,8 +7492,10 @@ async function main() {
|
|
|
4631
7492
|
canonical: { type: "boolean" },
|
|
4632
7493
|
json: { type: "boolean" },
|
|
4633
7494
|
install: { type: "boolean" },
|
|
7495
|
+
strict: { type: "boolean" },
|
|
4634
7496
|
agent: { type: "string", multiple: true },
|
|
4635
|
-
platform: { type: "string", multiple: true }
|
|
7497
|
+
platform: { type: "string", multiple: true },
|
|
7498
|
+
"component-format": { type: "string" }
|
|
4636
7499
|
}
|
|
4637
7500
|
}));
|
|
4638
7501
|
} catch {
|