@odla-ai/brand 0.7.1 → 0.8.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 +1 -1
- package/dist/{index-r36KQK5n.d.cts → index-r6SGidYT.d.cts} +6 -1
- package/dist/{index-r36KQK5n.d.ts → index-r6SGidYT.d.ts} +6 -1
- package/dist/index.cjs +133 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.js +135 -5
- package/dist/index.js.map +1 -1
- package/dist/tokens/index.d.cts +1 -1
- package/dist/tokens/index.d.ts +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ The package is a **schema + rules + an agent skill + a color engine + a token co
|
|
|
51
51
|
- **The upload surface is intentionally narrow.** Raster PNG/JPEG/GIF/WebP and
|
|
52
52
|
PDF inputs are supported. SVG active content and binary font files are not;
|
|
53
53
|
typography remains an exact, reviewable family/style proposal.
|
|
54
|
-
- **Tokens compile to the @odla-ai/ui contract.** `compileBrandTokens` maps swatch roles onto `--ui-*` names — always emitting every required token plus the accent-composing derived set, so a scoped preview island never keeps stale root composites — and `renderTokensCss` emits theme-structured CSS (light, dark, invert). Dark is derived algorithmically and contrast re-tuned per token.
|
|
54
|
+
- **Tokens compile to the @odla-ai/ui contract.** `compileBrandTokens` maps swatch roles onto `--ui-*` names — always emitting every required token plus the accent-composing derived set, so a scoped preview island never keeps stale root composites — and `renderTokensCss` emits theme-structured CSS (light, dark, invert). Dark is derived algorithmically and contrast re-tuned per token. A reviewed typography proposal may also carry bounded `tokenOverrides`; those `--ui-*` component and layout values are validated, stored with the approved section, and compiled into both published token maps.
|
|
55
55
|
- **Zero runtime dependencies.** The db client is injected structurally (a real `@odla-ai/db` `AdminDb` satisfies `BrandDb`); `@odla-ai/ai` is a types-only optional peer.
|
|
56
56
|
|
|
57
57
|
- **Claude Designs are a first-class input.** A design exported from
|
|
@@ -66,13 +66,18 @@ interface PaletteSection {
|
|
|
66
66
|
name: string;
|
|
67
67
|
swatches: Swatch[];
|
|
68
68
|
}
|
|
69
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* `typography`-kind section content. `scale` is a modular type-scale ratio.
|
|
71
|
+
* `tokenOverrides` carries bounded `--ui-*` component/layout values through
|
|
72
|
+
* the same reviewed activation that publishes the font choices.
|
|
73
|
+
*/
|
|
70
74
|
interface TypographySection {
|
|
71
75
|
fontDisplay?: string;
|
|
72
76
|
fontBody?: string;
|
|
73
77
|
fontMono?: string;
|
|
74
78
|
scale?: number;
|
|
75
79
|
notes?: string;
|
|
80
|
+
tokenOverrides?: Record<string, string>;
|
|
76
81
|
}
|
|
77
82
|
/** `voice`-kind section content: tone plus writing principles/examples. */
|
|
78
83
|
interface VoiceSection {
|
|
@@ -66,13 +66,18 @@ interface PaletteSection {
|
|
|
66
66
|
name: string;
|
|
67
67
|
swatches: Swatch[];
|
|
68
68
|
}
|
|
69
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* `typography`-kind section content. `scale` is a modular type-scale ratio.
|
|
71
|
+
* `tokenOverrides` carries bounded `--ui-*` component/layout values through
|
|
72
|
+
* the same reviewed activation that publishes the font choices.
|
|
73
|
+
*/
|
|
70
74
|
interface TypographySection {
|
|
71
75
|
fontDisplay?: string;
|
|
72
76
|
fontBody?: string;
|
|
73
77
|
fontMono?: string;
|
|
74
78
|
scale?: number;
|
|
75
79
|
notes?: string;
|
|
80
|
+
tokenOverrides?: Record<string, string>;
|
|
76
81
|
}
|
|
77
82
|
/** `voice`-kind section content: tone plus writing principles/examples. */
|
|
78
83
|
interface VoiceSection {
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ __export(src_exports, {
|
|
|
31
31
|
BRAND_EMITTED_TOKENS: () => BRAND_EMITTED_TOKENS,
|
|
32
32
|
BRAND_INSTRUCTIONS: () => BRAND_INSTRUCTIONS,
|
|
33
33
|
BRAND_NS: () => BRAND_NS,
|
|
34
|
+
BRAND_PROMOTION_ADAPTER: () => BRAND_PROMOTION_ADAPTER,
|
|
34
35
|
BRAND_REQUIRED_TOKENS: () => BRAND_REQUIRED_TOKENS,
|
|
35
36
|
BRAND_RULES: () => BRAND_RULES,
|
|
36
37
|
BRAND_SCHEMA: () => BRAND_SCHEMA,
|
|
@@ -119,6 +120,7 @@ __export(src_exports, {
|
|
|
119
120
|
digestDesignBundle: () => digestDesignBundle,
|
|
120
121
|
digestDesignHtml: () => digestDesignHtml,
|
|
121
122
|
dispatchBrandTurn: () => dispatchBrandTurn,
|
|
123
|
+
exportBrandPromotion: () => exportBrandPromotion,
|
|
122
124
|
extractColors: () => extractColors,
|
|
123
125
|
extractDesignTokens: () => extractDesignTokens,
|
|
124
126
|
extractFonts: () => extractFonts,
|
|
@@ -598,6 +600,7 @@ var DESIGN_CONTENT_TYPES = /* @__PURE__ */ new Set(["text/html"]);
|
|
|
598
600
|
var HEX_RGB = /^#[0-9a-f]{3}$/;
|
|
599
601
|
var HEX_RRGGBB = /^#[0-9a-f]{6}$/;
|
|
600
602
|
var HEX_ALPHA = /^#[0-9a-f]{4}$|^#[0-9a-f]{8}$/;
|
|
603
|
+
var UI_TOKEN_NAME = /^--ui-[a-z0-9-]+$/;
|
|
601
604
|
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
602
605
|
function assertHex(value, label = "color") {
|
|
603
606
|
if (typeof value !== "string")
|
|
@@ -664,6 +667,21 @@ function assertTypographySection(c) {
|
|
|
664
667
|
out.scale = c.scale;
|
|
665
668
|
}
|
|
666
669
|
if (c.notes !== void 0) out.notes = capString(c.notes, "content.notes", 2e3);
|
|
670
|
+
if (c.tokenOverrides !== void 0) {
|
|
671
|
+
if (!isRecord(c.tokenOverrides))
|
|
672
|
+
throw new BrandInputError("content.tokenOverrides must be an object");
|
|
673
|
+
const entries = Object.entries(c.tokenOverrides);
|
|
674
|
+
if (entries.length > 64)
|
|
675
|
+
throw new BrandInputError("content.tokenOverrides must have at most 64 entries");
|
|
676
|
+
out.tokenOverrides = Object.fromEntries(entries.map(([name, raw]) => {
|
|
677
|
+
if (!UI_TOKEN_NAME.test(name))
|
|
678
|
+
throw new BrandInputError(`content.tokenOverrides.${name} must be a --ui-* token name`);
|
|
679
|
+
const value = capString(raw, `content.tokenOverrides.${name}`, 500);
|
|
680
|
+
if (/[;{}]/.test(value) || /url\s*\(/i.test(value))
|
|
681
|
+
throw new BrandInputError(`content.tokenOverrides.${name} contains an unsafe CSS value`);
|
|
682
|
+
return [name, value];
|
|
683
|
+
}));
|
|
684
|
+
}
|
|
667
685
|
return out;
|
|
668
686
|
}
|
|
669
687
|
function assertVoiceSection(c) {
|
|
@@ -827,14 +845,14 @@ var SNAPSHOT_REQUIRED = [
|
|
|
827
845
|
];
|
|
828
846
|
function validSnapshotShape(value) {
|
|
829
847
|
if (!record3(value) || !exactKeys(value, SNAPSHOT_REQUIRED)) return false;
|
|
830
|
-
const
|
|
831
|
-
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(
|
|
848
|
+
const provenance2 = value.provenance;
|
|
849
|
+
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(provenance2) && exactKeys(provenance2, [
|
|
832
850
|
"sourceAssetId",
|
|
833
851
|
"sourceAsset",
|
|
834
852
|
"messageId",
|
|
835
853
|
"turnId",
|
|
836
854
|
"taintLabels"
|
|
837
|
-
]) && (
|
|
855
|
+
]) && (provenance2.sourceAssetId === null || boundedString(provenance2.sourceAssetId)) && (provenance2.sourceAsset === null || record3(provenance2.sourceAsset) && exactKeys(provenance2.sourceAsset, [
|
|
838
856
|
"assetId",
|
|
839
857
|
"contentDigest",
|
|
840
858
|
"objectEtag",
|
|
@@ -843,7 +861,7 @@ function validSnapshotShape(value) {
|
|
|
843
861
|
"contentType",
|
|
844
862
|
"analysisRevision",
|
|
845
863
|
"analysisDigest"
|
|
846
|
-
]) && boundedString(
|
|
864
|
+
]) && boundedString(provenance2.sourceAsset.assetId) && typeof provenance2.sourceAsset.contentDigest === "string" && DIGEST.test(provenance2.sourceAsset.contentDigest) && boundedString(provenance2.sourceAsset.objectEtag) && Number.isSafeInteger(provenance2.sourceAsset.objectSize) && provenance2.sourceAsset.objectSize > 0 && typeof provenance2.sourceAsset.pathDigest === "string" && DIGEST.test(provenance2.sourceAsset.pathDigest) && (provenance2.sourceAsset.contentType === null || boundedString(provenance2.sourceAsset.contentType, 160)) && (provenance2.sourceAsset.analysisRevision === null || Number.isSafeInteger(provenance2.sourceAsset.analysisRevision) && provenance2.sourceAsset.analysisRevision >= 0) && (provenance2.sourceAsset.analysisDigest === null || typeof provenance2.sourceAsset.analysisDigest === "string" && DIGEST.test(provenance2.sourceAsset.analysisDigest))) && (provenance2.sourceAssetId === null && provenance2.sourceAsset === null || provenance2.sourceAssetId !== null && provenance2.sourceAsset !== null && provenance2.sourceAsset.assetId === provenance2.sourceAssetId) && (provenance2.messageId === null || boundedString(provenance2.messageId)) && (provenance2.turnId === null || boundedString(provenance2.turnId)) && Array.isArray(provenance2.taintLabels) && provenance2.taintLabels.length <= 16 && provenance2.taintLabels.every((label) => boundedString(label, 120)) && new Set(provenance2.taintLabels).size === provenance2.taintLabels.length && typeof value.reviewDigest === "string" && DIGEST.test(value.reviewDigest) && Array.isArray(value.audience) && value.audience.length > 0 && value.audience.length <= 100 && value.audience.every((id) => boundedString(id)) && new Set(value.audience).size === value.audience.length && boundedString(value.createdBy) && boundedString(value.createdAuthorityRef) && safeTime(value.createdAt);
|
|
847
865
|
}
|
|
848
866
|
var CONSUMPTION_REQUIRED = [
|
|
849
867
|
"id",
|
|
@@ -4143,7 +4161,10 @@ var proposalGuardEquals = (snapshot) => ({ ...snapshot });
|
|
|
4143
4161
|
|
|
4144
4162
|
// src/routes/proposal-effects.ts
|
|
4145
4163
|
async function tokenSnapshot(swatches, typography, input, paletteId, paletteReceiptId, paletteActionDigest, typographyReceiptId, typographyActionDigest) {
|
|
4146
|
-
const compiled = compileBrandTokens({
|
|
4164
|
+
const compiled = compileBrandTokens({
|
|
4165
|
+
swatches,
|
|
4166
|
+
...typography ? { typography, overrides: typography.tokenOverrides } : {}
|
|
4167
|
+
});
|
|
4147
4168
|
const sources = {
|
|
4148
4169
|
paletteId,
|
|
4149
4170
|
paletteReceiptId,
|
|
@@ -5436,4 +5457,111 @@ function createBrandIntegration(options = {}) {
|
|
|
5436
5457
|
probes: [{ path: `${basePath}/books`, expectedStatus: 401 }]
|
|
5437
5458
|
};
|
|
5438
5459
|
}
|
|
5460
|
+
|
|
5461
|
+
// src/promotion.ts
|
|
5462
|
+
var import_promotion = require("@odla-ai/promotion");
|
|
5463
|
+
var BRAND_PROMOTION_ADAPTER = "@odla-ai/brand/v1";
|
|
5464
|
+
var provenance = (sourceIds, receiptDigests = []) => ({
|
|
5465
|
+
adapter: BRAND_PROMOTION_ADAPTER,
|
|
5466
|
+
sourceIds: [...sourceIds].sort(),
|
|
5467
|
+
receiptDigests: [...receiptDigests].sort()
|
|
5468
|
+
});
|
|
5469
|
+
var exclusions = (book) => [
|
|
5470
|
+
{
|
|
5471
|
+
kind: "brand.runtime",
|
|
5472
|
+
id: book.slug,
|
|
5473
|
+
reason: "environment_local",
|
|
5474
|
+
paths: [`${BRAND_NS.book}.ownerId`, `${BRAND_NS.book}.memberIds`, `${BRAND_NS.book}.channelId`, `${BRAND_NS.book}.*At`],
|
|
5475
|
+
detail: "Ownership, roster, channel, and timestamps remain local to the target environment."
|
|
5476
|
+
},
|
|
5477
|
+
{
|
|
5478
|
+
kind: "brand.drafts",
|
|
5479
|
+
id: book.slug,
|
|
5480
|
+
reason: "draft_state",
|
|
5481
|
+
paths: [`${BRAND_NS.proposal}.*`, `${BRAND_NS.section}[status=draft]`],
|
|
5482
|
+
detail: "Open proposals and unapproved sections are review state, not published brand content."
|
|
5483
|
+
},
|
|
5484
|
+
{
|
|
5485
|
+
kind: "brand.assets",
|
|
5486
|
+
id: book.slug,
|
|
5487
|
+
reason: "provider_identity",
|
|
5488
|
+
paths: [`${BRAND_NS.asset}.storageObjectId`, `${BRAND_NS.asset}.path`, `${BRAND_NS.asset}.*AuthorityRef`],
|
|
5489
|
+
detail: "Stored files and provider object identities require a separate asset-transfer contract."
|
|
5490
|
+
},
|
|
5491
|
+
{
|
|
5492
|
+
kind: "brand.authority",
|
|
5493
|
+
id: book.slug,
|
|
5494
|
+
reason: "authority_history",
|
|
5495
|
+
paths: [`${BRAND_NS.approvalReceipt}.*`, `${BRAND_NS.palette}.audience`],
|
|
5496
|
+
detail: "Authority rows stay immutable in their source environment; artifacts retain only receipt digests and ids."
|
|
5497
|
+
}
|
|
5498
|
+
];
|
|
5499
|
+
function exportBrandPromotion(input) {
|
|
5500
|
+
const { book } = input;
|
|
5501
|
+
if (book.status !== "active") throw new TypeError(`brand book ${book.slug} is not active`);
|
|
5502
|
+
const revision = `brand:${book.slug}:${book.activationRevision}`;
|
|
5503
|
+
const artifacts = [];
|
|
5504
|
+
if (book.tokens) {
|
|
5505
|
+
const {
|
|
5506
|
+
compiledAt: _compiledAt,
|
|
5507
|
+
paletteReceiptId,
|
|
5508
|
+
paletteActionDigest,
|
|
5509
|
+
typographyReceiptId,
|
|
5510
|
+
typographyActionDigest,
|
|
5511
|
+
sourceDigest,
|
|
5512
|
+
...stableTokens
|
|
5513
|
+
} = book.tokens;
|
|
5514
|
+
artifacts.push({
|
|
5515
|
+
kind: "brand.tokens",
|
|
5516
|
+
id: book.slug,
|
|
5517
|
+
sourceRevision: revision,
|
|
5518
|
+
provenance: provenance(
|
|
5519
|
+
[book.id, book.tokens.paletteId, paletteReceiptId, typographyReceiptId].filter((value) => Boolean(value)),
|
|
5520
|
+
[sourceDigest, paletteActionDigest, typographyActionDigest].filter((value) => Boolean(value))
|
|
5521
|
+
),
|
|
5522
|
+
content: (0, import_promotion.canonicalPromotionValue)({ bookSlug: book.slug, tokens: stableTokens })
|
|
5523
|
+
});
|
|
5524
|
+
}
|
|
5525
|
+
for (const section of input.sections.filter((row) => row.bookId === book.id && row.status === "approved")) {
|
|
5526
|
+
if (!section.approvalReceiptId || !section.approvalActionDigest) {
|
|
5527
|
+
throw new TypeError(`approved brand section ${section.key} lacks approval provenance`);
|
|
5528
|
+
}
|
|
5529
|
+
artifacts.push({
|
|
5530
|
+
kind: "brand.section",
|
|
5531
|
+
id: `${book.slug}/${section.kind}`,
|
|
5532
|
+
sourceRevision: revision,
|
|
5533
|
+
provenance: provenance([book.id, section.id, section.approvalReceiptId], [section.approvalActionDigest]),
|
|
5534
|
+
content: (0, import_promotion.canonicalPromotionValue)({ bookSlug: book.slug, kind: section.kind, content: section.content })
|
|
5535
|
+
});
|
|
5536
|
+
}
|
|
5537
|
+
const activePalette2 = input.palettes.find((row) => row.bookId === book.id && row.id === book.activePaletteId && row.status === "active");
|
|
5538
|
+
if (book.activePaletteId && !activePalette2) throw new TypeError(`active palette ${book.activePaletteId} is unavailable`);
|
|
5539
|
+
if (activePalette2) {
|
|
5540
|
+
artifacts.push({
|
|
5541
|
+
kind: "brand.palette",
|
|
5542
|
+
id: `${book.slug}/${activePalette2.id}`,
|
|
5543
|
+
sourceRevision: revision,
|
|
5544
|
+
provenance: provenance([book.id, activePalette2.id, activePalette2.proposalId, activePalette2.approvalReceiptId], [activePalette2.approvalActionDigest]),
|
|
5545
|
+
content: (0, import_promotion.canonicalPromotionValue)({
|
|
5546
|
+
bookSlug: book.slug,
|
|
5547
|
+
name: activePalette2.name,
|
|
5548
|
+
swatches: activePalette2.swatches,
|
|
5549
|
+
source: activePalette2.source,
|
|
5550
|
+
...activePalette2.seedHex ? { seedHex: activePalette2.seedHex } : {},
|
|
5551
|
+
...activePalette2.rationale ? { rationale: activePalette2.rationale } : {}
|
|
5552
|
+
})
|
|
5553
|
+
});
|
|
5554
|
+
}
|
|
5555
|
+
return {
|
|
5556
|
+
adapter: BRAND_PROMOTION_ADAPTER,
|
|
5557
|
+
artifacts,
|
|
5558
|
+
exclusions: exclusions(book),
|
|
5559
|
+
preconditions: [{
|
|
5560
|
+
code: "brand.schema",
|
|
5561
|
+
path: "services.brand.schemaVersion",
|
|
5562
|
+
expected: 1,
|
|
5563
|
+
detail: "The target must expose the Brand v1 approved-content schema."
|
|
5564
|
+
}]
|
|
5565
|
+
};
|
|
5566
|
+
}
|
|
5439
5567
|
//# sourceMappingURL=index.cjs.map
|