@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.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,35 @@ __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,
|
|
156
|
+
proposalAsWritten: () => proposalAsWritten,
|
|
128
157
|
proposalReviewSnapshot: () => proposalReviewSnapshot,
|
|
129
158
|
proposePaletteOps: () => proposePaletteOps,
|
|
130
159
|
proposeSectionOps: () => proposeSectionOps,
|
|
160
|
+
readDesignManifest: () => readDesignManifest,
|
|
131
161
|
readTools: () => readTools,
|
|
162
|
+
receiptAsWritten: () => receiptAsWritten,
|
|
132
163
|
recordAnalysisOps: () => recordAnalysisOps,
|
|
133
164
|
rejectProposalOps: () => rejectProposalOps,
|
|
134
165
|
relativeLuminance: () => relativeLuminance,
|
|
135
166
|
renderTokensCss: () => renderTokensCss,
|
|
136
167
|
resolveDeps: () => resolveDeps,
|
|
168
|
+
resolveVarRefs: () => resolveVarRefs,
|
|
137
169
|
rgbToHsl: () => rgbToHsl,
|
|
138
170
|
rgbToOklab: () => rgbToOklab,
|
|
139
171
|
rotateHue: () => rotateHue,
|
|
172
|
+
rowAsWritten: () => rowAsWritten,
|
|
140
173
|
safeFileName: () => safeFileName,
|
|
174
|
+
scanCustomProperties: () => scanCustomProperties,
|
|
141
175
|
sectionKey: () => sectionKey,
|
|
142
176
|
splitComplementary: () => splitComplementary,
|
|
143
177
|
srgbToLinear: () => srgbToLinear,
|
|
178
|
+
stripCssComments: () => stripCssComments,
|
|
179
|
+
styleSheetText: () => styleSheetText,
|
|
144
180
|
supportsBrandVision: () => supportsBrandVision,
|
|
181
|
+
swatchesFromDesignTokens: () => swatchesFromDesignTokens,
|
|
145
182
|
tetradic: () => tetradic,
|
|
146
183
|
tintShadeRamp: () => tintShadeRamp,
|
|
147
184
|
toHex: () => toHex,
|
|
@@ -168,7 +205,15 @@ var PALETTE_STATUSES = ["active", "archived"];
|
|
|
168
205
|
var PALETTE_SOURCES = ["extracted", "derived", "manual"];
|
|
169
206
|
var PROPOSAL_KINDS = SECTION_KINDS;
|
|
170
207
|
var PROPOSAL_STATUSES = ["open", "accepted", "rejected", "superseded"];
|
|
171
|
-
var ASSET_KINDS = [
|
|
208
|
+
var ASSET_KINDS = [
|
|
209
|
+
"logo",
|
|
210
|
+
"wordmark",
|
|
211
|
+
"inspiration",
|
|
212
|
+
"document",
|
|
213
|
+
"design",
|
|
214
|
+
"other"
|
|
215
|
+
];
|
|
216
|
+
var DESIGN_ASSET_KIND = "design";
|
|
172
217
|
var SWATCH_ROLES = [
|
|
173
218
|
"primary",
|
|
174
219
|
"secondary",
|
|
@@ -392,7 +437,7 @@ var BRAND_SCHEMA = {
|
|
|
392
437
|
id: uniq("string"),
|
|
393
438
|
bookId: idx("string"),
|
|
394
439
|
kind: idx("string"),
|
|
395
|
-
// logo | wordmark | inspiration | document | other
|
|
440
|
+
// logo | wordmark | inspiration | document | design | other
|
|
396
441
|
path: idx("string"),
|
|
397
442
|
storageObjectId: idx("string"),
|
|
398
443
|
contentDigest: idx("string"),
|
|
@@ -404,6 +449,11 @@ var BRAND_SCHEMA = {
|
|
|
404
449
|
analysis: opt("json"),
|
|
405
450
|
// { description, dominantColors: hex[], tags }
|
|
406
451
|
analyzedAt: opt("date"),
|
|
452
|
+
// `design` kind only: the DesignDigest computed from the uploaded
|
|
453
|
+
// bundle at upload time. Machine-derived and deterministic — unlike
|
|
454
|
+
// `analysis`, which is what a model reports seeing — so it needs no
|
|
455
|
+
// review gate and is rewritten only by re-uploading.
|
|
456
|
+
design: opt("json"),
|
|
407
457
|
analysisRevision: a("number"),
|
|
408
458
|
analysisDigest: opt("string"),
|
|
409
459
|
analyzedBy: opt("string"),
|
|
@@ -498,6 +548,7 @@ var ASSET_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
|
498
548
|
"image/webp",
|
|
499
549
|
"application/pdf"
|
|
500
550
|
]);
|
|
551
|
+
var DESIGN_CONTENT_TYPES = /* @__PURE__ */ new Set(["text/html"]);
|
|
501
552
|
var HEX_RGB = /^#[0-9a-f]{3}$/;
|
|
502
553
|
var HEX_RRGGBB = /^#[0-9a-f]{6}$/;
|
|
503
554
|
var HEX_ALPHA = /^#[0-9a-f]{4}$|^#[0-9a-f]{8}$/;
|
|
@@ -619,12 +670,13 @@ function safeFileName(name) {
|
|
|
619
670
|
throw new BrandInputError("file name is empty or a dot segment after sanitizing");
|
|
620
671
|
return cleaned.slice(0, 120);
|
|
621
672
|
}
|
|
622
|
-
function assertAssetContentType(value) {
|
|
673
|
+
function assertAssetContentType(value, kind) {
|
|
623
674
|
if (typeof value !== "string") throw new BrandInputError("contentType must be a string");
|
|
624
675
|
const ct = value.split(";")[0].trim().toLowerCase();
|
|
625
|
-
|
|
676
|
+
const allowed = kind === DESIGN_ASSET_KIND ? DESIGN_CONTENT_TYPES : ASSET_CONTENT_TYPES;
|
|
677
|
+
if (!allowed.has(ct))
|
|
626
678
|
throw new BrandInputError(
|
|
627
|
-
`unsupported content type ${ct || "(empty)"}; allowed: ${[...
|
|
679
|
+
`unsupported content type ${ct || "(empty)"} for kind ${kind ?? "other"}; allowed: ${[...allowed].join(", ")}`
|
|
628
680
|
);
|
|
629
681
|
return ct;
|
|
630
682
|
}
|
|
@@ -641,8 +693,40 @@ function assertAnalysis(value) {
|
|
|
641
693
|
};
|
|
642
694
|
}
|
|
643
695
|
|
|
696
|
+
// src/read-shape.ts
|
|
697
|
+
var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
698
|
+
var DATE_FIELDS = /* @__PURE__ */ new Map();
|
|
699
|
+
for (const [ns, entity] of Object.entries(BRAND_SCHEMA.entities))
|
|
700
|
+
DATE_FIELDS.set(
|
|
701
|
+
ns,
|
|
702
|
+
new Set(
|
|
703
|
+
Object.entries(entity.attrs).filter(([, attr]) => attr.type === "date").map(([label]) => label)
|
|
704
|
+
)
|
|
705
|
+
);
|
|
706
|
+
function rowAsWritten(ns, row) {
|
|
707
|
+
const dates = DATE_FIELDS.get(ns);
|
|
708
|
+
if (!dates?.size || !record(row)) return row;
|
|
709
|
+
let changed = false;
|
|
710
|
+
const out = { ...row };
|
|
711
|
+
for (const field of dates) {
|
|
712
|
+
const value = out[field];
|
|
713
|
+
if (typeof value !== "string") continue;
|
|
714
|
+
const parsed = Date.parse(value);
|
|
715
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) continue;
|
|
716
|
+
out[field] = parsed;
|
|
717
|
+
changed = true;
|
|
718
|
+
}
|
|
719
|
+
return changed ? out : row;
|
|
720
|
+
}
|
|
721
|
+
function receiptAsWritten(row) {
|
|
722
|
+
return rowAsWritten(BRAND_NS.approvalReceipt, row);
|
|
723
|
+
}
|
|
724
|
+
function proposalAsWritten(row) {
|
|
725
|
+
return rowAsWritten(BRAND_NS.proposal, row);
|
|
726
|
+
}
|
|
727
|
+
|
|
644
728
|
// src/review-json.ts
|
|
645
|
-
var
|
|
729
|
+
var record2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
646
730
|
function isBoundedBrandJson(root, limits = {}) {
|
|
647
731
|
const maxDepth = limits.maxDepth ?? 12;
|
|
648
732
|
const maxNodes = limits.maxNodes ?? 2048;
|
|
@@ -666,7 +750,7 @@ function isBoundedBrandJson(root, limits = {}) {
|
|
|
666
750
|
if (Array.isArray(value)) {
|
|
667
751
|
for (const child of value)
|
|
668
752
|
stack.push({ value: child, depth: depth + 1 });
|
|
669
|
-
} else if (
|
|
753
|
+
} else if (record2(value)) {
|
|
670
754
|
for (const child of Object.values(value))
|
|
671
755
|
stack.push({ value: child, depth: depth + 1 });
|
|
672
756
|
} else {
|
|
@@ -703,7 +787,7 @@ async function brandJsonDigest(value) {
|
|
|
703
787
|
|
|
704
788
|
// src/review.ts
|
|
705
789
|
var DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
706
|
-
var
|
|
790
|
+
var record3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
707
791
|
var exactKeys = (value, required, optional = []) => {
|
|
708
792
|
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
709
793
|
return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => allowed.has(key));
|
|
@@ -728,15 +812,15 @@ var SNAPSHOT_REQUIRED = [
|
|
|
728
812
|
"createdAt"
|
|
729
813
|
];
|
|
730
814
|
function validSnapshotShape(value) {
|
|
731
|
-
if (!
|
|
815
|
+
if (!record3(value) || !exactKeys(value, SNAPSHOT_REQUIRED)) return false;
|
|
732
816
|
const provenance = value.provenance;
|
|
733
|
-
return boundedString(value.id) && boundedString(value.bookId) && typeof value.kind === "string" && PROPOSAL_KINDS.includes(value.kind) && value.status === "open" &&
|
|
817
|
+
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, [
|
|
734
818
|
"sourceAssetId",
|
|
735
819
|
"sourceAsset",
|
|
736
820
|
"messageId",
|
|
737
821
|
"turnId",
|
|
738
822
|
"taintLabels"
|
|
739
|
-
]) && (provenance.sourceAssetId === null || boundedString(provenance.sourceAssetId)) && (provenance.sourceAsset === null ||
|
|
823
|
+
]) && (provenance.sourceAssetId === null || boundedString(provenance.sourceAssetId)) && (provenance.sourceAsset === null || record3(provenance.sourceAsset) && exactKeys(provenance.sourceAsset, [
|
|
740
824
|
"assetId",
|
|
741
825
|
"contentDigest",
|
|
742
826
|
"objectEtag",
|
|
@@ -769,8 +853,8 @@ var CONSUMPTION_REQUIRED = [
|
|
|
769
853
|
"consumedAt"
|
|
770
854
|
];
|
|
771
855
|
function isBrandHumanAuthorityConsumption(value) {
|
|
772
|
-
if (!
|
|
773
|
-
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) &&
|
|
856
|
+
if (!record3(value) || !exactKeys(value, CONSUMPTION_REQUIRED)) return false;
|
|
857
|
+
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);
|
|
774
858
|
}
|
|
775
859
|
var RECEIPT_REQUIRED = [
|
|
776
860
|
"version",
|
|
@@ -796,10 +880,10 @@ async function verifyBrandApprovalReceipt(receipt2) {
|
|
|
796
880
|
try {
|
|
797
881
|
if (!isBoundedBrandJson(receipt2, { maxDepth: 14, maxNodes: 2500, maxBytes: 48 * 1024 }))
|
|
798
882
|
return false;
|
|
799
|
-
if (!
|
|
883
|
+
if (!record3(receipt2) || !exactKeys(receipt2, RECEIPT_REQUIRED, ["paletteId", "resolutionNote"]))
|
|
800
884
|
return false;
|
|
801
885
|
const binding = receipt2.decisionBinding;
|
|
802
|
-
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) || !
|
|
886
|
+
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, [
|
|
803
887
|
"version",
|
|
804
888
|
"bookVersion",
|
|
805
889
|
"activationRevision",
|
|
@@ -1171,7 +1255,11 @@ function rejectProposalOps(input) {
|
|
|
1171
1255
|
function createAssetOps(input) {
|
|
1172
1256
|
if (!ASSET_KINDS.includes(input.kind))
|
|
1173
1257
|
throw new BrandInputError(`kind must be one of: ${ASSET_KINDS.join(", ")}`);
|
|
1174
|
-
const contentType = assertAssetContentType(input.contentType);
|
|
1258
|
+
const contentType = assertAssetContentType(input.contentType, input.kind);
|
|
1259
|
+
if (input.kind === DESIGN_ASSET_KIND && input.design === void 0)
|
|
1260
|
+
throw new BrandInputError("a design asset must carry its parsed digest");
|
|
1261
|
+
if (input.kind !== DESIGN_ASSET_KIND && input.design !== void 0)
|
|
1262
|
+
throw new BrandInputError(`only ${DESIGN_ASSET_KIND} assets may carry a design digest`);
|
|
1175
1263
|
if (typeof input.size !== "number" || !Number.isFinite(input.size) || input.size <= 0)
|
|
1176
1264
|
throw new BrandInputError("size must be a positive byte count");
|
|
1177
1265
|
if (!/^sha256:[0-9a-f]{64}$/.test(input.contentDigest))
|
|
@@ -1201,7 +1289,8 @@ function createAssetOps(input) {
|
|
|
1201
1289
|
200
|
|
1202
1290
|
),
|
|
1203
1291
|
createdAt: input.now,
|
|
1204
|
-
...title ? { title } : {}
|
|
1292
|
+
...title ? { title } : {},
|
|
1293
|
+
...input.design ? { design: input.design } : {}
|
|
1205
1294
|
}
|
|
1206
1295
|
},
|
|
1207
1296
|
{ t: "link", ns: BRAND_NS.asset, id: input.id, label: "book", target: input.bookId }
|
|
@@ -1253,6 +1342,180 @@ async function recordAnalysisOps(assetId, analysis, priorRevision, analyzedBy, a
|
|
|
1253
1342
|
];
|
|
1254
1343
|
}
|
|
1255
1344
|
|
|
1345
|
+
// src/design/bundle.ts
|
|
1346
|
+
var MAX_THUMBNAIL_CHARS = 16384;
|
|
1347
|
+
function island(html, name) {
|
|
1348
|
+
const open = `<script type="__bundler/${name}">`;
|
|
1349
|
+
const start = html.indexOf(open);
|
|
1350
|
+
if (start < 0) return null;
|
|
1351
|
+
const from = start + open.length;
|
|
1352
|
+
const end = html.indexOf("</script>", from);
|
|
1353
|
+
return end < 0 ? null : html.slice(from, end);
|
|
1354
|
+
}
|
|
1355
|
+
function isDesignBundle(html) {
|
|
1356
|
+
return html.includes('<script type="__bundler/manifest">') && html.includes('<script type="__bundler/template">');
|
|
1357
|
+
}
|
|
1358
|
+
function base64ByteLength(data) {
|
|
1359
|
+
const len = data.length;
|
|
1360
|
+
if (len === 0) return 0;
|
|
1361
|
+
const pad = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
1362
|
+
return Math.max(0, Math.floor(len * 3 / 4) - pad);
|
|
1363
|
+
}
|
|
1364
|
+
function parseIslandJson(text, name) {
|
|
1365
|
+
try {
|
|
1366
|
+
return JSON.parse(text);
|
|
1367
|
+
} catch {
|
|
1368
|
+
throw new BrandInputError(`design bundle's ${name} island is not valid JSON`);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1372
|
+
function readAssets(raw) {
|
|
1373
|
+
if (!isRecord2(raw)) throw new BrandInputError("design bundle's manifest island must be an object");
|
|
1374
|
+
const assets = [];
|
|
1375
|
+
for (const [uuid, value] of Object.entries(raw)) {
|
|
1376
|
+
if (!isRecord2(value)) continue;
|
|
1377
|
+
const data = value.data;
|
|
1378
|
+
const mime = value.mime;
|
|
1379
|
+
if (typeof data !== "string" || typeof mime !== "string") continue;
|
|
1380
|
+
assets.push({
|
|
1381
|
+
uuid,
|
|
1382
|
+
mime: mime.slice(0, 120),
|
|
1383
|
+
bytes: base64ByteLength(data),
|
|
1384
|
+
compressed: value.compressed === true
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
return assets;
|
|
1388
|
+
}
|
|
1389
|
+
function readExternals(raw) {
|
|
1390
|
+
if (raw === null || raw === void 0) return [];
|
|
1391
|
+
if (!Array.isArray(raw))
|
|
1392
|
+
throw new BrandInputError("design bundle's ext_resources island must be an array");
|
|
1393
|
+
const out = [];
|
|
1394
|
+
for (const entry of raw) {
|
|
1395
|
+
if (isRecord2(entry) && typeof entry.id === "string") out.push(entry.id.slice(0, 512));
|
|
1396
|
+
}
|
|
1397
|
+
return out;
|
|
1398
|
+
}
|
|
1399
|
+
function readPageOrder(raw) {
|
|
1400
|
+
if (raw === null || raw === void 0) return [];
|
|
1401
|
+
if (!Array.isArray(raw))
|
|
1402
|
+
throw new BrandInputError("design bundle's page_order island must be an array");
|
|
1403
|
+
return raw.filter((v) => typeof v === "string");
|
|
1404
|
+
}
|
|
1405
|
+
function extractThumbnailSvg(html) {
|
|
1406
|
+
const anchor = html.indexOf("__bundler_thumbnail");
|
|
1407
|
+
if (anchor < 0) return void 0;
|
|
1408
|
+
const open = html.indexOf("<svg", anchor);
|
|
1409
|
+
if (open < 0) return void 0;
|
|
1410
|
+
const close = html.indexOf("</svg>", open);
|
|
1411
|
+
if (close < 0) return void 0;
|
|
1412
|
+
const svg = html.slice(open, close + "</svg>".length);
|
|
1413
|
+
return svg.length > MAX_THUMBNAIL_CHARS ? void 0 : svg;
|
|
1414
|
+
}
|
|
1415
|
+
function readDesignManifest(html) {
|
|
1416
|
+
const raw = island(html, "manifest");
|
|
1417
|
+
if (raw === null) throw new BrandInputError("design bundle has no manifest island");
|
|
1418
|
+
const parsed = parseIslandJson(raw, "manifest");
|
|
1419
|
+
if (!isRecord2(parsed)) throw new BrandInputError("design bundle's manifest island must be an object");
|
|
1420
|
+
const out = {};
|
|
1421
|
+
for (const [uuid, value] of Object.entries(parsed)) {
|
|
1422
|
+
if (!isRecord2(value)) continue;
|
|
1423
|
+
const { data, mime } = value;
|
|
1424
|
+
if (typeof data !== "string" || typeof mime !== "string") continue;
|
|
1425
|
+
out[uuid] = { mime, compressed: value.compressed === true, data };
|
|
1426
|
+
}
|
|
1427
|
+
return out;
|
|
1428
|
+
}
|
|
1429
|
+
function parseDesignBundle(html) {
|
|
1430
|
+
if (!isDesignBundle(html))
|
|
1431
|
+
throw new BrandInputError(
|
|
1432
|
+
"not a Claude Design bundle: no __bundler/manifest and __bundler/template script islands. Export the design as standalone HTML and upload that file."
|
|
1433
|
+
);
|
|
1434
|
+
const templateRaw = island(html, "template");
|
|
1435
|
+
if (templateRaw === null)
|
|
1436
|
+
throw new BrandInputError("design bundle's template island is unterminated");
|
|
1437
|
+
const template = parseIslandJson(templateRaw, "template");
|
|
1438
|
+
if (typeof template !== "string")
|
|
1439
|
+
throw new BrandInputError("design bundle's template island must be a JSON string");
|
|
1440
|
+
const manifestRaw = island(html, "manifest");
|
|
1441
|
+
if (manifestRaw === null)
|
|
1442
|
+
throw new BrandInputError("design bundle's manifest island is unterminated");
|
|
1443
|
+
const extRaw = island(html, "ext_resources");
|
|
1444
|
+
const pageRaw = island(html, "page_order");
|
|
1445
|
+
const thumbnailSvg = extractThumbnailSvg(html);
|
|
1446
|
+
return {
|
|
1447
|
+
template,
|
|
1448
|
+
assets: readAssets(parseIslandJson(manifestRaw, "manifest")),
|
|
1449
|
+
externals: readExternals(extRaw === null ? null : parseIslandJson(extRaw, "ext_resources")),
|
|
1450
|
+
pageOrder: readPageOrder(pageRaw === null ? null : parseIslandJson(pageRaw, "page_order")),
|
|
1451
|
+
...thumbnailSvg ? { thumbnailSvg } : {}
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// src/design/css-scan.ts
|
|
1456
|
+
function stripCssComments(css) {
|
|
1457
|
+
let out = "";
|
|
1458
|
+
let i = 0;
|
|
1459
|
+
for (; ; ) {
|
|
1460
|
+
const start = css.indexOf("/*", i);
|
|
1461
|
+
if (start < 0) return out + css.slice(i);
|
|
1462
|
+
out += css.slice(i, start);
|
|
1463
|
+
const end = css.indexOf("*/", start + 2);
|
|
1464
|
+
if (end < 0) return out;
|
|
1465
|
+
i = end + 2;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
function styleSheetText(html) {
|
|
1469
|
+
const parts = [];
|
|
1470
|
+
let i = 0;
|
|
1471
|
+
for (; ; ) {
|
|
1472
|
+
const open = html.indexOf("<style", i);
|
|
1473
|
+
if (open < 0) break;
|
|
1474
|
+
const gt = html.indexOf(">", open);
|
|
1475
|
+
if (gt < 0) break;
|
|
1476
|
+
const close = html.indexOf("</style>", gt);
|
|
1477
|
+
if (close < 0) break;
|
|
1478
|
+
parts.push(html.slice(gt + 1, close));
|
|
1479
|
+
i = close + "</style>".length;
|
|
1480
|
+
}
|
|
1481
|
+
return parts.join("\n");
|
|
1482
|
+
}
|
|
1483
|
+
function scanCustomProperties(css) {
|
|
1484
|
+
const text = stripCssComments(css);
|
|
1485
|
+
const found = [];
|
|
1486
|
+
const stack = [];
|
|
1487
|
+
let paren = 0;
|
|
1488
|
+
let start = 0;
|
|
1489
|
+
const flush = (end) => {
|
|
1490
|
+
if (stack.length === 0) return;
|
|
1491
|
+
const chunk = text.slice(start, end).trim();
|
|
1492
|
+
if (!chunk.startsWith("--")) return;
|
|
1493
|
+
const colon = chunk.indexOf(":");
|
|
1494
|
+
if (colon < 0) return;
|
|
1495
|
+
const name = chunk.slice(0, colon).trim();
|
|
1496
|
+
if (name.length < 3) return;
|
|
1497
|
+
found.push({ selectors: [...stack], name, value: chunk.slice(colon + 1).trim() });
|
|
1498
|
+
};
|
|
1499
|
+
for (let i = 0; i < text.length; i++) {
|
|
1500
|
+
const ch = text[i];
|
|
1501
|
+
if (ch === "(") paren++;
|
|
1502
|
+
else if (ch === ")") paren = Math.max(0, paren - 1);
|
|
1503
|
+
if (paren !== 0) continue;
|
|
1504
|
+
if (ch === "{") {
|
|
1505
|
+
stack.push(text.slice(start, i).trim());
|
|
1506
|
+
start = i + 1;
|
|
1507
|
+
} else if (ch === "}") {
|
|
1508
|
+
flush(i);
|
|
1509
|
+
stack.pop();
|
|
1510
|
+
start = i + 1;
|
|
1511
|
+
} else if (ch === ";") {
|
|
1512
|
+
flush(i);
|
|
1513
|
+
start = i + 1;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
return found;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1256
1519
|
// src/color/hex.ts
|
|
1257
1520
|
function clamp01(x) {
|
|
1258
1521
|
return Number.isNaN(x) ? 0 : x < 0 ? 0 : x > 1 ? 1 : x;
|
|
@@ -1763,69 +2026,6 @@ function derivePalette(seedHex, opts = {}) {
|
|
|
1763
2026
|
return withNames ? swatches.map((s) => ({ ...s, name: nearestNamedColor(s.hex).name })) : swatches;
|
|
1764
2027
|
}
|
|
1765
2028
|
|
|
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
2029
|
// src/tokens/map.ts
|
|
1830
2030
|
var DEFAULT_ACCENT_SEED = "#3b5e8c";
|
|
1831
2031
|
var CHROMATIC_C = 0.02;
|
|
@@ -2025,6 +2225,452 @@ function mapPaletteToTokens(input) {
|
|
|
2025
2225
|
return { tokens, warnings };
|
|
2026
2226
|
}
|
|
2027
2227
|
|
|
2228
|
+
// src/design/decompile.ts
|
|
2229
|
+
var CHART_TOKENS = [
|
|
2230
|
+
"--ui-chart-1",
|
|
2231
|
+
"--ui-chart-2",
|
|
2232
|
+
"--ui-chart-3",
|
|
2233
|
+
"--ui-chart-4",
|
|
2234
|
+
"--ui-chart-5",
|
|
2235
|
+
"--ui-chart-6"
|
|
2236
|
+
];
|
|
2237
|
+
function cssColorToHex(value) {
|
|
2238
|
+
const text = value.trim();
|
|
2239
|
+
if (text.startsWith("#")) {
|
|
2240
|
+
try {
|
|
2241
|
+
return assertHex(text);
|
|
2242
|
+
} catch {
|
|
2243
|
+
return null;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
const fn = /^rgba?\(([^)]*)\)$/i.exec(text);
|
|
2247
|
+
if (!fn) return null;
|
|
2248
|
+
const parts = (fn[1] ?? "").split(/[,/\s]+/).filter((p) => p !== "");
|
|
2249
|
+
if (parts.length < 3 || parts.length > 4) return null;
|
|
2250
|
+
if (parts.length === 4) {
|
|
2251
|
+
const alpha = parts[3].endsWith("%") ? Number.parseFloat(parts[3]) / 100 : Number.parseFloat(parts[3]);
|
|
2252
|
+
if (!Number.isFinite(alpha) || alpha < 1) return null;
|
|
2253
|
+
}
|
|
2254
|
+
const channels = parts.slice(0, 3).map((part) => {
|
|
2255
|
+
const n = Number.parseFloat(part);
|
|
2256
|
+
if (!Number.isFinite(n)) return Number.NaN;
|
|
2257
|
+
return Math.round(part.endsWith("%") ? n / 100 * 255 : n);
|
|
2258
|
+
});
|
|
2259
|
+
if (channels.some((c) => !Number.isFinite(c) || c < 0 || c > 255)) return null;
|
|
2260
|
+
return `#${channels.map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
|
2261
|
+
}
|
|
2262
|
+
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";
|
|
2263
|
+
function swatchesFromDesignTokens(tokens) {
|
|
2264
|
+
const light = tokens.light;
|
|
2265
|
+
const swatches = [];
|
|
2266
|
+
const skipped = [];
|
|
2267
|
+
const missing2 = [];
|
|
2268
|
+
for (const [role, token] of Object.entries(ROLE_TOKEN)) {
|
|
2269
|
+
if (role === "chart") continue;
|
|
2270
|
+
const value = light[token];
|
|
2271
|
+
if (value === void 0) {
|
|
2272
|
+
missing2.push(role);
|
|
2273
|
+
continue;
|
|
2274
|
+
}
|
|
2275
|
+
const hex = cssColorToHex(value);
|
|
2276
|
+
if (hex === null) {
|
|
2277
|
+
skipped.push({ token, value, reason: skipReason(value) });
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
swatches.push({ role, hex, rationale: `declared by the design as ${token}` });
|
|
2281
|
+
}
|
|
2282
|
+
let anyChart = false;
|
|
2283
|
+
for (const token of CHART_TOKENS) {
|
|
2284
|
+
const value = light[token];
|
|
2285
|
+
if (value === void 0) continue;
|
|
2286
|
+
const hex = cssColorToHex(value);
|
|
2287
|
+
if (hex === null) {
|
|
2288
|
+
skipped.push({ token, value, reason: skipReason(value) });
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2291
|
+
anyChart = true;
|
|
2292
|
+
swatches.push({ role: "chart", hex, rationale: `declared by the design as ${token}` });
|
|
2293
|
+
}
|
|
2294
|
+
if (!anyChart) missing2.push("chart");
|
|
2295
|
+
return { swatches, skipped, missing: missing2 };
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
// src/design/html-text.ts
|
|
2299
|
+
var NAMED_ENTITIES = {
|
|
2300
|
+
amp: "&",
|
|
2301
|
+
lt: "<",
|
|
2302
|
+
gt: ">",
|
|
2303
|
+
quot: '"',
|
|
2304
|
+
apos: "'",
|
|
2305
|
+
nbsp: "\xA0",
|
|
2306
|
+
mdash: "\u2014",
|
|
2307
|
+
ndash: "\u2013",
|
|
2308
|
+
hellip: "\u2026",
|
|
2309
|
+
rsquo: "\u2019",
|
|
2310
|
+
lsquo: "\u2018",
|
|
2311
|
+
ldquo: "\u201C",
|
|
2312
|
+
rdquo: "\u201D"
|
|
2313
|
+
};
|
|
2314
|
+
function decodeEntities(text) {
|
|
2315
|
+
return text.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z][a-zA-Z0-9]{1,31});/g, (whole, body) => {
|
|
2316
|
+
if (body.startsWith("#x") || body.startsWith("#X")) {
|
|
2317
|
+
const code = Number.parseInt(body.slice(2), 16);
|
|
2318
|
+
return Number.isFinite(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
|
|
2319
|
+
}
|
|
2320
|
+
if (body.startsWith("#")) {
|
|
2321
|
+
const code = Number.parseInt(body.slice(1), 10);
|
|
2322
|
+
return Number.isFinite(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
|
|
2323
|
+
}
|
|
2324
|
+
return NAMED_ENTITIES[body.toLowerCase()] ?? whole;
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
var collapseWhitespace = (text) => text.replace(/\s+/g, " ").trim();
|
|
2328
|
+
function htmlToText(fragment) {
|
|
2329
|
+
const withoutCode = fragment.replace(/<script\b[^>]*>[\s\S]*?<\/script\s*>/gi, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi, " ");
|
|
2330
|
+
return collapseWhitespace(decodeEntities(withoutCode.replace(/<[^>]*>/g, " ")));
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// src/design/outline.ts
|
|
2334
|
+
var MAX_OUTLINE_ENTRIES = 120;
|
|
2335
|
+
var MAX_HEADING_CHARS = 200;
|
|
2336
|
+
function extractTitle(html) {
|
|
2337
|
+
const match = /<title\b[^>]*>([\s\S]{0,2000}?)<\/title\s*>/i.exec(html);
|
|
2338
|
+
if (!match) return void 0;
|
|
2339
|
+
const text = htmlToText(match[1] ?? "");
|
|
2340
|
+
return text === "" ? void 0 : text.slice(0, MAX_HEADING_CHARS);
|
|
2341
|
+
}
|
|
2342
|
+
function extractOutline(html) {
|
|
2343
|
+
const pattern = /<h([1-6])\b[^>]*>([\s\S]{0,4000}?)<\/h\1\s*>/gi;
|
|
2344
|
+
const entries = [];
|
|
2345
|
+
for (; ; ) {
|
|
2346
|
+
const match = pattern.exec(html);
|
|
2347
|
+
if (match === null) break;
|
|
2348
|
+
const text = htmlToText(match[2] ?? "");
|
|
2349
|
+
if (text === "") continue;
|
|
2350
|
+
if (entries.length >= MAX_OUTLINE_ENTRIES) return { entries, truncated: true };
|
|
2351
|
+
entries.push({ level: Number(match[1]), text: text.slice(0, MAX_HEADING_CHARS) });
|
|
2352
|
+
}
|
|
2353
|
+
return { entries, truncated: false };
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// src/design/props.ts
|
|
2357
|
+
var MAX_PROPS = 60;
|
|
2358
|
+
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2359
|
+
function attributeValue(html, attr) {
|
|
2360
|
+
const at = html.indexOf(`${attr}="`);
|
|
2361
|
+
if (at >= 0) {
|
|
2362
|
+
const from2 = at + attr.length + 2;
|
|
2363
|
+
const end2 = html.indexOf('"', from2);
|
|
2364
|
+
return end2 < 0 ? null : html.slice(from2, end2);
|
|
2365
|
+
}
|
|
2366
|
+
const single = html.indexOf(`${attr}='`);
|
|
2367
|
+
if (single < 0) return null;
|
|
2368
|
+
const from = single + attr.length + 2;
|
|
2369
|
+
const end = html.indexOf("'", from);
|
|
2370
|
+
return end < 0 ? null : html.slice(from, end);
|
|
2371
|
+
}
|
|
2372
|
+
function defaultText(value) {
|
|
2373
|
+
if (value === void 0 || value === null) return void 0;
|
|
2374
|
+
if (typeof value === "string") return value.slice(0, 400);
|
|
2375
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
2376
|
+
try {
|
|
2377
|
+
return JSON.stringify(value).slice(0, 400);
|
|
2378
|
+
} catch {
|
|
2379
|
+
return void 0;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
function toProp(name, spec) {
|
|
2383
|
+
const options = Array.isArray(spec.options) ? spec.options.filter((v) => typeof v === "string").slice(0, 24) : void 0;
|
|
2384
|
+
const declared = defaultText(spec.default);
|
|
2385
|
+
return {
|
|
2386
|
+
name: name.slice(0, 80),
|
|
2387
|
+
editor: typeof spec.editor === "string" ? spec.editor.slice(0, 40) : "unknown",
|
|
2388
|
+
...options && options.length > 0 ? { options } : {},
|
|
2389
|
+
...declared !== void 0 ? { default: declared } : {},
|
|
2390
|
+
...typeof spec.section === "string" ? { section: spec.section.slice(0, 80) } : {},
|
|
2391
|
+
...typeof spec.tsType === "string" ? { tsType: spec.tsType.slice(0, 200) } : {}
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
function extractProps(html) {
|
|
2395
|
+
const at = html.indexOf("data-props=");
|
|
2396
|
+
if (at < 0) return { props: [], truncated: false };
|
|
2397
|
+
const raw = attributeValue(html.slice(at), "data-props");
|
|
2398
|
+
if (raw === null) return { props: [], truncated: false };
|
|
2399
|
+
let parsed;
|
|
2400
|
+
try {
|
|
2401
|
+
parsed = JSON.parse(decodeEntities(raw));
|
|
2402
|
+
} catch {
|
|
2403
|
+
return { props: [], truncated: false };
|
|
2404
|
+
}
|
|
2405
|
+
if (!isRecord3(parsed)) return { props: [], truncated: false };
|
|
2406
|
+
const entries = Object.entries(parsed).filter(
|
|
2407
|
+
(entry) => isRecord3(entry[1])
|
|
2408
|
+
);
|
|
2409
|
+
return {
|
|
2410
|
+
props: entries.slice(0, MAX_PROPS).map(([name, spec]) => toProp(name, spec)),
|
|
2411
|
+
truncated: entries.length > MAX_PROPS
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
// src/design/styles.ts
|
|
2416
|
+
var MAX_FONTS = 16;
|
|
2417
|
+
var MAX_COLORS = 24;
|
|
2418
|
+
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
2419
|
+
"serif",
|
|
2420
|
+
"sans-serif",
|
|
2421
|
+
"monospace",
|
|
2422
|
+
"cursive",
|
|
2423
|
+
"fantasy",
|
|
2424
|
+
"system-ui",
|
|
2425
|
+
"ui-serif",
|
|
2426
|
+
"ui-sans-serif",
|
|
2427
|
+
"ui-monospace",
|
|
2428
|
+
"ui-rounded",
|
|
2429
|
+
"math",
|
|
2430
|
+
"emoji",
|
|
2431
|
+
"inherit",
|
|
2432
|
+
"initial",
|
|
2433
|
+
"revert",
|
|
2434
|
+
"unset",
|
|
2435
|
+
"currentcolor"
|
|
2436
|
+
]);
|
|
2437
|
+
var familyName = (raw) => raw.trim().replace(/^["']|["']$/g, "").trim();
|
|
2438
|
+
function fontFaceFamilies(css) {
|
|
2439
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2440
|
+
const blocks = /@font-face\s*\{([^}]{0,4000})\}/gi;
|
|
2441
|
+
for (; ; ) {
|
|
2442
|
+
const block = blocks.exec(css);
|
|
2443
|
+
if (block === null) break;
|
|
2444
|
+
const declared = /font-family\s*:\s*([^;]{1,200})/i.exec(block[1] ?? "");
|
|
2445
|
+
if (!declared) continue;
|
|
2446
|
+
const name = familyName(declared[1] ?? "");
|
|
2447
|
+
if (name !== "" && !GENERIC_FAMILIES.has(name.toLowerCase())) seen.add(name);
|
|
2448
|
+
}
|
|
2449
|
+
return [...seen];
|
|
2450
|
+
}
|
|
2451
|
+
function stackFamilies(css) {
|
|
2452
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2453
|
+
const stacks = /font-family\s*:\s*([^;{}]{1,400})/gi;
|
|
2454
|
+
for (; ; ) {
|
|
2455
|
+
const stack = stacks.exec(css);
|
|
2456
|
+
if (stack === null) break;
|
|
2457
|
+
for (const part of (stack[1] ?? "").split(",")) {
|
|
2458
|
+
if (!/["']/.test(part)) continue;
|
|
2459
|
+
const name = familyName(part);
|
|
2460
|
+
if (name === "" || GENERIC_FAMILIES.has(name.toLowerCase()) || name.includes("var(")) continue;
|
|
2461
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
return [...counts.entries()].sort((a2, b) => b[1] - a2[1] || (a2[0] < b[0] ? -1 : 1)).map(([name]) => name);
|
|
2465
|
+
}
|
|
2466
|
+
function extractFonts(templateHtml) {
|
|
2467
|
+
const css = stripCssComments(styleSheetText(templateHtml));
|
|
2468
|
+
const embedded = fontFaceFamilies(css);
|
|
2469
|
+
return (embedded.length > 0 ? embedded : stackFamilies(css)).slice(0, MAX_FONTS);
|
|
2470
|
+
}
|
|
2471
|
+
function extractColors(templateHtml) {
|
|
2472
|
+
const css = stripCssComments(styleSheetText(templateHtml));
|
|
2473
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2474
|
+
const hexes = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;
|
|
2475
|
+
for (; ; ) {
|
|
2476
|
+
const found = hexes.exec(css);
|
|
2477
|
+
if (found === null) break;
|
|
2478
|
+
const raw = (found[1] ?? "").toLowerCase();
|
|
2479
|
+
const hex = raw.length === 3 ? `#${raw[0]}${raw[0]}${raw[1]}${raw[1]}${raw[2]}${raw[2]}` : `#${raw}`;
|
|
2480
|
+
counts.set(hex, (counts.get(hex) ?? 0) + 1);
|
|
2481
|
+
}
|
|
2482
|
+
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 }));
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
// src/design/tokens.ts
|
|
2486
|
+
var MAX_VAR_DEPTH = 8;
|
|
2487
|
+
var DOC_SELECTOR = /(^|,)\s*(:root|html)\b|\[data-theme\s*=|\.ui-invert\b/;
|
|
2488
|
+
var DARK_SELECTOR = /data-theme\s*=\s*["']?dark|prefers-color-scheme\s*:\s*dark|\.ui-invert\b/;
|
|
2489
|
+
function splitVarArgs(inner) {
|
|
2490
|
+
let paren = 0;
|
|
2491
|
+
for (let i = 0; i < inner.length; i++) {
|
|
2492
|
+
const ch = inner[i];
|
|
2493
|
+
if (ch === "(") paren++;
|
|
2494
|
+
else if (ch === ")") paren--;
|
|
2495
|
+
else if (ch === "," && paren === 0)
|
|
2496
|
+
return { name: inner.slice(0, i).trim(), fallback: inner.slice(i + 1).trim() };
|
|
2497
|
+
}
|
|
2498
|
+
return { name: inner.trim() };
|
|
2499
|
+
}
|
|
2500
|
+
function resolveVarRefs(value, table, depth = 0) {
|
|
2501
|
+
if (depth >= MAX_VAR_DEPTH || !value.includes("var(")) return value;
|
|
2502
|
+
let out = "";
|
|
2503
|
+
let i = 0;
|
|
2504
|
+
for (; ; ) {
|
|
2505
|
+
const at = value.indexOf("var(", i);
|
|
2506
|
+
if (at < 0) return out + value.slice(i);
|
|
2507
|
+
out += value.slice(i, at);
|
|
2508
|
+
let paren = 1;
|
|
2509
|
+
let j = at + "var(".length;
|
|
2510
|
+
for (; j < value.length && paren > 0; j++) {
|
|
2511
|
+
if (value[j] === "(") paren++;
|
|
2512
|
+
else if (value[j] === ")") paren--;
|
|
2513
|
+
}
|
|
2514
|
+
if (paren > 0) return out + value.slice(at);
|
|
2515
|
+
const { name, fallback } = splitVarArgs(value.slice(at + "var(".length, j - 1));
|
|
2516
|
+
const referenced = table.get(name);
|
|
2517
|
+
const replacement = referenced !== void 0 ? resolveVarRefs(referenced, table, depth + 1) : fallback !== void 0 ? resolveVarRefs(fallback, table, depth + 1) : `var(${name})`;
|
|
2518
|
+
out += replacement;
|
|
2519
|
+
i = j;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
function foldDeclarations(html) {
|
|
2523
|
+
const light = /* @__PURE__ */ new Map();
|
|
2524
|
+
const darkOverrides = [];
|
|
2525
|
+
for (const decl of scanCustomProperties(styleSheetText(html))) {
|
|
2526
|
+
const innermost = decl.selectors[decl.selectors.length - 1] ?? "";
|
|
2527
|
+
if (!DOC_SELECTOR.test(innermost)) continue;
|
|
2528
|
+
if (decl.selectors.some((sel) => DARK_SELECTOR.test(sel))) darkOverrides.push([decl.name, decl.value]);
|
|
2529
|
+
else light.set(decl.name, decl.value);
|
|
2530
|
+
}
|
|
2531
|
+
const dark = new Map(light);
|
|
2532
|
+
for (const [name, value] of darkOverrides) dark.set(name, value);
|
|
2533
|
+
return { light, dark };
|
|
2534
|
+
}
|
|
2535
|
+
function resolveUiTokens(table) {
|
|
2536
|
+
const out = {};
|
|
2537
|
+
for (const [name, value] of table) {
|
|
2538
|
+
if (!name.startsWith("--ui-")) continue;
|
|
2539
|
+
out[name] = resolveVarRefs(value, table);
|
|
2540
|
+
}
|
|
2541
|
+
return out;
|
|
2542
|
+
}
|
|
2543
|
+
function extractDesignTokens(templateHtml) {
|
|
2544
|
+
const { light, dark } = foldDeclarations(templateHtml);
|
|
2545
|
+
return { light: resolveUiTokens(light), dark: resolveUiTokens(dark) };
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
// src/design/digest.ts
|
|
2549
|
+
var MAX_EXTERNALS = 24;
|
|
2550
|
+
function groupAssets(bundle) {
|
|
2551
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2552
|
+
for (const asset of bundle.assets) {
|
|
2553
|
+
const group = groups.get(asset.mime) ?? { mime: asset.mime, count: 0, bytes: 0 };
|
|
2554
|
+
group.count += 1;
|
|
2555
|
+
group.bytes += asset.bytes;
|
|
2556
|
+
groups.set(asset.mime, group);
|
|
2557
|
+
}
|
|
2558
|
+
return [...groups.values()].sort((a2, b) => b.bytes - a2.bytes || (a2.mime < b.mime ? -1 : 1));
|
|
2559
|
+
}
|
|
2560
|
+
function digestDesignBundle(bundle) {
|
|
2561
|
+
const template = bundle.template;
|
|
2562
|
+
const outline = extractOutline(template);
|
|
2563
|
+
const props = extractProps(template);
|
|
2564
|
+
const assetGroups = groupAssets(bundle);
|
|
2565
|
+
const title = extractTitle(template);
|
|
2566
|
+
const truncated = [];
|
|
2567
|
+
if (outline.truncated) truncated.push("outline");
|
|
2568
|
+
if (props.truncated) truncated.push("props");
|
|
2569
|
+
if (bundle.externals.length > MAX_EXTERNALS) truncated.push("externals");
|
|
2570
|
+
return {
|
|
2571
|
+
format: "claude-design-bundle/1",
|
|
2572
|
+
...title ? { title } : {},
|
|
2573
|
+
templateBytes: new TextEncoder().encode(template).byteLength,
|
|
2574
|
+
assetBytes: bundle.assets.reduce((total, asset) => total + asset.bytes, 0),
|
|
2575
|
+
assetCount: bundle.assets.length,
|
|
2576
|
+
assetGroups,
|
|
2577
|
+
externals: bundle.externals.slice(0, MAX_EXTERNALS),
|
|
2578
|
+
pageCount: bundle.pageOrder.length,
|
|
2579
|
+
tokens: extractDesignTokens(template),
|
|
2580
|
+
fonts: extractFonts(template),
|
|
2581
|
+
colors: extractColors(template),
|
|
2582
|
+
props: props.props,
|
|
2583
|
+
outline: outline.entries,
|
|
2584
|
+
...bundle.thumbnailSvg ? { thumbnailSvg: bundle.thumbnailSvg } : {},
|
|
2585
|
+
truncated
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
function digestDesignHtml(html) {
|
|
2589
|
+
return digestDesignBundle(parseDesignBundle(html));
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
// src/palette-report.ts
|
|
2593
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
2594
|
+
function contrastReport(swatches) {
|
|
2595
|
+
const byRole = (role) => swatches.find((s) => s.role === role)?.hex;
|
|
2596
|
+
const bg = byRole("bg") ?? "#ffffff";
|
|
2597
|
+
const report = {};
|
|
2598
|
+
const put = (label, fg) => {
|
|
2599
|
+
if (fg) report[label] = round2(contrastRatio(fg, bg));
|
|
2600
|
+
};
|
|
2601
|
+
put("text-on-bg", byRole("text"));
|
|
2602
|
+
put("primary-on-bg", byRole("primary"));
|
|
2603
|
+
put("good-on-bg", byRole("good"));
|
|
2604
|
+
put("warn-on-bg", byRole("warn"));
|
|
2605
|
+
put("danger-on-bg", byRole("danger"));
|
|
2606
|
+
const primary = byRole("primary");
|
|
2607
|
+
if (primary) report["text-on-primary"] = round2(contrastRatio(pickTextOn(primary), primary));
|
|
2608
|
+
return report;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// src/tokens/roles.ts
|
|
2612
|
+
var BRAND_REQUIRED_TOKENS = [
|
|
2613
|
+
"--ui-bg",
|
|
2614
|
+
"--ui-surface",
|
|
2615
|
+
"--ui-surface-2",
|
|
2616
|
+
"--ui-text",
|
|
2617
|
+
"--ui-text-muted",
|
|
2618
|
+
"--ui-text-faint",
|
|
2619
|
+
"--ui-border",
|
|
2620
|
+
"--ui-border-strong",
|
|
2621
|
+
"--ui-accent",
|
|
2622
|
+
"--ui-accent-strong",
|
|
2623
|
+
"--ui-accent-soft",
|
|
2624
|
+
"--ui-on-accent",
|
|
2625
|
+
"--ui-good",
|
|
2626
|
+
"--ui-good-soft",
|
|
2627
|
+
"--ui-warn",
|
|
2628
|
+
"--ui-warn-soft",
|
|
2629
|
+
"--ui-danger",
|
|
2630
|
+
"--ui-danger-soft",
|
|
2631
|
+
"--ui-code-bg",
|
|
2632
|
+
"--ui-code-text",
|
|
2633
|
+
"--ui-shadow",
|
|
2634
|
+
"--ui-font-sans",
|
|
2635
|
+
"--ui-font-serif",
|
|
2636
|
+
"--ui-font-mono",
|
|
2637
|
+
"--ui-font-display"
|
|
2638
|
+
];
|
|
2639
|
+
var BRAND_DERIVED_TOKENS = [
|
|
2640
|
+
"--ui-accent-glow",
|
|
2641
|
+
"--ui-accent-2",
|
|
2642
|
+
"--ui-accent-2-soft",
|
|
2643
|
+
"--ui-highlight",
|
|
2644
|
+
"--ui-focus",
|
|
2645
|
+
"--ui-shadow-strong"
|
|
2646
|
+
];
|
|
2647
|
+
var BRAND_CHART_TOKENS = [
|
|
2648
|
+
"--ui-chart-1",
|
|
2649
|
+
"--ui-chart-2",
|
|
2650
|
+
"--ui-chart-3",
|
|
2651
|
+
"--ui-chart-4",
|
|
2652
|
+
"--ui-chart-5",
|
|
2653
|
+
"--ui-chart-6",
|
|
2654
|
+
"--ui-chart-band",
|
|
2655
|
+
"--ui-chart-band-strong",
|
|
2656
|
+
"--ui-chart-flow",
|
|
2657
|
+
"--ui-chart-glow"
|
|
2658
|
+
];
|
|
2659
|
+
var BRAND_CHAT_TOKENS = [
|
|
2660
|
+
"--ui-chat-user-bg",
|
|
2661
|
+
"--ui-chat-user-text",
|
|
2662
|
+
"--ui-chat-assistant-bg",
|
|
2663
|
+
"--ui-chat-thinking-bg",
|
|
2664
|
+
"--ui-chat-thinking-text",
|
|
2665
|
+
"--ui-chat-tool-accent"
|
|
2666
|
+
];
|
|
2667
|
+
var BRAND_EMITTED_TOKENS = [
|
|
2668
|
+
...BRAND_REQUIRED_TOKENS,
|
|
2669
|
+
...BRAND_DERIVED_TOKENS,
|
|
2670
|
+
...BRAND_CHART_TOKENS,
|
|
2671
|
+
...BRAND_CHAT_TOKENS
|
|
2672
|
+
];
|
|
2673
|
+
|
|
2028
2674
|
// src/tokens/dark.ts
|
|
2029
2675
|
var DARK_FLIP_L_MIN = 0.2;
|
|
2030
2676
|
var DARK_FLIP_L_MAX = 0.95;
|
|
@@ -2182,10 +2828,10 @@ ${renderBlock(mediaSelector, dark, diffNames, " ")}
|
|
|
2182
2828
|
}
|
|
2183
2829
|
|
|
2184
2830
|
// src/tokens/compile.ts
|
|
2185
|
-
var
|
|
2831
|
+
var isRecord4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2186
2832
|
function applyOverrides(light, overrides, warnings) {
|
|
2187
2833
|
if (overrides === void 0) return;
|
|
2188
|
-
if (!
|
|
2834
|
+
if (!isRecord4(overrides)) {
|
|
2189
2835
|
warnings.push({ token: "--ui-accent", message: "overrides ignored: not an object" });
|
|
2190
2836
|
return;
|
|
2191
2837
|
}
|
|
@@ -2419,22 +3065,6 @@ function applyHarmony(swatches, seedHex, harmony) {
|
|
|
2419
3065
|
return { ...s, hex, name: nearestNamedColor(hex).name, rationale: `${harmony} companion of the seed` };
|
|
2420
3066
|
});
|
|
2421
3067
|
}
|
|
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
3068
|
var INCLUDE_SECTIONS = ["harmony", "ramp"];
|
|
2439
3069
|
function analyzeLines(hex, include) {
|
|
2440
3070
|
const lch = hexToOklch(hex);
|
|
@@ -2591,6 +3221,160 @@ function paletteTools(ctx) {
|
|
|
2591
3221
|
return [analyzeColor, evaluateContrast, proposePalette];
|
|
2592
3222
|
}
|
|
2593
3223
|
|
|
3224
|
+
// src/skill/design-tools.ts
|
|
3225
|
+
var MAX_SOURCE_WINDOW = 12e3;
|
|
3226
|
+
function describeDigest(asset, digest2) {
|
|
3227
|
+
const lines = [
|
|
3228
|
+
`Design ${asset.id}${digest2.title ? ` \u2014 "${digest2.title}"` : ""}${asset.title ? ` (uploaded as "${asset.title}")` : ""}`,
|
|
3229
|
+
`Template ${digest2.templateBytes} bytes; ${digest2.assetCount} embedded assets (${digest2.assetBytes} bytes); ${digest2.pageCount} nested pages.`
|
|
3230
|
+
];
|
|
3231
|
+
if (digest2.assetGroups.length > 0)
|
|
3232
|
+
lines.push(
|
|
3233
|
+
`Assets: ${digest2.assetGroups.map((g) => `${g.count}\xD7 ${g.mime} (${g.bytes} B)`).join(", ")}`
|
|
3234
|
+
);
|
|
3235
|
+
if (digest2.externals.length > 0) lines.push(`Built against: ${digest2.externals.join(", ")}`);
|
|
3236
|
+
if (digest2.fonts.length > 0) lines.push(`Typefaces: ${digest2.fonts.join(", ")}`);
|
|
3237
|
+
const tokenNames = Object.keys(digest2.tokens.light);
|
|
3238
|
+
lines.push(
|
|
3239
|
+
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."
|
|
3240
|
+
);
|
|
3241
|
+
if (digest2.colors.length > 0)
|
|
3242
|
+
lines.push(`Literal colors: ${digest2.colors.map((c) => `${c.hex}\xD7${c.count}`).join(", ")}`);
|
|
3243
|
+
if (digest2.props.length > 0)
|
|
3244
|
+
lines.push(
|
|
3245
|
+
"Configurable props:\n" + digest2.props.map(
|
|
3246
|
+
(p) => ` ${p.name} (${p.editor}${p.options ? `: ${p.options.join("|")}` : ""})${p.default === void 0 ? "" : ` default ${p.default}`}${p.section ? ` [${p.section}]` : ""}`
|
|
3247
|
+
).join("\n")
|
|
3248
|
+
);
|
|
3249
|
+
if (digest2.outline.length > 0)
|
|
3250
|
+
lines.push(
|
|
3251
|
+
"Outline:\n" + digest2.outline.map((h) => `${" ".repeat(h.level - 1)}h${h.level} ${h.text}`).join("\n")
|
|
3252
|
+
);
|
|
3253
|
+
if (digest2.truncated.length > 0)
|
|
3254
|
+
lines.push(`NOTE: truncated to fit digest caps: ${digest2.truncated.join(", ")}.`);
|
|
3255
|
+
return lines.join("\n");
|
|
3256
|
+
}
|
|
3257
|
+
function sourceWindow(template, input) {
|
|
3258
|
+
const length = Math.min(
|
|
3259
|
+
MAX_SOURCE_WINDOW,
|
|
3260
|
+
Math.max(1, typeof input.length === "number" ? input.length : MAX_SOURCE_WINDOW)
|
|
3261
|
+
);
|
|
3262
|
+
let start;
|
|
3263
|
+
if (input.find !== void 0 && input.find !== "") {
|
|
3264
|
+
const at = template.indexOf(input.find);
|
|
3265
|
+
if (at < 0) throw new BrandNotFoundError(`"${input.find}" in the design source`);
|
|
3266
|
+
start = Math.max(0, at - 400);
|
|
3267
|
+
} else {
|
|
3268
|
+
start = Math.max(0, Math.min(template.length, Math.trunc(input.offset ?? 0)));
|
|
3269
|
+
}
|
|
3270
|
+
const end = Math.min(template.length, start + length);
|
|
3271
|
+
return { text: template.slice(start, end), start, end };
|
|
3272
|
+
}
|
|
3273
|
+
function designTools(ctx) {
|
|
3274
|
+
const loadDesign = async (assetId) => {
|
|
3275
|
+
await ctx.authority("brand.read");
|
|
3276
|
+
const res = await ctx.db.query({
|
|
3277
|
+
[BRAND_NS.asset]: { $: { where: { id: assetId, bookId: ctx.bookId, status: "live" } } }
|
|
3278
|
+
});
|
|
3279
|
+
const row = (res[BRAND_NS.asset] ?? [])[0];
|
|
3280
|
+
if (!row || row.kind !== DESIGN_ASSET_KIND || !row.design)
|
|
3281
|
+
throw new BrandNotFoundError(`design asset ${assetId}`);
|
|
3282
|
+
return row;
|
|
3283
|
+
};
|
|
3284
|
+
const readDesign = {
|
|
3285
|
+
name: "read_design",
|
|
3286
|
+
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.",
|
|
3287
|
+
inputSchema: {
|
|
3288
|
+
type: "object",
|
|
3289
|
+
required: ["assetId"],
|
|
3290
|
+
properties: {
|
|
3291
|
+
assetId: { type: "string", description: "A design asset id from list_assets." }
|
|
3292
|
+
}
|
|
3293
|
+
},
|
|
3294
|
+
handler: ctx.guard(async (input) => {
|
|
3295
|
+
const asset = await loadDesign(capString(input.assetId, "assetId", 200));
|
|
3296
|
+
return { content: describeDigest(asset, asset.design) };
|
|
3297
|
+
})
|
|
3298
|
+
};
|
|
3299
|
+
const readDesignSource = {
|
|
3300
|
+
name: "read_design_source",
|
|
3301
|
+
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.",
|
|
3302
|
+
// The template is author-supplied content, not instructions.
|
|
3303
|
+
outputTaint: ["tool_untrusted:read_design_source"],
|
|
3304
|
+
inputSchema: {
|
|
3305
|
+
type: "object",
|
|
3306
|
+
required: ["assetId"],
|
|
3307
|
+
properties: {
|
|
3308
|
+
assetId: { type: "string" },
|
|
3309
|
+
find: { type: "string", description: "Jump to the first occurrence of this string." },
|
|
3310
|
+
offset: { type: "number", description: "Character offset to read from (ignored with find)." },
|
|
3311
|
+
length: { type: "number", description: `Characters to return (max ${MAX_SOURCE_WINDOW}).` }
|
|
3312
|
+
}
|
|
3313
|
+
},
|
|
3314
|
+
handler: ctx.guard(async (input) => {
|
|
3315
|
+
const asset = await loadDesign(capString(input.assetId, "assetId", 200));
|
|
3316
|
+
const template = await ctx.readDesignTemplate(asset.id);
|
|
3317
|
+
const found = sourceWindow(template, {
|
|
3318
|
+
...typeof input.find === "string" ? { find: input.find } : {},
|
|
3319
|
+
...typeof input.offset === "number" ? { offset: input.offset } : {},
|
|
3320
|
+
...typeof input.length === "number" ? { length: input.length } : {}
|
|
3321
|
+
});
|
|
3322
|
+
return {
|
|
3323
|
+
content: `Design ${asset.id} source, characters ${found.start}\u2013${found.end} of ${template.length}:
|
|
3324
|
+
` + found.text
|
|
3325
|
+
};
|
|
3326
|
+
})
|
|
3327
|
+
};
|
|
3328
|
+
const proposeFromDesign = {
|
|
3329
|
+
name: "propose_palette_from_design",
|
|
3330
|
+
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.",
|
|
3331
|
+
acceptsTaint: ["tool_untrusted:read_design_source"],
|
|
3332
|
+
inputSchema: {
|
|
3333
|
+
type: "object",
|
|
3334
|
+
required: ["assetId", "rationale"],
|
|
3335
|
+
properties: {
|
|
3336
|
+
assetId: { type: "string" },
|
|
3337
|
+
name: { type: "string", description: "Palette name; defaults to the design's title." },
|
|
3338
|
+
rationale: { type: "string", description: "Why this design's palette should become the brand's." }
|
|
3339
|
+
}
|
|
3340
|
+
},
|
|
3341
|
+
handler: ctx.guard(async (input) => {
|
|
3342
|
+
const book = await ctx.loadBook();
|
|
3343
|
+
await ctx.authority("brand.edit");
|
|
3344
|
+
const asset = await loadDesign(capString(input.assetId, "assetId", 200));
|
|
3345
|
+
const digest2 = asset.design;
|
|
3346
|
+
const rationale = capString(input.rationale, "rationale", 2e3);
|
|
3347
|
+
const name = capString(
|
|
3348
|
+
input.name ?? digest2.title ?? asset.title ?? "Design palette",
|
|
3349
|
+
"name",
|
|
3350
|
+
120
|
|
3351
|
+
);
|
|
3352
|
+
const { swatches, skipped, missing: missing2 } = swatchesFromDesignTokens(digest2.tokens);
|
|
3353
|
+
if (swatches.length === 0)
|
|
3354
|
+
throw new BrandInputError(
|
|
3355
|
+
`design ${asset.id} declares no --ui-* tokens that resolve to opaque colors; propose a palette explicitly instead.`
|
|
3356
|
+
);
|
|
3357
|
+
const report = contrastReport(swatches);
|
|
3358
|
+
const proposal = await ctx.createProposal({
|
|
3359
|
+
mutationId: ctx.newId(),
|
|
3360
|
+
kind: "palette",
|
|
3361
|
+
payload: { name, swatches, contrastReport: report, source: "design", designAssetId: asset.id },
|
|
3362
|
+
rationale,
|
|
3363
|
+
sourceAssetId: asset.id
|
|
3364
|
+
});
|
|
3365
|
+
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");
|
|
3366
|
+
const notes = [
|
|
3367
|
+
skipped.length > 0 ? `Not imported (not literal opaque colors): ${skipped.map((s) => `${s.token} \u2014 ${s.reason}`).join("; ")}.` : "",
|
|
3368
|
+
missing2.length > 0 ? `Roles the design declares no token for: ${missing2.join(", ")}.` : ""
|
|
3369
|
+
].filter((n) => n !== "");
|
|
3370
|
+
return {
|
|
3371
|
+
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()
|
|
3372
|
+
};
|
|
3373
|
+
})
|
|
3374
|
+
};
|
|
3375
|
+
return [readDesign, readDesignSource, proposeFromDesign];
|
|
3376
|
+
}
|
|
3377
|
+
|
|
2594
3378
|
// src/skill/read-tools.ts
|
|
2595
3379
|
var iso = (ms) => new Date(ms).toISOString();
|
|
2596
3380
|
var shortId = (id) => id.length <= 12 ? id : `${id.slice(0, 6)}\u2026${id.slice(-4)}`;
|
|
@@ -2706,10 +3490,11 @@ function readTools(ctx) {
|
|
|
2706
3490
|
}
|
|
2707
3491
|
|
|
2708
3492
|
// 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.\
|
|
3493
|
+
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
3494
|
function brandSkill(opts) {
|
|
2711
3495
|
if (opts.agentDbBinding.principalId !== opts.self.selfId || !opts.agentDbBinding.credentialRef) throw new BrandForbiddenError("brand db is not bound to the acting agent");
|
|
2712
3496
|
const deps = resolveDeps({ db: opts.db, now: opts.now, newId: opts.newId });
|
|
3497
|
+
const designTemplates = /* @__PURE__ */ new Map();
|
|
2713
3498
|
const authority = (capability) => Promise.resolve(opts.authorizeCapability({
|
|
2714
3499
|
agentId: opts.self.selfId,
|
|
2715
3500
|
bookId: opts.bookId,
|
|
@@ -2742,6 +3527,16 @@ function brandSkill(opts) {
|
|
|
2742
3527
|
bookId: opts.bookId,
|
|
2743
3528
|
assetId
|
|
2744
3529
|
}),
|
|
3530
|
+
readDesignTemplate: (assetId) => {
|
|
3531
|
+
const cached = designTemplates.get(assetId);
|
|
3532
|
+
if (cached) return cached;
|
|
3533
|
+
const pending = opts.agentBridge.readAssetContent({ jobId: opts.agentJobId, bookId: opts.bookId, assetId }).then(
|
|
3534
|
+
(fetched) => parseDesignBundle(new TextDecoder().decode(fetched.bytes)).template
|
|
3535
|
+
);
|
|
3536
|
+
pending.catch(() => designTemplates.delete(assetId));
|
|
3537
|
+
designTemplates.set(assetId, pending);
|
|
3538
|
+
return pending;
|
|
3539
|
+
},
|
|
2745
3540
|
resolvePrincipals: (principalIds) => {
|
|
2746
3541
|
const ids = [...new Set(principalIds)].slice(0, 100);
|
|
2747
3542
|
return opts.resolvePrincipals({
|
|
@@ -2773,7 +3568,13 @@ function brandSkill(opts) {
|
|
|
2773
3568
|
return {
|
|
2774
3569
|
name: "brand",
|
|
2775
3570
|
instructions: BRAND_INSTRUCTIONS,
|
|
2776
|
-
tools: [
|
|
3571
|
+
tools: [
|
|
3572
|
+
...readTools(ctx),
|
|
3573
|
+
...assetTools(ctx),
|
|
3574
|
+
...designTools(ctx),
|
|
3575
|
+
...paletteTools(ctx),
|
|
3576
|
+
...bookTools(ctx)
|
|
3577
|
+
]
|
|
2777
3578
|
};
|
|
2778
3579
|
}
|
|
2779
3580
|
|
|
@@ -2802,7 +3603,7 @@ async function linkedAsset(ctx, book, assetId, allowDeleting = false) {
|
|
|
2802
3603
|
book: {}
|
|
2803
3604
|
}
|
|
2804
3605
|
});
|
|
2805
|
-
const row = (result[BRAND_NS.asset] ?? [])[0];
|
|
3606
|
+
const row = rowAsWritten(BRAND_NS.asset, (result[BRAND_NS.asset] ?? [])[0]);
|
|
2806
3607
|
const links = row?.book;
|
|
2807
3608
|
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}`);
|
|
2808
3609
|
return row;
|
|
@@ -2876,7 +3677,7 @@ async function loadBook(db, bookId) {
|
|
|
2876
3677
|
const res = await db.query({ [BRAND_NS.book]: { $: { where: { id: bookId } } } });
|
|
2877
3678
|
const row = (res[BRAND_NS.book] ?? [])[0];
|
|
2878
3679
|
if (!row) throw new BrandNotFoundError(`brand book ${bookId}`);
|
|
2879
|
-
return row;
|
|
3680
|
+
return rowAsWritten(BRAND_NS.book, row);
|
|
2880
3681
|
}
|
|
2881
3682
|
async function loadMemberBook(db, bookId, actorId) {
|
|
2882
3683
|
const book = await loadBook(db, bookId);
|
|
@@ -2904,9 +3705,13 @@ async function uploadAsset(ctx, req, bookId) {
|
|
|
2904
3705
|
const file = form.get("file");
|
|
2905
3706
|
if (!(file instanceof File))
|
|
2906
3707
|
throw new BrandInputError('"file" must be an uploaded file field');
|
|
3708
|
+
const kindRaw = form.get("kind");
|
|
3709
|
+
const kind = typeof kindRaw === "string" && kindRaw ? kindRaw : "other";
|
|
3710
|
+
if (!ASSET_KINDS.includes(kind))
|
|
3711
|
+
throw new BrandInputError(`kind must be one of: ${ASSET_KINDS.join(", ")}`);
|
|
2907
3712
|
let contentType;
|
|
2908
3713
|
try {
|
|
2909
|
-
contentType = assertAssetContentType(file.type);
|
|
3714
|
+
contentType = assertAssetContentType(file.type, kind);
|
|
2910
3715
|
} catch (error) {
|
|
2911
3716
|
if (error instanceof BrandInputError)
|
|
2912
3717
|
return json({ error: error.message }, 415);
|
|
@@ -2914,10 +3719,7 @@ async function uploadAsset(ctx, req, bookId) {
|
|
|
2914
3719
|
}
|
|
2915
3720
|
if (file.size > ctx.maxUploadBytes)
|
|
2916
3721
|
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(", ")}`);
|
|
3722
|
+
const design = kind === DESIGN_ASSET_KIND ? digestDesignHtml(await file.text()) : void 0;
|
|
2921
3723
|
const titleRaw = form.get("title");
|
|
2922
3724
|
const title = typeof titleRaw === "string" && titleRaw ? capString(titleRaw, "title", 160) : void 0;
|
|
2923
3725
|
const fileName = safeFileName(file.name);
|
|
@@ -2930,14 +3732,14 @@ async function uploadAsset(ctx, req, bookId) {
|
|
|
2930
3732
|
"internal",
|
|
2931
3733
|
book.id
|
|
2932
3734
|
);
|
|
2933
|
-
const
|
|
3735
|
+
const record7 = await ctx.db.storage.upload(
|
|
2934
3736
|
path,
|
|
2935
3737
|
file,
|
|
2936
3738
|
contentType,
|
|
2937
3739
|
{ private: true }
|
|
2938
3740
|
);
|
|
2939
|
-
if (
|
|
2940
|
-
await ctx.db.storage.delete(
|
|
3741
|
+
if (record7.path !== path) {
|
|
3742
|
+
await ctx.db.storage.delete(record7.path);
|
|
2941
3743
|
throw new BrandConflictError(
|
|
2942
3744
|
"private storage returned an unexpected asset path"
|
|
2943
3745
|
);
|
|
@@ -2956,15 +3758,16 @@ async function uploadAsset(ctx, req, bookId) {
|
|
|
2956
3758
|
id,
|
|
2957
3759
|
bookId: book.id,
|
|
2958
3760
|
kind,
|
|
2959
|
-
path:
|
|
2960
|
-
storageObjectId:
|
|
3761
|
+
path: record7.path,
|
|
3762
|
+
storageObjectId: record7.id,
|
|
2961
3763
|
contentDigest: await contentDigest(file),
|
|
2962
3764
|
contentType,
|
|
2963
|
-
size:
|
|
3765
|
+
size: record7.size,
|
|
2964
3766
|
uploadedBy: ctx.actor.id,
|
|
2965
3767
|
uploadedAuthorityRef: authority.authorityRef,
|
|
2966
3768
|
audience: book.memberIds,
|
|
2967
3769
|
title,
|
|
3770
|
+
...design ? { design } : {},
|
|
2968
3771
|
now: ctx.now()
|
|
2969
3772
|
}), {
|
|
2970
3773
|
mutationId: `brand:asset-upload:v1:${id}`,
|
|
@@ -2986,7 +3789,7 @@ async function uploadAsset(ctx, req, bookId) {
|
|
|
2986
3789
|
asPrincipalKind: "human"
|
|
2987
3790
|
});
|
|
2988
3791
|
} catch (error) {
|
|
2989
|
-
await ctx.db.storage.delete(
|
|
3792
|
+
await ctx.db.storage.delete(record7.path);
|
|
2990
3793
|
throw error;
|
|
2991
3794
|
}
|
|
2992
3795
|
return json(await linkedAsset(ctx, book, id), 201);
|
|
@@ -3101,7 +3904,7 @@ async function handleAssetItem(ctx, req, bookId, assetId) {
|
|
|
3101
3904
|
async function handleBooksRoot(ctx, req) {
|
|
3102
3905
|
if (req.method === "GET") {
|
|
3103
3906
|
const res = await ctx.db.query({ [BRAND_NS.book]: { $: { order: { createdAt: "asc" } } } });
|
|
3104
|
-
const books = (res[BRAND_NS.book] ?? []).filter(
|
|
3907
|
+
const books = (res[BRAND_NS.book] ?? []).map((b) => rowAsWritten(BRAND_NS.book, b)).filter(
|
|
3105
3908
|
(b) => isMember(b, ctx.actor.id)
|
|
3106
3909
|
);
|
|
3107
3910
|
return json({ books });
|
|
@@ -3206,6 +4009,51 @@ async function handleBookItem(ctx, req, bookId) {
|
|
|
3206
4009
|
return json(await loadMemberBook(ctx.db, bookId, ctx.actor.id));
|
|
3207
4010
|
}
|
|
3208
4011
|
|
|
4012
|
+
// src/routes/design-preview.ts
|
|
4013
|
+
var SIGNED_READ_TTL_SECONDS = 60;
|
|
4014
|
+
function designPreviewHeaders(frameAncestors) {
|
|
4015
|
+
return {
|
|
4016
|
+
"content-type": "text/html; charset=utf-8",
|
|
4017
|
+
"content-security-policy": [
|
|
4018
|
+
"sandbox allow-scripts",
|
|
4019
|
+
`frame-ancestors ${frameAncestors.join(" ")}`
|
|
4020
|
+
].join("; "),
|
|
4021
|
+
"x-content-type-options": "nosniff",
|
|
4022
|
+
"referrer-policy": "no-referrer",
|
|
4023
|
+
"cache-control": "private, no-store"
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
async function handleDesignPreview(ctx, req, bookId, assetId) {
|
|
4027
|
+
if (req.method !== "GET") return methodNotAllowed();
|
|
4028
|
+
const book = await loadMemberBook(ctx.db, bookId, ctx.actor.id);
|
|
4029
|
+
const asset = await linkedAsset(ctx, book, assetId);
|
|
4030
|
+
if (asset.kind !== DESIGN_ASSET_KIND) throw new BrandNotFoundError(`design ${assetId}`);
|
|
4031
|
+
const signed = await ctx.db.storage.sign(asset.path, SIGNED_READ_TTL_SECONDS);
|
|
4032
|
+
const fetched = await ctx.fetchPrivateAsset(signed, {
|
|
4033
|
+
headers: { accept: "text/html" },
|
|
4034
|
+
redirect: "manual",
|
|
4035
|
+
signal: req.signal
|
|
4036
|
+
});
|
|
4037
|
+
if (!fetched.ok || fetched.type === "opaqueredirect")
|
|
4038
|
+
throw new BrandNotFoundError(`design ${assetId}`);
|
|
4039
|
+
return new Response(new Uint8Array(await fetched.arrayBuffer()), {
|
|
4040
|
+
status: 200,
|
|
4041
|
+
headers: designPreviewHeaders(ctx.previewFrameAncestors)
|
|
4042
|
+
});
|
|
4043
|
+
}
|
|
4044
|
+
async function handleDesignDigest(ctx, req, bookId, assetId) {
|
|
4045
|
+
if (req.method !== "GET") return methodNotAllowed();
|
|
4046
|
+
const book = await loadMemberBook(ctx.db, bookId, ctx.actor.id);
|
|
4047
|
+
const asset = await linkedAsset(ctx, book, assetId);
|
|
4048
|
+
if (asset.kind !== DESIGN_ASSET_KIND || !asset.design)
|
|
4049
|
+
throw new BrandNotFoundError(`design ${assetId}`);
|
|
4050
|
+
return json(
|
|
4051
|
+
{ assetId: asset.id, title: asset.title, digest: asset.design },
|
|
4052
|
+
200,
|
|
4053
|
+
{ "cache-control": "private, no-store" }
|
|
4054
|
+
);
|
|
4055
|
+
}
|
|
4056
|
+
|
|
3209
4057
|
// src/routes/proposal-input.ts
|
|
3210
4058
|
var SNAPSHOT_KEYS = [
|
|
3211
4059
|
"audience",
|
|
@@ -3221,7 +4069,7 @@ var SNAPSHOT_KEYS = [
|
|
|
3221
4069
|
"reviewDigest",
|
|
3222
4070
|
"status"
|
|
3223
4071
|
];
|
|
3224
|
-
var
|
|
4072
|
+
var record4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3225
4073
|
function parseProposalResolutionRequest(body, bookId, proposalId) {
|
|
3226
4074
|
const allowed = /* @__PURE__ */ new Set([
|
|
3227
4075
|
"mutationId",
|
|
@@ -3265,13 +4113,13 @@ function proposalReviewSnapshot(proposal) {
|
|
|
3265
4113
|
function parseReviewedProposal(value, bookId, proposalId) {
|
|
3266
4114
|
if (!isBoundedBrandJson(value, { maxDepth: 12, maxNodes: 1500, maxBytes: 32 * 1024 }))
|
|
3267
4115
|
throw new BrandInputError('"reviewedProposal" is too deeply nested, complex, or large');
|
|
3268
|
-
if (!
|
|
4116
|
+
if (!record4(value)) throw new BrandInputError('"reviewedProposal" must be an object');
|
|
3269
4117
|
const keys = Object.keys(value).sort();
|
|
3270
4118
|
if (keys.length !== SNAPSHOT_KEYS.length || !SNAPSHOT_KEYS.every((key, index) => key === keys[index])) throw new BrandInputError('"reviewedProposal" has an invalid shape');
|
|
3271
4119
|
if (value.id !== proposalId || value.bookId !== bookId || value.status !== "open")
|
|
3272
4120
|
throw new BrandInputError('"reviewedProposal" must identify this open proposal');
|
|
3273
|
-
if (typeof value.kind !== "string" || !PROPOSAL_KINDS.includes(value.kind) || !
|
|
3274
|
-
if (!
|
|
4121
|
+
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');
|
|
4122
|
+
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(
|
|
3275
4123
|
value.provenance.sourceAsset.analysisDigest
|
|
3276
4124
|
)) || 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');
|
|
3277
4125
|
return value;
|
|
@@ -3444,7 +4292,7 @@ async function proposalReceiptByMutation(ctx, mutationKey) {
|
|
|
3444
4292
|
const row = (result[BRAND_NS.approvalReceipt] ?? [])[0];
|
|
3445
4293
|
if (!row || !Array.isArray(row.book) || row.book.length !== 1 || row.book[0]?.id !== row.bookId) return void 0;
|
|
3446
4294
|
const { book: _book, ...receipt2 } = row;
|
|
3447
|
-
return receipt2;
|
|
4295
|
+
return receiptAsWritten(receipt2);
|
|
3448
4296
|
}
|
|
3449
4297
|
async function consumeProposalAuthority(ctx, req, bookId, proposalId, actionDigest, mutationKey) {
|
|
3450
4298
|
const authority = await ctx.consumeHumanExact({
|
|
@@ -3478,7 +4326,8 @@ async function receipt(ctx, id, actionDigest, expected) {
|
|
|
3478
4326
|
});
|
|
3479
4327
|
const row = (result[BRAND_NS.approvalReceipt] ?? [])[0];
|
|
3480
4328
|
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`);
|
|
3481
|
-
const { book: _book, ...
|
|
4329
|
+
const { book: _book, ...stored } = row;
|
|
4330
|
+
const unhydrated = receiptAsWritten(stored);
|
|
3482
4331
|
if (!await verifyBrandApprovalReceipt(unhydrated))
|
|
3483
4332
|
throw new BrandConflictError(`approval receipt ${id} is invalid`);
|
|
3484
4333
|
return unhydrated;
|
|
@@ -3716,9 +4565,9 @@ async function proposalDecisionState(ctx, req, book, proposal, resolution, recei
|
|
|
3716
4565
|
}
|
|
3717
4566
|
|
|
3718
4567
|
// src/routes/proposals.ts
|
|
3719
|
-
var
|
|
4568
|
+
var record5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3720
4569
|
var sameStrings2 = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
|
|
3721
|
-
var guardFailure2 = (error) =>
|
|
4570
|
+
var guardFailure2 = (error) => record5(error) && error.code === "transact_guard_failed";
|
|
3722
4571
|
async function handleProposalResolution(ctx, req, bookId, proposalId) {
|
|
3723
4572
|
if (req.method !== "POST") return methodNotAllowed();
|
|
3724
4573
|
const book = await loadMemberBook(ctx.db, bookId, ctx.actor.id);
|
|
@@ -3765,7 +4614,7 @@ async function handleProposalResolution(ctx, req, bookId, proposalId) {
|
|
|
3765
4614
|
book: {}
|
|
3766
4615
|
}
|
|
3767
4616
|
});
|
|
3768
|
-
const proposal = (result[BRAND_NS.proposal] ?? [])[0];
|
|
4617
|
+
const proposal = proposalAsWritten((result[BRAND_NS.proposal] ?? [])[0]);
|
|
3769
4618
|
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}`);
|
|
3770
4619
|
const current = proposalReviewSnapshot(proposal);
|
|
3771
4620
|
const { reviewDigest, ...unsignedReview } = current;
|
|
@@ -3898,7 +4747,7 @@ async function handleProposalResolution(ctx, req, bookId, proposalId) {
|
|
|
3898
4747
|
}
|
|
3899
4748
|
|
|
3900
4749
|
// src/routes/tokens.ts
|
|
3901
|
-
var
|
|
4750
|
+
var record6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3902
4751
|
var missing = (bookId) => {
|
|
3903
4752
|
throw new BrandNotFoundError(`compiled tokens for brand book ${bookId}`);
|
|
3904
4753
|
};
|
|
@@ -3911,13 +4760,14 @@ async function acceptedReceipt(db, bookId, receiptId, actionDigest) {
|
|
|
3911
4760
|
});
|
|
3912
4761
|
const receipt2 = (result[BRAND_NS.approvalReceipt] ?? [])[0];
|
|
3913
4762
|
if (!receipt2 || receipt2.resolution !== "accepted" || receipt2.actionDigest !== actionDigest || !Array.isArray(receipt2.book) || receipt2.book.length !== 1 || receipt2.book[0]?.id !== bookId) return missing(bookId);
|
|
3914
|
-
const { book: _book, ...
|
|
4763
|
+
const { book: _book, ...stored } = receipt2;
|
|
4764
|
+
const unhydrated = receiptAsWritten(stored);
|
|
3915
4765
|
if (!await verifyBrandApprovalReceipt(unhydrated)) return missing(bookId);
|
|
3916
4766
|
return unhydrated;
|
|
3917
4767
|
}
|
|
3918
4768
|
async function approvedCache(db, book) {
|
|
3919
4769
|
const cache = book.tokens;
|
|
3920
|
-
if (!cache || !
|
|
4770
|
+
if (!cache || !record6(cache.light) || !record6(cache.dark) || !book.activePaletteId || cache.paletteId !== book.activePaletteId || !cache.paletteReceiptId || !cache.paletteActionDigest || !cache.sourceDigest) return missing(book.id);
|
|
3921
4771
|
const paletteResult = await db.query({
|
|
3922
4772
|
[BRAND_NS.palette]: {
|
|
3923
4773
|
$: { where: { id: cache.paletteId, bookId: book.id } },
|
|
@@ -4276,6 +5126,10 @@ async function route(ctx, req, url, seg) {
|
|
|
4276
5126
|
if (seg.length === 4) return handleAssetItem(ctx, req, id, subId);
|
|
4277
5127
|
if (seg.length === 5 && action === "content")
|
|
4278
5128
|
return handleAssetContent(ctx, req, id, subId);
|
|
5129
|
+
if (seg.length === 5 && action === "preview")
|
|
5130
|
+
return handleDesignPreview(ctx, req, id, subId);
|
|
5131
|
+
if (seg.length === 5 && action === "design")
|
|
5132
|
+
return handleDesignDigest(ctx, req, id, subId);
|
|
4279
5133
|
return null;
|
|
4280
5134
|
}
|
|
4281
5135
|
if (sub === "proposals" && seg.length === 5 && action === "resolve")
|
|
@@ -4297,7 +5151,8 @@ function createBrandRoutes(options) {
|
|
|
4297
5151
|
newId: options.newId ?? (() => crypto.randomUUID()),
|
|
4298
5152
|
maxUploadBytes: options.maxUploadBytes ?? 8 * 1024 * 1024,
|
|
4299
5153
|
discussionBasePath: options.discussionBasePath ?? "/",
|
|
4300
|
-
fetchPrivateAsset: options.fetchPrivateAsset ?? fetch
|
|
5154
|
+
fetchPrivateAsset: options.fetchPrivateAsset ?? fetch,
|
|
5155
|
+
previewFrameAncestors: options.previewFrameAncestors ?? ["'self'"]
|
|
4301
5156
|
};
|
|
4302
5157
|
return async (req) => {
|
|
4303
5158
|
const url = new URL(req.url);
|