@drawnagency/primitives 0.1.114 → 0.1.115

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 (103) hide show
  1. package/dist/{chunk-OAGJBCSH.js → chunk-4WQ36ZP7.js} +19 -3
  2. package/dist/{chunk-PTY2RG2K.js → chunk-67P34WNW.js} +75 -130
  3. package/dist/chunk-APIJPTFG.js +64 -0
  4. package/dist/chunk-CDBURDPX.js +90 -0
  5. package/dist/chunk-DFKTF3CH.js +0 -0
  6. package/dist/chunk-DJABADOO.js +131 -0
  7. package/dist/{chunk-IP2SEBAJ.js → chunk-DJOGPSRU.js} +31 -72
  8. package/dist/{chunk-N3G7DOU3.js → chunk-FHTKSELX.js} +6 -6
  9. package/dist/chunk-GNF6KFQ7.js +18 -0
  10. package/dist/{chunk-QHMKIODQ.js → chunk-HHNPSKPT.js} +2 -89
  11. package/dist/{chunk-PZPGJZRG.js → chunk-JXATNKLV.js} +5 -5
  12. package/dist/{chunk-PLANHXS7.js → chunk-NX7TFHA4.js} +9 -9
  13. package/dist/{chunk-PNSCDBU7.js → chunk-O37AV27P.js} +6 -49
  14. package/dist/{chunk-5KSAS4VY.js → chunk-RIITGTV3.js} +1 -1
  15. package/dist/chunk-S7QRHUWO.js +8 -0
  16. package/dist/{chunk-7SAAJVDE.js → chunk-T26HAKW3.js} +37 -35
  17. package/dist/chunk-VY3IFGE3.js +70 -0
  18. package/dist/{chunk-VUYCRBJK.js → chunk-WYOFQHKI.js} +6 -4
  19. package/dist/{chunk-Y5QECJX7.js → chunk-YV3YSZEB.js} +5 -5
  20. package/dist/collab/rebase.d.ts.map +1 -1
  21. package/dist/collab/rebase.js +15 -15
  22. package/dist/components/editor/index.js +13 -13
  23. package/dist/components/primitives/index.js +16 -11
  24. package/dist/components/sections/brand-guide.js +13 -8
  25. package/dist/components/sections/register-schemas.js +20 -15
  26. package/dist/components/sections/register.js +20 -15
  27. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  28. package/dist/deck-builder/index.js +9 -9
  29. package/dist/hooks/useEditorPublish.d.ts +4 -1
  30. package/dist/hooks/useEditorPublish.d.ts.map +1 -1
  31. package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
  32. package/dist/index.js +85 -56
  33. package/dist/lib/dexie.d.ts +7 -1
  34. package/dist/lib/dexie.d.ts.map +1 -1
  35. package/dist/lib/dexie.js +3 -2
  36. package/dist/lib/image-variant-plan.d.ts +42 -0
  37. package/dist/lib/image-variant-plan.d.ts.map +1 -0
  38. package/dist/lib/image-variant-plan.js +53 -0
  39. package/dist/lib/index-ops.d.ts.map +1 -1
  40. package/dist/lib/index-ops.js +20 -16
  41. package/dist/lib/index.d.ts +2 -1
  42. package/dist/lib/index.d.ts.map +1 -1
  43. package/dist/lib/index.js +43 -22
  44. package/dist/lib/loader.d.ts +16 -17
  45. package/dist/lib/loader.d.ts.map +1 -1
  46. package/dist/lib/media-ops.d.ts +73 -0
  47. package/dist/lib/media-ops.d.ts.map +1 -0
  48. package/dist/lib/media-ops.js +60 -0
  49. package/dist/lib/nav-tree.d.ts +6 -0
  50. package/dist/lib/nav-tree.d.ts.map +1 -1
  51. package/dist/lib/pages.d.ts +2 -1
  52. package/dist/lib/pages.d.ts.map +1 -1
  53. package/dist/lib/sidecar-diagnostics.d.ts +20 -0
  54. package/dist/lib/sidecar-diagnostics.d.ts.map +1 -0
  55. package/dist/lib/sidecars.d.ts +118 -0
  56. package/dist/lib/sidecars.d.ts.map +1 -0
  57. package/dist/lib/sidecars.js +23 -0
  58. package/dist/lib/slug.d.ts +2 -0
  59. package/dist/lib/slug.d.ts.map +1 -0
  60. package/dist/lib/storage-tier.d.ts +17 -0
  61. package/dist/lib/storage-tier.d.ts.map +1 -0
  62. package/dist/lib/storage-tier.js +29 -0
  63. package/dist/lib/tags.d.ts +6 -0
  64. package/dist/lib/tags.d.ts.map +1 -1
  65. package/dist/media/index.js +15 -15
  66. package/dist/media/queue.d.ts +2 -0
  67. package/dist/media/queue.d.ts.map +1 -1
  68. package/dist/media/worker.d.ts +1 -35
  69. package/dist/media/worker.d.ts.map +1 -1
  70. package/dist/schemas/index.js +18 -12
  71. package/dist/schemas/media.d.ts +4 -0
  72. package/dist/schemas/media.d.ts.map +1 -1
  73. package/dist/storage/index.js +2 -1
  74. package/dist/storage/memory.d.ts +21 -0
  75. package/dist/storage/memory.d.ts.map +1 -0
  76. package/dist/storage/memory.js +129 -0
  77. package/dist/storage/types.d.ts +9 -0
  78. package/dist/storage/types.d.ts.map +1 -1
  79. package/package.json +21 -1
  80. package/src/collab/rebase.ts +12 -16
  81. package/src/components/shell/EditorShell.tsx +7 -4
  82. package/src/hooks/useEditorPublish.ts +9 -4
  83. package/src/hooks/useMediaPipeline.ts +29 -23
  84. package/src/lib/dexie.ts +9 -1
  85. package/src/lib/image-variant-plan.ts +92 -0
  86. package/src/lib/index-ops.ts +9 -18
  87. package/src/lib/index.ts +15 -0
  88. package/src/lib/loader.ts +18 -77
  89. package/src/lib/media-ops.ts +112 -0
  90. package/src/lib/nav-tree.ts +33 -4
  91. package/src/lib/pages.ts +2 -3
  92. package/src/lib/sidecar-diagnostics.ts +29 -0
  93. package/src/lib/sidecars.ts +221 -0
  94. package/src/lib/slug.ts +6 -0
  95. package/src/lib/storage-tier.ts +38 -0
  96. package/src/lib/tags.ts +20 -3
  97. package/src/media/queue.ts +3 -0
  98. package/src/media/utils.ts +1 -1
  99. package/src/media/worker.ts +17 -8
  100. package/src/schemas/media.ts +7 -2
  101. package/src/storage/memory.ts +178 -0
  102. package/src/storage/types.ts +15 -2
  103. /package/dist/{chunk-45FA7Z55.js → chunk-UUUSJDIG.js} +0 -0
