@drawnagency/primitives 0.1.38 → 0.1.39

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 (38) hide show
  1. package/dist/{chunk-OUFUUBZ4.js → chunk-GQV2554Z.js} +1 -1
  2. package/dist/{chunk-C2MVDXD7.js → chunk-I6ZPOEK2.js} +27 -8
  3. package/dist/{chunk-V43WVSVS.js → chunk-LW5EGJFM.js} +5 -2
  4. package/dist/{chunk-VCZBZMXU.js → chunk-TNHX35TE.js} +28 -10
  5. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  6. package/dist/hooks/useBuildStatus.d.ts.map +1 -1
  7. package/dist/hooks/useEditorPersistence.d.ts.map +1 -1
  8. package/dist/hooks/useEditorPublish.d.ts.map +1 -1
  9. package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
  10. package/dist/index.js +6 -4
  11. package/dist/lib/index.d.ts +1 -1
  12. package/dist/lib/index.d.ts.map +1 -1
  13. package/dist/lib/index.js +4 -2
  14. package/dist/lib/loader.d.ts.map +1 -1
  15. package/dist/lib/nav.d.ts.map +1 -1
  16. package/dist/lib/sanitize.d.ts +10 -0
  17. package/dist/lib/sanitize.d.ts.map +1 -1
  18. package/dist/lib/text.d.ts.map +1 -1
  19. package/dist/media/index.js +1 -1
  20. package/dist/media/queue.d.ts.map +1 -1
  21. package/dist/media/videoPoster.d.ts.map +1 -1
  22. package/dist/schemas/index.js +2 -2
  23. package/dist/schemas/site-config.d.ts.map +1 -1
  24. package/package.json +1 -1
  25. package/src/components/shell/EditorShell.tsx +2 -0
  26. package/src/components/shell/SiteSettingsModal.tsx +1 -1
  27. package/src/hooks/useBuildStatus.ts +13 -3
  28. package/src/hooks/useEditorPersistence.ts +19 -6
  29. package/src/hooks/useEditorPublish.ts +23 -6
  30. package/src/hooks/useMediaPipeline.ts +21 -6
  31. package/src/lib/index.ts +1 -1
  32. package/src/lib/loader.ts +3 -2
  33. package/src/lib/nav.ts +15 -3
  34. package/src/lib/sanitize.ts +22 -3
  35. package/src/lib/text.ts +5 -1
  36. package/src/media/queue.ts +3 -1
  37. package/src/media/videoPoster.ts +31 -10
  38. package/src/schemas/site-config.ts +6 -2
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HexColorSchema
3
- } from "./chunk-V43WVSVS.js";
3
+ } from "./chunk-LW5EGJFM.js";
4
4
 
5
5
  // src/schemas/audience.ts
6
6
  import { z } from "zod";
@@ -3,7 +3,7 @@ import {
3
3
  getAllSchemas,
4
4
  getSection,
5
5
  getSectionSchema
6
- } from "./chunk-V43WVSVS.js";
6
+ } from "./chunk-LW5EGJFM.js";
7
7
  import {
8
8
  safeNextPath
9
9
  } from "./chunk-S2L3BPLS.js";
@@ -17,7 +17,13 @@ function cn(...inputs) {
17
17
 
18
18
  // src/lib/nav.ts
19
19
  function toSectionId(text) {
20
- return text.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-");
20
+ const slug = text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/^-+|-+$/g, "");
21
+ if (slug) return slug;
22
+ let hash = 0;
23
+ for (let i = 0; i < text.length; i++) {
24
+ hash = (hash << 5) - hash + text.charCodeAt(i) | 0;
25
+ }
26
+ return `section-${Math.abs(hash).toString(36)}`;
21
27
  }
