@drawnagency/primitives 0.1.86 → 0.1.87

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 (39) hide show
  1. package/dist/{chunk-665DW3HR.js → chunk-6VOIZP2O.js} +3 -3
  2. package/dist/{chunk-YKOLOZB2.js → chunk-DRCPDOW3.js} +14 -0
  3. package/dist/{chunk-MBJUWOM2.js → chunk-EO7K45GV.js} +1 -1
  4. package/dist/{chunk-DKXISNJ2.js → chunk-NEDOJHHR.js} +2 -2
  5. package/dist/{chunk-DO3ZENLN.js → chunk-OEL57NYV.js} +1 -1
  6. package/dist/{chunk-FDBZZ4AT.js → chunk-PTVY3Q7P.js} +1 -1
  7. package/dist/{chunk-2CVP2AGK.js → chunk-VY5V4HBV.js} +6 -6
  8. package/dist/{chunk-EP3LVOXZ.js → chunk-WA3HFN54.js} +1 -1
  9. package/dist/{chunk-YAPG44NS.js → chunk-XDG2K6UT.js} +1 -1
  10. package/dist/{chunk-E3MTIMOG.js → chunk-XPUBUR64.js} +10 -0
  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/shared/Navigation.d.ts +6 -1
  17. package/dist/components/shared/Navigation.d.ts.map +1 -1
  18. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  19. package/dist/components/shell/SiteSettingsDisplay.d.ts.map +1 -1
  20. package/dist/hooks/useMediaPipeline.d.ts +2 -1
  21. package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
  22. package/dist/index.js +6 -4
  23. package/dist/lib/image-refs.d.ts +9 -0
  24. package/dist/lib/image-refs.d.ts.map +1 -1
  25. package/dist/lib/index.js +2 -2
  26. package/dist/media/index.js +4 -2
  27. package/dist/media/resolve.d.ts +25 -0
  28. package/dist/media/resolve.d.ts.map +1 -1
  29. package/dist/schemas/index.js +2 -2
  30. package/dist/schemas/site-config.d.ts +10 -0
  31. package/dist/schemas/site-config.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/components/shared/Navigation.tsx +20 -2
  34. package/src/components/shell/EditorShell.tsx +4 -0
  35. package/src/components/shell/SiteSettingsDisplay.tsx +106 -0
  36. package/src/hooks/useMediaPipeline.ts +16 -3
  37. package/src/lib/image-refs.ts +19 -0
  38. package/src/media/resolve.ts +34 -0
  39. package/src/schemas/site-config.ts +13 -0
@@ -9,13 +9,13 @@ import {
9
9
  MediaBlock,
10
10
  containerGridClass,
11
11
  useEditableCollection
12
- } from "./chunk-DKXISNJ2.js";
12
+ } from "./chunk-NEDOJHHR.js";
13
13
  import {
14
14
  AddIcon,
15
15
  DeleteIcon,
16
16
  DragHandle,
17
17
  IconButton
18
- } from "./chunk-MBJUWOM2.js";
18
+ } from "./chunk-EO7K45GV.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-E3MTIMOG.js";
33
+ } from "./chunk-XPUBUR64.js";
34
34
  import {
35
35
  defineSection
36
36
  } from "./chunk-VKAGMEKE.js";
@@ -229,6 +229,19 @@ function rebindImageRef(ref, imageId) {
229
229
  for (const key of BAKED_MEDIA_KEYS) delete fresh[key];
230
230
  return fresh;
231
231
  }
232
+ function resolveLogo(logo, manifest, provider) {
233
+ if (!logo) return null;
234
+ const light = resolveMedia({ imageId: logo.light.imageId }, manifest, [1], provider);
235
+ if (!light?.src || light.tag === "video") return null;
236
+ const dark = logo.dark ? resolveMedia({ imageId: logo.dark.imageId }, manifest, [1], provider) : null;
237
+ const darkUrl = dark && dark.tag !== "video" ? dark.src : null;
238
+ return {
239
+ lightUrl: light.src,
240
+ darkUrl,
241
+ invertInDark: logo.invertInDark,
242
+ showInNav: logo.showInNav
243
+ };
244
+ }
232
245
 
233
246
  // src/media/videoPoster.ts
234
247
  function generateVideoPoster(blob, quality) {
@@ -324,6 +337,7 @@ export {
324
337
  resolveManifestReferences,
325
338
  BAKED_MEDIA_KEYS,
326
339
  rebindImageRef,
340
+ resolveLogo,
327
341
  generateVideoPoster,
328
342
  setMediaProvider,
329
343
  getMediaProvider
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getMediaProvider
3
- } from "./chunk-YKOLOZB2.js";
3
+ } from "./chunk-DRCPDOW3.js";
4
4
  import {
5
5
  cn
6
6
  } from "./chunk-7EOSDFS3.js";
