@smallpen/cli 0.1.0-alpha.11 → 0.1.0-alpha.12
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/bin/command-contract.mjs +7 -8
- package/bin/design-output.mjs +65 -165
- package/bin/help-topics.mjs +2 -2
- package/bin/help.mjs +24 -15
- package/bin/inspection-output.mjs +825 -0
- package/bin/result-files.mjs +1 -1
- package/bin/smallpen.mjs +82 -251
- package/bin/workflow.mjs +4 -4
- package/package.json +3 -3
package/bin/command-contract.mjs
CHANGED
|
@@ -111,7 +111,7 @@ const PARAMETERS = {
|
|
|
111
111
|
contexts: path("--contexts", "JSON file containing render-matrix selectors"),
|
|
112
112
|
evidence: flag(
|
|
113
113
|
"--evidence",
|
|
114
|
-
"Export PNG and
|
|
114
|
+
"Export PNG, specification, wireframe and JSON evidence from one native layout; requires --format png",
|
|
115
115
|
),
|
|
116
116
|
designSystem: flag(
|
|
117
117
|
"--design-system",
|
|
@@ -152,7 +152,7 @@ const PARAMETERS = {
|
|
|
152
152
|
mediaName: parameter("--media", "string", "Media file name, as Group/Name"),
|
|
153
153
|
fontFamily: parameter("--font", "string", "Font family name"),
|
|
154
154
|
removeStart: flag("--remove", "Stop the page being a start"),
|
|
155
|
-
as: parameter("--as", "string", "
|
|
155
|
+
as: parameter("--as", "string", "Three design outputs: spec for implementation and sources; wireframe for structure; png for appearance. text (default) navigates; issues checks. Compare layout.hash", {
|
|
156
156
|
values: ["text", "wireframe", "png", "issues", "spec"],
|
|
157
157
|
errorCode: "unknown_view_as",
|
|
158
158
|
}),
|
|
@@ -327,16 +327,16 @@ const PARAMETERS = {
|
|
|
327
327
|
designFormat: parameter(
|
|
328
328
|
"--format",
|
|
329
329
|
"string",
|
|
330
|
-
"
|
|
330
|
+
"Design output: spec for implementation and provenance, wireframe for structure, png for appearance",
|
|
331
331
|
{
|
|
332
332
|
default: "outline",
|
|
333
333
|
values: ["outline", "wireframe", "spec", "structure", "semantic"],
|
|
334
334
|
errorCode: "unknown_view_format",
|
|
335
335
|
},
|
|
336
336
|
),
|
|
337
|
-
artifactFormat: parameter("--format", "string", "
|
|
337
|
+
artifactFormat: parameter("--format", "string", "spec implements and traces sources; wireframe shows structure; png reviews appearance; text is a navigation outline", {
|
|
338
338
|
default: "wireframe",
|
|
339
|
-
values: ["text", "wireframe", "png"],
|
|
339
|
+
values: ["text", "spec", "wireframe", "png"],
|
|
340
340
|
errorCode: "unknown_export_format",
|
|
341
341
|
}),
|
|
342
342
|
warningDetail: parameter(
|
|
@@ -438,7 +438,7 @@ export const COMMAND_CONTRACTS = {
|
|
|
438
438
|
["since", "limit"],
|
|
439
439
|
),
|
|
440
440
|
view: command(
|
|
441
|
-
"Look
|
|
441
|
+
"Look by name: spec for implementation, wireframe for structure, PNG for appearance; text navigates, issues checks",
|
|
442
442
|
[
|
|
443
443
|
"as",
|
|
444
444
|
"canvas",
|
|
@@ -461,7 +461,7 @@ export const COMMAND_CONTRACTS = {
|
|
|
461
461
|
{ conflicts: { designSystem: ["page", "component", "canvas", "token", "allTokens", "allComponents", "allPages"] } },
|
|
462
462
|
),
|
|
463
463
|
export: command(
|
|
464
|
-
"Export
|
|
464
|
+
"Export specification, wireframe or PNG by name; --format png --evidence exports all three from one layout",
|
|
465
465
|
[
|
|
466
466
|
...TARGET,
|
|
467
467
|
...VIEW,
|
|
@@ -475,7 +475,6 @@ export const COMMAND_CONTRACTS = {
|
|
|
475
475
|
],
|
|
476
476
|
{
|
|
477
477
|
conflicts: {
|
|
478
|
-
evidence: ["component", "variant", "designSystem", "locale"],
|
|
479
478
|
designSystem: ["page", "component"],
|
|
480
479
|
},
|
|
481
480
|
requires: [
|
package/bin/design-output.mjs
CHANGED
|
@@ -13,7 +13,46 @@ import {
|
|
|
13
13
|
SmallPenError,
|
|
14
14
|
tokenLibraryOf,
|
|
15
15
|
} from "@smallpen/core";
|
|
16
|
-
import {
|
|
16
|
+
import { inspectProjection } from "@smallpen/local-package";
|
|
17
|
+
|
|
18
|
+
export async function resolveDesignLayout(workspace, design, options = {}) {
|
|
19
|
+
if (design.layoutSnapshot && (!options.raster || design.render?.bytes?.length)) return design;
|
|
20
|
+
const result = await inspectProjection(workspace.product, design.renderProjection ?? design.projection, {
|
|
21
|
+
...workspace, ...options, selection: design.selection, nodeId: design.tree.root.id,
|
|
22
|
+
});
|
|
23
|
+
const tree = structuredClone(design.tree);
|
|
24
|
+
const update = node => {
|
|
25
|
+
node.bounds = result.snapshot.nodes[node.id]?.bounds ?? null;
|
|
26
|
+
node.visible = result.snapshot.nodes[node.id]?.visible ?? node.visible;
|
|
27
|
+
node.children.forEach(update);
|
|
28
|
+
};
|
|
29
|
+
update(tree.root);
|
|
30
|
+
const nodes = Object.fromEntries(Object.entries(design.projection.nodes).map(([id, node]) => {
|
|
31
|
+
const bounds = result.snapshot.nodes[id]?.layoutBox;
|
|
32
|
+
return [id, bounds ? { ...node, width: bounds.width, height: bounds.height } : node];
|
|
33
|
+
}));
|
|
34
|
+
const relevant = new Set([...Object.keys(design.projection.nodes), ...(design.ancestors ?? []).map(node => node.id)]);
|
|
35
|
+
const imported = [...new Set([workspace.product, workspace.foundation, ...(workspace.libraries ?? [])]
|
|
36
|
+
.filter(Boolean).flatMap(owner => owner.manifest.entries.assets.flatMap(entry =>
|
|
37
|
+
(owner.entries[entry].fonts ?? []).map(font => font.family))))].sort();
|
|
38
|
+
const snapshot = { ...result.snapshot, diagnostics: result.snapshot.diagnostics.filter(issue =>
|
|
39
|
+
!issue.nodeId || relevant.has(issue.nodeId)).map(issue => {
|
|
40
|
+
if (issue.code !== "font_render_fallback") return issue;
|
|
41
|
+
const requestedFont = issue.details.requestedFont;
|
|
42
|
+
const availableFonts = ["Source Sans Pro (bundled)", ...imported.map(name => `${name} (imported)`)];
|
|
43
|
+
return { ...issue, requestedFont, availableFonts,
|
|
44
|
+
message: `${issue.message}. Available fonts: ${availableFonts.join(", ")}. Import with smallpen asset font import, or use an available family.`,
|
|
45
|
+
nextOperations: [{ operation: "smallpen.import-font", argv: ["import-font", workspace.product.locator,
|
|
46
|
+
"--file", `<${requestedFont} .ttf|.otf|.woff file>`, "--family", requestedFont, "--weight", "<400|700|...>", "--json"] }],
|
|
47
|
+
};
|
|
48
|
+
}) };
|
|
49
|
+
return {
|
|
50
|
+
...design, tree, ancestors: design.ancestors ?? [], authoredProjection: design.authoredProjection ?? design.projection,
|
|
51
|
+
projection: { ...design.projection, nodes,
|
|
52
|
+
diagnostics: snapshot.diagnostics },
|
|
53
|
+
layoutSnapshot: snapshot, render: result.render,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
17
56
|
|
|
18
57
|
export function themeSettings(workspace, { full = false } = {}) {
|
|
19
58
|
const { product, foundation } = workspace;
|
|
@@ -509,159 +548,6 @@ export function outlineTree(tree, projection, { full = false, components, ids =
|
|
|
509
548
|
return lines.join("\n");
|
|
510
549
|
}
|
|
511
550
|
|
|
512
|
-
// Detailed design values: one line per element with its place (x,y from
|
|
513
|
-
// the top-left of the page or component), size and how it is sized, its
|
|
514
|
-
// layout, and every visual value with the Token it follows ({path}) or as
|
|
515
|
-
// a literal. Copies of a component name the component, variant and what
|
|
516
|
-
// they change; the component's own spec is view --component NAME --as spec.
|
|
517
|
-
export function specTree(tree, projection, { components, tokenName, heading, stored = {} } = {}) {
|
|
518
|
-
const lines = heading ? [heading] : [];
|
|
519
|
-
const origin = tree.root.bounds;
|
|
520
|
-
const round = (value) => Math.round(Number(value) * 100) / 100;
|
|
521
|
-
const tokenOf = (raw, ...fields) => {
|
|
522
|
-
const applied = appliedTokenFields(raw.appliedTokens);
|
|
523
|
-
for (const field of fields) {
|
|
524
|
-
const reference = raw.tokenBindings?.[field];
|
|
525
|
-
if (reference) return `{${tokenName(reference) ?? "unknown Token"}}`;
|
|
526
|
-
if (applied[field]) return `{${applied[field]}}`;
|
|
527
|
-
}
|
|
528
|
-
return "";
|
|
529
|
-
};
|
|
530
|
-
const withToken = (value, token) => (token ? `${value} ${token}` : String(value));
|
|
531
|
-
const paint = (list) => {
|
|
532
|
-
const first = Array.isArray(list) ? list.find((item) => item && item.hidden !== true) : undefined;
|
|
533
|
-
if (!first) return undefined;
|
|
534
|
-
const color = first.color ?? first.type ?? "paint";
|
|
535
|
-
const opacity = first.opacity !== undefined && first.opacity < 1 ? ` ${Math.round(first.opacity * 100)}%` : "";
|
|
536
|
-
return `${color}${opacity}`;
|
|
537
|
-
};
|
|
538
|
-
const sizing = (raw, node) => {
|
|
539
|
-
const word = (mode) => (mode === "fill" ? "fill" : mode === "auto" ? "hug" : undefined);
|
|
540
|
-
const width = node.type === "TEXT" && raw.growType === "auto-width" ? "hug" : word(raw["layout-item-h-sizing"]);
|
|
541
|
-
const height = node.type === "TEXT" && ["auto-width", "auto-height"].includes(raw.growType) ? "hug" : word(raw["layout-item-v-sizing"]);
|
|
542
|
-
const parts = [width && `width ${width}`, height && `height ${height}`].filter(Boolean);
|
|
543
|
-
return parts.length ? ` (${parts.join(", ")})` : "";
|
|
544
|
-
};
|
|
545
|
-
const box = (raw) => {
|
|
546
|
-
const parts = [];
|
|
547
|
-
if (raw.layout === "flex") {
|
|
548
|
-
const direction = raw["layout-flex-dir"]?.startsWith("column") ? "column" : "row";
|
|
549
|
-
parts.push(direction);
|
|
550
|
-
const gap = raw["layout-gap"];
|
|
551
|
-
const gapValue = direction === "column" ? gap?.["row-gap"] ?? gap?.["column-gap"] : gap?.["column-gap"] ?? gap?.["row-gap"];
|
|
552
|
-
if (gapValue) parts.push(`gap ${withToken(round(gapValue), tokenOf(raw, "itemSpacing", direction === "column" ? "rowGap" : "columnGap"))}`);
|
|
553
|
-
const pad = raw["layout-padding"];
|
|
554
|
-
if (pad && [pad.p1, pad.p2, pad.p3, pad.p4].some((value) => Number(value))) {
|
|
555
|
-
const sides = [pad.p1, pad.p2, pad.p3, pad.p4].map((value) => round(value ?? 0));
|
|
556
|
-
const tokens = ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"].map((field) => tokenOf(raw, field));
|
|
557
|
-
const same = sides.every((value) => value === sides[0]) && tokens.every((token) => token === tokens[0]);
|
|
558
|
-
parts.push(same ? `padding ${withToken(sides[0], tokens[0])}` : `padding ${sides.map((value, index) => withToken(value, tokens[index])).join(" ")}`);
|
|
559
|
-
}
|
|
560
|
-
if (raw["layout-align-items"]) parts.push(`align ${raw["layout-align-items"]}`);
|
|
561
|
-
if (raw["layout-justify-content"]) parts.push(`justify ${raw["layout-justify-content"]}`);
|
|
562
|
-
if (raw["layout-wrap-type"] === "wrap") parts.push("wrap");
|
|
563
|
-
} else if (raw.layout) parts.push(`${raw.layout} layout`);
|
|
564
|
-
return parts;
|
|
565
|
-
};
|
|
566
|
-
const visuals = (raw) => {
|
|
567
|
-
const parts = [];
|
|
568
|
-
const isText = raw.type === "TEXT";
|
|
569
|
-
const fill = paint(raw.fills);
|
|
570
|
-
if (fill) parts.push(`${isText ? "color" : "fill"} ${withToken(fill, tokenOf(raw, "fill", "fills.0"))}`);
|
|
571
|
-
const stroke = Array.isArray(raw.strokes) ? raw.strokes.find((item) => item && item.hidden !== true) : undefined;
|
|
572
|
-
if (stroke) {
|
|
573
|
-
const sides = ["Top", "Right", "Bottom", "Left"].map((side) => stroke[`width${side}`]);
|
|
574
|
-
const width = sides.every((value) => value !== undefined) && !sides.every((value) => value === sides[0])
|
|
575
|
-
? sides.map((value) => `${round(value)}`).join(" ")
|
|
576
|
-
: `${round(stroke.width ?? 1)}`;
|
|
577
|
-
parts.push(`stroke ${withToken(`${width}px ${paint([stroke])}${stroke.style && stroke.style !== "solid" ? ` ${stroke.style}` : ""}${stroke.alignment ? ` ${stroke.alignment}` : ""}`, tokenOf(raw, "stroke", "strokes.0"))}`);
|
|
578
|
-
}
|
|
579
|
-
if (Array.isArray(raw.cornerRadius) ? raw.cornerRadius.some(Number) : Number(raw.cornerRadius)) {
|
|
580
|
-
const corners = Array.isArray(raw.cornerRadius) ? raw.cornerRadius.map(round).join(" ") : round(raw.cornerRadius);
|
|
581
|
-
parts.push(`radius ${withToken(corners, tokenOf(raw, "cornerRadius", "radiusTopLeft"))}`);
|
|
582
|
-
}
|
|
583
|
-
if (raw.opacity !== undefined && raw.opacity < 1) parts.push(`opacity ${withToken(round(raw.opacity), tokenOf(raw, "opacity"))}`);
|
|
584
|
-
const shadows = (Array.isArray(raw.shadow) ? raw.shadow : raw.shadow ? [raw.shadow] : []).filter((item) => item && item.hidden !== true);
|
|
585
|
-
if (shadows.length)
|
|
586
|
-
parts.push(`shadow ${withToken(shadows.map((item) => `${round(item["offset-x"] ?? item.offsetX ?? 0)} ${round(item["offset-y"] ?? item.offsetY ?? 0)} ${round(item.blur ?? 0)} ${round(item.spread ?? 0)} ${paint([item.color ?? {}]) ?? item.color?.color ?? ""}`).join(", "), tokenOf(raw, "shadow"))}`);
|
|
587
|
-
if (raw.rotation) parts.push(`rotation ${round(raw.rotation)}°`);
|
|
588
|
-
for (const [key, field, word] of [["layout-item-min-w", "minWidth", "min width"], ["layout-item-max-w", "maxWidth", "max width"], ["layout-item-min-h", "minHeight", "min height"], ["layout-item-max-h", "maxHeight", "max height"]])
|
|
589
|
-
if (raw[key] !== undefined) parts.push(`${word} ${withToken(round(raw[key]), tokenOf(raw, field))}`);
|
|
590
|
-
const margin = raw["layout-item-margin"];
|
|
591
|
-
if (margin && [margin.m1, margin.m2, margin.m3, margin.m4].some(Number))
|
|
592
|
-
parts.push(`margin ${[margin.m1, margin.m2, margin.m3, margin.m4].map((value) => round(value ?? 0)).join(" ")}`);
|
|
593
|
-
return parts;
|
|
594
|
-
};
|
|
595
|
-
const typeSpec = (raw) => {
|
|
596
|
-
const style = raw.textStyle ?? {};
|
|
597
|
-
const typography = tokenOf(raw, "typography");
|
|
598
|
-
const parts = [];
|
|
599
|
-
const font = [style.fontFamily, style.fontSize !== undefined ? `${round(style.fontSize)}${style.lineHeight !== undefined ? `/${round(style.lineHeight)}` : ""}` : undefined, style.fontWeight !== undefined ? `weight ${style.fontWeight}` : undefined].filter(Boolean).join(" ");
|
|
600
|
-
// Unset text styles use the App's defaults.
|
|
601
|
-
parts.push(`font ${font ? withToken(font, typography) : "default"}`);
|
|
602
|
-
if (!paint(raw.fills)) parts.push("color default");
|
|
603
|
-
for (const [key, field, word] of [["fontFamily", "fontFamily", "family"], ["fontSize", "fontSize", "size"], ["fontWeight", "fontWeight", "weight"], ["lineHeight", "lineHeight", "line height"]]) {
|
|
604
|
-
const token = typography ? "" : tokenOf(raw, field);
|
|
605
|
-
if (token && style[key] !== undefined) parts.push(`${word} ${token}`);
|
|
606
|
-
}
|
|
607
|
-
if (style.letterSpacing) parts.push(`letter spacing ${withToken(round(style.letterSpacing), tokenOf(raw, "letterSpacing"))}`);
|
|
608
|
-
if (style.textAlign && style.textAlign !== "left") parts.push(`align ${style.textAlign}`);
|
|
609
|
-
if (style.textTransform && style.textTransform !== "none") parts.push(`case ${style.textTransform}`);
|
|
610
|
-
if (style.textDecoration && style.textDecoration !== "none") parts.push(style.textDecoration);
|
|
611
|
-
return parts;
|
|
612
|
-
};
|
|
613
|
-
const place = (node) => `at ${Math.round(node.bounds.x - origin.x)},${Math.round(node.bounds.y - origin.y)} · ${Math.round(node.bounds.width)}×${Math.round(node.bounds.height)}`;
|
|
614
|
-
const overrideLines = (raw, set) => {
|
|
615
|
-
const overrides = (stored[raw.id] ?? raw).instance?.overrides ?? {};
|
|
616
|
-
const variant = set?.variants.find((candidate) => Object.entries(raw.instance?.variant ?? {}).every(([axis, value]) => candidate.selection?.[axis] === value)) ?? set?.variants[0];
|
|
617
|
-
const nameOf = (id) => variant?.nodes?.[id]?.name ?? set?.variants.map((candidate) => candidate.nodes?.[id]?.name).find(Boolean) ?? id;
|
|
618
|
-
const parts = [];
|
|
619
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
620
|
-
const at = key.lastIndexOf(":");
|
|
621
|
-
const element = nameOf(key.slice(0, at)), field = key.slice(at + 1);
|
|
622
|
-
if (field === "tokenBindings") {
|
|
623
|
-
for (const [binding, reference] of Object.entries(value ?? {})) if (reference) parts.push(`${element} ${binding} {${tokenName(reference) ?? "unknown Token"}}`);
|
|
624
|
-
} else if (field === "text") parts.push(`${element} text ${JSON.stringify(String(value))}`);
|
|
625
|
-
else if (field === "fills" || field === "strokes") parts.push(`${element} ${field === "fills" ? "fill" : "stroke"} ${paint(value) ?? "none"}`);
|
|
626
|
-
else if (field === "variant") parts.push(`${element} variant ${Object.values(value ?? {}).join(", ")}`);
|
|
627
|
-
else if (field === "textStyle") parts.push(`${element} text style ${Object.entries(value ?? {}).filter(([name]) => !/Id$/.test(name)).map(([name, part]) => `${name} ${part}`).join(", ")}`);
|
|
628
|
-
else parts.push(`${element} ${field} ${typeof value === "object" ? JSON.stringify(value) : value}`);
|
|
629
|
-
}
|
|
630
|
-
return parts;
|
|
631
|
-
};
|
|
632
|
-
const visit = (node, depth) => {
|
|
633
|
-
if (!node.visible) return;
|
|
634
|
-
const raw = projection.nodes[node.id];
|
|
635
|
-
const reference = instanceReference(node, projection);
|
|
636
|
-
const indent = " ".repeat(depth);
|
|
637
|
-
if (reference && node !== tree.root) {
|
|
638
|
-
const set = components?.(reference);
|
|
639
|
-
const pairs = variantPairs(node.variant ?? raw.instance?.variant, set).map(([, text]) => text);
|
|
640
|
-
const changes = overrideLines(raw, set);
|
|
641
|
-
const texts = descendantTexts(node, projection);
|
|
642
|
-
lines.push(`${indent}${node.name} · copy of ${set?.name ?? "a component"}${pairs.length ? ` (${pairs.join(", ")})` : ""} · ${place(node)}${sizing(raw, node)}${texts.length ? ` · texts ${texts.map((text) => JSON.stringify(text)).join(", ")}` : ""}${changes.length ? ` · changes ${changes.join("; ")}` : ""}`);
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
const parts = [
|
|
646
|
-
`${node.name} · ${node.type === "TEXT" ? "text" : node.type.toLowerCase()} · ${place(node)}${sizing(raw, node)}`,
|
|
647
|
-
...box(raw),
|
|
648
|
-
...visuals(raw),
|
|
649
|
-
...(node.type === "TEXT" ? typeSpec(raw) : []),
|
|
650
|
-
];
|
|
651
|
-
if (node.type === "TEXT") {
|
|
652
|
-
const token = tokenOf(raw, "text");
|
|
653
|
-
parts.push(`text ${token ? `${token} ` : ""}${JSON.stringify(String(node.text ?? raw.text ?? ""))}`);
|
|
654
|
-
}
|
|
655
|
-
lines.push(`${indent}${parts.join(" · ")}`);
|
|
656
|
-
const direction = raw.layout === "flex" ? raw["layout-flex-dir"] ?? "row" : null;
|
|
657
|
-
const stored = node.children.filter(({ visible }) => visible);
|
|
658
|
-
const children = direction && !direction.endsWith("reverse") ? [...stored].reverse() : stored;
|
|
659
|
-
for (const child of children) visit(child, depth + 1);
|
|
660
|
-
};
|
|
661
|
-
visit(tree.root, 0);
|
|
662
|
-
return lines.join("\n");
|
|
663
|
-
}
|
|
664
|
-
|
|
665
551
|
export function localWireframe(design, { full = false } = {}) {
|
|
666
552
|
const collapse = (node) => ({
|
|
667
553
|
...node,
|
|
@@ -670,8 +556,17 @@ export function localWireframe(design, { full = false } = {}) {
|
|
|
670
556
|
node !== design.tree.root &&
|
|
671
557
|
instanceReference(node, design.projection)
|
|
672
558
|
? []
|
|
673
|
-
: node.children.filter(({ visible }) => visible).map(collapse),
|
|
559
|
+
: node.children.filter(({ visible }) => design.layoutSnapshot || visible).map(collapse),
|
|
674
560
|
});
|
|
561
|
+
if (design.layoutSnapshot) {
|
|
562
|
+
const snapshot = design.layoutSnapshot;
|
|
563
|
+
const header = `Geometry: ${snapshot.status} · renderer: ${snapshot.renderer} · revision: ${snapshot.revision}\nSnapshot: ${snapshot.id}\nWireframe: spatial orientation only; use spec for constraints and PNG for appearance.\n`;
|
|
564
|
+
const unknown = Object.entries(snapshot.nodes).filter(([, value]) => !value.bounds);
|
|
565
|
+
if (unknown.length)
|
|
566
|
+
return `${header}No spatial drawing: native geometry is incomplete.\n${unknown.map(([id, value]) => `${design.projection.nodes[id]?.name ?? "Element"}: x=? y=? width=? height=? · partial (${value.reason})`).join("\n")}`;
|
|
567
|
+
return header + asciiWireframe({ ...design.tree, root: collapse(design.tree.root) })
|
|
568
|
+
.replace("view --as text lists exact sizes", "view --as spec lists native bounds and authored constraints");
|
|
569
|
+
}
|
|
675
570
|
return asciiWireframe({ ...design.tree, root: collapse(design.tree.root) });
|
|
676
571
|
}
|
|
677
572
|
|
|
@@ -747,6 +642,17 @@ export function platformHints(workspace, design) {
|
|
|
747
642
|
}
|
|
748
643
|
|
|
749
644
|
export async function validateDesign(workspace, design) {
|
|
645
|
+
design = await resolveDesignLayout(workspace, design);
|
|
646
|
+
if (design.layoutSnapshot.status !== "resolved") {
|
|
647
|
+
const issues = design.layoutSnapshot.diagnostics.map(issue => ({ ...issue, severity: "warning" }));
|
|
648
|
+
const visibleCount = node => node.visible ? 1 + node.children.reduce((sum, child) => sum + visibleCount(child), 0) : 0;
|
|
649
|
+
return {
|
|
650
|
+
issues, issueCount: issues.length,
|
|
651
|
+
coverage: { complete: false, nodeCount: Object.keys(design.projection.nodes).length,
|
|
652
|
+
visibleNodeCount: visibleCount(design.tree.root), checks: [], skipped: [{ check: "native-layout",
|
|
653
|
+
reason: "Actual geometry is incomplete; seed bounds are not checked" }] },
|
|
654
|
+
};
|
|
655
|
+
}
|
|
750
656
|
const issues = (design.projection.diagnostics ?? []).map((diagnostic) => ({
|
|
751
657
|
...diagnostic,
|
|
752
658
|
severity: "warning",
|
|
@@ -768,16 +674,10 @@ export async function validateDesign(workspace, design) {
|
|
|
768
674
|
check: "visual",
|
|
769
675
|
reason: "The selected node is hidden by itself or an ancestor",
|
|
770
676
|
});
|
|
771
|
-
const metrics =
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
nodes: Object.fromEntries(
|
|
776
|
-
Object.entries(nodes).filter(([id]) => visibleIds.has(id)),
|
|
777
|
-
),
|
|
778
|
-
},
|
|
779
|
-
workspace,
|
|
780
|
-
);
|
|
677
|
+
const metrics = {
|
|
678
|
+
items: design.render.textMetrics.filter(item => visibleIds.has(item.nodeId)),
|
|
679
|
+
diagnostics: design.layoutSnapshot.diagnostics,
|
|
680
|
+
};
|
|
781
681
|
for (const item of metrics.items) {
|
|
782
682
|
const node = nodes[item.nodeId];
|
|
783
683
|
if (item.missingGlyphs.length) {
|
package/bin/help-topics.mjs
CHANGED
|
@@ -55,7 +55,7 @@ const rules = {
|
|
|
55
55
|
() => [CLI_RULES.modelling.packages, CLI_RULES.modelling.presentations],
|
|
56
56
|
],
|
|
57
57
|
output: [
|
|
58
|
-
"
|
|
58
|
+
"Specifications for implementation, wireframes for structure, PNG for appearance",
|
|
59
59
|
() =>
|
|
60
60
|
Object.values(CLI_RULES.output)
|
|
61
61
|
.filter((value) => typeof value === "string")
|
|
@@ -80,7 +80,7 @@ const notes = {
|
|
|
80
80
|
"Names the target: --page, --component, --token, --canvas or nothing for the whole package; --as text first.",
|
|
81
81
|
],
|
|
82
82
|
export: [
|
|
83
|
-
"
|
|
83
|
+
"Three outputs: --format spec for implementation, wireframe for structure, png for appearance. --format png --evidence writes all three from one native layout. Compare layout.hash; image inspection requires an image-capable caller.",
|
|
84
84
|
],
|
|
85
85
|
validate: [
|
|
86
86
|
"A page checks every version and a component every variant unless --platform or --variant names one; read coverage and skipped checks.",
|
package/bin/help.mjs
CHANGED
|
@@ -63,7 +63,7 @@ No --theme uses project defaults; App current selection is independent.
|
|
|
63
63
|
--theme overrides only named groups. No data or App state is written.
|
|
64
64
|
Write: token theme add|rename|default|delete --theme Group/Option.`,
|
|
65
65
|
view: `Usage:
|
|
66
|
-
smallpen view PACKAGE [what] [--as text|wireframe|png|issues] [--theme G/O]...
|
|
66
|
+
smallpen view PACKAGE [what] [--as text|spec|wireframe|png|issues] [--theme G/O]...
|
|
67
67
|
|
|
68
68
|
Look at anything by name; no IDs. What:
|
|
69
69
|
(nothing) the whole package: themes, Tokens, components, pages
|
|
@@ -74,9 +74,14 @@ Look at anything by name; no IDs. What:
|
|
|
74
74
|
--pages [--platform mobile] every page
|
|
75
75
|
--page Board one page ("Board" or "Tasks / Board")
|
|
76
76
|
[--platform mobile] [--element "Top bar"] one version, one part of it
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
Three design outputs: spec for implementation and provenance, wireframe for
|
|
78
|
+
spatial orientation, PNG for high-fidelity appearance. --as spec reports actual
|
|
79
|
+
bounds separately from fixed/fill/hug constraints, Token/component sources and
|
|
80
|
+
node diagnostics. --element expands details. Compare layout.hash and selection
|
|
81
|
+
across outputs. ?/partial means unresolved; never implement from seed geometry.
|
|
82
|
+
--as text (default) is a compact navigation outline; issues lists layout, text
|
|
83
|
+
and contrast problems. Tokens have text, wireframe and png. Specifications
|
|
84
|
+
target one page/component; choose a target before requesting --as spec.
|
|
80
85
|
--theme Group/Option shows values under that option for this call only.
|
|
81
86
|
Large results go to a file. Unknown names fail with the names that exist;
|
|
82
87
|
"Name [2]" picks one of a name stored twice.
|
|
@@ -105,15 +110,17 @@ for the caller.`,
|
|
|
105
110
|
export: `Usage:
|
|
106
111
|
smallpen export PACKAGE [--page P [--platform X] [--element E]]
|
|
107
112
|
[--component C [--variant V] [--element E]] [--design-system]
|
|
108
|
-
[--format text|wireframe|png] [--output FILE] [--scale N] [--json]
|
|
113
|
+
[--format text|spec|wireframe|png] [--output FILE] [--scale N] [--json]
|
|
109
114
|
|
|
110
|
-
Save a
|
|
111
|
-
named the way view names it. Nothing named is the main page.
|
|
112
|
-
--format
|
|
113
|
-
|
|
115
|
+
Save a specification (implementation), wireframe (structure, default) or PNG
|
|
116
|
+
(appearance), named the way view names it. Nothing named is the main page.
|
|
117
|
+
Read --format spec before implementation; inspect PNG only when the caller
|
|
118
|
+
can see images. Text files contain LF newlines and alignment spaces.
|
|
114
119
|
Missing --output uses a temporary file. Returns path, hash, bytes, target,
|
|
115
120
|
themes and revisions. --design-system exports the generated Token/component
|
|
116
|
-
page; do not draw a copy. --
|
|
121
|
+
page; do not draw a copy. --format png --evidence writes PNG, .spec.txt,
|
|
122
|
+
.wireframe.txt and JSON evidence from one native layout snapshot. Read skipped
|
|
123
|
+
coverage; untested sizes/states and undeclared business rules remain unknown.`,
|
|
117
124
|
version: `Usage:
|
|
118
125
|
smallpen version [--json]
|
|
119
126
|
|
|
@@ -661,14 +668,16 @@ Separate actions:
|
|
|
661
668
|
name; advanced apply runs an exact batch, such as an undo batch.
|
|
662
669
|
definitions token theme list is a cheap settings query; search finds reusable Tokens
|
|
663
670
|
and components.
|
|
664
|
-
view
|
|
671
|
+
view Navigate with text; implement with spec; orient with wireframe;
|
|
672
|
+
inspect appearance with PNG. Choose one page/component/region.
|
|
665
673
|
validate Problems and coverage; data validity is separate from visual QA.
|
|
666
|
-
export
|
|
667
|
-
|
|
668
|
-
|
|
674
|
+
export Specification, wireframe or PNG files with layout.hash/selection.
|
|
675
|
+
Three design outputs: spec for implementation and provenance; wireframe for
|
|
676
|
+
structure; PNG for appearance. --format png --evidence writes all three from
|
|
677
|
+
one native solve. Match layout.hash and selection; read unknowns and coverage.
|
|
669
678
|
export defaults to a text wireframe; PNG requires --format png. The CLI does
|
|
670
679
|
not detect model image capability. Text-only models may export PNG deliverables
|
|
671
|
-
but must review
|
|
680
|
+
but must review specifications/issues and must not claim visual inspection.
|
|
672
681
|
|
|
673
682
|
Usage:
|
|
674
683
|
smallpen <command> [arguments] [--json]
|