@odla-ai/brand 0.3.0 → 0.5.0
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/README.md +62 -0
- package/dist/{chunk-APLECQBR.js → chunk-52DO72LS.js} +4 -3
- package/dist/chunk-52DO72LS.js.map +1 -0
- package/dist/{index-CRl3IXHB.d.cts → index-r36KQK5n.d.cts} +16 -3
- package/dist/{index-CRl3IXHB.d.ts → index-r36KQK5n.d.ts} +16 -3
- package/dist/index.cjs +1061 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +370 -9
- package/dist/index.d.ts +370 -9
- package/dist/index.js +998 -35
- package/dist/index.js.map +1 -1
- package/dist/tokens/index.cjs +1 -0
- package/dist/tokens/index.cjs.map +1 -1
- package/dist/tokens/index.d.cts +1 -1
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.js +3 -1
- package/package.json +1 -1
- package/dist/chunk-APLECQBR.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -47,7 +47,16 @@ __export(src_exports, {
|
|
|
47
47
|
DARK_FLIP_L_MIN: () => DARK_FLIP_L_MIN,
|
|
48
48
|
DEFAULT_ACCENT_SEED: () => DEFAULT_ACCENT_SEED,
|
|
49
49
|
DEFAULT_BRAND_SYSTEM: () => DEFAULT_BRAND_SYSTEM,
|
|
50
|
+
DESIGN_ASSET_KIND: () => DESIGN_ASSET_KIND,
|
|
51
|
+
DESIGN_CONTENT_TYPES: () => DESIGN_CONTENT_TYPES,
|
|
52
|
+
MAX_COLORS: () => MAX_COLORS,
|
|
53
|
+
MAX_EXTERNALS: () => MAX_EXTERNALS,
|
|
54
|
+
MAX_FONTS: () => MAX_FONTS,
|
|
55
|
+
MAX_HEADING_CHARS: () => MAX_HEADING_CHARS,
|
|
56
|
+
MAX_OUTLINE_ENTRIES: () => MAX_OUTLINE_ENTRIES,
|
|
57
|
+
MAX_PROPS: () => MAX_PROPS,
|
|
50
58
|
MAX_SWATCHES: () => MAX_SWATCHES,
|
|
59
|
+
MAX_THUMBNAIL_CHARS: () => MAX_THUMBNAIL_CHARS,
|
|
51
60
|
MAX_VIEW_BYTES: () => MAX_VIEW_BYTES,
|
|
52
61
|
PALETTE_SOURCES: () => PALETTE_SOURCES,
|
|
53
62
|
PALETTE_STATUSES: () => PALETTE_STATUSES,
|
|
@@ -56,6 +65,7 @@ __export(src_exports, {
|
|
|
56
65
|
PROPOSAL_STATUSES: () => PROPOSAL_STATUSES,
|
|
57
66
|
RAMP_L_MAX: () => RAMP_L_MAX,
|
|
58
67
|
RAMP_L_MIN: () => RAMP_L_MIN,
|
|
68
|
+
ROLE_TOKEN: () => ROLE_TOKEN,
|
|
59
69
|
SECTION_KINDS: () => SECTION_KINDS,
|
|
60
70
|
SECTION_STATUSES: () => SECTION_STATUSES,
|
|
61
71
|
SWATCH_ROLES: () => SWATCH_ROLES,
|
|
@@ -70,6 +80,7 @@ __export(src_exports, {
|
|
|
70
80
|
assetTools: () => assetTools,
|
|
71
81
|
attachedBrandAssetIds: () => attachedBrandAssetIds,
|
|
72
82
|
audienceFanoutOps: () => audienceFanoutOps,
|
|
83
|
+
base64ByteLength: () => base64ByteLength,
|
|
73
84
|
base64FromBytes: () => base64FromBytes,
|
|
74
85
|
beginAssetDeleteOps: () => beginAssetDeleteOps,
|
|
75
86
|
bookForChannel: () => bookForChannel,
|
|
@@ -88,27 +99,43 @@ __export(src_exports, {
|
|
|
88
99
|
capStringArray: () => capStringArray,
|
|
89
100
|
clamp01: () => clamp01,
|
|
90
101
|
clampToGamut: () => clampToGamut,
|
|
102
|
+
collapseWhitespace: () => collapseWhitespace,
|
|
91
103
|
compileBrandTokens: () => compileBrandTokens,
|
|
92
104
|
complementary: () => complementary,
|
|
93
105
|
contrastRatio: () => contrastRatio,
|
|
106
|
+
contrastReport: () => contrastReport,
|
|
94
107
|
createAssetOps: () => createAssetOps,
|
|
95
108
|
createBookOps: () => createBookOps,
|
|
96
109
|
createBrandIntegration: () => createBrandIntegration,
|
|
97
110
|
createBrandPersona: () => createBrandPersona,
|
|
98
111
|
createBrandRoutes: () => createBrandRoutes,
|
|
112
|
+
cssColorToHex: () => cssColorToHex,
|
|
113
|
+
decodeEntities: () => decodeEntities,
|
|
99
114
|
deltaEOK: () => deltaEOK,
|
|
100
115
|
deltaEOKLab: () => deltaEOKLab,
|
|
101
116
|
deriveChartColors: () => deriveChartColors,
|
|
102
117
|
deriveDarkTokens: () => deriveDarkTokens,
|
|
103
118
|
derivePalette: () => derivePalette,
|
|
119
|
+
digestDesignBundle: () => digestDesignBundle,
|
|
120
|
+
digestDesignHtml: () => digestDesignHtml,
|
|
104
121
|
dispatchBrandTurn: () => dispatchBrandTurn,
|
|
122
|
+
extractColors: () => extractColors,
|
|
123
|
+
extractDesignTokens: () => extractDesignTokens,
|
|
124
|
+
extractFonts: () => extractFonts,
|
|
125
|
+
extractOutline: () => extractOutline,
|
|
126
|
+
extractProps: () => extractProps,
|
|
127
|
+
extractThumbnailSvg: () => extractThumbnailSvg,
|
|
128
|
+
extractTitle: () => extractTitle,
|
|
105
129
|
finishAssetDeleteOps: () => finishAssetDeleteOps,
|
|
106
130
|
formatBrandDiscussionReference: () => formatBrandDiscussionReference,
|
|
131
|
+
groupAssets: () => groupAssets,
|
|
107
132
|
hexToOklch: () => hexToOklch,
|
|
108
133
|
hslToRgb: () => hslToRgb,
|
|
134
|
+
htmlToText: () => htmlToText,
|
|
109
135
|
inSrgbGamut: () => inSrgbGamut,
|
|
110
136
|
isBoundedBrandJson: () => isBoundedBrandJson,
|
|
111
137
|
isBrandHumanAuthorityConsumption: () => isBrandHumanAuthorityConsumption,
|
|
138
|
+
isDesignBundle: () => isDesignBundle,
|
|
112
139
|
linearToSrgb: () => linearToSrgb,
|
|
113
140
|
mapPaletteToTokens: () => mapPaletteToTokens,
|
|
114
141
|
meetsAA: () => meetsAA,
|
|
@@ -123,25 +150,32 @@ __export(src_exports, {
|
|
|
123
150
|
paletteTools: () => paletteTools,
|
|
124
151
|
parseBrandDiscussionReference: () => parseBrandDiscussionReference,
|
|
125
152
|
parseBrandDispatch: () => parseBrandDispatch,
|
|
153
|
+
parseDesignBundle: () => parseDesignBundle,
|
|
126
154
|
parseHex: () => parseHex,
|
|
127
155
|
pickTextOn: () => pickTextOn,
|
|
128
156
|
proposalReviewSnapshot: () => proposalReviewSnapshot,
|
|
129
157
|
proposePaletteOps: () => proposePaletteOps,
|
|
130
158
|
proposeSectionOps: () => proposeSectionOps,
|
|
159
|
+
readDesignManifest: () => readDesignManifest,
|
|
131
160
|
readTools: () => readTools,
|
|
132
161
|
recordAnalysisOps: () => recordAnalysisOps,
|
|
133
162
|
rejectProposalOps: () => rejectProposalOps,
|
|
134
163
|
relativeLuminance: () => relativeLuminance,
|
|
135
164
|
renderTokensCss: () => renderTokensCss,
|
|
136
165
|
resolveDeps: () => resolveDeps,
|
|
166
|
+
resolveVarRefs: () => resolveVarRefs,
|
|
137
167
|
rgbToHsl: () => rgbToHsl,
|
|
138
168
|
rgbToOklab: () => rgbToOklab,
|
|
139
169
|
rotateHue: () => rotateHue,
|
|
140
170
|
safeFileName: () => safeFileName,
|
|
171
|
+
scanCustomProperties: () => scanCustomProperties,
|
|
141
172
|
sectionKey: () => sectionKey,
|
|
142
173
|
splitComplementary: () => splitComplementary,
|
|
143
174
|
srgbToLinear: () => srgbToLinear,
|
|
175
|
+
stripCssComments: () => stripCssComments,
|
|
176
|
+
styleSheetText: () => styleSheetText,
|
|
144
177
|
supportsBrandVision: () => supportsBrandVision,
|
|
178
|
+
swatchesFromDesignTokens: () => swatchesFromDesignTokens,
|
|
145
179
|
tetradic: () => tetradic,
|
|
146
180
|
tintShadeRamp: () => tintShadeRamp,
|
|
147
181
|
toHex: () => toHex,
|
|
@@ -168,7 +202,15 @@ var PALETTE_STATUSES = ["active", "archived"];
|
|
|
168
202
|
var PALETTE_SOURCES = ["extracted", "derived", "manual"];
|
|
169
203
|
var PROPOSAL_KINDS = SECTION_KINDS;
|
|
170
204
|
var PROPOSAL_STATUSES = ["open", "accepted", "rejected", "superseded"];
|
|
171
|
-
var ASSET_KINDS = [
|
|
205
|
+
var ASSET_KINDS = [
|
|
206
|
+
"logo",
|
|
207
|
+
"wordmark",
|
|
208
|
+
"inspiration",
|
|
209
|
+
"document",
|
|
210
|
+
"design",
|
|
211
|
+
"other"
|
|
212
|
+
];
|
|
213
|
+
var DESIGN_ASSET_KIND = "design";
|
|
172
214
|
var SWATCH_ROLES = [
|
|
173
215
|
"primary",
|
|
174
216
|
"secondary",
|
|
@@ -392,7 +434,7 @@ var BRAND_SCHEMA = {
|
|
|
392
434
|
id: uniq("string"),
|
|
393
435
|
bookId: idx("string"),
|
|
394
436
|
kind: idx("string"),
|
|
395
|
-
// logo | wordmark | inspiration | document | other
|
|
437
|
+
// logo | wordmark | inspiration | document | design | other
|
|
396
438
|
path: idx("string"),
|
|
397
439
|
storageObjectId: idx("string"),
|
|
398
440
|
contentDigest: idx("string"),
|
|
@@ -404,6 +446,11 @@ var BRAND_SCHEMA = {
|
|
|
404
446
|
analysis: opt("json"),
|
|
405
447
|
// { description, dominantColors: hex[], tags }
|
|
406
448
|
analyzedAt: opt("date"),
|
|
449
|
+
// `design` kind only: the DesignDigest computed from the uploaded
|
|
450
|
+
// bundle at upload time. Machine-derived and deterministic — unlike
|
|
451
|
+
// `analysis`, which is what a model reports seeing — so it needs no
|
|
452
|
+
// review gate and is rewritten only by re-uploading.
|
|
453
|
+
design: opt("json"),
|
|
407
454
|
analysisRevision: a("number"),
|
|
408
455
|
analysisDigest: opt("string"),
|
|
409
456
|
analyzedBy: opt("string"),
|
|
@@ -498,6 +545,7 @@ var ASSET_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
|
498
545
|
"image/webp",
|
|
499
546
|
"application/pdf"
|
|
500
547
|
]);
|
|
548
|
+
var DESIGN_CONTENT_TYPES = /* @__PURE__ */ new Set(["text/html"]);
|
|
501
549
|
var HEX_RGB = /^#[0-9a-f]{3}$/;
|
|
502
550
|
var HEX_RRGGBB = /^#[0-9a-f]{6}$/;
|
|
503
551
|
var HEX_ALPHA = /^#[0-9a-f]{4}$|^#[0-9a-f]{8}$/;
|
|
@@ -619,12 +667,13 @@ function safeFileName(name) {
|
|
|
619
667
|
throw new BrandInputError("file name is empty or a dot segment after sanitizing");
|
|
620
668
|
return cleaned.slice(0, 120);
|
|
621
669
|
}
|
|
622
|
-
function assertAssetContentType(value) {
|
|
670
|
+
function assertAssetContentType(value, kind) {
|
|
623
671
|
if (typeof value !== "string") throw new BrandInputError("contentType must be a string");
|
|
624
672
|
const ct = value.split(";")[0].trim().toLowerCase();
|
|
625
|
-
|
|
673
|
+
const allowed = kind === DESIGN_ASSET_KIND ? DESIGN_CONTENT_TYPES : ASSET_CONTENT_TYPES;
|
|
674
|
+
if (!allowed.has(ct))
|
|
626
675
|
throw new BrandInputError(
|
|
627
|
-
`unsupported content type ${ct || "(empty)"}; allowed: ${[...
|
|
676
|
+
`unsupported content type ${ct || "(empty)"} for kind ${kind ?? "other"}; allowed: ${[...allowed].join(", ")}`
|
|
628
677
|
);
|
|
629
678
|
return ct;
|
|
630
679
|
}
|
|
@@ -1171,7 +1220,11 @@ function rejectProposalOps(input) {
|
|
|
1171
1220
|
function createAssetOps(input) {
|
|
1172
1221
|
if (!ASSET_KINDS.includes(input.kind))
|
|
1173
1222
|
throw new BrandInputError(`kind must be one of: ${ASSET_KINDS.join(", ")}`);
|
|
1174
|
-
const contentType = assertAssetContentType(input.contentType);
|
|
1223
|
+
const contentType = assertAssetContentType(input.contentType, input.kind);
|
|
1224
|
+
if (input.kind === DESIGN_ASSET_KIND && input.design === void 0)
|
|
1225
|
+
throw new BrandInputError("a design asset must carry its parsed digest");
|
|
1226
|
+
if (input.kind !== DESIGN_ASSET_KIND && input.design !== void 0)
|
|
1227
|
+
throw new BrandInputError(`only ${DESIGN_ASSET_KIND} assets may carry a design digest`);
|
|
1175
1228
|
if (typeof input.size !== "number" || !Number.isFinite(input.size) || input.size <= 0)
|
|
1176
1229
|
throw new BrandInputError("size must be a positive byte count");
|
|
1177
1230
|
if (!/^sha256:[0-9a-f]{64}$/.test(input.contentDigest))
|
|
@@ -1201,7 +1254,8 @@ function createAssetOps(input) {
|
|
|
1201
1254
|
200
|
|
1202
1255
|
),
|
|
1203
1256
|
createdAt: input.now,
|
|
1204
|
-
...title ? { title } : {}
|
|
1257
|
+
...title ? { title } : {},
|
|
1258
|
+
...input.design ? { design: input.design } : {}
|
|
1205
1259
|
}
|
|
1206
1260
|
},
|
|
1207
1261
|
{ t: "link", ns: BRAND_NS.asset, id: input.id, label: "book", target: input.bookId }
|
|
@@ -1253,6 +1307,180 @@ async function recordAnalysisOps(assetId, analysis, priorRevision, analyzedBy, a
|
|
|
1253
1307
|
];
|
|
1254
1308
|
}
|
|
1255
1309
|
|
|
1310
|
+
// src/design/bundle.ts
|
|
1311
|
+
var MAX_THUMBNAIL_CHARS = 16384;
|
|
1312
|
+
function island(html, name) {
|
|
1313
|
+
const open = `<script type="__bundler/${name}">`;
|
|
1314
|
+
const start = html.indexOf(open);
|
|
1315
|
+
if (start < 0) return null;
|
|
1316
|
+
const from = start + open.length;
|
|
1317
|
+
const end = html.indexOf("</script>", from);
|
|
1318
|
+
return end < 0 ? null : html.slice(from, end);
|
|
1319
|
+
}
|
|
1320
|
+
function isDesignBundle(html) {
|
|
1321
|
+
return html.includes('<script type="__bundler/manifest">') && html.includes('<script type="__bundler/template">');
|
|
1322
|
+
}
|
|
1323
|
+
function base64ByteLength(data) {
|
|
1324
|
+
const len = data.length;
|
|
1325
|
+
if (len === 0) return 0;
|
|
1326
|
+
const pad = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
1327
|
+
return Math.max(0, Math.floor(len * 3 / 4) - pad);
|
|
1328
|
+
}
|
|
1329
|
+
function parseIslandJson(text, name) {
|
|
1330
|
+
try {
|
|
1331
|
+
return JSON.parse(text);
|
|
1332
|
+
} catch {
|
|
1333
|
+
throw new BrandInputError(`design bundle's ${name} island is not valid JSON`);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1337
|
+
function readAssets(raw) {
|
|
1338
|
+
if (!isRecord2(raw)) throw new BrandInputError("design bundle's manifest island must be an object");
|
|
1339
|
+
const assets = [];
|
|
1340
|
+
for (const [uuid, value] of Object.entries(raw)) {
|
|
1341
|
+
if (!isRecord2(value)) continue;
|
|
1342
|
+
const data = value.data;
|
|
1343
|
+
const mime = value.mime;
|
|
1344
|
+
if (typeof data !== "string" || typeof mime !== "string") continue;
|
|
1345
|
+
assets.push({
|
|
1346
|
+
uuid,
|
|
1347
|
+
mime: mime.slice(0, 120),
|
|
1348
|
+
bytes: base64ByteLength(data),
|
|
1349
|
+
compressed: value.compressed === true
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
return assets;
|
|
1353
|
+
}
|
|
1354
|
+
function readExternals(raw) {
|
|
1355
|
+
if (raw === null || raw === void 0) return [];
|
|
1356
|
+
if (!Array.isArray(raw))
|
|
1357
|
+
throw new BrandInputError("design bundle's ext_resources island must be an array");
|
|
1358
|
+
const out = [];
|
|
1359
|
+
for (const entry of raw) {
|
|
1360
|
+
if (isRecord2(entry) && typeof entry.id === "string") out.push(entry.id.slice(0, 512));
|
|
1361
|
+
}
|
|
1362
|
+
return out;
|
|
1363
|
+
}
|
|
1364
|
+
function readPageOrder(raw) {
|
|
1365
|
+
if (raw === null || raw === void 0) return [];
|
|
1366
|
+
if (!Array.isArray(raw))
|
|
1367
|
+
throw new BrandInputError("design bundle's page_order island must be an array");
|
|
1368
|
+
return raw.filter((v) => typeof v === "string");
|
|
1369
|
+
}
|
|
1370
|
+
function extractThumbnailSvg(html) {
|
|
1371
|
+
const anchor = html.indexOf("__bundler_thumbnail");
|
|
1372
|
+
if (anchor < 0) return void 0;
|
|
1373
|
+
const open = html.indexOf("<svg", anchor);
|
|
1374
|
+
if (open < 0) return void 0;
|
|
1375
|
+
const close = html.indexOf("</svg>", open);
|
|
1376
|
+
if (close < 0) return void 0;
|
|
1377
|
+
const svg = html.slice(open, close + "</svg>".length);
|
|
1378
|
+
return svg.length > MAX_THUMBNAIL_CHARS ? void 0 : svg;
|
|
1379
|
+
}
|
|
1380
|
+
function readDesignManifest(html) {
|
|
1381
|
+
const raw = island(html, "manifest");
|
|
1382
|
+
if (raw === null) throw new BrandInputError("design bundle has no manifest island");
|
|
1383
|
+
const parsed = parseIslandJson(raw, "manifest");
|
|
1384
|
+
if (!isRecord2(parsed)) throw new BrandInputError("design bundle's manifest island must be an object");
|
|
1385
|
+
const out = {};
|
|
1386
|
+
for (const [uuid, value] of Object.entries(parsed)) {
|
|
1387
|
+
if (!isRecord2(value)) continue;
|
|
1388
|
+
const { data, mime } = value;
|
|
1389
|
+
if (typeof data !== "string" || typeof mime !== "string") continue;
|
|
1390
|
+
out[uuid] = { mime, compressed: value.compressed === true, data };
|
|
1391
|
+
}
|
|
1392
|
+
return out;
|
|
1393
|
+
}
|
|
1394
|
+
function parseDesignBundle(html) {
|
|
1395
|
+
if (!isDesignBundle(html))
|
|
1396
|
+
throw new BrandInputError(
|
|
1397
|
+
"not a Claude Design bundle: no __bundler/manifest and __bundler/template script islands. Export the design as standalone HTML and upload that file."
|
|
1398
|
+
);
|
|
1399
|
+
const templateRaw = island(html, "template");
|
|
1400
|
+
if (templateRaw === null)
|
|
1401
|
+
throw new BrandInputError("design bundle's template island is unterminated");
|
|
1402
|
+
const template = parseIslandJson(templateRaw, "template");
|
|
1403
|
+
if (typeof template !== "string")
|
|
1404
|
+
throw new BrandInputError("design bundle's template island must be a JSON string");
|
|
1405
|
+
const manifestRaw = island(html, "manifest");
|
|
1406
|
+
if (manifestRaw === null)
|
|
1407
|
+
throw new BrandInputError("design bundle's manifest island is unterminated");
|
|
1408
|
+
const extRaw = island(html, "ext_resources");
|
|
1409
|
+
const pageRaw = island(html, "page_order");
|
|
1410
|
+
const thumbnailSvg = extractThumbnailSvg(html);
|
|
1411
|
+
return {
|
|
1412
|
+
template,
|
|
1413
|
+
assets: readAssets(parseIslandJson(manifestRaw, "manifest")),
|
|
1414
|
+
externals: readExternals(extRaw === null ? null : parseIslandJson(extRaw, "ext_resources")),
|
|
1415
|
+
pageOrder: readPageOrder(pageRaw === null ? null : parseIslandJson(pageRaw, "page_order")),
|
|
1416
|
+
...thumbnailSvg ? { thumbnailSvg } : {}
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// src/design/css-scan.ts
|
|
1421
|
+
function stripCssComments(css) {
|
|
1422
|
+
let out = "";
|
|
1423
|
+
let i = 0;
|
|
1424
|
+
for (; ; ) {
|
|
1425
|
+
const start = css.indexOf("/*", i);
|
|
1426
|
+
if (start < 0) return out + css.slice(i);
|
|
1427
|
+
out += css.slice(i, start);
|
|
1428
|
+
const end = css.indexOf("*/", start + 2);
|
|
1429
|
+
if (end < 0) return out;
|
|
1430
|
+
i = end + 2;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
function styleSheetText(html) {
|
|
1434
|
+
const parts = [];
|
|
1435
|
+
let i = 0;
|
|
1436
|
+
for (; ; ) {
|
|
1437
|
+
const open = html.indexOf("<style", i);
|
|
1438
|
+
if (open < 0) break;
|
|
1439
|
+
const gt = html.indexOf(">", open);
|
|
1440
|
+
if (gt < 0) break;
|
|
1441
|
+
const close = html.indexOf("</style>", gt);
|
|
1442
|
+
if (close < 0) break;
|
|
1443
|
+
parts.push(html.slice(gt + 1, close));
|
|
1444
|
+
i = close + "</style>".length;
|
|
1445
|
+
}
|
|
1446
|
+
return parts.join("\n");
|
|
1447
|
+
}
|
|
1448
|
+
function scanCustomProperties(css) {
|
|
1449
|
+
const text = stripCssComments(css);
|
|
1450
|
+
const found = [];
|
|
1451
|
+
const stack = [];
|
|
1452
|
+
let paren = 0;
|
|
1453
|
+
let start = 0;
|
|
1454
|
+
const flush = (end) => {
|
|
1455
|
+
if (stack.length === 0) return;
|
|
1456
|
+
const chunk = text.slice(start, end).trim();
|
|
1457
|
+
if (!chunk.startsWith("--")) return;
|
|
1458
|
+
const colon = chunk.indexOf(":");
|
|
1459
|
+
if (colon < 0) return;
|
|
1460
|
+
const name = chunk.slice(0, colon).trim();
|
|
1461
|
+
if (name.length < 3) return;
|
|
1462
|
+
found.push({ selectors: [...stack], name, value: chunk.slice(colon + 1).trim() });
|
|
1463
|
+
};
|
|
1464
|
+
for (let i = 0; i < text.length; i++) {
|
|
1465
|
+
const ch = text[i];
|
|
1466
|
+
if (ch === "(") paren++;
|
|
1467
|
+
else if (ch === ")") paren = Math.max(0, paren - 1);
|
|
1468
|
+
if (paren !== 0) continue;
|
|
1469
|
+
if (ch === "{") {
|
|
1470
|
+
stack.push(text.slice(start, i).trim());
|
|
1471
|
+
start = i + 1;
|
|
1472
|
+
} else if (ch === "}") {
|
|
1473
|
+
flush(i);
|
|
1474
|
+
stack.pop();
|
|
1475
|
+
start = i + 1;
|
|
1476
|
+
} else if (ch === ";") {
|
|
1477
|
+
flush(i);
|
|
1478
|
+
start = i + 1;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return found;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1256
1484
|
// src/color/hex.ts
|
|
1257
1485
|
function clamp01(x) {
|
|
1258
1486
|
return Number.isNaN(x) ? 0 : x < 0 ? 0 : x > 1 ? 1 : x;
|
|
@@ -1763,69 +1991,6 @@ function derivePalette(seedHex, opts = {}) {
|
|
|
1763
1991
|
return withNames ? swatches.map((s) => ({ ...s, name: nearestNamedColor(s.hex).name })) : swatches;
|
|
1764
1992
|
}
|
|
1765
1993
|
|
|
1766
|
-
// src/tokens/roles.ts
|
|
1767
|
-
var BRAND_REQUIRED_TOKENS = [
|
|
1768
|
-
"--ui-bg",
|
|
1769
|
-
"--ui-surface",
|
|
1770
|
-
"--ui-surface-2",
|
|
1771
|
-
"--ui-text",
|
|
1772
|
-
"--ui-text-muted",
|
|
1773
|
-
"--ui-text-faint",
|
|
1774
|
-
"--ui-border",
|
|
1775
|
-
"--ui-border-strong",
|
|
1776
|
-
"--ui-accent",
|
|
1777
|
-
"--ui-accent-strong",
|
|
1778
|
-
"--ui-accent-soft",
|
|
1779
|
-
"--ui-on-accent",
|
|
1780
|
-
"--ui-good",
|
|
1781
|
-
"--ui-good-soft",
|
|
1782
|
-
"--ui-warn",
|
|
1783
|
-
"--ui-warn-soft",
|
|
1784
|
-
"--ui-danger",
|
|
1785
|
-
"--ui-danger-soft",
|
|
1786
|
-
"--ui-code-bg",
|
|
1787
|
-
"--ui-code-text",
|
|
1788
|
-
"--ui-shadow",
|
|
1789
|
-
"--ui-font-sans",
|
|
1790
|
-
"--ui-font-serif",
|
|
1791
|
-
"--ui-font-mono",
|
|
1792
|
-
"--ui-font-display"
|
|
1793
|
-
];
|
|
1794
|
-
var BRAND_DERIVED_TOKENS = [
|
|
1795
|
-
"--ui-accent-glow",
|
|
1796
|
-
"--ui-accent-2",
|
|
1797
|
-
"--ui-accent-2-soft",
|
|
1798
|
-
"--ui-highlight",
|
|
1799
|
-
"--ui-focus",
|
|
1800
|
-
"--ui-shadow-strong"
|
|
1801
|
-
];
|
|
1802
|
-
var BRAND_CHART_TOKENS = [
|
|
1803
|
-
"--ui-chart-1",
|
|
1804
|
-
"--ui-chart-2",
|
|
1805
|
-
"--ui-chart-3",
|
|
1806
|
-
"--ui-chart-4",
|
|
1807
|
-
"--ui-chart-5",
|
|
1808
|
-
"--ui-chart-6",
|
|
1809
|
-
"--ui-chart-band",
|
|
1810
|
-
"--ui-chart-band-strong",
|
|
1811
|
-
"--ui-chart-flow",
|
|
1812
|
-
"--ui-chart-glow"
|
|
1813
|
-
];
|
|
1814
|
-
var BRAND_CHAT_TOKENS = [
|
|
1815
|
-
"--ui-chat-user-bg",
|
|
1816
|
-
"--ui-chat-user-text",
|
|
1817
|
-
"--ui-chat-assistant-bg",
|
|
1818
|
-
"--ui-chat-thinking-bg",
|
|
1819
|
-
"--ui-chat-thinking-text",
|
|
1820
|
-
"--ui-chat-tool-accent"
|
|
1821
|
-
];
|
|
1822
|
-
var BRAND_EMITTED_TOKENS = [
|
|
1823
|
-
...BRAND_REQUIRED_TOKENS,
|
|
1824
|
-
...BRAND_DERIVED_TOKENS,
|
|
1825
|
-
...BRAND_CHART_TOKENS,
|
|
1826
|
-
...BRAND_CHAT_TOKENS
|
|
1827
|
-
];
|
|
1828
|
-
|
|
1829
1994
|
// src/tokens/map.ts
|
|
1830
1995
|
var DEFAULT_ACCENT_SEED = "#3b5e8c";
|
|
1831
1996
|
var CHROMATIC_C = 0.02;
|
|
@@ -2025,6 +2190,452 @@ function mapPaletteToTokens(input) {
|
|
|
2025
2190
|
return { tokens, warnings };
|
|
2026
2191
|
}
|
|
2027
2192
|
|
|
2193
|
+
// src/design/decompile.ts
|
|
2194
|
+
var CHART_TOKENS = [
|
|
2195
|
+
"--ui-chart-1",
|
|
2196
|
+
"--ui-chart-2",
|
|
2197
|
+
"--ui-chart-3",
|
|
2198
|
+
"--ui-chart-4",
|
|
2199
|
+
"--ui-chart-5",
|
|
2200
|
+
"--ui-chart-6"
|
|
2201
|
+
];
|
|
2202
|
+
function cssColorToHex(value) {
|
|
2203
|
+
const text = value.trim();
|
|
2204
|
+
if (text.startsWith("#")) {
|
|
2205
|
+
try {
|
|
2206
|
+
return assertHex(text);
|
|
2207
|
+
} catch {
|
|
2208
|
+
return null;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
const fn = /^rgba?\(([^)]*)\)$/i.exec(text);
|
|
2212
|
+
if (!fn) return null;
|
|
2213
|
+
const parts = (fn[1] ?? "").split(/[,/\s]+/).filter((p) => p !== "");
|
|
2214
|
+
if (parts.length < 3 || parts.length > 4) return null;
|
|
2215
|
+
if (parts.length === 4) {
|
|
2216
|
+
const alpha = parts[3].endsWith("%") ? Number.parseFloat(parts[3]) / 100 : Number.parseFloat(parts[3]);
|
|
2217
|
+
if (!Number.isFinite(alpha) || alpha < 1) return null;
|
|
2218
|
+
}
|
|
2219
|
+
const channels = parts.slice(0, 3).map((part) => {
|
|
2220
|
+
const n = Number.parseFloat(part);
|
|
2221
|
+
if (!Number.isFinite(n)) return Number.NaN;
|
|
2222
|
+
return Math.round(part.endsWith("%") ? n / 100 * 255 : n);
|
|
2223
|
+
});
|
|
2224
|
+
if (channels.some((c) => !Number.isFinite(c) || c < 0 || c > 255)) return null;
|
|
2225
|
+
return `#${channels.map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
|
2226
|
+
}
|
|
2227
|
+
var skipReason = (value) => value.includes("var(") ? "unresolved var() reference" : value.includes("color-mix(") ? "composed with color-mix()" : /rgba?\(/i.test(value) ? "not fully opaque" : "not a literal color";
|
|
2228
|
+
function swatchesFromDesignTokens(tokens) {
|
|
2229
|
+
const light = tokens.light;
|
|
2230
|
+
const swatches = [];
|
|
2231
|
+
const skipped = [];
|
|
2232
|
+
const missing2 = [];
|
|
2233
|
+
for (const [role, token] of Object.entries(ROLE_TOKEN)) {
|
|
2234
|
+
if (role === "chart") continue;
|
|
2235
|
+
const value = light[token];
|
|
2236
|
+
if (value === void 0) {
|
|
2237
|
+
missing2.push(role);
|
|
2238
|
+
continue;
|
|
2239
|
+
}
|
|
2240
|
+
const hex = cssColorToHex(value);
|
|
2241
|
+
if (hex === null) {
|
|
2242
|
+
skipped.push({ token, value, reason: skipReason(value) });
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
swatches.push({ role, hex, rationale: `declared by the design as ${token}` });
|
|
2246
|
+
}
|
|
2247
|
+
let anyChart = false;
|
|
2248
|
+
for (const token of CHART_TOKENS) {
|
|
2249
|
+
const value = light[token];
|
|
2250
|
+
if (value === void 0) continue;
|
|
2251
|
+
const hex = cssColorToHex(value);
|
|
2252
|
+
if (hex === null) {
|
|
2253
|
+
skipped.push({ token, value, reason: skipReason(value) });
|
|
2254
|
+
continue;
|
|
2255
|
+
}
|
|
2256
|
+
anyChart = true;
|
|
2257
|
+
swatches.push({ role: "chart", hex, rationale: `declared by the design as ${token}` });
|
|
2258
|
+
}
|
|
2259
|
+
if (!anyChart) missing2.push("chart");
|
|
2260
|
+
return { swatches, skipped, missing: missing2 };
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// src/design/html-text.ts
|
|
2264
|
+
var NAMED_ENTITIES = {
|
|
2265
|
+
amp: "&",
|
|
2266
|
+
lt: "<",
|
|
2267
|
+
gt: ">",
|
|
2268
|
+
quot: '"',
|
|
2269
|
+
apos: "'",
|
|
2270
|
+
nbsp: "\xA0",
|
|
2271
|
+
mdash: "\u2014",
|
|
2272
|
+
ndash: "\u2013",
|
|
2273
|
+
hellip: "\u2026",
|
|
2274
|
+
rsquo: "\u2019",
|
|
2275
|
+
lsquo: "\u2018",
|
|
2276
|
+
ldquo: "\u201C",
|
|
2277
|
+
rdquo: "\u201D"
|
|
2278
|
+
};
|
|
2279
|
+
function decodeEntities(text) {
|
|
2280
|
+
return text.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z][a-zA-Z0-9]{1,31});/g, (whole, body) => {
|
|
2281
|
+
if (body.startsWith("#x") || body.startsWith("#X")) {
|
|
2282
|
+
const code = Number.parseInt(body.slice(2), 16);
|
|
2283
|
+
return Number.isFinite(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
|
|
2284
|
+
}
|
|
2285
|
+
if (body.startsWith("#")) {
|
|
2286
|
+
const code = Number.parseInt(body.slice(1), 10);
|
|
2287
|
+
return Number.isFinite(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
|
|
2288
|
+
}
|
|
2289
|
+
return NAMED_ENTITIES[body.toLowerCase()] ?? whole;
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
var collapseWhitespace = (text) => text.replace(/\s+/g, " ").trim();
|
|
2293
|
+
function htmlToText(fragment) {
|
|
2294
|
+
const withoutCode = fragment.replace(/<script\b[^>]*>[\s\S]*?<\/script\s*>/gi, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi, " ");
|
|
2295
|
+
return collapseWhitespace(decodeEntities(withoutCode.replace(/<[^>]*>/g, " ")));
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
// src/design/outline.ts
|
|
2299
|
+
var MAX_OUTLINE_ENTRIES = 120;
|
|
2300
|
+
var MAX_HEADING_CHARS = 200;
|
|
2301
|
+
function extractTitle(html) {
|
|
2302
|
+
const match = /<title\b[^>]*>([\s\S]{0,2000}?)<\/title\s*>/i.exec(html);
|
|
2303
|
+
if (!match) return void 0;
|
|
2304
|
+
const text = htmlToText(match[1] ?? "");
|
|
2305
|
+
return text === "" ? void 0 : text.slice(0, MAX_HEADING_CHARS);
|
|
2306
|
+
}
|
|
2307
|
+
function extractOutline(html) {
|
|
2308
|
+
const pattern = /<h([1-6])\b[^>]*>([\s\S]{0,4000}?)<\/h\1\s*>/gi;
|
|
2309
|
+
const entries = [];
|
|
2310
|
+
for (; ; ) {
|
|
2311
|
+
const match = pattern.exec(html);
|
|
2312
|
+
if (match === null) break;
|
|
2313
|
+
const text = htmlToText(match[2] ?? "");
|
|
2314
|
+
if (text === "") continue;
|
|
2315
|
+
if (entries.length >= MAX_OUTLINE_ENTRIES) return { entries, truncated: true };
|
|
2316
|
+
entries.push({ level: Number(match[1]), text: text.slice(0, MAX_HEADING_CHARS) });
|
|
2317
|
+
}
|
|
2318
|
+
return { entries, truncated: false };
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
// src/design/props.ts
|
|
2322
|
+
var MAX_PROPS = 60;
|
|
2323
|
+
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2324
|
+
function attributeValue(html, attr) {
|
|
2325
|
+
const at = html.indexOf(`${attr}="`);
|
|
2326
|
+
if (at >= 0) {
|
|
2327
|
+
const from2 = at + attr.length + 2;
|
|
2328
|
+
const end2 = html.indexOf('"', from2);
|
|
2329
|
+
return end2 < 0 ? null : html.slice(from2, end2);
|
|
2330
|
+
}
|
|
2331
|
+
const single = html.indexOf(`${attr}='`);
|
|
2332
|
+
if (single < 0) return null;
|
|
2333
|
+
const from = single + attr.length + 2;
|
|
2334
|
+
const end = html.indexOf("'", from);
|
|
2335
|
+
return end < 0 ? null : html.slice(from, end);
|
|
2336
|
+
}
|
|
2337
|
+
function defaultText(value) {
|
|
2338
|
+
if (value === void 0 || value === null) return void 0;
|
|
2339
|
+
if (typeof value === "string") return value.slice(0, 400);
|
|
2340
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
2341
|
+
try {
|
|
2342
|
+
return JSON.stringify(value).slice(0, 400);
|
|
2343
|
+
} catch {
|
|
2344
|
+
return void 0;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
function toProp(name, spec) {
|
|
2348
|
+
const options = Array.isArray(spec.options) ? spec.options.filter((v) => typeof v === "string").slice(0, 24) : void 0;
|
|
2349
|
+
const declared = defaultText(spec.default);
|
|
2350
|
+
return {
|
|
2351
|
+
name: name.slice(0, 80),
|
|
2352
|
+
editor: typeof spec.editor === "string" ? spec.editor.slice(0, 40) : "unknown",
|
|
2353
|
+
...options && options.length > 0 ? { options } : {},
|
|
2354
|
+
...declared !== void 0 ? { default: declared } : {},
|
|
2355
|
+
...typeof spec.section === "string" ? { section: spec.section.slice(0, 80) } : {},
|
|
2356
|
+
...typeof spec.tsType === "string" ? { tsType: spec.tsType.slice(0, 200) } : {}
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
function extractProps(html) {
|
|
2360
|
+
const at = html.indexOf("data-props=");
|
|
2361
|
+
if (at < 0) return { props: [], truncated: false };
|
|
2362
|
+
const raw = attributeValue(html.slice(at), "data-props");
|
|
2363
|
+
if (raw === null) return { props: [], truncated: false };
|
|
2364
|
+
let parsed;
|
|
2365
|
+
try {
|
|
2366
|
+
parsed = JSON.parse(decodeEntities(raw));
|
|
2367
|
+
} catch {
|
|
2368
|
+
return { props: [], truncated: false };
|
|
2369
|
+
}
|
|
2370
|
+
if (!isRecord3(parsed)) return { props: [], truncated: false };
|
|
2371
|
+
const entries = Object.entries(parsed).filter(
|
|
2372
|
+
(entry) => isRecord3(entry[1])
|
|
2373
|
+
);
|
|
2374
|
+
return {
|
|
2375
|
+
props: entries.slice(0, MAX_PROPS).map(([name, spec]) => toProp(name, spec)),
|
|
2376
|
+
truncated: entries.length > MAX_PROPS
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
// src/design/styles.ts
|
|
2381
|
+
var MAX_FONTS = 16;
|
|
2382
|
+
var MAX_COLORS = 24;
|
|
2383
|
+
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
2384
|
+
"serif",
|
|
2385
|
+
"sans-serif",
|
|
2386
|
+
"monospace",
|
|
2387
|
+
"cursive",
|
|
2388
|
+
"fantasy",
|
|
2389
|
+
"system-ui",
|
|
2390
|
+
"ui-serif",
|
|
2391
|
+
"ui-sans-serif",
|
|
2392
|
+
"ui-monospace",
|
|
2393
|
+
"ui-rounded",
|
|
2394
|
+
"math",
|
|
2395
|
+
"emoji",
|
|
2396
|
+
"inherit",
|
|
2397
|
+
"initial",
|
|
2398
|
+
"revert",
|
|
2399
|
+
"unset",
|
|
2400
|
+
"currentcolor"
|
|
2401
|
+
]);
|
|
2402
|
+
var familyName = (raw) => raw.trim().replace(/^["']|["']$/g, "").trim();
|
|
2403
|
+
function fontFaceFamilies(css) {
|
|
2404
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2405
|
+
const blocks = /@font-face\s*\{([^}]{0,4000})\}/gi;
|
|
2406
|
+
for (; ; ) {
|
|
2407
|
+
const block = blocks.exec(css);
|
|
2408
|
+
if (block === null) break;
|
|
2409
|
+
const declared = /font-family\s*:\s*([^;]{1,200})/i.exec(block[1] ?? "");
|
|
2410
|
+
if (!declared) continue;
|
|
2411
|
+
const name = familyName(declared[1] ?? "");
|
|
2412
|
+
if (name !== "" && !GENERIC_FAMILIES.has(name.toLowerCase())) seen.add(name);
|
|
2413
|
+
}
|
|
2414
|
+
return [...seen];
|
|
2415
|
+
}
|
|
2416
|
+
function stackFamilies(css) {
|
|
2417
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2418
|
+
const stacks = /font-family\s*:\s*([^;{}]{1,400})/gi;
|
|
2419
|
+
for (; ; ) {
|
|
2420
|
+
const stack = stacks.exec(css);
|
|
2421
|
+
if (stack === null) break;
|
|
2422
|
+
for (const part of (stack[1] ?? "").split(",")) {
|
|
2423
|
+
if (!/["']/.test(part)) continue;
|
|
2424
|
+
const name = familyName(part);
|
|
2425
|
+
if (name === "" || GENERIC_FAMILIES.has(name.toLowerCase()) || name.includes("var(")) continue;
|
|
2426
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
return [...counts.entries()].sort((a2, b) => b[1] - a2[1] || (a2[0] < b[0] ? -1 : 1)).map(([name]) => name);
|
|
2430
|
+
}
|
|
2431
|
+
function extractFonts(templateHtml) {
|
|
2432
|
+
const css = stripCssComments(styleSheetText(templateHtml));
|
|
2433
|
+
const embedded = fontFaceFamilies(css);
|
|
2434
|
+
return (embedded.length > 0 ? embedded : stackFamilies(css)).slice(0, MAX_FONTS);
|
|
2435
|
+
}
|
|
2436
|
+
function extractColors(templateHtml) {
|
|
2437
|
+
const css = stripCssComments(styleSheetText(templateHtml));
|
|
2438
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2439
|
+
const hexes = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;
|
|
2440
|
+
for (; ; ) {
|
|
2441
|
+
const found = hexes.exec(css);
|
|
2442
|
+
if (found === null) break;
|
|
2443
|
+
const raw = (found[1] ?? "").toLowerCase();
|
|
2444
|
+
const hex = raw.length === 3 ? `#${raw[0]}${raw[0]}${raw[1]}${raw[1]}${raw[2]}${raw[2]}` : `#${raw}`;
|
|
2445
|
+
counts.set(hex, (counts.get(hex) ?? 0) + 1);
|
|
2446
|
+
}
|
|
2447
|
+
return [...counts.entries()].sort((a2, b) => b[1] - a2[1] || (a2[0] < b[0] ? -1 : 1)).slice(0, MAX_COLORS).map(([hex, count]) => ({ hex, count }));
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// src/design/tokens.ts
|
|
2451
|
+
var MAX_VAR_DEPTH = 8;
|
|
2452
|
+
var DOC_SELECTOR = /(^|,)\s*(:root|html)\b|\[data-theme\s*=|\.ui-invert\b/;
|
|
2453
|
+
var DARK_SELECTOR = /data-theme\s*=\s*["']?dark|prefers-color-scheme\s*:\s*dark|\.ui-invert\b/;
|
|
2454
|
+
function splitVarArgs(inner) {
|
|
2455
|
+
let paren = 0;
|
|
2456
|
+
for (let i = 0; i < inner.length; i++) {
|
|
2457
|
+
const ch = inner[i];
|
|
2458
|
+
if (ch === "(") paren++;
|
|
2459
|
+
else if (ch === ")") paren--;
|
|
2460
|
+
else if (ch === "," && paren === 0)
|
|
2461
|
+
return { name: inner.slice(0, i).trim(), fallback: inner.slice(i + 1).trim() };
|
|
2462
|
+
}
|
|
2463
|
+
return { name: inner.trim() };
|
|
2464
|
+
}
|
|
2465
|
+
function resolveVarRefs(value, table, depth = 0) {
|
|
2466
|
+
if (depth >= MAX_VAR_DEPTH || !value.includes("var(")) return value;
|
|
2467
|
+
let out = "";
|
|
2468
|
+
let i = 0;
|
|
2469
|
+
for (; ; ) {
|
|
2470
|
+
const at = value.indexOf("var(", i);
|
|
2471
|
+
if (at < 0) return out + value.slice(i);
|
|
2472
|
+
out += value.slice(i, at);
|
|
2473
|
+
let paren = 1;
|
|
2474
|
+
let j = at + "var(".length;
|
|
2475
|
+
for (; j < value.length && paren > 0; j++) {
|
|
2476
|
+
if (value[j] === "(") paren++;
|
|
2477
|
+
else if (value[j] === ")") paren--;
|
|
2478
|
+
}
|
|
2479
|
+
if (paren > 0) return out + value.slice(at);
|
|
2480
|
+
const { name, fallback } = splitVarArgs(value.slice(at + "var(".length, j - 1));
|
|
2481
|
+
const referenced = table.get(name);
|
|
2482
|
+
const replacement = referenced !== void 0 ? resolveVarRefs(referenced, table, depth + 1) : fallback !== void 0 ? resolveVarRefs(fallback, table, depth + 1) : `var(${name})`;
|
|
2483
|
+
out += replacement;
|
|
2484
|
+
i = j;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
function foldDeclarations(html) {
|
|
2488
|
+
const light = /* @__PURE__ */ new Map();
|
|
2489
|
+
const darkOverrides = [];
|
|
2490
|
+
for (const decl of scanCustomProperties(styleSheetText(html))) {
|
|
2491
|
+
const innermost = decl.selectors[decl.selectors.length - 1] ?? "";
|
|
2492
|
+
if (!DOC_SELECTOR.test(innermost)) continue;
|
|
2493
|
+
if (decl.selectors.some((sel) => DARK_SELECTOR.test(sel))) darkOverrides.push([decl.name, decl.value]);
|
|
2494
|
+
else light.set(decl.name, decl.value);
|
|
2495
|
+
}
|
|
2496
|
+
const dark = new Map(light);
|
|
2497
|
+
for (const [name, value] of darkOverrides) dark.set(name, value);
|
|
2498
|
+
return { light, dark };
|
|
2499
|
+
}
|
|
2500
|
+
function resolveUiTokens(table) {
|
|
2501
|
+
const out = {};
|
|
2502
|
+
for (const [name, value] of table) {
|
|
2503
|
+
if (!name.startsWith("--ui-")) continue;
|
|
2504
|
+
out[name] = resolveVarRefs(value, table);
|
|
2505
|
+
}
|
|
2506
|
+
return out;
|
|
2507
|
+
}
|
|
2508
|
+
function extractDesignTokens(templateHtml) {
|
|
2509
|
+
const { light, dark } = foldDeclarations(templateHtml);
|
|
2510
|
+
return { light: resolveUiTokens(light), dark: resolveUiTokens(dark) };
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// src/design/digest.ts
|
|
2514
|
+
var MAX_EXTERNALS = 24;
|
|
2515
|
+
function groupAssets(bundle) {
|
|
2516
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2517
|
+
for (const asset of bundle.assets) {
|
|
2518
|
+
const group = groups.get(asset.mime) ?? { mime: asset.mime, count: 0, bytes: 0 };
|
|
2519
|
+
group.count += 1;
|
|
2520
|
+
group.bytes += asset.bytes;
|
|
2521
|
+
groups.set(asset.mime, group);
|
|
2522
|
+
}
|
|
2523
|
+
return [...groups.values()].sort((a2, b) => b.bytes - a2.bytes || (a2.mime < b.mime ? -1 : 1));
|
|
2524
|
+
}
|
|
2525
|
+
function digestDesignBundle(bundle) {
|
|
2526
|
+
const template = bundle.template;
|
|
2527
|
+
const outline = extractOutline(template);
|
|
2528
|
+
const props = extractProps(template);
|
|
2529
|
+
const assetGroups = groupAssets(bundle);
|
|
2530
|
+
const title = extractTitle(template);
|
|
2531
|
+
const truncated = [];
|
|
2532
|
+
if (outline.truncated) truncated.push("outline");
|
|
2533
|
+
if (props.truncated) truncated.push("props");
|
|
2534
|
+
if (bundle.externals.length > MAX_EXTERNALS) truncated.push("externals");
|
|
2535
|
+
return {
|
|
2536
|
+
format: "claude-design-bundle/1",
|
|
2537
|
+
...title ? { title } : {},
|
|
2538
|
+
templateBytes: new TextEncoder().encode(template).byteLength,
|
|
2539
|
+
assetBytes: bundle.assets.reduce((total, asset) => total + asset.bytes, 0),
|
|
2540
|
+
assetCount: bundle.assets.length,
|
|
2541
|
+
assetGroups,
|
|
2542
|
+
externals: bundle.externals.slice(0, MAX_EXTERNALS),
|
|
2543
|
+
pageCount: bundle.pageOrder.length,
|
|
2544
|
+
tokens: extractDesignTokens(template),
|
|
2545
|
+
fonts: extractFonts(template),
|
|
2546
|
+
colors: extractColors(template),
|
|
2547
|
+
props: props.props,
|
|
2548
|
+
outline: outline.entries,
|
|
2549
|
+
...bundle.thumbnailSvg ? { thumbnailSvg: bundle.thumbnailSvg } : {},
|
|
2550
|
+
truncated
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
function digestDesignHtml(html) {
|
|
2554
|
+
return digestDesignBundle(parseDesignBundle(html));
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
// src/palette-report.ts
|
|
2558
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
2559
|
+
function contrastReport(swatches) {
|
|
2560
|
+
const byRole = (role) => swatches.find((s) => s.role === role)?.hex;
|
|
2561
|
+
const bg = byRole("bg") ?? "#ffffff";
|
|
2562
|
+
const report = {};
|
|
2563
|
+
const put = (label, fg) => {
|
|
2564
|
+
if (fg) report[label] = round2(contrastRatio(fg, bg));
|
|
2565
|
+
};
|
|
2566
|
+
put("text-on-bg", byRole("text"));
|
|
2567
|
+
put("primary-on-bg", byRole("primary"));
|
|
2568
|
+
put("good-on-bg", byRole("good"));
|
|
2569
|
+
put("warn-on-bg", byRole("warn"));
|
|
2570
|
+
put("danger-on-bg", byRole("danger"));
|
|
2571
|
+
const primary = byRole("primary");
|
|
2572
|
+
if (primary) report["text-on-primary"] = round2(contrastRatio(pickTextOn(primary), primary));
|
|
2573
|
+
return report;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
// src/tokens/roles.ts
|
|
2577
|
+
var BRAND_REQUIRED_TOKENS = [
|
|
2578
|
+
"--ui-bg",
|
|
2579
|
+
"--ui-surface",
|
|
2580
|
+
"--ui-surface-2",
|
|
2581
|
+
"--ui-text",
|
|
2582
|
+
"--ui-text-muted",
|
|
2583
|
+
"--ui-text-faint",
|
|
2584
|
+
"--ui-border",
|
|
2585
|
+
"--ui-border-strong",
|
|
2586
|
+
"--ui-accent",
|
|
2587
|
+
"--ui-accent-strong",
|
|
2588
|
+
"--ui-accent-soft",
|
|
2589
|
+
"--ui-on-accent",
|
|
2590
|
+
"--ui-good",
|
|
2591
|
+
"--ui-good-soft",
|
|
2592
|
+
"--ui-warn",
|
|
2593
|
+
"--ui-warn-soft",
|
|
2594
|
+
"--ui-danger",
|
|
2595
|
+
"--ui-danger-soft",
|
|
2596
|
+
"--ui-code-bg",
|
|
2597
|
+
"--ui-code-text",
|
|
2598
|
+
"--ui-shadow",
|
|
2599
|
+
"--ui-font-sans",
|
|
2600
|
+
"--ui-font-serif",
|
|
2601
|
+
"--ui-font-mono",
|
|
2602
|
+
"--ui-font-display"
|
|
2603
|
+
];
|
|
2604
|
+
var BRAND_DERIVED_TOKENS = [
|
|
2605
|
+
"--ui-accent-glow",
|
|
2606
|
+
"--ui-accent-2",
|
|
2607
|
+
"--ui-accent-2-soft",
|
|
2608
|
+
"--ui-highlight",
|
|
2609
|
+
"--ui-focus",
|
|
2610
|
+
"--ui-shadow-strong"
|
|
2611
|
+
];
|
|
2612
|
+
var BRAND_CHART_TOKENS = [
|
|
2613
|
+
"--ui-chart-1",
|
|
2614
|
+
"--ui-chart-2",
|
|
2615
|
+
"--ui-chart-3",
|
|
2616
|
+
"--ui-chart-4",
|
|
2617
|
+
"--ui-chart-5",
|
|
2618
|
+
"--ui-chart-6",
|
|
2619
|
+
"--ui-chart-band",
|
|
2620
|
+
"--ui-chart-band-strong",
|
|
2621
|
+
"--ui-chart-flow",
|
|
2622
|
+
"--ui-chart-glow"
|
|
2623
|
+
];
|
|
2624
|
+
var BRAND_CHAT_TOKENS = [
|
|
2625
|
+
"--ui-chat-user-bg",
|
|
2626
|
+
"--ui-chat-user-text",
|
|
2627
|
+
"--ui-chat-assistant-bg",
|
|
2628
|
+
"--ui-chat-thinking-bg",
|
|
2629
|
+
"--ui-chat-thinking-text",
|
|
2630
|
+
"--ui-chat-tool-accent"
|
|
2631
|
+
];
|
|
2632
|
+
var BRAND_EMITTED_TOKENS = [
|
|
2633
|
+
...BRAND_REQUIRED_TOKENS,
|
|
2634
|
+
...BRAND_DERIVED_TOKENS,
|
|
2635
|
+
...BRAND_CHART_TOKENS,
|
|
2636
|
+
...BRAND_CHAT_TOKENS
|
|
2637
|
+
];
|
|
2638
|
+
|
|
2028
2639
|
// src/tokens/dark.ts
|
|
2029
2640
|
var DARK_FLIP_L_MIN = 0.2;
|
|
2030
2641
|
var DARK_FLIP_L_MAX = 0.95;
|
|
@@ -2182,10 +2793,10 @@ ${renderBlock(mediaSelector, dark, diffNames, " ")}
|
|
|
2182
2793
|
}
|
|
2183
2794
|
|
|
2184
2795
|
// src/tokens/compile.ts
|
|
2185
|
-
var
|
|
2796
|
+
var isRecord4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2186
2797
|
function applyOverrides(light, overrides, warnings) {
|
|
2187
2798
|
if (overrides === void 0) return;
|
|
2188
|
-
if (!
|
|
2799
|
+
if (!isRecord4(overrides)) {
|
|
2189
2800
|
warnings.push({ token: "--ui-accent", message: "overrides ignored: not an object" });
|
|
2190
2801
|
return;
|
|
2191
2802
|
}
|
|
@@ -2419,22 +3030,6 @@ function applyHarmony(swatches, seedHex, harmony) {
|
|
|
2419
3030
|
return { ...s, hex, name: nearestNamedColor(hex).name, rationale: `${harmony} companion of the seed` };
|
|
2420
3031
|
});
|
|
2421
3032
|
}
|
|
2422
|
-
function contrastReport(swatches) {
|
|
2423
|
-
const byRole = (role) => swatches.find((s) => s.role === role)?.hex;
|
|
2424
|
-
const bg = byRole("bg") ?? "#ffffff";
|
|
2425
|
-
const report = {};
|
|
2426
|
-
const put = (label, fg) => {
|
|
2427
|
-
if (fg) report[label] = Math.round(contrastRatio(fg, bg) * 100) / 100;
|
|
2428
|
-
};
|
|
2429
|
-
put("text-on-bg", byRole("text"));
|
|
2430
|
-
put("primary-on-bg", byRole("primary"));
|
|
2431
|
-
put("good-on-bg", byRole("good"));
|
|
2432
|
-
put("warn-on-bg", byRole("warn"));
|
|
2433
|
-
put("danger-on-bg", byRole("danger"));
|
|
2434
|
-
const primary = byRole("primary");
|
|
2435
|
-
if (primary) report["text-on-primary"] = Math.round(contrastRatio(pickTextOn(primary), primary) * 100) / 100;
|
|
2436
|
-
return report;
|
|
2437
|
-
}
|
|
2438
3033
|
var INCLUDE_SECTIONS = ["harmony", "ramp"];
|
|
2439
3034
|
function analyzeLines(hex, include) {
|
|
2440
3035
|
const lch = hexToOklch(hex);
|
|
@@ -2591,6 +3186,160 @@ function paletteTools(ctx) {
|
|
|
2591
3186
|
return [analyzeColor, evaluateContrast, proposePalette];
|
|
2592
3187
|
}
|
|
2593
3188
|
|
|
3189
|
+
// src/skill/design-tools.ts
|
|
3190
|
+
var MAX_SOURCE_WINDOW = 12e3;
|
|
3191
|
+
function describeDigest(asset, digest2) {
|
|
3192
|
+
const lines = [
|
|
3193
|
+
`Design ${asset.id}${digest2.title ? ` \u2014 "${digest2.title}"` : ""}${asset.title ? ` (uploaded as "${asset.title}")` : ""}`,
|
|
3194
|
+
`Template ${digest2.templateBytes} bytes; ${digest2.assetCount} embedded assets (${digest2.assetBytes} bytes); ${digest2.pageCount} nested pages.`
|
|
3195
|
+
];
|
|
3196
|
+
if (digest2.assetGroups.length > 0)
|
|
3197
|
+
lines.push(
|
|
3198
|
+
`Assets: ${digest2.assetGroups.map((g) => `${g.count}\xD7 ${g.mime} (${g.bytes} B)`).join(", ")}`
|
|
3199
|
+
);
|
|
3200
|
+
if (digest2.externals.length > 0) lines.push(`Built against: ${digest2.externals.join(", ")}`);
|
|
3201
|
+
if (digest2.fonts.length > 0) lines.push(`Typefaces: ${digest2.fonts.join(", ")}`);
|
|
3202
|
+
const tokenNames = Object.keys(digest2.tokens.light);
|
|
3203
|
+
lines.push(
|
|
3204
|
+
tokenNames.length > 0 ? `Declares ${tokenNames.length} --ui-* tokens (light) and ${Object.keys(digest2.tokens.dark).length} (dark). Key values: ${["--ui-bg", "--ui-text", "--ui-accent", "--ui-accent-strong", "--ui-surface"].filter((n) => digest2.tokens.light[n]).map((n) => `${n}=${digest2.tokens.light[n]}`).join(", ")}` : "Declares no --ui-* tokens; it was not authored on the @odla-ai/ui contract."
|
|
3205
|
+
);
|
|
3206
|
+
if (digest2.colors.length > 0)
|
|
3207
|
+
lines.push(`Literal colors: ${digest2.colors.map((c) => `${c.hex}\xD7${c.count}`).join(", ")}`);
|
|
3208
|
+
if (digest2.props.length > 0)
|
|
3209
|
+
lines.push(
|
|
3210
|
+
"Configurable props:\n" + digest2.props.map(
|
|
3211
|
+
(p) => ` ${p.name} (${p.editor}${p.options ? `: ${p.options.join("|")}` : ""})${p.default === void 0 ? "" : ` default ${p.default}`}${p.section ? ` [${p.section}]` : ""}`
|
|
3212
|
+
).join("\n")
|
|
3213
|
+
);
|
|
3214
|
+
if (digest2.outline.length > 0)
|
|
3215
|
+
lines.push(
|
|
3216
|
+
"Outline:\n" + digest2.outline.map((h) => `${" ".repeat(h.level - 1)}h${h.level} ${h.text}`).join("\n")
|
|
3217
|
+
);
|
|
3218
|
+
if (digest2.truncated.length > 0)
|
|
3219
|
+
lines.push(`NOTE: truncated to fit digest caps: ${digest2.truncated.join(", ")}.`);
|
|
3220
|
+
return lines.join("\n");
|
|
3221
|
+
}
|
|
3222
|
+
function sourceWindow(template, input) {
|
|
3223
|
+
const length = Math.min(
|
|
3224
|
+
MAX_SOURCE_WINDOW,
|
|
3225
|
+
Math.max(1, typeof input.length === "number" ? input.length : MAX_SOURCE_WINDOW)
|
|
3226
|
+
);
|
|
3227
|
+
let start;
|
|
3228
|
+
if (input.find !== void 0 && input.find !== "") {
|
|
3229
|
+
const at = template.indexOf(input.find);
|
|
3230
|
+
if (at < 0) throw new BrandNotFoundError(`"${input.find}" in the design source`);
|
|
3231
|
+
start = Math.max(0, at - 400);
|
|
3232
|
+
} else {
|
|
3233
|
+
start = Math.max(0, Math.min(template.length, Math.trunc(input.offset ?? 0)));
|
|
3234
|
+
}
|
|
3235
|
+
const end = Math.min(template.length, start + length);
|
|
3236
|
+
return { text: template.slice(start, end), start, end };
|
|
3237
|
+
}
|
|
3238
|
+
function designTools(ctx) {
|
|
3239
|
+
const loadDesign = async (assetId) => {
|
|
3240
|
+
await ctx.authority("brand.read");
|
|
3241
|
+
const res = await ctx.db.query({
|
|
3242
|
+
[BRAND_NS.asset]: { $: { where: { id: assetId, bookId: ctx.bookId, status: "live" } } }
|
|
3243
|
+
});
|
|
3244
|
+
const row = (res[BRAND_NS.asset] ?? [])[0];
|
|
3245
|
+
if (!row || row.kind !== DESIGN_ASSET_KIND || !row.design)
|
|
3246
|
+
throw new BrandNotFoundError(`design asset ${assetId}`);
|
|
3247
|
+
return row;
|
|
3248
|
+
};
|
|
3249
|
+
const readDesign = {
|
|
3250
|
+
name: "read_design",
|
|
3251
|
+
description: "Read an uploaded Claude Design: its design tokens, typefaces, colors, configurable props, and heading outline. Start here before building anything from a design \u2014 it is the whole design at a size you can reason about.",
|
|
3252
|
+
inputSchema: {
|
|
3253
|
+
type: "object",
|
|
3254
|
+
required: ["assetId"],
|
|
3255
|
+
properties: {
|
|
3256
|
+
assetId: { type: "string", description: "A design asset id from list_assets." }
|
|
3257
|
+
}
|
|
3258
|
+
},
|
|
3259
|
+
handler: ctx.guard(async (input) => {
|
|
3260
|
+
const asset = await loadDesign(capString(input.assetId, "assetId", 200));
|
|
3261
|
+
return { content: describeDigest(asset, asset.design) };
|
|
3262
|
+
})
|
|
3263
|
+
};
|
|
3264
|
+
const readDesignSource = {
|
|
3265
|
+
name: "read_design_source",
|
|
3266
|
+
description: "Read a window of a design's actual HTML source, to port exact markup or styles. Pass `find` to jump to the first occurrence of a string (a heading, a class name), or `offset` to page through. Returns at most 12000 characters.",
|
|
3267
|
+
// The template is author-supplied content, not instructions.
|
|
3268
|
+
outputTaint: ["tool_untrusted:read_design_source"],
|
|
3269
|
+
inputSchema: {
|
|
3270
|
+
type: "object",
|
|
3271
|
+
required: ["assetId"],
|
|
3272
|
+
properties: {
|
|
3273
|
+
assetId: { type: "string" },
|
|
3274
|
+
find: { type: "string", description: "Jump to the first occurrence of this string." },
|
|
3275
|
+
offset: { type: "number", description: "Character offset to read from (ignored with find)." },
|
|
3276
|
+
length: { type: "number", description: `Characters to return (max ${MAX_SOURCE_WINDOW}).` }
|
|
3277
|
+
}
|
|
3278
|
+
},
|
|
3279
|
+
handler: ctx.guard(async (input) => {
|
|
3280
|
+
const asset = await loadDesign(capString(input.assetId, "assetId", 200));
|
|
3281
|
+
const template = await ctx.readDesignTemplate(asset.id);
|
|
3282
|
+
const found = sourceWindow(template, {
|
|
3283
|
+
...typeof input.find === "string" ? { find: input.find } : {},
|
|
3284
|
+
...typeof input.offset === "number" ? { offset: input.offset } : {},
|
|
3285
|
+
...typeof input.length === "number" ? { length: input.length } : {}
|
|
3286
|
+
});
|
|
3287
|
+
return {
|
|
3288
|
+
content: `Design ${asset.id} source, characters ${found.start}\u2013${found.end} of ${template.length}:
|
|
3289
|
+
` + found.text
|
|
3290
|
+
};
|
|
3291
|
+
})
|
|
3292
|
+
};
|
|
3293
|
+
const proposeFromDesign = {
|
|
3294
|
+
name: "propose_palette_from_design",
|
|
3295
|
+
description: "Read a design's own --ui-* token declarations back into a brand palette and park it as a proposal for human review. Use when a design already carries the brand's colors and they should become the brand book's palette. Does NOT change the brand.",
|
|
3296
|
+
acceptsTaint: ["tool_untrusted:read_design_source"],
|
|
3297
|
+
inputSchema: {
|
|
3298
|
+
type: "object",
|
|
3299
|
+
required: ["assetId", "rationale"],
|
|
3300
|
+
properties: {
|
|
3301
|
+
assetId: { type: "string" },
|
|
3302
|
+
name: { type: "string", description: "Palette name; defaults to the design's title." },
|
|
3303
|
+
rationale: { type: "string", description: "Why this design's palette should become the brand's." }
|
|
3304
|
+
}
|
|
3305
|
+
},
|
|
3306
|
+
handler: ctx.guard(async (input) => {
|
|
3307
|
+
const book = await ctx.loadBook();
|
|
3308
|
+
await ctx.authority("brand.edit");
|
|
3309
|
+
const asset = await loadDesign(capString(input.assetId, "assetId", 200));
|
|
3310
|
+
const digest2 = asset.design;
|
|
3311
|
+
const rationale = capString(input.rationale, "rationale", 2e3);
|
|
3312
|
+
const name = capString(
|
|
3313
|
+
input.name ?? digest2.title ?? asset.title ?? "Design palette",
|
|
3314
|
+
"name",
|
|
3315
|
+
120
|
|
3316
|
+
);
|
|
3317
|
+
const { swatches, skipped, missing: missing2 } = swatchesFromDesignTokens(digest2.tokens);
|
|
3318
|
+
if (swatches.length === 0)
|
|
3319
|
+
throw new BrandInputError(
|
|
3320
|
+
`design ${asset.id} declares no --ui-* tokens that resolve to opaque colors; propose a palette explicitly instead.`
|
|
3321
|
+
);
|
|
3322
|
+
const report = contrastReport(swatches);
|
|
3323
|
+
const proposal = await ctx.createProposal({
|
|
3324
|
+
mutationId: ctx.newId(),
|
|
3325
|
+
kind: "palette",
|
|
3326
|
+
payload: { name, swatches, contrastReport: report, source: "design", designAssetId: asset.id },
|
|
3327
|
+
rationale,
|
|
3328
|
+
sourceAssetId: asset.id
|
|
3329
|
+
});
|
|
3330
|
+
if (proposal.bookId !== book.id || proposal.createdBy !== ctx.self.selfId || proposal.kind !== "palette" || proposal.status !== "open") throw new BrandInputError("brand proposal bridge returned an invalid proposal");
|
|
3331
|
+
const notes = [
|
|
3332
|
+
skipped.length > 0 ? `Not imported (not literal opaque colors): ${skipped.map((s) => `${s.token} \u2014 ${s.reason}`).join("; ")}.` : "",
|
|
3333
|
+
missing2.length > 0 ? `Roles the design declares no token for: ${missing2.join(", ")}.` : ""
|
|
3334
|
+
].filter((n) => n !== "");
|
|
3335
|
+
return {
|
|
3336
|
+
content: `Parked palette proposal ${proposal.id} ("${name}") from design ${asset.id}: ${swatches.length} swatches read back from its --ui-* declarations. Contrast: ${Object.entries(report).map(([k, v]) => `${k} ${v.toFixed(2)}:1`).join(", ")}. ` + `${notes.join(" ")} Awaiting a human decision in the brand approval surface.`.trim()
|
|
3337
|
+
};
|
|
3338
|
+
})
|
|
3339
|
+
};
|
|
3340
|
+
return [readDesign, readDesignSource, proposeFromDesign];
|
|
3341
|
+
}
|
|
3342
|
+
|
|
2594
3343
|
// src/skill/read-tools.ts
|
|
2595
3344
|
var iso = (ms) => new Date(ms).toISOString();
|
|
2596
3345
|
var shortId = (id) => id.length <= 12 ? id : `${id.slice(0, 6)}\u2026${id.slice(-4)}`;
|
|
@@ -2706,10 +3455,11 @@ function readTools(ctx) {
|
|
|
2706
3455
|
}
|
|
2707
3456
|
|
|
2708
3457
|
// src/skill/skill.ts
|
|
2709
|
-
var BRAND_INSTRUCTIONS = "You help build and maintain ONE brand book. Workflow, in order:\n1. Understand the brand first: read_brand_book and list_assets before proposing anything.\n2. Look at the real material: view_asset on logos and inspiration, then record what you saw with record_asset_analysis (description, dominant colors as hex, tags).\n3. Explore with the math tools: analyze_color and evaluate_contrast. Never invent a hex without a rationale \u2014 ground every color in an asset's dominant color, a harmony companion, or a contrast fix, and say which.\n4. propose_palette parks a proposal for review. It does NOT change the brand.\
|
|
3458
|
+
var BRAND_INSTRUCTIONS = "You help build and maintain ONE brand book. Workflow, in order:\n1. Understand the brand first: read_brand_book and list_assets before proposing anything.\n2. Look at the real material: view_asset on logos and inspiration, then record what you saw with record_asset_analysis (description, dominant colors as hex, tags).\n3. Explore with the math tools: analyze_color and evaluate_contrast. Never invent a hex without a rationale \u2014 ground every color in an asset's dominant color, a harmony companion, or a contrast fix, and say which.\n4. When a Claude Design has been uploaded, read_design is the fastest way to learn the brand: it reports the design tokens, typefaces, props, and page outline. read_design_source reads exact markup when you need it. If the design already carries the brand colors, propose_palette_from_design reads its --ui-* declarations back into a palette proposal.\n5. propose_palette parks a proposal for review. It does NOT change the brand.\n6. You cannot approve or resolve a proposal. Direct the human to the brand approval surface, which records a guarded receipt for their decision.\n7. update_section parks a proposal; it never overwrites an approved facet. Bind sourceAssetId when a real Brand asset grounds typography, voice, logo, or imagery. Human acceptance applies the exact reviewed change and automatically recompiles dependent tokens.\n8. Re-read the book after a decision and explain any compiler warnings conversationally.";
|
|
2710
3459
|
function brandSkill(opts) {
|
|
2711
3460
|
if (opts.agentDbBinding.principalId !== opts.self.selfId || !opts.agentDbBinding.credentialRef) throw new BrandForbiddenError("brand db is not bound to the acting agent");
|
|
2712
3461
|
const deps = resolveDeps({ db: opts.db, now: opts.now, newId: opts.newId });
|
|
3462
|
+
const designTemplates = /* @__PURE__ */ new Map();
|
|
2713
3463
|
const authority = (capability) => Promise.resolve(opts.authorizeCapability({
|
|
2714
3464
|
agentId: opts.self.selfId,
|
|
2715
3465
|
bookId: opts.bookId,
|
|
@@ -2742,6 +3492,16 @@ function brandSkill(opts) {
|
|
|
2742
3492
|
bookId: opts.bookId,
|
|
2743
3493
|
assetId
|
|
2744
3494
|
}),
|
|
3495
|
+
readDesignTemplate: (assetId) => {
|
|
3496
|
+
const cached = designTemplates.get(assetId);
|
|
3497
|
+
if (cached) return cached;
|
|
3498
|
+
const pending = opts.agentBridge.readAssetContent({ jobId: opts.agentJobId, bookId: opts.bookId, assetId }).then(
|
|
3499
|
+
(fetched) => parseDesignBundle(new TextDecoder().decode(fetched.bytes)).template
|
|
3500
|
+
);
|
|
3501
|
+
pending.catch(() => designTemplates.delete(assetId));
|
|
3502
|
+
designTemplates.set(assetId, pending);
|
|
3503
|
+
return pending;
|
|
3504
|
+
},
|
|
2745
3505
|
resolvePrincipals: (principalIds) => {
|
|
2746
3506
|
const ids = [...new Set(principalIds)].slice(0, 100);
|
|
2747
3507
|
return opts.resolvePrincipals({
|
|
@@ -2773,7 +3533,13 @@ function brandSkill(opts) {
|
|
|
2773
3533
|
return {
|
|
2774
3534
|
name: "brand",
|
|
2775
3535
|
instructions: BRAND_INSTRUCTIONS,
|
|
2776
|
-
tools: [
|
|
3536
|
+
tools: [
|
|
3537
|
+
...readTools(ctx),
|
|
3538
|
+
...assetTools(ctx),
|
|
3539
|
+
...designTools(ctx),
|
|
3540
|
+
...paletteTools(ctx),
|
|
3541
|
+
...bookTools(ctx)
|
|
3542
|
+
]
|
|
2777
3543
|
};
|
|
2778
3544
|
}
|
|
2779
3545
|
|
|
@@ -2904,9 +3670,13 @@ async function uploadAsset(ctx, req, bookId) {
|
|
|
2904
3670
|
const file = form.get("file");
|
|
2905
3671
|
if (!(file instanceof File))
|
|
2906
3672
|
throw new BrandInputError('"file" must be an uploaded file field');
|
|
3673
|
+
const kindRaw = form.get("kind");
|
|
3674
|
+
const kind = typeof kindRaw === "string" && kindRaw ? kindRaw : "other";
|
|
3675
|
+
if (!ASSET_KINDS.includes(kind))
|
|
3676
|
+
throw new BrandInputError(`kind must be one of: ${ASSET_KINDS.join(", ")}`);
|
|
2907
3677
|
let contentType;
|
|
2908
3678
|
try {
|
|
2909
|
-
contentType = assertAssetContentType(file.type);
|
|
3679
|
+
contentType = assertAssetContentType(file.type, kind);
|
|
2910
3680
|
} catch (error) {
|
|
2911
3681
|
if (error instanceof BrandInputError)
|
|
2912
3682
|
return json({ error: error.message }, 415);
|
|
@@ -2914,10 +3684,7 @@ async function uploadAsset(ctx, req, bookId) {
|
|
|
2914
3684
|
}
|
|
2915
3685
|
if (file.size > ctx.maxUploadBytes)
|
|
2916
3686
|
return json({ error: `file exceeds ${ctx.maxUploadBytes} bytes` }, 413);
|
|
2917
|
-
const
|
|
2918
|
-
const kind = typeof kindRaw === "string" && kindRaw ? kindRaw : "other";
|
|
2919
|
-
if (!ASSET_KINDS.includes(kind))
|
|
2920
|
-
throw new BrandInputError(`kind must be one of: ${ASSET_KINDS.join(", ")}`);
|
|
3687
|
+
const design = kind === DESIGN_ASSET_KIND ? digestDesignHtml(await file.text()) : void 0;
|
|
2921
3688
|
const titleRaw = form.get("title");
|
|
2922
3689
|
const title = typeof titleRaw === "string" && titleRaw ? capString(titleRaw, "title", 160) : void 0;
|
|
2923
3690
|
const fileName = safeFileName(file.name);
|
|
@@ -2965,6 +3732,7 @@ async function uploadAsset(ctx, req, bookId) {
|
|
|
2965
3732
|
uploadedAuthorityRef: authority.authorityRef,
|
|
2966
3733
|
audience: book.memberIds,
|
|
2967
3734
|
title,
|
|
3735
|
+
...design ? { design } : {},
|
|
2968
3736
|
now: ctx.now()
|
|
2969
3737
|
}), {
|
|
2970
3738
|
mutationId: `brand:asset-upload:v1:${id}`,
|
|
@@ -3206,6 +3974,51 @@ async function handleBookItem(ctx, req, bookId) {
|
|
|
3206
3974
|
return json(await loadMemberBook(ctx.db, bookId, ctx.actor.id));
|
|
3207
3975
|
}
|
|
3208
3976
|
|
|
3977
|
+
// src/routes/design-preview.ts
|
|
3978
|
+
var SIGNED_READ_TTL_SECONDS = 60;
|
|
3979
|
+
function designPreviewHeaders(frameAncestors) {
|
|
3980
|
+
return {
|
|
3981
|
+
"content-type": "text/html; charset=utf-8",
|
|
3982
|
+
"content-security-policy": [
|
|
3983
|
+
"sandbox allow-scripts",
|
|
3984
|
+
`frame-ancestors ${frameAncestors.join(" ")}`
|
|
3985
|
+
].join("; "),
|
|
3986
|
+
"x-content-type-options": "nosniff",
|
|
3987
|
+
"referrer-policy": "no-referrer",
|
|
3988
|
+
"cache-control": "private, no-store"
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
async function handleDesignPreview(ctx, req, bookId, assetId) {
|
|
3992
|
+
if (req.method !== "GET") return methodNotAllowed();
|
|
3993
|
+
const book = await loadMemberBook(ctx.db, bookId, ctx.actor.id);
|
|
3994
|
+
const asset = await linkedAsset(ctx, book, assetId);
|
|
3995
|
+
if (asset.kind !== DESIGN_ASSET_KIND) throw new BrandNotFoundError(`design ${assetId}`);
|
|
3996
|
+
const signed = await ctx.db.storage.sign(asset.path, SIGNED_READ_TTL_SECONDS);
|
|
3997
|
+
const fetched = await ctx.fetchPrivateAsset(signed, {
|
|
3998
|
+
headers: { accept: "text/html" },
|
|
3999
|
+
redirect: "manual",
|
|
4000
|
+
signal: req.signal
|
|
4001
|
+
});
|
|
4002
|
+
if (!fetched.ok || fetched.type === "opaqueredirect")
|
|
4003
|
+
throw new BrandNotFoundError(`design ${assetId}`);
|
|
4004
|
+
return new Response(new Uint8Array(await fetched.arrayBuffer()), {
|
|
4005
|
+
status: 200,
|
|
4006
|
+
headers: designPreviewHeaders(ctx.previewFrameAncestors)
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
async function handleDesignDigest(ctx, req, bookId, assetId) {
|
|
4010
|
+
if (req.method !== "GET") return methodNotAllowed();
|
|
4011
|
+
const book = await loadMemberBook(ctx.db, bookId, ctx.actor.id);
|
|
4012
|
+
const asset = await linkedAsset(ctx, book, assetId);
|
|
4013
|
+
if (asset.kind !== DESIGN_ASSET_KIND || !asset.design)
|
|
4014
|
+
throw new BrandNotFoundError(`design ${assetId}`);
|
|
4015
|
+
return json(
|
|
4016
|
+
{ assetId: asset.id, title: asset.title, digest: asset.design },
|
|
4017
|
+
200,
|
|
4018
|
+
{ "cache-control": "private, no-store" }
|
|
4019
|
+
);
|
|
4020
|
+
}
|
|
4021
|
+
|
|
3209
4022
|
// src/routes/proposal-input.ts
|
|
3210
4023
|
var SNAPSHOT_KEYS = [
|
|
3211
4024
|
"audience",
|
|
@@ -3426,13 +4239,13 @@ async function proposalDecisionOps(input) {
|
|
|
3426
4239
|
|
|
3427
4240
|
// src/routes/proposal-resolution-receipts.ts
|
|
3428
4241
|
async function proposalResolutionMutationKey(bookId, proposalId, mutationId) {
|
|
3429
|
-
const
|
|
4242
|
+
const digest2 = await brandJsonDigest({
|
|
3430
4243
|
version: 3,
|
|
3431
4244
|
bookId,
|
|
3432
4245
|
proposalId,
|
|
3433
4246
|
mutationId
|
|
3434
4247
|
});
|
|
3435
|
-
return `brand:proposal-resolution:v3:${
|
|
4248
|
+
return `brand:proposal-resolution:v3:${digest2.slice("sha256:".length)}`;
|
|
3436
4249
|
}
|
|
3437
4250
|
async function proposalReceiptByMutation(ctx, mutationKey) {
|
|
3438
4251
|
const result = await ctx.db.query({
|
|
@@ -3982,6 +4795,130 @@ async function handleTokens(db, req, bookId, which, actor) {
|
|
|
3982
4795
|
});
|
|
3983
4796
|
}
|
|
3984
4797
|
|
|
4798
|
+
// src/routes/discussion-asset.ts
|
|
4799
|
+
var MAX_DISCUSSION_ASSET_BYTES = 4718592;
|
|
4800
|
+
var SIGN_TTL_SECONDS = 30;
|
|
4801
|
+
var TYPES = /* @__PURE__ */ new Set([
|
|
4802
|
+
"image/png",
|
|
4803
|
+
"image/jpeg",
|
|
4804
|
+
"image/gif",
|
|
4805
|
+
"image/webp",
|
|
4806
|
+
"application/pdf"
|
|
4807
|
+
]);
|
|
4808
|
+
var bare = (value) => (value?.split(";", 1)[0] ?? "").trim().toLowerCase();
|
|
4809
|
+
var starts = (bytes, expected) => expected.every((value, index) => bytes[index] === value);
|
|
4810
|
+
function magicMatches(contentType, bytes) {
|
|
4811
|
+
if (contentType === "image/png") {
|
|
4812
|
+
return starts(bytes, [137, 80, 78, 71, 13, 10, 26, 10]);
|
|
4813
|
+
}
|
|
4814
|
+
if (contentType === "image/jpeg") {
|
|
4815
|
+
return starts(bytes, [255, 216, 255]);
|
|
4816
|
+
}
|
|
4817
|
+
const prefix = new TextDecoder().decode(bytes.subarray(0, 16));
|
|
4818
|
+
if (contentType === "image/gif") {
|
|
4819
|
+
return prefix.startsWith("GIF87a") || prefix.startsWith("GIF89a");
|
|
4820
|
+
}
|
|
4821
|
+
if (contentType === "image/webp") {
|
|
4822
|
+
return prefix.startsWith("RIFF") && prefix.slice(8, 12) === "WEBP";
|
|
4823
|
+
}
|
|
4824
|
+
return contentType === "application/pdf" && prefix.startsWith("%PDF-");
|
|
4825
|
+
}
|
|
4826
|
+
async function boundedBytes(response, max) {
|
|
4827
|
+
const length = Number(response.headers.get("content-length"));
|
|
4828
|
+
if (Number.isFinite(length) && length > max) return null;
|
|
4829
|
+
if (!response.body) return new Uint8Array();
|
|
4830
|
+
const reader = response.body.getReader();
|
|
4831
|
+
const chunks = [];
|
|
4832
|
+
let total = 0;
|
|
4833
|
+
while (true) {
|
|
4834
|
+
const next = await reader.read();
|
|
4835
|
+
if (next.done) break;
|
|
4836
|
+
total += next.value.byteLength;
|
|
4837
|
+
if (total > max) {
|
|
4838
|
+
await reader.cancel();
|
|
4839
|
+
return null;
|
|
4840
|
+
}
|
|
4841
|
+
chunks.push(next.value);
|
|
4842
|
+
}
|
|
4843
|
+
const out = new Uint8Array(total);
|
|
4844
|
+
let offset = 0;
|
|
4845
|
+
for (const chunk of chunks) {
|
|
4846
|
+
out.set(chunk, offset);
|
|
4847
|
+
offset += chunk.byteLength;
|
|
4848
|
+
}
|
|
4849
|
+
return out;
|
|
4850
|
+
}
|
|
4851
|
+
async function digest(bytes) {
|
|
4852
|
+
const value = await crypto.subtle.digest("SHA-256", bytes.slice().buffer);
|
|
4853
|
+
return `sha256:${[...new Uint8Array(value)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
4854
|
+
}
|
|
4855
|
+
function exactAsset(beforeBook, before, afterBook, after) {
|
|
4856
|
+
return afterBook.id === beforeBook.id && after.id === before.id && after.bookId === before.bookId && after.status === "live" && after.path === before.path && after.storageObjectId === before.storageObjectId && after.contentDigest === before.contentDigest && after.contentType === before.contentType && after.size === before.size;
|
|
4857
|
+
}
|
|
4858
|
+
function safeSignedUrl(value) {
|
|
4859
|
+
try {
|
|
4860
|
+
const url = new URL(value);
|
|
4861
|
+
return value.length <= 4096 && url.protocol === "https:" && !url.username && !url.password ? url : null;
|
|
4862
|
+
} catch {
|
|
4863
|
+
return null;
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
async function handleBrandDiscussionAsset(ctx, req, target) {
|
|
4867
|
+
const beforeBook = await loadMemberBook(ctx.db, target.bookId, ctx.actor.id);
|
|
4868
|
+
const before = await linkedAsset(ctx, beforeBook, target.resourceId);
|
|
4869
|
+
const contentType = bare(before.contentType);
|
|
4870
|
+
if (!TYPES.has(contentType)) {
|
|
4871
|
+
return json({ error: "asset content type is not viewable" }, 415);
|
|
4872
|
+
}
|
|
4873
|
+
if (before.size < 1 || before.size > MAX_DISCUSSION_ASSET_BYTES) {
|
|
4874
|
+
return json({ error: "asset exceeds the discussion media limit" }, 413);
|
|
4875
|
+
}
|
|
4876
|
+
const signed = safeSignedUrl(
|
|
4877
|
+
await ctx.db.storage.sign(before.path, SIGN_TTL_SECONDS)
|
|
4878
|
+
);
|
|
4879
|
+
if (!signed) throw new BrandInputError("private asset signing failed");
|
|
4880
|
+
const response = await ctx.fetchPrivateAsset(signed, {
|
|
4881
|
+
headers: { accept: contentType },
|
|
4882
|
+
redirect: "manual",
|
|
4883
|
+
signal: req.signal
|
|
4884
|
+
});
|
|
4885
|
+
if (!response.ok || response.type === "opaqueredirect") {
|
|
4886
|
+
throw new BrandNotFoundError(`asset ${before.id}`);
|
|
4887
|
+
}
|
|
4888
|
+
const served = bare(response.headers.get("content-type"));
|
|
4889
|
+
if (served === "image/svg+xml" || served && served !== "application/octet-stream" && served !== contentType) {
|
|
4890
|
+
return json({ error: "asset content type is inconsistent" }, 415);
|
|
4891
|
+
}
|
|
4892
|
+
const bytes = await boundedBytes(response, MAX_DISCUSSION_ASSET_BYTES);
|
|
4893
|
+
if (!bytes || bytes.byteLength !== before.size || !magicMatches(contentType, bytes) || await digest(bytes) !== before.contentDigest) {
|
|
4894
|
+
return json({ error: "asset bytes failed validation" }, 422);
|
|
4895
|
+
}
|
|
4896
|
+
const afterBook = await loadMemberBook(ctx.db, target.bookId, ctx.actor.id);
|
|
4897
|
+
const after = await linkedAsset(ctx, afterBook, target.resourceId);
|
|
4898
|
+
if (!exactAsset(beforeBook, before, afterBook, after)) {
|
|
4899
|
+
return json({ error: "asset changed during inspection" }, 409);
|
|
4900
|
+
}
|
|
4901
|
+
return json({
|
|
4902
|
+
asset: {
|
|
4903
|
+
reference: { kind: "brand:asset", id: `${before.bookId}/${before.id}` },
|
|
4904
|
+
contentType,
|
|
4905
|
+
byteLength: bytes.byteLength,
|
|
4906
|
+
data: base64(bytes),
|
|
4907
|
+
taint: "untrusted_project_material"
|
|
4908
|
+
}
|
|
4909
|
+
}, 200, {
|
|
4910
|
+
"cache-control": "private, no-store",
|
|
4911
|
+
"x-content-type-options": "nosniff"
|
|
4912
|
+
});
|
|
4913
|
+
}
|
|
4914
|
+
function base64(bytes) {
|
|
4915
|
+
let binary = "";
|
|
4916
|
+
for (let index = 0; index < bytes.length; index += 8192) {
|
|
4917
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + 8192));
|
|
4918
|
+
}
|
|
4919
|
+
return btoa(binary);
|
|
4920
|
+
}
|
|
4921
|
+
|
|
3985
4922
|
// src/routes/discussion-references.ts
|
|
3986
4923
|
var capLimit = (raw) => {
|
|
3987
4924
|
const parsed = Number(raw ?? 20);
|
|
@@ -4013,6 +4950,7 @@ var projection = (req, ctx, book, target, row) => {
|
|
|
4013
4950
|
let summary = `${book.name} brand book`;
|
|
4014
4951
|
let status = book.status;
|
|
4015
4952
|
let destination = "Brand Studio \xB7 Brand book";
|
|
4953
|
+
let swatches;
|
|
4016
4954
|
if (target.kind === "brand:asset") {
|
|
4017
4955
|
const asset = row;
|
|
4018
4956
|
label = asset.title?.trim() || `${asset.kind} asset`;
|
|
@@ -4022,6 +4960,11 @@ var projection = (req, ctx, book, target, row) => {
|
|
|
4022
4960
|
destination = "Brand Studio \xB7 Conversation";
|
|
4023
4961
|
} else if (target.kind === "brand:palette") {
|
|
4024
4962
|
const palette = row;
|
|
4963
|
+
swatches = assertSwatches(palette.swatches).map((swatch) => ({
|
|
4964
|
+
role: swatch.role,
|
|
4965
|
+
hex: swatch.hex,
|
|
4966
|
+
...swatch.name ? { name: swatch.name } : {}
|
|
4967
|
+
}));
|
|
4025
4968
|
label = palette.name;
|
|
4026
4969
|
hint = `${book.name} \xB7 ${palette.status} palette`;
|
|
4027
4970
|
summary = `${palette.swatches.length}-color palette in ${book.name}`;
|
|
@@ -4054,7 +4997,8 @@ var projection = (req, ctx, book, target, row) => {
|
|
|
4054
4997
|
new URL(req.url),
|
|
4055
4998
|
target,
|
|
4056
4999
|
ctx.discussionBasePath
|
|
4057
|
-
)
|
|
5000
|
+
),
|
|
5001
|
+
...swatches ? { swatches } : {}
|
|
4058
5002
|
};
|
|
4059
5003
|
};
|
|
4060
5004
|
async function exact(ctx, req, target) {
|
|
@@ -4108,11 +5052,23 @@ async function search(ctx, req, url) {
|
|
|
4108
5052
|
}
|
|
4109
5053
|
async function handleBrandDiscussionReferences(ctx, req, url) {
|
|
4110
5054
|
if (req.method !== "GET") return methodNotAllowed();
|
|
5055
|
+
const inspect = url.searchParams.get("inspect");
|
|
4111
5056
|
const kind = url.searchParams.get("kind");
|
|
4112
5057
|
const id = url.searchParams.get("id");
|
|
4113
5058
|
if (kind && !id || !kind && id) return json({ error: "kind and id must be paired" }, 400);
|
|
4114
5059
|
const target = targetFromQuery(url);
|
|
4115
5060
|
if ((kind || id) && !target) return json({ items: [] });
|
|
5061
|
+
if (inspect === "asset-content") {
|
|
5062
|
+
const allowed = /* @__PURE__ */ new Set(["inspect", "kind", "id"]);
|
|
5063
|
+
if ([...url.searchParams.keys()].some((key) => !allowed.has(key)) || target?.kind !== "brand:asset" || !target.resourceId) return json({ error: "exact brand asset kind and id required" }, 400);
|
|
5064
|
+
return handleBrandDiscussionAsset(ctx, req, {
|
|
5065
|
+
bookId: target.bookId,
|
|
5066
|
+
resourceId: target.resourceId
|
|
5067
|
+
});
|
|
5068
|
+
}
|
|
5069
|
+
if (inspect !== null && inspect !== "1") {
|
|
5070
|
+
return json({ error: "invalid inspection mode" }, 400);
|
|
5071
|
+
}
|
|
4116
5072
|
return json({
|
|
4117
5073
|
items: target ? await exact(ctx, req, target) : await search(ctx, req, url)
|
|
4118
5074
|
});
|
|
@@ -4133,6 +5089,10 @@ async function route(ctx, req, url, seg) {
|
|
|
4133
5089
|
if (seg.length === 4) return handleAssetItem(ctx, req, id, subId);
|
|
4134
5090
|
if (seg.length === 5 && action === "content")
|
|
4135
5091
|
return handleAssetContent(ctx, req, id, subId);
|
|
5092
|
+
if (seg.length === 5 && action === "preview")
|
|
5093
|
+
return handleDesignPreview(ctx, req, id, subId);
|
|
5094
|
+
if (seg.length === 5 && action === "design")
|
|
5095
|
+
return handleDesignDigest(ctx, req, id, subId);
|
|
4136
5096
|
return null;
|
|
4137
5097
|
}
|
|
4138
5098
|
if (sub === "proposals" && seg.length === 5 && action === "resolve")
|
|
@@ -4153,7 +5113,9 @@ function createBrandRoutes(options) {
|
|
|
4153
5113
|
now: options.now ?? Date.now,
|
|
4154
5114
|
newId: options.newId ?? (() => crypto.randomUUID()),
|
|
4155
5115
|
maxUploadBytes: options.maxUploadBytes ?? 8 * 1024 * 1024,
|
|
4156
|
-
discussionBasePath: options.discussionBasePath ?? "/"
|
|
5116
|
+
discussionBasePath: options.discussionBasePath ?? "/",
|
|
5117
|
+
fetchPrivateAsset: options.fetchPrivateAsset ?? fetch,
|
|
5118
|
+
previewFrameAncestors: options.previewFrameAncestors ?? ["'self'"]
|
|
4157
5119
|
};
|
|
4158
5120
|
return async (req) => {
|
|
4159
5121
|
const url = new URL(req.url);
|