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

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.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/dist/{AppCanvasPage-DCjiFfQW.js → AppCanvasPage-DtWEvs_Y.js} +576 -847
  3. package/dist/{CanvasComponentsFrame-CZXY0Dxi.js → CanvasComponentsFrame-CbgJJv-3.js} +1 -1
  4. package/dist/{ComponentGallery-D4U5hXZL.js → ComponentGallery-COqEqqN6.js} +2 -2
  5. package/dist/{Form-DmcByQts.d.ts → Form-DAtj2szV.d.ts} +1 -1
  6. package/dist/{Inputs-C0sUmuPv.d.ts → Inputs-C_6dVwC7.d.ts} +1 -1
  7. package/dist/{Table-BN2_J_Ne.js → Table-piX1SWvN.js} +54 -1
  8. package/dist/{UIConfigurator-CBU0G6rb.js → UIConfigurator-DekPXjv9.js} +1131 -273
  9. package/dist/config/app-canvas-tanstack.js +1 -1
  10. package/dist/config/app-canvas.d.ts +1 -1
  11. package/dist/config/app-canvas.js +1 -1
  12. package/dist/config/ui-config.d.ts +1 -1
  13. package/dist/config/ui-configurator-vite.d.ts +7 -0
  14. package/dist/config/ui-configurator-vite.js +327 -177
  15. package/dist/config/ui-configurator.d.ts +14 -2
  16. package/dist/config/ui-configurator.js +2 -2
  17. package/dist/{configuration.context-iuZu-ie0.d.ts → configuration.context-3I7-wBII.d.ts} +3 -2
  18. package/dist/{dynamicInputs-DdcsSoYu.d.ts → dynamicInputs-R5i7AT_C.d.ts} +1 -1
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/rhf/inputs.d.ts +1 -1
  22. package/dist/rhf.d.ts +2 -2
  23. package/dist/rhf.js +3 -3
  24. package/dist/styles/app-canvas.css +15 -42
  25. package/dist/styles/configurator.css +251 -92
  26. package/dist/{table.cva-D5oQ-QvA.js → table.cva-DlMT-zLD.js} +1 -54
  27. package/dist/tanstack/form.d.ts +1 -1
  28. package/dist/tanstack/inputs.d.ts +2 -2
  29. package/dist/tanstack/table.js +2 -2
  30. package/dist/tanstack.d.ts +4 -4
  31. package/dist/tanstack.js +3 -3
  32. package/dist/{uiConfig.context-BqKWd_MO.d.ts → uiConfig.context-BksnNQyw.d.ts} +1 -1
  33. package/dist/{useTableColumnConfig-6zn3zKl3.js → useTableColumnConfig-CNJ_ZvIN.js} +1 -1
  34. package/package.json +1 -1
  35. package/src/configurator/README.md +18 -1
