@drawnagency/primitives 0.1.91 → 0.1.92

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 (33) hide show
  1. package/dist/{chunk-ZLG7S54V.js → chunk-7QEJDYTG.js} +6 -6
  2. package/dist/{chunk-XUIJHYKI.js → chunk-A2V7BZPD.js} +3 -3
  3. package/dist/{chunk-NV4R6VHN.js → chunk-CI624HRS.js} +1 -1
  4. package/dist/{chunk-SIYQ37QY.js → chunk-IZLXF3CR.js} +1 -1
  5. package/dist/{chunk-XKRQMTE4.js → chunk-LQQY2RW6.js} +2 -2
  6. package/dist/{chunk-KMQZYORS.js → chunk-RUKO2N3F.js} +5 -1
  7. package/dist/{chunk-CZ3YXGK5.js → chunk-SOILPZ7U.js} +27 -3
  8. package/dist/{chunk-AGWE2LJG.js → chunk-WOYGQK2D.js} +1 -1
  9. package/dist/{chunk-KBQNJZUX.js → chunk-YI2HUFE3.js} +1 -1
  10. package/dist/{chunk-MK7YB2A4.js → chunk-Z66TBL4Q.js} +1 -1
  11. package/dist/components/editor/index.js +4 -4
  12. package/dist/components/primitives/index.js +5 -5
  13. package/dist/components/sections/brand-guide.js +5 -5
  14. package/dist/components/sections/register-schemas.js +8 -8
  15. package/dist/components/sections/register.js +8 -8
  16. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  17. package/dist/components/shell/ViewerLoginForm.d.ts +1 -0
  18. package/dist/components/shell/ViewerLoginForm.d.ts.map +1 -1
  19. package/dist/index.js +4 -4
  20. package/dist/lib/index.js +2 -2
  21. package/dist/media/compose-preview.d.ts +9 -0
  22. package/dist/media/compose-preview.d.ts.map +1 -1
  23. package/dist/media/index.js +2 -2
  24. package/dist/media/preview-inputs.d.ts.map +1 -1
  25. package/dist/schemas/index.js +2 -2
  26. package/dist/schemas/site-config.d.ts +1 -0
  27. package/dist/schemas/site-config.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/components/shell/EditorShell.tsx +17 -6
  30. package/src/components/shell/ViewerLoginForm.tsx +19 -8
  31. package/src/media/compose-preview.ts +56 -4
  32. package/src/media/preview-inputs.ts +3 -1
  33. package/src/schemas/site-config.ts +4 -0
@@ -3,7 +3,7 @@ import {
3
3
  pluralize,
4
4
  stripHtmlToPlainText,
5
5
  truncate
6
- } from "./chunk-XUIJHYKI.js";
6
+ } from "./chunk-A2V7BZPD.js";
7
7
  import {
8
8
  DeleteButton,
9
9
  DragHandle,
@@ -17,11 +17,11 @@ import {
17
17
  buildBlockDropData,
18
18
  canDropBlock,
19
19
  isBlockDragData
20
- } from "./chunk-SIYQ37QY.js";
20
+ } from "./chunk-IZLXF3CR.js";
21
21
  import {
22
22
  EditableRichText,
23
23
  defineHeadingSection
24
- } from "./chunk-KBQNJZUX.js";
24
+ } from "./chunk-YI2HUFE3.js";
25
25
  import {
26
26
  EditablePlainText,
27
27
  ImageDropZone,
@@ -31,11 +31,11 @@ import {
31
31
  containerGridClass,
32
32
  masonryColumnsClass,
33
33
  spacerHiddenClass
34
- } from "./chunk-XKRQMTE4.js";
34
+ } from "./chunk-LQQY2RW6.js";
35
35
  import {
36
36
  useMediaLibrary,
37
37
  useResolvedMedia
38
- } from "./chunk-NV4R6VHN.js";
38
+ } from "./chunk-CI624HRS.js";
39
39
  import {
40
40
  embedUrl
41
41
  } from "./chunk-Y4JQGO6G.js";
@@ -54,7 +54,7 @@ import {
54
54
  } from "./chunk-TZQPOR5A.js";
55
55
  import {
56
56
  SingleMediaReferenceSchema
57
- } from "./chunk-KMQZYORS.js";
57
+ } from "./chunk-RUKO2N3F.js";
58
58
  import {
59
59
  defineSection,
60
60
  getSection
@@ -9,13 +9,13 @@ import {
9
9
  MediaBlock,
10
10
  containerGridClass,
11
11
  useEditableCollection
12
- } from "./chunk-XKRQMTE4.js";
12
+ } from "./chunk-LQQY2RW6.js";
13
13
  import {
14
14
  AddIcon,
15
15
  DeleteIcon,
16
16
  DragHandle,
17
17
  IconButton
18
- } from "./chunk-NV4R6VHN.js";
18
+ } from "./chunk-CI624HRS.js";
19
19
  import {
20
20
  DEFAULT_LINK,
21
21
  LinkValueSchema
@@ -30,7 +30,7 @@ import {
30
30
  import {
31
31
  HexColorSchema,
32
32
  SingleMediaReferenceSchema
33
- } from "./chunk-KMQZYORS.js";
33
+ } from "./chunk-RUKO2N3F.js";
34
34
  import {
35
35
  defineSection
36
36
  } from "./chunk-VKAGMEKE.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getMediaProvider
3
- } from "./chunk-CZ3YXGK5.js";
3
+ } from "./chunk-SOILPZ7U.js";
4
4
  import {
5
5
  cn
6
6
  } from "./chunk-7EOSDFS3.js";
