@povio/ui 5.0.0-rc.10 → 5.0.0-rc.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,42 +1,19 @@
1
1
  import { t as createLucideIcon } from "./createLucideIcon-LcezOE4z.js";
2
- import { a as getBoxHighlights, d as Pin, f as Moon, l as useUIConfiguration, o as HighlightOverlay, r as inspectElement, s as InspectorDetails, t as UIConfigurationModal, u as Sun } from "./UIConfigurator-CTlq-hrG.js";
2
+ import { a as inspectElement, c as HighlightOverlay, d as emptyConfiguration, f as useUIConfiguration, g as Moon, h as Pin, i as saveThemeCss, l as InspectorDetails, m as Plus, p as Sun, r as getThemeWriter, s as getBoxHighlights, t as UIConfigurationModal } from "./UIConfigurator-CBU0G6rb.js";
3
3
  import { t as ChevronDown } from "./chevron-down-B4Pr8bD3.js";
4
4
  import { t as ChevronRight } from "./chevron-right-3nKJR5Bb.js";
5
5
  import { i as Search } from "./selectDesktop.cva-CHAMxhfO.js";
6
6
  import { t as Upload } from "./upload-CXU83BJ4.js";
7
7
  import { t as X } from "./x-B6vfz8Ps.js";
8
- import { memo, useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
8
+ import { c as $542a13ca2fa5b484$export$5b6b19405a83ff9d, n as $f2ff30fde7b014be$export$3ddf2d174ce01153 } from "./Dialog-CKLkS00H.js";
9
+ import { t as $2ec61d1d4f780267$export$a8a3e93435678ff9 } from "./Heading-nI1_VOaE.js";
10
+ import { t as Select } from "./Select-CgIhoWgk.js";
11
+ import { t as ThemeContext } from "./theme.context-B3wQDJ-i.js";
12
+ import { c as isEditableToken, i as exportCss, v as parseThemeCss } from "./theme-model-DtTsIxou.js";
13
+ import { t as componentArtboards } from "./groups-oQmSeqXn.js";
14
+ import { Suspense, lazy, memo, useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from "react";
9
15
  import { c } from "react/compiler-runtime";
10
16
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
- //#region ../../node_modules/.bun/lucide-react@1.43.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/code-xml.mjs
12
- /**
13
- * @license lucide-react v1.43.0 - ISC
14
- *
15
- * This source code is licensed under the ISC license.
16
- * See the LICENSE file in the root directory of this source tree.
17
- */
18
- const __iconData$9 = {
19
- name: "code-xml",
20
- size: 24,
21
- node: [
22
- ["path", {
23
- d: "m18 16 4-4-4-4",
24
- key: "1inbqp"
25
- }],
26
- ["path", {
27
- d: "m6 8-4 4 4 4",
28
- key: "15zrgr"
29
- }],
30
- ["path", {
31
- d: "m14.5 4-5 16",
32
- key: "e7oirm"
33
- }]
34
- ],
35
- aliases: ["code-2"]
36
- };
37
- __iconData$9.node;
38
- const CodeXml = createLucideIcon(__iconData$9);
39
- //#endregion
40
17
  //#region ../../node_modules/.bun/lucide-react@1.43.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/download.mjs
41
18
  /**
42
19
  * @license lucide-react v1.43.0 - ISC
@@ -44,7 +21,7 @@ const CodeXml = createLucideIcon(__iconData$9);
44
21
  * This source code is licensed under the ISC license.
45
22
  * See the LICENSE file in the root directory of this source tree.
46
23
  */
47
- const __iconData$8 = {
24
+ const __iconData$10 = {
48
25
  name: "download",
49
26
  size: 24,
50
27
  node: [
@@ -62,8 +39,8 @@ const __iconData$8 = {
62
39
  }]
63
40
  ]
64
41
  };
65
- __iconData$8.node;
66
- const Download = createLucideIcon(__iconData$8);
42
+ __iconData$10.node;
43
+ const Download = createLucideIcon(__iconData$10);
67
44
  //#endregion
68
45
  //#region ../../node_modules/.bun/lucide-react@1.43.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/frame.mjs
69
46
  /**
@@ -72,7 +49,7 @@ const Download = createLucideIcon(__iconData$8);
72
49
  * This source code is licensed under the ISC license.
73
50
  * See the LICENSE file in the root directory of this source tree.
74
51
  */
75
- const __iconData$7 = {
52
+ const __iconData$9 = {
76
53
  name: "frame",
77
54
  size: 24,
78
55
  node: [
@@ -106,8 +83,8 @@ const __iconData$7 = {
106
83
  }]
107
84
  ]
108
85
  };
109
- __iconData$7.node;
110
- const Frame = createLucideIcon(__iconData$7);
86
+ __iconData$9.node;
87
+ const Frame = createLucideIcon(__iconData$9);
111
88
  //#endregion
112
89
  //#region ../../node_modules/.bun/lucide-react@1.43.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/hand.mjs
113
90
  /**
@@ -116,7 +93,7 @@ const Frame = createLucideIcon(__iconData$7);
116
93
  * This source code is licensed under the ISC license.
117
94
  * See the LICENSE file in the root directory of this source tree.
118
95
  */
119
- const __iconData$6 = {
96
+ const __iconData$8 = {
120
97
  name: "hand",
121
98
  size: 24,
122
99
  node: [
@@ -138,8 +115,8 @@ const __iconData$6 = {
138
115
  }]
139
116
  ]
140
117
  };
141
- __iconData$6.node;
142
- const Hand = createLucideIcon(__iconData$6);
118
+ __iconData$8.node;
119
+ const Hand = createLucideIcon(__iconData$8);
143
120
  //#endregion
144
121
  //#region ../../node_modules/.bun/lucide-react@1.43.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/monitor.mjs
145
122
  /**
@@ -148,7 +125,7 @@ const Hand = createLucideIcon(__iconData$6);
148
125
  * This source code is licensed under the ISC license.
149
126
  * See the LICENSE file in the root directory of this source tree.
150
127
  */
151
- const __iconData$5 = {
128
+ const __iconData$7 = {
152
129
  name: "monitor",
153
130
  size: 24,
154
131
  node: [
@@ -176,8 +153,8 @@ const __iconData$5 = {
176
153
  }]
177
154
  ]
178
155
  };
179
- __iconData$5.node;
180
- const Monitor = createLucideIcon(__iconData$5);
156
+ __iconData$7.node;
157
+ const Monitor = createLucideIcon(__iconData$7);
181
158
  //#endregion
182
159
  //#region ../../node_modules/.bun/lucide-react@1.43.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/mouse-pointer-2.mjs
183
160
  /**
@@ -186,7 +163,7 @@ const Monitor = createLucideIcon(__iconData$5);
186
163
  * This source code is licensed under the ISC license.
187
164
  * See the LICENSE file in the root directory of this source tree.
188
165
  */
189
- const __iconData$4 = {
166
+ const __iconData$6 = {
190
167
  name: "mouse-pointer-2",
191
168
  size: 24,
192
169
  node: [["path", {
@@ -194,8 +171,8 @@ const __iconData$4 = {
194
171
  key: "edeuup"
195
172
  }]]
196
173
  };
197
- __iconData$4.node;
198
- const MousePointer2 = createLucideIcon(__iconData$4);
174
+ __iconData$6.node;
175
+ const MousePointer2 = createLucideIcon(__iconData$6);
199
176
  //#endregion
200
177
  //#region ../../node_modules/.bun/lucide-react@1.43.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/panel-left.mjs
201
178
  /**
@@ -204,7 +181,7 @@ const MousePointer2 = createLucideIcon(__iconData$4);
204
181
  * This source code is licensed under the ISC license.
205
182
  * See the LICENSE file in the root directory of this source tree.
206
183
  */
207
- const __iconData$3 = {
184
+ const __iconData$5 = {
208
185
  name: "panel-left",
209
186
  size: 24,
210
187
  node: [["rect", {
@@ -220,8 +197,8 @@ const __iconData$3 = {
220
197
  }]],
221
198
  aliases: ["sidebar"]
222
199
  };
223
- __iconData$3.node;
224
- const PanelLeft = createLucideIcon(__iconData$3);
200
+ __iconData$5.node;
201
+ const PanelLeft = createLucideIcon(__iconData$5);
225
202
  //#endregion
226
203
  //#region ../../node_modules/.bun/lucide-react@1.43.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/panels-top-left.mjs
227
204
  /**
@@ -230,7 +207,7 @@ const PanelLeft = createLucideIcon(__iconData$3);
230
207
  * This source code is licensed under the ISC license.
231
208
  * See the LICENSE file in the root directory of this source tree.
232
209
  */
233
- const __iconData$2 = {
210
+ const __iconData$4 = {
234
211
  name: "panels-top-left",
235
212
  size: 24,
236
213
  node: [
@@ -253,8 +230,29 @@ const __iconData$2 = {
253
230
  ],
254
231
  aliases: ["layout"]
255
232
  };
256
- __iconData$2.node;
257
- const PanelsTopLeft = createLucideIcon(__iconData$2);
233
+ __iconData$4.node;
234
+ const PanelsTopLeft = createLucideIcon(__iconData$4);
235
+ //#endregion
236
+ //#region ../../node_modules/.bun/lucide-react@1.43.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/pencil.mjs
237
+ /**
238
+ * @license lucide-react v1.43.0 - ISC
239
+ *
240
+ * This source code is licensed under the ISC license.
241
+ * See the LICENSE file in the root directory of this source tree.
242
+ */
243
+ const __iconData$3 = {
244
+ name: "pencil",
245
+ size: 24,
246
+ node: [["path", {
247
+ d: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",
248
+ key: "1a8usu"
249
+ }], ["path", {
250
+ d: "m15 5 4 4",
251
+ key: "1mk7zo"
252
+ }]]
253
+ };
254
+ __iconData$3.node;
255
+ const Pencil = createLucideIcon(__iconData$3);
258
256
  //#endregion
259
257
  //#region ../../node_modules/.bun/lucide-react@1.43.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/scan-line.mjs
260
258
  /**
@@ -263,7 +261,7 @@ const PanelsTopLeft = createLucideIcon(__iconData$2);
263
261
  * This source code is licensed under the ISC license.
264
262
  * See the LICENSE file in the root directory of this source tree.
265
263
  */
266
- const __iconData$1 = {
264
+ const __iconData$2 = {
267
265
  name: "scan-line",
268
266
  size: 24,
269
267
  node: [
@@ -289,8 +287,8 @@ const __iconData$1 = {
289
287
  }]
290
288
  ]
291
289
  };
292
- __iconData$1.node;
293
- const ScanLine = createLucideIcon(__iconData$1);
290
+ __iconData$2.node;
291
+ const ScanLine = createLucideIcon(__iconData$2);
294
292
  //#endregion
295
293
  //#region ../../node_modules/.bun/lucide-react@1.43.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/settings-2.mjs
296
294
  /**
@@ -299,7 +297,7 @@ const ScanLine = createLucideIcon(__iconData$1);
299
297
  * This source code is licensed under the ISC license.
300
298
  * See the LICENSE file in the root directory of this source tree.
301
299
  */
302
- const __iconData = {
300
+ const __iconData$1 = {
303
301
  name: "settings-2",
304
302
  size: 24,
305
303
  node: [
@@ -325,8 +323,937 @@ const __iconData = {
325
323
  }]
326
324
  ]
327
325
  };
326
+ __iconData$1.node;
327
+ const Settings2 = createLucideIcon(__iconData$1);
328
+ //#endregion
329
+ //#region ../../node_modules/.bun/lucide-react@1.43.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/trash.mjs
330
+ /**
331
+ * @license lucide-react v1.43.0 - ISC
332
+ *
333
+ * This source code is licensed under the ISC license.
334
+ * See the LICENSE file in the root directory of this source tree.
335
+ */
336
+ const __iconData = {
337
+ name: "trash",
338
+ size: 24,
339
+ node: [
340
+ ["path", {
341
+ d: "M10 11v6",
342
+ key: "nco0om"
343
+ }],
344
+ ["path", {
345
+ d: "M14 11v6",
346
+ key: "outv1u"
347
+ }],
348
+ ["path", {
349
+ d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",
350
+ key: "miytrc"
351
+ }],
352
+ ["path", {
353
+ d: "M3 6h18",
354
+ key: "d0wm0j"
355
+ }],
356
+ ["path", {
357
+ d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",
358
+ key: "e791ji"
359
+ }]
360
+ ],
361
+ aliases: ["trash-2"]
362
+ };
328
363
  __iconData.node;
329
- const Settings2 = createLucideIcon(__iconData);
364
+ const Trash = createLucideIcon(__iconData);
365
+ //#endregion
366
+ //#region src/app-canvas/useWorkspaceConfiguration.ts
367
+ function useWorkspaceConfiguration(workspace, ready, generation, setManifest) {
368
+ const $ = c(15);
369
+ const { currentConfiguration, replaceConfiguration, defaults } = useUIConfiguration();
370
+ const loaded = useRef(null);
371
+ const t0 = workspace.id;
372
+ let t1;
373
+ if ($[0] !== defaults) {
374
+ t1 = JSON.stringify(defaults);
375
+ $[0] = defaults;
376
+ $[1] = t1;
377
+ } else t1 = $[1];
378
+ const key = `${generation}:${t0}:${t1}`;
379
+ let t2;
380
+ if ($[2] !== workspace.configuration) {
381
+ t2 = JSON.stringify(workspace.configuration);
382
+ $[2] = workspace.configuration;
383
+ $[3] = t2;
384
+ } else t2 = $[3];
385
+ const saved = t2;
386
+ let t3;
387
+ let t4;
388
+ if ($[4] !== currentConfiguration || $[5] !== defaults || $[6] !== key || $[7] !== ready || $[8] !== replaceConfiguration || $[9] !== saved || $[10] !== setManifest || $[11] !== workspace.configuration || $[12] !== workspace.id) {
389
+ t3 = () => {
390
+ if (!ready) return;
391
+ if (!loaded.current || loaded.current.key !== key || loaded.current.saved !== saved) {
392
+ const value = workspace.configuration ?? (loaded.current ? emptyConfiguration() : currentConfiguration);
393
+ const switchingWorkspace = !!loaded.current && loaded.current.workspaceId !== workspace.id;
394
+ loaded.current = {
395
+ key,
396
+ workspaceId: workspace.id,
397
+ saved
398
+ };
399
+ replaceConfiguration({
400
+ ...value,
401
+ theme: {
402
+ light: Object.fromEntries(Object.entries(value.theme.light).filter((t5) => {
403
+ const [name, value_0] = t5;
404
+ return value_0 !== defaults.light[name];
405
+ })),
406
+ dark: Object.fromEntries(Object.entries(value.theme.dark).filter((t6) => {
407
+ const [name_0, value_1] = t6;
408
+ return value_1 !== defaults.dark[name_0];
409
+ })),
410
+ theme: Object.fromEntries(Object.entries(value.theme.theme).filter((t7) => {
411
+ const [name_1, value_2] = t7;
412
+ return value_2 !== defaults.theme[name_1];
413
+ }))
414
+ }
415
+ }, switchingWorkspace);
416
+ return;
417
+ }
418
+ const snapshot = {
419
+ ...currentConfiguration,
420
+ theme: {
421
+ light: {
422
+ ...defaults.light,
423
+ ...currentConfiguration.theme.light
424
+ },
425
+ dark: {
426
+ ...defaults.dark,
427
+ ...currentConfiguration.theme.dark
428
+ },
429
+ theme: {
430
+ ...defaults.theme,
431
+ ...currentConfiguration.theme.theme
432
+ }
433
+ }
434
+ };
435
+ const serialized = JSON.stringify(snapshot);
436
+ if (serialized === saved) return;
437
+ loaded.current.saved = serialized;
438
+ setManifest((current) => ({
439
+ ...current,
440
+ workspaces: current.workspaces.map((item) => item.id === workspace.id ? {
441
+ ...item,
442
+ configuration: structuredClone(snapshot)
443
+ } : item)
444
+ }));
445
+ };
446
+ t4 = [
447
+ defaults,
448
+ key,
449
+ saved,
450
+ workspace.id,
451
+ workspace.configuration,
452
+ ready,
453
+ currentConfiguration,
454
+ replaceConfiguration,
455
+ setManifest
456
+ ];
457
+ $[4] = currentConfiguration;
458
+ $[5] = defaults;
459
+ $[6] = key;
460
+ $[7] = ready;
461
+ $[8] = replaceConfiguration;
462
+ $[9] = saved;
463
+ $[10] = setManifest;
464
+ $[11] = workspace.configuration;
465
+ $[12] = workspace.id;
466
+ $[13] = t3;
467
+ $[14] = t4;
468
+ } else {
469
+ t3 = $[13];
470
+ t4 = $[14];
471
+ }
472
+ useLayoutEffect(t3, t4);
473
+ }
474
+ //#endregion
475
+ //#region src/app-canvas/SaveCanvasTheme.tsx
476
+ /** Uses the same development-only writer as the configurator's CSS export. */
477
+ function SaveCanvasTheme() {
478
+ const { active, defaults } = useUIConfiguration();
479
+ const [writer, setWriter] = useState(null);
480
+ const [saving, setSaving] = useState(false);
481
+ const [message, setMessage] = useState("");
482
+ const [savedTheme, setSavedTheme] = useState(defaults);
483
+ useEffect(() => setWriter(getThemeWriter()), []);
484
+ useEffect(() => setSavedTheme(defaults), [defaults]);
485
+ const css = exportCss(active.theme, defaults);
486
+ const exported = parseThemeCss(css);
487
+ const currentTheme = {
488
+ light: {
489
+ ...defaults.light,
490
+ ...exported.light
491
+ },
492
+ dark: {
493
+ ...defaults.dark,
494
+ ...exported.dark
495
+ },
496
+ theme: {
497
+ ...defaults.theme,
498
+ ...exported.theme
499
+ }
500
+ };
501
+ const hasChanges = [
502
+ "light",
503
+ "dark",
504
+ "theme"
505
+ ].some((mode) => {
506
+ return [.../* @__PURE__ */ new Set([...Object.keys(currentTheme[mode]), ...Object.keys(savedTheme[mode])])].some((key) => currentTheme[mode][key]?.trim() !== savedTheme[mode][key]?.trim());
507
+ });
508
+ useEffect(() => setMessage(""), [active.theme]);
509
+ if (!writer || !hasChanges && !saving) return null;
510
+ return /* @__PURE__ */ jsxs("div", {
511
+ className: "povio-canvas-save-theme",
512
+ children: [/* @__PURE__ */ jsx("button", {
513
+ disabled: saving,
514
+ title: `Save current CSS settings to ${writer.file}`,
515
+ onClick: async () => {
516
+ setSaving(true);
517
+ setMessage("");
518
+ try {
519
+ await saveThemeCss(writer, css);
520
+ setSavedTheme(currentTheme);
521
+ setMessage(`Saved CSS to ${writer.file}.`);
522
+ } catch (error) {
523
+ setMessage(error instanceof Error ? error.message : "Could not save CSS settings.");
524
+ } finally {
525
+ setSaving(false);
526
+ }
527
+ },
528
+ children: saving ? "Saving theme…" : "Save theme to repo"
529
+ }), message && /* @__PURE__ */ jsx("p", {
530
+ role: "status",
531
+ children: message
532
+ })]
533
+ });
534
+ }
535
+ //#endregion
536
+ //#region src/app-canvas/manifest.ts
537
+ function createCanvasWorkspace(name, id) {
538
+ return {
539
+ id,
540
+ name: name.trim() || "Workspace",
541
+ enabledPages: null,
542
+ positions: {}
543
+ };
544
+ }
545
+ function createCanvasManifest() {
546
+ return {
547
+ version: 2,
548
+ workspaces: [createCanvasWorkspace("All pages", "default")],
549
+ activeWorkspaceId: "default",
550
+ examples: {},
551
+ breakpoint: "desktop"
552
+ };
553
+ }
554
+ function isRecord(value) {
555
+ return value !== null && typeof value === "object" && !Array.isArray(value);
556
+ }
557
+ function isJSON(value, ancestors = /* @__PURE__ */ new Set(), depth = 0) {
558
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
559
+ if (typeof value === "number") return Number.isFinite(value);
560
+ if (typeof value !== "object" || depth > 30 || ancestors.has(value)) return false;
561
+ ancestors.add(value);
562
+ const valid = Object.values(value).every((child) => isJSON(child, ancestors, depth + 1));
563
+ ancestors.delete(value);
564
+ return valid;
565
+ }
566
+ function safeJSON(value, ancestors = /* @__PURE__ */ new Set(), depth = 0) {
567
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
568
+ if (typeof value === "number") return Number.isFinite(value);
569
+ if (typeof value !== "object" || depth > 30 || ancestors.has(value)) return false;
570
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return false;
571
+ ancestors.add(value);
572
+ const valid = Object.entries(value).every(([key, child]) => ![
573
+ "__proto__",
574
+ "prototype",
575
+ "constructor"
576
+ ].includes(key) && safeJSON(child, ancestors, depth + 1));
577
+ ancestors.delete(value);
578
+ return valid;
579
+ }
580
+ const stringMap = (value) => isRecord(value) && Object.values(value).every((item) => typeof item === "string");
581
+ const variantValue = (value) => value === null || typeof value === "string" || typeof value === "boolean";
582
+ function validCvaEdits(value) {
583
+ return isRecord(value) && Object.entries(value).every(([slot, definition]) => {
584
+ if (!/^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*$/.test(slot) || !isRecord(definition) || !Array.isArray(definition.base) || !definition.base.every((item) => typeof item === "string") || !isRecord(definition.config)) return false;
585
+ const config = definition.config;
586
+ return isRecord(config.variants) && Object.values(config.variants).every(stringMap) && isRecord(config.defaultVariants) && Object.values(config.defaultVariants).every(variantValue) && Array.isArray(config.compoundVariants) && config.compoundVariants.every((compound) => isRecord(compound) && Object.values(compound).every((item) => variantValue(item) || Array.isArray(item) && (item.every((part) => typeof part === "string") || item.every((part) => typeof part === "boolean"))));
587
+ });
588
+ }
589
+ /** Returns an independent JSON configuration, or undefined for invalid/unset overrides. */
590
+ function parseCanvasConfiguration(value) {
591
+ if (!isRecord(value) || !safeJSON(value) || !isRecord(value.theme) || ![
592
+ "light",
593
+ "dark",
594
+ "theme"
595
+ ].every((key) => stringMap(value.theme && value.theme[key])) || !isRecord(value.config) || value.overrides !== void 0 && !validCvaEdits(value.overrides)) return void 0;
596
+ return JSON.parse(JSON.stringify(value));
597
+ }
598
+ function parsePositions(input) {
599
+ return Object.fromEntries(Object.entries(isRecord(input) ? input : {}).flatMap(([breakpoint, entries]) => {
600
+ if (!isRecord(entries)) return [];
601
+ return [[breakpoint, Object.fromEntries(Object.entries(entries).flatMap(([id, point]) => isRecord(point) && typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y) ? [[id, {
602
+ x: point.x,
603
+ y: point.y
604
+ }]] : []))]];
605
+ }));
606
+ }
607
+ /** Sanitize either stored JSON text or an imported value, migrating v1 positions into the default workspace. */
608
+ function parseCanvasManifest(input) {
609
+ const fallback = createCanvasManifest();
610
+ let value = input;
611
+ if (typeof value === "string") try {
612
+ value = JSON.parse(value);
613
+ } catch {
614
+ return fallback;
615
+ }
616
+ if (!isRecord(value) || value.version !== 1 && value.version !== 2) return fallback;
617
+ const examples = Object.fromEntries(Object.entries(isRecord(value.examples) ? value.examples : {}).flatMap(([id, example]) => {
618
+ if (!isRecord(example)) return [];
619
+ const clean = {};
620
+ if (typeof example.title === "string") clean.title = example.title;
621
+ if (isRecord(example.params)) clean.params = Object.fromEntries(Object.entries(example.params).filter((entry) => typeof entry[1] === "string"));
622
+ if (isRecord(example.search)) clean.search = Object.fromEntries(Object.entries(example.search).filter(([, item]) => isJSON(item)));
623
+ return [[id, clean]];
624
+ }));
625
+ const workspaces = [];
626
+ const ids = /* @__PURE__ */ new Set();
627
+ if (value.version === 1) workspaces.push({
628
+ ...createCanvasWorkspace("All pages", "default"),
629
+ positions: parsePositions(value.positions)
630
+ });
631
+ else if (Array.isArray(value.workspaces)) for (const entry of value.workspaces) {
632
+ if (!isRecord(entry) || typeof entry.id !== "string" || !entry.id.trim() || ids.has(entry.id)) continue;
633
+ ids.add(entry.id);
634
+ const configuration = parseCanvasConfiguration(entry.configuration);
635
+ workspaces.push({
636
+ id: entry.id,
637
+ name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : "Workspace",
638
+ enabledPages: Array.isArray(entry.enabledPages) ? [...new Set(entry.enabledPages.filter((id) => typeof id === "string" && id.length > 0))] : null,
639
+ positions: parsePositions(entry.positions),
640
+ ...configuration ? { configuration } : {}
641
+ });
642
+ }
643
+ if (!workspaces.length) workspaces.push(createCanvasWorkspace("All pages", "default"));
644
+ return {
645
+ version: 2,
646
+ workspaces,
647
+ activeWorkspaceId: typeof value.activeWorkspaceId === "string" && workspaces.some((workspace) => workspace.id === value.activeWorkspaceId) ? value.activeWorkspaceId : workspaces[0].id,
648
+ examples,
649
+ breakpoint: typeof value.breakpoint === "string" && value.breakpoint.length > 0 ? value.breakpoint : fallback.breakpoint
650
+ };
651
+ }
652
+ //#endregion
653
+ //#region src/app-canvas/WorkspaceControls.tsx
654
+ function WorkspaceControls(t0) {
655
+ const $ = c(81);
656
+ const { manifest, setManifest, pages, disabled } = t0;
657
+ const createTrigger = useRef(null);
658
+ const pagesTrigger = useRef(null);
659
+ const editTrigger = useRef(null);
660
+ let t1;
661
+ if ($[0] !== manifest.activeWorkspaceId) {
662
+ t1 = (item) => item.id === manifest.activeWorkspaceId;
663
+ $[0] = manifest.activeWorkspaceId;
664
+ $[1] = t1;
665
+ } else t1 = $[1];
666
+ const workspace = manifest.workspaces.find(t1);
667
+ const [action, setAction] = useState(null);
668
+ const [name, setName] = useState("");
669
+ const [draftPages, setDraftPages] = useState(null);
670
+ const [query, setQuery] = useState("");
671
+ let t2;
672
+ if ($[2] !== setAction) {
673
+ t2 = () => {
674
+ setAction(null);
675
+ };
676
+ $[2] = setAction;
677
+ $[3] = t2;
678
+ } else t2 = $[3];
679
+ useEffect(t2, [workspace.id]);
680
+ let t3;
681
+ if ($[4] !== disabled || $[5] !== setAction) {
682
+ t3 = () => {
683
+ if (disabled) setAction(null);
684
+ };
685
+ $[4] = disabled;
686
+ $[5] = setAction;
687
+ $[6] = t3;
688
+ } else t3 = $[6];
689
+ let t4;
690
+ if ($[7] !== disabled) {
691
+ t4 = [disabled];
692
+ $[7] = disabled;
693
+ $[8] = t4;
694
+ } else t4 = $[8];
695
+ useEffect(t3, t4);
696
+ const open = (next) => {
697
+ setName(next === "create" ? "" : workspace.name);
698
+ setDraftPages(next === "create" ? [] : workspace.enabledPages);
699
+ setQuery("");
700
+ setAction(next);
701
+ };
702
+ let t5;
703
+ if ($[9] !== draftPages) {
704
+ t5 = (id) => draftPages === null || draftPages.includes(id);
705
+ $[9] = draftPages;
706
+ $[10] = t5;
707
+ } else t5 = $[10];
708
+ const enabled = t5;
709
+ const title = action === "create" ? "Create workspace" : action === "rename" ? "Edit workspace" : action === "delete" ? "Delete workspace?" : "Manage pages";
710
+ const save = () => {
711
+ if (disabled || !action) return;
712
+ if ((action === "create" || action === "rename") && !name.trim()) return;
713
+ const created = action === "create" ? {
714
+ ...createCanvasWorkspace(name.trim(), crypto.randomUUID()),
715
+ enabledPages: draftPages
716
+ } : null;
717
+ setManifest((current) => {
718
+ if (created) return {
719
+ ...current,
720
+ workspaces: [...current.workspaces, created],
721
+ activeWorkspaceId: created.id
722
+ };
723
+ if (action === "delete") {
724
+ if (current.workspaces.length < 2) return current;
725
+ const remaining = current.workspaces.filter((item_0) => item_0.id !== workspace.id);
726
+ return {
727
+ ...current,
728
+ workspaces: remaining,
729
+ activeWorkspaceId: remaining[0].id
730
+ };
731
+ }
732
+ return {
733
+ ...current,
734
+ workspaces: current.workspaces.map((item_1) => item_1.id === workspace.id ? {
735
+ ...item_1,
736
+ ...action === "rename" ? { name: name.trim() } : { enabledPages: draftPages }
737
+ } : item_1)
738
+ };
739
+ });
740
+ setAction(null);
741
+ };
742
+ let t6;
743
+ if ($[11] !== query) {
744
+ t6 = (page) => `${page.title} ${page.path}`.toLowerCase().includes(query.toLowerCase());
745
+ $[11] = query;
746
+ $[12] = t6;
747
+ } else t6 = $[12];
748
+ const visiblePages = pages.filter(t6);
749
+ const t7 = workspace.id;
750
+ let t8;
751
+ if ($[13] !== manifest.workspaces) {
752
+ t8 = manifest.workspaces.map(_temp);
753
+ $[13] = manifest.workspaces;
754
+ $[14] = t8;
755
+ } else t8 = $[14];
756
+ let t9;
757
+ if ($[15] !== setManifest) {
758
+ t9 = (id_0) => {
759
+ if (id_0 !== null) setManifest((current_0) => ({
760
+ ...current_0,
761
+ activeWorkspaceId: String(id_0)
762
+ }));
763
+ };
764
+ $[15] = setManifest;
765
+ $[16] = t9;
766
+ } else t9 = $[16];
767
+ let t10;
768
+ if ($[17] !== open) {
769
+ t10 = () => open("create");
770
+ $[17] = open;
771
+ $[18] = t10;
772
+ } else t10 = $[18];
773
+ let t11;
774
+ if ($[19] === Symbol.for("react.memo_cache_sentinel")) {
775
+ t11 = /* @__PURE__ */ jsx(Plus, { size: 15 });
776
+ $[19] = t11;
777
+ } else t11 = $[19];
778
+ let t12;
779
+ if ($[20] !== t10) {
780
+ t12 = /* @__PURE__ */ jsx("button", {
781
+ ref: createTrigger,
782
+ "aria-label": "Add workspace",
783
+ title: "Add workspace",
784
+ onClick: t10,
785
+ children: t11
786
+ });
787
+ $[20] = t10;
788
+ $[21] = t12;
789
+ } else t12 = $[21];
790
+ let t13;
791
+ if ($[22] !== open) {
792
+ t13 = () => open("rename");
793
+ $[22] = open;
794
+ $[23] = t13;
795
+ } else t13 = $[23];
796
+ let t14;
797
+ if ($[24] === Symbol.for("react.memo_cache_sentinel")) {
798
+ t14 = /* @__PURE__ */ jsx(Pencil, { size: 14 });
799
+ $[24] = t14;
800
+ } else t14 = $[24];
801
+ let t15;
802
+ if ($[25] !== t13) {
803
+ t15 = /* @__PURE__ */ jsx("button", {
804
+ ref: editTrigger,
805
+ "aria-label": "Rename workspace",
806
+ title: "Edit workspace",
807
+ onClick: t13,
808
+ children: t14
809
+ });
810
+ $[25] = t13;
811
+ $[26] = t15;
812
+ } else t15 = $[26];
813
+ let t16;
814
+ if ($[27] !== open) {
815
+ t16 = () => open("pages");
816
+ $[27] = open;
817
+ $[28] = t16;
818
+ } else t16 = $[28];
819
+ const t17 = pages.filter((page_0) => workspace.enabledPages === null || workspace.enabledPages.includes(page_0.id));
820
+ let t18;
821
+ if ($[29] !== pages.length || $[30] !== t17) {
822
+ t18 = /* @__PURE__ */ jsxs("span", { children: [
823
+ t17.length,
824
+ "/",
825
+ pages.length
826
+ ] });
827
+ $[29] = pages.length;
828
+ $[30] = t17;
829
+ $[31] = t18;
830
+ } else t18 = $[31];
831
+ let t19;
832
+ if ($[32] !== t16 || $[33] !== t18) {
833
+ t19 = /* @__PURE__ */ jsx("div", {
834
+ className: "povio-canvas-workspace-actions",
835
+ children: /* @__PURE__ */ jsxs("button", {
836
+ ref: pagesTrigger,
837
+ onClick: t16,
838
+ children: [
839
+ "Manage pages",
840
+ " ",
841
+ t18
842
+ ]
843
+ })
844
+ });
845
+ $[32] = t16;
846
+ $[33] = t18;
847
+ $[34] = t19;
848
+ } else t19 = $[34];
849
+ const T0 = $542a13ca2fa5b484$export$5b6b19405a83ff9d;
850
+ const t20 = action === "create" ? createTrigger : action === "pages" ? pagesTrigger : editTrigger;
851
+ const t21 = "bottom start";
852
+ const t22 = 6;
853
+ const t23 = "povio-canvas-workspace-popover";
854
+ const t24 = action !== null;
855
+ let t25;
856
+ if ($[35] !== setAction) {
857
+ t25 = (isOpen) => {
858
+ if (!isOpen) setAction(null);
859
+ };
860
+ $[35] = setAction;
861
+ $[36] = t25;
862
+ } else t25 = $[36];
863
+ const T1 = $f2ff30fde7b014be$export$3ddf2d174ce01153;
864
+ const t26 = action === "delete" ? "alertdialog" : "dialog";
865
+ let t27;
866
+ if ($[37] !== save) {
867
+ t27 = (event) => {
868
+ event.preventDefault();
869
+ save();
870
+ };
871
+ $[37] = save;
872
+ $[38] = t27;
873
+ } else t27 = $[38];
874
+ let t28;
875
+ if ($[39] !== title) {
876
+ t28 = /* @__PURE__ */ jsx($2ec61d1d4f780267$export$a8a3e93435678ff9, {
877
+ slot: "title",
878
+ children: title
879
+ });
880
+ $[39] = title;
881
+ $[40] = t28;
882
+ } else t28 = $[40];
883
+ let t29;
884
+ if ($[41] !== setAction) {
885
+ t29 = () => setAction(null);
886
+ $[41] = setAction;
887
+ $[42] = t29;
888
+ } else t29 = $[42];
889
+ let t30;
890
+ if ($[43] === Symbol.for("react.memo_cache_sentinel")) {
891
+ t30 = /* @__PURE__ */ jsx(X, { size: 12 });
892
+ $[43] = t30;
893
+ } else t30 = $[43];
894
+ let t31;
895
+ if ($[44] !== t29) {
896
+ t31 = /* @__PURE__ */ jsx("button", {
897
+ type: "button",
898
+ "aria-label": "Close workspace dialog",
899
+ onClick: t29,
900
+ children: t30
901
+ });
902
+ $[44] = t29;
903
+ $[45] = t31;
904
+ } else t31 = $[45];
905
+ let t32;
906
+ if ($[46] !== t28 || $[47] !== t31) {
907
+ t32 = /* @__PURE__ */ jsxs("header", {
908
+ className: "povio-canvas-workspace-popover-heading",
909
+ children: [t28, t31]
910
+ });
911
+ $[46] = t28;
912
+ $[47] = t31;
913
+ $[48] = t32;
914
+ } else t32 = $[48];
915
+ const t33 = action === "delete" ? /* @__PURE__ */ jsxs("p", { children: [
916
+ "Delete “",
917
+ workspace.name,
918
+ "” and its saved layout and theme?"
919
+ ] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [(action === "create" || action === "rename") && /* @__PURE__ */ jsxs("label", { children: ["Workspace name", /* @__PURE__ */ jsx("input", {
920
+ autoFocus: true,
921
+ value: name,
922
+ maxLength: 80,
923
+ placeholder: "Workspace name",
924
+ onChange: (event_0) => setName(event_0.target.value),
925
+ required: true
926
+ })] }), (action === "create" || action === "pages") && /* @__PURE__ */ jsxs("div", {
927
+ role: "group",
928
+ "aria-label": "Enabled pages",
929
+ className: "povio-canvas-popover-pages",
930
+ children: [
931
+ /* @__PURE__ */ jsxs("div", {
932
+ className: "povio-canvas-popover-page-tools",
933
+ children: [
934
+ /* @__PURE__ */ jsxs("span", { children: [
935
+ pages.filter((page_1) => enabled(page_1.id)).length,
936
+ "/",
937
+ pages.length,
938
+ " pages"
939
+ ] }),
940
+ /* @__PURE__ */ jsx("button", {
941
+ type: "button",
942
+ onClick: () => setDraftPages(null),
943
+ children: "Enable all"
944
+ }),
945
+ /* @__PURE__ */ jsx("button", {
946
+ type: "button",
947
+ onClick: () => setDraftPages([]),
948
+ children: "Disable all"
949
+ })
950
+ ]
951
+ }),
952
+ /* @__PURE__ */ jsxs("label", {
953
+ className: "povio-canvas-popover-search",
954
+ children: [/* @__PURE__ */ jsx(Search, { size: 12 }), /* @__PURE__ */ jsx("input", {
955
+ "aria-label": "Find workspace pages",
956
+ placeholder: "Find pages…",
957
+ value: query,
958
+ onChange: (event_1) => setQuery(event_1.target.value)
959
+ })]
960
+ }),
961
+ /* @__PURE__ */ jsxs("div", {
962
+ className: "povio-canvas-popover-page-list",
963
+ children: [visiblePages.map((page_2) => /* @__PURE__ */ jsxs("label", { children: [/* @__PURE__ */ jsx("input", {
964
+ type: "checkbox",
965
+ checked: enabled(page_2.id),
966
+ onChange: (event_2) => {
967
+ const ids = draftPages ?? pages.map(_temp2);
968
+ setDraftPages(event_2.target.checked ? [.../* @__PURE__ */ new Set([...ids, page_2.id])] : ids.filter((id_1) => id_1 !== page_2.id));
969
+ }
970
+ }), /* @__PURE__ */ jsx("span", { children: page_2.title })] }, page_2.id)), !visiblePages.length && /* @__PURE__ */ jsx("p", { children: "No matching pages." })]
971
+ })
972
+ ]
973
+ })] });
974
+ let t34;
975
+ if ($[49] !== action || $[50] !== manifest.workspaces.length || $[51] !== setAction) {
976
+ t34 = action === "rename" ? /* @__PURE__ */ jsxs("button", {
977
+ type: "button",
978
+ className: "povio-canvas-workspace-delete",
979
+ disabled: manifest.workspaces.length < 2,
980
+ onClick: () => setAction("delete"),
981
+ children: [/* @__PURE__ */ jsx(Trash, { size: 12 }), " Delete workspace"]
982
+ }) : /* @__PURE__ */ jsx("button", {
983
+ type: "button",
984
+ autoFocus: action === "delete",
985
+ onClick: () => action === "delete" ? setAction("rename") : setAction(null),
986
+ children: action === "delete" ? "Back" : "Cancel"
987
+ });
988
+ $[49] = action;
989
+ $[50] = manifest.workspaces.length;
990
+ $[51] = setAction;
991
+ $[52] = t34;
992
+ } else t34 = $[52];
993
+ const t35 = action === "delete" ? "is-danger" : "is-primary";
994
+ let t36;
995
+ if ($[53] !== action || $[54] !== disabled || $[55] !== name) {
996
+ t36 = disabled || (action === "create" || action === "rename") && !name.trim();
997
+ $[53] = action;
998
+ $[54] = disabled;
999
+ $[55] = name;
1000
+ $[56] = t36;
1001
+ } else t36 = $[56];
1002
+ const t37 = action === "create" ? "Create workspace" : action === "rename" ? "Save name" : action === "delete" ? "Delete" : "Save pages";
1003
+ let t38;
1004
+ if ($[57] !== t35 || $[58] !== t36 || $[59] !== t37) {
1005
+ t38 = /* @__PURE__ */ jsx("button", {
1006
+ type: "submit",
1007
+ className: t35,
1008
+ disabled: t36,
1009
+ children: t37
1010
+ });
1011
+ $[57] = t35;
1012
+ $[58] = t36;
1013
+ $[59] = t37;
1014
+ $[60] = t38;
1015
+ } else t38 = $[60];
1016
+ let t39;
1017
+ if ($[61] !== t34 || $[62] !== t38) {
1018
+ t39 = /* @__PURE__ */ jsxs("div", {
1019
+ className: "povio-canvas-workspace-popover-actions",
1020
+ children: [t34, t38]
1021
+ });
1022
+ $[61] = t34;
1023
+ $[62] = t38;
1024
+ $[63] = t39;
1025
+ } else t39 = $[63];
1026
+ let t40;
1027
+ if ($[64] !== t27 || $[65] !== t32 || $[66] !== t33 || $[67] !== t39) {
1028
+ t40 = /* @__PURE__ */ jsxs("form", {
1029
+ onSubmit: t27,
1030
+ children: [
1031
+ t32,
1032
+ t33,
1033
+ t39
1034
+ ]
1035
+ });
1036
+ $[64] = t27;
1037
+ $[65] = t32;
1038
+ $[66] = t33;
1039
+ $[67] = t39;
1040
+ $[68] = t40;
1041
+ } else t40 = $[68];
1042
+ let t41;
1043
+ if ($[69] !== T1 || $[70] !== action || $[71] !== t26 || $[72] !== t40 || $[73] !== title) {
1044
+ t41 = /* @__PURE__ */ jsx(T1, {
1045
+ "aria-label": title,
1046
+ role: t26,
1047
+ children: t40
1048
+ }, action);
1049
+ $[69] = T1;
1050
+ $[70] = action;
1051
+ $[71] = t26;
1052
+ $[72] = t40;
1053
+ $[73] = title;
1054
+ $[74] = t41;
1055
+ } else t41 = $[74];
1056
+ let t42;
1057
+ if ($[75] !== T0 || $[76] !== t20 || $[77] !== t24 || $[78] !== t25 || $[79] !== t41) {
1058
+ t42 = /* @__PURE__ */ jsx(T0, {
1059
+ triggerRef: t20,
1060
+ placement: t21,
1061
+ offset: t22,
1062
+ className: t23,
1063
+ isOpen: t24,
1064
+ onOpenChange: t25,
1065
+ children: t41
1066
+ });
1067
+ $[75] = T0;
1068
+ $[76] = t20;
1069
+ $[77] = t24;
1070
+ $[78] = t25;
1071
+ $[79] = t41;
1072
+ $[80] = t42;
1073
+ } else t42 = $[80];
1074
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("fieldset", {
1075
+ className: "povio-canvas-workspaces",
1076
+ disabled,
1077
+ children: [/* @__PURE__ */ jsxs("div", {
1078
+ className: "povio-canvas-workspace-picker",
1079
+ children: [
1080
+ /* @__PURE__ */ jsx("div", {
1081
+ className: "povio-canvas-workspace-select",
1082
+ children: /* @__PURE__ */ jsx(Select, {
1083
+ label: "Workspace",
1084
+ hideLabel: true,
1085
+ size: "small",
1086
+ variant: "outlined",
1087
+ as: "default",
1088
+ className: "povio-canvas-workspace-input",
1089
+ selectionMode: "single",
1090
+ isClearable: false,
1091
+ isDisabled: disabled,
1092
+ renderStaticInput: false,
1093
+ value: t7,
1094
+ items: t8,
1095
+ onChange: t9
1096
+ })
1097
+ }),
1098
+ t12,
1099
+ t15
1100
+ ]
1101
+ }), t19]
1102
+ }), t42] });
1103
+ }
1104
+ function _temp2(item_3) {
1105
+ return item_3.id;
1106
+ }
1107
+ function _temp(item_2) {
1108
+ return {
1109
+ id: item_2.id,
1110
+ label: item_2.name
1111
+ };
1112
+ }
1113
+ const canvasStorageTokenHeader = "x-povio-canvas-token";
1114
+ //#endregion
1115
+ //#region src/app-canvas/useCanvasStorage.ts
1116
+ function connectionFromDocument() {
1117
+ if (typeof document === "undefined") return null;
1118
+ try {
1119
+ const value = JSON.parse(document.querySelector(`meta[name="povio-ui-canvas-storage"]`)?.content ?? "null");
1120
+ return value && typeof value.endpoint === "string" && value.endpoint.startsWith("/") && !value.endpoint.startsWith("//") && typeof value.token === "string" && typeof value.file === "string" ? value : null;
1121
+ } catch {
1122
+ return null;
1123
+ }
1124
+ }
1125
+ function localManifest(key) {
1126
+ try {
1127
+ return parseCanvasManifest(localStorage.getItem(key));
1128
+ } catch {
1129
+ return createCanvasManifest();
1130
+ }
1131
+ }
1132
+ /** Repo is authoritative when connected; local storage remains an offline draft cache. */
1133
+ function useCanvasStorage(key, enabled = true) {
1134
+ const [connection] = useState(() => enabled ? connectionFromDocument() : null);
1135
+ const [manifest, setManifest] = useState(() => localManifest(key));
1136
+ const [ready, setReady] = useState(!connection);
1137
+ const [status, setStatus] = useState(connection ? "Loading workspace…" : "Saved in this browser");
1138
+ const [error, setError] = useState("");
1139
+ const latest = useRef(manifest);
1140
+ latest.current = manifest;
1141
+ const revision = useRef(null);
1142
+ const saved = useRef("");
1143
+ const busy = useRef(false);
1144
+ const paused = useRef(false);
1145
+ const alive = useRef(true);
1146
+ const loadGeneration = useRef(0);
1147
+ useEffect(() => {
1148
+ alive.current = true;
1149
+ return () => {
1150
+ alive.current = false;
1151
+ };
1152
+ }, []);
1153
+ const load = useCallback(async () => {
1154
+ if (!connection) return;
1155
+ const generation = ++loadGeneration.current;
1156
+ setReady(false);
1157
+ setError("");
1158
+ setStatus("Loading workspace…");
1159
+ paused.current = false;
1160
+ try {
1161
+ const response = await fetch(connection.endpoint, {
1162
+ headers: { [canvasStorageTokenHeader]: connection.token },
1163
+ cache: "no-store"
1164
+ });
1165
+ const result = await response.json();
1166
+ if (!response.ok) throw new Error(result.error ?? "Could not read workspace configuration.");
1167
+ if (!alive.current || generation !== loadGeneration.current) return;
1168
+ revision.current = result.revision;
1169
+ if (result.manifest) {
1170
+ const next = parseCanvasManifest(result.manifest);
1171
+ saved.current = JSON.stringify(next);
1172
+ latest.current = next;
1173
+ setManifest(next);
1174
+ } else saved.current = "";
1175
+ setStatus(`Saved to ${connection.file}`);
1176
+ setReady(true);
1177
+ } catch (cause) {
1178
+ if (!alive.current || generation !== loadGeneration.current) return;
1179
+ paused.current = true;
1180
+ setError(cause instanceof Error ? cause.message : "Could not load repository settings.");
1181
+ setStatus("Repository unavailable");
1182
+ setReady(true);
1183
+ }
1184
+ }, [connection]);
1185
+ useEffect(() => {
1186
+ load();
1187
+ }, [load]);
1188
+ const save = useCallback(async () => {
1189
+ if (!connection || busy.current || paused.current || !ready) return;
1190
+ busy.current = true;
1191
+ try {
1192
+ while (alive.current && !paused.current) {
1193
+ const value = latest.current;
1194
+ const serialized = JSON.stringify(value);
1195
+ if (serialized === saved.current) break;
1196
+ setStatus("Saving to repository…");
1197
+ const response_0 = await fetch(connection.endpoint, {
1198
+ method: "POST",
1199
+ headers: {
1200
+ "Content-Type": "application/json",
1201
+ [canvasStorageTokenHeader]: connection.token
1202
+ },
1203
+ body: JSON.stringify({
1204
+ manifest: value,
1205
+ revision: revision.current
1206
+ })
1207
+ });
1208
+ const result_0 = await response_0.json();
1209
+ if (!response_0.ok) throw new Error(response_0.status === 409 ? "Workspace file changed elsewhere. Export your draft, then reload repository settings." : result_0.error ?? "Could not save workspace settings.");
1210
+ revision.current = result_0.revision;
1211
+ saved.current = serialized;
1212
+ }
1213
+ if (alive.current) {
1214
+ setError("");
1215
+ setStatus(`Saved to ${connection.file}`);
1216
+ }
1217
+ } catch (cause_0) {
1218
+ paused.current = true;
1219
+ if (alive.current) {
1220
+ setError(cause_0 instanceof Error ? cause_0.message : "Could not save workspace settings.");
1221
+ setStatus("Workspace not saved to repository");
1222
+ }
1223
+ } finally {
1224
+ busy.current = false;
1225
+ }
1226
+ }, [connection, ready]);
1227
+ useEffect(() => {
1228
+ if (!ready) return;
1229
+ try {
1230
+ localStorage.setItem(key, JSON.stringify(manifest));
1231
+ } catch {
1232
+ if (!connection) setStatus("Browser storage unavailable — export your layout");
1233
+ }
1234
+ if (!connection || paused.current || JSON.stringify(manifest) === saved.current) return;
1235
+ setStatus("Saving to repository…");
1236
+ const timer = setTimeout(() => {
1237
+ save();
1238
+ }, 350);
1239
+ return () => clearTimeout(timer);
1240
+ }, [
1241
+ manifest,
1242
+ key,
1243
+ connection,
1244
+ ready,
1245
+ save
1246
+ ]);
1247
+ return {
1248
+ manifest,
1249
+ setManifest,
1250
+ ready,
1251
+ status,
1252
+ error,
1253
+ repositoryFile: connection?.file,
1254
+ reload: load
1255
+ };
1256
+ }
330
1257
  //#endregion
331
1258
  //#region src/app-canvas/useFrameStartup.ts
332
1259
  /** Bound concurrent bootstraps, without unloading any admitted page. */
@@ -406,7 +1333,7 @@ function labelOf(element) {
406
1333
  return (element.getAttribute("aria-label") || element.getAttribute("alt") || (!element.children.length ? element.textContent?.trim().replace(/\s+/g, " ") : ""))?.slice(0, 48) || (element.id ? `#${element.id}` : "");
407
1334
  }
408
1335
  /** Only expanded branches are read and rendered; large pages do not create a second full DOM tree. */
409
- function PageLayers({ frame, title, selected, onSelect }) {
1336
+ function PageLayers({ frame, title, selected, onSelect, onHover }) {
410
1337
  const [root, setRoot] = useState(null);
411
1338
  const [, refresh] = useReducer((value) => value + 1, 0);
412
1339
  const expanded = useRef(/* @__PURE__ */ new WeakSet());
@@ -488,7 +1415,13 @@ function PageLayers({ frame, title, selected, onSelect }) {
488
1415
  "aria-expanded": children.length ? open_0 : void 0,
489
1416
  children: [/* @__PURE__ */ jsxs("div", {
490
1417
  className: "povio-canvas-layer-row",
491
- style: { paddingLeft: 8 + depth * 12 },
1418
+ onMouseEnter: () => onHover(element_1),
1419
+ onMouseLeave: () => onHover(null),
1420
+ onFocus: () => onHover(element_1),
1421
+ onBlur: (event) => {
1422
+ if (!event.currentTarget.contains(event.relatedTarget)) onHover(null);
1423
+ },
1424
+ style: { paddingLeft: 4 + depth * 10 },
492
1425
  children: [children.length ? /* @__PURE__ */ jsx("button", {
493
1426
  className: "povio-canvas-layer-toggle",
494
1427
  "aria-label": `${open_0 ? "Collapse" : "Expand"} ${element_1.localName}${label ? ` ${label}` : ""}`,
@@ -498,13 +1431,13 @@ function PageLayers({ frame, title, selected, onSelect }) {
498
1431
  className: "povio-canvas-layer-select",
499
1432
  title: `${element_1.localName}${label ? ` · ${label}` : ""}`,
500
1433
  onClick: () => onSelect(element_1),
501
- onKeyDown: (event) => {
502
- if (event.key === "ArrowRight" && children.length && !open_0) {
503
- event.preventDefault();
1434
+ onKeyDown: (event_0) => {
1435
+ if (event_0.key === "ArrowRight" && children.length && !open_0) {
1436
+ event_0.preventDefault();
504
1437
  toggle(element_1, false);
505
1438
  }
506
- if (event.key === "ArrowLeft" && open_0) {
507
- event.preventDefault();
1439
+ if (event_0.key === "ArrowLeft" && open_0) {
1440
+ event_0.preventDefault();
508
1441
  toggle(element_1, true);
509
1442
  }
510
1443
  },
@@ -521,11 +1454,6 @@ function PageLayers({ frame, title, selected, onSelect }) {
521
1454
  className: "povio-canvas-layers",
522
1455
  "aria-label": `Layers for ${title}`,
523
1456
  children: [
524
- /* @__PURE__ */ jsxs("header", { children: [
525
- /* @__PURE__ */ jsx(CodeXml, { size: 14 }),
526
- /* @__PURE__ */ jsx("strong", { children: "Layers" }),
527
- /* @__PURE__ */ jsx("span", { children: title })
528
- ] }),
529
1457
  /* @__PURE__ */ jsx("div", {
530
1458
  role: "tree",
531
1459
  "aria-label": "HTML layers",
@@ -542,10 +1470,8 @@ function PageLayers({ frame, title, selected, onSelect }) {
542
1470
  }
543
1471
  //#endregion
544
1472
  //#region src/app-canvas/CanvasInspector.tsx
545
- const unknownToken = () => false;
546
- const readOnly = () => {};
547
1473
  /** Inspect the live iframe DOM using its own, unscaled viewport coordinates. */
548
- function useCanvasInspector(enabled, generation) {
1474
+ function useCanvasInspector(enabled, generation, isKnown) {
549
1475
  const [selection, setSelection] = useState(null);
550
1476
  const [measurement, setMeasurement] = useState(null);
551
1477
  const [pinned, setPinned] = useState(false);
@@ -632,7 +1558,7 @@ function useCanvasInspector(enabled, generation) {
632
1558
  return;
633
1559
  }
634
1560
  const rect = element_0.getBoundingClientRect();
635
- const snapshot = inspectElement(element_0, unknownToken);
1561
+ const snapshot = inspectElement(element_0, isKnown);
636
1562
  const details = highlightKey ? getBoxHighlights(element_0, highlightKey) : null;
637
1563
  const spacing = highlightKey ? [] : [
638
1564
  "padding.top",
@@ -726,7 +1652,8 @@ function useCanvasInspector(enabled, generation) {
726
1652
  enabled,
727
1653
  selection,
728
1654
  highlightKey,
729
- clear
1655
+ clear,
1656
+ isKnown
730
1657
  ]);
731
1658
  return {
732
1659
  selection,
@@ -748,8 +1675,8 @@ function useCanvasInspector(enabled, generation) {
748
1675
  };
749
1676
  }
750
1677
  function CanvasInspectorPanel(t0) {
751
- const $ = c(21);
752
- const { inspector, onClose } = t0;
1678
+ const $ = c(22);
1679
+ const { inspector, onClose, onSelectToken } = t0;
753
1680
  let t1;
754
1681
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
755
1682
  t1 = /* @__PURE__ */ jsx("strong", { children: "Inspector view" });
@@ -812,10 +1739,10 @@ function CanvasInspectorPanel(t0) {
812
1739
  $[13] = t9;
813
1740
  } else t9 = $[13];
814
1741
  let t10;
815
- if ($[14] !== inspector.highlight || $[15] !== inspector.measurement) {
1742
+ if ($[14] !== inspector.highlight || $[15] !== inspector.measurement || $[16] !== onSelectToken) {
816
1743
  t10 = inspector.measurement ? /* @__PURE__ */ jsx(InspectorDetails, {
817
1744
  snapshot: inspector.measurement.snapshot,
818
- onSelect: readOnly,
1745
+ onSelect: onSelectToken,
819
1746
  onHighlight: inspector.highlight
820
1747
  }) : /* @__PURE__ */ jsx("p", {
821
1748
  className: "povio-canvas-inspect-hint",
@@ -823,10 +1750,11 @@ function CanvasInspectorPanel(t0) {
823
1750
  });
824
1751
  $[14] = inspector.highlight;
825
1752
  $[15] = inspector.measurement;
826
- $[16] = t10;
827
- } else t10 = $[16];
1753
+ $[16] = onSelectToken;
1754
+ $[17] = t10;
1755
+ } else t10 = $[17];
828
1756
  let t11;
829
- if ($[17] !== t10 || $[18] !== t7 || $[19] !== t9) {
1757
+ if ($[18] !== t10 || $[19] !== t7 || $[20] !== t9) {
830
1758
  t11 = /* @__PURE__ */ jsxs("aside", {
831
1759
  className: "studio-inspector-panel povio-canvas-inspector",
832
1760
  "aria-label": "Inspector view",
@@ -836,11 +1764,11 @@ function CanvasInspectorPanel(t0) {
836
1764
  t10
837
1765
  ]
838
1766
  });
839
- $[17] = t10;
840
- $[18] = t7;
841
- $[19] = t9;
842
- $[20] = t11;
843
- } else t11 = $[20];
1767
+ $[18] = t10;
1768
+ $[19] = t7;
1769
+ $[20] = t9;
1770
+ $[21] = t11;
1771
+ } else t11 = $[21];
844
1772
  return t11;
845
1773
  }
846
1774
  function CanvasInspectionOverlay(t0) {
@@ -892,6 +1820,64 @@ function CanvasInspectionOverlay(t0) {
892
1820
  } else t3 = $[8];
893
1821
  return t3;
894
1822
  }
1823
+ /** Temporary layer hover never replaces the pinned inspector selection. */
1824
+ function CanvasLayerHighlight(t0) {
1825
+ const $ = c(5);
1826
+ const { element } = t0;
1827
+ const [bounds, setBounds] = useState(null);
1828
+ let t1;
1829
+ let t2;
1830
+ if ($[0] !== element) {
1831
+ t1 = () => {
1832
+ const owner = element.ownerDocument.defaultView;
1833
+ const measure = () => {
1834
+ if (!element.isConnected) {
1835
+ setBounds(null);
1836
+ return;
1837
+ }
1838
+ const rect = element.getBoundingClientRect();
1839
+ setBounds({
1840
+ left: rect.left,
1841
+ top: rect.top,
1842
+ width: rect.width,
1843
+ height: rect.height
1844
+ });
1845
+ };
1846
+ measure();
1847
+ const observer = new ResizeObserver(measure);
1848
+ observer.observe(element);
1849
+ owner?.addEventListener("scroll", measure, true);
1850
+ owner?.addEventListener("resize", measure);
1851
+ return () => {
1852
+ observer.disconnect();
1853
+ owner?.removeEventListener("scroll", measure, true);
1854
+ owner?.removeEventListener("resize", measure);
1855
+ };
1856
+ };
1857
+ t2 = [element];
1858
+ $[0] = element;
1859
+ $[1] = t1;
1860
+ $[2] = t2;
1861
+ } else {
1862
+ t1 = $[1];
1863
+ t2 = $[2];
1864
+ }
1865
+ useLayoutEffect(t1, t2);
1866
+ let t3;
1867
+ if ($[3] !== bounds) {
1868
+ t3 = bounds ? /* @__PURE__ */ jsx("div", {
1869
+ className: "povio-canvas-inspection-overlay povio-canvas-layer-highlight",
1870
+ "aria-hidden": "true",
1871
+ children: /* @__PURE__ */ jsx("div", {
1872
+ className: "studio-hover-outline",
1873
+ style: bounds
1874
+ })
1875
+ }) : null;
1876
+ $[3] = bounds;
1877
+ $[4] = t3;
1878
+ } else t3 = $[4];
1879
+ return t3;
1880
+ }
895
1881
  //#endregion
896
1882
  //#region src/app-canvas/route-model.ts
897
1883
  function joinPath(parent, path) {
@@ -1070,68 +2056,6 @@ function validConfiguration(value) {
1070
2056
  }) && !!config.config && typeof config.config === "object";
1071
2057
  }
1072
2058
  //#endregion
1073
- //#region src/app-canvas/manifest.ts
1074
- function createCanvasManifest() {
1075
- return {
1076
- version: 1,
1077
- positions: {},
1078
- examples: {},
1079
- manualEdges: [],
1080
- hiddenEdges: [],
1081
- breakpoint: "desktop"
1082
- };
1083
- }
1084
- function isRecord(value) {
1085
- return value !== null && typeof value === "object" && !Array.isArray(value);
1086
- }
1087
- function isJSON(value, ancestors = /* @__PURE__ */ new Set(), depth = 0) {
1088
- if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1089
- if (typeof value === "number") return Number.isFinite(value);
1090
- if (typeof value !== "object" || depth > 30 || ancestors.has(value)) return false;
1091
- ancestors.add(value);
1092
- const valid = Object.values(value).every((child) => isJSON(child, ancestors, depth + 1));
1093
- ancestors.delete(value);
1094
- return valid;
1095
- }
1096
- /** Sanitize either stored JSON text or an imported value, retaining valid v1 entries. */
1097
- function parseCanvasManifest(input) {
1098
- const fallback = createCanvasManifest();
1099
- let value = input;
1100
- if (typeof value === "string") try {
1101
- value = JSON.parse(value);
1102
- } catch {
1103
- return fallback;
1104
- }
1105
- if (!isRecord(value) || value.version !== 1) return fallback;
1106
- const positions = Object.fromEntries(Object.entries(isRecord(value.positions) ? value.positions : {}).flatMap(([breakpoint, entries]) => {
1107
- if (!isRecord(entries)) return [];
1108
- return [[breakpoint, Object.fromEntries(Object.entries(entries).flatMap(([id, point]) => isRecord(point) && typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y) ? [[id, {
1109
- x: point.x,
1110
- y: point.y
1111
- }]] : []))]];
1112
- }));
1113
- const examples = Object.fromEntries(Object.entries(isRecord(value.examples) ? value.examples : {}).flatMap(([id, example]) => {
1114
- if (!isRecord(example)) return [];
1115
- const clean = {};
1116
- if (typeof example.title === "string") clean.title = example.title;
1117
- if (isRecord(example.params)) clean.params = Object.fromEntries(Object.entries(example.params).filter((entry) => typeof entry[1] === "string"));
1118
- if (isRecord(example.search)) clean.search = Object.fromEntries(Object.entries(example.search).filter(([, item]) => isJSON(item)));
1119
- return [[id, clean]];
1120
- }));
1121
- const manualEdges = (Array.isArray(value.manualEdges) ? value.manualEdges : []).flatMap((edge) => isRecord(edge) && typeof edge.from === "string" && edge.from.length > 0 && typeof edge.to === "string" && edge.to.length > 0 ? [{
1122
- from: edge.from,
1123
- to: edge.to
1124
- }] : []);
1125
- return {
1126
- version: 1,
1127
- positions,
1128
- examples,
1129
- manualEdges: [...new Map(manualEdges.map((edge) => [JSON.stringify(edge), edge])).values()],
1130
- hiddenEdges: [...new Set((Array.isArray(value.hiddenEdges) ? value.hiddenEdges : []).filter((edge) => typeof edge === "string"))],
1131
- breakpoint: typeof value.breakpoint === "string" && value.breakpoint.length > 0 ? value.breakpoint : fallback.breakpoint
1132
- };
1133
- }
1134
- //#endregion
1135
2059
  //#region src/app-canvas/AppCanvas.tsx
1136
2060
  const defaultCanvasBreakpoints = [
1137
2061
  {
@@ -1155,14 +2079,9 @@ const defaultCanvasBreakpoints = [
1155
2079
  ];
1156
2080
  const emptyExamples = {};
1157
2081
  const emptyExclude = [];
1158
- function readManifest(key) {
1159
- if (typeof window === "undefined") return createCanvasManifest();
1160
- try {
1161
- return parseCanvasManifest(localStorage.getItem(key));
1162
- } catch {
1163
- return createCanvasManifest();
1164
- }
1165
- }
2082
+ /** Zoom per wheel pixel, and the largest delta one wheel event may contribute. */
2083
+ const ZOOM_SENSITIVITY = .006;
2084
+ const ZOOM_DELTA_LIMIT = 40;
1166
2085
  const PageFrame = memo(function PageFrame(t0) {
1167
2086
  const $ = c(16);
1168
2087
  const { id, src, width, height, name, interact, inspect, onLoad, register } = t0;
@@ -1211,19 +2130,33 @@ const PageFrame = memo(function PageFrame(t0) {
1211
2130
  return t6;
1212
2131
  });
1213
2132
  /** Live route artboards. The camera transforms only the canvas, never the iframe viewport width. */
1214
- function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, excludePaths = emptyExclude, breakpoints = defaultCanvasBreakpoints, storageKey = "povio-app-canvas", frameUrl }) {
2133
+ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, excludePaths = emptyExclude, breakpoints = defaultCanvasBreakpoints, storageKey = "povio-app-canvas", frameUrl, componentsHref, repositoryStorage = true }) {
1215
2134
  const configuration = useUIConfiguration();
1216
- const [manifest, setManifest] = useState(() => readManifest(storageKey));
2135
+ const storage = useCanvasStorage(storageKey, repositoryStorage);
2136
+ const { manifest, setManifest } = storage;
2137
+ const workspace = manifest.workspaces.find((item) => item.id === manifest.activeWorkspaceId);
1217
2138
  const [runtime, setRuntime] = useState({});
2139
+ const [hoveredLayer, setHoveredLayer] = useState(null);
1218
2140
  const [selected, setSelected] = useState(null);
1219
2141
  const [interact, setInteract] = useState(false);
1220
2142
  const [inspectMode, setInspectMode] = useState(false);
1221
2143
  const [dark, setDark] = useState(false);
2144
+ /** The canvas shows either the app's pages or the component gallery's sections, as artboards either way. */
2145
+ const [content, setContent] = useState("pages");
1222
2146
  const [mobilePanel, setMobilePanel] = useState(null);
1223
2147
  const [query, setQuery] = useState("");
1224
2148
  const [notice, setNotice] = useState("");
1225
2149
  const [documentGeneration, setDocumentGeneration] = useState(0);
2150
+ useWorkspaceConfiguration(workspace, storage.ready, documentGeneration, setManifest);
1226
2151
  const [reload, setReload] = useState({});
2152
+ const [showLayers, setShowLayers] = useState(true);
2153
+ useEffect(() => setHoveredLayer(null), [
2154
+ selected,
2155
+ showLayers,
2156
+ manifest.activeWorkspaceId,
2157
+ manifest.breakpoint,
2158
+ documentGeneration
2159
+ ]);
1227
2160
  const zoomOutput = useRef(null);
1228
2161
  const importInput = useRef(null);
1229
2162
  const viewport = useRef(null), world = useRef(null);
@@ -1236,9 +2169,12 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1236
2169
  }), pendingCamera = useRef(null);
1237
2170
  const pointer = useRef(null);
1238
2171
  const cameraRevision = useRef(0);
1239
- const bp = breakpoints.find((item) => item.id === manifest.breakpoint) ?? breakpoints[breakpoints.length - 1] ?? defaultCanvasBreakpoints[2];
1240
- const inspector = useCanvasInspector(inspectMode, `${bp.id}:${documentGeneration}:${JSON.stringify(reload)}`);
1241
- const catalog = useMemo(() => discoverCanvasPages(router, {
2172
+ const bp = breakpoints.find((item_0) => item_0.id === manifest.breakpoint) ?? breakpoints[breakpoints.length - 1] ?? defaultCanvasBreakpoints[2];
2173
+ const isKnownToken = useCallback((name) => isEditableToken(name, configuration.defaults), [configuration.defaults]);
2174
+ const { hostComponents } = configuration;
2175
+ useEffect(() => hostComponents(), [hostComponents]);
2176
+ const inspector = useCanvasInspector(inspectMode, `${manifest.activeWorkspaceId}:${content}:${bp.id}:${documentGeneration}:${JSON.stringify(reload)}`, isKnownToken);
2177
+ const allPages = useMemo(() => discoverCanvasPages(router, {
1242
2178
  examples: {
1243
2179
  ...examples,
1244
2180
  ...manifest.examples
@@ -1252,7 +2188,21 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1252
2188
  exclude,
1253
2189
  excludePaths
1254
2190
  ]);
1255
- const startupKeys = useMemo(() => new Map(catalog.filter((page) => page.href).map((page_0) => [page_0.id, `${documentGeneration}:${bp.id}:${page_0.id}:${page_0.href}:${reload[page_0.id] ?? 0}`])), [
2191
+ const pageCatalog = useMemo(() => storage.ready ? allPages.filter((page) => workspace.enabledPages === null || workspace.enabledPages.includes(page.id)) : [], [
2192
+ allPages,
2193
+ workspace.enabledPages,
2194
+ storage.ready
2195
+ ]);
2196
+ const componentCatalog = useMemo(() => componentsHref ? componentArtboards.map((group) => ({
2197
+ id: `components:${group.toLowerCase()}`,
2198
+ title: group,
2199
+ path: group,
2200
+ href: componentsHref(group),
2201
+ missingParams: [],
2202
+ kind: "page"
2203
+ })) : [], [componentsHref]);
2204
+ const catalog = content === "components" && componentCatalog.length ? componentCatalog : pageCatalog;
2205
+ const startupKeys = useMemo(() => new Map(catalog.filter((page_0) => page_0.href).map((page_1) => [page_1.id, `${documentGeneration}:${bp.id}:${page_1.id}:${page_1.href}:${reload[page_1.id] ?? 0}`])), [
1256
2206
  catalog,
1257
2207
  documentGeneration,
1258
2208
  bp.id,
@@ -1267,18 +2217,37 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1267
2217
  keys: startupKeys,
1268
2218
  complete: startup.complete
1269
2219
  };
1270
- const heights = useMemo(() => Object.fromEntries(catalog.map((page_1) => [page_1.id, runtime[`${bp.id}:${page_1.id}:${page_1.href ?? ""}`]?.height ?? bp.height])), [
2220
+ const heights = useMemo(() => Object.fromEntries(catalog.map((page_2) => [page_2.id, runtime[`${bp.id}:${page_2.id}:${page_2.href ?? ""}`]?.height ?? bp.height])), [
1271
2221
  catalog,
1272
2222
  runtime,
1273
2223
  bp
1274
2224
  ]);
1275
- const nodes = useMemo(() => layoutCanvasPages(catalog, bp.width, heights, manifest.positions[bp.id] ?? {}), [
2225
+ const nodes = useMemo(() => layoutCanvasPages(catalog, bp.width, heights, workspace.positions[bp.id] ?? {}), [
1276
2226
  catalog,
1277
2227
  bp,
1278
2228
  heights,
1279
- manifest.positions
2229
+ workspace.positions
1280
2230
  ]);
1281
2231
  const nodeMap = useMemo(() => new Map(nodes.map((node) => [node.id, node])), [nodes]);
2232
+ useLayoutEffect(() => {
2233
+ const gesture = pointer.current;
2234
+ if (gesture && viewport.current?.hasPointerCapture(gesture.id)) viewport.current.releasePointerCapture(gesture.id);
2235
+ pointer.current = null;
2236
+ setSelected(null);
2237
+ setQuery("");
2238
+ camera.current = {
2239
+ x: 40,
2240
+ y: 40,
2241
+ zoom: .25
2242
+ };
2243
+ }, [manifest.activeWorkspaceId]);
2244
+ useEffect(() => {
2245
+ if (selected && !nodeMap.has(selected)) setSelected(null);
2246
+ }, [selected, nodeMap]);
2247
+ useEffect(() => {
2248
+ const valid = new Set(catalog.map((page_3) => `${bp.id}:${page_3.id}:${page_3.href ?? ""}`));
2249
+ setRuntime((current) => Object.keys(current).some((key_0) => !valid.has(key_0)) ? Object.fromEntries(Object.entries(current).filter(([key]) => valid.has(key))) : current);
2250
+ }, [catalog, bp.id]);
1282
2251
  const latest = useRef({
1283
2252
  configuration: configuration.active,
1284
2253
  dark,
@@ -1300,11 +2269,18 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1300
2269
  else frames.current.delete(id);
1301
2270
  }, []);
1302
2271
  const paint = useCallback(() => {
1303
- if (world.current) world.current.style.transform = `translate3d(${camera.current.x}px,${camera.current.y}px,0) scale(${camera.current.zoom})`;
2272
+ if (world.current) {
2273
+ world.current.style.transform = `translate3d(${camera.current.x}px,${camera.current.y}px,0) scale(${camera.current.zoom})`;
2274
+ const zoom = String(camera.current.zoom);
2275
+ if (world.current.style.getPropertyValue("--canvas-zoom") !== zoom) world.current.style.setProperty("--canvas-zoom", zoom);
2276
+ }
1304
2277
  if (zoomOutput.current) zoomOutput.current.textContent = `${Math.round(camera.current.zoom * 1e3) / 10}%`;
1305
2278
  const canvas = viewport.current?.closest(".povio-app-canvas");
1306
2279
  if (canvas) canvas.setAttribute("data-camera-revision", String(++cameraRevision.current));
1307
2280
  }, []);
2281
+ useLayoutEffect(() => {
2282
+ paint();
2283
+ }, [manifest.activeWorkspaceId, paint]);
1308
2284
  const schedulePaint = useCallback(() => {
1309
2285
  if (pendingCamera.current !== null) return;
1310
2286
  pendingCamera.current = requestAnimationFrame(() => {
@@ -1315,18 +2291,18 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1315
2291
  const send = useCallback((id_0, type = "configuration") => {
1316
2292
  const frame_0 = frames.current.get(id_0);
1317
2293
  if (!frame_0?.contentWindow) return;
1318
- const current = latest.current;
2294
+ const current_0 = latest.current;
1319
2295
  try {
1320
2296
  frame_0.contentWindow.postMessage({
1321
2297
  protocol: canvasProtocol,
1322
2298
  id: id_0,
1323
2299
  token,
1324
- viewportHeight: current.bp.height,
2300
+ viewportHeight: current_0.bp.height,
1325
2301
  type,
1326
- configuration: current.configuration,
2302
+ configuration: current_0.configuration,
1327
2303
  revision: revision.current,
1328
- dark: current.dark,
1329
- interact: current.interact
2304
+ dark: current_0.dark,
2305
+ interact: current_0.interact
1330
2306
  }, new URL(frame_0.src).origin);
1331
2307
  } catch {
1332
2308
  setNotice("This configuration could not be sent to the page. Check for non-serializable values.");
@@ -1336,7 +2312,8 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1336
2312
  const node_0 = nodeMap.get(id_1), view = viewport.current;
1337
2313
  if (!node_0 || !view) return;
1338
2314
  setSelected(id_1);
1339
- camera.current.zoom = Math.min(1, Math.max(.08, (view.clientWidth - 100) / node_0.width));
2315
+ setShowLayers(true);
2316
+ camera.current.zoom = Math.min(1, Math.max(.005, (view.clientWidth - 80) / node_0.width));
1340
2317
  camera.current.x = 40 - node_0.x * camera.current.zoom;
1341
2318
  camera.current.y = 40 - node_0.y * camera.current.zoom;
1342
2319
  paint();
@@ -1351,18 +2328,18 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1351
2328
  batch = 0;
1352
2329
  const changes = Array.from(queue);
1353
2330
  queue.clear();
1354
- setRuntime((current_0) => {
1355
- const next = { ...current_0 };
2331
+ setRuntime((current_1) => {
2332
+ const next = { ...current_1 };
1356
2333
  let changed = false;
1357
- for (const [key, item_0] of changes) {
1358
- if (frames.current.get(item_0.id) !== item_0.frame || item_0.frame.contentWindow !== item_0.source) continue;
1359
- next[key] = {
1360
- ...next[key],
1361
- ...item_0.change
2334
+ for (const [key_1, item_1] of changes) {
2335
+ if (frames.current.get(item_1.id) !== item_1.frame || item_1.frame.contentWindow !== item_1.source) continue;
2336
+ next[key_1] = {
2337
+ ...next[key_1],
2338
+ ...item_1.change
1362
2339
  };
1363
2340
  changed = true;
1364
2341
  }
1365
- return changed ? next : current_0;
2342
+ return changed ? next : current_1;
1366
2343
  });
1367
2344
  };
1368
2345
  const receive = (event) => {
@@ -1370,7 +2347,7 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1370
2347
  if (packet?.protocol !== "povio-app-canvas-v1" || packet.token !== token || typeof packet.id !== "string") return;
1371
2348
  const frame_1 = frames.current.get(packet.id);
1372
2349
  if (!frame_1 || event.source !== frame_1.contentWindow || event.origin !== new URL(frame_1.src).origin) return;
1373
- const key_0 = `${latest.current.bp.id}:${packet.id}:${latest.current.catalog.find((page_2) => page_2.id === packet.id)?.href ?? ""}`;
2350
+ const key_2 = `${latest.current.bp.id}:${packet.id}:${latest.current.catalog.find((page_4) => page_4.id === packet.id)?.href ?? ""}`;
1374
2351
  let change = {};
1375
2352
  if (packet.type === "ready") {
1376
2353
  startupRef.current.complete(startupRef.current.keys.get(packet.id));
@@ -1392,9 +2369,9 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1392
2369
  return;
1393
2370
  }
1394
2371
  if (!Object.keys(change).length) return;
1395
- const pending = queue.get(key_0);
2372
+ const pending = queue.get(key_2);
1396
2373
  const previous = pending?.frame === frame_1 && pending.source === event.source ? pending.change : {};
1397
- queue.set(key_0, {
2374
+ queue.set(key_2, {
1398
2375
  id: packet.id,
1399
2376
  frame: frame_1,
1400
2377
  source: event.source,
@@ -1429,25 +2406,20 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1429
2406
  useEffect(() => {
1430
2407
  for (const id_3 of frames.current.keys()) send(id_3, "mode");
1431
2408
  }, [interact, send]);
1432
- useEffect(() => {
1433
- try {
1434
- localStorage.setItem(storageKey, JSON.stringify(manifest));
1435
- } catch {
1436
- setNotice("Browser storage is unavailable; use Export layout to save this canvas.");
1437
- }
1438
- }, [manifest, storageKey]);
1439
2409
  useEffect(() => {
1440
2410
  paint();
1441
2411
  const element = viewport.current;
1442
2412
  if (!element) return;
1443
2413
  const wheel = (event_0) => {
1444
2414
  event_0.preventDefault();
2415
+ if (pointer.current) return;
1445
2416
  if (event_0.ctrlKey || event_0.metaKey) {
1446
2417
  const box = element.getBoundingClientRect();
1447
2418
  const x = event_0.clientX - box.left, y = event_0.clientY - box.top;
1448
2419
  const previous_0 = camera.current.zoom;
1449
2420
  const unit = event_0.deltaMode === 1 ? 16 : event_0.deltaMode === 2 ? element.clientHeight : 1;
1450
- const next_0 = Math.max(.005, Math.min(2, previous_0 * Math.exp(-event_0.deltaY * unit * .01)));
2421
+ const delta = Math.max(-40, Math.min(ZOOM_DELTA_LIMIT, event_0.deltaY * unit));
2422
+ const next_0 = Math.max(.005, Math.min(2, previous_0 * Math.exp(-delta * ZOOM_SENSITIVITY)));
1451
2423
  camera.current.x = x - (x - camera.current.x) * next_0 / previous_0;
1452
2424
  camera.current.y = y - (y - camera.current.y) * next_0 / previous_0;
1453
2425
  camera.current.zoom = next_0;
@@ -1457,9 +2429,14 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1457
2429
  }
1458
2430
  schedulePaint();
1459
2431
  };
2432
+ const unscroll = () => {
2433
+ if (element.scrollTop || element.scrollLeft) element.scrollTo(0, 0);
2434
+ };
1460
2435
  element.addEventListener("wheel", wheel, { passive: false });
2436
+ element.addEventListener("scroll", unscroll);
1461
2437
  return () => {
1462
2438
  element.removeEventListener("wheel", wheel);
2439
+ element.removeEventListener("scroll", unscroll);
1463
2440
  if (pendingCamera.current !== null) cancelAnimationFrame(pendingCamera.current);
1464
2441
  pendingCamera.current = null;
1465
2442
  };
@@ -1473,13 +2450,22 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1473
2450
  camera.current.y = 40 - top * camera.current.zoom;
1474
2451
  paint();
1475
2452
  };
2453
+ const fitRef = useRef(fit);
2454
+ fitRef.current = fit;
2455
+ const switchContent = (next_1) => {
2456
+ if (next_1 === content) return;
2457
+ setContent(next_1);
2458
+ setSelected(null);
2459
+ setQuery("");
2460
+ requestAnimationFrame(() => fitRef.current());
2461
+ };
1476
2462
  const zoomTo = (value) => {
1477
2463
  const view_0 = viewport.current;
1478
2464
  if (!view_0) return;
1479
- const previous_1 = camera.current.zoom, next_1 = Math.max(.005, Math.min(2, value));
1480
- camera.current.x = view_0.clientWidth / 2 - (view_0.clientWidth / 2 - camera.current.x) * next_1 / previous_1;
1481
- camera.current.y = view_0.clientHeight / 2 - (view_0.clientHeight / 2 - camera.current.y) * next_1 / previous_1;
1482
- camera.current.zoom = next_1;
2465
+ const previous_1 = camera.current.zoom, next_2 = Math.max(.005, Math.min(2, value));
2466
+ camera.current.x = view_0.clientWidth / 2 - (view_0.clientWidth / 2 - camera.current.x) * next_2 / previous_1;
2467
+ camera.current.y = view_0.clientHeight / 2 - (view_0.clientHeight / 2 - camera.current.y) * next_2 / previous_1;
2468
+ camera.current.zoom = next_2;
1483
2469
  paint();
1484
2470
  };
1485
2471
  const stepZoom = (direction) => {
@@ -1496,58 +2482,74 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1496
2482
  1.5,
1497
2483
  2
1498
2484
  ];
1499
- const current_1 = camera.current.zoom;
1500
- const next_2 = direction === 1 ? stops.find((value_0) => value_0 > current_1 + 1e-4) : [...stops].reverse().find((value_1) => value_1 < current_1 - 1e-4);
1501
- if (next_2 !== void 0) zoomTo(next_2);
2485
+ const current_2 = camera.current.zoom;
2486
+ const next_3 = direction === 1 ? stops.find((value_0) => value_0 > current_2 + 1e-4) : [...stops].reverse().find((value_1) => value_1 < current_2 - 1e-4);
2487
+ if (next_3 !== void 0) zoomTo(next_3);
1502
2488
  };
1503
2489
  const startPan = (event_1) => {
1504
2490
  if (inspectMode && event_1.target.closest(".povio-canvas-frame-cover")) return;
1505
2491
  if (event_1.button !== 0 || event_1.target.closest("button:not(.povio-canvas-frame-cover),input,select,.povio-canvas-node-header")) return;
2492
+ event_1.preventDefault();
1506
2493
  event_1.currentTarget.setPointerCapture(event_1.pointerId);
1507
2494
  pointer.current = {
1508
2495
  id: event_1.pointerId,
1509
2496
  x: event_1.clientX,
1510
2497
  y: event_1.clientY,
2498
+ lastX: event_1.clientX,
2499
+ lastY: event_1.clientY,
1511
2500
  startX: camera.current.x,
1512
2501
  startY: camera.current.y
1513
2502
  };
1514
2503
  };
1515
2504
  const move = (event_2) => {
1516
- const current_2 = pointer.current;
1517
- if (!current_2 || current_2.id !== event_2.pointerId) return;
1518
- const dx = event_2.clientX - current_2.x, dy = event_2.clientY - current_2.y;
1519
- if (current_2.node) {
1520
- const element_0 = world.current?.querySelector(`[data-node-index="${nodes.findIndex((n_3) => n_3.id === current_2.node)}"]`);
2505
+ const current_3 = pointer.current;
2506
+ if (!current_3 || current_3.id !== event_2.pointerId) return;
2507
+ if (event_2.buttons === 0) {
2508
+ end(event_2, true);
2509
+ return;
2510
+ }
2511
+ current_3.lastX = event_2.clientX;
2512
+ current_3.lastY = event_2.clientY;
2513
+ const dx = event_2.clientX - current_3.x, dy = event_2.clientY - current_3.y;
2514
+ if (current_3.node) {
2515
+ const element_0 = world.current?.querySelector(`[data-node-index="${nodes.findIndex((n_3) => n_3.id === current_3.node)}"]`);
1521
2516
  if (element_0) {
1522
- element_0.style.left = `${current_2.nodeX + dx / camera.current.zoom}px`;
1523
- element_0.style.top = `${current_2.nodeY + dy / camera.current.zoom}px`;
2517
+ element_0.style.left = `${current_3.nodeX + dx / camera.current.zoom}px`;
2518
+ element_0.style.top = `${current_3.nodeY + dy / camera.current.zoom}px`;
1524
2519
  }
1525
2520
  } else {
1526
- camera.current.x = current_2.startX + dx;
1527
- camera.current.y = current_2.startY + dy;
2521
+ camera.current.x = current_3.startX + dx;
2522
+ camera.current.y = current_3.startY + dy;
1528
2523
  schedulePaint();
1529
2524
  }
1530
2525
  };
1531
- const end = (event_3) => {
1532
- const current_3 = pointer.current;
1533
- if (!current_3 || current_3.id !== event_3.pointerId) return;
1534
- if (current_3.node) {
2526
+ const end = (event_3, useLastPosition = false) => {
2527
+ const current_4 = pointer.current;
2528
+ if (!current_4 || current_4.id !== event_3.pointerId) return;
2529
+ pointer.current = null;
2530
+ if (current_4.node) {
1535
2531
  const position = {
1536
- x: current_3.nodeX + (event_3.clientX - current_3.x) / camera.current.zoom,
1537
- y: current_3.nodeY + (event_3.clientY - current_3.y) / camera.current.zoom
2532
+ x: current_4.nodeX + ((useLastPosition ? current_4.lastX : event_3.clientX) - current_4.x) / camera.current.zoom,
2533
+ y: current_4.nodeY + ((useLastPosition ? current_4.lastY : event_3.clientY) - current_4.y) / camera.current.zoom
1538
2534
  };
1539
2535
  setManifest((m) => ({
1540
2536
  ...m,
1541
- positions: {
1542
- ...m.positions,
1543
- [bp.id]: {
1544
- ...m.positions[bp.id],
1545
- [current_3.node]: position
2537
+ workspaces: m.workspaces.map((item_2) => item_2.id === m.activeWorkspaceId ? {
2538
+ ...item_2,
2539
+ positions: {
2540
+ ...item_2.positions,
2541
+ [bp.id]: {
2542
+ ...item_2.positions[bp.id],
2543
+ ...Object.fromEntries(nodes.map((node_1) => [node_1.id, {
2544
+ x: node_1.x,
2545
+ y: node_1.y
2546
+ }])),
2547
+ [current_4.node]: position
2548
+ }
1546
2549
  }
1547
- }
2550
+ } : item_2)
1548
2551
  }));
1549
2552
  }
1550
- pointer.current = null;
1551
2553
  if (event_3.currentTarget.hasPointerCapture(event_3.pointerId)) event_3.currentTarget.releasePointerCapture(event_3.pointerId);
1552
2554
  };
1553
2555
  const onLoad = useCallback((id_4, frame_3) => {
@@ -1557,16 +2559,16 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1557
2559
  send(id_4, "mode");
1558
2560
  }, [send]);
1559
2561
  useEffect(() => {
1560
- const timer = setTimeout(() => setRuntime((current_4) => {
1561
- const next_3 = { ...current_4 };
1562
- for (const page_3 of latest.current.catalog) {
1563
- const key_1 = `${latest.current.bp.id}:${page_3.id}:${page_3.href ?? ""}`;
1564
- if (page_3.href && frames.current.has(page_3.id) && !next_3[key_1]?.ready) next_3[key_1] = {
1565
- ...next_3[key_1],
2562
+ const timer = setTimeout(() => setRuntime((current_5) => {
2563
+ const next_4 = { ...current_5 };
2564
+ for (const page_5 of latest.current.catalog) {
2565
+ const key_3 = `${latest.current.bp.id}:${page_5.id}:${page_5.href ?? ""}`;
2566
+ if (page_5.href && frames.current.has(page_5.id) && !next_4[key_3]?.ready) next_4[key_3] = {
2567
+ ...next_4[key_3],
1566
2568
  timedOut: true
1567
2569
  };
1568
2570
  }
1569
- return next_3;
2571
+ return next_4;
1570
2572
  }), 15e3);
1571
2573
  return () => clearTimeout(timer);
1572
2574
  }, [
@@ -1606,13 +2608,13 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1606
2608
  if (!file) return;
1607
2609
  try {
1608
2610
  const raw = JSON.parse(await file.text());
1609
- if (raw?.version !== 1) throw new Error("version");
2611
+ if (raw?.version !== 1 && raw?.version !== 2) throw new Error("version");
1610
2612
  setManifest(parseCanvasManifest(raw));
1611
2613
  setRuntime({});
1612
2614
  setDocumentGeneration((value_2) => value_2 + 1);
1613
2615
  setNotice("Layout imported.");
1614
2616
  } catch {
1615
- setNotice("Choose a valid version 1 canvas layout JSON file.");
2617
+ setNotice("Choose a valid canvas workspace JSON file.");
1616
2618
  }
1617
2619
  }
1618
2620
  }),
@@ -1623,11 +2625,30 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1623
2625
  className: "povio-canvas-catalog",
1624
2626
  "aria-label": "Page catalog",
1625
2627
  children: [
2628
+ /* @__PURE__ */ jsx(WorkspaceControls, {
2629
+ manifest,
2630
+ setManifest,
2631
+ pages: allPages,
2632
+ disabled: !storage.ready
2633
+ }),
1626
2634
  /* @__PURE__ */ jsxs("div", {
1627
- className: "povio-canvas-brand",
1628
- children: [/* @__PURE__ */ jsx("strong", { children: "Pages" }), /* @__PURE__ */ jsx("span", {
1629
- className: "povio-canvas-page-count",
1630
- children: nodes.length
2635
+ className: "povio-canvas-brand povio-canvas-content-switch",
2636
+ role: "group",
2637
+ "aria-label": "Canvas content",
2638
+ children: [/* @__PURE__ */ jsxs("button", {
2639
+ "aria-pressed": content === "pages",
2640
+ onClick: () => switchContent("pages"),
2641
+ children: ["Pages", /* @__PURE__ */ jsx("span", {
2642
+ className: "povio-canvas-page-count",
2643
+ children: pageCatalog.length
2644
+ })]
2645
+ }), !!componentCatalog.length && /* @__PURE__ */ jsxs("button", {
2646
+ "aria-pressed": content === "components",
2647
+ onClick: () => switchContent("components"),
2648
+ children: ["Components", /* @__PURE__ */ jsx("span", {
2649
+ className: "povio-canvas-page-count",
2650
+ children: componentCatalog.length
2651
+ })]
1631
2652
  })]
1632
2653
  }),
1633
2654
  /* @__PURE__ */ jsxs("div", {
@@ -1635,8 +2656,8 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1635
2656
  children: [
1636
2657
  /* @__PURE__ */ jsx(Search, { size: 14 }),
1637
2658
  /* @__PURE__ */ jsx("input", {
1638
- "aria-label": "Find page",
1639
- placeholder: "Find page…",
2659
+ "aria-label": content === "components" ? "Find component section" : "Find page",
2660
+ placeholder: content === "components" ? "Find section…" : "Find page…",
1640
2661
  value: query,
1641
2662
  onChange: (event_5) => setQuery(event_5.target.value)
1642
2663
  }),
@@ -1648,37 +2669,49 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1648
2669
  })
1649
2670
  ]
1650
2671
  }),
1651
- /* @__PURE__ */ jsxs("div", {
2672
+ /* @__PURE__ */ jsx("div", {
1652
2673
  className: "povio-canvas-page-list",
1653
- children: [nodes.filter((node_1) => `${node_1.title} ${node_1.path}`.toLowerCase().includes(query.toLowerCase())).map((node_2) => /* @__PURE__ */ jsxs("button", {
1654
- "aria-pressed": selected === node_2.id,
1655
- onClick: () => {
1656
- focusPage(node_2.id);
1657
- setMobilePanel(null);
1658
- },
1659
- children: [/* @__PURE__ */ jsx(Frame, { size: 15 }), /* @__PURE__ */ jsxs("span", {
1660
- className: "povio-canvas-page-label",
1661
- children: [/* @__PURE__ */ jsx("strong", { children: node_2.title }), node_2.kind === "unresolved" && /* @__PURE__ */ jsx("small", { children: "Needs parameters" })]
2674
+ children: nodes.filter((node_2) => `${node_2.title} ${node_2.path}`.toLowerCase().includes(query.toLowerCase())).map((node_3) => /* @__PURE__ */ jsxs("div", {
2675
+ className: "povio-canvas-page-entry",
2676
+ children: [/* @__PURE__ */ jsxs("button", {
2677
+ "aria-expanded": node_3.href ? selected === node_3.id && showLayers : void 0,
2678
+ "aria-pressed": selected === node_3.id,
2679
+ onClick: () => {
2680
+ if (selected === node_3.id && showLayers) setShowLayers(false);
2681
+ else focusPage(node_3.id);
2682
+ },
2683
+ children: [node_3.href ? selected === node_3.id && showLayers ? /* @__PURE__ */ jsx(ChevronDown, { size: 12 }) : /* @__PURE__ */ jsx(ChevronRight, { size: 12 }) : /* @__PURE__ */ jsx(Frame, { size: 15 }), /* @__PURE__ */ jsxs("span", {
2684
+ className: "povio-canvas-page-label",
2685
+ children: [/* @__PURE__ */ jsx("strong", { children: node_3.title }), node_3.kind === "unresolved" && /* @__PURE__ */ jsx("small", { children: "Needs parameters" })]
2686
+ })]
2687
+ }), showLayers && selected === node_3.id && node_3.href && /* @__PURE__ */ jsx("div", {
2688
+ className: "povio-canvas-inline-layers",
2689
+ children: /* @__PURE__ */ jsx(PageLayers, {
2690
+ frame: frames.current.get(selected) ?? null,
2691
+ title: nodeMap.get(selected).title,
2692
+ selected: inspector.selection?.pageId === selected ? inspector.selection.element : void 0,
2693
+ onHover: (element_1) => setHoveredLayer(element_1 ? {
2694
+ pageId: node_3.id,
2695
+ element: element_1
2696
+ } : null),
2697
+ onSelect: (element_2) => {
2698
+ setInteract(false);
2699
+ setInspectMode(true);
2700
+ inspector.selectElement(selected, element_2);
2701
+ setMobilePanel(null);
2702
+ requestAnimationFrame(() => {
2703
+ const node_4 = nodeMap.get(selected), view_1 = viewport.current;
2704
+ if (!node_4 || !view_1 || !element_2.isConnected) return;
2705
+ const rect = element_2.getBoundingClientRect();
2706
+ camera.current.zoom = Math.min(camera.current.zoom, Math.max(.005, (view_1.clientWidth - 80) / node_4.width));
2707
+ camera.current.x = 40 - node_4.x * camera.current.zoom;
2708
+ camera.current.y = view_1.clientHeight / 2 - (node_4.y + 48 + rect.top + rect.height / 2) * camera.current.zoom;
2709
+ paint();
2710
+ });
2711
+ }
2712
+ }, `${bp.id}:${documentGeneration}:${reload[selected] ?? 0}:${selected}:${nodeMap.get(selected)?.href}`)
1662
2713
  })]
1663
- }, node_2.id)), selected && nodeMap.get(selected)?.href && /* @__PURE__ */ jsx(PageLayers, {
1664
- frame: frames.current.get(selected) ?? null,
1665
- title: nodeMap.get(selected).title,
1666
- selected: inspector.selection?.pageId === selected ? inspector.selection.element : void 0,
1667
- onSelect: (element_1) => {
1668
- setInteract(false);
1669
- setInspectMode(true);
1670
- inspector.selectElement(selected, element_1);
1671
- setMobilePanel(null);
1672
- requestAnimationFrame(() => {
1673
- const node_3 = nodeMap.get(selected), view_1 = viewport.current;
1674
- if (!node_3 || !view_1 || !element_1.isConnected) return;
1675
- const rect = element_1.getBoundingClientRect();
1676
- camera.current.x = view_1.clientWidth / 2 - (node_3.x + rect.left + rect.width / 2) * camera.current.zoom;
1677
- camera.current.y = view_1.clientHeight / 2 - (node_3.y + 48 + rect.top + rect.height / 2) * camera.current.zoom;
1678
- paint();
1679
- });
1680
- }
1681
- }, `${bp.id}:${documentGeneration}:${reload[selected] ?? 0}:${selected}:${nodeMap.get(selected)?.href}`)]
2714
+ }, node_3.id))
1682
2715
  }),
1683
2716
  /* @__PURE__ */ jsxs("div", {
1684
2717
  className: "povio-canvas-sidebar-actions",
@@ -1705,10 +2738,28 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1705
2738
  ready,
1706
2739
  "/",
1707
2740
  nodes.length,
1708
- " pages ready"
2741
+ " ",
2742
+ content === "components" ? "sections" : "pages",
2743
+ " ready"
1709
2744
  ]
1710
2745
  })
1711
2746
  ]
2747
+ }),
2748
+ /* @__PURE__ */ jsxs("div", {
2749
+ className: "povio-canvas-storage-status",
2750
+ role: "status",
2751
+ children: [
2752
+ storage.status,
2753
+ /* @__PURE__ */ jsx(SaveCanvasTheme, {}),
2754
+ storage.error && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", { children: storage.error }), /* @__PURE__ */ jsx("button", {
2755
+ onClick: () => {
2756
+ setRuntime({});
2757
+ setDocumentGeneration((value_3) => value_3 + 1);
2758
+ storage.reload();
2759
+ },
2760
+ children: "Reload repository settings"
2761
+ })] })
2762
+ ]
1712
2763
  })
1713
2764
  ]
1714
2765
  }),
@@ -1724,18 +2775,8 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1724
2775
  onPointerDown: startPan,
1725
2776
  onPointerMove: move,
1726
2777
  onPointerUp: end,
1727
- onPointerCancel: (event_7) => {
1728
- const current_5 = pointer.current;
1729
- if (current_5?.node) {
1730
- const element_2 = world.current?.querySelector(`[data-node-index="${nodes.findIndex((node_4) => node_4.id === current_5.node)}"]`);
1731
- if (element_2) {
1732
- element_2.style.left = `${current_5.nodeX}px`;
1733
- element_2.style.top = `${current_5.nodeY}px`;
1734
- }
1735
- }
1736
- pointer.current = null;
1737
- if (event_7.currentTarget.hasPointerCapture(event_7.pointerId)) event_7.currentTarget.releasePointerCapture(event_7.pointerId);
1738
- },
2778
+ onPointerCancel: (event_7) => end(event_7, true),
2779
+ onLostPointerCapture: (event_8) => end(event_8, true),
1739
2780
  children: [
1740
2781
  /* @__PURE__ */ jsx("div", {
1741
2782
  className: "povio-canvas-mobile-tools",
@@ -1781,14 +2822,18 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1781
2822
  children: [
1782
2823
  /* @__PURE__ */ jsxs("header", {
1783
2824
  className: "povio-canvas-node-header",
1784
- onPointerDown: (event_8) => {
1785
- if (event_8.button !== 0 || event_8.target.closest("button")) return;
1786
- event_8.stopPropagation();
1787
- viewport.current?.setPointerCapture(event_8.pointerId);
2825
+ onDragStart: (event_9) => event_9.preventDefault(),
2826
+ onPointerDown: (event_10) => {
2827
+ if (event_10.button !== 0 || event_10.target.closest("button")) return;
2828
+ event_10.preventDefault();
2829
+ event_10.stopPropagation();
2830
+ viewport.current?.setPointerCapture(event_10.pointerId);
1788
2831
  pointer.current = {
1789
- id: event_8.pointerId,
1790
- x: event_8.clientX,
1791
- y: event_8.clientY,
2832
+ id: event_10.pointerId,
2833
+ x: event_10.clientX,
2834
+ y: event_10.clientY,
2835
+ lastX: event_10.clientX,
2836
+ lastY: event_10.clientY,
1792
2837
  startX: 0,
1793
2838
  startY: 0,
1794
2839
  node: node_5.id,
@@ -1827,12 +2872,12 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1827
2872
  }),
1828
2873
  !node_5.href ? /* @__PURE__ */ jsxs("form", {
1829
2874
  className: "povio-canvas-unresolved",
1830
- onSubmit: (event_9) => {
1831
- event_9.preventDefault();
1832
- const form = new FormData(event_9.currentTarget);
1833
- const enteredParams = Object.fromEntries(node_5.missingParams.map((key_2) => {
1834
- const value_3 = form.get(key_2);
1835
- return [key_2, typeof value_3 === "string" ? value_3 : ""];
2875
+ onSubmit: (event_11) => {
2876
+ event_11.preventDefault();
2877
+ const form = new FormData(event_11.currentTarget);
2878
+ const enteredParams = Object.fromEntries(node_5.missingParams.map((key_4) => {
2879
+ const value_4 = form.get(key_4);
2880
+ return [key_4, typeof value_4 === "string" ? value_4 : ""];
1836
2881
  }));
1837
2882
  setManifest((m_0) => ({
1838
2883
  ...m_0,
@@ -1853,10 +2898,10 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1853
2898
  children: [
1854
2899
  /* @__PURE__ */ jsx("strong", { children: "Route needs parameters" }),
1855
2900
  /* @__PURE__ */ jsx("code", { children: node_5.path }),
1856
- node_5.missingParams.map((key_3) => /* @__PURE__ */ jsxs("label", { children: [key_3, /* @__PURE__ */ jsx("input", {
1857
- name: key_3,
2901
+ node_5.missingParams.map((key_5) => /* @__PURE__ */ jsxs("label", { children: [key_5, /* @__PURE__ */ jsx("input", {
2902
+ name: key_5,
1858
2903
  required: true
1859
- })] }, key_3)),
2904
+ })] }, key_5)),
1860
2905
  /* @__PURE__ */ jsx("button", { children: "Render page" })
1861
2906
  ]
1862
2907
  }) : originError ? /* @__PURE__ */ jsx("p", {
@@ -1887,25 +2932,26 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1887
2932
  className: "povio-canvas-frame-cover",
1888
2933
  tabIndex: 0,
1889
2934
  "aria-label": `Select ${node_5.title}`,
1890
- onKeyDown: (event_10) => {
1891
- if (event_10.key === "Enter" || event_10.key === " ") {
1892
- event_10.preventDefault();
2935
+ onKeyDown: (event_12) => {
2936
+ if (event_12.key === "Enter" || event_12.key === " ") {
2937
+ event_12.preventDefault();
1893
2938
  setSelected(node_5.id);
1894
2939
  }
1895
2940
  },
1896
- onPointerMove: (event_11) => {
2941
+ onPointerMove: (event_13) => {
1897
2942
  const frame_4 = frames.current.get(node_5.id);
1898
- if (frame_4) inspector.hover(node_5.id, frame_4, event_11.clientX, event_11.clientY);
2943
+ if (frame_4) inspector.hover(node_5.id, frame_4, event_13.clientX, event_13.clientY);
1899
2944
  },
1900
- onPointerDown: (event_12) => {
2945
+ onPointerDown: (event_14) => {
1901
2946
  setSelected(node_5.id);
1902
2947
  if (inspectMode) {
1903
2948
  const frame_5 = frames.current.get(node_5.id);
1904
- if (frame_5) inspector.select(node_5.id, frame_5, event_12.clientX, event_12.clientY);
2949
+ if (frame_5) inspector.select(node_5.id, frame_5, event_14.clientX, event_14.clientY);
1905
2950
  }
1906
2951
  },
1907
2952
  onClick: () => setSelected(node_5.id)
1908
2953
  }),
2954
+ hoveredLayer?.pageId === node_5.id && /* @__PURE__ */ jsx(CanvasLayerHighlight, { element: hoveredLayer.element }),
1909
2955
  inspectMode && inspector.selection?.pageId === node_5.id && inspector.measurement && /* @__PURE__ */ jsx(CanvasInspectionOverlay, {
1910
2956
  measurement: inspector.measurement,
1911
2957
  width: node_5.width
@@ -1923,6 +2969,10 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1923
2969
  }, node_5.id);
1924
2970
  })
1925
2971
  }),
2972
+ !nodes.length && /* @__PURE__ */ jsx("div", {
2973
+ className: "povio-canvas-empty",
2974
+ children: storage.ready ? "Enable pages in this workspace to start arranging your canvas." : "Loading workspace…"
2975
+ }),
1926
2976
  /* @__PURE__ */ jsxs("div", {
1927
2977
  className: "povio-canvas-tool-dock",
1928
2978
  children: [
@@ -1968,10 +3018,13 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1968
3018
  "aria-label": "Auto layout",
1969
3019
  onClick: () => setManifest((m_1) => ({
1970
3020
  ...m_1,
1971
- positions: {
1972
- ...m_1.positions,
1973
- [bp.id]: {}
1974
- }
3021
+ workspaces: m_1.workspaces.map((item_3) => item_3.id === m_1.activeWorkspaceId ? {
3022
+ ...item_3,
3023
+ positions: {
3024
+ ...item_3.positions,
3025
+ [bp.id]: {}
3026
+ }
3027
+ } : item_3)
1975
3028
  })),
1976
3029
  children: [/* @__PURE__ */ jsx(PanelsTopLeft, { size: 17 }), /* @__PURE__ */ jsx("span", { children: "Auto layout" })]
1977
3030
  })
@@ -1995,29 +3048,33 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
1995
3048
  title: "Toggle page theme",
1996
3049
  children: dark ? /* @__PURE__ */ jsx(Moon, { size: 16 }) : /* @__PURE__ */ jsx(Sun, { size: 16 })
1997
3050
  }),
1998
- /* @__PURE__ */ jsxs("div", {
1999
- className: "povio-canvas-breakpoint",
2000
- children: [/* @__PURE__ */ jsx(Monitor, { size: 15 }), /* @__PURE__ */ jsx("select", {
2001
- "aria-label": "Canvas breakpoint",
3051
+ /* @__PURE__ */ jsx("div", {
3052
+ className: "povio-canvas-breakpoint povio-canvas-workspace-select",
3053
+ children: /* @__PURE__ */ jsx(Select, {
3054
+ label: "Canvas breakpoint",
3055
+ hideLabel: true,
3056
+ size: "small",
3057
+ variant: "outlined",
3058
+ as: "default",
3059
+ className: "povio-canvas-workspace-input",
3060
+ selectionMode: "single",
3061
+ isClearable: false,
3062
+ renderStaticInput: false,
3063
+ leadingContent: /* @__PURE__ */ jsx(Monitor, { size: 15 }),
2002
3064
  value: bp.id,
2003
- onChange: (e) => {
2004
- if (e.target.value === bp.id) return;
3065
+ items: breakpoints.map((item_4) => ({
3066
+ id: item_4.id,
3067
+ label: `${item_4.label} · ${item_4.width}px`
3068
+ })),
3069
+ onChange: (id_5) => {
3070
+ if (id_5 === null || id_5 === bp.id) return;
2005
3071
  setRuntime({});
2006
3072
  setManifest((m_2) => ({
2007
3073
  ...m_2,
2008
- breakpoint: e.target.value
3074
+ breakpoint: String(id_5)
2009
3075
  }));
2010
- },
2011
- children: breakpoints.map((item_1) => /* @__PURE__ */ jsxs("option", {
2012
- value: item_1.id,
2013
- children: [
2014
- item_1.label,
2015
- " · ",
2016
- item_1.width,
2017
- "px"
2018
- ]
2019
- }, item_1.id))
2020
- })]
3076
+ }
3077
+ })
2021
3078
  }),
2022
3079
  /* @__PURE__ */ jsx("span", { className: "povio-canvas-dock-divider" }),
2023
3080
  /* @__PURE__ */ jsx("button", {
@@ -2050,7 +3107,8 @@ function AppCanvas({ router, examples = emptyExamples, exclude = emptyExclude, e
2050
3107
  }),
2051
3108
  inspectMode && /* @__PURE__ */ jsx(CanvasInspectorPanel, {
2052
3109
  inspector,
2053
- onClose: () => setInspectMode(false)
3110
+ onClose: () => setInspectMode(false),
3111
+ onSelectToken: configuration.revealToken
2054
3112
  })
2055
3113
  ]
2056
3114
  }),
@@ -2351,9 +3409,12 @@ function AppCanvasFrameBridge({ href }) {
2351
3409
  //#endregion
2352
3410
  //#region src/app-canvas/AppCanvasPage.tsx
2353
3411
  const APP_CANVAS_PATH = "/_canvas";
3412
+ /** Must match `componentsFrameParam`; kept inline so the canvas page does not load the gallery eagerly. */
3413
+ const componentsParam = "povio-components";
3414
+ const CanvasComponentsFrame = lazy(() => import("./CanvasComponentsFrame-CZXY0Dxi.js"));
2354
3415
  /** Ready-made route page. Mount beneath the app's UIConfigurationProvider. */
2355
3416
  function AppCanvasPage(t0) {
2356
- const $ = c(17);
3417
+ const $ = c(24);
2357
3418
  let excludePaths;
2358
3419
  let props;
2359
3420
  let t1;
@@ -2369,53 +3430,88 @@ function AppCanvasPage(t0) {
2369
3430
  t1 = $[3];
2370
3431
  }
2371
3432
  const canvasPath = t1 === void 0 ? APP_CANVAS_PATH : t1;
3433
+ const updateTheme = ThemeContext.useTheme()?.updateTheme;
2372
3434
  let t2;
2373
- if ($[4] !== excludePaths) {
2374
- t2 = excludePaths ?? [];
2375
- $[4] = excludePaths;
2376
- $[5] = t2;
2377
- } else t2 = $[5];
2378
3435
  let t3;
2379
- if ($[6] !== canvasPath || $[7] !== t2) {
2380
- t3 = /* @__PURE__ */ new Set([...t2, canvasPath]);
2381
- $[6] = canvasPath;
2382
- $[7] = t2;
2383
- $[8] = t3;
2384
- } else t3 = $[8];
3436
+ if ($[4] !== updateTheme) {
3437
+ t2 = () => {
3438
+ if (!isAppCanvasFrame()) updateTheme?.("light");
3439
+ };
3440
+ t3 = [updateTheme];
3441
+ $[4] = updateTheme;
3442
+ $[5] = t2;
3443
+ $[6] = t3;
3444
+ } else {
3445
+ t2 = $[5];
3446
+ t3 = $[6];
3447
+ }
3448
+ useEffect(t2, t3);
2385
3449
  let t4;
2386
- if ($[9] !== t3) {
2387
- t4 = [...t3];
2388
- $[9] = t3;
2389
- $[10] = t4;
2390
- } else t4 = $[10];
2391
- const exclusions = t4;
2392
- if (isAppCanvasFrame()) return null;
3450
+ if ($[7] !== excludePaths) {
3451
+ t4 = excludePaths ?? [];
3452
+ $[7] = excludePaths;
3453
+ $[8] = t4;
3454
+ } else t4 = $[8];
2393
3455
  let t5;
2394
- if ($[11] !== exclusions || $[12] !== props) {
2395
- t5 = /* @__PURE__ */ jsx(AppCanvas, {
2396
- ...props,
2397
- excludePaths: exclusions
2398
- });
2399
- $[11] = exclusions;
2400
- $[12] = props;
2401
- $[13] = t5;
2402
- } else t5 = $[13];
3456
+ if ($[9] !== canvasPath || $[10] !== t4) {
3457
+ t5 = /* @__PURE__ */ new Set([...t4, canvasPath]);
3458
+ $[9] = canvasPath;
3459
+ $[10] = t4;
3460
+ $[11] = t5;
3461
+ } else t5 = $[11];
2403
3462
  let t6;
2404
- if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
2405
- t6 = /* @__PURE__ */ jsx(UIConfigurationModal, {});
2406
- $[14] = t6;
2407
- } else t6 = $[14];
3463
+ if ($[12] !== t5) {
3464
+ t6 = [...t5];
3465
+ $[12] = t5;
3466
+ $[13] = t6;
3467
+ } else t6 = $[13];
3468
+ const exclusions = t6;
3469
+ if (isAppCanvasFrame()) {
3470
+ let t7;
3471
+ if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
3472
+ const group = new URLSearchParams(window.location.search).get(componentsParam);
3473
+ t7 = group ? /* @__PURE__ */ jsx(Suspense, {
3474
+ fallback: null,
3475
+ children: /* @__PURE__ */ jsx(CanvasComponentsFrame, { group })
3476
+ }) : null;
3477
+ $[14] = t7;
3478
+ } else t7 = $[14];
3479
+ return t7;
3480
+ }
2408
3481
  let t7;
2409
- if ($[15] !== t5) {
2410
- t7 = /* @__PURE__ */ jsxs("div", {
3482
+ if ($[15] !== canvasPath) {
3483
+ t7 = (group_0) => `${canvasPath}?${componentsParam}=${encodeURIComponent(group_0)}`;
3484
+ $[15] = canvasPath;
3485
+ $[16] = t7;
3486
+ } else t7 = $[16];
3487
+ let t8;
3488
+ if ($[17] !== exclusions || $[18] !== props || $[19] !== t7) {
3489
+ t8 = /* @__PURE__ */ jsx(AppCanvas, {
3490
+ ...props,
3491
+ excludePaths: exclusions,
3492
+ componentsHref: t7
3493
+ });
3494
+ $[17] = exclusions;
3495
+ $[18] = props;
3496
+ $[19] = t7;
3497
+ $[20] = t8;
3498
+ } else t8 = $[20];
3499
+ let t9;
3500
+ if ($[21] === Symbol.for("react.memo_cache_sentinel")) {
3501
+ t9 = /* @__PURE__ */ jsx(UIConfigurationModal, {});
3502
+ $[21] = t9;
3503
+ } else t9 = $[21];
3504
+ let t10;
3505
+ if ($[22] !== t8) {
3506
+ t10 = /* @__PURE__ */ jsxs("div", {
2411
3507
  className: "povio-app-canvas-page",
2412
3508
  "aria-label": "App canvas page",
2413
- children: [t5, t6]
3509
+ children: [t8, t9]
2414
3510
  });
2415
- $[15] = t5;
2416
- $[16] = t7;
2417
- } else t7 = $[16];
2418
- return t7;
3511
+ $[22] = t8;
3512
+ $[23] = t10;
3513
+ } else t10 = $[23];
3514
+ return t10;
2419
3515
  }
2420
3516
  //#endregion
2421
3517
  export { defaultCanvasBreakpoints as a, layoutCanvasPages as c, AppCanvas as i, AppCanvasPage as n, isAppCanvasFrame as o, AppCanvasFrameBridge as r, discoverCanvasPages as s, APP_CANVAS_PATH as t };