@penpair/core 0.3.7 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters.d.ts +1 -1
- package/dist/adapters.js +4 -0
- package/dist/approvals.d.ts +4 -1
- package/dist/approvals.js +7 -4
- package/dist/baseline-coverage.js +3 -3
- package/dist/baselines-config.d.ts +3 -0
- package/dist/baselines-config.js +3 -0
- package/dist/baselines.d.ts +7 -0
- package/dist/baselines.js +90 -21
- package/dist/bounded-file.d.ts +2 -0
- package/dist/bounded-file.js +38 -0
- package/dist/browser-wake.d.ts +22 -0
- package/dist/browser-wake.js +241 -0
- package/dist/cli-main.js +193 -18
- package/dist/design-commands/context.d.ts +9 -0
- package/dist/design-commands/context.js +1 -0
- package/dist/design-commands/index.d.ts +3 -0
- package/dist/design-commands/index.js +7 -0
- package/dist/design-commands/map-command.d.ts +4 -0
- package/dist/design-commands/map-command.js +685 -0
- package/dist/design-commands/release-command.d.ts +3 -0
- package/dist/design-commands/release-command.js +239 -0
- package/dist/design-releases/artifact-paths.d.ts +6 -0
- package/dist/design-releases/artifact-paths.js +82 -0
- package/dist/design-releases/assemble.d.ts +3 -0
- package/dist/design-releases/assemble.js +176 -0
- package/dist/design-releases/check.d.ts +16 -0
- package/dist/design-releases/check.js +323 -0
- package/dist/design-releases/context.d.ts +2 -0
- package/dist/design-releases/context.js +119 -0
- package/dist/design-releases/contract-graph.d.ts +13 -0
- package/dist/design-releases/contract-graph.js +75 -0
- package/dist/design-releases/fidelity.d.ts +23 -0
- package/dist/design-releases/fidelity.js +328 -0
- package/dist/design-releases/finalize.d.ts +4 -0
- package/dist/design-releases/finalize.js +201 -0
- package/dist/design-releases/index.d.ts +8 -0
- package/dist/design-releases/index.js +7 -0
- package/dist/design-releases/live.d.ts +22 -0
- package/dist/design-releases/live.js +97 -0
- package/dist/design-releases/snippets.d.ts +5 -0
- package/dist/design-releases/snippets.js +89 -0
- package/dist/design-releases/store.d.ts +10 -0
- package/dist/design-releases/store.js +205 -0
- package/dist/design-releases/types.d.ts +161 -0
- package/dist/design-releases/types.js +1 -0
- package/dist/design-releases/validation.d.ts +6 -0
- package/dist/design-releases/validation.js +384 -0
- package/dist/design-releases/verify.d.ts +3 -0
- package/dist/design-releases/verify.js +148 -0
- package/dist/errors.js +1 -1
- package/dist/image-codec.d.ts +3 -0
- package/dist/image-codec.js +78 -0
- package/dist/image-evidence.d.ts +15 -0
- package/dist/image-evidence.js +121 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/inspection.js +13 -5
- package/dist/instance/config-schema.d.ts +4 -0
- package/dist/instance/config-schema.js +7 -0
- package/dist/instance/connection.d.ts +2 -0
- package/dist/instance/connection.js +15 -3
- package/dist/kit-source.js +1 -1
- package/dist/layout-policy.js +21 -11
- package/dist/layout-settling.d.ts +1 -0
- package/dist/layout-settling.js +51 -17
- package/dist/layout.d.ts +7 -1
- package/dist/layout.js +19 -3
- package/dist/map-penpot/content-validation.d.ts +3 -0
- package/dist/map-penpot/content-validation.js +57 -0
- package/dist/map-penpot/content.d.ts +22 -0
- package/dist/map-penpot/content.js +173 -0
- package/dist/map-penpot/details.d.ts +4 -0
- package/dist/map-penpot/details.js +84 -0
- package/dist/map-penpot/evidence-error.d.ts +5 -0
- package/dist/map-penpot/evidence-error.js +9 -0
- package/dist/map-penpot/icons.d.ts +5 -0
- package/dist/map-penpot/icons.js +79 -0
- package/dist/map-penpot/import-content.d.ts +2 -0
- package/dist/map-penpot/import-content.js +150 -0
- package/dist/map-penpot/import.d.ts +5 -0
- package/dist/map-penpot/import.js +255 -0
- package/dist/map-penpot/index.d.ts +11 -0
- package/dist/map-penpot/index.js +10 -0
- package/dist/map-penpot/path-geometry.d.ts +5 -0
- package/dist/map-penpot/path-geometry.js +51 -0
- package/dist/map-penpot/presentation.d.ts +20 -0
- package/dist/map-penpot/presentation.js +205 -0
- package/dist/map-penpot/projection.d.ts +8 -0
- package/dist/map-penpot/projection.js +176 -0
- package/dist/map-penpot/publish.d.ts +4 -0
- package/dist/map-penpot/publish.js +301 -0
- package/dist/map-penpot/release-maps.d.ts +21 -0
- package/dist/map-penpot/release-maps.js +104 -0
- package/dist/map-penpot/snippets.d.ts +4 -0
- package/dist/map-penpot/snippets.js +164 -0
- package/dist/map-penpot/status-labels.d.ts +7 -0
- package/dist/map-penpot/status-labels.js +7 -0
- package/dist/map-penpot/types.d.ts +161 -0
- package/dist/map-penpot/types.js +1 -0
- package/dist/maps/annotation-layout.d.ts +24 -0
- package/dist/maps/annotation-layout.js +85 -0
- package/dist/maps/context.d.ts +13 -0
- package/dist/maps/context.js +207 -0
- package/dist/maps/index.d.ts +9 -0
- package/dist/maps/index.js +9 -0
- package/dist/maps/layout.d.ts +35 -0
- package/dist/maps/layout.js +364 -0
- package/dist/maps/proposal.d.ts +5 -0
- package/dist/maps/proposal.js +67 -0
- package/dist/maps/render.d.ts +4 -0
- package/dist/maps/render.js +331 -0
- package/dist/maps/review.d.ts +16 -0
- package/dist/maps/review.js +90 -0
- package/dist/maps/store.d.ts +21 -0
- package/dist/maps/store.js +222 -0
- package/dist/maps/types.d.ts +206 -0
- package/dist/maps/types.js +1 -0
- package/dist/maps/validation.d.ts +10 -0
- package/dist/maps/validation.js +375 -0
- package/dist/plugin-health.d.ts +13 -0
- package/dist/plugin-health.js +39 -0
- package/dist/skill-routing.js +4 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/skills/penpair-design-release/SKILL.md +55 -0
- package/skills/penpair-design-release/agents/openai.yaml +6 -0
- package/skills/penpair-design-release/references/workflow.md +55 -0
- package/skills/penpair-diagrams/SKILL.md +52 -0
- package/skills/penpair-diagrams/agents/openai.yaml +6 -0
- package/skills/penpair-diagrams/references/workflow.md +84 -0
- package/skills/penpair-handoff/SKILL.md +7 -0
- package/skills/penpair-handoff/references/handoff-flow.md +7 -1
- package/skills/penpair-integration-map/SKILL.md +44 -0
- package/skills/penpair-integration-map/agents/openai.yaml +6 -0
- package/skills/penpair-integration-map/references/workflow.md +33 -0
- package/skills/penpair-journey-map/SKILL.md +42 -0
- package/skills/penpair-journey-map/agents/openai.yaml +6 -0
- package/skills/penpair-journey-map/references/workflow.md +35 -0
- package/skills/penpair-verify/SKILL.md +7 -0
- package/skills/penpair-verify/references/browser-evidence.md +10 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
import { resolveProjectInput } from "./project-input.js";
|
|
3
|
+
export const maximumImageBytes = 64 * 1024 * 1024;
|
|
4
|
+
function losslessWebpSize(bytes) {
|
|
5
|
+
const malformed = () => new Error("Malformed static lossless WebP evidence");
|
|
6
|
+
if (bytes.readUInt32LE(4) + 8 !== bytes.length) {
|
|
7
|
+
throw malformed();
|
|
8
|
+
}
|
|
9
|
+
let imageSize;
|
|
10
|
+
let canvasSize;
|
|
11
|
+
let offset = 12;
|
|
12
|
+
while (offset < bytes.length) {
|
|
13
|
+
if (offset + 8 > bytes.length) {
|
|
14
|
+
throw malformed();
|
|
15
|
+
}
|
|
16
|
+
const chunk = bytes.subarray(offset, offset + 4).toString("latin1");
|
|
17
|
+
const size = bytes.readUInt32LE(offset + 4);
|
|
18
|
+
const payload = offset + 8;
|
|
19
|
+
const end = payload + size;
|
|
20
|
+
const paddedEnd = end + (size % 2);
|
|
21
|
+
if (paddedEnd > bytes.length || (size % 2 !== 0 && bytes[end] !== 0)) {
|
|
22
|
+
throw malformed();
|
|
23
|
+
}
|
|
24
|
+
if (chunk === "VP8 " || chunk === "ANIM" || chunk === "ANMF" || chunk === "ALPH") {
|
|
25
|
+
throw new Error("Provider evidence must reference PNG or static lossless WebP bytes");
|
|
26
|
+
}
|
|
27
|
+
if (chunk === "VP8X") {
|
|
28
|
+
if (offset !== 12 ||
|
|
29
|
+
canvasSize ||
|
|
30
|
+
size !== 10 ||
|
|
31
|
+
(bytes[payload] & 0xc3) !== 0 ||
|
|
32
|
+
bytes.readUIntLE(payload + 1, 3) !== 0) {
|
|
33
|
+
throw malformed();
|
|
34
|
+
}
|
|
35
|
+
canvasSize = {
|
|
36
|
+
width: bytes.readUIntLE(payload + 4, 3) + 1,
|
|
37
|
+
height: bytes.readUIntLE(payload + 7, 3) + 1,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
else if (chunk === "VP8L") {
|
|
41
|
+
if (imageSize || size <= 5 || bytes[payload] !== 0x2f) {
|
|
42
|
+
throw malformed();
|
|
43
|
+
}
|
|
44
|
+
const dimensions = bytes.readUInt32LE(payload + 1);
|
|
45
|
+
if (dimensions >>> 29 !== 0) {
|
|
46
|
+
throw malformed();
|
|
47
|
+
}
|
|
48
|
+
imageSize = {
|
|
49
|
+
width: (dimensions & 0x3fff) + 1,
|
|
50
|
+
height: ((dimensions >>> 14) & 0x3fff) + 1,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
else if (offset === 12) {
|
|
54
|
+
throw malformed();
|
|
55
|
+
}
|
|
56
|
+
offset = paddedEnd;
|
|
57
|
+
}
|
|
58
|
+
if (!imageSize ||
|
|
59
|
+
(canvasSize && (canvasSize.width !== imageSize.width || canvasSize.height !== imageSize.height))) {
|
|
60
|
+
throw malformed();
|
|
61
|
+
}
|
|
62
|
+
return imageSize;
|
|
63
|
+
}
|
|
64
|
+
/** Header/container assurance only; this does not decode compressed pixels. */
|
|
65
|
+
export function inspectImageEvidence(bytes) {
|
|
66
|
+
if (bytes.length > maximumImageBytes) {
|
|
67
|
+
throw new Error("Image exceeds 64 MiB");
|
|
68
|
+
}
|
|
69
|
+
if (bytes.length >= 24 &&
|
|
70
|
+
bytes.subarray(0, 8).toString("hex") === "89504e470d0a1a0a" &&
|
|
71
|
+
bytes.subarray(12, 16).toString("latin1") === "IHDR") {
|
|
72
|
+
const width = bytes.readUInt32BE(16);
|
|
73
|
+
const height = bytes.readUInt32BE(20);
|
|
74
|
+
if (width > 0 && height > 0) {
|
|
75
|
+
return {
|
|
76
|
+
format: "png",
|
|
77
|
+
width,
|
|
78
|
+
height,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (bytes.length >= 12 &&
|
|
83
|
+
bytes.subarray(0, 4).toString("latin1") === "RIFF" &&
|
|
84
|
+
bytes.subarray(8, 12).toString("latin1") === "WEBP") {
|
|
85
|
+
return {
|
|
86
|
+
format: "webp",
|
|
87
|
+
...losslessWebpSize(bytes),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
throw new Error("Provider evidence must reference PNG or static lossless WebP bytes");
|
|
91
|
+
}
|
|
92
|
+
export async function readImageEvidence(root, file) {
|
|
93
|
+
const input = await resolveProjectInput(root, file);
|
|
94
|
+
const handle = await open(input, "r");
|
|
95
|
+
try {
|
|
96
|
+
const info = await handle.stat();
|
|
97
|
+
if (!info.isFile() || info.size > maximumImageBytes) {
|
|
98
|
+
throw new Error("Image exceeds 64 MiB or is not a regular file");
|
|
99
|
+
}
|
|
100
|
+
const buffer = Buffer.alloc(info.size + 1);
|
|
101
|
+
let length = 0;
|
|
102
|
+
while (length < buffer.length) {
|
|
103
|
+
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
|
|
104
|
+
if (bytesRead === 0) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
length += bytesRead;
|
|
108
|
+
}
|
|
109
|
+
if (length > info.size) {
|
|
110
|
+
throw new Error("Image changed during bounded read");
|
|
111
|
+
}
|
|
112
|
+
const bytes = buffer.subarray(0, length);
|
|
113
|
+
return {
|
|
114
|
+
bytes,
|
|
115
|
+
...inspectImageEvidence(bytes),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await handle.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -56,3 +56,8 @@ export * from "./project-status.js";
|
|
|
56
56
|
export * from "./audit-rules.js";
|
|
57
57
|
export * from "./credentials.js";
|
|
58
58
|
export * from "./token-projection.js";
|
|
59
|
+
export * from "./image-evidence.js";
|
|
60
|
+
export * from "./image-codec.js";
|
|
61
|
+
export * from "./maps/index.js";
|
|
62
|
+
export * from "./map-penpot/index.js";
|
|
63
|
+
export * from "./design-releases/index.js";
|
package/dist/index.js
CHANGED
|
@@ -55,3 +55,8 @@ export * from "./project-status.js";
|
|
|
55
55
|
export * from "./audit-rules.js";
|
|
56
56
|
export * from "./credentials.js";
|
|
57
57
|
export * from "./token-projection.js";
|
|
58
|
+
export * from "./image-evidence.js";
|
|
59
|
+
export * from "./image-codec.js";
|
|
60
|
+
export * from "./maps/index.js";
|
|
61
|
+
export * from "./map-penpot/index.js";
|
|
62
|
+
export * from "./design-releases/index.js";
|
package/dist/inspection.js
CHANGED
|
@@ -79,6 +79,7 @@ const clone = (value, unknowns, key) => {
|
|
|
79
79
|
} catch { unknowns.push(key); return null; }
|
|
80
80
|
};
|
|
81
81
|
const read = (object,key,unknowns,label=key) => { try { return clone(object[key],unknowns,label); } catch { unknowns.push(label); return null; } };
|
|
82
|
+
const optional = (object,key,unknowns,label=key) => { try { const value=object[key]; return value===undefined?null:clone(value,unknowns,label); } catch {unknowns.push(label);return null;} };
|
|
82
83
|
const pick = (object,keys,unknowns,prefix) => Object.fromEntries(keys.map(key=>[key,read(object,key,unknowns,prefix+"."+key)]));
|
|
83
84
|
let estimatedBytes = 1024;
|
|
84
85
|
if (CFG.kind === "shape") {
|
|
@@ -97,11 +98,11 @@ if (CFG.kind === "shape") {
|
|
|
97
98
|
if (item.depth <= CFG.depth) {
|
|
98
99
|
const unknowns = [], row = {id:s.id,parentId:item.parentId,index:item.index,depth:item.depth,type:read(s,"type",unknowns),name:read(s,"name",unknowns),childIds:children.slice(0,CFG.maxNodes).map(c=>c.id),childrenTotal:children.length,unknowns};
|
|
99
100
|
if (children.length > CFG.maxNodes) reasons.add("node-budget");
|
|
100
|
-
if (CFG.fields.includes("geometry")) row.geometry = pick(s,["x","y","width","height","rotation","hidden","blocked"],unknowns,"geometry");
|
|
101
|
-
if (CFG.fields.includes("text")) row.text = s.type === "text" ? {characters:read(s,"characters",unknowns),typography:pick(s,["fontId","fontFamily","fontVariantId","fontSize","fontWeight","fontStyle","lineHeight","letterSpacing","growType","direction","align","verticalAlign"],unknowns,"typography")} : null;
|
|
101
|
+
if (CFG.fields.includes("geometry")) row.geometry = pick(s,["x","y","width","height","rotation","hidden","blocked","flipX","flipY","proportionLock"],unknowns,"geometry");
|
|
102
|
+
if (CFG.fields.includes("text")) row.text = s.type === "text" ? {characters:read(s,"characters",unknowns),typography:pick(s,["fontId","fontFamily","fontVariantId","fontSize","fontWeight","fontStyle","lineHeight","letterSpacing","growType","direction","align","verticalAlign","textTransform","textDecoration"],unknowns,"typography")} : null;
|
|
102
103
|
if (row.text && Object.values(row.text.typography).includes("mixed")) unknowns.push("text.mixedRangeStyles");
|
|
103
104
|
if (CFG.fields.includes("layout")) {
|
|
104
|
-
row.layout = {};
|
|
105
|
+
row.layout = {constraints:pick(s,["constraintsHorizontal","constraintsVertical","fixedWhenScrolling"],unknowns,"layout.constraints")};
|
|
105
106
|
for (const [key,keys] of [["flex",["dir","wrap","alignItems","alignContent","justifyContent","rowGap","columnGap","topPadding","rightPadding","bottomPadding","leftPadding","horizontalSizing","verticalSizing"]],["layoutChild",["absolute","zIndex","horizontalSizing","verticalSizing","alignSelf","topMargin","rightMargin","bottomMargin","leftMargin","minWidth","maxWidth","minHeight","maxHeight"]]]) {
|
|
106
107
|
try { row.layout[key] = s[key] ? pick(s[key],keys,unknowns,"layout."+key) : null; } catch { row.layout[key]=null; unknowns.push("layout."+key); }
|
|
107
108
|
}
|
|
@@ -111,10 +112,17 @@ if (CFG.kind === "shape") {
|
|
|
111
112
|
if(grid)for(const key of ["rows","columns"])row.layout.grid[key]=(grid[key]||[]).map((track,index)=>pick(track,["type","value"],unknowns,"layout.grid."+key+"."+index));
|
|
112
113
|
} catch {row.layout.grid=null;unknowns.push("layout.grid");}
|
|
113
114
|
try {row.layout.cell=s.layoutCell?pick(s.layoutCell,["row","rowSpan","column","columnSpan","areaName","position"],unknowns,"layout.cell"):null;} catch {row.layout.cell=null;unknowns.push("layout.cell");}
|
|
114
|
-
row.layout.clipContent = read(s,"clipContent",unknowns,"layout.clipContent");
|
|
115
|
+
row.layout.clipContent = s.type === "board" ? read(s,"clipContent",unknowns,"layout.clipContent") : null;
|
|
116
|
+
row.layout.showInViewMode = s.type === "board" ? read(s,"showInViewMode",unknowns,"layout.showInViewMode") : null;
|
|
117
|
+
row.layout.boardSizing = s.type === "board" ? Object.fromEntries(["horizontalSizing","verticalSizing"].map(key=>[key,optional(s,key,unknowns,"layout.boardSizing."+key)])) : null;
|
|
115
118
|
}
|
|
116
119
|
if (CFG.fields.includes("tokens")) row.tokens = read(s,"tokens",unknowns);
|
|
117
|
-
if (CFG.fields.includes("style"))
|
|
120
|
+
if (CFG.fields.includes("style")) {
|
|
121
|
+
row.style = pick(s,["fills","strokes","shadows","opacity","blendMode","borderRadiusTopLeft","borderRadiusTopRight","borderRadiusBottomRight","borderRadiusBottomLeft"],unknowns,"style");
|
|
122
|
+
for (const key of ["blur","backgroundBlur"]) {
|
|
123
|
+
row.style[key]=optional(s,key,unknowns,"style."+key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
118
126
|
if (CFG.fields.includes("component")) {
|
|
119
127
|
row.component = {};
|
|
120
128
|
for (const key of ["isComponentInstance","isComponentMainInstance","isComponentCopyInstance","isComponentRoot"]) {
|
|
@@ -160,6 +160,10 @@ export declare const projectConfigSchema: {
|
|
|
160
160
|
readonly type: "object";
|
|
161
161
|
readonly required: readonly ["page", "root", "frames"];
|
|
162
162
|
readonly properties: {
|
|
163
|
+
readonly format: {
|
|
164
|
+
readonly enum: readonly ["png", "webp"];
|
|
165
|
+
readonly description: "Format for new references; existing approved formats are preserved unless explicitly migrated.";
|
|
166
|
+
};
|
|
163
167
|
readonly page: {
|
|
164
168
|
readonly type: "string";
|
|
165
169
|
readonly minLength: 1;
|
|
@@ -280,6 +280,13 @@ export const projectConfigSchema = {
|
|
|
280
280
|
"frames",
|
|
281
281
|
],
|
|
282
282
|
properties: {
|
|
283
|
+
format: {
|
|
284
|
+
enum: [
|
|
285
|
+
"png",
|
|
286
|
+
"webp",
|
|
287
|
+
],
|
|
288
|
+
description: "Format for new references; existing approved formats are preserved unless explicitly migrated.",
|
|
289
|
+
},
|
|
283
290
|
page: {
|
|
284
291
|
type: "string",
|
|
285
292
|
minLength: 1,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PluginHealth } from "../plugin-health.js";
|
|
1
2
|
import { type ExternalConfig, type InstanceConfig, type ProjectConfig } from "./config.js";
|
|
2
3
|
/** What Penpot itself, the connection and the checks need, whichever way Penpot is run. Nothing here knows about Docker. */
|
|
3
4
|
export declare class LifecycleError extends Error {
|
|
@@ -18,6 +19,7 @@ export interface DoctorCheck extends Check {
|
|
|
18
19
|
pageId?: string;
|
|
19
20
|
apiVersion?: string;
|
|
20
21
|
heartbeat?: "unknown";
|
|
22
|
+
pluginHealth?: PluginHealth;
|
|
21
23
|
};
|
|
22
24
|
remedy?: string;
|
|
23
25
|
file?: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { commandSignal, withCommandSignal } from "../command-result.js";
|
|
3
3
|
import { redactEndpoint } from "../endpoint.js";
|
|
4
|
+
import { pluginHealthFromError, responsivePluginHealth, } from "../plugin-health.js";
|
|
4
5
|
import { isExternal, } from "./config.js";
|
|
5
6
|
/** What Penpot itself, the connection and the checks need, whichever way Penpot is run. Nothing here knows about Docker. */
|
|
6
7
|
export class LifecycleError extends Error {
|
|
@@ -105,7 +106,7 @@ export async function doctorEditor(config, options) {
|
|
|
105
106
|
check: "editor and plugin",
|
|
106
107
|
layer: "execution",
|
|
107
108
|
ok: true,
|
|
108
|
-
detail: `Fresh read from file "${actual.file}"`,
|
|
109
|
+
detail: `Fresh read from file "${actual.file}"; heartbeat age unavailable; keep-alive unknown`,
|
|
109
110
|
observedAt: new Date().toISOString(),
|
|
110
111
|
durationMs: Date.now() - started,
|
|
111
112
|
evidence: {
|
|
@@ -117,11 +118,16 @@ export async function doctorEditor(config, options) {
|
|
|
117
118
|
}
|
|
118
119
|
: {}),
|
|
119
120
|
heartbeat: "unknown",
|
|
121
|
+
pluginHealth: responsivePluginHealth(),
|
|
120
122
|
},
|
|
121
123
|
});
|
|
122
124
|
out.push(fileIdentityCheck(config, actual));
|
|
123
125
|
}
|
|
124
126
|
catch (error) {
|
|
127
|
+
const pluginHealth = pluginHealthFromError(error);
|
|
128
|
+
const heartbeatDetail = pluginHealth.heartbeatAgeMs === null
|
|
129
|
+
? "heartbeat age unavailable"
|
|
130
|
+
: `last heartbeat ${pluginHealth.heartbeatAgeMs / 1000}s ago (reported by upstream)`;
|
|
125
131
|
if (!connected) {
|
|
126
132
|
out.push({
|
|
127
133
|
check: "MCP transport",
|
|
@@ -136,10 +142,16 @@ export async function doctorEditor(config, options) {
|
|
|
136
142
|
check: "editor and plugin",
|
|
137
143
|
layer: "execution",
|
|
138
144
|
ok: false,
|
|
139
|
-
detail: redactEndpoint(error instanceof Error ? error.message : String(error))
|
|
145
|
+
detail: `${redactEndpoint(error instanceof Error ? error.message : String(error))}; ${heartbeatDetail}; keep-alive unknown`,
|
|
140
146
|
observedAt: new Date().toISOString(),
|
|
141
147
|
durationMs: Date.now() - started,
|
|
142
|
-
|
|
148
|
+
evidence: {
|
|
149
|
+
heartbeat: "unknown",
|
|
150
|
+
pluginHealth,
|
|
151
|
+
},
|
|
152
|
+
remedy: pluginHealth.state === "suspended"
|
|
153
|
+
? "Bring the intended Penpot tab to the foreground, then rerun doctor for a fresh read. Reconcile any uncertain run before replaying a write."
|
|
154
|
+
: "Open the intended file, connect the MCP plugin and keep the editor responsive; rerun doctor for a fresh read.",
|
|
143
155
|
});
|
|
144
156
|
}
|
|
145
157
|
finished = true;
|
package/dist/kit-source.js
CHANGED
|
@@ -225,7 +225,7 @@ const pp = (() => {
|
|
|
225
225
|
try { return await settleLayout(async nativeMs => {
|
|
226
226
|
if (penpot.currentFile.id !== fileId || penpot.currentPage.id !== pageId) throw new Error("Layout target changed between samples");
|
|
227
227
|
return observeLayout(penpot, root, { nativeMs, maxNodes: options.maxNodes });
|
|
228
|
-
}, { timeoutMs: options.timeoutMs === undefined ? 10000 : options.timeoutMs, intervalMs: options.intervalMs === undefined ? 100 : options.intervalMs, strategy: options.strategy === undefined ? "auto" : options.strategy, sleep: wait }); }
|
|
228
|
+
}, { timeoutMs: options.timeoutMs === undefined ? 10000 : options.timeoutMs, intervalMs: options.intervalMs === undefined ? 100 : options.intervalMs, strategy: options.strategy === undefined ? "auto" : options.strategy, maxNodes: options.maxNodes, sleep: wait }); }
|
|
229
229
|
catch (error) { return { stable: false, method: "unavailable", samples: 0, reason: String(error && error.message || error) }; }
|
|
230
230
|
};
|
|
231
231
|
const flush = async (options = {}) => {
|
package/dist/layout-policy.js
CHANGED
|
@@ -2,21 +2,26 @@ import { settleLayout } from "./layout-settling.js";
|
|
|
2
2
|
/** Trusted, dependency-free observation source for the Penpot sandbox. */
|
|
3
3
|
const layoutObservationSource = String.raw `
|
|
4
4
|
const observeLayout = async (penpot, root, options) => {
|
|
5
|
-
if (!Number.isInteger(options.maxNodes === undefined ? 10000 : options.maxNodes) || (options.maxNodes !== undefined && (options.maxNodes < 1 || options.maxNodes >
|
|
5
|
+
if (!Number.isInteger(options.maxNodes === undefined ? 10000 : options.maxNodes) || (options.maxNodes !== undefined && (options.maxNodes < 1 || options.maxNodes > 100000))) throw new Error("Invalid settling node budget");
|
|
6
|
+
const roots = Array.isArray(root) ? root : [root];
|
|
7
|
+
if (!roots.length || roots.some(shape => !shape)) throw new Error("layout target does not exist");
|
|
6
8
|
const fileId = penpot.currentFile.id, pageId = penpot.currentPage.id;
|
|
7
9
|
const validTarget = () => {
|
|
8
10
|
if (penpot.currentFile.id !== fileId || penpot.currentPage.id !== pageId) throw new Error("Layout target changed while waiting");
|
|
9
|
-
|
|
11
|
+
for (const shape of roots) {
|
|
12
|
+
if (shape.id !== penpot.currentPage.root.id && typeof penpot.currentPage.getShapeById === "function" && !penpot.currentPage.getShapeById(shape.id)) throw new Error("Layout target no longer exists");
|
|
13
|
+
}
|
|
10
14
|
};
|
|
11
|
-
|
|
15
|
+
|
|
12
16
|
let nativeOutcome = "skipped", nativeError;
|
|
13
|
-
|
|
17
|
+
validTarget();
|
|
18
|
+
const nativeTargets = roots.every(shape => typeof shape.waitForLayoutUpdate === "function") ? roots : [penpot];
|
|
14
19
|
if (options.nativeMs > 0) {
|
|
15
20
|
nativeOutcome = "unsupported";
|
|
16
|
-
if (typeof target.waitForLayoutUpdate === "function") {
|
|
21
|
+
if (nativeTargets.every(target => typeof target.waitForLayoutUpdate === "function")) {
|
|
17
22
|
let timer;
|
|
18
23
|
try {
|
|
19
|
-
await Promise.race([target.waitForLayoutUpdate(options.nativeMs), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("Native layout attempt timed out")), options.nativeMs); })]);
|
|
24
|
+
await Promise.race([Promise.all(nativeTargets.map(target => target.waitForLayoutUpdate(options.nativeMs))), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("Native layout attempt timed out")), options.nativeMs); })]);
|
|
20
25
|
nativeOutcome = "completed";
|
|
21
26
|
} catch (error) { nativeOutcome = "failed"; nativeError = String(error && error.message || error); }
|
|
22
27
|
finally { clearTimeout(timer); }
|
|
@@ -24,20 +29,25 @@ const observeLayout = async (penpot, root, options) => {
|
|
|
24
29
|
}
|
|
25
30
|
validTarget();
|
|
26
31
|
// Keep hierarchy and sibling order as well as geometry; repeated IDs include cycles.
|
|
27
|
-
const geometry = [], stack = [
|
|
32
|
+
const geometry = [], stack = roots.map(shape => [shape, null, 0]).reverse(), seen = new Set();
|
|
33
|
+
let truncated = false;
|
|
28
34
|
while (stack.length) {
|
|
29
|
-
if (geometry.length >= (options.maxNodes || 10000))
|
|
35
|
+
if (geometry.length >= (options.maxNodes || 10000)) { truncated = true; break; }
|
|
30
36
|
const [shape, parent, index] = stack.pop();
|
|
31
37
|
if (!shape || typeof shape.id !== "string" || !shape.id || seen.has(shape.id) || ![shape.x, shape.y, shape.width, shape.height].every(Number.isFinite)) throw new Error("Malformed layout geometry or duplicate IDs");
|
|
32
38
|
seen.add(shape.id);
|
|
33
39
|
geometry.push([shape.id, shape.x, shape.y, shape.width, shape.height, parent, index]);
|
|
34
40
|
const children = shape.children || [];
|
|
35
41
|
if (!Array.isArray(children)) throw new Error("Malformed layout hierarchy");
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
const available = (options.maxNodes || 10000) - geometry.length;
|
|
43
|
+
if (stack.length + children.length > available) truncated = true;
|
|
44
|
+
// Only retain the bounded depth-first prefix; avoid allocating a page-sized traversal stack.
|
|
45
|
+
if (children.length >= available) stack.length = 0;
|
|
46
|
+
else if (stack.length > available - children.length) stack.splice(0, stack.length - (available - children.length));
|
|
47
|
+
for (let index = Math.min(children.length, available) - 1; index >= 0; index--) stack.push([children[index], shape.id, index]);
|
|
38
48
|
}
|
|
39
49
|
validTarget();
|
|
40
|
-
return { ok: true, fileId, pageId, native: nativeOutcome === "completed", nativeOutcome, nativeError, geometry,
|
|
50
|
+
return { ok: true, fileId, pageId, native: nativeOutcome === "completed", nativeOutcome, nativeError, geometry, rootIds: roots.map(shape => shape.id), truncated };
|
|
41
51
|
};
|
|
42
52
|
`;
|
|
43
53
|
export const layoutPolicySource = `${layoutObservationSource}\nconst settleLayout = ${settleLayout.toString()};`;
|
package/dist/layout-settling.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/** Self-contained policy: its JavaScript source also runs inside the Penpot sandbox. */
|
|
2
2
|
export async function settleLayout(observe, options) {
|
|
3
|
-
const { timeoutMs, intervalMs, strategy = "auto", sleep } = options;
|
|
4
|
-
if (!
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
const { timeoutMs, intervalMs, strategy = "auto", maxNodes = 10000, sleep } = options;
|
|
4
|
+
if (!Number.isInteger(maxNodes) ||
|
|
5
|
+
maxNodes < 1 ||
|
|
6
|
+
maxNodes > 100000 ||
|
|
7
|
+
![
|
|
8
|
+
"auto",
|
|
9
|
+
"native",
|
|
10
|
+
"geometry",
|
|
11
|
+
].includes(strategy) ||
|
|
9
12
|
!Number.isFinite(timeoutMs) ||
|
|
10
13
|
timeoutMs < 1 ||
|
|
11
14
|
timeoutMs > 30000 ||
|
|
@@ -17,6 +20,9 @@ export async function settleLayout(observe, options) {
|
|
|
17
20
|
const started = Date.now();
|
|
18
21
|
const deadline = started + timeoutMs;
|
|
19
22
|
let samples = 0;
|
|
23
|
+
let nodeCount = 0;
|
|
24
|
+
let rootIds = [];
|
|
25
|
+
let truncated = false;
|
|
20
26
|
let nativeOutcome = "skipped";
|
|
21
27
|
let fallbackReason;
|
|
22
28
|
let targetIdentity;
|
|
@@ -24,8 +30,12 @@ export async function settleLayout(observe, options) {
|
|
|
24
30
|
let matchingSamples = 0;
|
|
25
31
|
const createResult = (stable, method, reason) => ({
|
|
26
32
|
stable,
|
|
27
|
-
method,
|
|
33
|
+
method: truncated ? "partial" : method,
|
|
28
34
|
samples,
|
|
35
|
+
rootIds,
|
|
36
|
+
nodeCount,
|
|
37
|
+
maxNodes,
|
|
38
|
+
truncated,
|
|
29
39
|
strategy,
|
|
30
40
|
nativeOutcome,
|
|
31
41
|
...(fallbackReason
|
|
@@ -34,9 +44,10 @@ export async function settleLayout(observe, options) {
|
|
|
34
44
|
}
|
|
35
45
|
: {}),
|
|
36
46
|
elapsedMs: Date.now() - started,
|
|
37
|
-
...(reason
|
|
47
|
+
...(reason || truncated
|
|
38
48
|
? {
|
|
39
|
-
reason
|
|
49
|
+
reason: reason ??
|
|
50
|
+
"Only the bounded layout sample was observed; independent readback is required",
|
|
40
51
|
}
|
|
41
52
|
: {}),
|
|
42
53
|
});
|
|
@@ -74,20 +85,34 @@ export async function settleLayout(observe, options) {
|
|
|
74
85
|
const nextTargetIdentity = JSON.stringify([
|
|
75
86
|
observation.fileId,
|
|
76
87
|
observation.pageId,
|
|
77
|
-
observation.
|
|
88
|
+
observation.rootIds ?? [
|
|
89
|
+
observation.geometry?.[0]?.[0],
|
|
90
|
+
],
|
|
78
91
|
]);
|
|
79
92
|
if (targetIdentity && targetIdentity !== nextTargetIdentity) {
|
|
80
93
|
throw new Error("Layout target changed between samples");
|
|
81
94
|
}
|
|
82
95
|
targetIdentity = nextTargetIdentity;
|
|
83
|
-
if (observation.
|
|
84
|
-
!Array.isArray(observation.geometry) ||
|
|
96
|
+
if (!Array.isArray(observation.geometry) ||
|
|
85
97
|
!observation.geometry.length ||
|
|
86
|
-
observation.geometry.length >
|
|
98
|
+
observation.geometry.length > maxNodes) {
|
|
87
99
|
throw new Error("Incomplete layout geometry evidence");
|
|
88
100
|
}
|
|
101
|
+
const observedRoots = observation.rootIds ?? [
|
|
102
|
+
observation.geometry[0][0],
|
|
103
|
+
];
|
|
104
|
+
if (!Array.isArray(observedRoots) ||
|
|
105
|
+
!observedRoots.length ||
|
|
106
|
+
observedRoots.some((id) => typeof id !== "string" || !id) ||
|
|
107
|
+
new Set(observedRoots).size !== observedRoots.length) {
|
|
108
|
+
throw new Error("Malformed layout root evidence");
|
|
109
|
+
}
|
|
110
|
+
rootIds = observedRoots;
|
|
111
|
+
nodeCount = observation.geometry.length;
|
|
112
|
+
truncated = observation.truncated;
|
|
89
113
|
const observedShapeIds = new Set();
|
|
90
114
|
const siblingCountByParent = new Map();
|
|
115
|
+
let rootIndex = 0;
|
|
91
116
|
for (const geometryRow of observation.geometry) {
|
|
92
117
|
if (!Array.isArray(geometryRow) ||
|
|
93
118
|
geometryRow.length !== 7 ||
|
|
@@ -102,9 +127,12 @@ export async function settleLayout(observe, options) {
|
|
|
102
127
|
geometryRow[6] < 0) {
|
|
103
128
|
throw new Error("Layout API returned no geometry evidence or malformed hierarchy");
|
|
104
129
|
}
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
130
|
+
if (geometryRow[5] === null) {
|
|
131
|
+
if (geometryRow[0] !== rootIds[rootIndex++] || geometryRow[6] !== 0) {
|
|
132
|
+
throw new Error("Malformed layout root hierarchy");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (typeof geometryRow[5] !== "string") {
|
|
108
136
|
throw new Error("Malformed layout root hierarchy");
|
|
109
137
|
}
|
|
110
138
|
if (geometryRow[5] !== null) {
|
|
@@ -116,6 +144,9 @@ export async function settleLayout(observe, options) {
|
|
|
116
144
|
}
|
|
117
145
|
observedShapeIds.add(geometryRow[0]);
|
|
118
146
|
}
|
|
147
|
+
if (!truncated && rootIndex !== rootIds.length) {
|
|
148
|
+
throw new Error("Incomplete layout root evidence");
|
|
149
|
+
}
|
|
119
150
|
if (sampleIndex === 0) {
|
|
120
151
|
nativeOutcome =
|
|
121
152
|
observation.nativeOutcome ||
|
|
@@ -143,7 +174,10 @@ export async function settleLayout(observe, options) {
|
|
|
143
174
|
if (Date.now() >= deadline) {
|
|
144
175
|
return createResult(false, "geometry", "Layout observation exceeded its deadline");
|
|
145
176
|
}
|
|
146
|
-
const currentGeometry = JSON.stringify(
|
|
177
|
+
const currentGeometry = JSON.stringify([
|
|
178
|
+
observation.geometry,
|
|
179
|
+
observation.truncated,
|
|
180
|
+
]);
|
|
147
181
|
matchingSamples = currentGeometry === previousGeometry ? matchingSamples + 1 : 1;
|
|
148
182
|
if (matchingSamples >= 3) {
|
|
149
183
|
return createResult(true, "geometry");
|
package/dist/layout.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { PenpotToolClient } from "./client.js";
|
|
|
2
2
|
export type LayoutStrategy = "auto" | "native" | "geometry";
|
|
3
3
|
export interface LayoutOptions {
|
|
4
4
|
shapeId?: string;
|
|
5
|
+
shapeIds?: string[];
|
|
6
|
+
maxNodes?: number;
|
|
5
7
|
strategy?: LayoutStrategy;
|
|
6
8
|
timeoutMs?: number;
|
|
7
9
|
intervalMs?: number;
|
|
@@ -9,7 +11,11 @@ export interface LayoutOptions {
|
|
|
9
11
|
}
|
|
10
12
|
export interface LayoutResult {
|
|
11
13
|
stable: boolean;
|
|
12
|
-
method: "native" | "geometry" | "unavailable";
|
|
14
|
+
method: "native" | "geometry" | "partial" | "unavailable";
|
|
15
|
+
rootIds?: string[];
|
|
16
|
+
nodeCount?: number;
|
|
17
|
+
maxNodes?: number;
|
|
18
|
+
truncated?: boolean;
|
|
13
19
|
samples: number;
|
|
14
20
|
reason?: string;
|
|
15
21
|
strategy?: LayoutStrategy;
|
package/dist/layout.js
CHANGED
|
@@ -5,10 +5,25 @@ import { layoutPolicySource } from "./layout-policy.js";
|
|
|
5
5
|
import { settleLayout } from "./layout-settling.js";
|
|
6
6
|
/** Layout evidence is independent of render equality and visual approval. Transport failures propagate without fallback. */
|
|
7
7
|
export async function waitForStableLayout(client, options = {}) {
|
|
8
|
-
const { timeoutMs = 30000, intervalMs = 200, strategy = "auto", sleep = commandSleep, shapeId, } = options;
|
|
8
|
+
const { timeoutMs = 30000, intervalMs = 200, strategy = "auto", sleep = commandSleep, shapeId, shapeIds, maxNodes = 10000, } = options;
|
|
9
|
+
const roots = shapeIds ??
|
|
10
|
+
(shapeId
|
|
11
|
+
? [
|
|
12
|
+
shapeId,
|
|
13
|
+
]
|
|
14
|
+
: []);
|
|
15
|
+
if (!Array.isArray(roots) ||
|
|
16
|
+
roots.some((id) => typeof id !== "string" || !id.trim()) ||
|
|
17
|
+
new Set(roots).size !== roots.length) {
|
|
18
|
+
throw new Error("Layout roots must be distinct nonempty shape IDs");
|
|
19
|
+
}
|
|
20
|
+
if (!Number.isInteger(maxNodes) || maxNodes < 1 || maxNodes > 100000) {
|
|
21
|
+
throw new Error("Layout node budget must be an integer from 1 to 100000");
|
|
22
|
+
}
|
|
9
23
|
try {
|
|
10
|
-
return await settleLayout(async (nativeMs, remaining) => runSnippet(client, snippet("layout-state", `${layoutPolicySource}\nconst
|
|
11
|
-
|
|
24
|
+
return await settleLayout(async (nativeMs, remaining) => runSnippet(client, snippet("layout-state", `${layoutPolicySource}\nconst roots = CFG.shapeIds.length ? CFG.shapeIds.map(id => penpot.currentPage.getShapeById(id)) : [penpot.currentPage.root];\nreturn await observeLayout(penpot, roots, CFG);`, {
|
|
25
|
+
shapeIds: roots,
|
|
26
|
+
maxNodes,
|
|
12
27
|
nativeMs,
|
|
13
28
|
}, "read"), {
|
|
14
29
|
retries: 0,
|
|
@@ -17,6 +32,7 @@ export async function waitForStableLayout(client, options = {}) {
|
|
|
17
32
|
timeoutMs,
|
|
18
33
|
intervalMs,
|
|
19
34
|
strategy,
|
|
35
|
+
maxNodes,
|
|
20
36
|
sleep,
|
|
21
37
|
});
|
|
22
38
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
function text(value, maximum) {
|
|
2
|
+
return value === undefined || (typeof value === "string" && value.length <= maximum);
|
|
3
|
+
}
|
|
4
|
+
function box(value) {
|
|
5
|
+
if (!value || typeof value !== "object") {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const bounds = value;
|
|
9
|
+
return ([
|
|
10
|
+
"x",
|
|
11
|
+
"y",
|
|
12
|
+
"width",
|
|
13
|
+
"height",
|
|
14
|
+
].every((key) => Number.isFinite(bounds[key])) &&
|
|
15
|
+
Number(bounds.x) >= 0 &&
|
|
16
|
+
Number(bounds.y) >= 0 &&
|
|
17
|
+
Number(bounds.width) >= 64 &&
|
|
18
|
+
Number(bounds.height) > 0);
|
|
19
|
+
}
|
|
20
|
+
/** Reject malformed content geometry before a publication can contact Penpot. */
|
|
21
|
+
export function validateMapDrawingContent(drawing) {
|
|
22
|
+
if (!text(drawing.goal, 8000) ||
|
|
23
|
+
!text(drawing.summary, 240) ||
|
|
24
|
+
!text(drawing.description, 8000)) {
|
|
25
|
+
throw new Error("Map descriptions exceed their supported text budgets");
|
|
26
|
+
}
|
|
27
|
+
if (drawing.header &&
|
|
28
|
+
(!box(drawing.header.titleBox) ||
|
|
29
|
+
(drawing.header.summaryBox !== undefined && !box(drawing.header.summaryBox)) ||
|
|
30
|
+
!Number.isFinite(drawing.header.legendY) ||
|
|
31
|
+
drawing.header.legendY < 0 ||
|
|
32
|
+
!Number.isFinite(drawing.header.height) ||
|
|
33
|
+
drawing.header.height <= 0)) {
|
|
34
|
+
throw new Error("Map header requires finite positive geometry");
|
|
35
|
+
}
|
|
36
|
+
if (drawing.annotations === undefined) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const nodeIds = new Set(drawing.nodes.map((node) => node.id));
|
|
40
|
+
if (!Array.isArray(drawing.annotations) ||
|
|
41
|
+
drawing.annotations.length > 1001 ||
|
|
42
|
+
new Set(drawing.annotations.map((annotation) => annotation?.id)).size !==
|
|
43
|
+
drawing.annotations.length ||
|
|
44
|
+
drawing.annotations.some((annotation) => !annotation ||
|
|
45
|
+
typeof annotation.id !== "string" ||
|
|
46
|
+
!/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,255}$/.test(annotation.id) ||
|
|
47
|
+
!box(annotation) ||
|
|
48
|
+
(annotation.nodeId !== undefined && !nodeIds.has(annotation.nodeId)) ||
|
|
49
|
+
typeof annotation.title !== "string" ||
|
|
50
|
+
!annotation.title.trim() ||
|
|
51
|
+
annotation.title.length > 4000 ||
|
|
52
|
+
!text(annotation.goal, 8000) ||
|
|
53
|
+
!text(annotation.summary, 240) ||
|
|
54
|
+
!text(annotation.description, annotation.nodeId ? 4000 : 8000))) {
|
|
55
|
+
throw new Error("Map annotations require unique identities, bounded text and finite geometry");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { MapDrawing, ProjectionShape } from "./types.js";
|
|
2
|
+
export declare const nativeMapRoles: readonly ["node", "label", "edge", "edge-label", "title", "badge", "legend", "map-summary", "map-full-summary", "map-goal", "map-description", "summary", "full-label", "full-summary", "description", "annotation-panel", "annotation-id", "annotation-heading", "icon-well", "icon", "port-in", "port-out"];
|
|
3
|
+
export interface NativeMapTextEntry {
|
|
4
|
+
role: ProjectionShape["role"];
|
|
5
|
+
logicalId: string;
|
|
6
|
+
text: string;
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
fontSize: number;
|
|
12
|
+
color: string;
|
|
13
|
+
strong?: boolean;
|
|
14
|
+
semantic?: {
|
|
15
|
+
scope: "map" | "node";
|
|
16
|
+
field: "title" | "goal" | "label" | "summary" | "description";
|
|
17
|
+
nodeId?: string;
|
|
18
|
+
compact?: boolean;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare function nativeMapTextEntries(drawing: MapDrawing): NativeMapTextEntry[];
|
|
22
|
+
export declare function nativeMapShapeKeys(drawing: MapDrawing): Set<string>;
|