@@ -14,7 +14,7 @@ import {
14
14
  SettingsIcon,
15
15
  useMediaLibrary,
16
16
  useResolvedMedia
17
- } from "./chunk-NV4R6VHN.js";
17
+ } from "./chunk-CI624HRS.js";
18
18
  import {
19
19
  cn
20
20
  } from "./chunk-7EOSDFS3.js";
@@ -6,10 +6,10 @@ import {
6
6
  SettingsIcon,
7
7
  useMediaLibrary,
8
8
  useResolvedMedia
9
- } from "./chunk-NV4R6VHN.js";
9
+ } from "./chunk-CI624HRS.js";
10
10
  import {
11
11
  rebindImageRef
12
- } from "./chunk-CZ3YXGK5.js";
12
+ } from "./chunk-SOILPZ7U.js";
13
13
  import {
14
14
  cn
15
15
  } from "./chunk-7EOSDFS3.js";
@@ -309,7 +309,11 @@ var SiteConfigSchema = z3.object({
309
309
  previewImage: z3.object({
310
310
  imageId: z3.string(),
311
311
  width: z3.number().int().positive(),
312
- height: z3.number().int().positive()
312
+ height: z3.number().int().positive(),
313
+ // Compose-logic version that produced this raster. Pre-existing rasters
314
+ // (and manifests written before this field) default to 0, which is below
315
+ // any current PREVIEW_COMPOSE_VERSION, so the editor regenerates them.
316
+ version: z3.number().int().nonnegative().default(0)
313
317
  }).nullable().default(null),
314
318
  // Optional site description → og:description + <meta name="description">.
315
319
  description: z3.string().max(300).nullable().default(null),
@@ -311,6 +311,31 @@ function generateVideoPoster(blob, quality) {
311
311
  // src/media/compose-preview.ts
312
312
  var WIDTH = 1200;
313
313
  var HEIGHT = 630;
314
+ var HEADING_WEIGHT = 700;
315
+ async function loadHeadingFont(family, googleFontsUrl) {
316
+ if (!family || family === "system-ui" || !googleFontsUrl || typeof FontFace === "undefined") return;
317
+ const spec = `${HEADING_WEIGHT} 84px "${family}"`;
318
+ try {
319
+ const loaded = await document.fonts.load(spec);
320
+ if (loaded.length > 0) return;
321
+ } catch {
322
+ }
323
+ try {
324
+ const css = await (await fetch(googleFontsUrl)).text();
325
+ const blocks = css.match(/@font-face\s*\{[^}]*\}/g) ?? [];
326
+ const wanted = blocks.filter((b) => new RegExp(`font-weight:\\s*${HEADING_WEIGHT}\\b`).test(b));
327
+ await Promise.all(
328
+ (wanted.length ? wanted : blocks).map(async (block) => {
329
+ const url = block.match(/url\(([^)]+)\)/)?.[1];
330
+ if (!url) return;
331
+ const face = new FontFace(family, `url(${url})`, { weight: String(HEADING_WEIGHT) });
332
+ await face.load();
333
+ document.fonts.add(face);
334
+ })
335
+ );
336
+ } catch {
337
+ }
338
+ }
314
339
  async function composePreviewImage(inputs) {
315
340
  const canvas = document.createElement("canvas");
316
341
  canvas.width = WIDTH;
@@ -340,12 +365,11 @@ async function composePreviewImage(inputs) {
340
365
  }
341
366
  if (!drewLogo) {
342
367
  if (document.fonts?.ready) await document.fonts.ready;
343
- await document.fonts.load(`600 84px ${inputs.headingFont}`).catch(() => {
344
- });
368
+ await loadHeadingFont(inputs.headingFont, inputs.googleFontsUrl);
345
369
  ctx.fillStyle = inputs.textColor;
346
370
  ctx.textAlign = "center";
347
371
  ctx.textBaseline = "middle";
348
- ctx.font = `600 84px ${inputs.headingFont}, system-ui, sans-serif`;
372
+ ctx.font = `${HEADING_WEIGHT} 84px "${inputs.headingFont}", system-ui, sans-serif`;
349
373
  ctx.fillText(inputs.siteName, WIDTH / 2, HEIGHT / 2, WIDTH * 0.86);
350
374
  }
351
375
  return await new Promise((resolve, reject) => {
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-TZQPOR5A.js";
10
10
  import {
11
11
  IndexSchema
12
- } from "./chunk-KMQZYORS.js";
12
+ } from "./chunk-RUKO2N3F.js";
13
13
  import {
14
14
  getAllSchemas
15
15
  } from "./chunk-VKAGMEKE.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  EditablePlainText
3
- } from "./chunk-XKRQMTE4.js";
3
+ } from "./chunk-LQQY2RW6.js";
4
4
  import {
5
5
  Button,
6
6
  usePages
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HexColorSchema
3
- } from "./chunk-KMQZYORS.js";
3
+ } from "./chunk-RUKO2N3F.js";
4
4
 