@@ -1,154 +1,23 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { lstat, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
3
- import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
- //#region src/app-canvas/manifest.ts
5
- function createCanvasWorkspace(name, id) {
6
- return {
7
- id,
8
- name: name.trim() || "Workspace",
9
- enabledPages: null,
10
- positions: {}
11
- };
12
- }
13
- function createCanvasManifest() {
14
- return {
15
- version: 2,
16
- workspaces: [createCanvasWorkspace("All pages", "default")],
17
- activeWorkspaceId: "default",
18
- examples: {},
19
- breakpoint: "desktop"
20
- };
21
- }
22
- function isRecord(value) {
23
- return value !== null && typeof value === "object" && !Array.isArray(value);
24
- }
25
- function isJSON(value, ancestors = /* @__PURE__ */ new Set(), depth = 0) {
26
- if (value === null || typeof value === "string" || typeof value === "boolean") return true;
27
- if (typeof value === "number") return Number.isFinite(value);
28
- if (typeof value !== "object" || depth > 30 || ancestors.has(value)) return false;
29
- ancestors.add(value);
30
- const valid = Object.values(value).every((child) => isJSON(child, ancestors, depth + 1));
31
- ancestors.delete(value);
32
- return valid;
33
- }
34
- function safeJSON(value, ancestors = /* @__PURE__ */ new Set(), depth = 0) {
35
- if (value === null || typeof value === "string" || typeof value === "boolean") return true;
36
- if (typeof value === "number") return Number.isFinite(value);
37
- if (typeof value !== "object" || depth > 30 || ancestors.has(value)) return false;
38
- if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return false;
39
- ancestors.add(value);
40
- const valid = Object.entries(value).every(([key, child]) => ![
41
- "__proto__",
42
- "prototype",
43
- "constructor"
44
- ].includes(key) && safeJSON(child, ancestors, depth + 1));
45
- ancestors.delete(value);
46
- return valid;
47
- }
48
- const stringMap = (value) => isRecord(value) && Object.values(value).every((item) => typeof item === "string");
49
- const variantValue = (value) => value === null || typeof value === "string" || typeof value === "boolean";
50
- function validCvaEdits(value) {
51
- return isRecord(value) && Object.entries(value).every(([slot, definition]) => {
52
- 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;
53
- const config = definition.config;
54
- 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"))));
55
- });
56
- }
57
- /** Returns an independent JSON configuration, or undefined for invalid/unset overrides. */
58
- function parseCanvasConfiguration(value) {
59
- if (!isRecord(value) || !safeJSON(value) || !isRecord(value.theme) || ![
60
- "light",
61
- "dark",
62
- "theme"
63
- ].every((key) => stringMap(value.theme && value.theme[key])) || !isRecord(value.config) || value.overrides !== void 0 && !validCvaEdits(value.overrides)) return void 0;
64
- return JSON.parse(JSON.stringify(value));
65
- }
66
- function parsePositions(input) {
67
- return Object.fromEntries(Object.entries(isRecord(input) ? input : {}).flatMap(([breakpoint, entries]) => {
68
- if (!isRecord(entries)) return [];
69
- 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, {
70
- x: point.x,
71
- y: point.y
72
- }]] : []))]];
73
- }));
74
- }
75
- /** Sanitize either stored JSON text or an imported value, migrating v1 positions into the default workspace. */
76
- function parseCanvasManifest(input) {
77
- const fallback = createCanvasManifest();
78
- let value = input;
79
- if (typeof value === "string") try {
80
- value = JSON.parse(value);
81
- } catch {
82
- return fallback;
83
- }
84
- if (!isRecord(value) || value.version !== 1 && value.version !== 2) return fallback;
85
- const examples = Object.fromEntries(Object.entries(isRecord(value.examples) ? value.examples : {}).flatMap(([id, example]) => {
86
- if (!isRecord(example)) return [];
87
- const clean = {};
88
- if (typeof example.title === "string") clean.title = example.title;
89
- if (isRecord(example.params)) clean.params = Object.fromEntries(Object.entries(example.params).filter((entry) => typeof entry[1] === "string"));
90
- if (isRecord(example.search)) clean.search = Object.fromEntries(Object.entries(example.search).filter(([, item]) => isJSON(item)));
91
- return [[id, clean]];
92
- }));
93
- const workspaces = [];
94
- const ids = /* @__PURE__ */ new Set();
95
- if (value.version === 1) workspaces.push({
96
- ...createCanvasWorkspace("All pages", "default"),
97
- positions: parsePositions(value.positions)
98
- });
99
- else if (Array.isArray(value.workspaces)) for (const entry of value.workspaces) {
100
- if (!isRecord(entry) || typeof entry.id !== "string" || !entry.id.trim() || ids.has(entry.id)) continue;
101
- ids.add(entry.id);
102
- const configuration = parseCanvasConfiguration(entry.configuration);
103
- workspaces.push({
104
- id: entry.id,
105
- name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : "Workspace",
106
- enabledPages: Array.isArray(entry.enabledPages) ? [...new Set(entry.enabledPages.filter((id) => typeof id === "string" && id.length > 0))] : null,
107
- positions: parsePositions(entry.positions),
108
- ...configuration ? { configuration } : {}
109
- });
110
- }
111
- if (!workspaces.length) workspaces.push(createCanvasWorkspace("All pages", "default"));
112
- return {
113
- version: 2,
114
- workspaces,
115
- activeWorkspaceId: typeof value.activeWorkspaceId === "string" && workspaces.some((workspace) => workspace.id === value.activeWorkspaceId) ? value.activeWorkspaceId : workspaces[0].id,
116
- examples,
117
- breakpoint: typeof value.breakpoint === "string" && value.breakpoint.length > 0 ? value.breakpoint : fallback.breakpoint
118
- };
119
- }
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ //#region src/configurator/repository-storage-protocol.ts
5
+ const componentConfigurationMetaName = "povio-ui-component-configuration";
6
+ /** Dev-server event sent when a repository storage file changes, so other tabs reload it. */
7
+ const repositoryStorageChangedEvent = "povio-ui:repository-storage-changed";
120
8
  //#endregion
121
- //#region src/app-canvas/storage-protocol.ts
122
- const canvasStorageMetaName = "povio-ui-canvas-storage";
123
- //#endregion
124
- //#region src/app-canvas/storage-vite.ts
9
+ //#region src/configurator/repository-storage-vite.ts
125
10
  var StorageError = class extends Error {
126
11
  constructor(message, status) {
127
12
  super(message);
128
13
  this.status = status;
129
14
  }
130
15
  };
131
- const maxBytes = 262144;
132
16
  const inside = (root, path) => {
133
17
  const rel = relative(root, path);
134
18
  return !!rel && !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
135
19
  };