@@ -6,10 +6,10 @@ import {
6
6
  SettingsIcon,
7
7
  useMediaLibrary,
8
8
  useResolvedMedia
9
- } from "./chunk-MBJUWOM2.js";
9
+ } from "./chunk-EO7K45GV.js";
10
10
  import {
11
11
  rebindImageRef
12
- } from "./chunk-YKOLOZB2.js";
12
+ } from "./chunk-DRCPDOW3.js";
13
13
  import {
14
14
  cn
15
15
  } from "./chunk-7EOSDFS3.js";
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-TZQPOR5A.js";
10
10
  import {
11
11
  IndexSchema
12
- } from "./chunk-E3MTIMOG.js";
12
+ } from "./chunk-XPUBUR64.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-DKXISNJ2.js";
3
+ } from "./chunk-NEDOJHHR.js";
4
4
  import {
5
5
  Button,
6
6
  usePages
@@ -3,7 +3,7 @@ import {
3
3
  pluralize,
4
4
  stripHtmlToPlainText,
5
5
  truncate
6
- } from "./chunk-665DW3HR.js";
6
+ } from "./chunk-6VOIZP2O.js";
7
7
  import {
8
8
  DeleteButton,
9
9
  DragHandle,
@@ -15,11 +15,11 @@ import {
15
15
  buildBlockDropData,
16
16
  canDropBlock,
17
17
  isBlockDragData
18
- } from "./chunk-YAPG44NS.js";
18
+ } from "./chunk-XDG2K6UT.js";
19
19
  import {
20
20
  EditableRichText,
21
21
  defineHeadingSection
22
- } from "./chunk-FDBZZ4AT.js";
22
+ } from "./chunk-PTVY3Q7P.js";
23
23
  import {
24
24
  EditablePlainText,
25
25
  ImageDropZone,
@@ -28,11 +28,11 @@ import {
28
28
  colSpanClass,
29
29
  containerGridClass,
30
30
  spacerHiddenClass
31
- } from "./chunk-DKXISNJ2.js";
31
+ } from "./chunk-NEDOJHHR.js";
32
32
  import {
33
33
  useMediaLibrary,
34
34
  useResolvedMedia
35
- } from "./chunk-MBJUWOM2.js";
35
+ } from "./chunk-EO7K45GV.js";
36
36
  import {
37
37
  embedUrl
38
38
  } from "./chunk-Y4JQGO6G.js";
@@ -51,7 +51,7 @@ import {
51
51
  } from "./chunk-TZQPOR5A.js";
52
52
  import {
53
53
  SingleMediaReferenceSchema
54
- } from "./chunk-E3MTIMOG.js";
54
+ } from "./chunk-XPUBUR64.js";
55
55
  import {
56
56
  defineSection,
57
57
  getSection
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HexColorSchema
3
- } from "./chunk-E3MTIMOG.js";
3
+ } from "./chunk-XPUBUR64.js";
4
4
 
5
5
  // src/schemas/audience.ts
6
6
  import { z } from "zod";
@@ -14,7 +14,7 @@ import {
14
14
  SettingsIcon,
15
15
  useMediaLibrary,
16
16
  useResolvedMedia
17
- } from "./chunk-MBJUWOM2.js";
17
+ } from "./chunk-EO7K45GV.js";
18
18
  import {
19
19
  cn
20
20
  } from "./chunk-7EOSDFS3.js";
@@ -277,6 +277,16 @@ var SiteConfigSchema = z3.object({
277
277
  // Favicon stored as a data URL (SVG/PNG, small) so it travels with
278
278
  // site-config.json — no media manifest or CDN round-trip needed.
279
279
  favicon: z3.string().refine((v) => v.startsWith("data:image/"), "Favicon must be an image data URL").nullable().default(null),
280
+ // Optional site logo, stored as media-library references (so it participates
281
+ // in usage tracking + GC protection, and its resolved URL is reusable for the
282
+ // Phase 3 social-preview image). Every field defaults so pre-existing
283
+ // site-config.json files keep parsing.
284
+ logo: z3.object({
285
+ light: z3.object({ imageId: z3.string() }),
286
+ dark: z3.object({ imageId: z3.string() }).nullable().default(null),
287
+ invertInDark: z3.boolean().default(false),
288
+ showInNav: z3.boolean().default(true)
289
+ }).nullable().default(null),
280
290
  media: MediaConfigSchema.default(MediaConfigSchema.parse({})),
281
291
  // Brand chatbot config (editor-owned; the platform kill switch lives in the
282
292
  // sites table, not here). Every field defaults so pre-existing
@@ -11,20 +11,20 @@ import {
11
11
  buildBlockDropData,
12
12
  canDropBlock,
13
13
  isBlockDragData
14
- } from "../../chunk-YAPG44NS.js";
14
+ } from "../../chunk-XDG2K6UT.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-MBJUWOM2.js";
21
- import "../../chunk-YKOLOZB2.js";
20
+ } from "../../chunk-EO7K45GV.js";
21
+ import "../../chunk-DRCPDOW3.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-E3MTIMOG.js";
27
+ import "../../chunk-XPUBUR64.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-FDBZZ4AT.js";
5
+ } from "../../chunk-PTVY3Q7P.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-DKXISNJ2.js";
13
+ } from "../../chunk-NEDOJHHR.js";
14
14
  import "../../chunk-ZSPCE6MZ.js";
15
15
  import {
16
16
  DeleteIcon,
17
17
  DragHandle,
18
18
  IconButton
19
- } from "../../chunk-MBJUWOM2.js";
20
- import "../../chunk-YKOLOZB2.js";
19
+ } from "../../chunk-EO7K45GV.js";
20
+ import "../../chunk-DRCPDOW3.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-E3MTIMOG.js";
26
+ import "../../chunk-XPUBUR64.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-665DW3HR.js";
5
+ } from "../../chunk-6VOIZP2O.js";
6
6
  import "../../chunk-5THS4K5D.js";
7
- import "../../chunk-DKXISNJ2.js";
8
- import "../../chunk-MBJUWOM2.js";
9
- import "../../chunk-YKOLOZB2.js";
7
+ import "../../chunk-NEDOJHHR.js";
8
+ import "../../chunk-EO7K45GV.js";
9
+ import "../../chunk-DRCPDOW3.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-E3MTIMOG.js";
14
+ import "../../chunk-XPUBUR64.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-2CVP2AGK.js";
4
- import "../../chunk-665DW3HR.js";
5
- import "../../chunk-YAPG44NS.js";
3
+ } from "../../chunk-VY5V4HBV.js";
4
+ import "../../chunk-6VOIZP2O.js";
5
+ import "../../chunk-XDG2K6UT.js";
6
6
  import "../../chunk-5THS4K5D.js";
7
- import "../../chunk-FDBZZ4AT.js";
8
- import "../../chunk-DKXISNJ2.js";
7
+ import "../../chunk-PTVY3Q7P.js";
8
+ import "../../chunk-NEDOJHHR.js";
9
9
  import "../../chunk-ZSPCE6MZ.js";