22
28
  function generateNavLinks(sections, registry) {
23
29
  const nav = [];
@@ -33,6 +39,10 @@ function generateNavLinks(sections, registry) {
33
39
  const role = lookupRole(section.type);
34
40
  if (!role) continue;
35
41
  if (role === "h1") {
42
+ if (content.excludeFromNav) {
43
+ currentParent = null;
44
+ continue;
45
+ }
36
46
  currentParent = {
37
47
  href: `#${toSectionId(content.heading)}`,
38
48
  label: content.heading,
@@ -77,15 +87,22 @@ function deriveContrast(hex) {
77
87
  }
78
88
 
79
89
  // src/lib/sanitize.ts
80
- var purifier;
90
+ var purifierPromise = null;
91
+ var purifier = null;
81
92
  if (typeof window !== "undefined") {
82
- import("dompurify").then((m) => {
83
- purifier = m.default;
93
+ purifierPromise = import("dompurify").then((mod) => {
94
+ const DOMPurify = mod.default ?? mod;
95
+ purifier = (html) => DOMPurify.sanitize(html);
96
+ return mod;
84
97
  });
85
98
  }
86
99
  function sanitizeHtml(html) {
87
100
  if (!html) return "";
88
- return purifier ? purifier.sanitize(html) : html;
101
+ return purifier ? purifier(html) : html;
102
+ }
103
+ async function ensureSanitizer() {
104
+ if (typeof window === "undefined") return;
105
+ if (!purifier && purifierPromise) await purifierPromise;
89
106
  }
90
107
 
91
108
  // src/lib/grid.ts
@@ -212,14 +229,15 @@ var historySelectEvent = createEvent("history-select");
212
229
  function mergeSiteContent(index, sectionFiles) {
213
230
  const sections = [];
214
231
  const canValidate = getAllSchemas().length >= 2;
232
+ const schema = canValidate ? getSectionSchema() : null;
215
233
  for (const id of index.order) {
216
234
  const raw = sectionFiles[id];
217
235
  if (!raw) {
218
236
  console.warn(`Section file missing for id: ${id}, skipping`);
219
237
  continue;
220
238
  }
221
- if (canValidate) {
222
- const result = getSectionSchema().safeParse(raw);
239
+ if (canValidate && schema) {
240
+ const result = schema.safeParse(raw);
223
241
  if (!result.success) {
224
242
  const type = raw.type ?? "unknown";
225
243
  console.warn(`Skipping section "${id}" (type: ${type}): invalid schema`);
@@ -263,6 +281,7 @@ export {
263
281
  generateNavLinks,
264
282
  deriveContrast,
265
283
  sanitizeHtml,
284
+ ensureSanitizer,
266
285
  gridColsClass,
267
286
  curatedIcons,
268
287
  getIcon,
@@ -167,8 +167,11 @@ var IndexSchema = z3.object({
167
167
  sections: z3.record(z3.string(), SectionMetaSchema),
168
168
  lastModified: z3.string().nullable().optional()
169
169
  }).refine(
170
- (data) => data.order.every((id) => id in data.sections),
171
- { message: "All order entries must have a corresponding section in sections" }
170
+ (data) => {
171
+ const orderSet = new Set(data.order);
172
+ return data.order.every((id) => id in data.sections) && Object.keys(data.sections).every((key) => orderSet.has(key));
173
+ },
174
+ { message: "Every id in order must have a sections entry and vice versa" }
172
175
  );
173
176
  var SiteConfigSchema = z3.object({
174
177
  siteName: z3.string().default("Brand Portal"),
@@ -149,7 +149,9 @@ var ProcessingQueue = class {
149
149
  this.activeWorkers.delete(id);
150
150
  }
151
151
  destroy() {
152
- for (const [id, worker] of this.activeWorkers) {
152
+ for (const [id, worker] of Array.from(this.activeWorkers)) {
153
+ worker.onmessage = null;
154
+ worker.onerror = null;
153
155
  worker.terminate();
154
156
  this.activeWorkers.delete(id);
155
157
  }
@@ -204,19 +206,38 @@ function generateVideoPoster(blob, quality) {
204
206
  return new Promise((resolve, reject) => {
205
207
  const video = document.createElement("video");
206
208
  const url = URL.createObjectURL(blob);
207
- video.muted = true;
208
- video.preload = "auto";
209
- video.src = url;
209
+ let settled = false;
210
210
  const cleanup = () => {
211
+ clearTimeout(timeoutId);
211
212
  URL.revokeObjectURL(url);
212
213
  video.removeAttribute("src");
213
214
  video.load();
214
215
  };
216
+ const safeReject = (err) => {
217
+ if (settled) return;
218
+ settled = true;
219
+ cleanup();
220
+ reject(err);
221
+ };
222
+ const timeoutId = setTimeout(() => {
223
+ safeReject(new Error("Video poster generation timed out"));
224
+ }, 1e4);
225
+ video.addEventListener("error", () => {
226
+ safeReject(new Error("Failed to load video for poster generation"));
227
+ }, { once: true });
228
+ video.muted = true;
229
+ video.preload = "auto";
230
+ video.src = url;
215
231
  video.addEventListener("loadeddata", () => {
216
232
  video.addEventListener("seeked", () => {
233
+ if (settled) return;
217
234
  try {
218
235
  const w = video.videoWidth;
219
236
  const h = video.videoHeight;
237
+ if (w === 0 || h === 0) {
238
+ safeReject(new Error("Video dimensions not available"));
239
+ return;
240
+ }
220
241
  const canvas = document.createElement("canvas");
221
242
  canvas.width = w;
222
243
  canvas.height = h;
@@ -224,6 +245,8 @@ function generateVideoPoster(blob, quality) {
224
245
  ctx.drawImage(video, 0, 0);
225
246
  canvas.toBlob(
226
247
  (posterBlob) => {
248
+ if (settled) return;
249
+ settled = true;
227
250
  cleanup();
228
251
  if (!posterBlob) {
229
252
  reject(new Error("Failed to create poster blob"));
@@ -235,16 +258,11 @@ function generateVideoPoster(blob, quality) {
235
258
  quality / 100
236
259
  );
237
260
  } catch (err) {
238
- cleanup();
239
- reject(err);
261
+ safeReject(err instanceof Error ? err : new Error(String(err)));
240
262
  }
241
263
  }, { once: true });
242
264
  video.currentTime = isFinite(video.duration) ? Math.min(0.1, video.duration / 2) : 0;
243
265
  }, { once: true });
244
- video.addEventListener("error", () => {
245
- cleanup();
246
- reject(new Error("Failed to load video for poster generation"));
247
- }, { once: true });
248
266
  });
249
267
  }
250
268
 
@@ -1 +1 @@
1
- {"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAoDjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAQxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,GACZ,EAAE,KAAK,2CAooBP"}
1
+ {"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAqDjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAQxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,GACZ,EAAE,KAAK,2CAqoBP"}
@@ -1 +1 @@
1
- {"version":3,"file":"useBuildStatus.d.ts","sourceRoot":"","sources":["../../src/hooks/useBuildStatus.ts"],"names":[],"mappings":"AAEA,KAAK,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;AASrE,UAAU,iBAAiB;IACzB,KAAK,EAAE,UAAU,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAMD,wBAAgB,cAAc,IAAI,iBAAiB,CAsHlD"}
1
+ {"version":3,"file":"useBuildStatus.d.ts","sourceRoot":"","sources":["../../src/hooks/useBuildStatus.ts"],"names":[],"mappings":"AAEA,KAAK,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;AASrE,UAAU,iBAAiB;IACzB,KAAK,EAAE,UAAU,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAMD,wBAAgB,cAAc,IAAI,iBAAiB,CAgIlD"}
@@ -1 +1 @@
1
- {"version":3,"file":"useEditorPersistence.d.ts","sourceRoot":"","sources":["../../src/hooks/useEditorPersistence.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AASpE,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC;kCAuC7D,MAAM,WAAW,cAAc;;4BAYI,UAAU;;;;;EAwB5D"}
1
+ {"version":3,"file":"useEditorPersistence.d.ts","sourceRoot":"","sources":["../../src/hooks/useEditorPersistence.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AASpE,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC;kCAoD7D,MAAM,WAAW,cAAc;;4BAYI,UAAU;;;;;EAwB5D"}
@@ -1 +1 @@
1
- {"version":3,"file":"useEditorPublish.d.ts","sourceRoot":"","sources":["../../src/hooks/useEditorPublish.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAe/D,UAAU,WAAW;IACnB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,aAAa,EAAE,MAAM,OAAO,CAAC;IAC7B,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACzC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,iBAAiB,EAAE,SAAS,EAAE,CAAC;IAC/B,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,gBAAgB,EAAE,CAAC,cAAc,EAAE,SAAS,EAAE,EAAE,kBAAkB,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACtF,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzE,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;CAChC;AAQD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,YAAY,CAAC;AAE7D,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,iBAAiB,EACjB,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,aAAa,EACb,iBAAiB,GAClB,EAAE,WAAW;;;;;;EA2Pb"}
1
+ {"version":3,"file":"useEditorPublish.d.ts","sourceRoot":"","sources":["../../src/hooks/useEditorPublish.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAe/D,UAAU,WAAW;IACnB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,aAAa,EAAE,MAAM,OAAO,CAAC;IAC7B,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACzC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,iBAAiB,EAAE,SAAS,EAAE,CAAC;IAC/B,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,gBAAgB,EAAE,CAAC,cAAc,EAAE,SAAS,EAAE,EAAE,kBAAkB,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACtF,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzE,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;CAChC;AAQD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,YAAY,CAAC;AAE7D,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,iBAAiB,EACjB,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,aAAa,EACb,iBAAiB,GAClB,EAAE,WAAW;;;;;;EA4Qb"}
@@ -1 +1 @@
1
- {"version":3,"file":"useMediaPipeline.d.ts","sourceRoot":"","sources":["../../src/hooks/useMediaPipeline.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,KAAK,SAAS,EAAmB,MAAM,gBAAgB,CAAC;AAClF,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,gBAAgB,CAAC;AACnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,yCAAyC,CAAC;AA0BxF,UAAU,qBAAqB;IAC7B,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,aAAa,CAAC;IAC7B,gBAAgB,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC,CAAC;IACtE,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,WAAW,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;IACnE,oBAAoB,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IACpE,kBAAkB,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACtE,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC;CACxE;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,QAAQ,EACR,WAAW,EACX,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,GACjB,EAAE,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAsH8B,IAAI,EAAE;6BAyBR,MAAM,EAAE;+BAyCZ,MAAM,OAAO,MAAM;;;wBA+B1B,IAAI,eAAe,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI;EAsEpF"}
1
+ {"version":3,"file":"useMediaPipeline.d.ts","sourceRoot":"","sources":["../../src/hooks/useMediaPipeline.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,KAAK,SAAS,EAAmB,MAAM,gBAAgB,CAAC;AAClF,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,gBAAgB,CAAC;AACnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,yCAAyC,CAAC;AA0BxF,UAAU,qBAAqB;IAC7B,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,aAAa,CAAC;IAC7B,gBAAgB,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC,CAAC;IACtE,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,WAAW,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;IACnE,oBAAoB,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IACpE,kBAAkB,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACtE,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC;CACxE;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,QAAQ,EACR,WAAW,EACX,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,GACjB,EAAE,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BA6H8B,IAAI,EAAE;6BAyBR,MAAM,EAAE;+BA+CZ,MAAM,OAAO,MAAM;;;wBA+B1B,IAAI,eAAe,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI;EAwEpF"}
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  AudienceNameSchema,
4
4
  MediaGridOptionsSchema,
5
5
  slugifyAudienceName
6
- } from "./chunk-OUFUUBZ4.js";
6
+ } from "./chunk-GQV2554Z.js";
7
7
  import {
8
8
  AudienceSchema,
9
9
  RoleSchema,
@@ -18,6 +18,7 @@ import {
18
18
  darkModeEvent,
19
19
  deriveContrast,
20
20
  editModeEvent,
21
+ ensureSanitizer,
21
22
  formatTimestamp,
22
23
  generateNavLinks,
23
24
  getIcon,
@@ -29,7 +30,7 @@ import {
29
30
  safeRedirect,
30
31
  sanitizeHtml,
31
32
  toSectionId
32
- } from "./chunk-C2MVDXD7.js";
33
+ } from "./chunk-I6ZPOEK2.js";
33
34
  import {
34
35
  ColorItemSchema,
35
36
  ColorSpaceSchema,
@@ -50,7 +51,7 @@ import {
50
51
  getSectionSchema,
51
52
  registerSchema,
52
53
  registerSection
53
- } from "./chunk-V43WVSVS.js";
54
+ } from "./chunk-LW5EGJFM.js";
54
55
  import {
55
56
  AUDIENCE_COOKIE,
56
57
  LastOwnerError,
@@ -81,7 +82,7 @@ import {
81
82
  resolveMedia,
82
83
  sanitizeMediaName,
83
84
  setMediaProvider
84
- } from "./chunk-VCZBZMXU.js";
85
+ } from "./chunk-TNHX35TE.js";
85
86
  import {
86
87
  ImageManifestSchema,
87
88
  MediaConfigSchema,
@@ -126,6 +127,7 @@ export {
126
127
  deriveContrast,
127
128
  displayFilenameExt,
128
129
  editModeEvent,
130
+ ensureSanitizer,
129
131
  env,
130
132
  formatTimestamp,
131
133
  generateNavLinks,
@@ -2,7 +2,7 @@ export { env } from "./env";
2
2
  export { cn } from "./cn";
3
3
  export { generateNavLinks, toSectionId, type NavItem } from "./nav";
4
4
  export { deriveContrast } from "./contrast";
5
- export { sanitizeHtml } from "./sanitize";
5
+ export { sanitizeHtml, ensureSanitizer } from "./sanitize";
6
6
  export { gridColsClass } from "./grid";
7
7
  export { getIcon, curatedIcons, type IconEntry } from "./icons";
8
8
  export { buildGoogleFontsUrl } from "./google-fonts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,EAAE,EAAE,MAAM,MAAM,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,KAAK,OAAO,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AACtG,OAAO,EACL,cAAc,EACd,aAAa,EACb,eAAe,EACf,cAAc,EACd,UAAU,EACV,SAAS,EACT,cAAc,EACd,aAAa,EACb,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,uBAAuB,GAC7B,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EACf,KAAK,aAAa,EAClB,KAAK,WAAW,GACjB,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,EAAE,EAAE,MAAM,MAAM,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,KAAK,OAAO,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AACtG,OAAO,EACL,cAAc,EACd,aAAa,EACb,eAAe,EACf,cAAc,EACd,UAAU,EACV,SAAS,EACT,cAAc,EACd,aAAa,EACb,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,uBAAuB,GAC7B,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EACf,KAAK,aAAa,EAClB,KAAK,WAAW,GACjB,MAAM,UAAU,CAAC"}
package/dist/lib/index.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  darkModeEvent,
7
7
  deriveContrast,
8
8
  editModeEvent,
9
+ ensureSanitizer,
9
10
  formatTimestamp,
10
11
  generateNavLinks,
11
12
  getIcon,
@@ -17,7 +18,7 @@ import {
17
18
  safeRedirect,
18
19
  sanitizeHtml,
19
20
  toSectionId
20
- } from "../chunk-C2MVDXD7.js";
21
+ } from "../chunk-I6ZPOEK2.js";
21
22
  import {
22
23
  clearRegistry,
23
24
  createRegistry,
@@ -28,7 +29,7 @@ import {
28
29
  getSection,
29
30
  registerSchema,
30
31
  registerSection
31
- } from "../chunk-V43WVSVS.js";
32
+ } from "../chunk-LW5EGJFM.js";
32
33
  import "../chunk-S2L3BPLS.js";
33
34
  import {
34
35
  env
@@ -45,6 +46,7 @@ export {
45
46
  defineSection,
46
47
  deriveContrast,
47
48
  editModeEvent,
49
+ ensureSanitizer,
48
50
  env,
49
51
  formatTimestamp,
50
52
  generateNavLinks,
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/lib/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,EAAe,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGvF,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,EAAE,SAAS,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,EAChB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACpC,WAAW,CA2Bb;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,WAAW,CAQb;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAe9E"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/lib/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,EAAe,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGvF,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,EAAE,SAAS,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,EAChB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACpC,WAAW,CA4Bb;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,WAAW,CAQb;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAe9E"}
@@ -1 +1 @@
1
- {"version":3,"file":"nav.d.ts","sourceRoot":"","sources":["../../src/lib/nav.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAE9D,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMhD;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,aAAa,EAAE,EACzB,QAAQ,CAAC,EAAE,eAAe,GACzB,OAAO,EAAE,CAiDX"}
1
+ {"version":3,"file":"nav.d.ts","sourceRoot":"","sources":["../../src/lib/nav.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAE9D,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAchD;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,aAAa,EAAE,EACzB,QAAQ,CAAC,EAAE,eAAe,GACzB,OAAO,EAAE,CAqDX"}
@@ -1,2 +1,12 @@
1
+ /**
2
+ * Synchronous sanitizer — returns sanitized HTML if DOMPurify has loaded,
3
+ * otherwise returns raw HTML. Call `ensureSanitizer()` during component mount
4
+ * to guarantee the purifier is ready before first render.
5
+ */
1
6
  export declare function sanitizeHtml(html: string): string;
7
+ /**
8
+ * Await this during component mount (e.g. useEffect) to guarantee
9
+ * DOMPurify is loaded before `sanitizeHtml` is called.
10
+ */
11
+ export declare function ensureSanitizer(): Promise<void>;
2
12
  //# sourceMappingURL=sanitize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/lib/sanitize.ts"],"names":[],"mappings":"AAMA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGjD"}
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/lib/sanitize.ts"],"names":[],"mappings":"AAWA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED;;;GAGG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAGrD"}
@@ -1 +1 @@
1
- {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/lib/text.ts"],"names":[],"mappings":"AAAA,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAGhE"}
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/lib/text.ts"],"names":[],"mappings":"AAAA,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMzD;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAGhE"}
@@ -11,7 +11,7 @@ import {
11
11
  resolveMedia,
12
12
  sanitizeMediaName,
13
13
  setMediaProvider
14
- } from "../chunk-VCZBZMXU.js";
14
+ } from "../chunk-TNHX35TE.js";
15
15
  import {
16
16
  ImageManifestSchema,
17
17
  MediaConfigSchema,
@@ -1 +1 @@
1
- {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../../src/media/queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,WAAW,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE;QACP,QAAQ,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,IAAI,CAAA;SAAE,EAAE,CAAC;QACxE,WAAW,EAAE,IAAI,CAAC;QAClB,UAAU,CAAC,EAAE,IAAI,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEvC,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,MAAM,CAAC;IAC3B,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACtC;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,OAAO,CAA2C;IAC1D,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,MAAM,CAAK;gBAEP,OAAO,EAAE,YAAY;IAIjC,GAAG,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM;IAkB9B,SAAS,IAAI;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,EAAE,CAAA;KAAE;IAelF,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,eAAe;IA0DvB,OAAO,CAAC,aAAa;IAKrB,OAAO,IAAI,IAAI;CAQhB"}
1
+ {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../../src/media/queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,WAAW,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE;QACP,QAAQ,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,IAAI,CAAA;SAAE,EAAE,CAAC;QACxE,WAAW,EAAE,IAAI,CAAC;QAClB,UAAU,CAAC,EAAE,IAAI,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEvC,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,MAAM,CAAC;IAC3B,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACtC;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,OAAO,CAA2C;IAC1D,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,MAAM,CAAK;gBAEP,OAAO,EAAE,YAAY;IAIjC,GAAG,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM;IAkB9B,SAAS,IAAI;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,EAAE,CAAA;KAAE;IAelF,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,eAAe;IA0DvB,OAAO,CAAC,aAAa;IAKrB,OAAO,IAAI,IAAI;CAUhB"}
@@ -1 +1 @@
1
- {"version":3,"file":"videoPoster.d.ts","sourceRoot":"","sources":["../../src/media/videoPoster.ts"],"names":[],"mappings":"AAAA,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,UAAU,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAiD9D"}
1
+ {"version":3,"file":"videoPoster.d.ts","sourceRoot":"","sources":["../../src/media/videoPoster.ts"],"names":[],"mappings":"AAAA,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,UAAU,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAsE9D"}
@@ -3,7 +3,7 @@ import {
3
3
  AudienceNameSchema,
4
4
  MediaGridOptionsSchema,
5
5
  slugifyAudienceName
6
- } from "../chunk-OUFUUBZ4.js";
6
+ } from "../chunk-GQV2554Z.js";
7
7
  import {
8
8
  AudienceSchema,
9
9
  RoleSchema,
@@ -21,7 +21,7 @@ import {
21
21
  TextLineSchema,
22
22
  getSectionContentSchema,
23
23
  getSectionSchema
24
- } from "../chunk-V43WVSVS.js";
24
+ } from "../chunk-LW5EGJFM.js";
25
25
  import {
26
26
  ImageManifestSchema,
27
27
  MediaConfigSchema,
@@ -1 +1 @@
1
- {"version":3,"file":"site-config.d.ts","sourceRoot":"","sources":["../../src/schemas/site-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,eAAO,MAAM,iBAAiB;;;;;;;;;iBAI5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,WAAW;;;;;;;;;;;;;;iBAQvB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEpD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;iBAY3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
1
+ {"version":3,"file":"site-config.d.ts","sourceRoot":"","sources":["../../src/schemas/site-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,eAAO,MAAM,iBAAiB;;;;;;;;;iBAI5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,WAAW;;;;;;;;;;;;;;iBAYvB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEpD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;iBAY3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawnagency/primitives",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",
@@ -8,6 +8,7 @@ import type { Audience } from "../../auth/types";
8
8
  import type { MediaManifest } from "../../media/types";
9
9
  import type { QueueItem } from "../../media/queue";
10
10
  import { SiteConfigSchema } from "../../schemas/site-config";
11
+ import { ensureSanitizer } from "../../lib/sanitize";
11
12
  import { EditorProvider, useEditorContext } from "./EditorContext";
12
13
  import { EditorModalProvider, useEditorModal } from "./EditorModalContext";
13
14
  import { EditorModal } from "./EditorModal";
@@ -112,6 +113,7 @@ export default function EditorShell({
112
113
  const siteIndexRef = useRef<SiteIndex>({ siteId, order: [], sections: {} });
113
114
  const fontLinkRef = useRef<HTMLLinkElement | null>(null);
114
115
  useEffect(() => { siteIndexRef.current = siteIndex; }, [siteIndex]);
116
+ useEffect(() => { void ensureSanitizer(); }, []);
115
117
 
116
118
  const persistence = useEditorPersistence(siteIndexRef);
117
119
 
@@ -29,7 +29,7 @@ type TabId = "users" | "viewer-access" | "display";
29
29
 
30
30
  export function SiteSettingsModal({ isOpen, onClose, siteConfig, onSiteConfigChange, onAudiencesChange, capabilities, currentUser }: Props) {
31
31
  const tabs: { id: TabId; label: string; show: boolean }[] = [
32
- { id: "users", label: "Users", show: capabilities.userManagement },
32
+ { id: "users", label: "Users", show: capabilities.userManagement && currentUser?.role === "owner" },
33
33
  { id: "viewer-access", label: "Viewer Access", show: true },
34
34
  { id: "display", label: "Display", show: true },
35
35
  ];
@@ -25,8 +25,10 @@ export function useBuildStatus(): BuildStatusResult {
25
25
  const [elapsedSeconds, setElapsedSeconds] = useState(0);
26
26
  const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
27
27
  const clearRef = useRef<ReturnType<typeof setTimeout> | null>(null);
28
+ const fadeRef = useRef<ReturnType<typeof setTimeout> | null>(null);
28
29
  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
29
30
  const isPolling = useRef(false);
31
+ const mountedRef = useRef(true);
30
32
 
31
33
  const stopTimer = useCallback(() => {
32
34
  if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
@@ -61,6 +63,8 @@ export function useBuildStatus(): BuildStatusResult {
61
63
 
62
64
  const handleStatusUpdate = useCallback(
63
65
  (data: BuildStatusResponse | null, isInitialLoad: boolean) => {
66
+ if (!mountedRef.current) return;
67
+
64
68
  if (!data) {
65
69
  if (isInitialLoad) {
66
70
  setState("idle");
@@ -80,13 +84,17 @@ export function useBuildStatus(): BuildStatusResult {
80
84
  stopTimer();
81
85
  if (data.state === "ready") {
82
86
  clearRef.current = setTimeout(() => {
87
+ if (!mountedRef.current) return;
83
88
  setState("fading");
84
- clearRef.current = setTimeout(() => setState("idle"), FADE_DURATION);
89
+ fadeRef.current = setTimeout(() => {
90
+ if (!mountedRef.current) return;
91
+ setState("idle");
92
+ }, FADE_DURATION);
85
93
  }, AUTO_CLEAR_DELAY);
86
94
  }
87
95
  }
88
96
  },
89
- [stopPolling],
97
+ [stopPolling, stopTimer],
90
98
  );
91
99
 
92
100
  const startPolling = useCallback(() => {
@@ -117,11 +125,13 @@ export function useBuildStatus(): BuildStatusResult {
117
125
 
118
126
  return () => {
119
127
  cancelled = true;
128
+ mountedRef.current = false;
120
129
  stopPolling();
121
130
  stopTimer();
122
131
  if (clearRef.current) clearTimeout(clearRef.current);
132
+ if (fadeRef.current) clearTimeout(fadeRef.current);
123
133
  };
124
- }, [fetchStatus, handleStatusUpdate, startPolling, stopPolling]);
134
+ }, [fetchStatus, handleStatusUpdate, startPolling, stopPolling, stopTimer]);
125
135
 
126
136
  const startTracking = useCallback(() => {
127
137
  if (clearRef.current) { clearTimeout(clearRef.current); clearRef.current = null; }
@@ -25,15 +25,27 @@ export function useEditorPersistence(siteIndexRef: React.RefObject<SiteIndex>) {
25
25
  sectionId,
26
26
  content,
27
27
  }));
28
+ const wasIndexDirty = s.indexDirty;
28
29
 
30
+ // Clear optimistically so new edits during the await go into a fresh set
29
31
  s.pendingSections = new Map();
30
- const wasIndexDirty = s.indexDirty;
31
32
  s.indexDirty = false;
32
33
 
33
- await persistAll(
34
- entries,
35
- wasIndexDirty ? siteIndexRef.current : undefined,
36
- );
34
+ try {
35
+ await persistAll(
36
+ entries,
37
+ wasIndexDirty ? siteIndexRef.current : undefined,
38
+ );
39
+ } catch (err) {
40
+ // Restore: merge back any entries that weren't re-dirtied during the await
41
+ for (const { sectionId, content } of entries) {
42
+ if (!s.pendingSections.has(sectionId)) {
43
+ s.pendingSections.set(sectionId, content);
44
+ }
45
+ }
46
+ if (wasIndexDirty) s.indexDirty = true;
47
+ console.error("Failed to flush to Dexie:", err);
48
+ }
37
49
  }, [siteIndexRef]);
38
50
 
39
51
  const scheduleFlush = useCallback(() => {
@@ -44,8 +56,9 @@ export function useEditorPersistence(siteIndexRef: React.RefObject<SiteIndex>) {
44
56
  useEffect(() => {
45
57
  return () => {
46
58
  if (flushTimerRef.current) clearTimeout(flushTimerRef.current);
59
+ void flushToDexie();
47
60
  };
48
- }, []);
61
+ }, [flushToDexie]);
49
62
 
50
63
  const markSectionDirty = useCallback(
51
64
  (sectionId: string, content: SectionContent) => {
@@ -62,6 +62,7 @@ export function useEditorPublish({
62
62
  const [publishAction, setPublishAction] = useState<PublishAction>("idle");
63
63
  const [publishFeedback, setPublishFeedback] = useState<string | null>(null);
64
64
  const feedbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
65
+ const inFlightRef = useRef(false);
65
66
 
66
67
  useEffect(() => {
67
68
  return () => {
@@ -184,11 +185,16 @@ export function useEditorPublish({
184
185
  throw new Error(errorBody.error || "Save failed");
185
186
  }
186
187
 
187
- return response.json();
188
+ const responseData = await response.json().catch(() => null);
189
+ if (!responseData?.sha) {
190
+ throw new Error("Save response missing SHA");
191
+ }
192
+ return responseData;
188
193
  }
189
194
 
190
195
  const handleSave = useCallback(async () => {
191
- if (!siteConfig) return;
196
+ if (!siteConfig || inFlightRef.current) return;
197
+ inFlightRef.current = true;
192
198
 
193
199
  setPublishAction("saving");
194
200
  setPublishFeedback(null);
@@ -222,9 +228,10 @@ export function useEditorPublish({
222
228
  console.error("Save failed:", error);
223
229
  showFeedback("Save failed", 5000);
224
230
  } finally {
231
+ inFlightRef.current = false;
225
232
  setPublishAction("idle");
226
233
  }
227
- }, [flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, deletedSectionIds, onSuccess, mediaManifest, pendingMediaItems, pendingMediaDeletions, onMediaPublished, onShasUpdated, showFeedback]);
234
+ }, [flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, deletedSectionIds, onSuccess, mediaManifest, pendingMediaItems, pendingMediaDeletions, onMediaPublished, onShasUpdated, showFeedback]);
228
235
 
229
236
  const handlePublish = useCallback(async () => {
230
237
  setPublishAction("publishing");
@@ -241,7 +248,11 @@ export function useEditorPublish({
241
248
  throw new Error(errorBody.error || "Publish failed");
242
249
  }
243
250
 
244
- const { sha } = await response.json();
251
+ const responseData = await response.json().catch(() => null);
252
+ if (!responseData?.sha) {
253
+ throw new Error("Publish response missing SHA");
254
+ }
255
+ const { sha } = responseData;
245
256
 
246
257
  onShasUpdated(null, sha);
247
258
  onPublishComplete?.();
@@ -254,7 +265,8 @@ export function useEditorPublish({
254
265
  }, [onShasUpdated, showFeedback, onPublishComplete]);
255
266
 
256
267
  const handleSaveAndPublish = useCallback(async () => {
257
- if (!siteConfig) return;
268
+ if (!siteConfig || inFlightRef.current) return;
269
+ inFlightRef.current = true;
258
270
 
259
271
  setPublishAction("saving");
260
272
  setPublishFeedback(null);
@@ -287,7 +299,11 @@ export function useEditorPublish({
287
299
  throw new Error(errorBody.error || "Publish failed");
288
300
  }
289
301
 
290
- const { sha } = await publishResponse.json();
302
+ const publishData = await publishResponse.json().catch(() => null);
303
+ if (!publishData?.sha) {
304
+ throw new Error("Publish response missing SHA");
305
+ }
306
+ const { sha } = publishData;
291
307
 
292
308
  if (hasLocalEdits) {
293
309
  await discardLocalChanges();
@@ -304,6 +320,7 @@ export function useEditorPublish({
304
320
  console.error("Publish failed:", error);
305
321
  showFeedback("Publish failed", 5000);
306
322
  } finally {
323
+ inFlightRef.current = false;
307
324
  setPublishAction("idle");
308
325
  }
309
326
  }, [flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, deletedSectionIds, onSuccess, mediaManifest, pendingMediaItems, pendingMediaDeletions, onMediaPublished, onShasUpdated, showFeedback, onPublishComplete]);
@@ -59,6 +59,7 @@ export function useMediaPipeline({
59
59
 
60
60
  const queueRef = useRef<ProcessingQueue | null>(null);
61
61
  const uploadCallbacksRef = useRef<Map<string, (imageId: string) => void>>(new Map());
62
+ const destroyedRef = useRef(false);
62
63
 
63
64
  // --- Processing queue ---
64
65
 
@@ -68,6 +69,7 @@ export function useMediaPipeline({
68
69
 
69
70
  useEffect(() => {
70
71
  if (!siteConfig) return;
72
+ destroyedRef.current = false;
71
73
  const mediaConfig = siteConfig.media;
72
74
  const queue = new ProcessingQueue({
73
75
  sizes: mediaConfig.sizes,
@@ -93,6 +95,7 @@ export function useMediaPipeline({
93
95
  width: number,
94
96
  height: number,
95
97
  ) => {
98
+ if (destroyedRef.current) return;
96
99
  const item: LibraryMediaItem = {
97
100
  id: event.item.hash,
98
101
  hash: event.item.hash,
@@ -163,7 +166,11 @@ export function useMediaPipeline({
163
166
  },
164
167
  });
165
168
  queueRef.current = queue;
166
- return () => queue.destroy();
169
+ return () => {
170
+ destroyedRef.current = true;
171
+ queue.destroy();
172
+ uploadCallbacksRef.current = new Map();
173
+ };
167
174
  // eslint-disable-next-line react-hooks/exhaustive-deps -- rebuild only when processing params change, not on every siteConfig reference change
168
175
  }, [mediaConfigKey, setLocalChangesExist]);
169
176
 
@@ -198,14 +205,20 @@ export function useMediaPipeline({
198
205
  const idSet = new Set(ids);
199
206
  const pendingIds = new Set(pendingMediaItems.filter((i) => idSet.has(i.id)).map((i) => i.id));
200
207
 
201
- for (const id of ids) {
202
- if (pendingIds.has(id)) {
203
- await removePendingMediaItem(id);
204
- } else {
205
- await markPendingMediaDeleted(id);
208
+ try {
209
+ for (const id of ids) {
210
+ if (pendingIds.has(id)) {
211
+ await removePendingMediaItem(id);
212
+ } else {
213
+ await markPendingMediaDeleted(id);
214
+ }
206
215
  }
216
+ } catch (err) {
217
+ console.error("Failed to delete media:", err);
218
+ throw err;
207
219
  }
208
220
 
221
+ // Only update React state after all Dexie writes succeed
209
222
  setPendingMediaItems((prev) => prev.filter((i) => !idSet.has(i.id)));
210
223
  setPendingLocalUrls((prev) => {
211
224
  const next = { ...prev };
@@ -271,9 +284,11 @@ export function useMediaPipeline({
271
284
  const mediaConfig = siteConfig.media;
272
285
  if (file.size > mediaConfig.maxFileSize) return;
273
286
 
287
+ const currentQueue = queueRef.current;
274
288
  (async () => {
275
289
  try {
276
290
  const buffer = await file.arrayBuffer();
291
+ if (queueRef.current !== currentQueue) return; // queue was rebuilt, skip
277
292
  const hash = await hashFileBuffer(buffer);
278
293
 
279
294
  const isDeleted = pendingDeletions.includes(hash);
package/src/lib/index.ts CHANGED
@@ -2,7 +2,7 @@ export { env } from "./env";
2
2
  export { cn } from "./cn";
3
3
  export { generateNavLinks, toSectionId, type NavItem } from "./nav";
4
4
  export { deriveContrast } from "./contrast";
5
- export { sanitizeHtml } from "./sanitize";
5
+ export { sanitizeHtml, ensureSanitizer } from "./sanitize";
6
6
  export { gridColsClass } from "./grid";
7
7
  export { getIcon, curatedIcons, type IconEntry } from "./icons";
8
8
  export { buildGoogleFontsUrl } from "./google-fonts";
package/src/lib/loader.ts CHANGED
@@ -24,6 +24,7 @@ export function mergeSiteContent(
24
24
  const sections: LoadedSection[] = [];
25
25
 
26
26
  const canValidate = getAllSchemas().length >= 2;
27
+ const schema = canValidate ? getSectionSchema() : null;
27
28
 
28
29
  for (const id of index.order) {
29
30
  const raw = sectionFiles[id];
@@ -31,8 +32,8 @@ export function mergeSiteContent(
31
32
  console.warn(`Section file missing for id: ${id}, skipping`);
32
33
  continue;
33
34
  }
34
- if (canValidate) {
35
- const result = getSectionSchema().safeParse(raw);
35
+ if (canValidate && schema) {
36
+ const result = schema.safeParse(raw);
36
37
  if (!result.success) {
37
38
  const type = (raw as Record<string, unknown>).type ?? "unknown";
38
39
  console.warn(`Skipping section "${id}" (type: ${type}): invalid schema`);
package/src/lib/nav.ts CHANGED
@@ -9,11 +9,19 @@ export interface NavItem {
9
9
  }
10
10
 
11
11
  export function toSectionId(text: string): string {
12
- return text
12
+ const slug = text
13
+ .normalize("NFKD")
14
+ .replace(/[̀-ͯ]/g, "")
13
15
  .toLowerCase()
14
16
  .replace(/[^\w\s-]/g, "")
15
- .trim()
16
- .replace(/\s+/g, "-");
17
+ .replace(/\s+/g, "-")
18
+ .replace(/^-+|-+$/g, "");
19
+ if (slug) return slug;
20
+ let hash = 0;
21
+ for (let i = 0; i < text.length; i++) {
22
+ hash = ((hash << 5) - hash + text.charCodeAt(i)) | 0;
23
+ }
24
+ return `section-${Math.abs(hash).toString(36)}`;
17
25
  }
18
26
 
19
27
  export function generateNavLinks(
@@ -37,6 +45,10 @@ export function generateNavLinks(
37
45
  if (!role) continue;
38
46
 
39
47
  if (role === "h1") {
48
+ if (content.excludeFromNav) {
49
+ currentParent = null;
50
+ continue;
51
+ }
40
52
  currentParent = {
41
53
  href: `#${toSectionId(content.heading)}`,
42
54
  label: content.heading,
@@ -1,10 +1,29 @@
1
- let purifier: { sanitize: (html: string) => string } | undefined;
1
+ let purifierPromise: Promise<typeof import("dompurify")> | null = null;
2
+ let purifier: ((html: string) => string) | null = null;
2
3
 
3
4
  if (typeof window !== "undefined") {
4
- import("dompurify").then((m) => { purifier = m.default; });
5
+ purifierPromise = import("dompurify").then((mod) => {
6
+ const DOMPurify = mod.default ?? mod;
7
+ purifier = (html: string) => DOMPurify.sanitize(html);
8
+ return mod;
9
+ });
5
10
  }
6
11
 
12
+ /**
13
+ * Synchronous sanitizer — returns sanitized HTML if DOMPurify has loaded,
14
+ * otherwise returns raw HTML. Call `ensureSanitizer()` during component mount
15
+ * to guarantee the purifier is ready before first render.
16
+ */
7
17
  export function sanitizeHtml(html: string): string {
8
18
  if (!html) return "";
9
- return purifier ? purifier.sanitize(html) : html;
19
+ return purifier ? purifier(html) : html;
20
+ }
21
+
22
+ /**
23
+ * Await this during component mount (e.g. useEffect) to guarantee
24
+ * DOMPurify is loaded before `sanitizeHtml` is called.
25
+ */
26
+ export async function ensureSanitizer(): Promise<void> {
27
+ if (typeof window === "undefined") return;
28
+ if (!purifier && purifierPromise) await purifierPromise;
10
29
  }
package/src/lib/text.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export function stripHtmlToPlainText(html: string): string {
2
- return html.replace(/<\/[^>]+>/g, " ").replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
2
+ return html
3
+ .replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, "")
4
+ .replace(/<[^>]+>/g, " ")
5
+ .replace(/\s+/g, " ")
6
+ .trim();
3
7
  }
4
8
 
5
9
  export function truncate(text: string, maxLength: number): string {
@@ -156,7 +156,9 @@ export class ProcessingQueue {
156
156
  }
157
157
 
158
158
  destroy(): void {
159
- for (const [id, worker] of this.activeWorkers) {
159
+ for (const [id, worker] of Array.from(this.activeWorkers)) {
160
+ worker.onmessage = null;
161
+ worker.onerror = null;
160
162
  worker.terminate();
161
163
  this.activeWorkers.delete(id);
162
164
  }
@@ -5,21 +5,46 @@ export function generateVideoPoster(
5
5
  return new Promise((resolve, reject) => {
6
6
  const video = document.createElement("video");
7
7
  const url = URL.createObjectURL(blob);
8
- video.muted = true;
9
- video.preload = "auto";
10
- video.src = url;
8
+ let settled = false;
11
9
 
12
10
  const cleanup = () => {
11
+ clearTimeout(timeoutId);
13
12
  URL.revokeObjectURL(url);
14
13
  video.removeAttribute("src");
15
14
  video.load();
16
15
  };
17
16
 
17
+ const safeReject = (err: Error) => {
18
+ if (settled) return;
19
+ settled = true;
20
+ cleanup();
21
+ reject(err);
22
+ };
23
+
24
+ const timeoutId = setTimeout(() => {
25
+ safeReject(new Error("Video poster generation timed out"));
26
+ }, 10_000);
27
+
28
+ video.addEventListener("error", () => {
29
+ safeReject(new Error("Failed to load video for poster generation"));
30
+ }, { once: true });
31
+
32
+ video.muted = true;
33
+ video.preload = "auto";
34
+ video.src = url;
35
+
18
36
  video.addEventListener("loadeddata", () => {
19
37
  video.addEventListener("seeked", () => {
38
+ if (settled) return;
20
39
  try {
21
40
  const w = video.videoWidth;
22
41
  const h = video.videoHeight;
42
+
43
+ if (w === 0 || h === 0) {
44
+ safeReject(new Error("Video dimensions not available"));
45
+ return;
46
+ }
47
+
23
48
  const canvas = document.createElement("canvas");
24
49
  canvas.width = w;
25
50
  canvas.height = h;
@@ -27,6 +52,8 @@ export function generateVideoPoster(
27
52
  ctx.drawImage(video, 0, 0);
28
53
  canvas.toBlob(
29
54
  (posterBlob) => {
55
+ if (settled) return;
56
+ settled = true;
30
57
  cleanup();
31
58
  if (!posterBlob) {
32
59
  reject(new Error("Failed to create poster blob"));
@@ -38,16 +65,10 @@ export function generateVideoPoster(
38
65
  quality / 100,
39
66
  );
40
67
  } catch (err) {
41
- cleanup();
42
- reject(err);
68
+ safeReject(err instanceof Error ? err : new Error(String(err)));
43
69
  }
44
70
  }, { once: true });
45
71
  video.currentTime = isFinite(video.duration) ? Math.min(0.1, video.duration / 2) : 0;
46
72
  }, { once: true });
47
-
48
- video.addEventListener("error", () => {
49
- cleanup();
50
- reject(new Error("Failed to load video for poster generation"));
51
- }, { once: true });
52
73
  });
53
74
  }
@@ -20,8 +20,12 @@ export const IndexSchema = z.object({
20
20
  sections: z.record(z.string(), SectionMetaSchema),
21
21
  lastModified: z.string().nullable().optional(),
22
22
  }).refine(
23
- (data) => data.order.every((id) => id in data.sections),
24
- { message: "All order entries must have a corresponding section in sections" }
23
+ (data) => {
24
+ const orderSet = new Set(data.order);
25
+ return data.order.every((id) => id in data.sections) &&
26
+ Object.keys(data.sections).every((key) => orderSet.has(key));
27
+ },
28
+ { message: "Every id in order must have a sections entry and vice versa" }
25
29
  );
26
30
 
27
31
  export type SiteIndex = z.infer<typeof IndexSchema>;