@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.
- package/dist/{chunk-OAGJBCSH.js → chunk-4WQ36ZP7.js} +19 -3
- package/dist/{chunk-PTY2RG2K.js → chunk-67P34WNW.js} +75 -130
- package/dist/chunk-APIJPTFG.js +64 -0
- package/dist/chunk-CDBURDPX.js +90 -0
- package/dist/chunk-DFKTF3CH.js +0 -0
- package/dist/chunk-DJABADOO.js +131 -0
- package/dist/{chunk-IP2SEBAJ.js → chunk-DJOGPSRU.js} +31 -72
- package/dist/{chunk-N3G7DOU3.js → chunk-FHTKSELX.js} +6 -6
- package/dist/chunk-GNF6KFQ7.js +18 -0
- package/dist/{chunk-QHMKIODQ.js → chunk-HHNPSKPT.js} +2 -89
- package/dist/{chunk-PZPGJZRG.js → chunk-JXATNKLV.js} +5 -5
- package/dist/{chunk-PLANHXS7.js → chunk-NX7TFHA4.js} +9 -9
- package/dist/{chunk-PNSCDBU7.js → chunk-O37AV27P.js} +6 -49
- package/dist/{chunk-5KSAS4VY.js → chunk-RIITGTV3.js} +1 -1
- package/dist/chunk-S7QRHUWO.js +8 -0
- package/dist/{chunk-7SAAJVDE.js → chunk-T26HAKW3.js} +37 -35
- package/dist/chunk-VY3IFGE3.js +70 -0
- package/dist/{chunk-VUYCRBJK.js → chunk-WYOFQHKI.js} +6 -4
- package/dist/{chunk-Y5QECJX7.js → chunk-YV3YSZEB.js} +5 -5
- package/dist/collab/rebase.d.ts.map +1 -1
- package/dist/collab/rebase.js +15 -15
- package/dist/components/editor/index.js +13 -13
- package/dist/components/primitives/index.js +16 -11
- package/dist/components/sections/brand-guide.js +13 -8
- package/dist/components/sections/register-schemas.js +20 -15
- package/dist/components/sections/register.js +20 -15
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/deck-builder/index.js +9 -9
- package/dist/hooks/useEditorPublish.d.ts +4 -1
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
- package/dist/index.js +85 -56
- package/dist/lib/dexie.d.ts +7 -1
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +3 -2
- package/dist/lib/image-variant-plan.d.ts +42 -0
- package/dist/lib/image-variant-plan.d.ts.map +1 -0
- package/dist/lib/image-variant-plan.js +53 -0
- package/dist/lib/index-ops.d.ts.map +1 -1
- package/dist/lib/index-ops.js +20 -16
- package/dist/lib/index.d.ts +2 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +43 -22
- package/dist/lib/loader.d.ts +16 -17
- package/dist/lib/loader.d.ts.map +1 -1
- package/dist/lib/media-ops.d.ts +73 -0
- package/dist/lib/media-ops.d.ts.map +1 -0
- package/dist/lib/media-ops.js +60 -0
- package/dist/lib/nav-tree.d.ts +6 -0
- package/dist/lib/nav-tree.d.ts.map +1 -1
- package/dist/lib/pages.d.ts +2 -1
- package/dist/lib/pages.d.ts.map +1 -1
- package/dist/lib/sidecar-diagnostics.d.ts +20 -0
- package/dist/lib/sidecar-diagnostics.d.ts.map +1 -0
- package/dist/lib/sidecars.d.ts +118 -0
- package/dist/lib/sidecars.d.ts.map +1 -0
- package/dist/lib/sidecars.js +23 -0
- package/dist/lib/slug.d.ts +2 -0
- package/dist/lib/slug.d.ts.map +1 -0
- package/dist/lib/storage-tier.d.ts +17 -0
- package/dist/lib/storage-tier.d.ts.map +1 -0
- package/dist/lib/storage-tier.js +29 -0
- package/dist/lib/tags.d.ts +6 -0
- package/dist/lib/tags.d.ts.map +1 -1
- package/dist/media/index.js +15 -15
- package/dist/media/queue.d.ts +2 -0
- package/dist/media/queue.d.ts.map +1 -1
- package/dist/media/worker.d.ts +1 -35
- package/dist/media/worker.d.ts.map +1 -1
- package/dist/schemas/index.js +18 -12
- package/dist/schemas/media.d.ts +4 -0
- package/dist/schemas/media.d.ts.map +1 -1
- package/dist/storage/index.js +2 -1
- package/dist/storage/memory.d.ts +21 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +129 -0
- package/dist/storage/types.d.ts +9 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +21 -1
- package/src/collab/rebase.ts +12 -16
- package/src/components/shell/EditorShell.tsx +7 -4
- package/src/hooks/useEditorPublish.ts +9 -4
- package/src/hooks/useMediaPipeline.ts +29 -23
- package/src/lib/dexie.ts +9 -1
- package/src/lib/image-variant-plan.ts +92 -0
- package/src/lib/index-ops.ts +9 -18
- package/src/lib/index.ts +15 -0
- package/src/lib/loader.ts +18 -77
- package/src/lib/media-ops.ts +112 -0
- package/src/lib/nav-tree.ts +33 -4
- package/src/lib/pages.ts +2 -3
- package/src/lib/sidecar-diagnostics.ts +29 -0
- package/src/lib/sidecars.ts +221 -0
- package/src/lib/slug.ts +6 -0
- package/src/lib/storage-tier.ts +38 -0
- package/src/lib/tags.ts +20 -3
- package/src/media/queue.ts +3 -0
- package/src/media/utils.ts +1 -1
- package/src/media/worker.ts +17 -8
- package/src/schemas/media.ts +7 -2
- package/src/storage/memory.ts +178 -0
- package/src/storage/types.ts +15 -2
- /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 === ""
|
|
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
|
|
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
|
|
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 =
|
|
19
|
+
var StatusSchema = z.enum(["draft", "live", "archived", "published"]).transform(
|
|
70
20
|
(val) => val === "published" ? "live" : val
|
|
71
21
|
);
|
|
72
|
-
var SectionMetaSchema =
|
|
73
|
-
type:
|
|
22
|
+
var SectionMetaSchema = z.object({
|
|
23
|
+
type: z.string(),
|
|
74
24
|
status: StatusSchema,
|
|
75
|
-
access:
|
|
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 =
|
|
80
|
-
var PageSchema =
|
|
81
|
-
id:
|
|
82
|
-
title:
|
|
83
|
-
slug:
|
|
84
|
-
isHome:
|
|
85
|
-
showInNav:
|
|
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:
|
|
88
|
-
order:
|
|
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 =
|
|
93
|
-
siteId:
|
|
94
|
-
order:
|
|
95
|
-
pages:
|
|
96
|
-
sections:
|
|
97
|
-
lastModified:
|
|
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 =
|
|
128
|
-
siteId:
|
|
129
|
-
pages:
|
|
130
|
-
sections:
|
|
131
|
-
lastModified:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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 =
|
|
146
|
+
var ChatbotTaskSchema = z.object({
|
|
197
147
|
id: ChatbotTaskIdSchema,
|
|
198
|
-
label:
|
|
199
|
-
prompt:
|
|
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:
|
|
151
|
+
access: z.array(z.string()).default([])
|
|
202
152
|
});
|
|
203
|
-
var ChatbotConfigSchema =
|
|
204
|
-
enabled:
|
|
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:
|
|
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:
|
|
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:
|
|
214
|
-
systemPrompt:
|
|
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:
|
|
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:
|
|
225
|
-
tasks:
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
245
|
-
siteName:
|
|
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:
|
|
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:
|
|
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:
|
|
261
|
-
uppercaseSubheadings:
|
|
262
|
-
uppercaseNavHeadings:
|
|
263
|
-
googleFontsUrl:
|
|
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:
|
|
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:
|
|
272
|
-
light:
|
|
273
|
-
dark:
|
|
274
|
-
invertInDark:
|
|
275
|
-
showInNav:
|
|
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:
|
|
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:
|
|
288
|
-
imageId:
|
|
289
|
-
width:
|
|
290
|
-
height:
|
|
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:
|
|
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:
|
|
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
|