5
5
  // src/schemas/audience.ts
6
6
  import { z } from "zod";
@@ -11,20 +11,20 @@ import {
11
11
  buildBlockDropData,
12
12
  canDropBlock,
13
13
  isBlockDragData
14
- } from "../../chunk-SIYQ37QY.js";
14
+ } from "../../chunk-IZLXF3CR.js";
15
15
  import "../../chunk-5THS4K5D.js";
16
16
  import "../../chunk-ZSPCE6MZ.js";
17
17
  import {
18
18
  AddIcon,
19
19
  IconButton
20
- } from "../../chunk-NV4R6VHN.js";
21
- import "../../chunk-CZ3YXGK5.js";
20
+ } from "../../chunk-CI624HRS.js";
21
+ import "../../chunk-SOILPZ7U.js";
22
22
  import "../../chunk-677NTHAZ.js";
23
23
  import {
24
24
  cn
25
25
  } from "../../chunk-7EOSDFS3.js";
26
26
  import "../../chunk-YBVK254P.js";
27
- import "../../chunk-KMQZYORS.js";
27
+ import "../../chunk-RUKO2N3F.js";
28
28
  import "../../chunk-FM4M4Q2W.js";
29
29
  import "../../chunk-JSBRDJBE.js";
30
30
 
@@ -2,7 +2,7 @@ import {
2
2
  EditableRichText,
3
3
  LinkPopover,
4
4
  defineHeadingSection
5
- } from "../../chunk-KBQNJZUX.js";
5
+ } from "../../chunk-YI2HUFE3.js";
6
6
  import {
7
7
  EditableGrid,
8
8
  EditablePlainText,
@@ -10,20 +10,20 @@ import {
10
10
  MediaBlock,
11
11
  ResolvedMedia,
12
12
  useEditableCollection
13
- } from "../../chunk-XKRQMTE4.js";
13
+ } from "../../chunk-LQQY2RW6.js";
14
14
  import "../../chunk-ZSPCE6MZ.js";
15
15
  import {
16
16
  DeleteIcon,
17
17
  DragHandle,
18
18
  IconButton
19
- } from "../../chunk-NV4R6VHN.js";
20
- import "../../chunk-CZ3YXGK5.js";
19
+ } from "../../chunk-CI624HRS.js";
20
+ import "../../chunk-SOILPZ7U.js";
21
21
  import "../../chunk-53DKVAB6.js";
22
22
  import {
23
23
  cn
24
24
  } from "../../chunk-7EOSDFS3.js";
25
25
  import "../../chunk-YBVK254P.js";
26
- import "../../chunk-KMQZYORS.js";
26
+ import "../../chunk-RUKO2N3F.js";
27
27
  import "../../chunk-VKAGMEKE.js";
28
28
  import "../../chunk-FM4M4Q2W.js";
29
29
  import "../../chunk-JSBRDJBE.js";
@@ -2,16 +2,16 @@ import {
2
2
  ColorItemSchema,
3
3
  ColorSpaceSchema,
4
4
  brandGuideSectionDefs
5
- } from "../../chunk-XUIJHYKI.js";
5
+ } from "../../chunk-A2V7BZPD.js";
6
6
  import "../../chunk-5THS4K5D.js";
7
- import "../../chunk-XKRQMTE4.js";
8
- import "../../chunk-NV4R6VHN.js";
9
- import "../../chunk-CZ3YXGK5.js";
7
+ import "../../chunk-LQQY2RW6.js";
8
+ import "../../chunk-CI624HRS.js";
9
+ import "../../chunk-SOILPZ7U.js";
10
10
  import "../../chunk-PZJL2S3Z.js";
11
11
  import "../../chunk-WDSVHKAH.js";
12
12
  import "../../chunk-7EOSDFS3.js";
13
13
  import "../../chunk-YBVK254P.js";
14
- import "../../chunk-KMQZYORS.js";
14
+ import "../../chunk-RUKO2N3F.js";
15
15
  import "../../chunk-VKAGMEKE.js";
16
16
  import "../../chunk-FM4M4Q2W.js";
17
17
  import "../../chunk-JSBRDJBE.js";
@@ -1,22 +1,22 @@
1
1
  import {
2
2
  allSectionDefs
3
- } from "../../chunk-ZLG7S54V.js";
4
- import "../../chunk-XUIJHYKI.js";
5
- import "../../chunk-SIYQ37QY.js";
3
+ } from "../../chunk-7QEJDYTG.js";
4
+ import "../../chunk-A2V7BZPD.js";
5
+ import "../../chunk-IZLXF3CR.js";
6
6
  import "../../chunk-5THS4K5D.js";