10
- import "../../chunk-MBJUWOM2.js";
10
+ import "../../chunk-EO7K45GV.js";
11
11
  import "../../chunk-Y4JQGO6G.js";
12
- import "../../chunk-YKOLOZB2.js";
12
+ import "../../chunk-DRCPDOW3.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-E3MTIMOG.js";
19
+ import "../../chunk-XPUBUR64.js";
20
20
  import {
21
21
  registerRichText,
22
22
  registerSchema
@@ -1,22 +1,22 @@
1
1
  import {
2
2
  allSectionDefs
3
- } from "../../chunk-2CVP2AGK.js";
4
- import "../../chunk-665DW3HR.js";
5
- import "../../chunk-YAPG44NS.js";
3
+ } from "../../chunk-VY5V4HBV.js";
4
+ import "../../chunk-6VOIZP2O.js";
5
+ import "../../chunk-XDG2K6UT.js";
6
6
  import "../../chunk-5THS4K5D.js";
7
- import "../../chunk-FDBZZ4AT.js";
8
- import "../../chunk-DKXISNJ2.js";
7
+ import "../../chunk-PTVY3Q7P.js";
8
+ import "../../chunk-NEDOJHHR.js";
9
9
  import "../../chunk-ZSPCE6MZ.js";
10
- import "../../chunk-MBJUWOM2.js";
10
+ import "../../chunk-EO7K45GV.js";
11
11
  import "../../chunk-Y4JQGO6G.js";
12
- import "../../chunk-YKOLOZB2.js";
12
+ import "../../chunk-DRCPDOW3.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-E3MTIMOG.js";
19
+ import "../../chunk-XPUBUR64.js";
20
20
  import {
21
21
  registerSection
22
22
  } from "../../chunk-VKAGMEKE.js";
@@ -4,8 +4,13 @@ interface Props {
4
4
  siteName: string;
5
5
  darkMode: "light" | "dark" | "optional";
6
6
  lastUpdated: string | null;
7
+ logo?: {
8
+ lightUrl: string;
9
+ darkUrl: string | null;
10
+ invertInDark: boolean;
11
+ } | null;
7
12
  onPageSelect?: (pageId: string) => void;
8
13
  }
9
- export default function Navigation({ siteNav: initialNav, siteName, darkMode, lastUpdated, onPageSelect }: Props): import("react/jsx-runtime").JSX.Element;
14
+ export default function Navigation({ siteNav: initialNav, siteName, darkMode, lastUpdated, logo, onPageSelect }: Props): import("react/jsx-runtime").JSX.Element;
10
15
  export {};
11
16
  //# sourceMappingURL=Navigation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Navigation.d.ts","sourceRoot":"","sources":["../../../src/components/shared/Navigation.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAW,MAAM,eAAe,CAAC;AAOtD,UAAU,KAAK;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,UAAU,CAAC;IACxC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAG3B,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,EAAE,KAAK,2CAwO/G"}