@@ -1,21 +1,36 @@
1
+ import {
2
+ diagnosticsCollector
3
+ } from "./chunk-GNF6KFQ7.js";
4
+
1
5
  // src/lib/tags.ts
2
6
  function reconcileTags(entries, sections) {
7
+ return reconcileTagsReport(entries, sections).entries;
8
+ }
9
+ function reconcileTagsReport(entries, sections) {
10
+ const report = diagnosticsCollector();
3
11
  const entryList = Array.isArray(entries) ? entries : [];
4
12
  const validIds = new Set(sections && typeof sections === "object" ? Object.keys(sections) : []);
5
13
  const seenTagIds = /* @__PURE__ */ new Set();
6
14
  const result = [];
7
15
  for (const entry of entryList) {
8
16
  if (!entry || typeof entry !== "object") continue;
9
- if (typeof entry.id !== "string" || entry.id === "" || seenTagIds.has(entry.id)) continue;
17
+ if (typeof entry.id !== "string" || entry.id === "") continue;
18
+ if (seenTagIds.has(entry.id)) {
19
+ report.add("duplicate-id", entry.id);
20
+ continue;
21
+ }
10
22
  seenTagIds.add(entry.id);
11
23
  const claimed = Array.isArray(entry.sectionIds) ? entry.sectionIds : [];
12
24
  const sectionIds = [];
13
25
  for (const id of claimed) {
14
- if (typeof id === "string" && validIds.has(id) && !sectionIds.includes(id)) sectionIds.push(id);
26
+ if (typeof id !== "string") continue;
27
+ if (!validIds.has(id)) report.add("dangling", id);
28
+ else if (sectionIds.includes(id)) report.add("duplicate-member", id);
29
+ else sectionIds.push(id);
15
30
  }
16
31
  result.push({ id: entry.id, label: typeof entry.label === "string" ? entry.label : "", sectionIds });
17
32
  }
18
- return result;
33
+ return { entries: result, diagnostics: report.list() };
19
34
  }