7
- import "../../chunk-KBQNJZUX.js";
8
- import "../../chunk-XKRQMTE4.js";
7
+ import "../../chunk-YI2HUFE3.js";
8
+ import "../../chunk-LQQY2RW6.js";
9
9
  import "../../chunk-ZSPCE6MZ.js";
10
- import "../../chunk-NV4R6VHN.js";
10
+ import "../../chunk-CI624HRS.js";
11
11
  import "../../chunk-Y4JQGO6G.js";
12
- import "../../chunk-CZ3YXGK5.js";
12
+ import "../../chunk-SOILPZ7U.js";
13
13
  import "../../chunk-PZJL2S3Z.js";
14
14
  import "../../chunk-WDSVHKAH.js";
15
15
  import "../../chunk-53DKVAB6.js";
16
16
  import "../../chunk-7EOSDFS3.js";
17
17
  import "../../chunk-YBVK254P.js";
18
18
  import "../../chunk-TZQPOR5A.js";
19
- import "../../chunk-KMQZYORS.js";
19
+ import "../../chunk-RUKO2N3F.js";
20
20
  import {
21
21
  registerRichText,
22
22
  registerSchema
@@ -1,22 +1,22 @@
1
1
  import {
2
2
  allSectionDefs
3
- } from "../../chunk-ZLG7S54V.js";
4
- import "../../chunk-XUIJHYKI.js";
5
- import "../../chunk-SIYQ37QY.js";
3
+ } from "../../chunk-7QEJDYTG.js";
4
+ import "../../chunk-A2V7BZPD.js";
5
+ import "../../chunk-IZLXF3CR.js";
6
6
  import "../../chunk-5THS4K5D.js";
7
- import "../../chunk-KBQNJZUX.js";
8
- import "../../chunk-XKRQMTE4.js";
7
+ import "../../chunk-YI2HUFE3.js";
8
+ import "../../chunk-LQQY2RW6.js";
9
9
  import "../../chunk-ZSPCE6MZ.js";
10
- import "../../chunk-NV4R6VHN.js";
10
+ import "../../chunk-CI624HRS.js";
11
11
  import "../../chunk-Y4JQGO6G.js";
12
- import "../../chunk-CZ3YXGK5.js";
12
+ import "../../chunk-SOILPZ7U.js";
13
13
  import "../../chunk-PZJL2S3Z.js";
14
14
  import "../../chunk-WDSVHKAH.js";
15
15
  import "../../chunk-53DKVAB6.js";
16
16
  import "../../chunk-7EOSDFS3.js";
17
17
  import "../../chunk-YBVK254P.js";
18
18
  import "../../chunk-TZQPOR5A.js";
19
- import "../../chunk-KMQZYORS.js";
19
+ import "../../chunk-RUKO2N3F.js";
20
20
  import {
21
21
  registerSection
22
22
  } from "../../chunk-VKAGMEKE.js";
