@odla-ai/brand 0.4.0 → 0.6.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 +45 -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 +983 -128
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +380 -9
- package/dist/index.d.ts +380 -9
- package/dist/index.js +920 -64
- 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.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
PICK_TEXT_DEFAULT_CANDIDATES,
|
|
13
13
|
RAMP_L_MAX,
|
|
14
14
|
RAMP_L_MIN,
|
|
15
|
+
ROLE_TOKEN,
|
|
15
16
|
adjustLightnessUntil,
|
|
16
17
|
analogous,
|
|
17
18
|
clamp01,
|
|
@@ -51,7 +52,7 @@ import {
|
|
|
51
52
|
tintShadeRamp,
|
|
52
53
|
toHex,
|
|
53
54
|
triadic
|
|
54
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-52DO72LS.js";
|
|
55
56
|
|
|
56
57
|
// src/constants.ts
|
|
57
58
|
var BRAND_NS = {
|
|
@@ -69,7 +70,15 @@ var PALETTE_STATUSES = ["active", "archived"];
|
|
|
69
70
|
var PALETTE_SOURCES = ["extracted", "derived", "manual"];
|
|
70
71
|
var PROPOSAL_KINDS = SECTION_KINDS;
|
|
71
72
|
var PROPOSAL_STATUSES = ["open", "accepted", "rejected", "superseded"];
|
|
72
|
-
var ASSET_KINDS = [
|
|
73
|
+
var ASSET_KINDS = [
|
|
74
|
+
"logo",
|
|
75
|
+
"wordmark",
|
|
76
|
+
"inspiration",
|
|
77
|
+
"document",
|
|
78
|
+
"design",
|
|
79
|
+
"other"
|
|
80
|
+
];
|
|
81
|
+
var DESIGN_ASSET_KIND = "design";
|
|
73
82
|
var SWATCH_ROLES = [
|
|
74
83
|
"primary",
|
|
75
84
|
"secondary",
|
|
@@ -293,7 +302,7 @@ var BRAND_SCHEMA = {
|
|
|
293
302
|
id: uniq("string"),
|
|
294
303
|
bookId: idx("string"),
|
|
295
304
|
kind: idx("string"),
|
|
296
|
-
// logo | wordmark | inspiration | document | other
|
|
305
|
+
// logo | wordmark | inspiration | document | design | other
|
|
297
306
|
path: idx("string"),
|
|
298
307
|
storageObjectId: idx("string"),
|
|
299
308
|
contentDigest: idx("string"),
|
|
@@ -305,6 +314,11 @@ var BRAND_SCHEMA = {
|
|
|
305
314
|
analysis: opt("json"),
|
|
306
315
|
// { description, dominantColors: hex[], tags }
|
|
307
316
|
analyzedAt: opt("date"),
|
|
317
|
+
// `design` kind only: the DesignDigest computed from the uploaded
|
|
318
|
+
// bundle at upload time. Machine-derived and deterministic — unlike
|
|
319
|
+
// `analysis`, which is what a model reports seeing — so it needs no
|
|
320
|
+
// review gate and is rewritten only by re-uploading.
|
|
321
|
+
design: opt("json"),
|
|
308
322
|
analysisRevision: a("number"),
|
|
309
323
|
analysisDigest: opt("string"),
|
|
310
324
|
analyzedBy: opt("string"),
|
|
@@ -399,6 +413,7 @@ var ASSET_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
|
399
413
|
"image/webp",
|
|
400
414
|
"application/pdf"
|
|
401
415
|
]);
|
|
416
|
+
var DESIGN_CONTENT_TYPES = /* @__PURE__ */ new Set(["text/html"]);
|
|
402
417
|
var HEX_RGB = /^#[0-9a-f]{3}$/;
|
|
403
418
|
var HEX_RRGGBB = /^#[0-9a-f]{6}$/;
|
|
404
419
|
var HEX_ALPHA = /^#[0-9a-f]{4}$|^#[0-9a-f]{8}$/;
|
|
@@ -520,12 +535,13 @@ function safeFileName(name) {
|
|
|
520
535
|
throw new BrandInputError("file name is empty or a dot segment after sanitizing");
|
|
521
536
|
return cleaned.slice(0, 120);
|
|
522
537
|
}
|
|
523
|
-
function assertAssetContentType(value) {
|
|
538
|
+
function assertAssetContentType(value, kind) {
|
|
524
539
|
if (typeof value !== "string") throw new BrandInputError("contentType must be a string");
|
|
525
540
|
const ct = value.split(";")[0].trim().toLowerCase();
|
|
526
|
-
|
|
541
|
+
const allowed = kind === DESIGN_ASSET_KIND ? DESIGN_CONTENT_TYPES : ASSET_CONTENT_TYPES;
|
|
542
|
+
if (!allowed.has(ct))
|
|
527
543
|
throw new BrandInputError(
|
|
528
|
-
`unsupported content type ${ct || "(empty)"}; allowed: ${[...
|
|
544
|
+
`unsupported content type ${ct || "(empty)"} for kind ${kind ?? "other"}; allowed: ${[...allowed].join(", ")}`
|
|
529
545
|
);
|
|
530
546
|
return ct;
|
|
531
547
|
}
|
|
@@ -542,8 +558,40 @@ function assertAnalysis(value) {
|
|
|
542
558
|
};
|
|
543
559
|
}
|
|
544
560
|
|
|
561
|
+
// src/read-shape.ts
|
|
562
|
+
var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
563
|
+
var DATE_FIELDS = /* @__PURE__ */ new Map();
|
|
564
|
+
for (const [ns, entity] of Object.entries(BRAND_SCHEMA.entities))
|
|
565
|
+
DATE_FIELDS.set(
|
|
566
|
+
ns,
|
|
567
|
+
new Set(
|
|
568
|
+
Object.entries(entity.attrs).filter(([, attr]) => attr.type === "date").map(([label]) => label)
|
|
569
|
+
)
|
|
570
|
+
);
|
|
571
|
+
function rowAsWritten(ns, row) {
|
|
572
|
+
const dates = DATE_FIELDS.get(ns);
|
|
573
|
+
if (!dates?.size || !record(row)) return row;
|
|
574
|
+
let changed = false;
|
|
575
|
+
const out = { ...row };
|
|
576
|
+
for (const field of dates) {
|
|
577
|
+
const value = out[field];
|
|
578
|
+
if (typeof value !== "string") continue;
|
|
579
|
+
const parsed = Date.parse(value);
|
|
580
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) continue;
|
|
581
|
+
out[field] = parsed;
|
|
582
|
+
changed = true;
|
|
583
|
+
}
|
|
584
|
+
return changed ? out : row;
|
|
585
|
+
}
|
|
586
|
+
function receiptAsWritten(row) {
|
|
587
|
+
return rowAsWritten(BRAND_NS.approvalReceipt, row);
|
|
588
|
+
}
|
|
589
|
+
function proposalAsWritten(row) {
|
|
590
|
+
return rowAsWritten(BRAND_NS.proposal, row);
|
|
591
|
+
}
|
|
592
|
+
|
|
545
593
|
// src/review-json.ts
|
|
546
|
-
var
|
|
594
|
+
var record2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
547
595
|
function isBoundedBrandJson(root, limits = {}) {
|
|
548
596
|
const maxDepth = limits.maxDepth ?? 12;
|
|
549
597
|
const maxNodes = limits.maxNodes ?? 2048;
|
|
@@ -567,7 +615,7 @@ function isBoundedBrandJson(root, limits = {}) {
|
|
|
567
615
|
if (Array.isArray(value)) {
|
|
568
616
|
for (const child of value)
|
|
569
617
|
stack.push({ value: child, depth: depth + 1 });
|
|
570
|
-
} else if (
|
|
618
|
+
} else if (record2(value)) {
|
|
571
619
|
for (const child of Object.values(value))
|
|
572
620
|
stack.push({ value: child, depth: depth + 1 });
|
|
573
621
|
} else {
|
|
@@ -604,7 +652,7 @@ async function brandJsonDigest(value) {
|
|
|
604
652
|
|
|
605
653
|
// src/review.ts
|
|
606
654
|
var DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
607
|
-
var
|
|
655
|
+
var record3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
608
656
|
var exactKeys = (value, required, optional = []) => {
|
|
609
657
|
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
610
658
|
return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => allowed.has(key));
|
|
@@ -629,15 +677,15 @@ var SNAPSHOT_REQUIRED = [
|
|
|
629
677
|
"createdAt"
|
|
630
678
|
];
|
|
631
679
|
function validSnapshotShape(value) {
|
|
632
|
-
if (!
|
|
680
|
+
if (!record3(value) || !exactKeys(value, SNAPSHOT_REQUIRED)) return false;
|
|
633
681
|
const provenance = value.provenance;
|
|
634
|
-
return boundedString(value.id) && boundedString(value.bookId) && typeof value.kind === "string" && PROPOSAL_KINDS.includes(value.kind) && value.status === "open" &&
|
|
682
|
+
return boundedString(value.id) && boundedString(value.bookId) && typeof value.kind === "string" && PROPOSAL_KINDS.includes(value.kind) && value.status === "open" && record3(value.payload) && boundedString(value.rationale, 2e3) && record3(provenance) && exactKeys(provenance, [
|
|
635
683
|
"sourceAssetId",
|
|
636
684
|
"sourceAsset",
|
|
637
685
|
"messageId",
|
|
638
686
|
"turnId",
|
|
639
687
|
"taintLabels"
|
|
640
|
-
]) && (provenance.sourceAssetId === null || boundedString(provenance.sourceAssetId)) && (provenance.sourceAsset === null ||
|
|
688
|
+
]) && (provenance.sourceAssetId === null || boundedString(provenance.sourceAssetId)) && (provenance.sourceAsset === null || record3(provenance.sourceAsset) && exactKeys(provenance.sourceAsset, [
|
|
641
689
|
"assetId",
|
|
642
690
|
"contentDigest",
|
|
643
691
|
"objectEtag",
|
|
@@ -670,8 +718,8 @@ var CONSUMPTION_REQUIRED = [
|
|
|
670
718
|
"consumedAt"
|
|
671
719
|
];
|
|
672
720
|
function isBrandHumanAuthorityConsumption(value) {
|
|
673
|
-
if (!
|
|
674
|
-
return boundedString(value.id) && boundedString(value.grantId) && Number.isSafeInteger(value.grantVersion) && value.grantVersion > 0 && Number.isSafeInteger(value.useNumber) && value.useNumber > 0 && boundedString(value.actorPrincipalId) && value.actorKind === "human" && boundedString(value.credentialId) && value.credentialKind === "clerk" && boundedString(value.appId) && typeof value.appIncarnation === "string" && /^[a-f0-9]{32}$/.test(value.appIncarnation) && value.capability === "brand.proposal.resolve" && value.projectCapability === "brand.approve" && value.effect === "internal" && typeof value.actionDigest === "string" && DIGEST.test(value.actionDigest) && typeof value.resourceDigest === "string" && DIGEST.test(value.resourceDigest) &&
|
|
721
|
+
if (!record3(value) || !exactKeys(value, CONSUMPTION_REQUIRED)) return false;
|
|
722
|
+
return boundedString(value.id) && boundedString(value.grantId) && Number.isSafeInteger(value.grantVersion) && value.grantVersion > 0 && Number.isSafeInteger(value.useNumber) && value.useNumber > 0 && boundedString(value.actorPrincipalId) && value.actorKind === "human" && boundedString(value.credentialId) && value.credentialKind === "clerk" && boundedString(value.appId) && typeof value.appIncarnation === "string" && /^[a-f0-9]{32}$/.test(value.appIncarnation) && value.capability === "brand.proposal.resolve" && value.projectCapability === "brand.approve" && value.effect === "internal" && typeof value.actionDigest === "string" && DIGEST.test(value.actionDigest) && typeof value.resourceDigest === "string" && DIGEST.test(value.resourceDigest) && record3(value.constraintEvidence) && boundedString(value.consumptionIdempotencyKey) && typeof value.requestDigest === "string" && DIGEST.test(value.requestDigest) && safeTime(value.consumedAt);
|
|
675
723
|
}
|
|
676
724
|
var RECEIPT_REQUIRED = [
|
|
677
725
|
"version",
|
|
@@ -697,10 +745,10 @@ async function verifyBrandApprovalReceipt(receipt2) {
|
|
|
697
745
|
try {
|
|
698
746
|
if (!isBoundedBrandJson(receipt2, { maxDepth: 14, maxNodes: 2500, maxBytes: 48 * 1024 }))
|
|
699
747
|
return false;
|
|
700
|
-
if (!
|
|
748
|
+
if (!record3(receipt2) || !exactKeys(receipt2, RECEIPT_REQUIRED, ["paletteId", "resolutionNote"]))
|
|
701
749
|
return false;
|
|
702
750
|
const binding = receipt2.decisionBinding;
|
|
703
|
-
if (receipt2.version !== 1 || !boundedString(receipt2.id) || !boundedString(receipt2.mutationKey) || !boundedString(receipt2.bookId) || !boundedString(receipt2.proposalId) || receipt2.resolution !== "accepted" && receipt2.resolution !== "rejected" || !validSnapshotShape(receipt2.reviewedProposal) || typeof receipt2.actionDigest !== "string" || !DIGEST.test(receipt2.actionDigest) || !
|
|
751
|
+
if (receipt2.version !== 1 || !boundedString(receipt2.id) || !boundedString(receipt2.mutationKey) || !boundedString(receipt2.bookId) || !boundedString(receipt2.proposalId) || receipt2.resolution !== "accepted" && receipt2.resolution !== "rejected" || !validSnapshotShape(receipt2.reviewedProposal) || typeof receipt2.actionDigest !== "string" || !DIGEST.test(receipt2.actionDigest) || !record3(binding) || !exactKeys(binding, [
|
|
704
752
|
"version",
|
|
705
753
|
"bookVersion",
|
|
706
754
|
"activationRevision",
|
|
@@ -1072,7 +1120,11 @@ function rejectProposalOps(input) {
|
|
|
1072
1120
|
function createAssetOps(input) {
|
|
1073
1121
|
if (!ASSET_KINDS.includes(input.kind))
|
|
1074
1122
|
throw new BrandInputError(`kind must be one of: ${ASSET_KINDS.join(", ")}`);
|
|
1075
|
-
const contentType = assertAssetContentType(input.contentType);
|
|
1123
|
+
const contentType = assertAssetContentType(input.contentType, input.kind);
|
|
1124
|
+
if (input.kind === DESIGN_ASSET_KIND && input.design === void 0)
|
|
1125
|
+
throw new BrandInputError("a design asset must carry its parsed digest");
|
|
1126
|
+
if (input.kind !== DESIGN_ASSET_KIND && input.design !== void 0)
|
|
1127
|
+
throw new BrandInputError(`only ${DESIGN_ASSET_KIND} assets may carry a design digest`);
|
|
1076
1128
|
if (typeof input.size !== "number" || !Number.isFinite(input.size) || input.size <= 0)
|
|
1077
1129
|
throw new BrandInputError("size must be a positive byte count");
|
|
1078
1130
|
if (!/^sha256:[0-9a-f]{64}$/.test(input.contentDigest))
|
|
@@ -1102,7 +1154,8 @@ function createAssetOps(input) {
|
|
|
1102
1154
|
200
|
|
1103
1155
|
),
|
|
1104
1156
|
createdAt: input.now,
|
|
1105
|
-
...title ? { title } : {}
|
|
1157
|
+
...title ? { title } : {},
|
|
1158
|
+
...input.design ? { design: input.design } : {}
|
|
1106
1159
|
}
|
|
1107
1160
|
},
|
|
1108
1161
|
{ t: "link", ns: BRAND_NS.asset, id: input.id, label: "book", target: input.bookId }
|
|
@@ -1154,6 +1207,563 @@ async function recordAnalysisOps(assetId, analysis, priorRevision, analyzedBy, a
|
|
|
1154
1207
|
];
|
|
1155
1208
|
}
|
|
1156
1209
|
|
|
1210
|
+
// src/design/bundle.ts
|
|
1211
|
+
var MAX_THUMBNAIL_CHARS = 16384;
|
|
1212
|
+
function island(html, name) {
|
|
1213
|
+
const open = `<script type="__bundler/${name}">`;
|
|
1214
|
+
const start = html.indexOf(open);
|
|
1215
|
+
if (start < 0) return null;
|
|
1216
|
+
const from = start + open.length;
|
|
1217
|
+
const end = html.indexOf("</script>", from);
|
|
1218
|
+
return end < 0 ? null : html.slice(from, end);
|
|
1219
|
+
}
|
|
1220
|
+
function isDesignBundle(html) {
|
|
1221
|
+
return html.includes('<script type="__bundler/manifest">') && html.includes('<script type="__bundler/template">');
|
|
1222
|
+
}
|
|
1223
|
+
function base64ByteLength(data) {
|
|
1224
|
+
const len = data.length;
|
|
1225
|
+
if (len === 0) return 0;
|
|
1226
|
+
const pad = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
1227
|
+
return Math.max(0, Math.floor(len * 3 / 4) - pad);
|
|
1228
|
+
}
|
|
1229
|
+
function parseIslandJson(text, name) {
|
|
1230
|
+
try {
|
|
1231
|
+
return JSON.parse(text);
|
|
1232
|
+
} catch {
|
|
1233
|
+
throw new BrandInputError(`design bundle's ${name} island is not valid JSON`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1237
|
+
function readAssets(raw) {
|
|
1238
|
+
if (!isRecord2(raw)) throw new BrandInputError("design bundle's manifest island must be an object");
|
|
1239
|
+
const assets = [];
|
|
1240
|
+
for (const [uuid, value] of Object.entries(raw)) {
|
|
1241
|
+
if (!isRecord2(value)) continue;
|
|
1242
|
+
const data = value.data;
|
|
1243
|
+
const mime = value.mime;
|
|
1244
|
+
if (typeof data !== "string" || typeof mime !== "string") continue;
|
|
1245
|
+
assets.push({
|
|
1246
|
+
uuid,
|
|
1247
|
+
mime: mime.slice(0, 120),
|
|
1248
|
+
bytes: base64ByteLength(data),
|
|
1249
|
+
compressed: value.compressed === true
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
return assets;
|
|
1253
|
+
}
|
|
1254
|
+
function readExternals(raw) {
|
|
1255
|
+
if (raw === null || raw === void 0) return [];
|
|
1256
|
+
if (!Array.isArray(raw))
|
|
1257
|
+
throw new BrandInputError("design bundle's ext_resources island must be an array");
|
|
1258
|
+
const out = [];
|
|
1259
|
+
for (const entry of raw) {
|
|
1260
|
+
if (isRecord2(entry) && typeof entry.id === "string") out.push(entry.id.slice(0, 512));
|
|
1261
|
+
}
|
|
1262
|
+
return out;
|
|
1263
|
+
}
|
|
1264
|
+
function readPageOrder(raw) {
|
|
1265
|
+
if (raw === null || raw === void 0) return [];
|
|
1266
|
+
if (!Array.isArray(raw))
|
|
1267
|
+
throw new BrandInputError("design bundle's page_order island must be an array");
|
|
1268
|
+
return raw.filter((v) => typeof v === "string");
|
|
1269
|
+
}
|
|
1270
|
+
function extractThumbnailSvg(html) {
|
|
1271
|
+
const anchor = html.indexOf("__bundler_thumbnail");
|
|
1272
|
+
if (anchor < 0) return void 0;
|
|
1273
|
+
const open = html.indexOf("<svg", anchor);
|
|
1274
|
+
if (open < 0) return void 0;
|
|
1275
|
+
const close = html.indexOf("</svg>", open);
|
|
1276
|
+
if (close < 0) return void 0;
|
|
1277
|
+
const svg = html.slice(open, close + "</svg>".length);
|
|
1278
|
+
return svg.length > MAX_THUMBNAIL_CHARS ? void 0 : svg;
|
|
1279
|
+
}
|
|
1280
|
+
function readDesignManifest(html) {
|
|
1281
|
+
const raw = island(html, "manifest");
|
|
1282
|
+
if (raw === null) throw new BrandInputError("design bundle has no manifest island");
|
|
1283
|
+
const parsed = parseIslandJson(raw, "manifest");
|
|
1284
|
+
if (!isRecord2(parsed)) throw new BrandInputError("design bundle's manifest island must be an object");
|
|
1285
|
+
const out = {};
|
|
1286
|
+
for (const [uuid, value] of Object.entries(parsed)) {
|
|
1287
|
+
if (!isRecord2(value)) continue;
|
|
1288
|
+
const { data, mime } = value;
|
|
1289
|
+
if (typeof data !== "string" || typeof mime !== "string") continue;
|
|
1290
|
+
out[uuid] = { mime, compressed: value.compressed === true, data };
|
|
1291
|
+
}
|
|
1292
|
+
return out;
|
|
1293
|
+
}
|
|
1294
|
+
function parseDesignBundle(html) {
|
|
1295
|
+
if (!isDesignBundle(html))
|
|
1296
|
+
throw new BrandInputError(
|
|
1297
|
+
"not a Claude Design bundle: no __bundler/manifest and __bundler/template script islands. Export the design as standalone HTML and upload that file."
|
|
1298
|
+
);
|
|
1299
|
+
const templateRaw = island(html, "template");
|
|
1300
|
+
if (templateRaw === null)
|
|
1301
|
+
throw new BrandInputError("design bundle's template island is unterminated");
|
|
1302
|
+
const template = parseIslandJson(templateRaw, "template");
|
|
1303
|
+
if (typeof template !== "string")
|
|
1304
|
+
throw new BrandInputError("design bundle's template island must be a JSON string");
|
|
1305
|
+
const manifestRaw = island(html, "manifest");
|
|
1306
|
+
if (manifestRaw === null)
|
|
1307
|
+
throw new BrandInputError("design bundle's manifest island is unterminated");
|
|
1308
|
+
const extRaw = island(html, "ext_resources");
|
|
1309
|
+
const pageRaw = island(html, "page_order");
|
|
1310
|
+
const thumbnailSvg = extractThumbnailSvg(html);
|
|
1311
|
+
return {
|
|
1312
|
+
template,
|
|
1313
|
+
assets: readAssets(parseIslandJson(manifestRaw, "manifest")),
|
|
1314
|
+
externals: readExternals(extRaw === null ? null : parseIslandJson(extRaw, "ext_resources")),
|
|
1315
|
+
pageOrder: readPageOrder(pageRaw === null ? null : parseIslandJson(pageRaw, "page_order")),
|
|
1316
|
+
...thumbnailSvg ? { thumbnailSvg } : {}
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/design/css-scan.ts
|
|
1321
|
+
function stripCssComments(css) {
|
|
1322
|
+
let out = "";
|
|
1323
|
+
let i = 0;
|
|
1324
|
+
for (; ; ) {
|
|
1325
|
+
const start = css.indexOf("/*", i);
|
|
1326
|
+
if (start < 0) return out + css.slice(i);
|
|
1327
|
+
out += css.slice(i, start);
|
|
1328
|
+
const end = css.indexOf("*/", start + 2);
|
|
1329
|
+
if (end < 0) return out;
|
|
1330
|
+
i = end + 2;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function styleSheetText(html) {
|
|
1334
|
+
const parts = [];
|
|
1335
|
+
let i = 0;
|
|
1336
|
+
for (; ; ) {
|
|
1337
|
+
const open = html.indexOf("<style", i);
|
|
1338
|
+
if (open < 0) break;
|
|
1339
|
+
const gt = html.indexOf(">", open);
|
|
1340
|
+
if (gt < 0) break;
|
|
1341
|
+
const close = html.indexOf("</style>", gt);
|
|
1342
|
+
if (close < 0) break;
|
|
1343
|
+
parts.push(html.slice(gt + 1, close));
|
|
1344
|
+
i = close + "</style>".length;
|
|
1345
|
+
}
|
|
1346
|
+
return parts.join("\n");
|
|
1347
|
+
}
|
|
1348
|
+
function scanCustomProperties(css) {
|
|
1349
|
+
const text = stripCssComments(css);
|
|
1350
|
+
const found = [];
|
|
1351
|
+
const stack = [];
|
|
1352
|
+
let paren = 0;
|
|
1353
|
+
let start = 0;
|
|
1354
|
+
const flush = (end) => {
|
|
1355
|
+
if (stack.length === 0) return;
|
|
1356
|
+
const chunk = text.slice(start, end).trim();
|
|
1357
|
+
if (!chunk.startsWith("--")) return;
|
|
1358
|
+
const colon = chunk.indexOf(":");
|
|
1359
|
+
if (colon < 0) return;
|
|
1360
|
+
const name = chunk.slice(0, colon).trim();
|
|
1361
|
+
if (name.length < 3) return;
|
|
1362
|
+
found.push({ selectors: [...stack], name, value: chunk.slice(colon + 1).trim() });
|
|
1363
|
+
};
|
|
1364
|
+
for (let i = 0; i < text.length; i++) {
|
|
1365
|
+
const ch = text[i];
|
|
1366
|
+
if (ch === "(") paren++;
|
|
1367
|
+
else if (ch === ")") paren = Math.max(0, paren - 1);
|
|
1368
|
+
if (paren !== 0) continue;
|
|
1369
|
+
if (ch === "{") {
|
|
1370
|
+
stack.push(text.slice(start, i).trim());
|
|
1371
|
+
start = i + 1;
|
|
1372
|
+
} else if (ch === "}") {
|
|
1373
|
+
flush(i);
|
|
1374
|
+
stack.pop();
|
|
1375
|
+
start = i + 1;
|
|
1376
|
+
} else if (ch === ";") {
|
|
1377
|
+
flush(i);
|
|
1378
|
+
start = i + 1;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return found;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// src/design/decompile.ts
|
|
1385
|
+
var CHART_TOKENS = [
|
|
1386
|
+
"--ui-chart-1",
|
|
1387
|
+
"--ui-chart-2",
|
|
1388
|
+
"--ui-chart-3",
|
|
1389
|
+
"--ui-chart-4",
|
|
1390
|
+
"--ui-chart-5",
|
|
1391
|
+
"--ui-chart-6"
|
|
1392
|
+
];
|
|
1393
|
+
function cssColorToHex(value) {
|
|
1394
|
+
const text = value.trim();
|
|
1395
|
+
if (text.startsWith("#")) {
|
|
1396
|
+
try {
|
|
1397
|
+
return assertHex(text);
|
|
1398
|
+
} catch {
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
const fn = /^rgba?\(([^)]*)\)$/i.exec(text);
|
|
1403
|
+
if (!fn) return null;
|
|
1404
|
+
const parts = (fn[1] ?? "").split(/[,/\s]+/).filter((p) => p !== "");
|
|
1405
|
+
if (parts.length < 3 || parts.length > 4) return null;
|
|
1406
|
+
if (parts.length === 4) {
|
|
1407
|
+
const alpha = parts[3].endsWith("%") ? Number.parseFloat(parts[3]) / 100 : Number.parseFloat(parts[3]);
|
|
1408
|
+
if (!Number.isFinite(alpha) || alpha < 1) return null;
|
|
1409
|
+
}
|
|
1410
|
+
const channels = parts.slice(0, 3).map((part) => {
|
|
1411
|
+
const n = Number.parseFloat(part);
|
|
1412
|
+
if (!Number.isFinite(n)) return Number.NaN;
|
|
1413
|
+
return Math.round(part.endsWith("%") ? n / 100 * 255 : n);
|
|
1414
|
+
});
|
|
1415
|
+
if (channels.some((c) => !Number.isFinite(c) || c < 0 || c > 255)) return null;
|
|
1416
|
+
return `#${channels.map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
|
1417
|
+
}
|
|
1418
|
+
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";
|
|
1419
|
+
function swatchesFromDesignTokens(tokens) {
|
|
1420
|
+
const light = tokens.light;
|
|
1421
|
+
const swatches = [];
|
|
1422
|
+
const skipped = [];
|
|
1423
|
+
const missing2 = [];
|
|
1424
|
+
for (const [role, token] of Object.entries(ROLE_TOKEN)) {
|
|
1425
|
+
if (role === "chart") continue;
|
|
1426
|
+
const value = light[token];
|
|
1427
|
+
if (value === void 0) {
|
|
1428
|
+
missing2.push(role);
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
const hex = cssColorToHex(value);
|
|
1432
|
+
if (hex === null) {
|
|
1433
|
+
skipped.push({ token, value, reason: skipReason(value) });
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
swatches.push({ role, hex, rationale: `declared by the design as ${token}` });
|
|
1437
|
+
}
|
|
1438
|
+
let anyChart = false;
|
|
1439
|
+
for (const token of CHART_TOKENS) {
|
|
1440
|
+
const value = light[token];
|
|
1441
|
+
if (value === void 0) continue;
|
|
1442
|
+
const hex = cssColorToHex(value);
|
|
1443
|
+
if (hex === null) {
|
|
1444
|
+
skipped.push({ token, value, reason: skipReason(value) });
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
anyChart = true;
|
|
1448
|
+
swatches.push({ role: "chart", hex, rationale: `declared by the design as ${token}` });
|
|
1449
|
+
}
|
|
1450
|
+
if (!anyChart) missing2.push("chart");
|
|
1451
|
+
return { swatches, skipped, missing: missing2 };
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// src/design/html-text.ts
|
|
1455
|
+
var NAMED_ENTITIES = {
|
|
1456
|
+
amp: "&",
|
|
1457
|
+
lt: "<",
|
|
1458
|
+
gt: ">",
|
|
1459
|
+
quot: '"',
|
|
1460
|
+
apos: "'",
|
|
1461
|
+
nbsp: "\xA0",
|
|
1462
|
+
mdash: "\u2014",
|
|
1463
|
+
ndash: "\u2013",
|
|
1464
|
+
hellip: "\u2026",
|
|
1465
|
+
rsquo: "\u2019",
|
|
1466
|
+
lsquo: "\u2018",
|
|
1467
|
+
ldquo: "\u201C",
|
|
1468
|
+
rdquo: "\u201D"
|
|
1469
|
+
};
|
|
1470
|
+
function decodeEntities(text) {
|
|
1471
|
+
return text.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z][a-zA-Z0-9]{1,31});/g, (whole, body) => {
|
|
1472
|
+
if (body.startsWith("#x") || body.startsWith("#X")) {
|
|
1473
|
+
const code = Number.parseInt(body.slice(2), 16);
|
|
1474
|
+
return Number.isFinite(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
|
|
1475
|
+
}
|
|
1476
|
+
if (body.startsWith("#")) {
|
|
1477
|
+
const code = Number.parseInt(body.slice(1), 10);
|
|
1478
|
+
return Number.isFinite(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
|
|
1479
|
+
}
|
|
1480
|
+
return NAMED_ENTITIES[body.toLowerCase()] ?? whole;
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
var collapseWhitespace = (text) => text.replace(/\s+/g, " ").trim();
|
|
1484
|
+
function htmlToText(fragment) {
|
|
1485
|
+
const withoutCode = fragment.replace(/<script\b[^>]*>[\s\S]*?<\/script\s*>/gi, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi, " ");
|
|
1486
|
+
return collapseWhitespace(decodeEntities(withoutCode.replace(/<[^>]*>/g, " ")));
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// src/design/outline.ts
|
|
1490
|
+
var MAX_OUTLINE_ENTRIES = 120;
|
|
1491
|
+
var MAX_HEADING_CHARS = 200;
|
|
1492
|
+
function extractTitle(html) {
|
|
1493
|
+
const match = /<title\b[^>]*>([\s\S]{0,2000}?)<\/title\s*>/i.exec(html);
|
|
1494
|
+
if (!match) return void 0;
|
|
1495
|
+
const text = htmlToText(match[1] ?? "");
|
|
1496
|
+
return text === "" ? void 0 : text.slice(0, MAX_HEADING_CHARS);
|
|
1497
|
+
}
|
|
1498
|
+
function extractOutline(html) {
|
|
1499
|
+
const pattern = /<h([1-6])\b[^>]*>([\s\S]{0,4000}?)<\/h\1\s*>/gi;
|
|
1500
|
+
const entries = [];
|
|
1501
|
+
for (; ; ) {
|
|
1502
|
+
const match = pattern.exec(html);
|
|
1503
|
+
if (match === null) break;
|
|
1504
|
+
const text = htmlToText(match[2] ?? "");
|
|
1505
|
+
if (text === "") continue;
|
|
1506
|
+
if (entries.length >= MAX_OUTLINE_ENTRIES) return { entries, truncated: true };
|
|
1507
|
+
entries.push({ level: Number(match[1]), text: text.slice(0, MAX_HEADING_CHARS) });
|
|
1508
|
+
}
|
|
1509
|
+
return { entries, truncated: false };
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// src/design/props.ts
|
|
1513
|
+
var MAX_PROPS = 60;
|
|
1514
|
+
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1515
|
+
function attributeValue(html, attr) {
|
|
1516
|
+
const at = html.indexOf(`${attr}="`);
|
|
1517
|
+
if (at >= 0) {
|
|
1518
|
+
const from2 = at + attr.length + 2;
|
|
1519
|
+
const end2 = html.indexOf('"', from2);
|
|
1520
|
+
return end2 < 0 ? null : html.slice(from2, end2);
|
|
1521
|
+
}
|
|
1522
|
+
const single = html.indexOf(`${attr}='`);
|
|
1523
|
+
if (single < 0) return null;
|
|
1524
|
+
const from = single + attr.length + 2;
|
|
1525
|
+
const end = html.indexOf("'", from);
|
|
1526
|
+
return end < 0 ? null : html.slice(from, end);
|
|
1527
|
+
}
|
|
1528
|
+
function defaultText(value) {
|
|
1529
|
+
if (value === void 0 || value === null) return void 0;
|
|
1530
|
+
if (typeof value === "string") return value.slice(0, 400);
|
|
1531
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1532
|
+
try {
|
|
1533
|
+
return JSON.stringify(value).slice(0, 400);
|
|
1534
|
+
} catch {
|
|
1535
|
+
return void 0;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
function toProp(name, spec) {
|
|
1539
|
+
const options = Array.isArray(spec.options) ? spec.options.filter((v) => typeof v === "string").slice(0, 24) : void 0;
|
|
1540
|
+
const declared = defaultText(spec.default);
|
|
1541
|
+
return {
|
|
1542
|
+
name: name.slice(0, 80),
|
|
1543
|
+
editor: typeof spec.editor === "string" ? spec.editor.slice(0, 40) : "unknown",
|
|
1544
|
+
...options && options.length > 0 ? { options } : {},
|
|
1545
|
+
...declared !== void 0 ? { default: declared } : {},
|
|
1546
|
+
...typeof spec.section === "string" ? { section: spec.section.slice(0, 80) } : {},
|
|
1547
|
+
...typeof spec.tsType === "string" ? { tsType: spec.tsType.slice(0, 200) } : {}
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
function extractProps(html) {
|
|
1551
|
+
const at = html.indexOf("data-props=");
|
|
1552
|
+
if (at < 0) return { props: [], truncated: false };
|
|
1553
|
+
const raw = attributeValue(html.slice(at), "data-props");
|
|
1554
|
+
if (raw === null) return { props: [], truncated: false };
|
|
1555
|
+
let parsed;
|
|
1556
|
+
try {
|
|
1557
|
+
parsed = JSON.parse(decodeEntities(raw));
|
|
1558
|
+
} catch {
|
|
1559
|
+
return { props: [], truncated: false };
|
|
1560
|
+
}
|
|
1561
|
+
if (!isRecord3(parsed)) return { props: [], truncated: false };
|
|
1562
|
+
const entries = Object.entries(parsed).filter(
|
|
1563
|
+
(entry) => isRecord3(entry[1])
|
|
1564
|
+
);
|
|
1565
|
+
return {
|
|
1566
|
+
props: entries.slice(0, MAX_PROPS).map(([name, spec]) => toProp(name, spec)),
|
|
1567
|
+
truncated: entries.length > MAX_PROPS
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// src/design/styles.ts
|
|
1572
|
+
var MAX_FONTS = 16;
|
|
1573
|
+
var MAX_COLORS = 24;
|
|
1574
|
+
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
1575
|
+
"serif",
|
|
1576
|
+
"sans-serif",
|
|
1577
|
+
"monospace",
|
|
1578
|
+
"cursive",
|
|
1579
|
+
"fantasy",
|
|
1580
|
+
"system-ui",
|
|
1581
|
+
"ui-serif",
|
|
1582
|
+
"ui-sans-serif",
|
|
1583
|
+
"ui-monospace",
|
|
1584
|
+
"ui-rounded",
|
|
1585
|
+
"math",
|
|
1586
|
+
"emoji",
|
|
1587
|
+
"inherit",
|
|
1588
|
+
"initial",
|
|
1589
|
+
"revert",
|
|
1590
|
+
"unset",
|
|
1591
|
+
"currentcolor"
|
|
1592
|
+
]);
|
|
1593
|
+
var familyName = (raw) => raw.trim().replace(/^["']|["']$/g, "").trim();
|
|
1594
|
+
function fontFaceFamilies(css) {
|
|
1595
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1596
|
+
const blocks = /@font-face\s*\{([^}]{0,4000})\}/gi;
|
|
1597
|
+
for (; ; ) {
|
|
1598
|
+
const block = blocks.exec(css);
|
|
1599
|
+
if (block === null) break;
|
|
1600
|
+
const declared = /font-family\s*:\s*([^;]{1,200})/i.exec(block[1] ?? "");
|
|
1601
|
+
if (!declared) continue;
|
|
1602
|
+
const name = familyName(declared[1] ?? "");
|
|
1603
|
+
if (name !== "" && !GENERIC_FAMILIES.has(name.toLowerCase())) seen.add(name);
|
|
1604
|
+
}
|
|
1605
|
+
return [...seen];
|
|
1606
|
+
}
|
|
1607
|
+
function stackFamilies(css) {
|
|
1608
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1609
|
+
const stacks = /font-family\s*:\s*([^;{}]{1,400})/gi;
|
|
1610
|
+
for (; ; ) {
|
|
1611
|
+
const stack = stacks.exec(css);
|
|
1612
|
+
if (stack === null) break;
|
|
1613
|
+
for (const part of (stack[1] ?? "").split(",")) {
|
|
1614
|
+
if (!/["']/.test(part)) continue;
|
|
1615
|
+
const name = familyName(part);
|
|
1616
|
+
if (name === "" || GENERIC_FAMILIES.has(name.toLowerCase()) || name.includes("var(")) continue;
|
|
1617
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return [...counts.entries()].sort((a2, b) => b[1] - a2[1] || (a2[0] < b[0] ? -1 : 1)).map(([name]) => name);
|
|
1621
|
+
}
|
|
1622
|
+
function extractFonts(templateHtml) {
|
|
1623
|
+
const css = stripCssComments(styleSheetText(templateHtml));
|
|
1624
|
+
const embedded = fontFaceFamilies(css);
|
|
1625
|
+
return (embedded.length > 0 ? embedded : stackFamilies(css)).slice(0, MAX_FONTS);
|
|
1626
|
+
}
|
|
1627
|
+
function extractColors(templateHtml) {
|
|
1628
|
+
const css = stripCssComments(styleSheetText(templateHtml));
|
|
1629
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1630
|
+
const hexes = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;
|
|
1631
|
+
for (; ; ) {
|
|
1632
|
+
const found = hexes.exec(css);
|
|
1633
|
+
if (found === null) break;
|
|
1634
|
+
const raw = (found[1] ?? "").toLowerCase();
|
|
1635
|
+
const hex = raw.length === 3 ? `#${raw[0]}${raw[0]}${raw[1]}${raw[1]}${raw[2]}${raw[2]}` : `#${raw}`;
|
|
1636
|
+
counts.set(hex, (counts.get(hex) ?? 0) + 1);
|
|
1637
|
+
}
|
|
1638
|
+
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 }));
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// src/design/tokens.ts
|
|
1642
|
+
var MAX_VAR_DEPTH = 8;
|
|
1643
|
+
var DOC_SELECTOR = /(^|,)\s*(:root|html)\b|\[data-theme\s*=|\.ui-invert\b/;
|
|
1644
|
+
var DARK_SELECTOR = /data-theme\s*=\s*["']?dark|prefers-color-scheme\s*:\s*dark|\.ui-invert\b/;
|
|
1645
|
+
function splitVarArgs(inner) {
|
|
1646
|
+
let paren = 0;
|
|
1647
|
+
for (let i = 0; i < inner.length; i++) {
|
|
1648
|
+
const ch = inner[i];
|
|
1649
|
+
if (ch === "(") paren++;
|
|
1650
|
+
else if (ch === ")") paren--;
|
|
1651
|
+
else if (ch === "," && paren === 0)
|
|
1652
|
+
return { name: inner.slice(0, i).trim(), fallback: inner.slice(i + 1).trim() };
|
|
1653
|
+
}
|
|
1654
|
+
return { name: inner.trim() };
|
|
1655
|
+
}
|
|
1656
|
+
function resolveVarRefs(value, table, depth = 0) {
|
|
1657
|
+
if (depth >= MAX_VAR_DEPTH || !value.includes("var(")) return value;
|
|
1658
|
+
let out = "";
|
|
1659
|
+
let i = 0;
|
|
1660
|
+
for (; ; ) {
|
|
1661
|
+
const at = value.indexOf("var(", i);
|
|
1662
|
+
if (at < 0) return out + value.slice(i);
|
|
1663
|
+
out += value.slice(i, at);
|
|
1664
|
+
let paren = 1;
|
|
1665
|
+
let j = at + "var(".length;
|
|
1666
|
+
for (; j < value.length && paren > 0; j++) {
|
|
1667
|
+
if (value[j] === "(") paren++;
|
|
1668
|
+
else if (value[j] === ")") paren--;
|
|
1669
|
+
}
|
|
1670
|
+
if (paren > 0) return out + value.slice(at);
|
|
1671
|
+
const { name, fallback } = splitVarArgs(value.slice(at + "var(".length, j - 1));
|
|
1672
|
+
const referenced = table.get(name);
|
|
1673
|
+
const replacement = referenced !== void 0 ? resolveVarRefs(referenced, table, depth + 1) : fallback !== void 0 ? resolveVarRefs(fallback, table, depth + 1) : `var(${name})`;
|
|
1674
|
+
out += replacement;
|
|
1675
|
+
i = j;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
function foldDeclarations(html) {
|
|
1679
|
+
const light = /* @__PURE__ */ new Map();
|
|
1680
|
+
const darkOverrides = [];
|
|
1681
|
+
for (const decl of scanCustomProperties(styleSheetText(html))) {
|
|
1682
|
+
const innermost = decl.selectors[decl.selectors.length - 1] ?? "";
|
|
1683
|
+
if (!DOC_SELECTOR.test(innermost)) continue;
|
|
1684
|
+
if (decl.selectors.some((sel) => DARK_SELECTOR.test(sel))) darkOverrides.push([decl.name, decl.value]);
|
|
1685
|
+
else light.set(decl.name, decl.value);
|
|
1686
|
+
}
|
|
1687
|
+
const dark = new Map(light);
|
|
1688
|
+
for (const [name, value] of darkOverrides) dark.set(name, value);
|
|
1689
|
+
return { light, dark };
|
|
1690
|
+
}
|
|
1691
|
+
function resolveUiTokens(table) {
|
|
1692
|
+
const out = {};
|
|
1693
|
+
for (const [name, value] of table) {
|
|
1694
|
+
if (!name.startsWith("--ui-")) continue;
|
|
1695
|
+
out[name] = resolveVarRefs(value, table);
|
|
1696
|
+
}
|
|
1697
|
+
return out;
|
|
1698
|
+
}
|
|
1699
|
+
function extractDesignTokens(templateHtml) {
|
|
1700
|
+
const { light, dark } = foldDeclarations(templateHtml);
|
|
1701
|
+
return { light: resolveUiTokens(light), dark: resolveUiTokens(dark) };
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// src/design/digest.ts
|
|
1705
|
+
var MAX_EXTERNALS = 24;
|
|
1706
|
+
function groupAssets(bundle) {
|
|
1707
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1708
|
+
for (const asset of bundle.assets) {
|
|
1709
|
+
const group = groups.get(asset.mime) ?? { mime: asset.mime, count: 0, bytes: 0 };
|
|
1710
|
+
group.count += 1;
|
|
1711
|
+
group.bytes += asset.bytes;
|
|
1712
|
+
groups.set(asset.mime, group);
|
|
1713
|
+
}
|
|
1714
|
+
return [...groups.values()].sort((a2, b) => b.bytes - a2.bytes || (a2.mime < b.mime ? -1 : 1));
|
|
1715
|
+
}
|
|
1716
|
+
function digestDesignBundle(bundle) {
|
|
1717
|
+
const template = bundle.template;
|
|
1718
|
+
const outline = extractOutline(template);
|
|
1719
|
+
const props = extractProps(template);
|
|
1720
|
+
const assetGroups = groupAssets(bundle);
|
|
1721
|
+
const title = extractTitle(template);
|
|
1722
|
+
const truncated = [];
|
|
1723
|
+
if (outline.truncated) truncated.push("outline");
|
|
1724
|
+
if (props.truncated) truncated.push("props");
|
|
1725
|
+
if (bundle.externals.length > MAX_EXTERNALS) truncated.push("externals");
|
|
1726
|
+
return {
|
|
1727
|
+
format: "claude-design-bundle/1",
|
|
1728
|
+
...title ? { title } : {},
|
|
1729
|
+
templateBytes: new TextEncoder().encode(template).byteLength,
|
|
1730
|
+
assetBytes: bundle.assets.reduce((total, asset) => total + asset.bytes, 0),
|
|
1731
|
+
assetCount: bundle.assets.length,
|
|
1732
|
+
assetGroups,
|
|
1733
|
+
externals: bundle.externals.slice(0, MAX_EXTERNALS),
|
|
1734
|
+
pageCount: bundle.pageOrder.length,
|
|
1735
|
+
tokens: extractDesignTokens(template),
|
|
1736
|
+
fonts: extractFonts(template),
|
|
1737
|
+
colors: extractColors(template),
|
|
1738
|
+
props: props.props,
|
|
1739
|
+
outline: outline.entries,
|
|
1740
|
+
...bundle.thumbnailSvg ? { thumbnailSvg: bundle.thumbnailSvg } : {},
|
|
1741
|
+
truncated
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
function digestDesignHtml(html) {
|
|
1745
|
+
return digestDesignBundle(parseDesignBundle(html));
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// src/palette-report.ts
|
|
1749
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
1750
|
+
function contrastReport(swatches) {
|
|
1751
|
+
const byRole = (role) => swatches.find((s) => s.role === role)?.hex;
|
|
1752
|
+
const bg = byRole("bg") ?? "#ffffff";
|
|
1753
|
+
const report = {};
|
|
1754
|
+
const put = (label, fg) => {
|
|
1755
|
+
if (fg) report[label] = round2(contrastRatio(fg, bg));
|
|
1756
|
+
};
|
|
1757
|
+
put("text-on-bg", byRole("text"));
|
|
1758
|
+
put("primary-on-bg", byRole("primary"));
|
|
1759
|
+
put("good-on-bg", byRole("good"));
|
|
1760
|
+
put("warn-on-bg", byRole("warn"));
|
|
1761
|
+
put("danger-on-bg", byRole("danger"));
|
|
1762
|
+
const primary = byRole("primary");
|
|
1763
|
+
if (primary) report["text-on-primary"] = round2(contrastRatio(pickTextOn(primary), primary));
|
|
1764
|
+
return report;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1157
1767
|
// src/skill/asset-tools.ts
|
|
1158
1768
|
var MAX_VIEW_BYTES = 4718592;
|
|
1159
1769
|
var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
@@ -1365,22 +1975,6 @@ function applyHarmony(swatches, seedHex, harmony) {
|
|
|
1365
1975
|
return { ...s, hex, name: nearestNamedColor(hex).name, rationale: `${harmony} companion of the seed` };
|
|
1366
1976
|
});
|
|
1367
1977
|
}
|
|
1368
|
-
function contrastReport(swatches) {
|
|
1369
|
-
const byRole = (role) => swatches.find((s) => s.role === role)?.hex;
|
|
1370
|
-
const bg = byRole("bg") ?? "#ffffff";
|
|
1371
|
-
const report = {};
|
|
1372
|
-
const put = (label, fg) => {
|
|
1373
|
-
if (fg) report[label] = Math.round(contrastRatio(fg, bg) * 100) / 100;
|
|
1374
|
-
};
|
|
1375
|
-
put("text-on-bg", byRole("text"));
|
|
1376
|
-
put("primary-on-bg", byRole("primary"));
|
|
1377
|
-
put("good-on-bg", byRole("good"));
|
|
1378
|
-
put("warn-on-bg", byRole("warn"));
|
|
1379
|
-
put("danger-on-bg", byRole("danger"));
|
|
1380
|
-
const primary = byRole("primary");
|
|
1381
|
-
if (primary) report["text-on-primary"] = Math.round(contrastRatio(pickTextOn(primary), primary) * 100) / 100;
|
|
1382
|
-
return report;
|
|
1383
|
-
}
|
|
1384
1978
|
var INCLUDE_SECTIONS = ["harmony", "ramp"];
|
|
1385
1979
|
function analyzeLines(hex, include) {
|
|
1386
1980
|
const lch = hexToOklch(hex);
|
|
@@ -1537,6 +2131,160 @@ function paletteTools(ctx) {
|
|
|
1537
2131
|
return [analyzeColor, evaluateContrast, proposePalette];
|
|
1538
2132
|
}
|
|
1539
2133
|
|
|
2134
|
+
// src/skill/design-tools.ts
|
|
2135
|
+
var MAX_SOURCE_WINDOW = 12e3;
|
|
2136
|
+
function describeDigest(asset, digest2) {
|
|
2137
|
+
const lines = [
|
|
2138
|
+
`Design ${asset.id}${digest2.title ? ` \u2014 "${digest2.title}"` : ""}${asset.title ? ` (uploaded as "${asset.title}")` : ""}`,
|
|
2139
|
+
`Template ${digest2.templateBytes} bytes; ${digest2.assetCount} embedded assets (${digest2.assetBytes} bytes); ${digest2.pageCount} nested pages.`
|
|
2140
|
+
];
|
|
2141
|
+
if (digest2.assetGroups.length > 0)
|
|
2142
|
+
lines.push(
|
|
2143
|
+
`Assets: ${digest2.assetGroups.map((g) => `${g.count}\xD7 ${g.mime} (${g.bytes} B)`).join(", ")}`
|
|
2144
|
+
);
|
|
2145
|
+
if (digest2.externals.length > 0) lines.push(`Built against: ${digest2.externals.join(", ")}`);
|
|
2146
|
+
if (digest2.fonts.length > 0) lines.push(`Typefaces: ${digest2.fonts.join(", ")}`);
|
|
2147
|
+
const tokenNames = Object.keys(digest2.tokens.light);
|
|
2148
|
+
lines.push(
|
|
2149
|
+
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."
|
|
2150
|
+
);
|
|
2151
|
+
if (digest2.colors.length > 0)
|
|
2152
|
+
lines.push(`Literal colors: ${digest2.colors.map((c) => `${c.hex}\xD7${c.count}`).join(", ")}`);
|
|
2153
|
+
if (digest2.props.length > 0)
|
|
2154
|
+
lines.push(
|
|
2155
|
+
"Configurable props:\n" + digest2.props.map(
|
|
2156
|
+
(p) => ` ${p.name} (${p.editor}${p.options ? `: ${p.options.join("|")}` : ""})${p.default === void 0 ? "" : ` default ${p.default}`}${p.section ? ` [${p.section}]` : ""}`
|
|
2157
|
+
).join("\n")
|
|
2158
|
+
);
|
|
2159
|
+
if (digest2.outline.length > 0)
|
|
2160
|
+
lines.push(
|
|
2161
|
+
"Outline:\n" + digest2.outline.map((h) => `${" ".repeat(h.level - 1)}h${h.level} ${h.text}`).join("\n")
|
|
2162
|
+
);
|
|
2163
|
+
if (digest2.truncated.length > 0)
|
|
2164
|
+
lines.push(`NOTE: truncated to fit digest caps: ${digest2.truncated.join(", ")}.`);
|
|
2165
|
+
return lines.join("\n");
|
|
2166
|
+
}
|
|
2167
|
+
function sourceWindow(template, input) {
|
|
2168
|
+
const length = Math.min(
|
|
2169
|
+
MAX_SOURCE_WINDOW,
|
|
2170
|
+
Math.max(1, typeof input.length === "number" ? input.length : MAX_SOURCE_WINDOW)
|
|
2171
|
+
);
|
|
2172
|
+
let start;
|
|
2173
|
+
if (input.find !== void 0 && input.find !== "") {
|
|
2174
|
+
const at = template.indexOf(input.find);
|
|
2175
|
+
if (at < 0) throw new BrandNotFoundError(`"${input.find}" in the design source`);
|
|
2176
|
+
start = Math.max(0, at - 400);
|
|
2177
|
+
} else {
|
|
2178
|
+
start = Math.max(0, Math.min(template.length, Math.trunc(input.offset ?? 0)));
|
|
2179
|
+
}
|
|
2180
|
+
const end = Math.min(template.length, start + length);
|
|
2181
|
+
return { text: template.slice(start, end), start, end };
|
|
2182
|
+
}
|
|
2183
|
+
function designTools(ctx) {
|
|
2184
|
+
const loadDesign = async (assetId) => {
|
|
2185
|
+
await ctx.authority("brand.read");
|
|
2186
|
+
const res = await ctx.db.query({
|
|
2187
|
+
[BRAND_NS.asset]: { $: { where: { id: assetId, bookId: ctx.bookId, status: "live" } } }
|
|
2188
|
+
});
|
|
2189
|
+
const row = (res[BRAND_NS.asset] ?? [])[0];
|
|
2190
|
+
if (!row || row.kind !== DESIGN_ASSET_KIND || !row.design)
|
|
2191
|
+
throw new BrandNotFoundError(`design asset ${assetId}`);
|
|
2192
|
+
return row;
|
|
2193
|
+
};
|
|
2194
|
+
const readDesign = {
|
|
2195
|
+
name: "read_design",
|
|
2196
|
+
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.",
|
|
2197
|
+
inputSchema: {
|
|
2198
|
+
type: "object",
|
|
2199
|
+
required: ["assetId"],
|
|
2200
|
+
properties: {
|
|
2201
|
+
assetId: { type: "string", description: "A design asset id from list_assets." }
|
|
2202
|
+
}
|
|
2203
|
+
},
|
|
2204
|
+
handler: ctx.guard(async (input) => {
|
|
2205
|
+
const asset = await loadDesign(capString(input.assetId, "assetId", 200));
|
|
2206
|
+
return { content: describeDigest(asset, asset.design) };
|
|
2207
|
+
})
|
|
2208
|
+
};
|
|
2209
|
+
const readDesignSource = {
|
|
2210
|
+
name: "read_design_source",
|
|
2211
|
+
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.",
|
|
2212
|
+
// The template is author-supplied content, not instructions.
|
|
2213
|
+
outputTaint: ["tool_untrusted:read_design_source"],
|
|
2214
|
+
inputSchema: {
|
|
2215
|
+
type: "object",
|
|
2216
|
+
required: ["assetId"],
|
|
2217
|
+
properties: {
|
|
2218
|
+
assetId: { type: "string" },
|
|
2219
|
+
find: { type: "string", description: "Jump to the first occurrence of this string." },
|
|
2220
|
+
offset: { type: "number", description: "Character offset to read from (ignored with find)." },
|
|
2221
|
+
length: { type: "number", description: `Characters to return (max ${MAX_SOURCE_WINDOW}).` }
|
|
2222
|
+
}
|
|
2223
|
+
},
|
|
2224
|
+
handler: ctx.guard(async (input) => {
|
|
2225
|
+
const asset = await loadDesign(capString(input.assetId, "assetId", 200));
|
|
2226
|
+
const template = await ctx.readDesignTemplate(asset.id);
|
|
2227
|
+
const found = sourceWindow(template, {
|
|
2228
|
+
...typeof input.find === "string" ? { find: input.find } : {},
|
|
2229
|
+
...typeof input.offset === "number" ? { offset: input.offset } : {},
|
|
2230
|
+
...typeof input.length === "number" ? { length: input.length } : {}
|
|
2231
|
+
});
|
|
2232
|
+
return {
|
|
2233
|
+
content: `Design ${asset.id} source, characters ${found.start}\u2013${found.end} of ${template.length}:
|
|
2234
|
+
` + found.text
|
|
2235
|
+
};
|
|
2236
|
+
})
|
|
2237
|
+
};
|
|
2238
|
+
const proposeFromDesign = {
|
|
2239
|
+
name: "propose_palette_from_design",
|
|
2240
|
+
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.",
|
|
2241
|
+
acceptsTaint: ["tool_untrusted:read_design_source"],
|
|
2242
|
+
inputSchema: {
|
|
2243
|
+
type: "object",
|
|
2244
|
+
required: ["assetId", "rationale"],
|
|
2245
|
+
properties: {
|
|
2246
|
+
assetId: { type: "string" },
|
|
2247
|
+
name: { type: "string", description: "Palette name; defaults to the design's title." },
|
|
2248
|
+
rationale: { type: "string", description: "Why this design's palette should become the brand's." }
|
|
2249
|
+
}
|
|
2250
|
+
},
|
|
2251
|
+
handler: ctx.guard(async (input) => {
|
|
2252
|
+
const book = await ctx.loadBook();
|
|
2253
|
+
await ctx.authority("brand.edit");
|
|
2254
|
+
const asset = await loadDesign(capString(input.assetId, "assetId", 200));
|
|
2255
|
+
const digest2 = asset.design;
|
|
2256
|
+
const rationale = capString(input.rationale, "rationale", 2e3);
|
|
2257
|
+
const name = capString(
|
|
2258
|
+
input.name ?? digest2.title ?? asset.title ?? "Design palette",
|
|
2259
|
+
"name",
|
|
2260
|
+
120
|
|
2261
|
+
);
|
|
2262
|
+
const { swatches, skipped, missing: missing2 } = swatchesFromDesignTokens(digest2.tokens);
|
|
2263
|
+
if (swatches.length === 0)
|
|
2264
|
+
throw new BrandInputError(
|
|
2265
|
+
`design ${asset.id} declares no --ui-* tokens that resolve to opaque colors; propose a palette explicitly instead.`
|
|
2266
|
+
);
|
|
2267
|
+
const report = contrastReport(swatches);
|
|
2268
|
+
const proposal = await ctx.createProposal({
|
|
2269
|
+
mutationId: ctx.newId(),
|
|
2270
|
+
kind: "palette",
|
|
2271
|
+
payload: { name, swatches, contrastReport: report, source: "design", designAssetId: asset.id },
|
|
2272
|
+
rationale,
|
|
2273
|
+
sourceAssetId: asset.id
|
|
2274
|
+
});
|
|
2275
|
+
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");
|
|
2276
|
+
const notes = [
|
|
2277
|
+
skipped.length > 0 ? `Not imported (not literal opaque colors): ${skipped.map((s) => `${s.token} \u2014 ${s.reason}`).join("; ")}.` : "",
|
|
2278
|
+
missing2.length > 0 ? `Roles the design declares no token for: ${missing2.join(", ")}.` : ""
|
|
2279
|
+
].filter((n) => n !== "");
|
|
2280
|
+
return {
|
|
2281
|
+
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()
|
|
2282
|
+
};
|
|
2283
|
+
})
|
|
2284
|
+
};
|
|
2285
|
+
return [readDesign, readDesignSource, proposeFromDesign];
|
|
2286
|
+
}
|
|
2287
|
+
|
|
1540
2288
|
// src/skill/read-tools.ts
|
|
1541
2289
|
var iso = (ms) => new Date(ms).toISOString();
|
|
1542
2290
|
var shortId = (id) => id.length <= 12 ? id : `${id.slice(0, 6)}\u2026${id.slice(-4)}`;
|
|
@@ -1652,10 +2400,11 @@ function readTools(ctx) {
|
|
|
1652
2400
|
}
|
|
1653
2401
|
|
|
1654
2402
|
// src/skill/skill.ts
|
|
1655
|
-
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.\
|
|
2403
|
+
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.";
|
|
1656
2404
|
function brandSkill(opts) {
|
|
1657
2405
|
if (opts.agentDbBinding.principalId !== opts.self.selfId || !opts.agentDbBinding.credentialRef) throw new BrandForbiddenError("brand db is not bound to the acting agent");
|
|
1658
2406
|
const deps = resolveDeps({ db: opts.db, now: opts.now, newId: opts.newId });
|
|
2407
|
+
const designTemplates = /* @__PURE__ */ new Map();
|
|
1659
2408
|
const authority = (capability) => Promise.resolve(opts.authorizeCapability({
|
|
1660
2409
|
agentId: opts.self.selfId,
|
|
1661
2410
|
bookId: opts.bookId,
|
|
@@ -1688,6 +2437,16 @@ function brandSkill(opts) {
|
|
|
1688
2437
|
bookId: opts.bookId,
|
|
1689
2438
|
assetId
|
|
1690
2439
|
}),
|
|
2440
|
+
readDesignTemplate: (assetId) => {
|
|
2441
|
+
const cached = designTemplates.get(assetId);
|
|
2442
|
+
if (cached) return cached;
|
|
2443
|
+
const pending = opts.agentBridge.readAssetContent({ jobId: opts.agentJobId, bookId: opts.bookId, assetId }).then(
|
|
2444
|
+
(fetched) => parseDesignBundle(new TextDecoder().decode(fetched.bytes)).template
|
|
2445
|
+
);
|
|
2446
|
+
pending.catch(() => designTemplates.delete(assetId));
|
|
2447
|
+
designTemplates.set(assetId, pending);
|
|
2448
|
+
return pending;
|
|
2449
|
+
},
|
|
1691
2450
|
resolvePrincipals: (principalIds) => {
|
|
1692
2451
|
const ids = [...new Set(principalIds)].slice(0, 100);
|
|
1693
2452
|
return opts.resolvePrincipals({
|
|
@@ -1719,7 +2478,13 @@ function brandSkill(opts) {
|
|
|
1719
2478
|
return {
|
|
1720
2479
|
name: "brand",
|
|
1721
2480
|
instructions: BRAND_INSTRUCTIONS,
|
|
1722
|
-
tools: [
|
|
2481
|
+
tools: [
|
|
2482
|
+
...readTools(ctx),
|
|
2483
|
+
...assetTools(ctx),
|
|
2484
|
+
...designTools(ctx),
|
|
2485
|
+
...paletteTools(ctx),
|
|
2486
|
+
...bookTools(ctx)
|
|
2487
|
+
]
|
|
1723
2488
|
};
|
|
1724
2489
|
}
|
|
1725
2490
|
|
|
@@ -1748,7 +2513,7 @@ async function linkedAsset(ctx, book, assetId, allowDeleting = false) {
|
|
|
1748
2513
|
book: {}
|
|
1749
2514
|
}
|
|
1750
2515
|
});
|
|
1751
|
-
const row = (result[BRAND_NS.asset] ?? [])[0];
|
|
2516
|
+
const row = rowAsWritten(BRAND_NS.asset, (result[BRAND_NS.asset] ?? [])[0]);
|
|
1752
2517
|
const links = row?.book;
|
|
1753
2518
|
if (!row || row.bookId !== book.id || !Array.isArray(links) || links.length !== 1 || links[0]?.id !== book.id || row.status !== "live" && !(allowDeleting && row.status === "deleting")) throw new BrandNotFoundError(`asset ${assetId}`);
|
|
1754
2519
|
return row;
|
|
@@ -1822,7 +2587,7 @@ async function loadBook(db, bookId) {
|
|
|
1822
2587
|
const res = await db.query({ [BRAND_NS.book]: { $: { where: { id: bookId } } } });
|
|
1823
2588
|
const row = (res[BRAND_NS.book] ?? [])[0];
|
|
1824
2589
|
if (!row) throw new BrandNotFoundError(`brand book ${bookId}`);
|
|
1825
|
-
return row;
|
|
2590
|
+
return rowAsWritten(BRAND_NS.book, row);
|
|
1826
2591
|
}
|
|
1827
2592
|
async function loadMemberBook(db, bookId, actorId) {
|
|
1828
2593
|
const book = await loadBook(db, bookId);
|
|
@@ -1850,9 +2615,13 @@ async function uploadAsset(ctx, req, bookId) {
|
|
|
1850
2615
|
const file = form.get("file");
|
|
1851
2616
|
if (!(file instanceof File))
|
|
1852
2617
|
throw new BrandInputError('"file" must be an uploaded file field');
|
|
2618
|
+
const kindRaw = form.get("kind");
|
|
2619
|
+
const kind = typeof kindRaw === "string" && kindRaw ? kindRaw : "other";
|
|
2620
|
+
if (!ASSET_KINDS.includes(kind))
|
|
2621
|
+
throw new BrandInputError(`kind must be one of: ${ASSET_KINDS.join(", ")}`);
|
|
1853
2622
|
let contentType;
|
|
1854
2623
|
try {
|
|
1855
|
-
contentType = assertAssetContentType(file.type);
|
|
2624
|
+
contentType = assertAssetContentType(file.type, kind);
|
|
1856
2625
|
} catch (error) {
|
|
1857
2626
|
if (error instanceof BrandInputError)
|
|
1858
2627
|
return json({ error: error.message }, 415);
|
|
@@ -1860,10 +2629,7 @@ async function uploadAsset(ctx, req, bookId) {
|
|
|
1860
2629
|
}
|
|
1861
2630
|
if (file.size > ctx.maxUploadBytes)
|
|
1862
2631
|
return json({ error: `file exceeds ${ctx.maxUploadBytes} bytes` }, 413);
|
|
1863
|
-
const
|
|
1864
|
-
const kind = typeof kindRaw === "string" && kindRaw ? kindRaw : "other";
|
|
1865
|
-
if (!ASSET_KINDS.includes(kind))
|
|
1866
|
-
throw new BrandInputError(`kind must be one of: ${ASSET_KINDS.join(", ")}`);
|
|
2632
|
+
const design = kind === DESIGN_ASSET_KIND ? digestDesignHtml(await file.text()) : void 0;
|
|
1867
2633
|
const titleRaw = form.get("title");
|
|
1868
2634
|
const title = typeof titleRaw === "string" && titleRaw ? capString(titleRaw, "title", 160) : void 0;
|
|
1869
2635
|
const fileName = safeFileName(file.name);
|
|
@@ -1876,14 +2642,14 @@ async function uploadAsset(ctx, req, bookId) {
|
|
|
1876
2642
|
"internal",
|
|
1877
2643
|
book.id
|
|
1878
2644
|
);
|
|
1879
|
-
const
|
|
2645
|
+
const record7 = await ctx.db.storage.upload(
|
|
1880
2646
|
path,
|
|
1881
2647
|
file,
|
|
1882
2648
|
contentType,
|
|
1883
2649
|
{ private: true }
|
|
1884
2650
|
);
|
|
1885
|
-
if (
|
|
1886
|
-
await ctx.db.storage.delete(
|
|
2651
|
+
if (record7.path !== path) {
|
|
2652
|
+
await ctx.db.storage.delete(record7.path);
|
|
1887
2653
|
throw new BrandConflictError(
|
|
1888
2654
|
"private storage returned an unexpected asset path"
|
|
1889
2655
|
);
|
|
@@ -1902,15 +2668,16 @@ async function uploadAsset(ctx, req, bookId) {
|
|
|
1902
2668
|
id,
|
|
1903
2669
|
bookId: book.id,
|
|
1904
2670
|
kind,
|
|
1905
|
-
path:
|
|
1906
|
-
storageObjectId:
|
|
2671
|
+
path: record7.path,
|
|
2672
|
+
storageObjectId: record7.id,
|
|
1907
2673
|
contentDigest: await contentDigest(file),
|
|
1908
2674
|
contentType,
|
|
1909
|
-
size:
|
|
2675
|
+
size: record7.size,
|
|
1910
2676
|
uploadedBy: ctx.actor.id,
|
|
1911
2677
|
uploadedAuthorityRef: authority.authorityRef,
|
|
1912
2678
|
audience: book.memberIds,
|
|
1913
2679
|
title,
|
|
2680
|
+
...design ? { design } : {},
|
|
1914
2681
|
now: ctx.now()
|
|
1915
2682
|
}), {
|
|
1916
2683
|
mutationId: `brand:asset-upload:v1:${id}`,
|
|
@@ -1932,7 +2699,7 @@ async function uploadAsset(ctx, req, bookId) {
|
|
|
1932
2699
|
asPrincipalKind: "human"
|
|
1933
2700
|
});
|
|
1934
2701
|
} catch (error) {
|
|
1935
|
-
await ctx.db.storage.delete(
|
|
2702
|
+
await ctx.db.storage.delete(record7.path);
|
|
1936
2703
|
throw error;
|
|
1937
2704
|
}
|
|
1938
2705
|
return json(await linkedAsset(ctx, book, id), 201);
|
|
@@ -2047,7 +2814,7 @@ async function handleAssetItem(ctx, req, bookId, assetId) {
|
|
|
2047
2814
|
async function handleBooksRoot(ctx, req) {
|
|
2048
2815
|
if (req.method === "GET") {
|
|
2049
2816
|
const res = await ctx.db.query({ [BRAND_NS.book]: { $: { order: { createdAt: "asc" } } } });
|
|
2050
|
-
const books = (res[BRAND_NS.book] ?? []).filter(
|
|
2817
|
+
const books = (res[BRAND_NS.book] ?? []).map((b) => rowAsWritten(BRAND_NS.book, b)).filter(
|
|
2051
2818
|
(b) => isMember(b, ctx.actor.id)
|
|
2052
2819
|
);
|
|
2053
2820
|
return json({ books });
|
|
@@ -2152,6 +2919,51 @@ async function handleBookItem(ctx, req, bookId) {
|
|
|
2152
2919
|
return json(await loadMemberBook(ctx.db, bookId, ctx.actor.id));
|
|
2153
2920
|
}
|
|
2154
2921
|
|
|
2922
|
+
// src/routes/design-preview.ts
|
|
2923
|
+
var SIGNED_READ_TTL_SECONDS = 60;
|
|
2924
|
+
function designPreviewHeaders(frameAncestors) {
|
|
2925
|
+
return {
|
|
2926
|
+
"content-type": "text/html; charset=utf-8",
|
|
2927
|
+
"content-security-policy": [
|
|
2928
|
+
"sandbox allow-scripts",
|
|
2929
|
+
`frame-ancestors ${frameAncestors.join(" ")}`
|
|
2930
|
+
].join("; "),
|
|
2931
|
+
"x-content-type-options": "nosniff",
|
|
2932
|
+
"referrer-policy": "no-referrer",
|
|
2933
|
+
"cache-control": "private, no-store"
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
async function handleDesignPreview(ctx, req, bookId, assetId) {
|
|
2937
|
+
if (req.method !== "GET") return methodNotAllowed();
|
|
2938
|
+
const book = await loadMemberBook(ctx.db, bookId, ctx.actor.id);
|
|
2939
|
+
const asset = await linkedAsset(ctx, book, assetId);
|
|
2940
|
+
if (asset.kind !== DESIGN_ASSET_KIND) throw new BrandNotFoundError(`design ${assetId}`);
|
|
2941
|
+
const signed = await ctx.db.storage.sign(asset.path, SIGNED_READ_TTL_SECONDS);
|
|
2942
|
+
const fetched = await ctx.fetchPrivateAsset(signed, {
|
|
2943
|
+
headers: { accept: "text/html" },
|
|
2944
|
+
redirect: "manual",
|
|
2945
|
+
signal: req.signal
|
|
2946
|
+
});
|
|
2947
|
+
if (!fetched.ok || fetched.type === "opaqueredirect")
|
|
2948
|
+
throw new BrandNotFoundError(`design ${assetId}`);
|
|
2949
|
+
return new Response(new Uint8Array(await fetched.arrayBuffer()), {
|
|
2950
|
+
status: 200,
|
|
2951
|
+
headers: designPreviewHeaders(ctx.previewFrameAncestors)
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
async function handleDesignDigest(ctx, req, bookId, assetId) {
|
|
2955
|
+
if (req.method !== "GET") return methodNotAllowed();
|
|
2956
|
+
const book = await loadMemberBook(ctx.db, bookId, ctx.actor.id);
|
|
2957
|
+
const asset = await linkedAsset(ctx, book, assetId);
|
|
2958
|
+
if (asset.kind !== DESIGN_ASSET_KIND || !asset.design)
|
|
2959
|
+
throw new BrandNotFoundError(`design ${assetId}`);
|
|
2960
|
+
return json(
|
|
2961
|
+
{ assetId: asset.id, title: asset.title, digest: asset.design },
|
|
2962
|
+
200,
|
|
2963
|
+
{ "cache-control": "private, no-store" }
|
|
2964
|
+
);
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2155
2967
|
// src/routes/proposal-input.ts
|
|
2156
2968
|
var SNAPSHOT_KEYS = [
|
|
2157
2969
|
"audience",
|
|
@@ -2167,7 +2979,7 @@ var SNAPSHOT_KEYS = [
|
|
|
2167
2979
|
"reviewDigest",
|
|
2168
2980
|
"status"
|
|
2169
2981
|
];
|
|
2170
|
-
var
|
|
2982
|
+
var record4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2171
2983
|
function parseProposalResolutionRequest(body, bookId, proposalId) {
|
|
2172
2984
|
const allowed = /* @__PURE__ */ new Set([
|
|
2173
2985
|
"mutationId",
|
|
@@ -2211,13 +3023,13 @@ function proposalReviewSnapshot(proposal) {
|
|
|
2211
3023
|
function parseReviewedProposal(value, bookId, proposalId) {
|
|
2212
3024
|
if (!isBoundedBrandJson(value, { maxDepth: 12, maxNodes: 1500, maxBytes: 32 * 1024 }))
|
|
2213
3025
|
throw new BrandInputError('"reviewedProposal" is too deeply nested, complex, or large');
|
|
2214
|
-
if (!
|
|
3026
|
+
if (!record4(value)) throw new BrandInputError('"reviewedProposal" must be an object');
|
|
2215
3027
|
const keys = Object.keys(value).sort();
|
|
2216
3028
|
if (keys.length !== SNAPSHOT_KEYS.length || !SNAPSHOT_KEYS.every((key, index) => key === keys[index])) throw new BrandInputError('"reviewedProposal" has an invalid shape');
|
|
2217
3029
|
if (value.id !== proposalId || value.bookId !== bookId || value.status !== "open")
|
|
2218
3030
|
throw new BrandInputError('"reviewedProposal" must identify this open proposal');
|
|
2219
|
-
if (typeof value.kind !== "string" || !PROPOSAL_KINDS.includes(value.kind) || !
|
|
2220
|
-
if (!
|
|
3031
|
+
if (typeof value.kind !== "string" || !PROPOSAL_KINDS.includes(value.kind) || !record4(value.payload) || typeof value.rationale !== "string" || !Array.isArray(value.audience) || value.audience.length < 1 || value.audience.length > 100 || value.audience.some((id) => typeof id !== "string" || id.length < 1 || id.length > 200) || new Set(value.audience).size !== value.audience.length || typeof value.createdBy !== "string" || value.createdBy.length < 1 || value.createdBy.length > 200 || typeof value.createdAuthorityRef !== "string" || value.createdAuthorityRef.length < 1 || value.createdAuthorityRef.length > 200 || !Number.isSafeInteger(value.createdAt) || value.createdAt < 0 || typeof value.reviewDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(value.reviewDigest)) throw new BrandInputError('"reviewedProposal" has invalid fields');
|
|
3032
|
+
if (!record4(value.provenance) || Object.keys(value.provenance).sort().join(",") !== "messageId,sourceAsset,sourceAssetId,taintLabels,turnId" || value.provenance.sourceAssetId !== null && (typeof value.provenance.sourceAssetId !== "string" || value.provenance.sourceAssetId.length < 1 || value.provenance.sourceAssetId.length > 200) || value.provenance.sourceAsset !== null && (!record4(value.provenance.sourceAsset) || Object.keys(value.provenance.sourceAsset).sort().join(",") !== "analysisDigest,analysisRevision,assetId,contentDigest,contentType,objectEtag,objectSize,pathDigest" || typeof value.provenance.sourceAsset.assetId !== "string" || value.provenance.sourceAsset.assetId.length < 1 || value.provenance.sourceAsset.assetId.length > 200 || typeof value.provenance.sourceAsset.objectEtag !== "string" || value.provenance.sourceAsset.objectEtag.length < 1 || value.provenance.sourceAsset.objectEtag.length > 200 || !Number.isSafeInteger(value.provenance.sourceAsset.objectSize) || value.provenance.sourceAsset.objectSize < 1 || typeof value.provenance.sourceAsset.pathDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(value.provenance.sourceAsset.pathDigest) || value.provenance.sourceAsset.contentType !== null && (typeof value.provenance.sourceAsset.contentType !== "string" || value.provenance.sourceAsset.contentType.length < 1 || value.provenance.sourceAsset.contentType.length > 160) || typeof value.provenance.sourceAsset.contentDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(value.provenance.sourceAsset.contentDigest) || value.provenance.sourceAsset.analysisRevision !== null && (!Number.isSafeInteger(value.provenance.sourceAsset.analysisRevision) || value.provenance.sourceAsset.analysisRevision < 0) || value.provenance.sourceAsset.analysisDigest !== null && (typeof value.provenance.sourceAsset.analysisDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(
|
|
2221
3033
|
value.provenance.sourceAsset.analysisDigest
|
|
2222
3034
|
)) || value.provenance.sourceAsset.assetId !== value.provenance.sourceAssetId) || value.provenance.sourceAssetId === null !== (value.provenance.sourceAsset === null) || value.provenance.messageId !== null && (typeof value.provenance.messageId !== "string" || value.provenance.messageId.length < 1 || value.provenance.messageId.length > 200) || value.provenance.turnId !== null && (typeof value.provenance.turnId !== "string" || value.provenance.turnId.length < 1 || value.provenance.turnId.length > 200) || !Array.isArray(value.provenance.taintLabels) || value.provenance.taintLabels.length > 16 || value.provenance.taintLabels.some((label) => typeof label !== "string" || label.length < 1 || label.length > 120) || new Set(value.provenance.taintLabels).size !== value.provenance.taintLabels.length) throw new BrandInputError('"reviewedProposal.provenance" is invalid');
|
|
2223
3035
|
return value;
|
|
@@ -2390,7 +3202,7 @@ async function proposalReceiptByMutation(ctx, mutationKey) {
|
|
|
2390
3202
|
const row = (result[BRAND_NS.approvalReceipt] ?? [])[0];
|
|
2391
3203
|
if (!row || !Array.isArray(row.book) || row.book.length !== 1 || row.book[0]?.id !== row.bookId) return void 0;
|
|
2392
3204
|
const { book: _book, ...receipt2 } = row;
|
|
2393
|
-
return receipt2;
|
|
3205
|
+
return receiptAsWritten(receipt2);
|
|
2394
3206
|
}
|
|
2395
3207
|
async function consumeProposalAuthority(ctx, req, bookId, proposalId, actionDigest, mutationKey) {
|
|
2396
3208
|
const authority = await ctx.consumeHumanExact({
|
|
@@ -2424,7 +3236,8 @@ async function receipt(ctx, id, actionDigest, expected) {
|
|
|
2424
3236
|
});
|
|
2425
3237
|
const row = (result[BRAND_NS.approvalReceipt] ?? [])[0];
|
|
2426
3238
|
if (!row || row.actionDigest !== actionDigest || row.resolution !== "accepted" || row.bookId !== expected.bookId || row.reviewedProposal.bookId !== expected.bookId || row.reviewedProposal.kind !== expected.kind || !Array.isArray(row.book) || row.book.length !== 1 || row.book[0]?.id !== expected.bookId || expected.paletteId !== void 0 && row.paletteId !== expected.paletteId || expected.proposalId !== void 0 && row.proposalId !== expected.proposalId || expected.content !== void 0 && canonicalBrandJson(row.reviewedProposal.payload.content) !== canonicalBrandJson(expected.content)) throw new BrandConflictError(`approval receipt ${id} is invalid`);
|
|
2427
|
-
const { book: _book, ...
|
|
3239
|
+
const { book: _book, ...stored } = row;
|
|
3240
|
+
const unhydrated = receiptAsWritten(stored);
|
|
2428
3241
|
if (!await verifyBrandApprovalReceipt(unhydrated))
|
|
2429
3242
|
throw new BrandConflictError(`approval receipt ${id} is invalid`);
|
|
2430
3243
|
return unhydrated;
|
|
@@ -2662,9 +3475,9 @@ async function proposalDecisionState(ctx, req, book, proposal, resolution, recei
|
|
|
2662
3475
|
}
|
|
2663
3476
|
|
|
2664
3477
|
// src/routes/proposals.ts
|
|
2665
|
-
var
|
|
3478
|
+
var record5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2666
3479
|
var sameStrings2 = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
|
|
2667
|
-
var guardFailure2 = (error) =>
|
|
3480
|
+
var guardFailure2 = (error) => record5(error) && error.code === "transact_guard_failed";
|
|
2668
3481
|
async function handleProposalResolution(ctx, req, bookId, proposalId) {
|
|
2669
3482
|
if (req.method !== "POST") return methodNotAllowed();
|
|
2670
3483
|
const book = await loadMemberBook(ctx.db, bookId, ctx.actor.id);
|
|
@@ -2711,7 +3524,7 @@ async function handleProposalResolution(ctx, req, bookId, proposalId) {
|
|
|
2711
3524
|
book: {}
|
|
2712
3525
|
}
|
|
2713
3526
|
});
|
|
2714
|
-
const proposal = (result[BRAND_NS.proposal] ?? [])[0];
|
|
3527
|
+
const proposal = proposalAsWritten((result[BRAND_NS.proposal] ?? [])[0]);
|
|
2715
3528
|
if (!proposal || proposal.bookId !== book.id || !Array.isArray(proposal.book) || proposal.book.length !== 1 || proposal.book[0]?.id !== book.id) throw new BrandNotFoundError(`proposal ${proposalId}`);
|
|
2716
3529
|
const current = proposalReviewSnapshot(proposal);
|
|
2717
3530
|
const { reviewDigest, ...unsignedReview } = current;
|
|
@@ -2844,7 +3657,7 @@ async function handleProposalResolution(ctx, req, bookId, proposalId) {
|
|
|
2844
3657
|
}
|
|
2845
3658
|
|
|
2846
3659
|
// src/routes/tokens.ts
|
|
2847
|
-
var
|
|
3660
|
+
var record6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2848
3661
|
var missing = (bookId) => {
|
|
2849
3662
|
throw new BrandNotFoundError(`compiled tokens for brand book ${bookId}`);
|
|
2850
3663
|
};
|
|
@@ -2857,13 +3670,14 @@ async function acceptedReceipt(db, bookId, receiptId, actionDigest) {
|
|
|
2857
3670
|
});
|
|
2858
3671
|
const receipt2 = (result[BRAND_NS.approvalReceipt] ?? [])[0];
|
|
2859
3672
|
if (!receipt2 || receipt2.resolution !== "accepted" || receipt2.actionDigest !== actionDigest || !Array.isArray(receipt2.book) || receipt2.book.length !== 1 || receipt2.book[0]?.id !== bookId) return missing(bookId);
|
|
2860
|
-
const { book: _book, ...
|
|
3673
|
+
const { book: _book, ...stored } = receipt2;
|
|
3674
|
+
const unhydrated = receiptAsWritten(stored);
|
|
2861
3675
|
if (!await verifyBrandApprovalReceipt(unhydrated)) return missing(bookId);
|
|
2862
3676
|
return unhydrated;
|
|
2863
3677
|
}
|
|
2864
3678
|
async function approvedCache(db, book) {
|
|
2865
3679
|
const cache = book.tokens;
|
|
2866
|
-
if (!cache || !
|
|
3680
|
+
if (!cache || !record6(cache.light) || !record6(cache.dark) || !book.activePaletteId || cache.paletteId !== book.activePaletteId || !cache.paletteReceiptId || !cache.paletteActionDigest || !cache.sourceDigest) return missing(book.id);
|
|
2867
3681
|
const paletteResult = await db.query({
|
|
2868
3682
|
[BRAND_NS.palette]: {
|
|
2869
3683
|
$: { where: { id: cache.paletteId, bookId: book.id } },
|
|
@@ -3222,6 +4036,10 @@ async function route(ctx, req, url, seg) {
|
|
|
3222
4036
|
if (seg.length === 4) return handleAssetItem(ctx, req, id, subId);
|
|
3223
4037
|
if (seg.length === 5 && action === "content")
|
|
3224
4038
|
return handleAssetContent(ctx, req, id, subId);
|
|
4039
|
+
if (seg.length === 5 && action === "preview")
|
|
4040
|
+
return handleDesignPreview(ctx, req, id, subId);
|
|
4041
|
+
if (seg.length === 5 && action === "design")
|
|
4042
|
+
return handleDesignDigest(ctx, req, id, subId);
|
|
3225
4043
|
return null;
|
|
3226
4044
|
}
|
|
3227
4045
|
if (sub === "proposals" && seg.length === 5 && action === "resolve")
|
|
@@ -3243,7 +4061,8 @@ function createBrandRoutes(options) {
|
|
|
3243
4061
|
newId: options.newId ?? (() => crypto.randomUUID()),
|
|
3244
4062
|
maxUploadBytes: options.maxUploadBytes ?? 8 * 1024 * 1024,
|
|
3245
4063
|
discussionBasePath: options.discussionBasePath ?? "/",
|
|
3246
|
-
fetchPrivateAsset: options.fetchPrivateAsset ?? fetch
|
|
4064
|
+
fetchPrivateAsset: options.fetchPrivateAsset ?? fetch,
|
|
4065
|
+
previewFrameAncestors: options.previewFrameAncestors ?? ["'self'"]
|
|
3247
4066
|
};
|
|
3248
4067
|
return async (req) => {
|
|
3249
4068
|
const url = new URL(req.url);
|
|
@@ -3536,7 +4355,16 @@ export {
|
|
|
3536
4355
|
DARK_FLIP_L_MIN,
|
|
3537
4356
|
DEFAULT_ACCENT_SEED,
|
|
3538
4357
|
DEFAULT_BRAND_SYSTEM,
|
|
4358
|
+
DESIGN_ASSET_KIND,
|
|
4359
|
+
DESIGN_CONTENT_TYPES,
|
|
4360
|
+
MAX_COLORS,
|
|
4361
|
+
MAX_EXTERNALS,
|
|
4362
|
+
MAX_FONTS,
|
|
4363
|
+
MAX_HEADING_CHARS,
|
|
4364
|
+
MAX_OUTLINE_ENTRIES,
|
|
4365
|
+
MAX_PROPS,
|
|
3539
4366
|
MAX_SWATCHES,
|
|
4367
|
+
MAX_THUMBNAIL_CHARS,
|
|
3540
4368
|
MAX_VIEW_BYTES,
|
|
3541
4369
|
PALETTE_SOURCES,
|
|
3542
4370
|
PALETTE_STATUSES,
|
|
@@ -3545,6 +4373,7 @@ export {
|
|
|
3545
4373
|
PROPOSAL_STATUSES,
|
|
3546
4374
|
RAMP_L_MAX,
|
|
3547
4375
|
RAMP_L_MIN,
|
|
4376
|
+
ROLE_TOKEN,
|
|
3548
4377
|
SECTION_KINDS,
|
|
3549
4378
|
SECTION_STATUSES,
|
|
3550
4379
|
SWATCH_ROLES,
|
|
@@ -3559,6 +4388,7 @@ export {
|
|
|
3559
4388
|
assetTools,
|
|
3560
4389
|
attachedBrandAssetIds,
|
|
3561
4390
|
audienceFanoutOps,
|
|
4391
|
+
base64ByteLength,
|
|
3562
4392
|
base64FromBytes,
|
|
3563
4393
|
beginAssetDeleteOps,
|
|
3564
4394
|
bookForChannel,
|
|
@@ -3577,27 +4407,43 @@ export {
|
|
|
3577
4407
|
capStringArray,
|
|
3578
4408
|
clamp01,
|
|
3579
4409
|
clampToGamut,
|
|
4410
|
+
collapseWhitespace,
|
|
3580
4411
|
compileBrandTokens,
|
|
3581
4412
|
complementary,
|
|
3582
4413
|
contrastRatio,
|
|
4414
|
+
contrastReport,
|
|
3583
4415
|
createAssetOps,
|
|
3584
4416
|
createBookOps,
|
|
3585
4417
|
createBrandIntegration,
|
|
3586
4418
|
createBrandPersona,
|
|
3587
4419
|
createBrandRoutes,
|
|
4420
|
+
cssColorToHex,
|
|
4421
|
+
decodeEntities,
|
|
3588
4422
|
deltaEOK,
|
|
3589
4423
|
deltaEOKLab,
|
|
3590
4424
|
deriveChartColors,
|
|
3591
4425
|
deriveDarkTokens,
|
|
3592
4426
|
derivePalette,
|
|
4427
|
+
digestDesignBundle,
|
|
4428
|
+
digestDesignHtml,
|
|
3593
4429
|
dispatchBrandTurn,
|
|
4430
|
+
extractColors,
|
|
4431
|
+
extractDesignTokens,
|
|
4432
|
+
extractFonts,
|
|
4433
|
+
extractOutline,
|
|
4434
|
+
extractProps,
|
|
4435
|
+
extractThumbnailSvg,
|
|
4436
|
+
extractTitle,
|
|
3594
4437
|
finishAssetDeleteOps,
|
|
3595
4438
|
formatBrandDiscussionReference,
|
|
4439
|
+
groupAssets,
|
|
3596
4440
|
hexToOklch,
|
|
3597
4441
|
hslToRgb,
|
|
4442
|
+
htmlToText,
|
|
3598
4443
|
inSrgbGamut,
|
|
3599
4444
|
isBoundedBrandJson,
|
|
3600
4445
|
isBrandHumanAuthorityConsumption,
|
|
4446
|
+
isDesignBundle,
|
|
3601
4447
|
linearToSrgb,
|
|
3602
4448
|
mapPaletteToTokens,
|
|
3603
4449
|
meetsAA,
|
|
@@ -3612,25 +4458,35 @@ export {
|
|
|
3612
4458
|
paletteTools,
|
|
3613
4459
|
parseBrandDiscussionReference,
|
|
3614
4460
|
parseBrandDispatch,
|
|
4461
|
+
parseDesignBundle,
|
|
3615
4462
|
parseHex,
|
|
3616
4463
|
pickTextOn,
|
|
4464
|
+
proposalAsWritten,
|
|
3617
4465
|
proposalReviewSnapshot,
|
|
3618
4466
|
proposePaletteOps,
|
|
3619
4467
|
proposeSectionOps,
|
|
4468
|
+
readDesignManifest,
|
|
3620
4469
|
readTools,
|
|
4470
|
+
receiptAsWritten,
|
|
3621
4471
|
recordAnalysisOps,
|
|
3622
4472
|
rejectProposalOps,
|
|
3623
4473
|
relativeLuminance,
|
|
3624
4474
|
renderTokensCss,
|
|
3625
4475
|
resolveDeps,
|
|
4476
|
+
resolveVarRefs,
|
|
3626
4477
|
rgbToHsl,
|
|
3627
4478
|
rgbToOklab,
|
|
3628
4479
|
rotateHue,
|
|
4480
|
+
rowAsWritten,
|
|
3629
4481
|
safeFileName,
|
|
4482
|
+
scanCustomProperties,
|
|
3630
4483
|
sectionKey,
|
|
3631
4484
|
splitComplementary,
|
|
3632
4485
|
srgbToLinear,
|
|
4486
|
+
stripCssComments,
|
|
4487
|
+
styleSheetText,
|
|
3633
4488
|
supportsBrandVision,
|
|
4489
|
+
swatchesFromDesignTokens,
|
|
3634
4490
|
tetradic,
|
|
3635
4491
|
tintShadeRamp,
|
|
3636
4492
|
toHex,
|