@syntax-syllogism/flow-delta 0.5.1 → 0.6.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.
@@ -0,0 +1,704 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/flexipage-cli.ts
4
+ import { mkdirSync, writeFileSync } from "node:fs";
5
+ import { join, resolve } from "node:path";
6
+ import { execFileSync as execFileSync2 } from "node:child_process";
7
+ import { parseArgs } from "node:util";
8
+
9
+ // src/io/read-metadata.ts
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { execFileSync } from "node:child_process";
12
+ function readMetadataFromFile(path) {
13
+ if (!path.endsWith(".xml")) {
14
+ throw new Error(`Expected an XML file, got ${path}`);
15
+ }
16
+ if (!existsSync(path)) {
17
+ throw new Error(`File not found: ${path}`);
18
+ }
19
+ return readFileSync(path, "utf8");
20
+ }
21
+ function readMetadataFromGit(repo, ref, filePath) {
22
+ try {
23
+ return execFileSync("git", ["-C", repo, "show", `${ref}:${filePath}`], {
24
+ encoding: "utf8",
25
+ stdio: ["ignore", "pipe", "pipe"]
26
+ });
27
+ } catch (error) {
28
+ const message = String(error.stderr ?? error.message ?? "");
29
+ if (message.includes("does not exist in") || message.includes("exists on disk, but not in") || message.includes("pathspec") || message.includes("fatal: path")) {
30
+ return null;
31
+ }
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ // src/util/file-name.ts
37
+ function safeFileName(value) {
38
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_");
39
+ }
40
+
41
+ // src/util/is-main-module.ts
42
+ import { realpathSync } from "node:fs";
43
+ import { fileURLToPath } from "node:url";
44
+ function isMainModule(metaUrl, argvPath = process.argv[1]) {
45
+ if (!argvPath) {
46
+ return false;
47
+ }
48
+ try {
49
+ return realpathSync(fileURLToPath(metaUrl)) === realpathSync(argvPath);
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ // src/diff/deep-diff.ts
56
+ function deepDiff(before, after, path = "") {
57
+ if (Object.is(before, after)) {
58
+ return [];
59
+ }
60
+ if (Array.isArray(before) && Array.isArray(after)) {
61
+ const changes = [];
62
+ const shared = Math.min(before.length, after.length);
63
+ for (let index = 0; index < shared; index += 1) {
64
+ changes.push(...deepDiff(before[index], after[index], joinPath(path, `[${index}]`)));
65
+ }
66
+ for (let index = shared; index < before.length; index += 1) {
67
+ changes.push({ path: joinPath(path, `[${index}]`), before: before[index], after: void 0 });
68
+ }
69
+ for (let index = shared; index < after.length; index += 1) {
70
+ changes.push({ path: joinPath(path, `[${index}]`), before: void 0, after: after[index] });
71
+ }
72
+ return changes;
73
+ }
74
+ if (isPlainObject(before) && isPlainObject(after)) {
75
+ const changes = [];
76
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
77
+ for (const key of [...keys].sort()) {
78
+ if (!(key in before)) {
79
+ changes.push({ path: joinPath(path, key), before: void 0, after: after[key] });
80
+ continue;
81
+ }
82
+ if (!(key in after)) {
83
+ changes.push({ path: joinPath(path, key), before: before[key], after: void 0 });
84
+ continue;
85
+ }
86
+ changes.push(...deepDiff(before[key], after[key], joinPath(path, key)));
87
+ }
88
+ return changes;
89
+ }
90
+ return [{ path, before, after }];
91
+ }
92
+ function isPlainObject(value) {
93
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
94
+ }
95
+ function joinPath(base, segment) {
96
+ if (!base) {
97
+ return segment;
98
+ }
99
+ return segment.startsWith("[") ? `${base}${segment}` : `${base}.${segment}`;
100
+ }
101
+
102
+ // src/flexipage/diff-page.ts
103
+ var HEADER_ORDER = ["masterLabel", "template", "sobjectType", "type", "parentFlexiPage", "description"];
104
+ function diffPage(oldModel, newModel) {
105
+ const oldRegions = new Map(oldModel.regions.map((region) => [region.name, region]));
106
+ const newRegions = new Map(newModel.regions.map((region) => [region.name, region]));
107
+ const names = [.../* @__PURE__ */ new Set([...newModel.regions.map((region) => region.name), ...oldModel.regions.map((region) => region.name)])];
108
+ const regions = names.map((name) => diffRegion(name, oldRegions.get(name), newRegions.get(name)));
109
+ const pageChanges = oldModel.header && newModel.header && Object.keys(oldModel.header).length > 0 && Object.keys(newModel.header).length > 0 ? deepDiff(oldModel.header, newModel.header).sort((left, right) => orderOf(left.path) - orderOf(right.path)) : [];
110
+ const items = regions.flatMap((region) => region.items);
111
+ return {
112
+ pageName: selectPageName(oldModel, newModel),
113
+ kind: "flexipage",
114
+ summary: {
115
+ addedComponents: items.filter((item) => item.status === "added").length,
116
+ removedComponents: items.filter((item) => item.status === "deleted").length,
117
+ modifiedComponents: items.filter((item) => item.status === "modified").length,
118
+ unchangedComponents: items.filter((item) => item.status === "unchanged").length,
119
+ addedRegions: regions.filter((region) => region.status === "added").length,
120
+ removedRegions: regions.filter((region) => region.status === "deleted").length,
121
+ modifiedRegions: regions.filter((region) => (region.changes?.length ?? 0) > 0).length,
122
+ changedPageAttributes: pageChanges.length
123
+ },
124
+ ...pageChanges.length ? { pageChanges } : {},
125
+ regions
126
+ };
127
+ }
128
+ function diffRegion(name, oldRegion, newRegion) {
129
+ if (!oldRegion) return { name, type: newRegion.type, mode: newRegion.mode, status: "added", items: newRegion.items.map((item, index) => classifyItem(void 0, item, `${name}:after:${index}`)) };
130
+ if (!newRegion) return { name, type: oldRegion.type, mode: oldRegion.mode, status: "deleted", items: oldRegion.items.map((item, index) => classifyItem(item, void 0, `${name}:before:${index}`)) };
131
+ const items = diffItems(oldRegion.items, newRegion.items, name);
132
+ const regionChanges = deepDiff({ type: oldRegion.type, mode: oldRegion.mode }, { type: newRegion.type, mode: newRegion.mode });
133
+ return { name, type: newRegion.type, mode: newRegion.mode, status: regionChanges.length || items.some((item) => item.status !== "unchanged") ? "modified" : "unchanged", ...regionChanges.length ? { changes: regionChanges } : {}, items };
134
+ }
135
+ function diffItems(oldItems, newItems, regionName) {
136
+ const pairs = lcs(oldItems, newItems);
137
+ const output = [];
138
+ let oldIndex = 0;
139
+ let newIndex = 0;
140
+ for (const [matchOld, matchNew] of pairs) {
141
+ while (oldIndex < matchOld) output.push(classifyItem(oldItems[oldIndex], void 0, `${regionName}:before:${oldIndex++}`));
142
+ while (newIndex < matchNew) output.push(classifyItem(void 0, newItems[newIndex], `${regionName}:after:${newIndex++}`));
143
+ output.push(classifyItem(oldItems[matchOld], newItems[matchNew], `${regionName}:${matchNew}`));
144
+ oldIndex = matchOld + 1;
145
+ newIndex = matchNew + 1;
146
+ }
147
+ while (oldIndex < oldItems.length) output.push(classifyItem(oldItems[oldIndex], void 0, `${regionName}:before:${oldIndex++}`));
148
+ while (newIndex < newItems.length) output.push(classifyItem(void 0, newItems[newIndex], `${regionName}:after:${newIndex++}`));
149
+ return output;
150
+ }
151
+ function classifyItem(before, after, id) {
152
+ const item = after ?? before;
153
+ const base = item.kind === "component" ? { id, kind: item.kind, componentName: item.componentName } : { id, kind: item.kind, fieldItem: item.fieldItem };
154
+ if (!before) return { ...base, status: "added", after };
155
+ if (!after) return { ...base, status: "deleted", before };
156
+ const changes = before.kind === "component" && after.kind === "component" ? deepDiff(before.properties, after.properties) : deepDiff(before, after);
157
+ return changes.length ? { ...base, status: "modified", changes, before, after } : { ...base, status: "unchanged", after };
158
+ }
159
+ function lcs(oldItems, newItems) {
160
+ const table = Array.from({ length: oldItems.length + 1 }, () => Array(newItems.length + 1).fill(0));
161
+ for (let oldIndex2 = oldItems.length - 1; oldIndex2 >= 0; oldIndex2 -= 1) {
162
+ for (let newIndex2 = newItems.length - 1; newIndex2 >= 0; newIndex2 -= 1) {
163
+ table[oldIndex2][newIndex2] = itemKey(oldItems[oldIndex2]) === itemKey(newItems[newIndex2]) ? table[oldIndex2 + 1][newIndex2 + 1] + 1 : Math.max(table[oldIndex2 + 1][newIndex2], table[oldIndex2][newIndex2 + 1]);
164
+ }
165
+ }
166
+ const matches = [];
167
+ let oldIndex = 0;
168
+ let newIndex = 0;
169
+ while (oldIndex < oldItems.length && newIndex < newItems.length) {
170
+ if (itemKey(oldItems[oldIndex]) === itemKey(newItems[newIndex])) {
171
+ matches.push([oldIndex++, newIndex++]);
172
+ } else if (table[oldIndex + 1][newIndex] >= table[oldIndex][newIndex + 1]) {
173
+ oldIndex += 1;
174
+ } else {
175
+ newIndex += 1;
176
+ }
177
+ }
178
+ return matches;
179
+ }
180
+ function itemKey(item) {
181
+ return item.kind === "component" ? `component:${item.componentName}` : `field:${item.fieldItem}`;
182
+ }
183
+ function orderOf(path) {
184
+ const index = HEADER_ORDER.indexOf(path);
185
+ return index >= 0 ? index : HEADER_ORDER.length;
186
+ }
187
+ function selectPageName(oldModel, newModel) {
188
+ return newModel.pageName !== "(unknown)" ? newModel.pageName : oldModel.pageName;
189
+ }
190
+
191
+ // src/flexipage/parse.ts
192
+ import { Parser } from "xml2js";
193
+ import { createHash } from "node:crypto";
194
+ var FACET_GUID = /^Facet-[0-9a-f-]{36}$/i;
195
+ var HEADER_KEYS = ["masterLabel", "type", "sobjectType", "parentFlexiPage", "description"];
196
+ async function parseFlexiPage(xml) {
197
+ const parsed = await new Parser({ explicitArray: true, trim: true, normalize: false }).parseStringPromise(xml);
198
+ const page = parsed.FlexiPage;
199
+ if (!page) {
200
+ throw new Error("Expected a FlexiPage root element");
201
+ }
202
+ const header = {};
203
+ for (const key of HEADER_KEYS) {
204
+ const value = scalar(page[key]);
205
+ if (value !== void 0) header[key] = value;
206
+ }
207
+ const template = objectAt(page.template)?.name;
208
+ if (template !== void 0) header.template = scalar(template);
209
+ const fullName = scalar(page.fullName);
210
+ const regions = arrayAt(page.flexiPageRegions).map(parseRegion);
211
+ return canonicalize({
212
+ pageName: header.masterLabel ?? fullName ?? "(unknown)",
213
+ header,
214
+ regions
215
+ });
216
+ }
217
+ function parseRegion(value) {
218
+ const region = objectValue(value);
219
+ const type = scalar(region.type) === "Facet" ? "Facet" : "Region";
220
+ const mode = scalar(region.mode);
221
+ return {
222
+ name: scalar(region.name) ?? "(unnamed)",
223
+ type,
224
+ ...mode === "Replace" || mode === "Append" || mode === "Prepend" ? { mode } : {},
225
+ items: arrayAt(region.itemInstances).flatMap(parseItemInstance)
226
+ };
227
+ }
228
+ function parseItemInstance(value) {
229
+ const instance = objectValue(value);
230
+ const component = objectAt(instance.componentInstance);
231
+ if (component) {
232
+ const properties = arrayAt(component.componentInstanceProperties).map((entry) => objectValue(entry)).map((entry) => [scalar(entry.name), valueText(entry.value)]).filter((entry) => entry[0] !== void 0 && entry[1] !== void 0).sort(([left], [right]) => left.localeCompare(right));
233
+ return [{
234
+ kind: "component",
235
+ componentName: scalar(component.componentName) ?? "(unnamed component)",
236
+ properties: Object.fromEntries(properties),
237
+ facetRefs: []
238
+ }];
239
+ }
240
+ const field = objectAt(instance.fieldInstance);
241
+ if (field) {
242
+ const attributes = Object.fromEntries(
243
+ Object.entries(field).filter(([key]) => key !== "fieldItem").map(([key, raw]) => [key, valueText(raw)]).filter((entry) => entry[1] !== void 0)
244
+ );
245
+ return [{
246
+ kind: "field",
247
+ fieldItem: scalar(field.fieldItem) ?? "(unnamed field)",
248
+ attributes
249
+ }];
250
+ }
251
+ return [];
252
+ }
253
+ function canonicalize(model) {
254
+ const rawRegions = model.regions;
255
+ const facets = new Map(rawRegions.filter((region) => region.type === "Facet").map((region) => [region.name, region]));
256
+ const references = /* @__PURE__ */ new Map();
257
+ for (const region of rawRegions) {
258
+ region.items.forEach((item) => {
259
+ if (item.kind !== "component") return;
260
+ for (const [property, value] of Object.entries(item.properties)) {
261
+ if (!facets.has(value)) continue;
262
+ const context = `${region.name}:${item.componentName}:${property}`;
263
+ references.set(value, [...references.get(value) ?? [], context]);
264
+ }
265
+ });
266
+ }
267
+ const canonicalNames = /* @__PURE__ */ new Map();
268
+ for (const [name, contexts] of references) {
269
+ if (!FACET_GUID.test(name)) continue;
270
+ const context = [...contexts].sort()[0];
271
+ canonicalNames.set(name, `Facet:${context}`);
272
+ }
273
+ for (const facet of rawRegions.filter((region) => region.type === "Facet" && FACET_GUID.test(region.name))) {
274
+ if (!canonicalNames.has(facet.name)) {
275
+ canonicalNames.set(facet.name, `Facet:orphan:${facetHash(facet)}`);
276
+ }
277
+ }
278
+ const regions = rawRegions.map((region) => ({
279
+ ...region,
280
+ name: canonicalNames.get(region.name) ?? region.name,
281
+ items: region.items.map((item) => {
282
+ if (item.kind !== "component") return item;
283
+ const properties = Object.fromEntries(Object.entries(item.properties).map(([key, value]) => [key, canonicalNames.get(value) ?? value]));
284
+ return {
285
+ ...item,
286
+ properties,
287
+ facetRefs: Object.values(properties).filter((value) => rawRegions.some((candidate) => candidate.type === "Facet" && (canonicalNames.get(candidate.name) ?? candidate.name) === value))
288
+ };
289
+ })
290
+ }));
291
+ return { ...model, regions };
292
+ }
293
+ function facetHash(region) {
294
+ const stable = {
295
+ type: region.type,
296
+ mode: region.mode,
297
+ items: region.items.map((item) => item.kind === "component" ? { kind: item.kind, componentName: item.componentName, properties: item.properties } : { kind: item.kind, fieldItem: item.fieldItem, attributes: item.attributes })
298
+ };
299
+ return createHash("sha1").update(JSON.stringify(stable)).digest("hex").slice(0, 12);
300
+ }
301
+ function arrayAt(value) {
302
+ return Array.isArray(value) ? value : [];
303
+ }
304
+ function objectAt(value) {
305
+ const first = Array.isArray(value) ? value[0] : value;
306
+ return first && typeof first === "object" && !Array.isArray(first) ? first : void 0;
307
+ }
308
+ function objectValue(value) {
309
+ return objectAt(value) ?? {};
310
+ }
311
+ function scalar(value) {
312
+ if (Array.isArray(value)) return scalar(value[0]);
313
+ if (value === void 0 || value === null || typeof value === "object") return void 0;
314
+ return String(value);
315
+ }
316
+ function valueText(value) {
317
+ const direct = scalar(value);
318
+ if (direct !== void 0) return direct;
319
+ const object = objectAt(value);
320
+ if (!object) return void 0;
321
+ const entries = Object.entries(object).sort(([left], [right]) => left.localeCompare(right));
322
+ return entries.map(([key, child]) => `${key}=${valueText(child) ?? ""}`).join(";");
323
+ }
324
+
325
+ // src/render/shell.ts
326
+ var THEME_STORAGE_KEY = "flow-delta-theme";
327
+ var VIEW_STORAGE_KEY = "flow-delta-view";
328
+ function renderShell(options) {
329
+ const title = escapeHtml(options.title);
330
+ const productName = escapeHtml(options.productName ?? "FlowDelta");
331
+ return `<!doctype html>
332
+ <html lang="en">
333
+ <head>
334
+ <meta charset="utf-8" />
335
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
336
+ <title>${title}</title>
337
+ <script>
338
+ (() => {
339
+ const key = ${JSON.stringify(THEME_STORAGE_KEY)};
340
+ const allowed = new Set(["system", "light", "dark"]);
341
+ let theme = "system";
342
+ try { const stored = localStorage.getItem(key); if (allowed.has(stored)) theme = stored; } catch {}
343
+ document.documentElement.dataset.theme = theme;
344
+ })();${options.wireframeHtml === void 0 ? "" : `
345
+ (() => {
346
+ const key = ${JSON.stringify(VIEW_STORAGE_KEY)};
347
+ const allowed = new Set(["outline", "wireframe"]);
348
+ let view = ${JSON.stringify(options.defaultView ?? "outline")};
349
+ try { const stored = localStorage.getItem(key); if (allowed.has(stored)) view = stored; } catch {}
350
+ document.documentElement.dataset.view = view;
351
+ })();`}
352
+ </script>
353
+ <style>
354
+ :root { color-scheme: light; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; --bg:#f5f7fb; --surface:#fff; --surface-muted:#f8fafc; --surface-selected:#eff6ff; --border:#e3e8ef; --focus:#93c5fd; --text:#1f2937; --muted:#64748b; --faint:#94a3b8; --added:#16a34a; --added-fill:#dcfce7; --deleted:#dc2626; --deleted-fill:#fee2e2; --modified:#d97706; --modified-fill:#fef3c7; --unchanged:#94a3b8; --unchanged-fill:#e9edf3; --panel-width:34vw; }
355
+ :root[data-theme="dark"] { color-scheme:dark; --bg:#0f172a; --surface:#111827; --surface-muted:#1f2937; --surface-selected:#1e3a5f; --border:#334155; --focus:#60a5fa; --text:#e5e7eb; --muted:#a5b4c7; --faint:#718096; --added:#4ade80; --added-fill:#123f2a; --deleted:#f87171; --deleted-fill:#4a1d23; --modified:#fbbf24; --modified-fill:#46330d; --unchanged:#64748b; --unchanged-fill:#263241; }
356
+ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { color-scheme:dark; --bg:#0f172a; --surface:#111827; --surface-muted:#1f2937; --surface-selected:#1e3a5f; --border:#334155; --focus:#60a5fa; --text:#e5e7eb; --muted:#a5b4c7; --faint:#718096; --added:#4ade80; --added-fill:#123f2a; --deleted:#f87171; --deleted-fill:#4a1d23; --modified:#fbbf24; --modified-fill:#46330d; --unchanged:#64748b; --unchanged-fill:#263241; } }
357
+ * { box-sizing:border-box; } body { margin:0; height:100vh; display:grid; grid-template-rows:auto 1fr; background:var(--bg); color:var(--text); font-size:14px; line-height:1.45; } header { display:flex; justify-content:space-between; gap:16px; align-items:center; padding:12px 20px; border-bottom:1px solid var(--border); background:var(--surface); } h1 { margin:0 0 2px; font-size:15px; font-weight:650; } .meta { color:var(--muted); font-size:12.5px; } .header-actions { display:flex; flex-direction:column; align-items:flex-end; gap:9px; } .filters { display:flex; gap:7px; flex-wrap:wrap; justify-content:flex-end; } .display-controls { display:flex; align-items:center; justify-content:flex-end; gap:8px; } button { font:inherit; cursor:pointer; } .filters button, .theme-toggle button, .panel-toggle, .panel-reopen { border:1px solid var(--border); background:var(--surface-muted); color:var(--muted); border-radius:999px; padding:5px 10px; font-size:12px; font-weight:600; } .filters button.active, .theme-toggle button[aria-pressed="true"] { background:var(--surface-selected); border-color:var(--focus); color:var(--text); } .theme-toggle { display:inline-flex; gap:2px; padding:2px; border:1px solid var(--border); border-radius:999px; background:var(--surface-muted); } .theme-toggle button { border:0; padding:3px 8px; } main { position:relative; display:grid; grid-template-columns:minmax(0,1fr) clamp(300px,var(--panel-width),70vw); min-height:0; } body.panel-collapsed main { grid-template-columns:minmax(0,1fr) 0; } .canvas { overflow:auto; padding:24px; position:relative; } .outline { max-width:1100px; margin:0 auto; } .outline-region { margin:0 0 13px; border:1px solid var(--border); border-left-width:4px; border-radius:9px; background:var(--surface); overflow:hidden; } .outline-region.added { border-left-color:var(--added); } .outline-region.deleted { border-left-color:var(--deleted); } .outline-region.modified { border-left-color:var(--modified); } .outline-region.unchanged { border-left-color:var(--unchanged); } .region-head { display:flex; align-items:center; gap:8px; padding:10px 13px; background:var(--surface-muted); font-weight:700; } .region-items { padding:7px 10px 10px 25px; } .outline-row { display:flex; align-items:center; gap:8px; margin:4px 0; padding:8px 10px; border:1px solid var(--border); border-radius:7px; background:var(--surface); cursor:pointer; } .outline-row:hover, .outline-row:focus-visible { border-color:var(--focus); outline:none; } .outline-row.added { background:var(--added-fill); border-color:color-mix(in srgb,var(--added) 45%,var(--border)); } .outline-row.deleted { background:var(--deleted-fill); border-color:color-mix(in srgb,var(--deleted) 45%,var(--border)); } .outline-row.modified { background:var(--modified-fill); border-color:color-mix(in srgb,var(--modified) 45%,var(--border)); } .facet-region { margin:8px 0 3px 22px; } .facet-region .region-head { font-size:12px; padding:7px 10px; } .facet-region .region-items { padding-left:14px; } .status-badge { min-width:68px; color:var(--muted); font-size:10px; font-weight:750; text-transform:uppercase; letter-spacing:.04em; } .added .status-badge { color:var(--added); } .deleted .status-badge { color:var(--deleted); } .modified .status-badge { color:var(--modified); } .summary-stat { margin-right:10px; } .summary-stat.added { color:var(--added); } .summary-stat.deleted { color:var(--deleted); } .summary-stat.modified { color:var(--modified); } .banner { margin:0 auto 16px; max-width:1100px; padding:11px 14px; border:1px solid var(--modified); border-radius:8px; background:var(--modified-fill); color:var(--modified); font-weight:750; } .panel { position:relative; min-width:0; border-left:1px solid var(--border); background:var(--surface); padding:20px; overflow:auto; } body.panel-collapsed .panel { padding:0; border-left:0; overflow:hidden; } body.panel-collapsed .panel > *:not(.panel-resizer) { display:none; } .panel-resizer { position:absolute; left:0; top:0; bottom:0; width:9px; transform:translateX(-50%); cursor:col-resize; z-index:2; } .panel-resizer:hover { background:var(--focus); opacity:.5; } .panel-head { display:flex; justify-content:space-between; align-items:flex-start; gap:8px; } .panel h2 { margin:0 0 8px; font-size:15px; } .panel-toggle { border-radius:7px; padding:6px 9px; } .panel-reopen { display:none; position:absolute; top:14px; right:14px; z-index:3; border-radius:7px; } body.panel-collapsed .panel-reopen { display:block; } .panel-badge { display:inline-block; margin:0 0 15px; padding:3px 9px; border-radius:999px; background:var(--unchanged-fill); color:var(--muted); font-size:11px; font-weight:650; } .panel-badge.added { background:var(--added-fill); color:var(--added); } .panel-badge.deleted { background:var(--deleted-fill); color:var(--deleted); } .panel-badge.modified { background:var(--modified-fill); color:var(--modified); } .empty { color:var(--muted); font-size:13px; } .changes { list-style:none; margin:0; padding:0; } .changes li { margin:0 0 10px; } .change-path { display:block; margin-bottom:3px; color:var(--muted); font-size:11px; font-weight:700; } .val { padding:2px 5px; border-radius:4px; font:12px ui-monospace,Consolas,monospace; overflow-wrap:anywhere; } .val.before { color:var(--deleted); background:var(--deleted-fill); text-decoration:line-through; } .val.after { color:var(--added); background:var(--added-fill); } .arrow { color:var(--faint); margin:0 3px; } [hidden] { display:none !important; }
358
+ .view-toggle { display:inline-flex; gap:2px; padding:2px; border:1px solid var(--border); border-radius:999px; background:var(--surface-muted); }
359
+ .view-toggle button { border:1px solid var(--border); background:var(--surface-muted); color:var(--muted); border-radius:999px; padding:3px 8px; font-size:12px; font-weight:600; }
360
+ .view-toggle button.active { background:var(--surface-selected); border-color:var(--focus); color:var(--text); }
361
+ .view-canvas { min-height:100%; }
362
+ /* The head script stamps data-view before first paint, so the active canvas is
363
+ correct on the first frame. Without JS no attribute is set and the outline shows. */
364
+ [data-view-canvas="wireframe"] { display:none; }
365
+ :root[data-view="wireframe"] [data-view-canvas="wireframe"] { display:block; }
366
+ :root[data-view="wireframe"] [data-view-canvas="outline"] { display:none; }
367
+ .wireframe { max-width:1100px; margin:0 auto; }
368
+ .wireframe-caption { margin:0 0 12px; color:var(--muted); font-size:12px; font-weight:650; }
369
+ .wireframe-grid { display:grid; gap:12px; }
370
+ .wireframe-grid-row, .wireframe-stack-row { display:grid; gap:12px; align-items:stretch; }
371
+ .wireframe-cell { min-width:0; border:1px solid var(--border); border-radius:9px; background:var(--surface); overflow:hidden; }
372
+ .wireframe-cell.added { border-color:var(--added); }
373
+ .wireframe-cell.deleted { border-color:var(--deleted); }
374
+ .wireframe-cell.modified { border-color:var(--modified); }
375
+ .wireframe-cell.unchanged { border-color:var(--unchanged); }
376
+ .wireframe-slot-head { display:flex; align-items:center; gap:8px; padding:9px 11px; background:var(--surface-muted); font-weight:700; }
377
+ .wireframe-items { min-height:56px; padding:7px 10px 10px; }
378
+ .wireframe-empty-label { display:block; padding:8px 2px; color:var(--faint); font-style:italic; }
379
+ .wireframe-stack-cell { padding:0; }
380
+ .wireframe-stack { display:grid; gap:12px; height:100%; }
381
+ .wireframe-removed { margin-top:18px; padding:14px; border:1px dashed var(--deleted); border-radius:9px; background:var(--surface-muted); }
382
+ .wireframe-removed h2 { margin:0 0 10px; font-size:14px; color:var(--deleted); }
383
+ .wireframe-removed-region { margin-top:8px; border:1px solid var(--border); border-radius:7px; overflow:hidden; }
384
+ </style>
385
+ </head>
386
+ <body>
387
+ <header><div><h1>${productName} \u2014 ${title}</h1><div class="meta">${options.metaHtml}</div></div><div class="header-actions"><div class="filters" aria-label="View filters"><button class="filter-button active" data-view-mode="all">All</button><button class="filter-button" data-view-mode="after">After</button><button class="filter-button" data-view-mode="before">Before</button><button class="filter-button" data-view-mode="changes">Changes only</button></div><div class="display-controls">${options.wireframeHtml === void 0 ? "" : `<div class="view-toggle" aria-label="Canvas view"><button class="view-button" data-view-mode="outline">Outline</button><button class="view-button" data-view-mode="wireframe">Wireframe</button></div>`}<div class="theme-toggle" aria-label="Theme"><button class="theme-button" data-theme-choice="system">System</button><button class="theme-button" data-theme-choice="light">Light</button><button class="theme-button" data-theme-choice="dark">Dark</button></div></div></div></header>
388
+ <main><section class="canvas">${options.bannerHtml ?? ""}<div class="view-canvas" data-view-canvas="outline">${options.contentHtml}</div>${options.wireframeHtml === void 0 ? "" : `<div class="view-canvas" data-view-canvas="wireframe">${options.wireframeHtml}</div>`}</section><aside class="panel" id="detail-panel"><div class="panel-resizer" id="panel-resizer"></div><div class="panel-head"><h2 id="panel-title">Select a component or field</h2><button class="panel-toggle" id="panel-toggle" type="button" aria-expanded="true" aria-label="Collapse details" title="Collapse details">\u203A</button></div><div class="panel-badge" id="panel-badge" hidden></div><div id="panel-body" class="empty">Click a component or field row to inspect its property deltas.</div></aside><button class="panel-reopen" id="panel-reopen" type="button">Show details</button></main>
389
+ <script>(() => { const viewKey = ${JSON.stringify(VIEW_STORAGE_KEY)}; const viewButtons = [...document.querySelectorAll('.view-button')]; const hasWireframe = viewButtons.length > 0; function applyViewMode(mode) { const selected = hasWireframe && mode === "wireframe" ? "wireframe" : "outline"; document.documentElement.dataset.view = selected; viewButtons.forEach((button) => button.classList.toggle('active', button.dataset.viewMode === selected)); } viewButtons.forEach((button) => button.addEventListener('click', () => { const mode = button.dataset.viewMode; applyViewMode(mode); try { localStorage.setItem(viewKey, mode); } catch {} })); applyViewMode(document.documentElement.dataset.view ?? "outline"); })(); (() => { ${options.clientScript} })();</script>
390
+ </body>
391
+ </html>`;
392
+ }
393
+ function escapeHtml(value) {
394
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
395
+ }
396
+
397
+ // src/flexipage/render-helpers.ts
398
+ function renderItem(item) {
399
+ const label = item.componentName ?? item.fieldItem ?? "Item";
400
+ return `<div class="outline-row ${item.status}" data-status="${item.status}" data-item-id="${escapeHtml2(item.id)}" tabindex="0" role="button"><span class="status-badge">${item.status}</span><span>${escapeHtml2(label)}</span></div>`;
401
+ }
402
+ function itemFacetRefs(item) {
403
+ const value = item.after ?? item.before;
404
+ return value?.kind === "component" ? value.facetRefs : [];
405
+ }
406
+ function escapeHtml2(value) {
407
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
408
+ }
409
+
410
+ // src/flexipage/template-geometry.ts
411
+ var headerAndMain = {
412
+ label: "Header and One Region",
413
+ rows: [[{ slot: "header", widthPercent: 100 }], [{ slot: "main", widthPercent: 100 }]]
414
+ };
415
+ var singleRegion = {
416
+ label: "One Region",
417
+ rows: [[{ slot: "main", widthPercent: 100 }]]
418
+ };
419
+ var headerThreeColumns = {
420
+ label: "Header and Three Regions",
421
+ rows: [
422
+ [{ slot: "header", widthPercent: 100 }],
423
+ [{ slot: "leftsidebar", widthPercent: 33.34 }, { slot: "main", widthPercent: 33.33 }, { slot: "rightsidebar", widthPercent: 33.33 }]
424
+ ]
425
+ };
426
+ var registry = {
427
+ recordHomeTemplateDesktop: { label: "Header and Right Sidebar", rows: [[{ slot: "header", widthPercent: 100 }], [{ slot: "main", widthPercent: 67 }, { slot: "sidebar", widthPercent: 33 }]] },
428
+ recordHomeLeftSidebarTemplateDesktop: { label: "Header and Left Sidebar", rows: [[{ slot: "header", widthPercent: 100 }], [{ slot: "sidebar", widthPercent: 33 }, { slot: "main", widthPercent: 67 }]] },
429
+ recordHomeTwoColEqualHeaderTemplateDesktop: { label: "Header and Two Equal Regions", rows: [[{ slot: "header", widthPercent: 100 }], [{ slot: "leftcol", widthPercent: 50 }, { slot: "rightcol", widthPercent: 50 }]] },
430
+ recordHomeThreeColHeaderTemplateDesktop: headerThreeColumns,
431
+ recordHomeSingleColTemplateDesktop: headerAndMain,
432
+ recordHomeSimpleViewTemplate: headerAndMain,
433
+ recordHomeWithSubheaderTemplateDesktop: { label: "Header, Subheader, Right Sidebar", rows: [[{ slot: "header", widthPercent: 100 }], [{ slot: "subheader", widthPercent: 100 }], [{ slot: "main", widthPercent: 67 }, { slot: "sidebar", widthPercent: 33 }]] },
434
+ recordHomeWithSubheaderLeftSidebarTemplateDesktop: { label: "Header, Subheader, Left Sidebar", rows: [[{ slot: "header", widthPercent: 100 }], [{ slot: "subheader", widthPercent: 100 }], [{ slot: "sidebar", widthPercent: 33 }, { slot: "main", widthPercent: 67 }]] },
435
+ recordHomePinnedHeaderTemplateDesktop: headerThreeColumns,
436
+ recordPrintableViewTemplate: { label: "Printable View", rows: [[{ slot: "header", widthPercent: 100 }], [{ slot: "main", widthPercent: 100 }], [{ slot: "footer", widthPercent: 100 }]] },
437
+ defaultAppHomeTemplate: singleRegion,
438
+ appHomeTemplateTwoColumns: { label: "Two Regions", rows: [[{ slot: "column1", widthPercent: 50 }, { slot: "column2", widthPercent: 50 }]] },
439
+ appHomeTemplateThreeColumns: { label: "Three Regions", rows: [[{ slot: "region1", widthPercent: 33.34 }, { slot: "region2", widthPercent: 33.33 }, { slot: "region3", widthPercent: 33.33 }]] },
440
+ appHomeTemplateTwoColumnsStacked: { label: "Main Region and Right Sidebar", rows: [[{ slot: "region1", widthPercent: 67 }, { widthPercent: 33, stack: [{ slot: "region2" }, { slot: "region3" }] }]] },
441
+ appHomeTemplateHeaderTwoColumns15LeftSidebar: { label: "Header and Left Sidebar with 15% width", rows: [[{ slot: "region1", widthPercent: 100 }], [{ slot: "region2", widthPercent: 15 }, { slot: "region3", widthPercent: 85 }]] },
442
+ appHomeTemplateHeaderTwoColumns: { label: "Header and Two Regions", rows: [[{ slot: "region1", widthPercent: 100 }], [{ slot: "region2", widthPercent: 67 }, { slot: "region3", widthPercent: 33 }]] },
443
+ appHomeTemplateHeaderTwoColumnsEqualWidth: { label: "Header and Two Equal Regions", rows: [[{ slot: "region1", widthPercent: 100 }], [{ slot: "region2", widthPercent: 50 }, { slot: "region3", widthPercent: 50 }]] },
444
+ appHomeTemplateHeaderTwoColumnsLeftSidebar: { label: "Header and Left Sidebar", rows: [[{ slot: "region1", widthPercent: 100 }], [{ slot: "region2", widthPercent: 33 }, { slot: "region3", widthPercent: 67 }]] },
445
+ "home:desktopTemplate": { label: "Standard Home Page", rows: [[{ widthPercent: 67, stack: [{ slot: "top" }, [{ slot: "bottomLeft", widthPercent: 50 }, { slot: "bottomRight", widthPercent: 50 }]] }, { slot: "sidebar", widthPercent: 33 }]] },
446
+ "industries_common:homeTemplateOneRegion": singleRegion
447
+ };
448
+ var baseRegistry = registry;
449
+ var canonicalRegistry = {};
450
+ for (const [template, geometry] of Object.entries(baseRegistry)) {
451
+ const canonical = template.includes(":") ? template : `flexipage:${template}`;
452
+ canonicalRegistry[canonical] = geometry;
453
+ if (canonical.startsWith("flexipage:")) canonicalRegistry[template] = geometry;
454
+ }
455
+ for (const [template, geometry] of Object.entries(canonicalRegistry)) validateTemplateGeometry(geometry, template);
456
+ var TEMPLATE_GEOMETRY = deepFreeze(canonicalRegistry);
457
+ function getTemplateGeometry(template) {
458
+ return template === void 0 ? void 0 : TEMPLATE_GEOMETRY[template];
459
+ }
460
+ function validateTemplateGeometry(geometry, template = "template") {
461
+ if (geometry.rows.length === 0 || geometry.rows.some((row) => row.length === 0 || !validRow(row))) {
462
+ throw new Error(`Invalid geometry for ${template}: each row must contain positive widths totaling 100%`);
463
+ }
464
+ }
465
+ function validRow(row) {
466
+ const total = row.reduce((sum, cell) => sum + cellWidth(cell), 0);
467
+ return row.every((cell) => cellWidth(cell) > 0 && (isStack(cell) ? cell.stack.length > 0 && cell.stack.every((child) => Array.isArray(child) ? validRow(child) : true) : true)) && total > 99.9 && total < 100.1;
468
+ }
469
+ function cellWidth(cell) {
470
+ return cell.widthPercent ?? 100;
471
+ }
472
+ function isStack(cell) {
473
+ return "stack" in cell;
474
+ }
475
+ function deepFreeze(value) {
476
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
477
+ Object.freeze(value);
478
+ for (const child of Object.values(value)) deepFreeze(child);
479
+ }
480
+ return value;
481
+ }
482
+
483
+ // src/flexipage/render-wireframe.ts
484
+ function renderWireframe(diff, geometry) {
485
+ if (!geometry) return void 0;
486
+ validateTemplateGeometry(geometry);
487
+ const slots = geometry.rows.flatMap((row) => row.flatMap(slotsInCell));
488
+ const slotNames = new Set(slots);
489
+ const currentRegions = diff.regions.filter((region) => region.type === "Region" && region.status !== "deleted");
490
+ if (slots.length !== slotNames.size || currentRegions.some((region) => !slotNames.has(region.name))) return void 0;
491
+ const regions = new Map(diff.regions.map((region) => [region.name, region]));
492
+ const referenced = /* @__PURE__ */ new Set();
493
+ for (const region of diff.regions) for (const item of region.items) itemFacetRefs(item).forEach((ref) => referenced.add(ref));
494
+ const renderedFacets = /* @__PURE__ */ new Set();
495
+ const renderRegionItems = (region) => region.items.map((item) => {
496
+ const nested = itemFacetRefs(item).map((ref) => {
497
+ const facet = regions.get(ref);
498
+ if (!facet || renderedFacets.has(facet.name)) return "";
499
+ renderedFacets.add(facet.name);
500
+ return renderFacet(facet);
501
+ }).join("");
502
+ return renderItem(item) + nested;
503
+ }).join("");
504
+ const renderSlot = (slot) => {
505
+ const region = regions.get(slot);
506
+ const status = region?.status ?? "unchanged";
507
+ return `<div class="wireframe-cell${region ? ` ${status}` : " wireframe-empty"}" data-region-status="${status}" data-slot="${escapeHtml2(slot)}"><div class="wireframe-slot-head"><span class="status-badge">${status}</span><span>${escapeHtml2(slot)}</span></div><div class="wireframe-items">${region ? renderRegionItems(region) : '<span class="wireframe-empty-label">Empty slot</span>'}</div></div>`;
508
+ };
509
+ const renderCell = (cell) => {
510
+ if (isStack2(cell)) return `<div class="wireframe-cell wireframe-stack-cell"><div class="wireframe-stack">${cell.stack.map((child) => Array.isArray(child) ? renderNestedRow(child) : renderSlot(child.slot)).join("")}</div></div>`;
511
+ return renderSlot(cell.slot);
512
+ };
513
+ const renderRow = (row, className = "wireframe-grid-row") => `<div class="${className}" style="grid-template-columns:${row.map((cell) => `${cellWidth2(cell)}fr`).join(" ")}">${row.map(renderCell).join("")}</div>`;
514
+ const renderNestedRow = (row) => renderRow(row, "wireframe-stack-row");
515
+ const grid = geometry.rows.map((row) => renderRow(row)).join("");
516
+ const removed = diff.regions.filter((region) => region.type === "Region" && region.status === "deleted" && !slotNames.has(region.name));
517
+ const unplacedFacets = diff.regions.filter((region) => region.type === "Facet" && !referenced.has(region.name));
518
+ const removedMarkup = removed.length === 0 ? "" : `<section class="wireframe-removed"><h2>Removed (not in current template)</h2>${removed.map(renderAppendixRegion).join("")}</section>`;
519
+ const facetMarkup = unplacedFacets.length === 0 ? "" : `<section class="wireframe-unplaced"><h2>Unplaced facet regions</h2>${unplacedFacets.map(renderAppendixRegion).join("")}</section>`;
520
+ return `<div class="wireframe" data-template-label="${escapeHtml2(geometry.label)}"><div class="wireframe-caption">Template geometry: ${escapeHtml2(geometry.label)}</div><div class="wireframe-grid">${grid}</div>${removedMarkup}${facetMarkup}</div>`;
521
+ function renderAppendixRegion(region) {
522
+ return `<div class="wireframe-removed-region" data-region-status="${region.status}"><div class="wireframe-slot-head"><span class="status-badge">${region.status}</span><span>${escapeHtml2(region.name)}</span></div><div class="wireframe-items">${renderRegionItems(region)}</div></div>`;
523
+ }
524
+ }
525
+ function slotsInCell(cell) {
526
+ if (!isStack2(cell)) return [cell.slot];
527
+ return cell.stack.flatMap((child) => Array.isArray(child) ? child.flatMap(slotsInCell) : [child.slot]);
528
+ }
529
+ function cellWidth2(cell) {
530
+ return cell.widthPercent ?? 100;
531
+ }
532
+ function isStack2(cell) {
533
+ return "stack" in cell;
534
+ }
535
+ function renderFacet(region) {
536
+ return `<section class="facet-region" data-region-status="${region.status}"><div class="region-head"><span class="status-badge">${region.status}</span><span>${escapeHtml2(region.name)}</span></div><div class="region-items">${region.items.map(renderItem).join("")}</div></section>`;
537
+ }
538
+
539
+ // src/flexipage/render-outline.ts
540
+ function renderOutline(diff, options = {}) {
541
+ const referenced = /* @__PURE__ */ new Set();
542
+ for (const region of diff.regions) {
543
+ for (const item of region.items) {
544
+ const value = item.after ?? item.before;
545
+ if (value?.kind === "component") value.facetRefs.forEach((ref) => referenced.add(ref));
546
+ }
547
+ }
548
+ const regions = new Map(diff.regions.map((region) => [region.name, region]));
549
+ const rendered = /* @__PURE__ */ new Set();
550
+ const renderRegion = (region, facet = false) => {
551
+ if (rendered.has(region.name)) return "";
552
+ rendered.add(region.name);
553
+ const items = region.items.map((item) => {
554
+ const nested = itemFacetRefs(item).map((ref) => {
555
+ const child = regions.get(ref);
556
+ return child ? renderRegion(child, true) : "";
557
+ }).join("");
558
+ return renderItem(item) + nested;
559
+ }).join("");
560
+ return `<section class="outline-region ${facet ? "facet-region " : ""}${region.status}" data-region-status="${region.status}">
561
+ <div class="region-head"><span class="status-badge">${region.status}</span><span>${escapeHtml2(region.name)}</span></div>
562
+ <div class="region-items">${items}</div>
563
+ </section>`;
564
+ };
565
+ const content = [...diff.regions.filter((region) => !referenced.has(region.name)), ...diff.regions.filter((region) => referenced.has(region.name))].map((region) => renderRegion(region)).join("");
566
+ const summary = diff.summary;
567
+ const metaHtml = `<span class="summary-stat added">+${summary.addedComponents} components</span><span class="summary-stat deleted">\u2212${summary.removedComponents} components</span><span class="summary-stat modified">~${summary.modifiedComponents} components</span><span class="summary-stat">+${summary.addedRegions}/\u2212${summary.removedRegions} regions</span><span class="summary-stat">${summary.changedPageAttributes} page attributes</span>`;
568
+ const templateChange = diff.pageChanges?.find((change) => change.path === "template");
569
+ const bannerHtml = templateChange ? `<div class="banner">Template changed: ${renderInlineValue(templateChange.before)} \u2192 ${renderInlineValue(templateChange.after)}</div>` : "";
570
+ const data = Object.fromEntries(diff.regions.flatMap((region) => region.items.map((item) => [item.id, item])));
571
+ const json = JSON.stringify({ items: data }).replace(/</g, "\\u003c");
572
+ const clientScript = `const DATA = ${json};
573
+ const rows = [...document.querySelectorAll('.outline-row')];
574
+ const regions = [...document.querySelectorAll('[data-region-status]')];
575
+ const filters = [...document.querySelectorAll('.filter-button')];
576
+ function changed(el) { return el.dataset.regionStatus !== 'unchanged' || el.querySelector('.outline-row.added,.outline-row.deleted,.outline-row.modified') !== null; }
577
+ function applyView(mode) {
578
+ filters.forEach((button) => button.classList.toggle('active', button.dataset.viewMode === mode));
579
+ rows.forEach((row) => { const status = row.dataset.status; row.hidden = mode === 'after' ? status === 'deleted' : mode === 'before' ? status === 'added' : mode === 'changes' ? status === 'unchanged' : false; });
580
+ regions.forEach((region) => { const wireframeCell = region.classList.contains('wireframe-cell'); region.hidden = mode === 'after' ? region.dataset.regionStatus === 'deleted' : mode === 'before' ? region.dataset.regionStatus === 'added' : mode === 'changes' ? !wireframeCell && !changed(region) : false; });
581
+ }
582
+ filters.forEach((button) => button.addEventListener('click', () => applyView(button.dataset.viewMode)));
583
+ function text(value) { return value === undefined ? '(missing)' : String(value); }
584
+ function escape(value) { return text(value).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;').replaceAll('"','&quot;'); }
585
+ function renderValue(before, after) { if (before === undefined) return '<span class="val after">' + escape(after) + '</span>'; if (after === undefined) return '<span class="val before">' + escape(before) + '</span>'; return '<span class="val before">' + escape(before) + '</span><span class="arrow">\u2192</span><span class="val after">' + escape(after) + '</span>'; }
586
+ function selectRow(row) { const item = DATA.items[row.dataset.itemId]; if (!item) return; const title = item.componentName || item.fieldItem || 'Item'; const badge = document.getElementById('panel-badge'); document.getElementById('panel-title').textContent = title; badge.hidden = false; badge.textContent = item.status; badge.className = 'panel-badge ' + item.status; const body = document.getElementById('panel-body'); if (!item.changes || item.changes.length === 0) { body.className = 'empty'; body.textContent = item.status === 'unchanged' ? 'No property changes.' : 'No property details available.'; return; } body.className = ''; body.innerHTML = '<ul class="changes">' + item.changes.map((change) => '<li><span class="change-path">' + escape(change.path) + '</span>' + renderValue(change.before, change.after) + '</li>').join('') + '</ul>'; }
587
+ rows.forEach((row) => { row.addEventListener('click', () => selectRow(row)); row.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); selectRow(row); } }); });
588
+ const themeButtons = [...document.querySelectorAll('.theme-button')];
589
+ function applyTheme(theme) { document.documentElement.dataset.theme = theme; themeButtons.forEach((button) => button.setAttribute('aria-pressed', button.dataset.themeChoice === theme ? 'true' : 'false')); }
590
+ function storedTheme() { try { const value = localStorage.getItem(${JSON.stringify("flow-delta-theme")}); return ['system','light','dark'].includes(value) ? value : 'system'; } catch { return 'system'; } }
591
+ themeButtons.forEach((button) => button.addEventListener('click', () => { const theme = button.dataset.themeChoice; applyTheme(theme); try { localStorage.setItem(${JSON.stringify("flow-delta-theme")}, theme); } catch {} }));
592
+ applyTheme(storedTheme()); applyView('all');
593
+ const panelToggle = document.getElementById('panel-toggle'); const panelReopen = document.getElementById('panel-reopen'); panelToggle.addEventListener('click', () => { document.body.classList.add('panel-collapsed'); panelToggle.setAttribute('aria-expanded','false'); }); panelReopen.addEventListener('click', () => { document.body.classList.remove('panel-collapsed'); panelToggle.setAttribute('aria-expanded','true'); });
594
+ const resizer = document.getElementById('panel-resizer'); let resizeStart; resizer.addEventListener('pointerdown', (event) => { resizeStart = { x:event.clientX, width:document.getElementById('detail-panel').getBoundingClientRect().width }; resizer.setPointerCapture(event.pointerId); }); resizer.addEventListener('pointermove', (event) => { if (!resizeStart) return; document.documentElement.style.setProperty('--panel-width', Math.max(300, resizeStart.width - (event.clientX - resizeStart.x)) + 'px'); }); resizer.addEventListener('pointerup', () => { resizeStart = undefined; });`;
595
+ const wireframe = renderWireframe(diff, getTemplateGeometry(options.template));
596
+ return renderShell({ title: diff.pageName, productName: "FlexiPageDelta", metaHtml, bannerHtml, contentHtml: content, wireframeHtml: wireframe ?? void 0, defaultView: wireframe ? "wireframe" : "outline", clientScript });
597
+ }
598
+ function renderInlineValue(value) {
599
+ return escapeHtml2(value === void 0 ? "(missing)" : String(value));
600
+ }
601
+
602
+ // src/flexipage-cli.ts
603
+ var CLI_USAGE = `Usage:
604
+ flexipage-delta --old <old.flexipage-meta.xml> --new <new.flexipage-meta.xml> [--out dir] [--json]
605
+ flexipage-delta --from <ref> --to <ref> --repo <path> [--path <glob>] [--changed-only] [--out dir] [--json]`;
606
+ async function main(argv = process.argv.slice(2)) {
607
+ const parsed = parseArgs({
608
+ args: argv,
609
+ options: {
610
+ old: { type: "string" },
611
+ new: { type: "string" },
612
+ from: { type: "string" },
613
+ to: { type: "string" },
614
+ repo: { type: "string" },
615
+ path: { type: "string" },
616
+ out: { type: "string" },
617
+ json: { type: "boolean" },
618
+ "changed-only": { type: "boolean" },
619
+ help: { type: "boolean", short: "h" }
620
+ },
621
+ allowPositionals: false
622
+ });
623
+ const options = parsed.values;
624
+ if (options.help) {
625
+ console.log(CLI_USAGE);
626
+ return;
627
+ }
628
+ const outDir = resolve(options.out ?? "./flexipage-delta-out");
629
+ mkdirSync(outDir, { recursive: true });
630
+ try {
631
+ if (options.old || options.new) {
632
+ if (!options.old || !options.new) throw new Error("File mode requires both --old and --new");
633
+ await runFileMode(options.old, options.new, outDir, Boolean(options.json));
634
+ return;
635
+ }
636
+ if (options.from || options.to || options.repo || options.path) {
637
+ if (!options.from || !options.to || !options.repo) throw new Error("Git mode requires --from, --to, and --repo");
638
+ await runGitMode(options.repo, options.from, options.to, options.path ?? "force-app/**/*.flexipage-meta.xml", outDir, Boolean(options.json), Boolean(options["changed-only"]));
639
+ return;
640
+ }
641
+ throw new Error("Specify either --old/--new or --from/--to/--repo");
642
+ } catch (error) {
643
+ console.error(error.message);
644
+ process.exitCode = 1;
645
+ }
646
+ }
647
+ async function runFileMode(oldPath, newPath, outDir, writeJson) {
648
+ await writeArtifacts(await parseFlexiPage(readMetadataFromFile(oldPath)), await parseFlexiPage(readMetadataFromFile(newPath)), outDir, writeJson);
649
+ }
650
+ async function runGitMode(repo, from, to, pattern, outDir, writeJson, changedOnly) {
651
+ let failed = false;
652
+ for (const filePath of discoverGitFiles(repo, from, to, pattern, changedOnly)) {
653
+ try {
654
+ const oldXml = readMetadataFromGit(repo, from, filePath);
655
+ const newXml = readMetadataFromGit(repo, to, filePath);
656
+ await writeArtifacts(oldXml ? await parseFlexiPage(oldXml) : emptyPage(), newXml ? await parseFlexiPage(newXml) : emptyPage(), outDir, writeJson, filePath);
657
+ } catch (error) {
658
+ failed = true;
659
+ console.error(`${filePath}: ${error.message}`);
660
+ }
661
+ }
662
+ if (failed) process.exitCode = 1;
663
+ }
664
+ function emptyPage() {
665
+ return { pageName: "(unknown)", header: {}, regions: [] };
666
+ }
667
+ async function writeArtifacts(oldModel, newModel, outDir, writeJson, sourcePath) {
668
+ const diff = diffPage(oldModel, newModel);
669
+ const stem = safeFileName(diff.pageName === "(unknown)" ? sourcePath ?? "flexipage" : diff.pageName);
670
+ writeFileSync(join(outDir, `${stem}.html`), renderOutline(diff, { template: newModel.header.template }), "utf8");
671
+ if (writeJson) writeFileSync(join(outDir, `${stem}.diff.json`), JSON.stringify(diff, null, 2), "utf8");
672
+ console.log(`${diff.pageName}: components +${diff.summary.addedComponents}/\u2212${diff.summary.removedComponents}/~${diff.summary.modifiedComponents}; regions +${diff.summary.addedRegions}/\u2212${diff.summary.removedRegions}/~${diff.summary.modifiedRegions}; page attributes ${diff.summary.changedPageAttributes}`);
673
+ }
674
+ function discoverGitFiles(repo, from, to, pattern, changedOnly) {
675
+ const files = changedOnly ? gitFiles(repo, ["diff", "--name-only", "--diff-filter=ACMRD", from, to, "--", pattern]) : [.../* @__PURE__ */ new Set([...gitFiles(repo, ["ls-tree", "-r", "--name-only", from]), ...gitFiles(repo, ["ls-tree", "-r", "--name-only", to])])];
676
+ const matcher = globToRegExp(pattern.replaceAll("\\", "/"));
677
+ return files.filter((file) => matcher.test(file)).sort();
678
+ }
679
+ function gitFiles(repo, args) {
680
+ return execFileSync2("git", ["-C", repo, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).split("\n").map((line) => line.trim()).filter(Boolean);
681
+ }
682
+ function globToRegExp(pattern) {
683
+ let source = "^";
684
+ for (let index = 0; index < pattern.length; index += 1) {
685
+ const char = pattern[index];
686
+ if (char === "*") {
687
+ if (pattern[index + 1] === "*") {
688
+ source += ".*";
689
+ index += 1;
690
+ } else source += "[^/]*";
691
+ continue;
692
+ }
693
+ if (char === "?") {
694
+ source += "[^/]";
695
+ continue;
696
+ }
697
+ source += "\\^$+?.()|{}[]".includes(char) ? `\\${char}` : char;
698
+ }
699
+ return new RegExp(`${source}$`);
700
+ }
701
+ if (isMainModule(import.meta.url)) void main();
702
+ export {
703
+ main
704
+ };