spec-layer 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -16
- package/dist/cli.js +2556 -210
- 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;
|
|
@@ -562,6 +562,609 @@ import { parseArgs } from "node:util";
|
|
|
562
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 = [
|
|
@@ -845,6 +2109,14 @@ function canonicalJson(value) {
|
|
|
845
2109
|
}
|
|
846
2110
|
return JSON.stringify(value);
|
|
847
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
|
+
}
|
|
848
2120
|
|
|
849
2121
|
// ../extractor/src/v5/validate.ts
|
|
850
2122
|
var ROOT = "<artifact>";
|
|
@@ -1322,46 +2594,392 @@ function validateRootSections(artifact, out) {
|
|
|
1322
2594
|
}
|
|
1323
2595
|
}
|
|
1324
2596
|
}
|
|
1325
|
-
function validateLevel1(artifact) {
|
|
1326
|
-
try {
|
|
1327
|
-
const out = [];
|
|
1328
|
-
if (!isRecord(artifact)) {
|
|
1329
|
-
out.push(shape(ROOT, "The artifact root must be an object."));
|
|
1330
|
-
return out;
|
|
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);
|
|
2780
|
+
}
|
|
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) };
|
|
2798
|
+
}
|
|
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
|
|
1331
2837
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
2838
|
+
};
|
|
2839
|
+
}
|
|
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}`;
|
|
2877
|
+
}
|
|
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}`;
|
|
1337
2882
|
}
|
|
1338
|
-
return out;
|
|
1339
|
-
} catch (err) {
|
|
1340
|
-
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.";
|
|
1341
|
-
return [diagnostic("INCONSISTENT_VALUE_SHAPE", {
|
|
1342
|
-
entity_id: "artifact",
|
|
1343
|
-
message
|
|
1344
|
-
})];
|
|
1345
2883
|
}
|
|
2884
|
+
return String(value);
|
|
1346
2885
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
const
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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);
|
|
1359
2929
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
+
};
|
|
1365
2983
|
}
|
|
1366
2984
|
|
|
1367
2985
|
// ../extractor/src/v5/dtcg.ts
|
|
@@ -2318,6 +3936,598 @@ function dtcgExportFiles(out) {
|
|
|
2318
3936
|
return files;
|
|
2319
3937
|
}
|
|
2320
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
|
+
|
|
2321
4531
|
// ../extractor/src/v5/usageUnits.ts
|
|
2322
4532
|
var LENGTH_SCOPES = ["CORNER_RADIUS", "WIDTH_HEIGHT", "GAP", "FONT_SIZE", "STROKE_FLOAT"];
|
|
2323
4533
|
var LENGTH_PROPERTIES = [
|
|
@@ -2538,12 +4748,12 @@ function acceptCssDeclared(declared) {
|
|
|
2538
4748
|
if (BARE_IDENT.test(declared)) return `--${declared}`;
|
|
2539
4749
|
return null;
|
|
2540
4750
|
}
|
|
2541
|
-
var
|
|
4751
|
+
var asRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
|
|
2542
4752
|
function collectLeaves(tree, prefix, out) {
|
|
2543
|
-
const node =
|
|
4753
|
+
const node = asRecord2(tree);
|
|
2544
4754
|
if (!node) return;
|
|
2545
4755
|
if ("$value" in node) {
|
|
2546
|
-
const ours =
|
|
4756
|
+
const ours = asRecord2(asRecord2(node.$extensions)?.[EXT]);
|
|
2547
4757
|
out.push({
|
|
2548
4758
|
path: prefix.join("."),
|
|
2549
4759
|
type: typeof node.$type === "string" ? node.$type : "",
|
|
@@ -2559,7 +4769,7 @@ function collectLeaves(tree, prefix, out) {
|
|
|
2559
4769
|
}
|
|
2560
4770
|
var unpointer = (s) => s.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
2561
4771
|
var refFile = (src) => {
|
|
2562
|
-
const r =
|
|
4772
|
+
const r = asRecord2(src);
|
|
2563
4773
|
return r && typeof r.$ref === "string" ? r.$ref : null;
|
|
2564
4774
|
};
|
|
2565
4775
|
function sourcesOf(resolver) {
|
|
@@ -2634,7 +4844,7 @@ function cssValue(ctx, type, value, property) {
|
|
|
2634
4844
|
switch (type) {
|
|
2635
4845
|
case "color": {
|
|
2636
4846
|
if (typeof value === "string") return value;
|
|
2637
|
-
const c =
|
|
4847
|
+
const c = asRecord2(value);
|
|
2638
4848
|
if (c && typeof c.hex === "string" && typeof c.alpha === "number") {
|
|
2639
4849
|
if (c.alpha === 1) return c.hex;
|
|
2640
4850
|
const [r, g, b] = hexChannels(c.hex);
|
|
@@ -2645,7 +4855,7 @@ function cssValue(ctx, type, value, property) {
|
|
|
2645
4855
|
case "dimension":
|
|
2646
4856
|
case "duration": {
|
|
2647
4857
|
if (typeof value === "string") return value;
|
|
2648
|
-
const d =
|
|
4858
|
+
const d = asRecord2(value);
|
|
2649
4859
|
if (d && typeof d.value === "number" && typeof d.unit === "string") return `${d.value}${d.unit}`;
|
|
2650
4860
|
break;
|
|
2651
4861
|
}
|
|
@@ -2707,7 +4917,7 @@ function typographyMemberKeys(value, ext) {
|
|
|
2707
4917
|
}
|
|
2708
4918
|
for (const key of ["lineHeight", "letterSpacing"]) {
|
|
2709
4919
|
if (key in value) continue;
|
|
2710
|
-
const d =
|
|
4920
|
+
const d = asRecord2(ext[key]);
|
|
2711
4921
|
if (d && typeof d.value === "number" && typeof d.unit === "string") keys.push(key);
|
|
2712
4922
|
}
|
|
2713
4923
|
for (const [key, extKey] of TEXT_MEMBERS) {
|
|
@@ -2724,7 +4934,7 @@ function converted(ctx, property, from, to) {
|
|
|
2724
4934
|
});
|
|
2725
4935
|
}
|
|
2726
4936
|
function extensionDimension(ctx, ext, key, name, percentTo) {
|
|
2727
|
-
const d =
|
|
4937
|
+
const d = asRecord2(ext[key]);
|
|
2728
4938
|
if (!d || typeof d.value !== "number" || typeof d.unit !== "string") return null;
|
|
2729
4939
|
if (d.unit === "%") {
|
|
2730
4940
|
const to = percentTo(canonicalNumber(d.value / 100));
|
|
@@ -2736,7 +4946,7 @@ function extensionDimension(ctx, ext, key, name, percentTo) {
|
|
|
2736
4946
|
function typographyDecls(ctx, leaf, names) {
|
|
2737
4947
|
const decls = [];
|
|
2738
4948
|
const declaredPaths = [];
|
|
2739
|
-
const value =
|
|
4949
|
+
const value = asRecord2(leaf.value) ?? {};
|
|
2740
4950
|
const ext = leaf.ext ?? {};
|
|
2741
4951
|
const nameFor = (key) => names.get(`${leaf.path}.${key}`);
|
|
2742
4952
|
for (const [key, type] of TYPOGRAPHY_MEMBERS2) {
|
|
@@ -2769,12 +4979,12 @@ function typographyDecls(ctx, leaf, names) {
|
|
|
2769
4979
|
}
|
|
2770
4980
|
}
|
|
2771
4981
|
}
|
|
2772
|
-
for (const [key, extKey,
|
|
4982
|
+
for (const [key, extKey, table2] of TEXT_MEMBERS) {
|
|
2773
4983
|
const raw = ext[extKey];
|
|
2774
4984
|
if (typeof raw !== "string") continue;
|
|
2775
4985
|
const name = nameFor(key);
|
|
2776
4986
|
if (name === void 0) continue;
|
|
2777
|
-
const css =
|
|
4987
|
+
const css = table2[raw];
|
|
2778
4988
|
if (css !== void 0) {
|
|
2779
4989
|
decls.push(`${name}: ${css};`);
|
|
2780
4990
|
declaredPaths.push(`${leaf.path}.${key}`);
|
|
@@ -2805,7 +5015,7 @@ function shadowDecl(ctx, leaf, name) {
|
|
|
2805
5015
|
if (layers.length === 0) return omit("no_visible_shadow", "The style has no visible shadow, so no box-shadow was written.");
|
|
2806
5016
|
const parts = [];
|
|
2807
5017
|
for (const layer of layers) {
|
|
2808
|
-
const l =
|
|
5018
|
+
const l = asRecord2(layer);
|
|
2809
5019
|
if (!l) return omit("layer_not_an_object", "A shadow layer is not an object; the style was omitted.");
|
|
2810
5020
|
const members = [];
|
|
2811
5021
|
for (const [key, type] of SHADOW_MEMBERS) {
|
|
@@ -2930,7 +5140,7 @@ function cssOutput(exp, header, options = {}) {
|
|
|
2930
5140
|
for (const leaves of leavesByFile.values()) {
|
|
2931
5141
|
for (const leaf of leaves) {
|
|
2932
5142
|
if (leaf.type === "typography") {
|
|
2933
|
-
const value =
|
|
5143
|
+
const value = asRecord2(leaf.value) ?? {};
|
|
2934
5144
|
const ext = leaf.ext ?? {};
|
|
2935
5145
|
for (const key of typographyMemberKeys(value, ext)) candidatePaths.add(`${leaf.path}.${key}`);
|
|
2936
5146
|
} else {
|
|
@@ -3002,16 +5212,13 @@ ${imports.join("\n")}
|
|
|
3002
5212
|
return { files, map, report: sortReport(entries) };
|
|
3003
5213
|
}
|
|
3004
5214
|
|
|
3005
|
-
// ../extractor/src/v5/componentContext.ts
|
|
3006
|
-
var import_js_sha2564 = __toESM(require_sha256(), 1);
|
|
3007
|
-
|
|
3008
5215
|
// ../extractor/src/libraryBundle.ts
|
|
3009
5216
|
var LIBRARY_BUNDLE_SCHEMA = "spec-layer-library-bundle";
|
|
3010
5217
|
var LibraryBundleError = class extends Error {
|
|
3011
|
-
constructor(
|
|
5218
|
+
constructor(code3, message) {
|
|
3012
5219
|
super(message);
|
|
3013
5220
|
this.name = "LibraryBundleError";
|
|
3014
|
-
this.code =
|
|
5221
|
+
this.code = code3;
|
|
3015
5222
|
}
|
|
3016
5223
|
};
|
|
3017
5224
|
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -3020,11 +5227,26 @@ function hasContentHash(artifact) {
|
|
|
3020
5227
|
const exp = artifact.spec_layer.export;
|
|
3021
5228
|
return isRecord3(exp) && typeof exp.content_hash === "string";
|
|
3022
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;
|
|
5243
|
+
}
|
|
3023
5244
|
function entry(v, where) {
|
|
3024
5245
|
if (!isRecord3(v) || typeof v.name !== "string" || typeof v.ai !== "string" || !hasContentHash(v.artifact)) {
|
|
3025
5246
|
throw new LibraryBundleError("malformed", `The ${where} entry in this bundle is malformed.`);
|
|
3026
5247
|
}
|
|
3027
|
-
|
|
5248
|
+
const variants = readVariants(v.variants);
|
|
5249
|
+
return { name: v.name, ai: v.ai, artifact: v.artifact, ...variants ? { variants } : {} };
|
|
3028
5250
|
}
|
|
3029
5251
|
function supportedVersion(version) {
|
|
3030
5252
|
return typeof version === "string" && /^1\.\d+\.\d+$/.test(version);
|
|
@@ -3073,6 +5295,39 @@ function parseLibraryBundle(input) {
|
|
|
3073
5295
|
// ../extractor/src/libraryBundleHash.ts
|
|
3074
5296
|
var import_js_sha2565 = __toESM(require_sha256(), 1);
|
|
3075
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
|
+
|
|
3076
5331
|
// src/bundle.ts
|
|
3077
5332
|
function parseBundle(raw) {
|
|
3078
5333
|
try {
|
|
@@ -3393,15 +5648,17 @@ var inside = (parent, child) => {
|
|
|
3393
5648
|
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
3394
5649
|
};
|
|
3395
5650
|
var isDotfile = (name) => name.startsWith(".");
|
|
5651
|
+
var markerList = (marker) => typeof marker === "string" ? [marker] : marker;
|
|
3396
5652
|
function carriesMarker(abs, marker) {
|
|
3397
|
-
const
|
|
3398
|
-
|
|
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));
|
|
3399
5656
|
const fd = openSync(abs, "r");
|
|
3400
5657
|
try {
|
|
3401
5658
|
const buf = Buffer.alloc(wantBytes);
|
|
3402
5659
|
const read = readSync(fd, buf, 0, wantBytes, 0);
|
|
3403
5660
|
const text = buf.subarray(0, read).toString("utf8").replace(/\r\n/g, "\n");
|
|
3404
|
-
return text.startsWith(
|
|
5661
|
+
return markers.some((m) => text.startsWith(m));
|
|
3405
5662
|
} finally {
|
|
3406
5663
|
closeSync(fd);
|
|
3407
5664
|
}
|
|
@@ -3478,18 +5735,18 @@ function parseOutput(value, index) {
|
|
|
3478
5735
|
}
|
|
3479
5736
|
const spec = specOf(r.platform, r.format);
|
|
3480
5737
|
if (!spec) throw new Error(`${at}: unknown platform/format "${r.platform}/${r.format}". Known: ${knownFormats()}.`);
|
|
3481
|
-
const
|
|
5738
|
+
const str2 = (key) => {
|
|
3482
5739
|
if (r[key] !== void 0 && typeof r[key] !== "string") throw new Error(`${at} "${key}" must be a string.`);
|
|
3483
5740
|
return r[key];
|
|
3484
5741
|
};
|
|
3485
|
-
const path =
|
|
5742
|
+
const path = str2("path") ?? spec.defaultPath;
|
|
3486
5743
|
if (path === null) throw new Error(`${at} needs "path": ${spec.platform} has no default location.`);
|
|
3487
|
-
const nameCase =
|
|
5744
|
+
const nameCase = str2("case");
|
|
3488
5745
|
if (nameCase !== void 0 && !NAME_CASES.includes(nameCase)) {
|
|
3489
5746
|
throw new Error(`${at} "case" takes ${NAME_CASES.join(", ")}.`);
|
|
3490
5747
|
}
|
|
3491
|
-
const root =
|
|
3492
|
-
const modeSelector =
|
|
5748
|
+
const root = str2("root");
|
|
5749
|
+
const modeSelector = str2("modeSelector");
|
|
3493
5750
|
let modes;
|
|
3494
5751
|
if (r.modes !== void 0) {
|
|
3495
5752
|
const m = r.modes;
|
|
@@ -3545,6 +5802,11 @@ var DEFAULT_API = "https://api.spec-layer.com";
|
|
|
3545
5802
|
var DEFAULT_OUT_DIR = ".speclayer";
|
|
3546
5803
|
var DEFAULT_COMPONENT_SPECS_DIR = "component-specs";
|
|
3547
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
|
+
}
|
|
3548
5810
|
var invalidConfig = () => new Error(`${CONFIG_NAME} is not valid JSON. Fix or delete it, then retry.`);
|
|
3549
5811
|
function parseInclude(value) {
|
|
3550
5812
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalidConfig();
|
|
@@ -3583,6 +5845,10 @@ function parseComponentSpecsDir(value) {
|
|
|
3583
5845
|
if (dir.length === 0) throw new Error('speclayer.json "componentSpecsDir" must be a non-empty string.');
|
|
3584
5846
|
return dir;
|
|
3585
5847
|
}
|
|
5848
|
+
function parseComponentSpecsFormat(value) {
|
|
5849
|
+
if (!isComponentFormat(value)) throw new Error('speclayer.json "componentSpecsFormat" must be "yaml" or "md".');
|
|
5850
|
+
return value;
|
|
5851
|
+
}
|
|
3586
5852
|
function parsePlatforms(value) {
|
|
3587
5853
|
if (!Array.isArray(value) || !value.every((p) => typeof p === "string" && isPlatform(p))) {
|
|
3588
5854
|
throw new Error(`speclayer.json "platforms" must be an array of ${PLATFORMS.join(", ")}.`);
|
|
@@ -3617,6 +5883,7 @@ function readConfig(cwd) {
|
|
|
3617
5883
|
...typeof record.libraryId === "string" ? { libraryId: record.libraryId } : {},
|
|
3618
5884
|
...typeof record.outDir === "string" ? { outDir: record.outDir } : {},
|
|
3619
5885
|
...record.componentSpecsDir !== void 0 ? { componentSpecsDir: parseComponentSpecsDir(record.componentSpecsDir) } : {},
|
|
5886
|
+
...record.componentSpecsFormat !== void 0 ? { componentSpecsFormat: parseComponentSpecsFormat(record.componentSpecsFormat) } : {},
|
|
3620
5887
|
...record.include !== void 0 ? { include: parseInclude(record.include) } : {},
|
|
3621
5888
|
...record.dtcg !== void 0 ? { dtcg: parseDtcg(record.dtcg) } : {},
|
|
3622
5889
|
...record.platforms !== void 0 ? { platforms: parsePlatforms(record.platforms) } : {},
|
|
@@ -3628,6 +5895,7 @@ function writeConfig(cwd, config) {
|
|
|
3628
5895
|
libraryId: config.libraryId,
|
|
3629
5896
|
outDir: config.outDir,
|
|
3630
5897
|
...config.componentSpecsDir ? { componentSpecsDir: config.componentSpecsDir } : {},
|
|
5898
|
+
...config.componentSpecsFormat ? { componentSpecsFormat: config.componentSpecsFormat } : {},
|
|
3631
5899
|
...config.include ? { include: config.include } : {},
|
|
3632
5900
|
...config.dtcg ? { dtcg: config.dtcg } : {},
|
|
3633
5901
|
...config.platforms && config.platforms.length > 0 ? { platforms: config.platforms } : {},
|
|
@@ -3657,6 +5925,7 @@ function resolveOptions(cwd, flags, env, manifestLibraryId) {
|
|
|
3657
5925
|
// A trailing slash would build "//v1/..." paths the proxy router 404s on.
|
|
3658
5926
|
api: (flags.api ?? env.SPEC_LAYER_API ?? DEFAULT_API).replace(/\/+$/, ""),
|
|
3659
5927
|
key: supplied ?? storedKey,
|
|
5928
|
+
...config?.componentSpecsFormat ? { componentSpecsFormat: config.componentSpecsFormat } : {},
|
|
3660
5929
|
...config?.include ? { include: config.include } : {},
|
|
3661
5930
|
...config?.dtcg ? { dtcg: config.dtcg } : {},
|
|
3662
5931
|
...config?.platforms ? { platforms: config.platforms } : {},
|
|
@@ -3680,7 +5949,8 @@ async function fetchBundle(opts) {
|
|
|
3680
5949
|
} catch {
|
|
3681
5950
|
return { kind: "error", message: `Could not reach ${opts.api}.` };
|
|
3682
5951
|
}
|
|
3683
|
-
|
|
5952
|
+
const version = res.headers.get("X-Library-Version");
|
|
5953
|
+
if (res.status === 304) return { kind: "not_modified", version };
|
|
3684
5954
|
if (res.status === 401) {
|
|
3685
5955
|
return {
|
|
3686
5956
|
kind: "error",
|
|
@@ -3694,7 +5964,8 @@ async function fetchBundle(opts) {
|
|
|
3694
5964
|
kind: "ok",
|
|
3695
5965
|
raw,
|
|
3696
5966
|
publishedAt: res.headers.get("X-Published-At") ?? "unknown",
|
|
3697
|
-
bundleHash: createHash("sha256").update(raw).digest("hex")
|
|
5967
|
+
bundleHash: createHash("sha256").update(raw).digest("hex"),
|
|
5968
|
+
version
|
|
3698
5969
|
};
|
|
3699
5970
|
}
|
|
3700
5971
|
|
|
@@ -3745,11 +6016,21 @@ function cliVersion() {
|
|
|
3745
6016
|
}
|
|
3746
6017
|
|
|
3747
6018
|
// src/files.ts
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
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;
|
|
3751
6033
|
}
|
|
3752
|
-
var COMPONENT_SPEC_MARKER = "spec_layer:\n kind: component";
|
|
3753
6034
|
function readManifest(outDir) {
|
|
3754
6035
|
const path = join5(outDir, "manifest.json");
|
|
3755
6036
|
if (!existsSync5(path)) return null;
|
|
@@ -3776,27 +6057,6 @@ function readLocalBundle(outDir) {
|
|
|
3776
6057
|
throw new Error(`${path} could not be read as a library bundle. Run spec-layer pull again.`);
|
|
3777
6058
|
}
|
|
3778
6059
|
}
|
|
3779
|
-
function componentSlugs(bundle) {
|
|
3780
|
-
const usedSlugs = /* @__PURE__ */ new Set();
|
|
3781
|
-
const nextSuffix = /* @__PURE__ */ new Map();
|
|
3782
|
-
return bundle.components.map((component) => {
|
|
3783
|
-
const base = slugify(component.name);
|
|
3784
|
-
let slug2 = base;
|
|
3785
|
-
if (usedSlugs.has(slug2)) {
|
|
3786
|
-
let n = (nextSuffix.get(base) ?? 1) + 1;
|
|
3787
|
-
slug2 = `${base}-${n}`;
|
|
3788
|
-
while (usedSlugs.has(slug2)) {
|
|
3789
|
-
n += 1;
|
|
3790
|
-
slug2 = `${base}-${n}`;
|
|
3791
|
-
}
|
|
3792
|
-
nextSuffix.set(base, n);
|
|
3793
|
-
} else {
|
|
3794
|
-
nextSuffix.set(base, 1);
|
|
3795
|
-
}
|
|
3796
|
-
usedSlugs.add(slug2);
|
|
3797
|
-
return slug2;
|
|
3798
|
-
});
|
|
3799
|
-
}
|
|
3800
6060
|
function assertReplaceable(outDir, cwd) {
|
|
3801
6061
|
const rel = relative2(resolve3(cwd), resolve3(outDir));
|
|
3802
6062
|
if (rel === "" || rel.startsWith("..") || isAbsolute2(rel)) {
|
|
@@ -3810,12 +6070,13 @@ function writeBundleFiles(opts) {
|
|
|
3810
6070
|
assertReplaceable(opts.outDir, opts.cwd);
|
|
3811
6071
|
const selection = opts.selection ?? DEFAULT_SELECTION;
|
|
3812
6072
|
const selected = selectComponents(opts.bundle, selection);
|
|
3813
|
-
const slugs = componentSlugs(opts.bundle);
|
|
6073
|
+
const slugs = componentSlugs(opts.bundle.components.map((c) => c.name));
|
|
3814
6074
|
const outputs = opts.outputs ?? [];
|
|
3815
6075
|
const componentSpecsDir = opts.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
|
|
6076
|
+
const componentSpecsFormat = opts.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT;
|
|
3816
6077
|
const outDirRel = relative2(resolve3(opts.cwd), resolve3(opts.outDir)).split(sep).join("/");
|
|
3817
6078
|
const outputPaths = outputs.map((o) => o.path);
|
|
3818
|
-
const specsProblem = visibleDirProblem(opts.cwd, outDirRel, componentSpecsDir,
|
|
6079
|
+
const specsProblem = visibleDirProblem(opts.cwd, outDirRel, componentSpecsDir, COMPONENT_SPEC_MARKERS, outputPaths, "componentSpecsDir");
|
|
3819
6080
|
if (specsProblem) throw new Error(specsProblem);
|
|
3820
6081
|
for (const o of outputs) {
|
|
3821
6082
|
const problem = outputPathProblem(opts.cwd, outDirRel, o, [componentSpecsDir, ...outputPaths.filter((p) => p !== o.path)]);
|
|
@@ -3824,6 +6085,10 @@ function writeBundleFiles(opts) {
|
|
|
3824
6085
|
const briefs = {};
|
|
3825
6086
|
opts.bundle.components.forEach((component, i) => {
|
|
3826
6087
|
if (!selected[i]) return;
|
|
6088
|
+
if (componentSpecsFormat === "md") {
|
|
6089
|
+
briefs[`${slugs[i]}.md`] = componentMarkdownPage(component);
|
|
6090
|
+
return;
|
|
6091
|
+
}
|
|
3827
6092
|
if (!component.ai.startsWith(COMPONENT_SPEC_MARKER)) {
|
|
3828
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.`);
|
|
3829
6094
|
}
|
|
@@ -3879,7 +6144,7 @@ function writeBundleFiles(opts) {
|
|
|
3879
6144
|
kind: "component",
|
|
3880
6145
|
name: component.name,
|
|
3881
6146
|
contentHash: component.artifact.spec_layer.export.content_hash,
|
|
3882
|
-
path: selected[i] ? `${componentSpecsDir}/${slugs[i]}
|
|
6147
|
+
path: selected[i] ? `${componentSpecsDir}/${slugs[i]}.${componentSpecsFormat}` : null
|
|
3883
6148
|
});
|
|
3884
6149
|
});
|
|
3885
6150
|
const manifest = {
|
|
@@ -3891,7 +6156,9 @@ function writeBundleFiles(opts) {
|
|
|
3891
6156
|
cliVersion: cliVersion(),
|
|
3892
6157
|
selection,
|
|
3893
6158
|
componentSpecsDir,
|
|
6159
|
+
componentSpecsFormat,
|
|
3894
6160
|
artifacts,
|
|
6161
|
+
...opts.version ? { version: opts.version } : {},
|
|
3895
6162
|
...opts.dtcg && Object.keys(opts.dtcg).length > 0 ? { dtcg: opts.dtcg } : {},
|
|
3896
6163
|
...opts.platforms && opts.platforms.length > 0 ? { platforms: opts.platforms } : {},
|
|
3897
6164
|
...opts.outputs ? { outputs: opts.outputs } : {}
|
|
@@ -3903,7 +6170,7 @@ function writeBundleFiles(opts) {
|
|
|
3903
6170
|
}
|
|
3904
6171
|
rmSync2(opts.outDir, { recursive: true, force: true });
|
|
3905
6172
|
renameSync2(staging, opts.outDir);
|
|
3906
|
-
const componentSpecs = { path: componentSpecsDir, files: writeVisibleDir(opts.cwd, componentSpecsDir,
|
|
6173
|
+
const componentSpecs = { path: componentSpecsDir, files: writeVisibleDir(opts.cwd, componentSpecsDir, COMPONENT_SPEC_MARKERS, briefs) };
|
|
3907
6174
|
const outputResults = [];
|
|
3908
6175
|
for (const d of deliverables) {
|
|
3909
6176
|
outputResults.push({ path: d.output.path, files: writeVisibleDir(opts.cwd, d.output.path, CSS_HEADER_PREFIX, d.files, CSS_INDEX_FILE) });
|
|
@@ -3979,7 +6246,7 @@ var PULL_EXITS = {
|
|
|
3979
6246
|
var TOOLS = [
|
|
3980
6247
|
{
|
|
3981
6248
|
name: "setup",
|
|
3982
|
-
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]...",
|
|
3983
6250
|
summary: "Records the library id, stores the pull key in a gitignored speclayer.local.json, then pulls.",
|
|
3984
6251
|
when: "Once, with the command the plugin's Publish screen hands out. Re-run it after the key is rotated.",
|
|
3985
6252
|
network: true,
|
|
@@ -3996,7 +6263,7 @@ var TOOLS = [
|
|
|
3996
6263
|
},
|
|
3997
6264
|
{
|
|
3998
6265
|
name: "init",
|
|
3999
|
-
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]...",
|
|
4000
6267
|
summary: "Writes speclayer.json, with the platforms and default outputs, so later commands need no flags. Stores no key and reaches no server.",
|
|
4001
6268
|
when: "A repo that supplies the key from SPEC_LAYER_KEY instead of a stored file.",
|
|
4002
6269
|
network: false,
|
|
@@ -4006,9 +6273,9 @@ var TOOLS = [
|
|
|
4006
6273
|
},
|
|
4007
6274
|
{
|
|
4008
6275
|
name: "pull",
|
|
4009
|
-
usage: "spec-layer pull [--id lib_...] [--key sl_...] [--out DIR] [--platform web|ios|android|flutter]... [--only foundation|components] [--component NAME]... [--strict]",
|
|
4010
|
-
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. 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.",
|
|
4011
|
-
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.",
|
|
4012
6279
|
network: true,
|
|
4013
6280
|
needsKey: true,
|
|
4014
6281
|
writes: [
|
|
@@ -4040,8 +6307,8 @@ var TOOLS = [
|
|
|
4040
6307
|
},
|
|
4041
6308
|
{
|
|
4042
6309
|
name: "show",
|
|
4043
|
-
usage: "spec-layer show foundation | component NAME [--canonical] [--out DIR]",
|
|
4044
|
-
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.",
|
|
4045
6312
|
when: "To read one component or the token document without opening files; it pipes cleanly.",
|
|
4046
6313
|
network: false,
|
|
4047
6314
|
needsKey: false,
|
|
@@ -4081,7 +6348,7 @@ function toolsText() {
|
|
|
4081
6348
|
lines.push(` ${tool.summary}`);
|
|
4082
6349
|
lines.push(` When: ${tool.when}`);
|
|
4083
6350
|
lines.push(` Network: ${tool.network ? "yes" : "no"}. Key: ${tool.needsKey ? "required" : "not needed"}. Writes: ${tool.writes.length ? tool.writes.join(", ") : "nothing"}.`);
|
|
4084
|
-
lines.push(` Exits: ${Object.entries(tool.exits).map(([
|
|
6351
|
+
lines.push(` Exits: ${Object.entries(tool.exits).map(([code3, meaning]) => `${code3} ${meaning}`).join("; ")}.`);
|
|
4085
6352
|
lines.push("");
|
|
4086
6353
|
}
|
|
4087
6354
|
lines.push("Flags every command accepts where they apply:");
|
|
@@ -4209,6 +6476,7 @@ function summarizePull(cwd, outDir, manifest) {
|
|
|
4209
6476
|
publishedAt: manifest.publishedAt,
|
|
4210
6477
|
pluginVersion: manifest.pluginVersion,
|
|
4211
6478
|
componentSpecsDir: manifest.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR,
|
|
6479
|
+
componentSpecsFormat: manifest.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT,
|
|
4212
6480
|
components,
|
|
4213
6481
|
foundation,
|
|
4214
6482
|
outputs: (manifest.outputs ?? []).map((o) => {
|
|
@@ -4237,7 +6505,7 @@ function summarizePull(cwd, outDir, manifest) {
|
|
|
4237
6505
|
})
|
|
4238
6506
|
};
|
|
4239
6507
|
}
|
|
4240
|
-
var
|
|
6508
|
+
var code2 = (s) => `\`${s}\``;
|
|
4241
6509
|
function stackSection(input) {
|
|
4242
6510
|
const { profile, platforms, platformSource, pull } = input;
|
|
4243
6511
|
const lines = ["## This codebase", ""];
|
|
@@ -4247,7 +6515,7 @@ function stackSection(input) {
|
|
|
4247
6515
|
);
|
|
4248
6516
|
} else {
|
|
4249
6517
|
lines.push("Detected from the repository root (the file that carries each signal is named, and nothing deeper was read):", "");
|
|
4250
|
-
for (const e of profile.evidence) lines.push(`- ${e.signal} (${
|
|
6518
|
+
for (const e of profile.evidence) lines.push(`- ${e.signal} (${code2(e.file)})`);
|
|
4251
6519
|
lines.push("");
|
|
4252
6520
|
const facts = [];
|
|
4253
6521
|
if (profile.languages.length) facts.push(`Languages: ${profile.languages.join(", ")}.`);
|
|
@@ -4257,7 +6525,7 @@ function stackSection(input) {
|
|
|
4257
6525
|
}
|
|
4258
6526
|
if (platformSource === "none") {
|
|
4259
6527
|
lines.push(
|
|
4260
|
-
`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.",
|
|
4261
6529
|
""
|
|
4262
6530
|
);
|
|
4263
6531
|
} else {
|
|
@@ -4269,7 +6537,7 @@ function stackSection(input) {
|
|
|
4269
6537
|
const n = pull.foundation.unitlessNumbers;
|
|
4270
6538
|
const cssReport = pull.outputs.find((o) => o.platform === "web" && o.format === "css" && o.written) ?? null;
|
|
4271
6539
|
lines.push(
|
|
4272
|
-
`**${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 ${
|
|
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.` : "."),
|
|
4273
6541
|
""
|
|
4274
6542
|
);
|
|
4275
6543
|
}
|
|
@@ -4292,17 +6560,17 @@ function stackSection(input) {
|
|
|
4292
6560
|
}
|
|
4293
6561
|
const fallbackTarget = cssOut ? `${cssOut.path}/` : `${input.outDir}/`;
|
|
4294
6562
|
lines.push(
|
|
4295
|
-
`The token holds a family name and no fallback stack. Never write ${
|
|
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.`,
|
|
4296
6564
|
""
|
|
4297
6565
|
);
|
|
4298
6566
|
} else if (fontsStatus === "ok") {
|
|
4299
6567
|
lines.push(
|
|
4300
|
-
`${
|
|
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.`,
|
|
4301
6569
|
""
|
|
4302
6570
|
);
|
|
4303
6571
|
} else {
|
|
4304
6572
|
lines.push(
|
|
4305
|
-
fontsStatus === "missing" ? `${
|
|
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.`,
|
|
4306
6574
|
""
|
|
4307
6575
|
);
|
|
4308
6576
|
}
|
|
@@ -4310,58 +6578,58 @@ function stackSection(input) {
|
|
|
4310
6578
|
if (cssOut) {
|
|
4311
6579
|
const mapPath = `${input.outDir}/outputs/web-css.map.json`;
|
|
4312
6580
|
lines.push(
|
|
4313
|
-
`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.`,
|
|
4314
6582
|
""
|
|
4315
6583
|
);
|
|
4316
6584
|
const partFiles = cssOut.files.filter((f) => f !== CSS_INDEX_FILE);
|
|
4317
6585
|
lines.push(
|
|
4318
|
-
`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.`,
|
|
4319
6587
|
""
|
|
4320
6588
|
);
|
|
4321
6589
|
if (profile.tokenTools.includes("style-dictionary") || profile.tokenTools.includes("tokens-studio")) {
|
|
4322
6590
|
lines.push(
|
|
4323
|
-
`${
|
|
6591
|
+
`${code2(`${cssOut.path}/`)} is a projection of the same ${code2(tokensDir)} files, not a second source. Import one or the other.`,
|
|
4324
6592
|
""
|
|
4325
6593
|
);
|
|
4326
6594
|
}
|
|
4327
6595
|
} else {
|
|
4328
6596
|
lines.push(
|
|
4329
|
-
`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.`,
|
|
4330
6598
|
""
|
|
4331
6599
|
);
|
|
4332
6600
|
const configuredNotWritten = pull?.outputs.find((o) => o.platform === "web" && !o.written) ?? null;
|
|
4333
6601
|
if (configuredNotWritten?.indexMissing) {
|
|
4334
6602
|
lines.push(
|
|
4335
|
-
`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.`,
|
|
4336
6604
|
""
|
|
4337
6605
|
);
|
|
4338
6606
|
} else if (configuredNotWritten) {
|
|
4339
6607
|
lines.push(
|
|
4340
|
-
`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.`,
|
|
4341
6609
|
""
|
|
4342
6610
|
);
|
|
4343
6611
|
} else if (pull?.foundation?.written) {
|
|
4344
6612
|
lines.push(
|
|
4345
|
-
`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/")}.`,
|
|
4346
6614
|
""
|
|
4347
6615
|
);
|
|
4348
6616
|
}
|
|
4349
6617
|
}
|
|
4350
6618
|
if (profile.tokenTools.includes("tailwind")) {
|
|
4351
6619
|
lines.push(
|
|
4352
|
-
`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.`,
|
|
4353
6621
|
""
|
|
4354
6622
|
);
|
|
4355
6623
|
}
|
|
4356
6624
|
if (profile.tokenTools.includes("style-dictionary")) {
|
|
4357
6625
|
const major = profile.styleDictionaryMajor;
|
|
4358
6626
|
lines.push(
|
|
4359
|
-
`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.`
|
|
4360
6628
|
);
|
|
4361
6629
|
if (major !== null && major < 5) {
|
|
4362
6630
|
lines.push(
|
|
4363
6631
|
"",
|
|
4364
|
-
`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.`
|
|
4365
6633
|
);
|
|
4366
6634
|
}
|
|
4367
6635
|
lines.push("");
|
|
@@ -4369,19 +6637,19 @@ function stackSection(input) {
|
|
|
4369
6637
|
} else if (platform === "ios") {
|
|
4370
6638
|
lines.push("### iOS", "");
|
|
4371
6639
|
lines.push(
|
|
4372
|
-
`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.`,
|
|
4373
6641
|
""
|
|
4374
6642
|
);
|
|
4375
6643
|
} else if (platform === "android") {
|
|
4376
6644
|
lines.push("### Android", "");
|
|
4377
6645
|
lines.push(
|
|
4378
|
-
`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.`,
|
|
4379
6647
|
""
|
|
4380
6648
|
);
|
|
4381
6649
|
} else {
|
|
4382
6650
|
lines.push("### Flutter", "");
|
|
4383
6651
|
lines.push(
|
|
4384
|
-
`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.`,
|
|
4385
6653
|
""
|
|
4386
6654
|
);
|
|
4387
6655
|
}
|
|
@@ -4393,46 +6661,46 @@ function pullSection(input) {
|
|
|
4393
6661
|
const lines = ["## What is on disk", ""];
|
|
4394
6662
|
if (!pull) {
|
|
4395
6663
|
lines.push(
|
|
4396
|
-
`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.`,
|
|
4397
6665
|
""
|
|
4398
6666
|
);
|
|
4399
6667
|
return lines;
|
|
4400
6668
|
}
|
|
4401
6669
|
lines.push(
|
|
4402
|
-
`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.`,
|
|
4403
6671
|
""
|
|
4404
6672
|
);
|
|
4405
|
-
lines.push(`- ${
|
|
4406
|
-
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.`);
|
|
4407
6675
|
if (pull.foundation) {
|
|
4408
6676
|
if (pull.foundation.written) {
|
|
4409
|
-
lines.push(`- ${
|
|
4410
|
-
lines.push(` - ${
|
|
4411
|
-
lines.push(` - ${
|
|
4412
|
-
lines.push(` - ${
|
|
4413
|
-
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)}`);
|
|
4414
6682
|
} else {
|
|
4415
|
-
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.`);
|
|
4416
6684
|
}
|
|
4417
6685
|
} else {
|
|
4418
6686
|
lines.push("- This library has no Foundation, so there is no tokens/ directory.");
|
|
4419
6687
|
}
|
|
4420
|
-
lines.push(`- ${
|
|
6688
|
+
lines.push(`- ${code2(`${pull.componentSpecsDir}/`)}: one ${pull.componentSpecsFormat === "md" ? "Markdown page" : "YAML"} per component.`);
|
|
4421
6689
|
for (const o of pull.outputs) {
|
|
4422
|
-
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.`);
|
|
4423
6691
|
}
|
|
4424
6692
|
lines.push("");
|
|
4425
6693
|
if (pull.foundation && (pull.foundation.sets.length || pull.foundation.modifiers.length)) {
|
|
4426
6694
|
lines.push("### Token collections", "");
|
|
4427
|
-
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.`);
|
|
4428
6696
|
for (const m of pull.foundation.modifiers) {
|
|
4429
|
-
lines.push(`- ${
|
|
6697
|
+
lines.push(`- ${code2(m.name)}: modes ${m.contexts.map(code2).join(", ")}${m.default ? `, default ${code2(m.default)}` : ""}.`);
|
|
4430
6698
|
}
|
|
4431
6699
|
lines.push("");
|
|
4432
6700
|
const counts = Object.entries(pull.foundation.reportCounts);
|
|
4433
6701
|
if (counts.length) {
|
|
4434
6702
|
lines.push(
|
|
4435
|
-
`${
|
|
6703
|
+
`${code2("report.json")} lists ${counts.map(([c, n]) => `${n} ${code2(c)}`).join(", ")}. Read it before assuming a token is missing.`,
|
|
4436
6704
|
""
|
|
4437
6705
|
);
|
|
4438
6706
|
}
|
|
@@ -4442,7 +6710,7 @@ function pullSection(input) {
|
|
|
4442
6710
|
lines.push("The library documents no components.", "");
|
|
4443
6711
|
} else {
|
|
4444
6712
|
for (const c of pull.components) {
|
|
4445
|
-
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.`);
|
|
4446
6714
|
}
|
|
4447
6715
|
lines.push("");
|
|
4448
6716
|
}
|
|
@@ -4453,42 +6721,43 @@ function commandsSection() {
|
|
|
4453
6721
|
const cell = (s) => s.replace(/\|/g, "\\|");
|
|
4454
6722
|
lines.push("| Command | What it does | When | Network | Key | Writes |", "|---|---|---|---|---|---|");
|
|
4455
6723
|
for (const t of TOOLS) {
|
|
4456
|
-
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"} |`);
|
|
4457
6725
|
}
|
|
4458
6726
|
lines.push("");
|
|
4459
6727
|
lines.push("Exit codes:", "");
|
|
4460
6728
|
for (const t of TOOLS) {
|
|
4461
|
-
lines.push(`- ${
|
|
6729
|
+
lines.push(`- ${code2(t.name)}: ${Object.entries(t.exits).map(([c, m]) => `${c} = ${m}`).join("; ")}.`);
|
|
4462
6730
|
}
|
|
4463
6731
|
lines.push("");
|
|
4464
|
-
for (const f of GLOBAL_FLAGS) lines.push(`- ${
|
|
6732
|
+
for (const f of GLOBAL_FLAGS) lines.push(`- ${code2(f.flag)}: ${f.summary}`);
|
|
4465
6733
|
lines.push("", KEY_RESOLUTION, "");
|
|
4466
|
-
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.`, "");
|
|
4467
6735
|
return lines;
|
|
4468
6736
|
}
|
|
4469
6737
|
function buildSkillGuide(input) {
|
|
4470
6738
|
const { outDir } = input;
|
|
4471
6739
|
const lines = [];
|
|
6740
|
+
const markdown = (input.pull?.componentSpecsFormat ?? input.config?.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT) === "md";
|
|
4472
6741
|
lines.push("# Spec Layer: design-system context for this repository", "");
|
|
4473
6742
|
lines.push(
|
|
4474
|
-
`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.",
|
|
4475
6744
|
""
|
|
4476
6745
|
);
|
|
4477
6746
|
const componentSpecsDir = input.pull?.componentSpecsDir ?? input.config?.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
|
|
4478
6747
|
lines.push("## How to use it", "");
|
|
4479
|
-
lines.push(`1. Run ${
|
|
4480
|
-
lines.push(`2. Building or changing a component: read its YAML under ${
|
|
4481
|
-
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.`);
|
|
4482
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.`);
|
|
4483
|
-
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.`);
|
|
4484
6753
|
const writtenOutputs = input.pull?.outputs.filter((o) => o.written) ?? [];
|
|
4485
|
-
const outputNote = writtenOutputs.length ? ` Never edit ${writtenOutputs.map((o) =>
|
|
4486
|
-
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.`);
|
|
4487
6756
|
lines.push("");
|
|
4488
6757
|
lines.push(...pullSection(input));
|
|
4489
6758
|
lines.push(...stackSection(input));
|
|
4490
6759
|
lines.push(...commandsSection());
|
|
4491
|
-
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.`);
|
|
4492
6761
|
return `${lines.join("\n")}
|
|
4493
6762
|
`;
|
|
4494
6763
|
}
|
|
@@ -4561,19 +6830,27 @@ ${BLOCK_END}
|
|
|
4561
6830
|
const sep2 = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
4562
6831
|
return `${existing}${sep2}${block}`;
|
|
4563
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
|
+
}
|
|
4564
6839
|
function installSkill(cwd, host, guide) {
|
|
4565
6840
|
const target = installTarget(host);
|
|
4566
6841
|
const abs = join7(cwd, target.path);
|
|
4567
6842
|
const existing = existsSync7(abs) ? readFileSync8(abs, "utf8") : null;
|
|
6843
|
+
const staleSnapshot = staleSnapshotDirs(cwd, target);
|
|
4568
6844
|
const next = target.mode === "file" ? renderForHost(host, guide) : upsertBlock(existing, renderForHost(host, guide));
|
|
4569
|
-
if (existing === next) return { path: target.path, result: "unchanged" };
|
|
6845
|
+
if (existing === next) return { path: target.path, result: "unchanged", staleSnapshot };
|
|
4570
6846
|
mkdirSync3(dirname3(abs), { recursive: true });
|
|
4571
6847
|
writeFileSync6(abs, next);
|
|
4572
|
-
return { path: target.path, result: existing === null ? "created" : "updated" };
|
|
6848
|
+
return { path: target.path, result: existing === null ? "created" : "updated", staleSnapshot };
|
|
4573
6849
|
}
|
|
4574
6850
|
|
|
4575
6851
|
// src/commands.ts
|
|
4576
6852
|
var NO_LOCAL_PULL = "No local pull found. Run spec-layer pull.";
|
|
6853
|
+
var FORMAT_NAME = { yaml: "YAML", md: "Markdown" };
|
|
4577
6854
|
function manifestReader() {
|
|
4578
6855
|
const cache = /* @__PURE__ */ new Map();
|
|
4579
6856
|
return (outDir) => {
|
|
@@ -4584,7 +6861,7 @@ function manifestReader() {
|
|
|
4584
6861
|
function sameOutput(a, b) {
|
|
4585
6862
|
const selectionKey = (s) => JSON.stringify([s.foundation, s.components === null ? null : [...new Set(s.components.map(slugify))].sort()]);
|
|
4586
6863
|
const key = (v) => JSON.stringify(sortKeys(v ?? {}));
|
|
4587
|
-
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);
|
|
4588
6865
|
}
|
|
4589
6866
|
function sortKeys(value) {
|
|
4590
6867
|
if (Array.isArray(value)) return value.map(sortKeys);
|
|
@@ -4605,6 +6882,13 @@ function platformsFromFlags(flags, io2) {
|
|
|
4605
6882
|
}
|
|
4606
6883
|
return out;
|
|
4607
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
|
+
}
|
|
4608
6892
|
function resolvePlatforms(cwd, fromFlags, config) {
|
|
4609
6893
|
if (fromFlags) return { platforms: fromFlags, source: "flag" };
|
|
4610
6894
|
if (config?.platforms && config.platforms.length > 0) return { platforms: config.platforms, source: "config" };
|
|
@@ -4637,6 +6921,8 @@ function runInit(cwd, flags, io2) {
|
|
|
4637
6921
|
}
|
|
4638
6922
|
const fromFlags = platformsFromFlags(flags, io2);
|
|
4639
6923
|
if (fromFlags === null) return 1;
|
|
6924
|
+
const format = componentFormatFromFlags(flags, io2);
|
|
6925
|
+
if (format === null) return 1;
|
|
4640
6926
|
const { platforms, source } = resolvePlatforms(cwd, fromFlags, null);
|
|
4641
6927
|
const outputs = defaultOutputs(platforms);
|
|
4642
6928
|
const outDir = flags.out ?? DEFAULT_OUT_DIR;
|
|
@@ -4644,6 +6930,7 @@ function runInit(cwd, flags, io2) {
|
|
|
4644
6930
|
libraryId: flags.id,
|
|
4645
6931
|
outDir,
|
|
4646
6932
|
componentSpecsDir: DEFAULT_COMPONENT_SPECS_DIR,
|
|
6933
|
+
...format ? { componentSpecsFormat: format } : {},
|
|
4647
6934
|
...include ? { include } : {},
|
|
4648
6935
|
...platforms.length > 0 ? { platforms } : {},
|
|
4649
6936
|
...outputs.length > 0 ? { outputs } : {}
|
|
@@ -4715,16 +7002,20 @@ async function runSetup(cwd, flags, env, io2, fetcher) {
|
|
|
4715
7002
|
}
|
|
4716
7003
|
const fromFlags = platformsFromFlags(flags, io2);
|
|
4717
7004
|
if (fromFlags === null) return 1;
|
|
7005
|
+
const format = componentFormatFromFlags(flags, io2);
|
|
7006
|
+
if (format === null) return 1;
|
|
4718
7007
|
const outDir = flags.out ?? existing?.outDir ?? DEFAULT_OUT_DIR;
|
|
4719
7008
|
const componentSpecsDir = existing?.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
|
|
4720
7009
|
const keptInclude = include ?? existing?.include ?? null;
|
|
4721
7010
|
const keptDtcg = existing?.dtcg ?? null;
|
|
7011
|
+
const keptFormat = format ?? existing?.componentSpecsFormat ?? null;
|
|
4722
7012
|
const { platforms } = resolvePlatforms(cwd, fromFlags, existing);
|
|
4723
7013
|
const outputs = withDefaults(existing?.outputs ?? [], platforms);
|
|
4724
7014
|
writeConfig(cwd, {
|
|
4725
7015
|
libraryId: flags.id,
|
|
4726
7016
|
outDir,
|
|
4727
7017
|
componentSpecsDir,
|
|
7018
|
+
...keptFormat ? { componentSpecsFormat: keptFormat } : {},
|
|
4728
7019
|
...keptInclude ? { include: keptInclude } : {},
|
|
4729
7020
|
...keptDtcg ? { dtcg: keptDtcg } : {},
|
|
4730
7021
|
...platforms.length > 0 ? { platforms } : {},
|
|
@@ -4768,8 +7059,8 @@ git rm --cached ${ignored.line}`);
|
|
|
4768
7059
|
}
|
|
4769
7060
|
const { replaced } = writeCredentials(cwd, { libraryId: flags.id, key });
|
|
4770
7061
|
io2.out(replaced ? `Replaced the stored key in ${CREDENTIALS_NAME}.` : `Stored the pull key in ${CREDENTIALS_NAME}.`);
|
|
4771
|
-
const
|
|
4772
|
-
if (
|
|
7062
|
+
const code3 = await runPull(cwd, { ...flags, key }, env, io2, fetcher);
|
|
7063
|
+
if (code3 !== 0) return code3;
|
|
4773
7064
|
const hosts = detectRepo(cwd).agents;
|
|
4774
7065
|
io2.out("");
|
|
4775
7066
|
io2.out("Next step for a coding agent: npx spec-layer skill --install");
|
|
@@ -4831,6 +7122,9 @@ function printReportSummary(cwd, outDir, outputs, io2) {
|
|
|
4831
7122
|
}
|
|
4832
7123
|
return errors;
|
|
4833
7124
|
}
|
|
7125
|
+
function publishedPhrase(version, publishedAt) {
|
|
7126
|
+
return version ? `(v${version}, published ${publishedAt})` : `(published ${publishedAt})`;
|
|
7127
|
+
}
|
|
4834
7128
|
async function runPull(cwd, flags, env, io2, fetcher) {
|
|
4835
7129
|
const manifestAt = manifestReader();
|
|
4836
7130
|
const opts = resolved(cwd, flags, env, io2, manifestAt);
|
|
@@ -4844,6 +7138,9 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
4844
7138
|
}
|
|
4845
7139
|
const fromFlags = platformsFromFlags(flags, io2);
|
|
4846
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;
|
|
4847
7144
|
const { platforms, source } = resolvePlatforms(cwd, fromFlags, opts);
|
|
4848
7145
|
const outputs = outputsForRun(fromFlags, opts, platforms);
|
|
4849
7146
|
const manifest = manifestAt(join8(cwd, opts.outDir));
|
|
@@ -4851,8 +7148,14 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
4851
7148
|
const willWriteFoundation = selection.foundation && foundationOnDisk;
|
|
4852
7149
|
const briefsOnDisk = (manifest?.artifacts ?? []).filter((a) => a.kind === "component" && a.path !== null).every((a) => existsSync8(resolve5(cwd, a.path)));
|
|
4853
7150
|
const etag = manifest && manifest.cliVersion === cliVersion() && sameOutput(
|
|
4854
|
-
{
|
|
4855
|
-
|
|
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 }
|
|
4856
7159
|
) && briefsOnDisk && (!willWriteFoundation || foundationFilesOnDisk(cwd, opts.outDir)) && (!willWriteFoundation || outputs.every((o) => outputFilesOnDisk(cwd, opts.outDir, o))) ? manifest.bundleHash : void 0;
|
|
4857
7160
|
const result = await fetchBundle({
|
|
4858
7161
|
api: opts.api,
|
|
@@ -4866,7 +7169,7 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
4866
7169
|
return 1;
|
|
4867
7170
|
}
|
|
4868
7171
|
if (result.kind === "not_modified") {
|
|
4869
|
-
io2.out(`Already up to date (
|
|
7172
|
+
io2.out(`Already up to date ${publishedPhrase(result.version ?? manifest?.version, manifest?.publishedAt ?? "unknown")}.`);
|
|
4870
7173
|
const cachedErrors = printReportSummary(cwd, opts.outDir, outputs, io2);
|
|
4871
7174
|
if (flags.strict && cachedErrors > 0) return 1;
|
|
4872
7175
|
return 0;
|
|
@@ -4886,16 +7189,18 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
4886
7189
|
libraryId: opts.libraryId,
|
|
4887
7190
|
publishedAt: result.publishedAt,
|
|
4888
7191
|
bundleHash: result.bundleHash,
|
|
7192
|
+
version: result.version,
|
|
4889
7193
|
dtcg: opts.dtcg,
|
|
4890
7194
|
platforms,
|
|
4891
7195
|
outputs,
|
|
4892
|
-
componentSpecsDir: opts.componentSpecsDir
|
|
7196
|
+
componentSpecsDir: opts.componentSpecsDir,
|
|
7197
|
+
componentSpecsFormat
|
|
4893
7198
|
});
|
|
4894
7199
|
written = writeResult.written;
|
|
4895
7200
|
componentSpecs = writeResult.componentSpecs;
|
|
4896
7201
|
outputResults = writeResult.outputs;
|
|
4897
7202
|
io2.out(
|
|
4898
|
-
`Pulled ${bundle.fileName ?? opts.libraryId}: ${describePull(bundle, selection, selected)}
|
|
7203
|
+
`Pulled ${bundle.fileName ?? opts.libraryId}: ${describePull(bundle, selection, selected)} ${publishedPhrase(result.version, result.publishedAt)}.`
|
|
4899
7204
|
);
|
|
4900
7205
|
} catch (err) {
|
|
4901
7206
|
io2.err(errorText(err));
|
|
@@ -4903,7 +7208,10 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
4903
7208
|
}
|
|
4904
7209
|
const count = (n) => `${n} file${n === 1 ? "" : "s"}`;
|
|
4905
7210
|
io2.out(`Wrote ${written.length} files under ${opts.outDir}/.`);
|
|
4906
|
-
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
|
+
}
|
|
4907
7215
|
for (const r of outputResults) {
|
|
4908
7216
|
const o = outputs.find((x) => x.path === r.path);
|
|
4909
7217
|
if (o) io2.out(`Wrote ${r.path}/ (${count(r.files.length)}, ${o.platform}/${o.format}, ${o.case} names).`);
|
|
@@ -4948,10 +7256,10 @@ async function runStatus(cwd, flags, env, io2, fetcher) {
|
|
|
4948
7256
|
return 1;
|
|
4949
7257
|
}
|
|
4950
7258
|
if (result.kind === "not_modified") {
|
|
4951
|
-
io2.out(`Up to date (
|
|
7259
|
+
io2.out(`Up to date ${publishedPhrase(result.version ?? manifest.version, manifest.publishedAt)}.`);
|
|
4952
7260
|
return 0;
|
|
4953
7261
|
}
|
|
4954
|
-
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.`);
|
|
4955
7263
|
return 2;
|
|
4956
7264
|
}
|
|
4957
7265
|
function runList(cwd, flags, io2) {
|
|
@@ -4962,7 +7270,7 @@ function runList(cwd, flags, io2) {
|
|
|
4962
7270
|
io2.err(NO_LOCAL_PULL);
|
|
4963
7271
|
return 1;
|
|
4964
7272
|
}
|
|
4965
|
-
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}.`);
|
|
4966
7274
|
const rows = manifest.artifacts.map((a) => [a.kind, a.name, a.path ?? "not written", a.contentHash]);
|
|
4967
7275
|
const widths = [0, 1, 2].map((i) => Math.max(...rows.map((r) => r[i].length)));
|
|
4968
7276
|
for (const row of rows) {
|
|
@@ -4983,6 +7291,12 @@ function runShow(cwd, flags, args, io2) {
|
|
|
4983
7291
|
io2.err(SHOW_USAGE);
|
|
4984
7292
|
return 1;
|
|
4985
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
|
+
}
|
|
4986
7300
|
const outDir = resolvedOutDir(cwd, flags, io2);
|
|
4987
7301
|
if (!outDir) return 1;
|
|
4988
7302
|
let bundle;
|
|
@@ -4997,6 +7311,7 @@ function runShow(cwd, flags, args, io2) {
|
|
|
4997
7311
|
return 1;
|
|
4998
7312
|
}
|
|
4999
7313
|
let entry2;
|
|
7314
|
+
let component = null;
|
|
5000
7315
|
if (wantsFoundation) {
|
|
5001
7316
|
if (!bundle.foundation) {
|
|
5002
7317
|
io2.err("This library has no Foundation. Run spec-layer list to see what it holds.");
|
|
@@ -5016,9 +7331,33 @@ Available: ${available || "none"}.`);
|
|
|
5016
7331
|
return 1;
|
|
5017
7332
|
}
|
|
5018
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
|
+
}
|
|
5019
7359
|
}
|
|
5020
|
-
io2.write(
|
|
5021
|
-
` : entry2.ai);
|
|
7360
|
+
io2.write(entry2.ai);
|
|
5022
7361
|
return 0;
|
|
5023
7362
|
}
|
|
5024
7363
|
function runTools(flags, io2) {
|
|
@@ -5090,6 +7429,11 @@ function runSkill(cwd, flags, io2) {
|
|
|
5090
7429
|
}
|
|
5091
7430
|
const verb = outcome.result === "created" ? "Wrote" : outcome.result === "updated" ? "Updated" : "Unchanged:";
|
|
5092
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
|
+
}
|
|
5093
7437
|
}
|
|
5094
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.`);
|
|
5095
7439
|
if (input.platformSource === "none") io2.out(`No target platform detected. Pass --platform ${PLATFORMS.join("|")} to write platform-specific token advice.`);
|
|
@@ -5100,17 +7444,17 @@ function runSkill(cwd, flags, io2) {
|
|
|
5100
7444
|
var USAGE = `spec-layer <command>
|
|
5101
7445
|
|
|
5102
7446
|
Commands:
|
|
5103
|
-
setup --id lib_... --key sl_... [--out DIR] [selection] [--platform P]...
|
|
7447
|
+
setup --id lib_... --key sl_... [--out DIR] [selection] [--platform P]... [--component-format F]
|
|
5104
7448
|
store the key, then pull
|
|
5105
|
-
init --id lib_... [--out DIR] [selection] [--platform P]...
|
|
7449
|
+
init --id lib_... [--out DIR] [selection] [--platform P]... [--component-format F]
|
|
5106
7450
|
write speclayer.json
|
|
5107
|
-
pull [--id lib_...] [--key sl_...] [selection] [--platform P]... [--strict]
|
|
7451
|
+
pull [--id lib_...] [--key sl_...] [selection] [--platform P]... [--component-format F] [--strict]
|
|
5108
7452
|
fetch the library into DIR (default .speclayer); the foundation lands as DTCG under DIR/tokens/;
|
|
5109
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)
|
|
5110
7454
|
status [--id lib_...] [--key sl_...] check freshness; exits 2 when behind
|
|
5111
7455
|
list list every artifact in the last pull
|
|
5112
|
-
show foundation | component NAME [--canonical]
|
|
5113
|
-
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)
|
|
5114
7458
|
tools [--json] list every command with what it reaches and writes
|
|
5115
7459
|
skill [--install] [--agent HOST]... [--platform P]... [--json]
|
|
5116
7460
|
print a guide for a coding agent, adapted to this repo and the last pull;
|
|
@@ -5123,6 +7467,7 @@ Selection (setup, pull and init; flags replace the include block in speclayer.js
|
|
|
5123
7467
|
Options:
|
|
5124
7468
|
--api URL override the API origin (default https://api.spec-layer.com)
|
|
5125
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
|
|
5126
7471
|
The pull key comes from --key, SPEC_LAYER_KEY, or speclayer.local.json written by setup.`;
|
|
5127
7472
|
var io = {
|
|
5128
7473
|
out: (l) => console.log(l),
|
|
@@ -5149,7 +7494,8 @@ async function main() {
|
|
|
5149
7494
|
install: { type: "boolean" },
|
|
5150
7495
|
strict: { type: "boolean" },
|
|
5151
7496
|
agent: { type: "string", multiple: true },
|
|
5152
|
-
platform: { type: "string", multiple: true }
|
|
7497
|
+
platform: { type: "string", multiple: true },
|
|
7498
|
+
"component-format": { type: "string" }
|
|
5153
7499
|
}
|
|
5154
7500
|
}));
|
|
5155
7501
|
} catch {
|