pi-mono-all 1.0.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/CHANGELOG.md +13 -0
- package/LICENCE.md +7 -0
- package/node_modules/pi-common/package.json +22 -0
- package/node_modules/pi-common/src/auth-config.ts +290 -0
- package/node_modules/pi-common/src/auth.ts +63 -0
- package/node_modules/pi-common/src/cache.ts +60 -0
- package/node_modules/pi-common/src/errors.ts +47 -0
- package/node_modules/pi-common/src/http-client.ts +118 -0
- package/node_modules/pi-common/src/index.ts +7 -0
- package/node_modules/pi-common/src/rate-limiter.ts +32 -0
- package/node_modules/pi-common/src/tool-result.ts +27 -0
- package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
- package/node_modules/pi-mono-ask-user-question/README.md +226 -0
- package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
- package/node_modules/pi-mono-ask-user-question/package.json +29 -0
- package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
- package/node_modules/pi-mono-auto-fix/README.md +77 -0
- package/node_modules/pi-mono-auto-fix/index.ts +488 -0
- package/node_modules/pi-mono-auto-fix/package.json +23 -0
- package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-btw/README.md +24 -0
- package/node_modules/pi-mono-btw/index.ts +499 -0
- package/node_modules/pi-mono-btw/package.json +29 -0
- package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-clear/README.md +40 -0
- package/node_modules/pi-mono-clear/index.ts +45 -0
- package/node_modules/pi-mono-clear/package.json +29 -0
- package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
- package/node_modules/pi-mono-context/README.md +74 -0
- package/node_modules/pi-mono-context/index.ts +641 -0
- package/node_modules/pi-mono-context/package.json +29 -0
- package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
- package/node_modules/pi-mono-context-guard/README.md +81 -0
- package/node_modules/pi-mono-context-guard/index.ts +212 -0
- package/node_modules/pi-mono-context-guard/package.json +23 -0
- package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
- package/node_modules/pi-mono-figma/README.md +236 -0
- package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
- package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
- package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
- package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
- package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
- package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
- package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
- package/node_modules/pi-mono-figma/index.ts +6 -0
- package/node_modules/pi-mono-figma/package.json +33 -0
- package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
- package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
- package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
- package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
- package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
- package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
- package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
- package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
- package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
- package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
- package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
- package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
- package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
- package/node_modules/pi-mono-linear/README.md +159 -0
- package/node_modules/pi-mono-linear/index.ts +6 -0
- package/node_modules/pi-mono-linear/package.json +30 -0
- package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
- package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
- package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
- package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
- package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
- package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
- package/node_modules/pi-mono-loop/README.md +54 -0
- package/node_modules/pi-mono-loop/index.ts +291 -0
- package/node_modules/pi-mono-loop/package.json +26 -0
- package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
- package/node_modules/pi-mono-multi-edit/README.md +244 -0
- package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
- package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
- package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
- package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
- package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
- package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
- package/node_modules/pi-mono-multi-edit/index.ts +266 -0
- package/node_modules/pi-mono-multi-edit/package.json +37 -0
- package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
- package/node_modules/pi-mono-multi-edit/types.ts +53 -0
- package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
- package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
- package/node_modules/pi-mono-review/README.md +30 -0
- package/node_modules/pi-mono-review/common.ts +930 -0
- package/node_modules/pi-mono-review/index.ts +8 -0
- package/node_modules/pi-mono-review/package.json +29 -0
- package/node_modules/pi-mono-review/review-tui.ts +194 -0
- package/node_modules/pi-mono-review/review.ts +119 -0
- package/node_modules/pi-mono-review/reviewer.ts +339 -0
- package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
- package/node_modules/pi-mono-sentinel/README.md +87 -0
- package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
- package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
- package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
- package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
- package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
- package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
- package/node_modules/pi-mono-sentinel/index.ts +43 -0
- package/node_modules/pi-mono-sentinel/package.json +26 -0
- package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
- package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
- package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
- package/node_modules/pi-mono-sentinel/session.ts +95 -0
- package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
- package/node_modules/pi-mono-sentinel/types.ts +39 -0
- package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
- package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
- package/node_modules/pi-mono-simplify/README.md +56 -0
- package/node_modules/pi-mono-simplify/index.ts +78 -0
- package/node_modules/pi-mono-simplify/package.json +29 -0
- package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-status-line/README.md +96 -0
- package/node_modules/pi-mono-status-line/basic.ts +89 -0
- package/node_modules/pi-mono-status-line/expert.ts +689 -0
- package/node_modules/pi-mono-status-line/index.ts +54 -0
- package/node_modules/pi-mono-status-line/package.json +29 -0
- package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
- package/node_modules/pi-mono-team-mode/README.md +246 -0
- package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
- package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
- package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
- package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
- package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
- package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
- package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
- package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
- package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
- package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
- package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
- package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
- package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
- package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
- package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
- package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
- package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
- package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
- package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
- package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
- package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
- package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
- package/node_modules/pi-mono-team-mode/index.ts +825 -0
- package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
- package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
- package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
- package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
- package/node_modules/pi-mono-team-mode/package.json +33 -0
- package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
- package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
- package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
- package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
- package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
- package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
- package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
- package/package.json +76 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildAccessibilityHints,
|
|
3
|
+
buildCssLayoutHints,
|
|
4
|
+
buildDesignTokenHints,
|
|
5
|
+
buildFrameworkHints,
|
|
6
|
+
buildResponsiveHints,
|
|
7
|
+
type FigmaFramework,
|
|
8
|
+
type FigmaStyling,
|
|
9
|
+
} from "./figma-implementation.js";
|
|
10
|
+
import type { FigmaTokenMap } from "./figma-tokens.js";
|
|
11
|
+
|
|
12
|
+
export interface FigmaSummarizerOptions {
|
|
13
|
+
depth?: number;
|
|
14
|
+
includeHidden?: boolean;
|
|
15
|
+
includeVectors?: boolean;
|
|
16
|
+
includeComponentInternals?: boolean;
|
|
17
|
+
maxVisibleText?: number;
|
|
18
|
+
maxChildren?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface FigmaRenderedAsset {
|
|
22
|
+
nodeId: string;
|
|
23
|
+
url?: string | null;
|
|
24
|
+
path?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FigmaSummaryMetadata {
|
|
28
|
+
truncated: boolean;
|
|
29
|
+
truncatedReasons: string[];
|
|
30
|
+
nextSteps: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FigmaNodeSummary {
|
|
34
|
+
id?: string;
|
|
35
|
+
name: string;
|
|
36
|
+
type: string;
|
|
37
|
+
size?: { width: number; height: number };
|
|
38
|
+
layout?: Record<string, unknown>;
|
|
39
|
+
spacing?: Record<string, unknown>;
|
|
40
|
+
style?: Record<string, unknown>;
|
|
41
|
+
text?: string[];
|
|
42
|
+
visibleText?: string[];
|
|
43
|
+
component?: Record<string, unknown>;
|
|
44
|
+
roleGuess?: string;
|
|
45
|
+
children?: FigmaNodeSummary[];
|
|
46
|
+
metadata?: FigmaSummaryMetadata;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FigmaTextExtractionResult {
|
|
50
|
+
node: string;
|
|
51
|
+
nodeId?: string;
|
|
52
|
+
texts: string[];
|
|
53
|
+
metadata: FigmaSummaryMetadata;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface FigmaImplementationContext {
|
|
57
|
+
purpose: string;
|
|
58
|
+
node: FigmaNodeSummary;
|
|
59
|
+
sections: Array<Record<string, unknown>>;
|
|
60
|
+
fields: Array<Record<string, unknown>>;
|
|
61
|
+
buttons: Array<Record<string, unknown>>;
|
|
62
|
+
layoutMeasurements: Record<string, unknown>;
|
|
63
|
+
typography: Array<Record<string, unknown>>;
|
|
64
|
+
colors: Array<Record<string, unknown>>;
|
|
65
|
+
spacing: Array<Record<string, unknown>>;
|
|
66
|
+
cssLayout?: Record<string, unknown>;
|
|
67
|
+
responsive?: Array<Record<string, unknown>>;
|
|
68
|
+
accessibility?: Array<Record<string, unknown>>;
|
|
69
|
+
designTokens?: Record<string, unknown>;
|
|
70
|
+
frameworkHints?: Record<string, unknown>;
|
|
71
|
+
componentHierarchy: FigmaNodeSummary[];
|
|
72
|
+
assets?: FigmaRenderedAsset[];
|
|
73
|
+
metadata: FigmaSummaryMetadata;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface FigmaImplementationContextOptions extends FigmaSummarizerOptions {
|
|
77
|
+
assets?: FigmaRenderedAsset[];
|
|
78
|
+
framework?: FigmaFramework;
|
|
79
|
+
styling?: FigmaStyling;
|
|
80
|
+
resolveTokens?: boolean;
|
|
81
|
+
includeCodeSnippets?: boolean;
|
|
82
|
+
tokenMap?: FigmaTokenMap;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface SummaryState {
|
|
86
|
+
visibleText: string[];
|
|
87
|
+
truncatedReasons: string[];
|
|
88
|
+
options: Required<Pick<FigmaSummarizerOptions, "depth" | "includeHidden" | "includeVectors" | "includeComponentInternals" | "maxVisibleText" | "maxChildren">>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const DEFAULT_DEPTH = 2;
|
|
92
|
+
const MAX_DEPTH = 4;
|
|
93
|
+
const DEFAULT_MAX_VISIBLE_TEXT = 200;
|
|
94
|
+
const DEFAULT_MAX_CHILDREN = 100;
|
|
95
|
+
const VECTOR_TYPES = new Set(["VECTOR", "BOOLEAN_OPERATION", "STAR", "LINE", "ELLIPSE", "POLYGON", "REGULAR_POLYGON"]);
|
|
96
|
+
|
|
97
|
+
export function normalizeSummarizerOptions(options: FigmaSummarizerOptions = {}): SummaryState["options"] {
|
|
98
|
+
return {
|
|
99
|
+
depth: clampInteger(options.depth ?? DEFAULT_DEPTH, 1, MAX_DEPTH),
|
|
100
|
+
includeHidden: options.includeHidden ?? false,
|
|
101
|
+
includeVectors: options.includeVectors ?? false,
|
|
102
|
+
includeComponentInternals: options.includeComponentInternals ?? false,
|
|
103
|
+
maxVisibleText: clampInteger(options.maxVisibleText ?? DEFAULT_MAX_VISIBLE_TEXT, 1, DEFAULT_MAX_VISIBLE_TEXT),
|
|
104
|
+
maxChildren: clampInteger(options.maxChildren ?? DEFAULT_MAX_CHILDREN, 1, DEFAULT_MAX_CHILDREN),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function summarizeNode(node: unknown, options: FigmaSummarizerOptions = {}): FigmaNodeSummary {
|
|
109
|
+
const normalized = normalizeSummarizerOptions(options);
|
|
110
|
+
const state: SummaryState = { visibleText: [], truncatedReasons: [], options: normalized };
|
|
111
|
+
const summary = summarizeNodeInternal(node, state, 0, true) ?? emptySummary(node);
|
|
112
|
+
summary.visibleText = state.visibleText;
|
|
113
|
+
summary.metadata = buildMetadata(state, summary);
|
|
114
|
+
return summary;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function extractVisibleText(node: unknown, options: FigmaSummarizerOptions = {}): FigmaTextExtractionResult {
|
|
118
|
+
const normalized = normalizeSummarizerOptions(options);
|
|
119
|
+
const state: SummaryState = { visibleText: [], truncatedReasons: [], options: normalized };
|
|
120
|
+
collectVisibleText(node, state);
|
|
121
|
+
const record = asRecord(node);
|
|
122
|
+
return {
|
|
123
|
+
node: String(record.name ?? "Unknown node"),
|
|
124
|
+
nodeId: stringValue(record.id),
|
|
125
|
+
texts: state.visibleText,
|
|
126
|
+
metadata: buildMetadata(state),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function explainNode(node: unknown, options: FigmaSummarizerOptions & { assets?: FigmaRenderedAsset[] } = {}): string {
|
|
131
|
+
const summary = summarizeNode(node, options);
|
|
132
|
+
const lines: string[] = [];
|
|
133
|
+
const size = summary.size ? ` (${formatNumber(summary.size.width)} × ${formatNumber(summary.size.height)})` : "";
|
|
134
|
+
lines.push(`# ${summary.name}`);
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push(`This node is a **${summary.type}**${size}${summary.roleGuess ? ` that appears to function as a **${summary.roleGuess}**` : ""}.`);
|
|
137
|
+
if (summary.visibleText?.length) {
|
|
138
|
+
lines.push("");
|
|
139
|
+
lines.push("## Visible text");
|
|
140
|
+
for (const text of summary.visibleText.slice(0, 40)) lines.push(`- ${text}`);
|
|
141
|
+
if (summary.visibleText.length > 40) lines.push(`- …${summary.visibleText.length - 40} more text nodes omitted`);
|
|
142
|
+
}
|
|
143
|
+
if (summary.children?.length) {
|
|
144
|
+
lines.push("");
|
|
145
|
+
lines.push("## Sections");
|
|
146
|
+
summary.children.slice(0, 20).forEach((child, index) => {
|
|
147
|
+
const childSize = child.size ? ` — ${formatNumber(child.size.width)} × ${formatNumber(child.size.height)}` : "";
|
|
148
|
+
const childText = child.text?.length ? ` Text: ${child.text.slice(0, 6).join(" / ")}` : "";
|
|
149
|
+
lines.push(`${index + 1}. **${child.name}** (${child.type}${child.roleGuess ? `, ${child.roleGuess}` : ""})${childSize}.${childText}`);
|
|
150
|
+
});
|
|
151
|
+
if (summary.children.length > 20) lines.push(`${summary.children.length - 20} additional sections omitted.`);
|
|
152
|
+
}
|
|
153
|
+
if (summary.layout || summary.spacing) {
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push("## Layout notes");
|
|
156
|
+
if (summary.layout) lines.push(`- Layout: ${compactInline(summary.layout)}`);
|
|
157
|
+
if (summary.spacing) lines.push(`- Spacing/padding: ${compactInline(summary.spacing)}`);
|
|
158
|
+
}
|
|
159
|
+
if (summary.style) {
|
|
160
|
+
lines.push("");
|
|
161
|
+
lines.push("## Visual style");
|
|
162
|
+
lines.push(`- ${compactInline(summary.style)}`);
|
|
163
|
+
}
|
|
164
|
+
if (options.assets?.length) {
|
|
165
|
+
lines.push("");
|
|
166
|
+
lines.push("## Rendered assets");
|
|
167
|
+
for (const asset of options.assets) lines.push(`- ${asset.nodeId}: ${asset.path ?? asset.url ?? "not available"}`);
|
|
168
|
+
}
|
|
169
|
+
if (summary.metadata?.nextSteps.length) {
|
|
170
|
+
lines.push("");
|
|
171
|
+
lines.push("## Suggested next steps");
|
|
172
|
+
for (const step of summary.metadata.nextSteps) lines.push(`- ${step}`);
|
|
173
|
+
}
|
|
174
|
+
return lines.join("\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getImplementationContext(node: unknown, options: FigmaImplementationContextOptions = {}): FigmaImplementationContext {
|
|
178
|
+
const summary = summarizeNode(node, options);
|
|
179
|
+
const typography = collectTypography(node, normalizeSummarizerOptions(options));
|
|
180
|
+
const colors = collectColors(node, normalizeSummarizerOptions(options));
|
|
181
|
+
const spacing = collectSpacing(node, normalizeSummarizerOptions(options));
|
|
182
|
+
const sections = (summary.children ?? []).slice(0, DEFAULT_MAX_CHILDREN).map((child) => ({
|
|
183
|
+
id: child.id,
|
|
184
|
+
name: child.name,
|
|
185
|
+
type: child.type,
|
|
186
|
+
size: child.size,
|
|
187
|
+
layout: child.layout,
|
|
188
|
+
spacing: child.spacing,
|
|
189
|
+
text: child.text ?? [],
|
|
190
|
+
roleGuess: child.roleGuess,
|
|
191
|
+
}));
|
|
192
|
+
const controls = collectControls(summary);
|
|
193
|
+
return {
|
|
194
|
+
purpose: inferPurpose(summary),
|
|
195
|
+
node: summary,
|
|
196
|
+
sections,
|
|
197
|
+
fields: controls.fields,
|
|
198
|
+
buttons: controls.buttons,
|
|
199
|
+
layoutMeasurements: {
|
|
200
|
+
size: summary.size,
|
|
201
|
+
layout: summary.layout,
|
|
202
|
+
spacing: summary.spacing,
|
|
203
|
+
sectionCount: sections.length,
|
|
204
|
+
},
|
|
205
|
+
typography,
|
|
206
|
+
colors,
|
|
207
|
+
spacing,
|
|
208
|
+
cssLayout: buildCssLayoutHints(node),
|
|
209
|
+
responsive: buildResponsiveHints(node),
|
|
210
|
+
accessibility: buildAccessibilityHints(summary),
|
|
211
|
+
designTokens: options.resolveTokens === false ? undefined : buildDesignTokenHints(node, options.tokenMap),
|
|
212
|
+
frameworkHints: buildFrameworkHints(summary, options),
|
|
213
|
+
componentHierarchy: flattenHierarchy(summary).slice(0, DEFAULT_MAX_CHILDREN),
|
|
214
|
+
assets: options.assets?.length ? options.assets : undefined,
|
|
215
|
+
metadata: summary.metadata ?? buildMetadata({ visibleText: [], truncatedReasons: [], options: normalizeSummarizerOptions(options) }),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function summarizeNodeInternal(node: unknown, state: SummaryState, level: number, isRoot = false): FigmaNodeSummary | null {
|
|
220
|
+
const record = asRecord(node);
|
|
221
|
+
if (!state.options.includeHidden && record.visible === false) return null;
|
|
222
|
+
const type = String(record.type ?? "UNKNOWN");
|
|
223
|
+
const isVector = VECTOR_TYPES.has(type);
|
|
224
|
+
if (isVector && !state.options.includeVectors && !isRoot) return null;
|
|
225
|
+
|
|
226
|
+
const name = String(record.name ?? "Unnamed node");
|
|
227
|
+
const text = type === "TEXT" ? normalizeText(record.characters) : undefined;
|
|
228
|
+
if (text) pushVisibleText(state, text);
|
|
229
|
+
|
|
230
|
+
const summary: FigmaNodeSummary = {
|
|
231
|
+
id: stringValue(record.id),
|
|
232
|
+
name,
|
|
233
|
+
type,
|
|
234
|
+
size: extractSize(record),
|
|
235
|
+
layout: extractLayout(record),
|
|
236
|
+
spacing: extractSpacing(record),
|
|
237
|
+
style: extractStyle(record),
|
|
238
|
+
text: text ? [text] : undefined,
|
|
239
|
+
component: extractComponent(record),
|
|
240
|
+
roleGuess: guessRole(record, text),
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const shouldCollapseInstance = type === "INSTANCE" && !state.options.includeComponentInternals && !isRoot;
|
|
244
|
+
const shouldVisitChildren = level < state.options.depth && (!isVector || state.options.includeVectors) && !shouldCollapseInstance;
|
|
245
|
+
const children = getChildren(record);
|
|
246
|
+
|
|
247
|
+
if (shouldCollapseInstance) {
|
|
248
|
+
const before = state.visibleText.length;
|
|
249
|
+
collectVisibleText(record, state);
|
|
250
|
+
const instanceText = state.visibleText.slice(before);
|
|
251
|
+
if (instanceText.length) summary.text = uniqueStrings([...(summary.text ?? []), ...instanceText]).slice(0, 20);
|
|
252
|
+
if (children.length) state.truncatedReasons.push(`Collapsed component instance "${name}" (${children.length} internal child nodes hidden).`);
|
|
253
|
+
} else if (children.length && shouldVisitChildren) {
|
|
254
|
+
const visibleChildren: FigmaNodeSummary[] = [];
|
|
255
|
+
for (const child of children) {
|
|
256
|
+
if (visibleChildren.length >= state.options.maxChildren) {
|
|
257
|
+
state.truncatedReasons.push(`Capped children of "${name}" at ${state.options.maxChildren}.`);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
const childSummary = summarizeNodeInternal(child, state, level + 1);
|
|
261
|
+
if (childSummary) visibleChildren.push(childSummary);
|
|
262
|
+
}
|
|
263
|
+
if (visibleChildren.length) summary.children = visibleChildren;
|
|
264
|
+
} else if (children.length && level >= state.options.depth) {
|
|
265
|
+
state.truncatedReasons.push(`Reached depth limit ${state.options.depth} at "${name}".`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return pruneEmpty(summary);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function collectVisibleText(node: unknown, state: SummaryState): void {
|
|
272
|
+
const record = asRecord(node);
|
|
273
|
+
if (!state.options.includeHidden && record.visible === false) return;
|
|
274
|
+
const type = String(record.type ?? "UNKNOWN");
|
|
275
|
+
if (type === "TEXT") {
|
|
276
|
+
const text = normalizeText(record.characters);
|
|
277
|
+
if (text) pushVisibleText(state, text);
|
|
278
|
+
}
|
|
279
|
+
if (VECTOR_TYPES.has(type) && !state.options.includeVectors) return;
|
|
280
|
+
if (type === "INSTANCE" && !state.options.includeComponentInternals) {
|
|
281
|
+
// Text labels inside component instances are useful, but the structural internals are not.
|
|
282
|
+
for (const child of getChildren(record)) collectVisibleText(child, state);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
for (const child of getChildren(record)) collectVisibleText(child, state);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function pushVisibleText(state: SummaryState, text: string): void {
|
|
289
|
+
if (state.visibleText.length >= state.options.maxVisibleText) {
|
|
290
|
+
if (!state.truncatedReasons.some((reason) => reason.includes("visible text"))) {
|
|
291
|
+
state.truncatedReasons.push(`Capped visible text at ${state.options.maxVisibleText} items.`);
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
state.visibleText.push(text);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function collectTypography(node: unknown, options: SummaryState["options"]): Array<Record<string, unknown>> {
|
|
299
|
+
const out: Array<Record<string, unknown>> = [];
|
|
300
|
+
walk(node, options, (record, path) => {
|
|
301
|
+
if (record.type !== "TEXT") return;
|
|
302
|
+
const style = asRecord(record.style);
|
|
303
|
+
out.push({
|
|
304
|
+
path,
|
|
305
|
+
text: normalizeText(record.characters)?.slice(0, 80),
|
|
306
|
+
fontFamily: style.fontFamily,
|
|
307
|
+
fontPostScriptName: style.fontPostScriptName,
|
|
308
|
+
fontSize: style.fontSize,
|
|
309
|
+
fontWeight: style.fontWeight,
|
|
310
|
+
lineHeightPx: style.lineHeightPx,
|
|
311
|
+
letterSpacing: style.letterSpacing,
|
|
312
|
+
color: firstSolidPaint(record.fills),
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
return dedupeObjects(out, ["fontFamily", "fontSize", "fontWeight", "lineHeightPx", "color"]).slice(0, 50);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function collectColors(node: unknown, options: SummaryState["options"]): Array<Record<string, unknown>> {
|
|
319
|
+
const out: Array<Record<string, unknown>> = [];
|
|
320
|
+
walk(node, options, (record, path) => {
|
|
321
|
+
for (const [source, paints] of [["fills", record.fills], ["strokes", record.strokes]] as const) {
|
|
322
|
+
for (const paint of compactPaints(paints) ?? []) out.push({ path, source, ...paint });
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
return dedupeObjects(out, ["source", "type", "hex", "opacity"]).slice(0, 60);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function collectSpacing(node: unknown, options: SummaryState["options"]): Array<Record<string, unknown>> {
|
|
329
|
+
const out: Array<Record<string, unknown>> = [];
|
|
330
|
+
walk(node, options, (record, path) => {
|
|
331
|
+
const spacing = extractSpacing(record);
|
|
332
|
+
if (spacing) out.push({ path, ...spacing });
|
|
333
|
+
});
|
|
334
|
+
return dedupeObjects(out, ["itemSpacing", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom"]).slice(0, 50);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function collectControls(summary: FigmaNodeSummary): { fields: Array<Record<string, unknown>>; buttons: Array<Record<string, unknown>> } {
|
|
338
|
+
const fields: Array<Record<string, unknown>> = [];
|
|
339
|
+
const buttons: Array<Record<string, unknown>> = [];
|
|
340
|
+
for (const node of flattenHierarchy(summary)) {
|
|
341
|
+
const text = (node.text ?? []).join(" / ");
|
|
342
|
+
const searchable = `${node.name} ${text}`.toLowerCase();
|
|
343
|
+
if (node.roleGuess === "button" || /\b(button|continue|back|cancel|save|submit|done|next|previous|go back)\b/i.test(searchable)) {
|
|
344
|
+
buttons.push({ id: node.id, name: node.name, text: node.text ?? [], size: node.size });
|
|
345
|
+
} else if (/\b(input|field|select|dropdown|checkbox|radio|toggle|year|value|baseline|target)\b/i.test(searchable)) {
|
|
346
|
+
fields.push({ id: node.id, name: node.name, text: node.text ?? [], size: node.size, roleGuess: node.roleGuess });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return { fields: fields.slice(0, 60), buttons: buttons.slice(0, 40) };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function walk(node: unknown, options: SummaryState["options"], visit: (record: Record<string, unknown>, path: string) => void, path = ""): void {
|
|
353
|
+
const record = asRecord(node);
|
|
354
|
+
if (!options.includeHidden && record.visible === false) return;
|
|
355
|
+
const type = String(record.type ?? "UNKNOWN");
|
|
356
|
+
const name = String(record.name ?? "Unnamed node");
|
|
357
|
+
const nextPath = path ? `${path} > ${name}` : name;
|
|
358
|
+
visit(record, nextPath);
|
|
359
|
+
if (VECTOR_TYPES.has(type) && !options.includeVectors) return;
|
|
360
|
+
if (type === "INSTANCE" && !options.includeComponentInternals) {
|
|
361
|
+
for (const child of getChildren(record)) {
|
|
362
|
+
const childRecord = asRecord(child);
|
|
363
|
+
if (childRecord.type === "TEXT") visit(childRecord, `${nextPath} > ${String(childRecord.name ?? "Text")}`);
|
|
364
|
+
}
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
for (const child of getChildren(record)) walk(child, options, visit, nextPath);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function buildMetadata(state: SummaryState, summary?: FigmaNodeSummary): FigmaSummaryMetadata {
|
|
371
|
+
const truncatedReasons = uniqueStrings(state.truncatedReasons);
|
|
372
|
+
const nextSteps = new Set<string>();
|
|
373
|
+
if ((summary?.children?.length ?? 0) >= state.options.maxChildren || truncatedReasons.some((reason) => reason.includes("Capped children"))) {
|
|
374
|
+
nextSteps.add("Inspect a specific child node by ID with figma_get_node_summary.");
|
|
375
|
+
}
|
|
376
|
+
if (truncatedReasons.some((reason) => reason.includes("depth limit")) && state.options.depth < MAX_DEPTH) {
|
|
377
|
+
nextSteps.add(`Call figma_get_node_summary with depth ${state.options.depth + 1} for more hierarchy.`);
|
|
378
|
+
}
|
|
379
|
+
if (truncatedReasons.some((reason) => reason.includes("Collapsed component instance"))) {
|
|
380
|
+
nextSteps.add("Set includeComponentInternals=true for a specific component instance if its internals matter.");
|
|
381
|
+
}
|
|
382
|
+
if (truncatedReasons.some((reason) => reason.includes("visible text"))) {
|
|
383
|
+
nextSteps.add("Use figma_extract_text on a narrower child node to see more text.");
|
|
384
|
+
}
|
|
385
|
+
return { truncated: truncatedReasons.length > 0, truncatedReasons, nextSteps: [...nextSteps] };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function inferPurpose(summary: FigmaNodeSummary): string {
|
|
389
|
+
const text = summary.visibleText?.slice(0, 6).join("; ");
|
|
390
|
+
if (text) return `${summary.name} appears to be a ${summary.type.toLowerCase()} for: ${text}`;
|
|
391
|
+
return `${summary.name} appears to be a ${summary.type.toLowerCase()} design node.`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function flattenHierarchy(summary: FigmaNodeSummary): FigmaNodeSummary[] {
|
|
395
|
+
const out: FigmaNodeSummary[] = [];
|
|
396
|
+
function visit(node: FigmaNodeSummary): void {
|
|
397
|
+
out.push({ ...node, children: undefined, visibleText: undefined, metadata: undefined });
|
|
398
|
+
for (const child of node.children ?? []) visit(child);
|
|
399
|
+
}
|
|
400
|
+
visit(summary);
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function extractSize(record: Record<string, unknown>): { width: number; height: number } | undefined {
|
|
405
|
+
const box = asRecord(record.absoluteBoundingBox) ?? asRecord(record.absoluteRenderBounds);
|
|
406
|
+
const width = numberValue(box.width);
|
|
407
|
+
const height = numberValue(box.height);
|
|
408
|
+
return width !== undefined && height !== undefined ? { width, height } : undefined;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function extractLayout(record: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
412
|
+
return compactObject({
|
|
413
|
+
mode: record.layoutMode,
|
|
414
|
+
primaryAxisAlignItems: record.primaryAxisAlignItems,
|
|
415
|
+
counterAxisAlignItems: record.counterAxisAlignItems,
|
|
416
|
+
layoutWrap: record.layoutWrap,
|
|
417
|
+
layoutAlign: record.layoutAlign,
|
|
418
|
+
layoutGrow: record.layoutGrow,
|
|
419
|
+
layoutSizingHorizontal: record.layoutSizingHorizontal,
|
|
420
|
+
layoutSizingVertical: record.layoutSizingVertical,
|
|
421
|
+
minWidth: record.minWidth,
|
|
422
|
+
maxWidth: record.maxWidth,
|
|
423
|
+
minHeight: record.minHeight,
|
|
424
|
+
maxHeight: record.maxHeight,
|
|
425
|
+
layoutGrids: compactLayoutGrids(record.layoutGrids),
|
|
426
|
+
constraints: record.constraints,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function extractSpacing(record: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
431
|
+
return compactObject({
|
|
432
|
+
itemSpacing: record.itemSpacing,
|
|
433
|
+
counterAxisSpacing: record.counterAxisSpacing,
|
|
434
|
+
paddingLeft: record.paddingLeft,
|
|
435
|
+
paddingRight: record.paddingRight,
|
|
436
|
+
paddingTop: record.paddingTop,
|
|
437
|
+
paddingBottom: record.paddingBottom,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function compactLayoutGrids(value: unknown): Array<Record<string, unknown>> | undefined {
|
|
442
|
+
if (!Array.isArray(value)) return undefined;
|
|
443
|
+
const grids = value
|
|
444
|
+
.slice(0, 4)
|
|
445
|
+
.map((grid) => {
|
|
446
|
+
const record = asRecord(grid);
|
|
447
|
+
return compactObject({ pattern: record.pattern, count: record.count, gutterSize: record.gutterSize, sectionSize: record.sectionSize, alignment: record.alignment });
|
|
448
|
+
})
|
|
449
|
+
.filter((grid): grid is Record<string, unknown> => Boolean(grid));
|
|
450
|
+
return grids.length ? grids : undefined;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function compactBoundVariables(value: unknown): Record<string, unknown> | undefined {
|
|
454
|
+
const out: Record<string, unknown> = {};
|
|
455
|
+
function visit(raw: unknown, path: string): void {
|
|
456
|
+
if (Array.isArray(raw)) {
|
|
457
|
+
raw.forEach((item, index) => visit(item, `${path}[${index}]`));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const record = asRecord(raw);
|
|
461
|
+
if (typeof record.id === "string") {
|
|
462
|
+
out[path || "value"] = record.id;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
for (const [key, child] of Object.entries(record)) visit(child, path ? `${path}.${key}` : key);
|
|
466
|
+
}
|
|
467
|
+
visit(value, "");
|
|
468
|
+
return Object.keys(out).length ? out : undefined;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function extractStyle(record: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
472
|
+
return compactObject({
|
|
473
|
+
fills: compactPaints(record.fills),
|
|
474
|
+
strokes: compactPaints(record.strokes),
|
|
475
|
+
strokeWeight: record.strokeWeight,
|
|
476
|
+
cornerRadius: record.cornerRadius,
|
|
477
|
+
opacity: record.opacity,
|
|
478
|
+
effects: compactEffects(record.effects),
|
|
479
|
+
textStyle: record.type === "TEXT" ? compactTextStyle(record.style) : undefined,
|
|
480
|
+
styleIds: compactObject(asRecord(record.styles)),
|
|
481
|
+
boundVariables: compactBoundVariables(record.boundVariables),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function extractComponent(record: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
486
|
+
return compactObject({
|
|
487
|
+
componentId: record.componentId,
|
|
488
|
+
componentSetId: record.componentSetId,
|
|
489
|
+
componentProperties: compactComponentProperties(record.componentProperties),
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function compactPaints(value: unknown): Array<Record<string, unknown>> | undefined {
|
|
494
|
+
if (!Array.isArray(value)) return undefined;
|
|
495
|
+
const paints = value
|
|
496
|
+
.filter((paint) => asRecord(paint).visible !== false)
|
|
497
|
+
.slice(0, 8)
|
|
498
|
+
.map((paint) => {
|
|
499
|
+
const record = asRecord(paint);
|
|
500
|
+
return compactObject({
|
|
501
|
+
type: record.type,
|
|
502
|
+
hex: record.type === "SOLID" ? colorToHex(asRecord(record.color), numberValue(record.opacity) ?? 1) : undefined,
|
|
503
|
+
rgba: record.type === "SOLID" ? colorToRgba(asRecord(record.color), numberValue(record.opacity) ?? 1) : undefined,
|
|
504
|
+
opacity: record.opacity,
|
|
505
|
+
scaleMode: record.scaleMode,
|
|
506
|
+
imageRef: record.imageRef ? "present" : undefined,
|
|
507
|
+
});
|
|
508
|
+
})
|
|
509
|
+
.filter((paint): paint is Record<string, unknown> => Boolean(paint && Object.keys(paint).length > 0));
|
|
510
|
+
return paints.length ? paints : undefined;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function firstSolidPaint(value: unknown): string | undefined {
|
|
514
|
+
return compactPaints(value)?.find((paint) => paint.type === "SOLID")?.hex as string | undefined;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function compactEffects(value: unknown): Array<Record<string, unknown>> | undefined {
|
|
518
|
+
if (!Array.isArray(value)) return undefined;
|
|
519
|
+
const effects = value
|
|
520
|
+
.filter((effect) => asRecord(effect).visible !== false)
|
|
521
|
+
.slice(0, 6)
|
|
522
|
+
.map((effect) => {
|
|
523
|
+
const record = asRecord(effect);
|
|
524
|
+
return compactObject({
|
|
525
|
+
type: record.type,
|
|
526
|
+
radius: record.radius,
|
|
527
|
+
offset: record.offset,
|
|
528
|
+
color: record.color ? colorToRgba(asRecord(record.color), 1) : undefined,
|
|
529
|
+
});
|
|
530
|
+
})
|
|
531
|
+
.filter((effect): effect is Record<string, unknown> => Boolean(effect && Object.keys(effect).length > 0));
|
|
532
|
+
return effects.length ? effects : undefined;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function compactTextStyle(value: unknown): Record<string, unknown> | undefined {
|
|
536
|
+
const style = asRecord(value);
|
|
537
|
+
return compactObject({
|
|
538
|
+
fontFamily: style.fontFamily,
|
|
539
|
+
fontPostScriptName: style.fontPostScriptName,
|
|
540
|
+
fontSize: style.fontSize,
|
|
541
|
+
fontWeight: style.fontWeight,
|
|
542
|
+
lineHeightPx: style.lineHeightPx,
|
|
543
|
+
letterSpacing: style.letterSpacing,
|
|
544
|
+
textAlignHorizontal: style.textAlignHorizontal,
|
|
545
|
+
textAlignVertical: style.textAlignVertical,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function compactComponentProperties(value: unknown): Record<string, unknown> | undefined {
|
|
550
|
+
const properties = asRecord(value);
|
|
551
|
+
const out: Record<string, unknown> = {};
|
|
552
|
+
for (const [key, raw] of Object.entries(properties).slice(0, 30)) {
|
|
553
|
+
const record = asRecord(raw);
|
|
554
|
+
out[key] = compactObject({ type: record.type, value: record.value, preferredValues: record.preferredValues });
|
|
555
|
+
}
|
|
556
|
+
return Object.keys(out).length ? out : undefined;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function guessRole(record: Record<string, unknown>, text?: string): string | undefined {
|
|
560
|
+
const haystack = `${String(record.name ?? "")} ${text ?? ""}`.toLowerCase();
|
|
561
|
+
if (/modal|dialog/.test(haystack)) return "modal";
|
|
562
|
+
if (/header|title/.test(haystack)) return "modal-header";
|
|
563
|
+
if (/footer|action/.test(haystack)) return "modal-actions";
|
|
564
|
+
if (/button|continue|cancel|save|go back|back|next|submit/.test(haystack)) return "button";
|
|
565
|
+
if (/input|field|select|dropdown|checkbox|radio|toggle/.test(haystack)) return "form-control";
|
|
566
|
+
if (/summary|preview|card/.test(haystack)) return "content-card";
|
|
567
|
+
if (/icon/.test(haystack)) return "icon";
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function emptySummary(node: unknown): FigmaNodeSummary {
|
|
572
|
+
const record = asRecord(node);
|
|
573
|
+
return { id: stringValue(record.id), name: String(record.name ?? "Unknown node"), type: String(record.type ?? "UNKNOWN") };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function pruneEmpty<T extends object>(value: T): T {
|
|
577
|
+
const record = value as Record<string, unknown>;
|
|
578
|
+
for (const key of Object.keys(record)) {
|
|
579
|
+
const current = record[key];
|
|
580
|
+
if (current === undefined || (Array.isArray(current) && current.length === 0) || (isPlainObject(current) && Object.keys(current).length === 0)) {
|
|
581
|
+
delete record[key];
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return value;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function compactObject(value: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
588
|
+
const out: Record<string, unknown> = {};
|
|
589
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
590
|
+
if (raw === undefined || raw === null) continue;
|
|
591
|
+
if (Array.isArray(raw) && raw.length === 0) continue;
|
|
592
|
+
if (isPlainObject(raw) && Object.keys(raw).length === 0) continue;
|
|
593
|
+
out[key] = raw;
|
|
594
|
+
}
|
|
595
|
+
return Object.keys(out).length ? out : undefined;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function getChildren(record: Record<string, unknown>): unknown[] {
|
|
599
|
+
return Array.isArray(record.children) ? record.children : [];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
603
|
+
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
607
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function normalizeText(value: unknown): string | undefined {
|
|
611
|
+
if (typeof value !== "string") return undefined;
|
|
612
|
+
const text = value.replace(/\s+/g, " ").trim();
|
|
613
|
+
return text || undefined;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function numberValue(value: unknown): number | undefined {
|
|
617
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function stringValue(value: unknown): string | undefined {
|
|
621
|
+
return typeof value === "string" ? value : undefined;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function clampInteger(value: number, min: number, max: number): number {
|
|
625
|
+
return Math.max(min, Math.min(max, Math.trunc(value)));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function colorToRgba(color: Record<string, unknown>, opacity: number): string | undefined {
|
|
629
|
+
const r = numberValue(color.r);
|
|
630
|
+
const g = numberValue(color.g);
|
|
631
|
+
const b = numberValue(color.b);
|
|
632
|
+
if (r === undefined || g === undefined || b === undefined) return undefined;
|
|
633
|
+
const a = numberValue(color.a) ?? opacity;
|
|
634
|
+
return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${round(a)})`;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function colorToHex(color: Record<string, unknown>, opacity: number): string | undefined {
|
|
638
|
+
const r = numberValue(color.r);
|
|
639
|
+
const g = numberValue(color.g);
|
|
640
|
+
const b = numberValue(color.b);
|
|
641
|
+
if (r === undefined || g === undefined || b === undefined) return undefined;
|
|
642
|
+
const hex = [r, g, b].map((channel) => Math.round(channel * 255).toString(16).padStart(2, "0")).join("");
|
|
643
|
+
const alpha = numberValue(color.a) ?? opacity;
|
|
644
|
+
return alpha >= 1 ? `#${hex}` : `#${hex}${Math.round(alpha * 255).toString(16).padStart(2, "0")}`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function round(value: number): number {
|
|
648
|
+
return Math.round(value * 100) / 100;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function formatNumber(value: number): string {
|
|
652
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function uniqueStrings(values: string[]): string[] {
|
|
656
|
+
return [...new Set(values)];
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function dedupeObjects(values: Array<Record<string, unknown>>, keys: string[]): Array<Record<string, unknown>> {
|
|
660
|
+
const seen = new Set<string>();
|
|
661
|
+
const out: Array<Record<string, unknown>> = [];
|
|
662
|
+
for (const value of values) {
|
|
663
|
+
const key = keys.map((property) => JSON.stringify(value[property])).join("|");
|
|
664
|
+
if (seen.has(key)) continue;
|
|
665
|
+
seen.add(key);
|
|
666
|
+
out.push(value);
|
|
667
|
+
}
|
|
668
|
+
return out;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function compactInline(value: Record<string, unknown>): string {
|
|
672
|
+
return JSON.stringify(value).replace(/[{}]/g, "").replace(/\"/g, "");
|
|
673
|
+
}
|