@odla-ai/brand 0.7.0 → 0.7.2

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 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
- /** `typography`-kind section content. `scale` is a modular type-scale ratio. */
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
- /** `typography`-kind section content. `scale` is a modular type-scale ratio. */
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
@@ -598,6 +598,7 @@ var DESIGN_CONTENT_TYPES = /* @__PURE__ */ new Set(["text/html"]);
598
598
  var HEX_RGB = /^#[0-9a-f]{3}$/;
599
599
  var HEX_RRGGBB = /^#[0-9a-f]{6}$/;
600
600
  var HEX_ALPHA = /^#[0-9a-f]{4}$|^#[0-9a-f]{8}$/;
601
+ var UI_TOKEN_NAME = /^--ui-[a-z0-9-]+$/;
601
602
  var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
602
603
  function assertHex(value, label = "color") {
603
604
  if (typeof value !== "string")
@@ -664,6 +665,21 @@ function assertTypographySection(c) {
664
665
  out.scale = c.scale;
665
666
  }
666
667
  if (c.notes !== void 0) out.notes = capString(c.notes, "content.notes", 2e3);
668
+ if (c.tokenOverrides !== void 0) {
669
+ if (!isRecord(c.tokenOverrides))
670
+ throw new BrandInputError("content.tokenOverrides must be an object");
671
+ const entries = Object.entries(c.tokenOverrides);
672
+ if (entries.length > 64)
673
+ throw new BrandInputError("content.tokenOverrides must have at most 64 entries");
674
+ out.tokenOverrides = Object.fromEntries(entries.map(([name, raw]) => {
675
+ if (!UI_TOKEN_NAME.test(name))
676
+ throw new BrandInputError(`content.tokenOverrides.${name} must be a --ui-* token name`);
677
+ const value = capString(raw, `content.tokenOverrides.${name}`, 500);
678
+ if (/[;{}]/.test(value) || /url\s*\(/i.test(value))
679
+ throw new BrandInputError(`content.tokenOverrides.${name} contains an unsafe CSS value`);
680
+ return [name, value];
681
+ }));
682
+ }
667
683
  return out;
668
684
  }
669
685
  function assertVoiceSection(c) {
@@ -4043,7 +4059,8 @@ async function handleDesignPreview(ctx, req, bookId, assetId) {
4043
4059
  const asset = await linkedAsset(ctx, book, assetId);
4044
4060
  if (asset.kind !== DESIGN_ASSET_KIND) throw new BrandNotFoundError(`design ${assetId}`);
4045
4061
  const signed = await ctx.db.storage.sign(asset.path, SIGNED_READ_TTL_SECONDS);
4046
- const fetched = await ctx.fetchPrivateAsset(signed, {
4062
+ const fetchPrivateAsset = ctx.fetchPrivateAsset;
4063
+ const fetched = await fetchPrivateAsset(signed, {
4047
4064
  headers: { accept: "text/html" },
4048
4065
  redirect: "manual",
4049
4066
  signal: req.signal
@@ -4142,7 +4159,10 @@ var proposalGuardEquals = (snapshot) => ({ ...snapshot });
4142
4159
 
4143
4160
  // src/routes/proposal-effects.ts
4144
4161
  async function tokenSnapshot(swatches, typography, input, paletteId, paletteReceiptId, paletteActionDigest, typographyReceiptId, typographyActionDigest) {
4145
- const compiled = compileBrandTokens({ swatches, ...typography ? { typography } : {} });
4162
+ const compiled = compileBrandTokens({
4163
+ swatches,
4164
+ ...typography ? { typography, overrides: typography.tokenOverrides } : {}
4165
+ });
4146
4166
  const sources = {
4147
4167
  paletteId,
4148
4168
  paletteReceiptId,
@@ -4928,7 +4948,8 @@ async function handleBrandDiscussionAsset(ctx, req, target) {
4928
4948
  await ctx.db.storage.sign(before.path, SIGN_TTL_SECONDS)
4929
4949
  );
4930
4950
  if (!signed) throw new BrandInputError("private asset signing failed");
4931
- const response = await ctx.fetchPrivateAsset(signed, {
4951
+ const fetchPrivateAsset = ctx.fetchPrivateAsset;
4952
+ const response = await fetchPrivateAsset(signed, {
4932
4953
  headers: { accept: contentType },
4933
4954
  redirect: "manual",
4934
4955
  signal: req.signal
@@ -5165,7 +5186,10 @@ function createBrandRoutes(options) {
5165
5186
  newId: options.newId ?? (() => crypto.randomUUID()),
5166
5187
  maxUploadBytes: options.maxUploadBytes ?? 8 * 1024 * 1024,
5167
5188
  discussionBasePath: options.discussionBasePath ?? "/",
5168
- fetchPrivateAsset: options.fetchPrivateAsset ?? fetch,
5189
+ // Bound: this is stored on the context and invoked from route handlers,
5190
+ // where a property call would set the receiver to that context and
5191
+ // Cloudflare's fetch refuses a foreign receiver ("Illegal invocation").
5192
+ fetchPrivateAsset: options.fetchPrivateAsset ?? globalThis.fetch.bind(globalThis),
5169
5193
  previewFrameAncestors: options.previewFrameAncestors ?? ["'self'"]
5170
5194
  };
5171
5195
  return async (req) => {