20
35
  function siteTags(index) {
21
36
  return reconcileTags(index.tags ?? [], index.sections);
@@ -61,6 +76,7 @@ function mergeTags(args) {
61
76
 
62
77
  export {
63
78
  reconcileTags,
79
+ reconcileTagsReport,
64
80
  siteTags,
65
81
  tagDisplayLabel,
66
82
  mergeTags
@@ -1,100 +1,50 @@
1
1
  import {
2
2
  documentOptionsSchema
3
3
  } from "./chunk-4TZJZU2T.js";
4
+ import {
5
+ MediaConfigSchema
6
+ } from "./chunk-APIJPTFG.js";
4
7
  import {
5
8
  FontNameSchema,
6
9
  HexColorSchema
7
10
  } from "./chunk-AASEGXNT.js";
8
11
 
9
- // src/schemas/media.ts
10
- import { z } from "zod";
11
- var VariantSchema = z.object({
12
- width: z.number().int().positive(),
13
- height: z.number().int().positive(),
14
- size: z.number().int().nonnegative()
15
- });
16
- var MediaFolderSchema = z.object({
17
- name: z.string().min(1),
18
- parentId: z.string().nullable()
19
- // null = root
20
- });
21
- var MediaItemSchema = z.object({
22
- id: z.string(),
23
- hash: z.string(),
24
- kind: z.enum(["image", "animated", "video"]),
25
- originalName: z.string(),
26
- width: z.number().int().positive(),
27
- height: z.number().int().positive(),
28
- mimeType: z.string(),
29
- size: z.number().int().nonnegative(),
30
- folder: z.string(),
31
- variants: z.array(VariantSchema),
32
- alt: z.string().default(""),
33
- // Where the original bytes live. "github" = committed to the repo (images,
34
- // small loops); "assets" = large original in the AssetStore bucket. Legacy
35
- // manifests have no field — default keeps them on the git path.
36
- storage: z.enum(["github", "assets"]).default("github"),
37
- // Public CDN URL for assets-backed originals, baked at upload time so
38
- // resolution needs no env access in the browser or at SSR.
39
- url: z.string().optional(),
40
- // Seconds; videos only. Informational (library UI).
41
- duration: z.number().positive().optional(),
42
- // Display folder (MediaFolderSchema key). Absent = root/unfiled. NOT the
43
- // git storage dir — that's `folder` above, which never changes.
44
- folderId: z.string().optional(),
45
- // System-generated assets (e.g. the Phase 3 social-preview raster) set this
46
- // so the media-library UI can exclude them. Absent on all user uploads.
47
- hidden: z.boolean().optional()
48
- });
49
- var ImageManifestSchema = z.object({
50
- images: z.record(z.string(), MediaItemSchema),
51
- folders: z.record(z.string(), MediaFolderSchema).default({})
52
- });
53
- var MediaConfigSchema = z.object({
54
- sizes: z.array(z.number()).default([640, 1080, 1920]),
55
- maxFileSize: z.number().default(5242880),
56
- quality: z.number().min(1).max(100).default(85),
57
- // Per-file cap for bucket-backed video uploads (bytes). The git-path cap is
58
- // maxFileSize; anything between the two routes to the AssetStore.
59
- maxAssetSize: z.number().default(209715200)
60
- });
61
-
62
12
  // src/schemas/site-config.ts
63
- import { z as z2 } from "zod";
13
+ import { z } from "zod";
64
14
 
65
15
  // src/lib/corner-radius.ts
66
16
  var CORNER_RADIUS_OPTIONS = ["none", "small", "medium", "large"];
67
17
 
68
18
  // src/schemas/site-config.ts
69
- var StatusSchema = z2.enum(["draft", "live", "archived", "published"]).transform(
19
+ var StatusSchema = z.enum(["draft", "live", "archived", "published"]).transform(
70
20
  (val) => val === "published" ? "live" : val
71
21
  );
72
- var SectionMetaSchema = z2.object({
73
- type: z2.string(),
22
+ var SectionMetaSchema = z.object({
23
+ type: z.string(),
74
24
  status: StatusSchema,
75
- access: z2.array(z2.string())
25
+ access: z.array(z.string())
76
26
  });
77
27
  var RESERVED_SLUGS = ["edit", "api", "login", "set-password", "404", "present", "print", "audiences", "decks"];
78
28
  var PARSE_REJECTED_SLUGS = ["edit", "api", "login", "set-password", "404"];
79
- var PageStatusSchema = z2.enum(["live", "archived"]);
80
- var PageSchema = z2.object({
81
- id: z2.string().min(1),
82
- title: z2.string(),
83
- slug: z2.string(),
84
- isHome: z2.boolean(),
85
- showInNav: z2.boolean(),
29
+ var PageStatusSchema = z.enum(["live", "archived"]);
30
+ var PageSchema = z.object({
31
+ id: z.string().min(1),
32
+ title: z.string(),
33
+ slug: z.string(),
34
+ isHome: z.boolean(),
35
+ showInNav: z.boolean(),
86
36
  status: PageStatusSchema,
87
- access: z2.array(z2.string()),
88
- order: z2.array(z2.string()),
37
+ access: z.array(z.string()),
38
+ order: z.array(z.string()),
89
39
  document: documentOptionsSchema.optional()
90
40
  });
91
41
  var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
92
- var RawIndexSchema = z2.object({
93
- siteId: z2.string(),
94
- order: z2.array(z2.string()).optional(),
95
- pages: z2.array(PageSchema).optional(),
96
- sections: z2.record(z2.string(), SectionMetaSchema),
97
- lastModified: z2.string().nullable().optional()
42
+ var RawIndexSchema = z.object({
43
+ siteId: z.string(),
44
+ order: z.array(z.string()).optional(),
45
+ pages: z.array(PageSchema).optional(),
46
+ sections: z.record(z.string(), SectionMetaSchema),
47
+ lastModified: z.string().nullable().optional()
98
48
  });
99
49
  function normalizeRawIndex(data) {
100
50
  if (data.pages && data.pages.length > 0) {
@@ -124,34 +74,34 @@ function normalizeRawIndex(data) {
124
74
  lastModified: data.lastModified ?? null
125
75
  };
126
76
  }
127
- var CanonicalIndexSchema = z2.object({
128
- siteId: z2.string(),
129
- pages: z2.array(PageSchema),
130
- sections: z2.record(z2.string(), SectionMetaSchema),
131
- lastModified: z2.string().nullable().optional()
77
+ var CanonicalIndexSchema = z.object({
78
+ siteId: z.string(),
79
+ pages: z.array(PageSchema),
80
+ sections: z.record(z.string(), SectionMetaSchema),
81
+ lastModified: z.string().nullable().optional()
132
82
  }).superRefine((data, ctx) => {
133
83
  const homes = data.pages.filter((p) => p.isHome);
134
84
  if (homes.length !== 1) {
135
- ctx.addIssue({ code: z2.ZodIssueCode.custom, message: "Exactly one page must be the home page" });
85
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Exactly one page must be the home page" });
136
86
  }
137
87
  if (homes[0]?.status === "archived") {
138
- ctx.addIssue({ code: z2.ZodIssueCode.custom, message: "The home page cannot be archived" });
88
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "The home page cannot be archived" });
139
89
  }
140
90
  if (homes[0] && homes[0].slug !== "") {
141
- ctx.addIssue({ code: z2.ZodIssueCode.custom, message: "The home page slug must be empty" });
91
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "The home page slug must be empty" });
142
92
  }
143
93
  const seenSlugs = /* @__PURE__ */ new Set();
144
94
  for (const p of data.pages) {
145
95
  if (!p.isHome) {
146
96
  if (!SLUG_RE.test(p.slug)) {
147
- ctx.addIssue({ code: z2.ZodIssueCode.custom, message: `Invalid slug "${p.slug}"` });
97
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Invalid slug "${p.slug}"` });
148
98
  }
149
99
  if (PARSE_REJECTED_SLUGS.includes(p.slug)) {
150
- ctx.addIssue({ code: z2.ZodIssueCode.custom, message: `Slug "${p.slug}" is reserved` });
100
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Slug "${p.slug}" is reserved` });
151
101
  }
152
102
  }
153
103
  if (seenSlugs.has(p.slug)) {
154
- ctx.addIssue({ code: z2.ZodIssueCode.custom, message: `Duplicate slug "${p.slug}"` });
104
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Duplicate slug "${p.slug}"` });
155
105
  }
156
106
  seenSlugs.add(p.slug);
157
107
  }
@@ -166,21 +116,21 @@ var CanonicalIndexSchema = z2.object({
166
116
  const duplicated = [...counts].filter(([, n]) => n > 1).map(([id]) => id);
167
117
  if (duplicated.length > 0) {
168
118
  ctx.addIssue({
169
- code: z2.ZodIssueCode.custom,
119
+ code: z.ZodIssueCode.custom,
170
120
  message: `Section id(s) appear in more than one page order: ${fmtIds(duplicated)}`
171
121
  });
172
122
  }
173
123
  const orderedNotInSections = [...fromPagesSet].filter((id) => !sectionSet.has(id));
174
124
  if (orderedNotInSections.length > 0) {
175
125
  ctx.addIssue({
176
- code: z2.ZodIssueCode.custom,
126
+ code: z.ZodIssueCode.custom,
177
127
  message: `Section id(s) in a page order but missing from sections{}: ${fmtIds(orderedNotInSections)}`
178
128
  });
179
129
  }
180
130
  const sectionsNotOrdered = sectionKeys.filter((id) => !fromPagesSet.has(id));
181
131
  if (sectionsNotOrdered.length > 0) {
182
132
  ctx.addIssue({
183
- code: z2.ZodIssueCode.custom,
133
+ code: z.ZodIssueCode.custom,
184
134
  message: `Section id(s) in sections{} but not in any page order: ${fmtIds(sectionsNotOrdered)}`
185
135
  });
186
136
  }
@@ -189,60 +139,60 @@ var IndexSchema = RawIndexSchema.transform(normalizeRawIndex).pipe(CanonicalInde
189
139
  function normalizeSiteIndex(raw) {
190
140
  return IndexSchema.parse(raw);
191
141
  }
192
- var ChatbotTaskIdSchema = z2.string().min(1).max(32).regex(
142
+ var ChatbotTaskIdSchema = z.string().min(1).max(32).regex(
193
143
  /^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/,
194
144
  "Task id must be lowercase letters, numbers, dashes, or underscores"
195
145
  );
196
- var ChatbotTaskSchema = z2.object({
146
+ var ChatbotTaskSchema = z.object({
197
147
  id: ChatbotTaskIdSchema,
198
- label: z2.string().min(1).max(60),
199
- prompt: z2.string().max(4e3),
148
+ label: z.string().min(1).max(60),
149
+ prompt: z.string().max(4e3),
200
150
  // Audience names allowed to see this task; [] = all — same semantics as SectionMeta.access.
201
- access: z2.array(z2.string()).default([])
151
+ access: z.array(z.string()).default([])
202
152
  });
203
- var ChatbotConfigSchema = z2.object({
204
- enabled: z2.boolean().default(false),
153
+ var ChatbotConfigSchema = z.object({
154
+ enabled: z.boolean().default(false),
205
155
  // Assistant display name shown in the widget header and used in the system
206
156
  // prompt. Blank falls back to "Portal Assistant" (resolveChatbotName).
207
- name: z2.string().max(60).default("Portal Assistant"),
157
+ name: z.string().max(60).default("Portal Assistant"),
208
158
  // Per-audience name overrides, keyed by audience name — same inert-when-
209
159
  // deselected semantics as audiencePrompts.
210
- audienceNames: z2.record(z2.string(), z2.string().max(60)).default({}),
160
+ audienceNames: z.record(z.string(), z.string().max(60)).default({}),
211
161
  // Brand-flavored waiting phrases, shown (with "…" appended) while the
212
162
  // assistant streams no text yet. Empty = the widget's "Thinking…" default.
213
- thinkingPhrases: z2.array(z2.string().max(80)).max(30).default([]),
214
- systemPrompt: z2.string().max(8e3).default(""),
163
+ thinkingPhrases: z.array(z.string().max(80)).max(30).default([]),
164
+ systemPrompt: z.string().max(8e3).default(""),
215
165
  // Audience names the widget is shown to — strict opt-in: [] = no viewer
216
166
  // sees it (editors always can via /edit). Gates: [...slug].astro + /api/chat.
217
- audiences: z2.array(z2.string()).default([]),
167
+ audiences: z.array(z.string()).default([]),
218
168
  // Per-audience prompt overlays, keyed by audience name. Appended AFTER the
219
169
  // base systemPrompt, and ONLY for requests whose session audience matches
220
170
  // (or an editor's explicit test-panel simulation) — enforced in
221
171
  // assembleBrandChat. Keys are not validated against the audience list:
222
172
  // entries for deselected/renamed audiences are inert (strict opt-in means
223
173
  // those audiences cannot chat at all).
224
- audiencePrompts: z2.record(z2.string(), z2.string().max(8e3)).default({}),
225
- tasks: z2.array(ChatbotTaskSchema).max(20).default([])
174
+ audiencePrompts: z.record(z.string(), z.string().max(8e3)).default({}),
175
+ tasks: z.array(ChatbotTaskSchema).max(20).default([])
226
176
  }).superRefine((cfg, ctx) => {
227
177
  const seen = /* @__PURE__ */ new Set();
228
178
  for (const t of cfg.tasks) {
229
179
  if (seen.has(t.id)) {
230
- ctx.addIssue({ code: z2.ZodIssueCode.custom, message: `Duplicate task id "${t.id}"` });
180
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Duplicate task id "${t.id}"` });
231
181
  }
232
182
  seen.add(t.id);
233
183
  }
234
184
  });
235
- var DeckBuilderConfigSchema = z2.object({
185
+ var DeckBuilderConfigSchema = z.object({
236
186
  // Audience names whose members may CREATE decks — a Deck Creator. Strict
237
187
  // opt-in exactly like chatbot.audiences: [] = no viewer may build a deck.
238
188
  // Editors are super-admins over decks and are never gated by this list.
239
189
  // Gates: [...slug].astro's FAB, /decks, and POST /api/decks (canCreateDeck).
240
- audiences: z2.array(z2.string()).default([]).describe(
190
+ audiences: z.array(z.string()).default([]).describe(
241
191
  "Audience names allowed to create decks in the Deck Builder. Strict opt-in \u2014 an empty list means no viewer can. Editors always can."
242
192
  )
243
193
  });
244
- var SiteConfigSchema = z2.object({
245
- siteName: z2.string().default("Brand Portal"),
194
+ var SiteConfigSchema = z.object({
195
+ siteName: z.string().default("Brand Portal"),
246
196
  primaryColor: HexColorSchema.default("#009ca6"),
247
197
  primaryContrast: HexColorSchema.default("#f0f0f0"),
248
198
  // Dark-mode overrides for the brand primary / accent. Only applied when
@@ -251,31 +201,31 @@ var SiteConfigSchema = z2.object({
251
201
  // CSS light-dark() function (see lib/theme-vars.ts).
252
202
  primaryColorDark: HexColorSchema.nullable().default(null),
253
203
  primaryContrastDark: HexColorSchema.nullable().default(null),
254
- darkMode: z2.enum(["light", "dark", "optional"]).default("light"),
204
+ darkMode: z.enum(["light", "dark", "optional"]).default("light"),
255
205
  // Maps to the --radius-outer design token (see lib/corner-radius.ts);
256
206
  // defaults so pre-existing site-config.json files keep parsing.
257
- cornerRadius: z2.enum(CORNER_RADIUS_OPTIONS).default("medium"),
207
+ cornerRadius: z.enum(CORNER_RADIUS_OPTIONS).default("medium"),
258
208
  headingFont: FontNameSchema.default("system-ui"),
259
209
  bodyFont: FontNameSchema.default("system-ui"),
260
- uppercaseHeadings: z2.boolean().default(true),
261
- uppercaseSubheadings: z2.boolean().default(true),
262
- uppercaseNavHeadings: z2.boolean().default(true),
263
- googleFontsUrl: z2.string().refine((url) => url.startsWith("https://fonts.googleapis.com/"), "Must be a Google Fonts URL").nullable().default(null),
210
+ uppercaseHeadings: z.boolean().default(true),
211
+ uppercaseSubheadings: z.boolean().default(true),
212
+ uppercaseNavHeadings: z.boolean().default(true),
213
+ googleFontsUrl: z.string().refine((url) => url.startsWith("https://fonts.googleapis.com/"), "Must be a Google Fonts URL").nullable().default(null),
264
214
  // Favicon stored as a data URL (SVG/PNG, small) so it travels with
265
215
  // site-config.json — no media manifest or CDN round-trip needed.
266
- favicon: z2.string().refine((v) => v.startsWith("data:image/"), "Favicon must be an image data URL").nullable().default(null),
216
+ favicon: z.string().refine((v) => v.startsWith("data:image/"), "Favicon must be an image data URL").nullable().default(null),
267
217
  // Optional site logo, stored as media-library references (so it participates
268
218
  // in usage tracking + GC protection, and its resolved URL is reusable for the
269
219
  // Phase 3 social-preview image). Every field defaults so pre-existing
270
220
  // site-config.json files keep parsing.
271
- logo: z2.object({
272
- light: z2.object({ imageId: z2.string() }),
273
- dark: z2.object({ imageId: z2.string() }).nullable().default(null),
274
- invertInDark: z2.boolean().default(false),
275
- showInNav: z2.boolean().default(true),
221
+ logo: z.object({
222
+ light: z.object({ imageId: z.string() }),
223
+ dark: z.object({ imageId: z.string() }).nullable().default(null),
224
+ invertInDark: z.boolean().default(false),
225
+ showInNav: z.boolean().default(true),
276
226
  // A nav logo stands in for the site name by default; the toggle is for
277
227
  // wordmark-less logos that still need the name spelled out.
278
- hideTitle: z2.boolean().default(true)
228
+ hideTitle: z.boolean().default(true)
279
229
  }).nullable().default(null),
280
230
  media: MediaConfigSchema.default(MediaConfigSchema.parse({})),
281
231
  // Composed social-preview raster, stored as a media-library reference so it
@@ -284,17 +234,17 @@ var SiteConfigSchema = z2.object({
284
234
  // (the /api/media route only serves exact generated-variant widths), so the
285
235
  // og:image URL + dimensions are correct. Defaults null so pre-existing
286
236
  // configs keep parsing and sites without a preview emit no og:image tags.
287
- previewImage: z2.object({
288
- imageId: z2.string(),
289
- width: z2.number().int().positive(),
290
- height: z2.number().int().positive(),
237
+ previewImage: z.object({
238
+ imageId: z.string(),
239
+ width: z.number().int().positive(),
240
+ height: z.number().int().positive(),
291
241
  // Compose-logic version that produced this raster. Pre-existing rasters
292
242
  // (and manifests written before this field) default to 0, which is below
293
243
  // any current PREVIEW_COMPOSE_VERSION, so the editor regenerates them.
294
- version: z2.number().int().nonnegative().default(0)
244
+ version: z.number().int().nonnegative().default(0)
295
245
  }).nullable().default(null),
296
246
  // Optional site description → og:description + <meta name="description">.
297
- description: z2.string().max(300).nullable().default(null),
247
+ description: z.string().max(300).nullable().default(null),
298
248
  // Brand chatbot config (editor-owned; the platform kill switch lives in the
299
249
  // sites table, not here). Every field defaults so pre-existing
300
250
  // site-config.json files keep parsing.
@@ -305,11 +255,6 @@ var SiteConfigSchema = z2.object({
305
255
  });
306
256
 
307
257
  export {
308
- VariantSchema,
309
- MediaFolderSchema,
310
- MediaItemSchema,
311
- ImageManifestSchema,
312
- MediaConfigSchema,
313
258
  StatusSchema,
314
259
  SectionMetaSchema,
315
260
  RESERVED_SLUGS,
@@ -0,0 +1,64 @@
1
+ // src/schemas/media.ts
2
+ import { z } from "zod";
3
+ var VariantSchema = z.object({
4
+ width: z.number().int().positive(),
5
+ height: z.number().int().positive(),
6
+ size: z.number().int().nonnegative()
7
+ });
8
+ var MediaFolderSchema = z.object({
9
+ name: z.string().min(1),
10
+ parentId: z.string().nullable()
11
+ // null = root
12
+ });
13
+ var MediaItemSchema = z.object({
14
+ id: z.string(),
15
+ hash: z.string(),
16
+ kind: z.enum(["image", "animated", "video"]),
17
+ originalName: z.string(),
18
+ width: z.number().int().positive(),
19
+ height: z.number().int().positive(),
20
+ mimeType: z.string(),
21
+ size: z.number().int().nonnegative(),
22
+ folder: z.string(),
23
+ variants: z.array(VariantSchema),
24
+ alt: z.string().default(""),
25
+ // Where the original bytes live. "github" = committed to the repo (images,
26
+ // small loops); "assets" = large original in the AssetStore bucket. Legacy
27
+ // manifests have no field — default keeps them on the git path.
28
+ storage: z.enum(["github", "assets"]).default("github"),
29
+ // Public CDN URL for assets-backed originals, baked at upload time so
30
+ // resolution needs no env access in the browser or at SSR.
31
+ url: z.string().optional(),
32
+ // Seconds; videos only. Informational (library UI).
33
+ duration: z.number().positive().optional(),
34
+ // Display folder (MediaFolderSchema key). Absent = root/unfiled. NOT the
35
+ // git storage dir — that's `folder` above, which never changes.
36
+ folderId: z.string().optional(),
37
+ // System-generated assets (e.g. the Phase 3 social-preview raster) set this
38
+ // so the media-library UI can exclude them. Absent on all user uploads.
39
+ hidden: z.boolean().optional()
40
+ });
41
+ var ImageManifestSchema = z.object({
42
+ images: z.record(z.string(), MediaItemSchema),
43
+ folders: z.record(z.string(), MediaFolderSchema).default({})
44
+ });
45
+ var MEDIA_GIT_MAX_BYTES = 5242880;
46
+ var MEDIA_BUCKET_MAX_BYTES = 209715200;
47
+ var MediaConfigSchema = z.object({
48
+ sizes: z.array(z.number()).default([640, 1080, 1920]),
49
+ maxFileSize: z.number().default(MEDIA_GIT_MAX_BYTES),
50
+ quality: z.number().min(1).max(100).default(85),
51
+ // Per-file cap for bucket-backed video uploads (bytes). The git-path cap is
52
+ // maxFileSize; anything between the two routes to the AssetStore.
53
+ maxAssetSize: z.number().default(MEDIA_BUCKET_MAX_BYTES)
54
+ });
55
+
56
+ export {
57
+ VariantSchema,
58
+ MediaFolderSchema,
59
+ MediaItemSchema,
60
+ ImageManifestSchema,
61
+ MEDIA_GIT_MAX_BYTES,
62
+ MEDIA_BUCKET_MAX_BYTES,
63
+ MediaConfigSchema
64
+ };
@@ -0,0 +1,90 @@
1
+ import {
2
+ slugifyPageSlug
3
+ } from "./chunk-S7QRHUWO.js";
4
+
5
+ // src/media/utils.ts
6
+ async function hashFileBuffer(buffer) {
7
+ if (typeof crypto !== "undefined" && crypto.subtle) {
8
+ const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
9
+ const hashArray = new Uint8Array(hashBuffer);
10
+ return Array.from(hashArray.slice(0, 8)).map((b) => b.toString(16).padStart(2, "0")).join("");
11
+ }
12
+ const bytes = new Uint8Array(buffer);
13
+ const seeds = [2166136261, 84696351];
14
+ let result = "";
15
+ for (const seed of seeds) {
16
+ let h = seed;
17
+ for (let i = 0; i < bytes.length; i++) {
18
+ h ^= bytes[i];
19
+ h = Math.imul(h, 16777619);
20
+ }
21
+ result += (h >>> 0).toString(16).padStart(8, "0");
22
+ }
23
+ return result;
24
+ }
25
+ function sanitizeMediaName(name) {
26
+ return slugifyPageSlug(name.replace(/\.[^.]+$/, ""));
27
+ }
28
+ var MIME_TO_EXT = {
29
+ "image/gif": "gif",
30
+ "image/apng": "apng",
31
+ "image/png": "png",
32
+ "image/jpeg": "jpg",
33
+ "image/webp": "webp",
34
+ "image/svg+xml": "svg",
35
+ "video/mp4": "mp4",
36
+ "video/webm": "webm"
37
+ };
38
+ var EXT_TO_MIME = {
39
+ ...Object.fromEntries(
40
+ Object.entries(MIME_TO_EXT).map(([mime, ext]) => [`.${ext}`, mime])
41
+ ),
42
+ ".jpeg": "image/jpeg"
43
+ };
44
+ function mimeToExt(mime) {
45
+ return MIME_TO_EXT[mime] ?? "bin";
46
+ }
47
+ function mediaThumbnailUrl(item) {
48
+ if (item.mimeType === "image/svg+xml" || item.kind !== "image") {
49
+ return `/api/media/${item.id}/original.${mimeToExt(item.mimeType)}`;
50
+ }
51
+ if (item.variants.length === 0) return null;
52
+ const smallest = item.variants.reduce((a, b) => a.width < b.width ? a : b);
53
+ return `/api/media/${item.id}/${smallest.width}.webp`;
54
+ }
55
+ function imageThumbnailUrl(item) {
56
+ return item?.kind === "image" ? mediaThumbnailUrl(item) : null;
57
+ }
58
+ function displayFilenameExt(mime) {
59
+ const ext = MIME_TO_EXT[mime];
60
+ return ext ? `.${ext}` : "";
61
+ }
62
+ function displayFilename(originalName, mimeType) {
63
+ const ext = MIME_TO_EXT[mimeType];
64
+ if (!ext) return originalName;
65
+ const suffix = `.${ext}`;
66
+ if (originalName.toLowerCase().endsWith(suffix)) return originalName;
67
+ return `${originalName}${suffix}`;
68
+ }
69
+ function formatFileSize(bytes) {
70
+ if (bytes >= 1048576) return `${Math.round(bytes / 1048576)}MB`;
71
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)}KB`;
72
+ return `${bytes}B`;
73
+ }
74
+ function displayName(item) {
75
+ return item.originalName;
76
+ }
77
+
78
+ export {
79
+ hashFileBuffer,
80
+ sanitizeMediaName,
81
+ MIME_TO_EXT,
82
+ EXT_TO_MIME,
83
+ mimeToExt,
84
+ mediaThumbnailUrl,
85
+ imageThumbnailUrl,
86
+ displayFilenameExt,
87
+ displayFilename,
88
+ formatFileSize,
89
+ displayName
90
+ };
File without changes