@@ -1 +1 @@
1
- {"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAcjD,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAuDtF,OAAO,KAAK,EAAE,eAAe,EAA2D,MAAM,oBAAoB,CAAC;AAwCnH,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAoBxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAIhB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,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;IAIhE,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAKjC,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC1C;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,YAAY,EACZ,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,EACX,sBAA8B,EAC9B,eAAe,GAChB,EAAE,KAAK,2CA4mDP;AAuED;6EAC6E;AAC7E,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,WAAW,CAAC,YAAY,CAAC,EACvC,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,eAAe,EAAE,MAAM,GAAG,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAK/B"}
1
+ {"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAcjD,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAuDtF,OAAO,KAAK,EAAE,eAAe,EAA2D,MAAM,oBAAoB,CAAC;AAwCnH,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAoBxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAIhB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,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;IAIhE,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAKjC,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC1C;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,YAAY,EACZ,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,EACX,sBAA8B,EAC9B,eAAe,GAChB,EAAE,KAAK,2CAunDP;AAuED;6EAC6E;AAC7E,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,WAAW,CAAC,YAAY,CAAC,EACvC,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,eAAe,EAAE,MAAM,GAAG,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAK/B"}
@@ -2,6 +2,7 @@ interface Props {
2
2
  audiences: Array<{
3
3
  name: string;
4
4
  displayName: string;
5
+ hasPassword?: boolean;
5
6
  }>;
6
7
  next?: string | null;
7
8
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ViewerLoginForm.d.ts","sourceRoot":"","sources":["../../../src/components/shell/ViewerLoginForm.tsx"],"names":[],"mappings":"AAKA,UAAU,KAAK;IACb,SAAS,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,KAAK,2CAoFzD"}
1
+ {"version":3,"file":"ViewerLoginForm.d.ts","sourceRoot":"","sources":["../../../src/components/shell/ViewerLoginForm.tsx"],"names":[],"mappings":"AAKA,UAAU,KAAK;IACb,SAAS,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC/E,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,KAAK,2CA+FzD"}
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  resolveMedia,
33
33
  sanitizeMediaName,
34
34
  setMediaProvider
35
- } from "./chunk-CZ3YXGK5.js";
35
+ } from "./chunk-SOILPZ7U.js";
36
36
  import {
37
37
  CollabPeerSchema,
38
38
  HEARTBEAT_INTERVAL_MS,
@@ -51,7 +51,7 @@ import {
51
51
  AudienceNameSchema,
52
52
  MediaGridOptionsSchema,
53
53
  slugifyAudienceName
54
- } from "./chunk-MK7YB2A4.js";
54
+ } from "./chunk-Z66TBL4Q.js";
55
55
  import {
56
56
  AudienceSchema,
57
57
  RoleSchema,
@@ -77,7 +77,7 @@ import {
77
77
  resolveChatbotName,
78
78
  resolveThinkingPhrases,
79
79
  safeRedirect
80
- } from "./chunk-AGWE2LJG.js";
80
+ } from "./chunk-WOYGQK2D.js";
81
81
  import {
82
82
  curatedIcons,
83
83
  getIcon
@@ -131,7 +131,7 @@ import {
131
131
  TextLineSchema,
132
132
  VariantSchema,
133
133
  normalizeSiteIndex
134
- } from "./chunk-KMQZYORS.js";
134
+ } from "./chunk-RUKO2N3F.js";
135
135
  import {
136
136
  clearRegistry,
137
137
  createRegistry,
package/dist/lib/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  resolveChatbotName,
12
12
  resolveThinkingPhrases,
13
13
  safeRedirect
14
- } from "../chunk-AGWE2LJG.js";
14
+ } from "../chunk-WOYGQK2D.js";
15
15
  import {
16
16
  curatedIcons,
17
17
  getIcon
@@ -37,7 +37,7 @@ import {
37
37
  } from "../chunk-YBVK254P.js";
38
38
  import "../chunk-5BM2Y3NJ.js";
39
39
  import "../chunk-TZQPOR5A.js";
40
- import "../chunk-KMQZYORS.js";
40
+ import "../chunk-RUKO2N3F.js";
41
41
  import {
42
42
  clearRegistry,
43
43
  createRegistry,
@@ -10,9 +10,18 @@ export interface PreviewImageInputs {
10
10
  invertLogo: boolean;
11
11
  siteName: string;
12
12
  headingFont: string;
13
+ /** The site's Google Fonts CSS URL (or null for system-ui), used to load the
14
+ * heading font before drawing so text mode never falls back to system-ui. */
15
+ googleFontsUrl: string | null;
13
16
  /** Computed contrasting text color (text mode). */
14
17
  textColor: string;
15
18
  }
19
+ /**
20
+ * Bump when the composition logic changes so already-composed rasters (stamped
21
+ * with the old version, or 0 for pre-versioning ones) regenerate on next editor
22
+ * open. v2 = load the Google heading font before drawing + draw at weight 700.
23
+ */
24
+ export declare const PREVIEW_COMPOSE_VERSION = 2;
16
25
  /**
17
26
  * Composes the 1200×630 social-preview raster in the browser (Canvas API only,
18
27
  * no dependency). Returns a WebP blob. Must run client-side (uses document/Image
@@ -1 +1 @@
1
- {"version":3,"file":"compose-preview.d.ts","sourceRoot":"","sources":["../../src/media/compose-preview.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC;mFAC+E;IAC/E,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,oFAAoF;IACpF,UAAU,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,uFAAuF;IACvF,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;CACnB;AAKD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDnF"}
1
+ {"version":3,"file":"compose-preview.d.ts","sourceRoot":"","sources":["../../src/media/compose-preview.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC;mFAC+E;IAC/E,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,oFAAoF;IACpF,UAAU,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,uFAAuF;IACvF,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB;kFAC8E;IAC9E,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;CACnB;AAKD;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AA0CzC;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwDnF"}
@@ -18,7 +18,7 @@ import {
18
18
  resolveMedia,
19
19
  sanitizeMediaName,
20
20
  setMediaProvider
21
- } from "../chunk-CZ3YXGK5.js";
21
+ } from "../chunk-SOILPZ7U.js";
22
22
  import "../chunk-YBVK254P.js";
23
23
  import {
24
24
  ImageManifestSchema,
@@ -26,7 +26,7 @@ import {
26
26
  MediaFolderSchema,
27
27
  MediaItemSchema,
28
28
  VariantSchema
29
- } from "../chunk-KMQZYORS.js";
29
+ } from "../chunk-RUKO2N3F.js";
30
30
  import "../chunk-FM4M4Q2W.js";
31
31
  import "../chunk-JSBRDJBE.js";
32
32
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"preview-inputs.d.ts","sourceRoot":"","sources":["../../src/media/preview-inputs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE5D;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GACjD;IAAE,MAAM,EAAE,kBAAkB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAuCnD;AAKD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAKpF"}
1
+ {"version":3,"file":"preview-inputs.d.ts","sourceRoot":"","sources":["../../src/media/preview-inputs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE5D;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GACjD;IAAE,MAAM,EAAE,kBAAkB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAyCnD;AAKD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAKpF"}
@@ -3,7 +3,7 @@ import {
3
3
  AudienceNameSchema,
4
4
  MediaGridOptionsSchema,
5
5
  slugifyAudienceName
6
- } from "../chunk-MK7YB2A4.js";
6
+ } from "../chunk-Z66TBL4Q.js";
7
7
  import {
8
8
  AudienceSchema,
9
9
  RoleSchema,
@@ -41,7 +41,7 @@ import {
41
41
  TextLineSchema,
42
42
  VariantSchema,
43
43
  normalizeSiteIndex
44
- } from "../chunk-KMQZYORS.js";
44
+ } from "../chunk-RUKO2N3F.js";
45
45
  import "../chunk-VKAGMEKE.js";
46
46
  import "../chunk-FM4M4Q2W.js";
47
47
  import "../chunk-NSCT3AMV.js";
@@ -485,6 +485,7 @@ export declare const SiteConfigSchema: z.ZodObject<{
485
485
  imageId: z.ZodString;
486
486
  width: z.ZodNumber;
487
487
  height: z.ZodNumber;
488
+ version: z.ZodDefault<z.ZodNumber>;
488
489
  }, z.core.$strip>>>;
489
490
  description: z.ZodDefault<z.ZodNullable<z.ZodString>>;
490
491
  chatbot: z.ZodDefault<z.ZodObject<{
@@ -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;AASxB,eAAO,MAAM,YAAY;;;;;+FAExB,CAAC;AAEF,eAAO,MAAM,iBAAiB;;;;;;;;;iBAI5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAQ5D,eAAO,MAAM,cAAc,8EAA+E,CAAC;AAW3G,eAAO,MAAM,gBAAgB;;;EAA+B,CAAC;AAE7D,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAUrB,CAAC;AAEH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AA0C9C,QAAA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6CtB,CAAC;AAEL,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAAyE,CAAC;AAElG,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAE7D,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAE1D;AAMD,eAAO,MAAM,iBAAiB;;;;;iBAM5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;iBAiC5B,CAAC;AACL,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyD3B,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;AASxB,eAAO,MAAM,YAAY;;;;;+FAExB,CAAC;AAEF,eAAO,MAAM,iBAAiB;;;;;;;;;iBAI5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAQ5D,eAAO,MAAM,cAAc,8EAA+E,CAAC;AAW3G,eAAO,MAAM,gBAAgB;;;EAA+B,CAAC;AAE7D,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAUrB,CAAC;AAEH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AA0C9C,QAAA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6CtB,CAAC;AAEL,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAAyE,CAAC;AAElG,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAE7D,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAE1D;AAMD,eAAO,MAAM,iBAAiB;;;;;iBAM5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;iBAiC5B,CAAC;AACL,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6D3B,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.91",
3
+ "version": "0.1.92",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",
@@ -10,7 +10,7 @@ import type { SiteIndex, SiteConfig, Page } from "../../schemas/site-config";
10
10
  import type { Audience } from "../../auth/types";
11
11
  import type { MediaManifest } from "../../media/types";
12
12
  import type { QueueItem } from "../../media/queue";
13
- import { composePreviewImage } from "../../media/compose-preview";
13
+ import { composePreviewImage, PREVIEW_COMPOSE_VERSION } from "../../media/compose-preview";
14
14
  import { derivePreviewInputs, servedPreviewSize } from "../../media/preview-inputs";
15
15
  import { resolveMedia } from "../../media/resolve";
16
16
  import { getMediaProvider } from "../../media";
@@ -264,6 +264,9 @@ export default function EditorShell({
264
264
  // dedupes concurrent runs against the same composition signature.
265
265
  const previewInitializedRef = useRef(false);
266
266
  const lastPreviewSigRef = useRef<string | null>(null);
267
+ // Increments per compose; the async upload callback applies its result only if
268
+ // still current, so a slower earlier upload can't overwrite a newer preview.
269
+ const previewGenRef = useRef(0);
267
270
 
268
271
  // --- Collaboration refs ---
269
272
  // Freshest dirty-section set for the inbound remote-section handler (never
@@ -1376,12 +1379,14 @@ export default function EditorShell({
1376
1379
  };
1377
1380
  const { inputs, signature } = derivePreviewInputs(siteConfig, resolveLogoUrl);
1378
1381
 
1379
- // On first run: if a preview already exists, seed the signature and skip
1380
- // (don't recompose/re-upload on every editor open). If none exists yet,
1381
- // fall through to compose one. Subsequent runs recompose only on change.
1382
+ // On first run: if a preview already exists AND was composed by the current
1383
+ // logic version, seed the signature and skip (don't recompose on every open).
1384
+ // A missing preview, or one stamped with an older version (or 0 for
1385
+ // pre-versioning rasters), falls through to recompose so stale rasters
1386
+ // self-heal. Subsequent runs recompose only on change.
1382
1387
  if (!previewInitializedRef.current) {
1383
1388
  previewInitializedRef.current = true;
1384
- if (siteConfig.previewImage) {
1389
+ if (siteConfig.previewImage && siteConfig.previewImage.version === PREVIEW_COMPOSE_VERSION) {
1385
1390
  lastPreviewSigRef.current = signature;
1386
1391
  return;
1387
1392
  }
@@ -1390,13 +1395,19 @@ export default function EditorShell({
1390
1395
 
1391
1396
  const timer = setTimeout(() => {
1392
1397
  lastPreviewSigRef.current = signature; // guard against duplicate concurrent runs
1398
+ const gen = ++previewGenRef.current;
1393
1399
  (async () => {
1394
1400
  try {
1395
1401
  const blob = await composePreviewImage(inputs);
1396
1402
  const file = new File([blob], "site-preview.webp", { type: "image/webp" });
1397
1403
  const { width, height } = servedPreviewSize(siteConfigRef.current!.media.sizes);
1398
1404
  mediaPipeline.contextValue.uploadFileWithCallback(file, (imageId) => {
1399
- handleSiteConfigChange({ ...siteConfigRef.current!, previewImage: { imageId, width, height } });
1405
+ // Ignore a stale upload: a newer compose has superseded this one.
1406
+ if (gen !== previewGenRef.current) return;
1407
+ handleSiteConfigChange({
1408
+ ...siteConfigRef.current!,
1409
+ previewImage: { imageId, width, height, version: PREVIEW_COMPOSE_VERSION },
1410
+ });
1400
1411
  }, { hidden: true });
1401
1412
  } catch (err) {
1402
1413
  console.error("[EditorShell] preview-image composition failed:", err);
@@ -4,7 +4,7 @@ import { Button } from "../shared/Button";
4
4
  import { safeRedirect } from "../../lib/safeRedirect";
5
5
 
6
6
  interface Props {
7
- audiences: Array<{ name: string; displayName: string }>;
7
+ audiences: Array<{ name: string; displayName: string; hasPassword?: boolean }>;
8
8
  next?: string | null;
9
9
  }
10
10
 
@@ -14,12 +14,21 @@ export function ViewerLoginForm({ audiences, next }: Props) {
14
14
  const [error, setError] = useState<string | null>(null);
15
15
  const [loading, setLoading] = useState(false);
16
16
 
17
+ // An open (passwordless) audience needs no password field — enter with one click.
18
+ // Default to requiring a password until an audience is chosen so the field
19
+ // doesn't flash for the "Select audience..." state.
20
+ const selected = audiences.find((a) => a.name === audience);
21
+ const needsPassword = selected ? selected.hasPassword !== false : true;
22
+
17
23
  async function handleSubmit(e: FormEvent) {
18
24
  e.preventDefault();
19
25
  setLoading(true);
20
26
  setError(null);
21
27
 
22
- const body: { audience: string; password: string; next?: string } = { audience, password };
28
+ const body: { audience: string; password: string; next?: string } = {
29
+ audience,
30
+ password: needsPassword ? password : "",
31
+ };
23
32
  if (next) body.next = next;
24
33
 
25
34
  try {
@@ -67,12 +76,14 @@ export function ViewerLoginForm({ audiences, next }: Props) {
67
76
  </select>
68
77
  </div>
69
78
 
70
- <PasswordInput
71
- label="Password"
72
- value={password}
73
- onChange={setPassword}
74
- autoComplete="current-password"
75
- />
79
+ {needsPassword && (
80
+ <PasswordInput
81
+ label="Password"
82
+ value={password}
83
+ onChange={setPassword}
84
+ autoComplete="current-password"
85
+ />
86
+ )}
76
87
 
77
88
  {error && (
78
89
  <p className="text-sm text-red-600">{error}</p>
@@ -10,6 +10,9 @@ export interface PreviewImageInputs {
10
10
  invertLogo: boolean;
11
11
  siteName: string;
12
12
  headingFont: string;
13
+ /** The site's Google Fonts CSS URL (or null for system-ui), used to load the
14
+ * heading font before drawing so text mode never falls back to system-ui. */
15
+ googleFontsUrl: string | null;
13
16
  /** Computed contrasting text color (text mode). */
14
17
  textColor: string;
15
18
  }
@@ -17,6 +20,53 @@ export interface PreviewImageInputs {
17
20
  const WIDTH = 1200;
18
21
  const HEIGHT = 630;
19
22
 
23
+ /**
24
+ * Bump when the composition logic changes so already-composed rasters (stamped
25
+ * with the old version, or 0 for pre-versioning ones) regenerate on next editor
26
+ * open. v2 = load the Google heading font before drawing + draw at weight 700.
27
+ */
28
+ export const PREVIEW_COMPOSE_VERSION = 2;
29
+
30
+ // The raster draws the site name at this weight; the loader ensures this exact
31
+ // weight is available so canvas doesn't silently fall back to a nearest weight.
32
+ const HEADING_WEIGHT = 700;
33
+
34
+ /**
35
+ * Ensure the heading font is actually loaded before canvas draws with it.
36
+ * `document.fonts.load` only fetches a family that already has a matching
37
+ * @font-face rule registered; in the editor that rule comes from a <link> that
38
+ * may not be parsed yet when composition runs, so an ambient load() can resolve
39
+ * empty and the draw silently falls back to system-ui. If the ambient load
40
+ * yields nothing, fetch the Google Fonts CSS and register the weight-700
41
+ * face(s) explicitly via the CSS Font Loading API.
42
+ */
43
+ async function loadHeadingFont(family: string, googleFontsUrl: string | null): Promise<void> {
44
+ if (!family || family === "system-ui" || !googleFontsUrl || typeof FontFace === "undefined") return;
45
+ const spec = `${HEADING_WEIGHT} 84px "${family}"`;
46
+ try {
47
+ const loaded = await document.fonts.load(spec);
48
+ if (loaded.length > 0) return;
49
+ } catch {
50
+ // fall through to explicit registration
51
+ }
52
+ try {
53
+ const css = await (await fetch(googleFontsUrl)).text();
54
+ const blocks = css.match(/@font-face\s*\{[^}]*\}/g) ?? [];
55
+ const wanted = blocks.filter((b) => new RegExp(`font-weight:\\s*${HEADING_WEIGHT}\\b`).test(b));
56
+ await Promise.all(
57
+ (wanted.length ? wanted : blocks).map(async (block) => {
58
+ const url = block.match(/url\(([^)]+)\)/)?.[1];
59
+ if (!url) return;
60
+ const face = new FontFace(family, `url(${url})`, { weight: String(HEADING_WEIGHT) });
61
+ await face.load();
62
+ document.fonts.add(face);
63
+ }),
64
+ );
65
+ } catch {
66
+ // give up — the draw below falls back to system-ui
67
+ }
68
+ }
69
+
20
70
  /**
21
71
  * Composes the 1200×630 social-preview raster in the browser (Canvas API only,
22
72
  * no dependency). Returns a WebP blob. Must run client-side (uses document/Image
@@ -60,13 +110,15 @@ export async function composePreviewImage(inputs: PreviewImageInputs): Promise<B
60
110
 
61
111
  if (!drewLogo) {
62
112
  if (document.fonts?.ready) await document.fonts.ready;
63
- // fillText doesn't itself trigger a font load — explicitly load the exact
64
- // weight/family used below so it's available before we draw.
65
- await document.fonts.load(`600 84px ${inputs.headingFont}`).catch(() => {});
113
+ // Load the heading font (exact weight) before drawing — fillText doesn't
114
+ // itself trigger a webfont load, so without this canvas falls back silently.
115
+ await loadHeadingFont(inputs.headingFont, inputs.googleFontsUrl);
66
116
  ctx.fillStyle = inputs.textColor;
67
117
  ctx.textAlign = "center";
68
118
  ctx.textBaseline = "middle";
69
- ctx.font = `600 84px ${inputs.headingFont}, system-ui, sans-serif`;
119
+ // Quote the family: multi-word names ("Playfair Display") break the CSS font
120
+ // shorthand parse when unquoted, which would drop the whole declaration.
121
+ ctx.font = `${HEADING_WEIGHT} 84px "${inputs.headingFont}", system-ui, sans-serif`;
70
122
  ctx.fillText(inputs.siteName, WIDTH / 2, HEIGHT / 2, WIDTH * 0.86);
71
123
  }
72
124
 
@@ -19,7 +19,7 @@ export function derivePreviewInputs(
19
19
  siteConfig: SiteConfig,
20
20
  resolveLogoUrl: (imageId: string) => string | null,
21
21
  ): { inputs: PreviewImageInputs; signature: string } {
22
- const { logo, darkMode, primaryColor, siteName, headingFont } = siteConfig;
22
+ const { logo, darkMode, primaryColor, siteName, headingFont, googleFontsUrl } = siteConfig;
23
23
  const dark = darkMode === "dark";
24
24
 
25
25
  let mode: "logo" | "text" = "text";
@@ -43,6 +43,7 @@ export function derivePreviewInputs(
43
43
  invertLogo,
44
44
  siteName,
45
45
  headingFont,
46
+ googleFontsUrl,
46
47
  textColor: contrastColor(primaryColor),
47
48
  };
48
49
 
@@ -53,6 +54,7 @@ export function derivePreviewInputs(
53
54
  invertLogo: inputs.invertLogo,
54
55
  siteName: inputs.siteName,
55
56
  headingFont: inputs.headingFont,
57
+ googleFontsUrl: inputs.googleFontsUrl,
56
58
  textColor: inputs.textColor,
57
59
  });
58
60
 
@@ -245,6 +245,10 @@ export const SiteConfigSchema = z.object({
245
245
  imageId: z.string(),
246
246
  width: z.number().int().positive(),
247
247
  height: z.number().int().positive(),
248
+ // Compose-logic version that produced this raster. Pre-existing rasters
249
+ // (and manifests written before this field) default to 0, which is below
250
+ // any current PREVIEW_COMPOSE_VERSION, so the editor regenerates them.
251
+ version: z.number().int().nonnegative().default(0),
248
252
  })
249
253
  .nullable()
250
254
  .default(null),