1
+ {"version":3,"file":"Navigation.d.ts","sourceRoot":"","sources":["../../../src/components/shared/Navigation.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAW,MAAM,eAAe,CAAC;AAOtD,UAAU,KAAK;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,UAAU,CAAC;IACxC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,IAAI,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,YAAY,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAGlF,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,KAAK,2CAyPrH"}
@@ -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;AASjD,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAuDtF,OAAO,KAAK,EAAE,eAAe,EAA2D,MAAM,oBAAoB,CAAC;AAmCnH,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,2CAshDP;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;AASjD,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAuDtF,OAAO,KAAK,EAAE,eAAe,EAA2D,MAAM,oBAAoB,CAAC;AAmCnH,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,2CA0hDP;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 +1 @@
1
- {"version":3,"file":"SiteSettingsDisplay.d.ts","sourceRoot":"","sources":["../../../src/components/shell/SiteSettingsDisplay.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,UAAU,KAAK;IACb,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;CACxC;AAkBD,wBAAgB,mBAAmB,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,KAAK,2CA6KlE"}
1
+ {"version":3,"file":"SiteSettingsDisplay.d.ts","sourceRoot":"","sources":["../../../src/components/shell/SiteSettingsDisplay.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAG5D,UAAU,KAAK;IACb,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;CACxC;AA4BD,wBAAgB,mBAAmB,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,KAAK,2CA2QlE"}
@@ -15,8 +15,9 @@ interface UseMediaPipelineInput {
15
15
  markSectionDirty: (sectionId: string, content: SectionContent) => void;
16
16
  onUploadError?: (message: string) => void;
17
17
  onUploadNotice?: (message: string) => void;
18
+ onSiteConfigChange?: (updater: (prev: SiteConfig) => SiteConfig) => void;
18
19
  }
19
- export declare function useMediaPipeline({ siteConfig, mediaManifest, setMediaManifest, sections, setSections, setLocalChangesExist, setDirtySectionIds, markSectionDirty, onUploadError, onUploadNotice, }: UseMediaPipelineInput): {
20
+ export declare function useMediaPipeline({ siteConfig, mediaManifest, setMediaManifest, sections, setSections, setLocalChangesExist, setDirtySectionIds, markSectionDirty, onUploadError, onUploadNotice, onSiteConfigChange, }: UseMediaPipelineInput): {
20
21
  processingItems: QueueItem[];
21
22
  pendingMediaItems: {
22
23
  id: string;
@@ -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;AAexF,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;IAIvE,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAG1C,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,QAAQ,EACR,WAAW,EACX,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,cAAc,GACf,EAAE,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAkS8B,IAAI,EAAE,mBAAkB,MAAM,GAAG,IAAI;6BAyDvC,MAAM,EAAE;+BA+CZ,MAAM,OAAO,MAAM;+BAiBnB,MAAM,YAAY,MAAM,GAAG,IAAI,KAAG,MAAM;6BAW1C,MAAM,QAAQ,MAAM;2BAWtB,MAAM,YAAY,MAAM,GAAG,IAAI;6BA4BvB,MAAM,YAAY,MAAM,GAAG,QAAQ;2BAd3C,MAAM,EAAE,YAAY,MAAM,GAAG,IAAI;kCA4DhE;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,EAAE,mBAC9B,MAAM,GAAG,IAAI;;;wBAiCO,IAAI,eAAe,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI;EAoFpF"}
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;AAexF,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;IAIvE,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAG1C,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAI3C,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,UAAU,KAAK,IAAI,CAAC;CAC1E;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,QAAQ,EACR,WAAW,EACX,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,kBAAkB,GACnB,EAAE,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAkS8B,IAAI,EAAE,mBAAkB,MAAM,GAAG,IAAI;6BAyDvC,MAAM,EAAE;+BAsDZ,MAAM,OAAO,MAAM;+BAiBnB,MAAM,YAAY,MAAM,GAAG,IAAI,KAAG,MAAM;6BAW1C,MAAM,QAAQ,MAAM;2BAWtB,MAAM,YAAY,MAAM,GAAG,IAAI;6BA4BvB,MAAM,YAAY,MAAM,GAAG,QAAQ;2BAd3C,MAAM,EAAE,YAAY,MAAM,GAAG,IAAI;kCA4DhE;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,EAAE,mBAC9B,MAAM,GAAG,IAAI;;;wBAkCO,IAAI,eAAe,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI;EAoFpF"}
package/dist/index.js CHANGED
@@ -26,11 +26,12 @@ import {
26
26
  hashFileBuffer,
27
27
  mimeToExt,
28
28
  rebindImageRef,
29
+ resolveLogo,
29
30
  resolveManifestReferences,
30
31
  resolveMedia,
31
32
  sanitizeMediaName,
32
33
  setMediaProvider
33
- } from "./chunk-YKOLOZB2.js";
34
+ } from "./chunk-DRCPDOW3.js";
34
35
  import {
35
36
  CollabPeerSchema,
36
37
  HEARTBEAT_INTERVAL_MS,
@@ -49,7 +50,7 @@ import {
49
50
  AudienceNameSchema,
50
51
  MediaGridOptionsSchema,
51
52
  slugifyAudienceName
52
- } from "./chunk-EP3LVOXZ.js";
53
+ } from "./chunk-WA3HFN54.js";
53
54
  import {
54
55
  AudienceSchema,
55
56
  RoleSchema,
@@ -71,7 +72,7 @@ import {
71
72
  loadStaticSiteContent,
72
73
  mergeSiteContent,
73
74
  safeRedirect
74
- } from "./chunk-DO3ZENLN.js";
75
+ } from "./chunk-OEL57NYV.js";
75
76
  import {
76
77
  curatedIcons,
77
78
  getIcon
@@ -124,7 +125,7 @@ import {
124
125
  TextLineSchema,
125
126
  VariantSchema,
126
127
  normalizeSiteIndex
127
- } from "./chunk-E3MTIMOG.js";
128
+ } from "./chunk-XPUBUR64.js";
128
129
  import {
129
130
  clearRegistry,
130
131
  createRegistry,
@@ -243,6 +244,7 @@ export {
243
244
  registerSchema,
244
245
  registerSection,
245
246
  requireSessionSecret,
247
+ resolveLogo,
246
248
  resolveManifestReferences,
247
249
  resolveMedia,
248
250
  safeNextPath,
@@ -1,3 +1,4 @@
1
+ import type { SiteConfig } from "../schemas/site-config";
1
2
  /**
2
3
  * Structural walkers over section content for `imageId` references.
3
4
  *
@@ -18,4 +19,12 @@ export declare function collectImageIds(value: unknown): Set<string>;
18
19
  * including arrays and nesting, is preserved unchanged.
19
20
  */
20
21
  export declare function clearImageIds(value: unknown, ids: Set<string>): unknown;
22
+ /**
23
+ * Applies a media-deletion id set to a site logo config, granularly: deleting
24
+ * the light variant's backing asset removes the logo entirely (no light = no
25
+ * logo to show), while deleting only the dark variant's asset keeps the
26
+ * light logo + flags and drops just the dark override. Returns the same
27
+ * `logo` reference unchanged when neither id was deleted.
28
+ */
29
+ export declare function applyLogoDeletion(logo: SiteConfig["logo"], deletedIds: Set<string>): SiteConfig["logo"];
21
30
  //# sourceMappingURL=image-refs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"image-refs.d.ts","sourceRoot":"","sources":["../../src/lib/image-refs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,GAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAa,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAa1G;AAED,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAE3D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAevE"}
1
+ {"version":3,"file":"image-refs.d.ts","sourceRoot":"","sources":["../../src/lib/image-refs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD;;;;;;;;;GASG;AAEH,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,GAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAa,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAa1G;AAED,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAE3D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAevE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,EACxB,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,GACtB,UAAU,CAAC,MAAM,CAAC,CAKpB"}
package/dist/lib/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  loadStaticSiteContent,
8
8
  mergeSiteContent,
9
9
  safeRedirect
10
- } from "../chunk-DO3ZENLN.js";
10
+ } from "../chunk-OEL57NYV.js";
11
11
  import {
12
12
  curatedIcons,
13
13
  getIcon
@@ -33,7 +33,7 @@ import {
33
33
  } from "../chunk-YBVK254P.js";
34
34
  import "../chunk-75P6HWPN.js";
35
35
  import "../chunk-TZQPOR5A.js";
36
- import "../chunk-E3MTIMOG.js";
36
+ import "../chunk-XPUBUR64.js";
37
37
  import {
38
38
  clearRegistry,
39
39
  createRegistry,
@@ -12,11 +12,12 @@ import {
12
12
  hashFileBuffer,
13
13
  mimeToExt,
14
14
  rebindImageRef,
15
+ resolveLogo,
15
16
  resolveManifestReferences,
16
17
  resolveMedia,
17
18
  sanitizeMediaName,
18
19
  setMediaProvider
19
- } from "../chunk-YKOLOZB2.js";
20
+ } from "../chunk-DRCPDOW3.js";
20
21
  import "../chunk-YBVK254P.js";
21
22
  import {
22
23
  ImageManifestSchema,
@@ -24,7 +25,7 @@ import {
24
25
  MediaFolderSchema,
25
26
  MediaItemSchema,
26
27
  VariantSchema
27
- } from "../chunk-E3MTIMOG.js";
28
+ } from "../chunk-XPUBUR64.js";
28
29
  import "../chunk-FM4M4Q2W.js";
29
30
  import "../chunk-JSBRDJBE.js";
30
31
  export {
@@ -46,6 +47,7 @@ export {
46
47
  hashFileBuffer,
47
48
  mimeToExt,
48
49
  rebindImageRef,
50
+ resolveLogo,
49
51
  resolveManifestReferences,
50
52
  resolveMedia,
51
53
  sanitizeMediaName,
@@ -26,4 +26,29 @@ export declare const BAKED_MEDIA_KEYS: readonly ["src", "srcset", "alt", "width"
26
26
  export declare function rebindImageRef<T extends {
27
27
  imageId?: string;
28
28
  }>(ref: T, imageId: string): T;
29
+ export interface ResolvedLogo {
30
+ lightUrl: string;
31
+ darkUrl: string | null;
32
+ invertInDark: boolean;
33
+ showInNav: boolean;
34
+ }
35
+ /**
36
+ * Resolves a SiteConfig["logo"] entry into concrete URLs. Returns null when
37
+ * there is no logo configured, when the light variant's imageId can't be
38
+ * resolved against the manifest, or when the light variant resolves to a
39
+ * video (an `<img>` can't render mp4/webm) — callers should fall back to
40
+ * rendering the site name as text rather than a broken image. Still-image and
41
+ * animated (gif) picks are both fine — only a video pick is rejected, since
42
+ * both those tags resolve to `tag: "img"`.
43
+ */
44
+ export declare function resolveLogo(logo: {
45
+ light: {
46
+ imageId: string;
47
+ };
48
+ dark: {
49
+ imageId: string;
50
+ } | null;
51
+ invertInDark: boolean;
52
+ showInNav: boolean;
53
+ } | null, manifest: MediaManifest, provider: MediaProvider): ResolvedLogo | null;
29
54
  //# sourceMappingURL=resolve.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/media/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,wBAAgB,YAAY,CAC1B,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACxC,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,aAAa,GACtB,aAAa,GAAG,IAAI,CAKtB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,aAAa,GACtB,OAAO,CA0CT;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,gEAAiE,CAAC;AAE/F;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC,CAIzF"}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/media/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,wBAAgB,YAAY,CAC1B,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACxC,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,aAAa,GACtB,aAAa,GAAG,IAAI,CAKtB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,aAAa,GACtB,OAAO,CA0CT;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,gEAAiE,CAAC;AAE/F;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC,CAIzF;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE;IAAE,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAAC,YAAY,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,EACxH,QAAQ,EAAE,aAAa,EACvB,QAAQ,EAAE,aAAa,GACtB,YAAY,GAAG,IAAI,CAYrB"}
@@ -3,7 +3,7 @@ import {
3
3
  AudienceNameSchema,
4
4
  MediaGridOptionsSchema,
5
5
  slugifyAudienceName
6
- } from "../chunk-EP3LVOXZ.js";
6
+ } from "../chunk-WA3HFN54.js";
7
7
  import {
8
8
  AudienceSchema,
9
9
  RoleSchema,
@@ -40,7 +40,7 @@ import {
40
40
  TextLineSchema,
41
41
  VariantSchema,
42
42
  normalizeSiteIndex
43
- } from "../chunk-E3MTIMOG.js";
43
+ } from "../chunk-XPUBUR64.js";
44
44
  import "../chunk-VKAGMEKE.js";
45
45
  import "../chunk-FM4M4Q2W.js";
46
46
  import "../chunk-NSCT3AMV.js";
@@ -456,6 +456,16 @@ export declare const SiteConfigSchema: z.ZodObject<{
456
456
  uppercaseNavHeadings: z.ZodDefault<z.ZodBoolean>;
457
457
  googleFontsUrl: z.ZodDefault<z.ZodNullable<z.ZodString>>;
458
458
  favicon: z.ZodDefault<z.ZodNullable<z.ZodString>>;
459
+ logo: z.ZodDefault<z.ZodNullable<z.ZodObject<{
460
+ light: z.ZodObject<{
461
+ imageId: z.ZodString;
462
+ }, z.core.$strip>;
463
+ dark: z.ZodDefault<z.ZodNullable<z.ZodObject<{
464
+ imageId: z.ZodString;
465
+ }, z.core.$strip>>>;
466
+ invertInDark: z.ZodDefault<z.ZodBoolean>;
467
+ showInNav: z.ZodDefault<z.ZodBoolean>;
468
+ }, z.core.$strip>>>;
459
469
  media: z.ZodDefault<z.ZodObject<{
460
470
  sizes: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
461
471
  maxFileSize: z.ZodDefault<z.ZodNumber>;
@@ -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;AAaxB,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;;;;;;;;;;;iBAwB5B,CAAC;AACL,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4B3B,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;AAaxB,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;;;;;;;;;;;iBAwB5B,CAAC;AACL,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyC3B,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.86",
3
+ "version": "0.1.87",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",
@@ -14,12 +14,13 @@ interface Props {
14
14
  siteName: string;
15
15
  darkMode: "light" | "dark" | "optional";
16
16
  lastUpdated: string | null;
17
+ logo?: { lightUrl: string; darkUrl: string | null; invertInDark: boolean } | null;
17
18
  // Optional same-island handler (tests). In production the editor nav is a
18
19
  // separate island from EditorShell, so a click dispatches pageSelectEvent.
19
20
  onPageSelect?: (pageId: string) => void;
20
21
  }
21
22
 
22
- export default function Navigation({ siteNav: initialNav, siteName, darkMode, lastUpdated, onPageSelect }: Props) {
23
+ export default function Navigation({ siteNav: initialNav, siteName, darkMode, lastUpdated, logo, onPageSelect }: Props) {
23
24
  const [isOpen, setIsOpen] = useState(false);
24
25
  const [isEditMode, setIsEditMode] = useState(false);
25
26
  const [currentDarkMode, setCurrentDarkMode] = useState(darkMode);
@@ -135,6 +136,19 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
135
136
  </ul>
136
137
  );
137
138
 
139
+ const logoMark = logo ? (
140
+ <>
141
+ <img
142
+ src={logo.lightUrl}
143
+ alt={currentSiteName}
144
+ className={logo.darkUrl ? "block h-8 w-auto object-contain dark:hidden" : logo.invertInDark ? "h-8 w-auto object-contain dark:invert" : "h-8 w-auto object-contain"}
145
+ />
146
+ {logo.darkUrl && (
147
+ <img src={logo.darkUrl} alt={currentSiteName} className="hidden h-8 w-auto object-contain dark:block" />
148
+ )}
149
+ </>
150
+ ) : null;
151
+
138
152
  const handlePageClick = useCallback((id: string) => {
139
153
  if (onPageSelect) onPageSelect(id);
140
154
  else pageSelectEvent.dispatch(id);
@@ -193,7 +207,10 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
193
207
  <>
194
208
  <header className="fixed top-0 left-0 right-0 z-50 bg-base lg:hidden">
195
209
  <div className="mx-auto max-w-screen-xl flex h-16 items-center justify-between px-4">
196
- <span className="text-lg font-bold text-primary">{currentSiteName}</span>
210
+ <div className="flex flex-col items-start gap-1">
211
+ {logoMark}
212
+ <span className="text-lg font-bold text-primary">{currentSiteName}</span>
213
+ </div>
197
214
  <button onClick={() => setIsOpen(!isOpen)} className="cursor-pointer p-2 text-base-contrast" aria-label="Toggle navigation">
198
215
  <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
199
216
  {isOpen
@@ -209,6 +226,7 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
209
226
  <nav className={cn("fixed top-0 left-0 lg:left-auto z-40 h-full w-64 flex flex-col overflow-y-auto border-r border-base-200 bg-base pt-16 transition-transform lg:translate-x-0 nav-sidebar",
210
227
  isOpen ? "translate-x-0" : "-translate-x-full")}>
211
228
  <div className="hidden px-4 py-4 lg:block">
229
+ {logoMark}
212
230
  <span className="text-lg font-bold text-primary">{currentSiteName}</span>
213
231
  </div>
214
232
 
@@ -363,6 +363,10 @@ export default function EditorShell({
363
363
  markSectionDirty: persistence.markSectionDirty,
364
364
  onUploadError: (message) => setUploadErrorMessage(message),
365
365
  onUploadNotice: setUploadNotice,
366
+ onSiteConfigChange: (updater) => {
367
+ if (!siteConfig) return;
368
+ handleSiteConfigChange(updater(siteConfig));
369
+ },
366
370
  });
367
371
 
368
372
  const buildStatus = useBuildStatus();
@@ -6,15 +6,27 @@ import { FontPicker } from "../shared/FontPicker";
6
6
  import { Input } from "../shared/Input";
7
7
  import { Select } from "../shared/Select";
8
8
  import { FormLabel } from "../shared/FormLabel";
9
+ import { useMediaLibrary } from "./MediaLibraryContext";
9
10
  import { deriveContrast } from "../../lib/contrast";
10
11
  import { buildGoogleFontsUrl } from "../../lib/google-fonts";
11
12
  import type { SiteConfig } from "../../schemas/site-config";
13
+ import type { MediaItem } from "../../media/types";
12
14
 
13
15
  interface Props {
14
16
  siteConfig: SiteConfig;
15
17
  onChange: (config: SiteConfig) => void;
16
18
  }
17
19
 
20
+ // Smallest generated variant for the editor-preview thumbnail (mirrors
21
+ // DocInspector's thumbSrc) — avoids needing a MediaProvider in this context.
22
+ function thumbSrc(imageId: string, item: MediaItem | undefined): string | null {
23
+ if (!item || item.kind !== "image") return null;
24
+ if (item.mimeType === "image/svg+xml") return `/api/media/${imageId}/original.svg`;
25
+ const widths = item.variants.map((v) => v.width).sort((a, b) => a - b);
26
+ if (widths.length === 0) return null;
27
+ return `/api/media/${imageId}/${widths[0]}.webp`;
28
+ }
29
+
18
30
  const darkModeOptions = [
19
31
  { label: "Light", value: "light" },
20
32
  { label: "Dark", value: "dark" },
@@ -33,11 +45,19 @@ const FAVICON_MAX_BYTES = 100 * 1024;
33
45
 
34
46
  export function SiteSettingsDisplay({ siteConfig, onChange }: Props) {
35
47
  const [faviconError, setFaviconError] = useState<string | null>(null);
48
+ const mediaLibrary = useMediaLibrary();
36
49
 
37
50
  function update(patch: Partial<SiteConfig>) {
38
51
  onChange({ ...siteConfig, ...patch });
39
52
  }
40
53
 
54
+ function pickLogo(which: "light" | "dark") {
55
+ mediaLibrary?.openSelectModal((imageId) => {
56
+ const base = siteConfig.logo ?? { light: { imageId: "" }, dark: null, invertInDark: false, showInNav: true };
57
+ update({ logo: { ...base, [which]: { imageId } } });
58
+ });
59
+ }
60
+
41
61
  function handleFaviconFile(file: File | undefined) {
42
62
  if (!file) return;
43
63
  if (!FAVICON_TYPES.includes(file.type)) {
@@ -154,6 +174,92 @@ export function SiteSettingsDisplay({ siteConfig, onChange }: Props) {
154
174
  {faviconError && <p className="mt-1 text-xs text-red-600">{faviconError}</p>}
155
175
  </div>
156
176
 
177
+ {(() => {
178
+ const logo = siteConfig.logo;
179
+ const lightSrc = logo ? thumbSrc(logo.light.imageId, mediaLibrary?.manifest.images[logo.light.imageId]) : null;
180
+ const darkSrc = logo?.dark ? thumbSrc(logo.dark.imageId, mediaLibrary?.manifest.images[logo.dark.imageId]) : null;
181
+ const needsDarkHint = logo != null && siteConfig.darkMode !== "light" && !logo.dark && !logo.invertInDark;
182
+
183
+ return (
184
+ <div>
185
+ <FormLabel>Logo</FormLabel>
186
+ <div className="flex items-center gap-3">
187
+ {logo && (lightSrc ? (
188
+ <img
189
+ src={lightSrc}
190
+ alt="Current logo"
191
+ className="h-8 w-auto max-w-[8rem] rounded border border-base-200 bg-base-accent object-contain p-1"
192
+ />
193
+ ) : (
194
+ <span className="text-xs text-base-contrast-light">Logo selected</span>
195
+ ))}
196
+ <Button variant="secondary" size="sm" onClick={() => pickLogo("light")}>
197
+ {logo ? "Replace logo" : "Add logo"}
198
+ </Button>
199
+ {logo && (
200
+ <Button variant="ghost" tone="destructive" size="sm" aria-label="Remove logo" onClick={() => update({ logo: null })}>
201
+ Remove logo
202
+ </Button>
203
+ )}
204
+ </div>
205
+ <p className="mt-1 text-xs text-base-contrast/70">Shown in the site navigation in place of the site name.</p>
206
+
207
+ {logo && (
208
+ <div className="mt-3 space-y-3 border-l-2 border-base-200 pl-3">
209
+ <div>
210
+ <FormLabel>Dark variant</FormLabel>
211
+ <div className="flex items-center gap-3">
212
+ {logo.dark && (darkSrc ? (
213
+ <img
214
+ src={darkSrc}
215
+ alt="Current dark-mode logo"
216
+ className="h-8 w-auto max-w-[8rem] rounded border border-base-200 bg-base-accent object-contain p-1"
217
+ />
218
+ ) : (
219
+ <span className="text-xs text-base-contrast-light">Dark logo selected</span>
220
+ ))}
221
+ <Button variant="secondary" size="sm" onClick={() => pickLogo("dark")}>
222
+ {logo.dark ? "Replace dark variant" : "Add dark variant"}
223
+ </Button>
224
+ {logo.dark && (
225
+ <Button
226
+ variant="ghost"
227
+ tone="destructive"
228
+ size="sm"
229
+ aria-label="Remove dark variant"
230
+ onClick={() => update({ logo: { ...logo, dark: null } })}
231
+ >
232
+ Remove
233
+ </Button>
234
+ )}
235
+ </div>
236
+ <Checkbox
237
+ className="mt-2"
238
+ checked={logo.invertInDark}
239
+ onChange={(v) => update({ logo: { ...logo, invertInDark: v } })}
240
+ label="Invert in dark mode"
241
+ description="Flips the light logo's colors instead of uploading a separate file."
242
+ disabled={Boolean(logo.dark)}
243
+ />
244
+ </div>
245
+
246
+ <Checkbox
247
+ checked={logo.showInNav}
248
+ onChange={(v) => update({ logo: { ...logo, showInNav: v } })}
249
+ label="Show in navigation"
250
+ />
251
+
252
+ {needsDarkHint && (
253
+ <p className="text-xs text-amber-600">
254
+ Add a dark variant or enable invert so the logo stays legible in dark mode.
255
+ </p>
256
+ )}
257
+ </div>
258
+ )}
259
+ </div>
260
+ );
261
+ })()}
262
+
157
263
  <Select
158
264
  label="Dark mode"
159
265
  value={siteConfig.darkMode}
@@ -13,7 +13,7 @@ import {
13
13
  removePendingMediaDeletion,
14
14
  } from "../lib/dexie";
15
15
  import { generateVideoPoster } from "../media/videoPoster";
16
- import { tallyImageIds, collectImageIds, clearImageIds } from "../lib/image-refs";
16
+ import { tallyImageIds, collectImageIds, clearImageIds, applyLogoDeletion } from "../lib/image-refs";
17
17
  import {
18
18
  createFolderId, uniqueFolderName, effectiveParentId, canMoveFolder,
19
19
  descendantFolderIds, itemFolderId, planFolderUpload,
@@ -36,6 +36,10 @@ interface UseMediaPipelineInput {
36
36
  // Surfaces non-error, informational notices (e.g. "N files already exist
37
37
  // and were left where they are" on dedup skip) to the caller.
38
38
  onUploadNotice?: (message: string) => void;
39
+ // Lets handleMediaDelete null out siteConfig.logo when its backing asset is
40
+ // deleted. Updater form (not a plain setter) so the caller can adapt its own
41
+ // value-based site-config change handler.
42
+ onSiteConfigChange?: (updater: (prev: SiteConfig) => SiteConfig) => void;
39
43
  }
40
44
 
41
45
  export function useMediaPipeline({
@@ -49,6 +53,7 @@ export function useMediaPipeline({
49
53
  markSectionDirty,
50
54
  onUploadError,
51
55
  onUploadNotice,
56
+ onSiteConfigChange,
52
57
  }: UseMediaPipelineInput) {
53
58
  const [processingItems, setProcessingItems] = useState<QueueItem[]>([]);
54
59
  const [pendingMediaItems, setPendingMediaItems] = useState<LibraryMediaItem[]>([]);
@@ -440,8 +445,15 @@ export function useMediaPipeline({
440
445
  }),
441
446
  );
442
447
 
448
+ if (siteConfig?.logo && onSiteConfigChange) {
449
+ const logoIds = collectImageIds(siteConfig.logo);
450
+ if (ids.some((id) => logoIds.has(id))) {
451
+ onSiteConfigChange((prev) => ({ ...prev, logo: applyLogoDeletion(prev.logo, idSet) }));
452
+ }
453
+ }
454
+
443
455
  setLocalChangesExist(true);
444
- }, [pendingMediaItems, markSectionDirty, setSections, setDirtySectionIds, setLocalChangesExist]);
456
+ }, [pendingMediaItems, markSectionDirty, setSections, setDirtySectionIds, setLocalChangesExist, siteConfig, onSiteConfigChange]);
445
457
 
446
458
  const handleMediaAltChange = useCallback((id: string, alt: string) => {
447
459
  setMediaManifest((prev) => {
@@ -585,8 +597,9 @@ export function useMediaPipeline({
585
597
  const referenceCountMap = useMemo((): Record<string, number> => {
586
598
  const counts = new Map<string, number>();
587
599
  for (const { section } of sections) tallyImageIds(section, counts);
600
+ if (siteConfig) tallyImageIds(siteConfig, counts);
588
601
  return Object.fromEntries(counts);
589
- }, [sections]);
602
+ }, [sections, siteConfig]);
590
603
 
591
604
  // --- Unified enqueue ---
592
605
 
@@ -1,3 +1,5 @@
1
+ import type { SiteConfig } from "../schemas/site-config";
2
+
1
3
  /**
2
4
  * Structural walkers over section content for `imageId` references.
3
5
  *
@@ -51,3 +53,20 @@ export function clearImageIds(value: unknown, ids: Set<string>): unknown {
51
53
  }
52
54
  return value;
53
55
  }
56
+
57
+ /**
58
+ * Applies a media-deletion id set to a site logo config, granularly: deleting
59
+ * the light variant's backing asset removes the logo entirely (no light = no
60
+ * logo to show), while deleting only the dark variant's asset keeps the
61
+ * light logo + flags and drops just the dark override. Returns the same
62
+ * `logo` reference unchanged when neither id was deleted.
63
+ */
64
+ export function applyLogoDeletion(
65
+ logo: SiteConfig["logo"],
66
+ deletedIds: Set<string>,
67
+ ): SiteConfig["logo"] {
68
+ if (!logo) return logo;
69
+ if (deletedIds.has(logo.light.imageId)) return null;
70
+ if (logo.dark && deletedIds.has(logo.dark.imageId)) return { ...logo, dark: null };
71
+ return logo;
72
+ }
@@ -86,3 +86,37 @@ export function rebindImageRef<T extends { imageId?: string }>(ref: T, imageId:
86
86
  for (const key of BAKED_MEDIA_KEYS) delete fresh[key];
87
87
  return fresh as T;
88
88
  }
89
+
90
+ export interface ResolvedLogo {
91
+ lightUrl: string;
92
+ darkUrl: string | null;
93
+ invertInDark: boolean;
94
+ showInNav: boolean;
95
+ }
96
+
97
+ /**
98
+ * Resolves a SiteConfig["logo"] entry into concrete URLs. Returns null when
99
+ * there is no logo configured, when the light variant's imageId can't be
100
+ * resolved against the manifest, or when the light variant resolves to a
101
+ * video (an `<img>` can't render mp4/webm) — callers should fall back to
102
+ * rendering the site name as text rather than a broken image. Still-image and
103
+ * animated (gif) picks are both fine — only a video pick is rejected, since
104
+ * both those tags resolve to `tag: "img"`.
105
+ */
106
+ export function resolveLogo(
107
+ logo: { light: { imageId: string }; dark: { imageId: string } | null; invertInDark: boolean; showInNav: boolean } | null,
108
+ manifest: MediaManifest,
109
+ provider: MediaProvider,
110
+ ): ResolvedLogo | null {
111
+ if (!logo) return null;
112
+ const light = resolveMedia({ imageId: logo.light.imageId }, manifest, [1], provider);
113
+ if (!light?.src || light.tag === "video") return null;
114
+ const dark = logo.dark ? resolveMedia({ imageId: logo.dark.imageId }, manifest, [1], provider) : null;
115
+ const darkUrl = dark && dark.tag !== "video" ? dark.src : null;
116
+ return {
117
+ lightUrl: light.src,
118
+ darkUrl,
119
+ invertInDark: logo.invertInDark,
120
+ showInNav: logo.showInNav,
121
+ };
122
+ }
@@ -211,6 +211,19 @@ export const SiteConfigSchema = z.object({
211
211
  .refine((v) => v.startsWith("data:image/"), "Favicon must be an image data URL")
212
212
  .nullable()
213
213
  .default(null),
214
+ // Optional site logo, stored as media-library references (so it participates
215
+ // in usage tracking + GC protection, and its resolved URL is reusable for the
216
+ // Phase 3 social-preview image). Every field defaults so pre-existing
217
+ // site-config.json files keep parsing.
218
+ logo: z
219
+ .object({
220
+ light: z.object({ imageId: z.string() }),
221
+ dark: z.object({ imageId: z.string() }).nullable().default(null),
222
+ invertInDark: z.boolean().default(false),
223
+ showInNav: z.boolean().default(true),
224
+ })
225
+ .nullable()
226
+ .default(null),
214
227
  media: MediaConfigSchema.default(MediaConfigSchema.parse({})),
215
228
  // Brand chatbot config (editor-owned; the platform kill switch lives in the
216
229
  // sites table, not here). Every field defaults so pre-existing