136
- const record = (value) => !!value && typeof value === "object" && !Array.isArray(value);
137
- function validPositions(value) {
138
- return record(value) && Object.values(value).every((entries) => record(entries) && Object.values(entries).every((position) => record(position) && typeof position.x === "number" && Number.isFinite(position.x) && typeof position.y === "number" && Number.isFinite(position.y)));
139
- }
140
- function decodeManifest(value) {
141
- if (!record(value) || ![1, 2].includes(Number(value.version)) || typeof value.breakpoint !== "string" || !record(value.examples)) throw new StorageError("Invalid canvas manifest.", 400);
142
- if (value.version === 1) {
143
- if (!validPositions(value.positions)) throw new StorageError("Invalid canvas positions.", 400);
144
- } else if (value.version !== 2 || !Array.isArray(value.workspaces) || !value.workspaces.length || typeof value.activeWorkspaceId !== "string" || !value.workspaces.every((workspace) => record(workspace) && typeof workspace.id === "string" && workspace.id.length > 0 && typeof workspace.name === "string" && (workspace.enabledPages === null || Array.isArray(workspace.enabledPages) && workspace.enabledPages.every((id) => typeof id === "string")) && validPositions(workspace.positions) && (workspace.configuration === void 0 || parseCanvasConfiguration(workspace.configuration) !== void 0))) throw new StorageError("Invalid canvas workspaces.", 400);
145
- if (!Object.values(value.examples).every((example) => record(example) && (example.title === void 0 || typeof example.title === "string") && (example.params === void 0 || record(example.params) && Object.values(example.params).every((param) => typeof param === "string")) && (example.search === void 0 || record(example.search)))) throw new StorageError("Invalid canvas examples.", 400);
146
- if (value.version === 2) {
147
- const workspaces = value.workspaces;
148
- if (new Set(workspaces.map((workspace) => workspace.id)).size !== workspaces.length || !workspaces.some((workspace) => workspace.id === value.activeWorkspaceId)) throw new StorageError("Invalid canvas workspace identity.", 400);
149
- }
150
- return parseCanvasManifest(value);
151
- }
20
+ const record$2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
152
21
  const revisionOf = (text) => createHash("sha256").update(text).digest("hex");
153
22
  const respond$1 = (res, status, body) => {
154
23
  res.writeHead(status, {
@@ -157,32 +26,33 @@ const respond$1 = (res, status, body) => {
157
26
  });
158
27
  res.end(JSON.stringify(body));
159
28
  };
160
- async function readBody(req) {
29
+ async function readBody(req, label, maxBytes) {
161
30
  let size = 0;
162
31
  const chunks = [];
163
32
  for await (const chunk of req) {
164
33
  size += Buffer.byteLength(chunk);
165
- if (size > maxBytes) throw new StorageError("Canvas manifest exceeds 256 KB.", 413);
34
+ if (size > maxBytes) throw new StorageError(`${label} file exceeds ${maxBytes / 1024} KB.`, 413);
166
35
  chunks.push(Buffer.from(chunk));
167
36
  }
168
37
  try {
169
38
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
170
39
  } catch {
171
- throw new StorageError("Expected JSON canvas data.", 400);
40
+ throw new StorageError(`Expected JSON ${label.toLowerCase()} data.`, 400);
172
41
  }
173
42
  }
174
43
  /** Fixed-path development persistence only. Never commits or pushes repository changes. */
175
- async function configureCanvasStorage(server, filename = "povio.canvas.json") {
44
+ async function configureRepositoryStorage(server, { filename, route, field, label, decode, maxBytes = 262144 }) {
45
+ const noun = label.toLowerCase();
176
46
  const root = await realpath(server.config.root);
177
47
  const filePath = resolve(root, filename);
178
48
  const parent = dirname(filePath);
179
- if (!inside(root, filePath) || !filePath.endsWith(".json")) throw new Error("Povio UI canvasFile must be a JSON file inside this app's Vite root.");
49
+ if (!inside(root, filePath) || !filePath.endsWith(".json")) throw new Error(`Povio UI ${noun} file must be a JSON file inside this app's Vite root.`);
180
50
  const guard = async () => {
181
- if (await realpath(parent) !== parent) throw new StorageError("Canvas file parent cannot be a symbolic link.", 409);
51
+ if (await realpath(parent) !== parent) throw new StorageError(`${label} file parent cannot be a symbolic link.`, 409);
182
52
  try {
183
53
  const stat = await lstat(filePath);
184
- if (stat.isSymbolicLink() || !stat.isFile()) throw new StorageError("Canvas file must be a regular file, not a symbolic link.", 409);
185
- if (stat.size > maxBytes) throw new StorageError("Canvas manifest exceeds 256 KB.", 413);
54
+ if (stat.isSymbolicLink() || !stat.isFile()) throw new StorageError(`${label} file must be a regular file, not a symbolic link.`, 409);
55
+ if (stat.size > maxBytes) throw new StorageError(`${label} file exceeds ${maxBytes / 1024} KB.`, 413);
186
56
  } catch (error) {
187
57
  if (error.code !== "ENOENT") throw error;
188
58
  }
@@ -190,7 +60,7 @@ async function configureCanvasStorage(server, filename = "povio.canvas.json") {
190
60
  await guard();
191
61
  const file = relative(root, filePath).split(sep).join("/");
192
62
  const connection = {
193
- endpoint: `${new URL(server.config.base, "http://localhost").pathname.replace(/\/?$/, "/")}__povio_ui/canvas`,
63
+ endpoint: `${new URL(server.config.base, "http://localhost").pathname.replace(/\/?$/, "/")}__povio_ui/${route}`,
194
64
  token: randomUUID(),
195
65
  file
196
66
  };
@@ -212,7 +82,7 @@ async function configureCanvasStorage(server, filename = "povio.canvas.json") {
212
82
  }
213
83
  };
214
84
  const handle = async (req, res) => {
215
- if (req.headers["x-povio-canvas-token"] !== connection.token) throw new StorageError("Invalid canvas storage token.", 403);
85
+ if (req.headers["x-povio-ui-storage-token"] !== connection.token) throw new StorageError(`Invalid ${noun} storage token.`, 403);
216
86
  const origin = req.headers.origin;
217
87
  let validOrigin = true;
218
88
  if (origin) try {
@@ -220,18 +90,18 @@ async function configureCanvasStorage(server, filename = "povio.canvas.json") {
220
90
  } catch {
221
91
  validOrigin = false;
222
92
  }
223
- if (!validOrigin || req.headers["sec-fetch-site"] === "cross-site") throw new StorageError("Canvas storage requires the app's origin.", 403);
93
+ if (!validOrigin || req.headers["sec-fetch-site"] === "cross-site") throw new StorageError(`${label} storage requires the app's origin.`, 403);
224
94
  if (req.method === "GET") {
225
95
  await pending;
226
96
  const current = await readCurrent();
227
- let manifest = null;
97
+ let value = null;
228
98
  if (current.text !== null) try {
229
- manifest = decodeManifest(JSON.parse(current.text));
99
+ value = decode(JSON.parse(current.text));
230
100
  } catch {
231
- throw new StorageError("Repository canvas file is invalid. Correct it before saving.", 409);
101
+ throw new StorageError(`Repository ${noun} file is invalid. Correct it before saving.`, 409);
232
102
  }
233
103
  return respond$1(res, 200, {
234
- manifest,
104
+ [field]: value,
235
105
  revision: current.revision,
236
106
  file
237
107
  });
@@ -241,13 +111,13 @@ async function configureCanvasStorage(server, filename = "povio.canvas.json") {
241
111
  throw new StorageError("Use GET or POST.", 405);
242
112
  }
243
113
  if (!req.headers["content-type"]?.startsWith("application/json")) throw new StorageError("Expected application/json.", 415);
244
- const body = await readBody(req);
245
- if (!record(body) || !(body.revision === null || typeof body.revision === "string")) throw new StorageError("Expected manifest and revision.", 400);
246
- const manifest = decodeManifest(body.manifest);
247
- const text = `${JSON.stringify(manifest, null, 2)}\n`;
248
- if (Buffer.byteLength(text) > maxBytes) throw new StorageError("Canvas manifest exceeds 256 KB.", 413);
114
+ const body = await readBody(req, label, maxBytes);
115
+ if (!record$2(body) || !(body.revision === null || typeof body.revision === "string")) throw new StorageError(`Expected ${field} and revision.`, 400);
116
+ const value = decode(body[field]);
117
+ const text = `${JSON.stringify(value, null, 2)}\n`;
118
+ if (Buffer.byteLength(text) > maxBytes) throw new StorageError(`${label} file exceeds ${maxBytes / 1024} KB.`, 413);
249
119
  const write = pending.then(async () => {
250
- if ((await readCurrent()).revision !== body.revision) throw new StorageError("Canvas file changed. Reload its saved layout before saving again.", 409);
120
+ if ((await readCurrent()).revision !== body.revision) throw new StorageError(`${label} file changed. Reload it before saving again.`, 409);
251
121
  const temporary = `${filePath}.povio-${randomUUID()}.tmp`;
252
122
  try {
253
123
  await writeFile(temporary, text, {
@@ -270,14 +140,257 @@ async function configureCanvasStorage(server, filename = "povio.canvas.json") {
270
140
  };
271
141
  server.middlewares.use((req, res, next) => {
272
142
  if (req.url?.split("?")[0] !== connection.endpoint) return next();
273
- handle(req, res).catch((error) => respond$1(res, error instanceof StorageError ? error.status : 500, { error: error instanceof Error ? error.message : "Could not access canvas file." }));
143
+ handle(req, res).catch((error) => respond$1(res, error instanceof StorageError ? error.status : 500, { error: error instanceof Error ? error.message : `Could not access ${noun} file.` }));
274
144
  });
145
+ const isFile = (path) => resolve(path) === filePath;
275
146
  return {
276
147
  connection,
277
- isStorageFile: (path) => path === filePath || path.startsWith(`${filePath}.povio-`)
148
+ isFile,
149
+ isStorageFile: (path) => isFile(path) || resolve(path).startsWith(`${filePath}.povio-`),
150
+ revisionOf
151
+ };
152
+ }
153
+ //#endregion
154
+ //#region src/app-canvas/manifest.ts
155
+ function createCanvasWorkspace(name, id) {
156
+ return {
157
+ id,
158
+ name: name.trim() || "Workspace",
159
+ enabledPages: null,
160
+ positions: {}
161
+ };
162
+ }
163
+ function createCanvasManifest() {
164
+ return {
165
+ version: 2,
166
+ workspaces: [createCanvasWorkspace("All pages", "default")],
167
+ activeWorkspaceId: "default",
168
+ examples: {},
169
+ breakpoint: "desktop"
170
+ };
171
+ }
172
+ function isRecord$1(value) {
173
+ return value !== null && typeof value === "object" && !Array.isArray(value);
174
+ }
175
+ function isJSON(value, ancestors = /* @__PURE__ */ new Set(), depth = 0) {
176
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
177
+ if (typeof value === "number") return Number.isFinite(value);
178
+ if (typeof value !== "object" || depth > 30 || ancestors.has(value)) return false;
179
+ ancestors.add(value);
180
+ const valid = Object.values(value).every((child) => isJSON(child, ancestors, depth + 1));
181
+ ancestors.delete(value);
182
+ return valid;
183
+ }
184
+ function parsePositions(input) {
185
+ return Object.fromEntries(Object.entries(isRecord$1(input) ? input : {}).flatMap(([breakpoint, entries]) => {
186
+ if (!isRecord$1(entries)) return [];
187
+ return [[breakpoint, Object.fromEntries(Object.entries(entries).flatMap(([id, point]) => isRecord$1(point) && typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y) ? [[id, {
188
+ x: point.x,
189
+ y: point.y
190
+ }]] : []))]];
191
+ }));
192
+ }
193
+ /** Sanitize either stored JSON text or an imported value, migrating v1 positions into the default workspace. */
194
+ function parseCanvasManifest(input) {
195
+ const fallback = createCanvasManifest();
196
+ let value = input;
197
+ if (typeof value === "string") try {
198
+ value = JSON.parse(value);
199
+ } catch {
200
+ return fallback;
201
+ }
202
+ if (!isRecord$1(value) || value.version !== 1 && value.version !== 2) return fallback;
203
+ const examples = Object.fromEntries(Object.entries(isRecord$1(value.examples) ? value.examples : {}).flatMap(([id, example]) => {
204
+ if (!isRecord$1(example)) return [];
205
+ const clean = {};
206
+ if (typeof example.title === "string") clean.title = example.title;
207
+ if (isRecord$1(example.params)) clean.params = Object.fromEntries(Object.entries(example.params).filter((entry) => typeof entry[1] === "string"));
208
+ if (isRecord$1(example.search)) clean.search = Object.fromEntries(Object.entries(example.search).filter(([, item]) => isJSON(item)));
209
+ return [[id, clean]];
210
+ }));
211
+ const workspaces = [];
212
+ const ids = /* @__PURE__ */ new Set();
213
+ if (value.version === 1) workspaces.push({
214
+ ...createCanvasWorkspace("All pages", "default"),
215
+ positions: parsePositions(value.positions)
216
+ });
217
+ else if (Array.isArray(value.workspaces)) for (const entry of value.workspaces) {
218
+ if (!isRecord$1(entry) || typeof entry.id !== "string" || !entry.id.trim() || ids.has(entry.id)) continue;
219
+ ids.add(entry.id);
220
+ workspaces.push({
221
+ id: entry.id,
222
+ name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : "Workspace",
223
+ enabledPages: Array.isArray(entry.enabledPages) ? [...new Set(entry.enabledPages.filter((id) => typeof id === "string" && id.length > 0))] : null,
224
+ positions: parsePositions(entry.positions)
225
+ });
226
+ }
227
+ if (!workspaces.length) workspaces.push(createCanvasWorkspace("All pages", "default"));
228
+ return {
229
+ version: 2,
230
+ workspaces,
231
+ activeWorkspaceId: typeof value.activeWorkspaceId === "string" && workspaces.some((workspace) => workspace.id === value.activeWorkspaceId) ? value.activeWorkspaceId : workspaces[0].id,
232
+ examples,
233
+ breakpoint: typeof value.breakpoint === "string" && value.breakpoint.length > 0 ? value.breakpoint : fallback.breakpoint
234
+ };
235
+ }
236
+ //#endregion
237
+ //#region src/app-canvas/storage-vite.ts
238
+ const record$1 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
239
+ function validPositions(value) {
240
+ return record$1(value) && Object.values(value).every((entries) => record$1(entries) && Object.values(entries).every((position) => record$1(position) && typeof position.x === "number" && Number.isFinite(position.x) && typeof position.y === "number" && Number.isFinite(position.y)));
241
+ }
242
+ function decodeManifest(value) {
243
+ if (!record$1(value) || ![1, 2].includes(Number(value.version)) || typeof value.breakpoint !== "string" || !record$1(value.examples)) throw new StorageError("Invalid canvas manifest.", 400);
244
+ if (value.version === 1) {
245
+ if (!validPositions(value.positions)) throw new StorageError("Invalid canvas positions.", 400);
246
+ } else if (value.version !== 2 || !Array.isArray(value.workspaces) || !value.workspaces.length || typeof value.activeWorkspaceId !== "string" || !value.workspaces.every((workspace) => record$1(workspace) && typeof workspace.id === "string" && workspace.id.length > 0 && typeof workspace.name === "string" && (workspace.enabledPages === null || Array.isArray(workspace.enabledPages) && workspace.enabledPages.every((id) => typeof id === "string")) && validPositions(workspace.positions))) throw new StorageError("Invalid canvas workspaces.", 400);
247
+ if (!Object.values(value.examples).every((example) => record$1(example) && (example.title === void 0 || typeof example.title === "string") && (example.params === void 0 || record$1(example.params) && Object.values(example.params).every((param) => typeof param === "string")) && (example.search === void 0 || record$1(example.search)))) throw new StorageError("Invalid canvas examples.", 400);
248
+ if (value.version === 2) {
249
+ const workspaces = value.workspaces;
250
+ if (new Set(workspaces.map((workspace) => workspace.id)).size !== workspaces.length || !workspaces.some((workspace) => workspace.id === value.activeWorkspaceId)) throw new StorageError("Invalid canvas workspace identity.", 400);
251
+ }
252
+ return parseCanvasManifest(value);
253
+ }
254
+ /** Fixed-path development persistence for canvas workspaces. Never commits or pushes repository changes. */
255
+ function configureCanvasStorage(server, filename = "povio.canvas.json") {
256
+ return configureRepositoryStorage(server, {
257
+ filename,
258
+ route: "canvas",
259
+ field: "manifest",
260
+ label: "Canvas",
261
+ decode: decodeManifest
262
+ });
263
+ }
264
+ //#endregion
265
+ //#region src/app-canvas/storage-protocol.ts
266
+ const canvasStorageMetaName = "povio-ui-canvas-storage";
267
+ //#endregion
268
+ //#region src/configurator/theme-versions.ts
269
+ const themeVersionsMetaName = "povio-ui-theme-versions";
270
+ function createThemeVersion(name, id, configuration) {
271
+ return {
272
+ id,
273
+ name: name.trim() || "Theme",
274
+ ...configuration ? { configuration: structuredClone(configuration) } : {}
275
+ };
276
+ }
277
+ function createThemeVersions() {
278
+ return {
279
+ version: 1,
280
+ versions: [createThemeVersion("Default", "default")],
281
+ activeVersionId: "default"
282
+ };
283
+ }
284
+ function isRecord(value) {
285
+ return value !== null && typeof value === "object" && !Array.isArray(value);
286
+ }
287
+ function safeJSON(value, ancestors = /* @__PURE__ */ new Set(), depth = 0) {
288
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
289
+ if (typeof value === "number") return Number.isFinite(value);
290
+ if (typeof value !== "object" || depth > 30 || ancestors.has(value)) return false;
291
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return false;
292
+ ancestors.add(value);
293
+ const valid = Object.entries(value).every(([key, child]) => ![
294
+ "__proto__",
295
+ "prototype",
296
+ "constructor"
297
+ ].includes(key) && safeJSON(child, ancestors, depth + 1));
298
+ ancestors.delete(value);
299
+ return valid;
300
+ }
301
+ const stringMap = (value) => isRecord(value) && Object.values(value).every((item) => typeof item === "string");
302
+ const variantValue = (value) => value === null || typeof value === "string" || typeof value === "boolean";
303
+ function validCvaEdits(value) {
304
+ return isRecord(value) && Object.entries(value).every(([slot, definition]) => {
305
+ 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;
306
+ const config = definition.config;
307
+ 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"))));
308
+ });
309
+ }
310
+ /** Returns an independent JSON configuration, or undefined for invalid/unset overrides. */
311
+ function parseUIConfiguration(value) {
312
+ if (!isRecord(value) || !safeJSON(value) || !isRecord(value.theme) || ![
313
+ "light",
314
+ "dark",
315
+ "theme"
316
+ ].every((key) => stringMap(value.theme && value.theme[key])) || !isRecord(value.config) || value.overrides !== void 0 && !validCvaEdits(value.overrides)) return void 0;
317
+ return JSON.parse(JSON.stringify(value));
318
+ }
319
+ /** Returns an independent component configuration, or undefined when invalid. */
320
+ function parseComponentConfiguration(value) {
321
+ if (!isRecord(value) || !safeJSON(value) || !isRecord(value.config) || value.overrides !== void 0 && !validCvaEdits(value.overrides)) return void 0;
322
+ const copy = JSON.parse(JSON.stringify(value));
323
+ return {
324
+ config: copy.config ?? {},
325
+ overrides: copy.overrides ?? {}
326
+ };
327
+ }
328
+ /** Sanitize stored JSON text or a repository value. */
329
+ function parseThemeVersions(input) {
330
+ const fallback = createThemeVersions();
331
+ let value = input;
332
+ if (typeof value === "string") try {
333
+ value = JSON.parse(value);
334
+ } catch {
335
+ return fallback;
336
+ }
337
+ if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.versions)) return fallback;
338
+ const versions = [];
339
+ const ids = /* @__PURE__ */ new Set();
340
+ for (const entry of value.versions) {
341
+ if (!isRecord(entry) || typeof entry.id !== "string" || !entry.id.trim() || ids.has(entry.id)) continue;
342
+ ids.add(entry.id);
343
+ const configuration = parseUIConfiguration(entry.configuration);
344
+ versions.push({
345
+ id: entry.id,
346
+ name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : "Theme",
347
+ ...configuration ? { configuration } : {}
348
+ });
349
+ }
350
+ if (!versions.length) return fallback;
351
+ return {
352
+ version: 1,
353
+ versions,
354
+ activeVersionId: typeof value.activeVersionId === "string" && versions.some((version) => version.id === value.activeVersionId) ? value.activeVersionId : versions[0].id
278
355
  };
279
356
  }
280
357
  //#endregion
358
+ //#region src/configurator/theme-versions-vite.ts
359
+ const record = (value) => !!value && typeof value === "object" && !Array.isArray(value);
360
+ function decodeThemeVersions(value) {
361
+ if (!record(value) || value.version !== 1 || !Array.isArray(value.versions) || !value.versions.length || typeof value.activeVersionId !== "string" || !value.versions.every((version) => record(version) && typeof version.id === "string" && version.id.length > 0 && typeof version.name === "string" && (version.configuration === void 0 || parseUIConfiguration(version.configuration) !== void 0))) throw new StorageError("Invalid theme versions.", 400);
362
+ const ids = value.versions.map((version) => version.id);
363
+ if (new Set(ids).size !== ids.length || !ids.includes(value.activeVersionId)) throw new StorageError("Invalid theme version identity.", 400);
364
+ return parseThemeVersions(value);
365
+ }
366
+ /** Theme versions shared through the repository, separate from canvas layout. Never commits or pushes. */
367
+ function configureThemeVersionsStorage(server, filename = "povio.themes.json") {
368
+ return configureRepositoryStorage(server, {
369
+ filename,
370
+ route: "themes",
371
+ field: "themes",
372
+ label: "Theme versions",
373
+ decode: decodeThemeVersions,
374
+ maxBytes: 4194304
375
+ });
376
+ }
377
+ //#endregion
378
+ //#region src/configurator/component-configuration-vite.ts
379
+ /** The component settings Apply theme writes beside the theme CSS. Hot updates reach the app like the CSS. */
380
+ function configureComponentConfigurationStorage(server, filename) {
381
+ return configureRepositoryStorage(server, {
382
+ filename,
383
+ route: "configuration",
384
+ field: "configuration",
385
+ label: "UI configuration",
386
+ decode: (value) => {
387
+ const configuration = parseComponentConfiguration(value);
388
+ if (!configuration) throw new StorageError("Invalid UI configuration.", 400);
389
+ return configuration;
390
+ }
391
+ });
392
+ }
393
+ //#endregion
281
394
  //#region src/configurator/theme-model.ts
282
395
  const declarations = (source) => Object.fromEntries([...source.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)].map(([, name, value]) => [name, value.trim()]));
283
396
  const selectorDeclarations = (source, selector) => Object.assign({}, ...[...source.matchAll(selector)].map((match) => declarations(match[1])));
@@ -384,17 +497,21 @@ function uiConfigurator(options = {}) {
384
497
  let connection;
385
498
  let pendingWrite = Promise.resolve();
386
499
  let canvasStorage;
500
+ let themeVersionsStorage;
501
+ let componentStorage;
387
502
  return {
388
503
  name: "vite-plugin-povio-ui-configurator",
389
504
  apply: (_config, environment) => environment.command === "serve" && !environment.isPreview,
390
505
  async configureServer(server) {
391
506
  if (options.canvasFile !== false) canvasStorage = await configureCanvasStorage(server, options.canvasFile);
507
+ if (options.themeVersionsFile !== false) themeVersionsStorage = await configureThemeVersionsStorage(server, options.themeVersionsFile);
392
508
  const root = await realpath(server.config.root);
393
509
  const configuredFile = resolve(server.config.root, options.themeFile ?? "src/styles/theme.css");
394
510
  const themeFile = await realpath(configuredFile);
395
511
  const file = relative(root, themeFile);
396
512
  if (!file || isAbsolute(file) || file === ".." || file.startsWith(`..${sep}`)) throw new Error("Povio UI themeFile must be inside this app's Vite root.");
397
513
  if (!themeFile.endsWith(".css")) throw new Error("Povio UI themeFile must be a CSS file.");
514
+ if (options.configurationFile !== false) componentStorage = await configureComponentConfigurationStorage(server, options.configurationFile ?? join(dirname(file), "ui-configuration.json"));
398
515
  const endpoint = `${new URL(server.config.base, "http://localhost").pathname.replace(/\/?$/, "/")}__povio_ui/theme`;
399
516
  connection = {
400
517
  endpoint,
@@ -436,26 +553,59 @@ function uiConfigurator(options = {}) {
436
553
  });
437
554
  });
438
555
  },
439
- handleHotUpdate(context) {
440
- if (canvasStorage?.isStorageFile(context.file)) return [];
556
+ async handleHotUpdate(context) {
557
+ const storage = [canvasStorage, themeVersionsStorage].find((item) => item?.isStorageFile(context.file));
558
+ if (!storage) return;
559
+ if (storage.isFile(context.file)) {
560
+ const text = await Promise.resolve(context.read()).catch(() => null);
561
+ const change = {
562
+ endpoint: storage.connection.endpoint,
563
+ revision: text ? storage.revisionOf(text) : null
564
+ };
565
+ context.server.ws.send({
566
+ type: "custom",
567
+ event: repositoryStorageChangedEvent,
568
+ data: change
569
+ });
570
+ }
571
+ return [];
441
572
  },
442
573
  transformIndexHtml() {
443
574
  if (!connection) return;
444
- return [{
445
- tag: "meta",
446
- attrs: {
447
- name: themeWriterMetaName,
448
- content: JSON.stringify(connection)
449
- },
450
- injectTo: "head"
451
- }, ...canvasStorage ? [{
452
- tag: "meta",
453
- attrs: {
454
- name: canvasStorageMetaName,
455
- content: JSON.stringify(canvasStorage.connection)
575
+ return [
576
+ {
577
+ tag: "meta",
578
+ attrs: {
579
+ name: themeWriterMetaName,
580
+ content: JSON.stringify(connection)
581
+ },
582
+ injectTo: "head"
456
583
  },
457
- injectTo: "head"
458
- }] : []];
584
+ ...canvasStorage ? [{
585
+ tag: "meta",
586
+ attrs: {
587
+ name: canvasStorageMetaName,
588
+ content: JSON.stringify(canvasStorage.connection)
589
+ },
590
+ injectTo: "head"
591
+ }] : [],
592
+ ...componentStorage ? [{
593
+ tag: "meta",
594
+ attrs: {
595
+ name: componentConfigurationMetaName,
596
+ content: JSON.stringify(componentStorage.connection)
597
+ },
598
+ injectTo: "head"
599
+ }] : [],
600
+ ...themeVersionsStorage ? [{
601
+ tag: "meta",
602
+ attrs: {
603
+ name: themeVersionsMetaName,
604
+ content: JSON.stringify(themeVersionsStorage.connection)
605
+ },
606
+ injectTo: "head"
607
+ }] : []
608
+ ];
459
609
  }
460
610
  };
461
611
  }
@@ -1,8 +1,20 @@
1
- import { a as CvaDefinition, i as useUIConfiguration, n as UIConfigurationProvider, o as CvaEdits, r as UIConfigurationProviderProps, t as UIConfiguration } from "../configuration.context-iuZu-ie0.js";
1
+ import { t as UIOverrides } from "../uiOverrides.context-DEp139He.js";
2
+ import { n as UIConfigOptions } from "../uiConfig.context-BksnNQyw.js";
3
+ import { a as CvaDefinition, i as useUIConfiguration, n as UIConfigurationProvider, o as CvaEdits, r as UIConfigurationProviderProps, t as UIConfiguration } from "../configuration.context-3I7-wBII.js";
2
4
  import { a as parseThemeCss, i as exportCss, n as ThemeEdits, r as Tokens, t as Mode } from "../theme-model-T1zIdBg8.js";
3
5
  //#region src/configurator/UIConfigurator.d.ts
4
6
  declare function UIConfigurationModal(): import("react").JSX.Element | null;
5
7
  //#endregion
8
+ //#region src/configurator/component-configuration.d.ts
9
+ /**
10
+ * The app's applied component settings: pass the JSON written by the configurator's Apply theme
11
+ * and the app's code-level overrides, which stay underneath the applied variant overrides.
12
+ */
13
+ declare function resolveUIConfiguration(file: unknown, baseOverrides?: UIOverrides.Config | null): {
14
+ config: Partial<Pick<UIConfigOptions, "button" | "input" | "renderStaticInput">>;
15
+ overrides: Partial<UIOverrides.Options>;
16
+ };
17
+ //#endregion
6
18
  //#region src/configurator/ColorModeSwitch.d.ts
7
19
  interface ColorModeSwitchProps {
8
20
  value: Mode;
@@ -12,4 +24,4 @@ interface ColorModeSwitchProps {
12
24
  /** Controlled color-mode switch, shared by configurators and application settings. */
13
25
  declare function ColorModeSwitch({ value, onChange, label }: ColorModeSwitchProps): import("react").JSX.Element;
14
26
  //#endregion
15
- export { ColorModeSwitch, type ColorModeSwitchProps, type CvaDefinition, type CvaEdits, type ThemeEdits, type Tokens, type UIConfiguration, UIConfigurationModal, UIConfigurationProvider, type UIConfigurationProviderProps, exportCss, parseThemeCss, useUIConfiguration };
27
+ export { ColorModeSwitch, type ColorModeSwitchProps, type CvaDefinition, type CvaEdits, type ThemeEdits, type Tokens, type UIConfiguration, UIConfigurationModal, UIConfigurationProvider, type UIConfigurationProviderProps, exportCss, parseThemeCss, resolveUIConfiguration, useUIConfiguration };
@@ -1,3 +1,3 @@
1
- import { f as useUIConfiguration, n as ColorModeSwitch, t as UIConfigurationModal, u as UIConfigurationProvider } from "../UIConfigurator-CBU0G6rb.js";
1
+ import { d as useUIConfiguration, i as resolveUIConfiguration, n as ColorModeSwitch, t as UIConfigurationModal, u as UIConfigurationProvider } from "../UIConfigurator-DekPXjv9.js";
2
2
  import { i as exportCss, v as parseThemeCss } from "../theme-model-DtTsIxou.js";
3
- export { ColorModeSwitch, UIConfigurationModal, UIConfigurationProvider, exportCss, parseThemeCss, useUIConfiguration };
3
+ export { ColorModeSwitch, UIConfigurationModal, UIConfigurationProvider, exportCss, parseThemeCss, resolveUIConfiguration, useUIConfiguration };