@ideare/kit 0.0.0-stage → 0.1.0-next.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -2
  3. package/THIRD_PARTY.md +52 -0
  4. package/dist/brand/schema.d.ts +265 -0
  5. package/dist/brand/schema.js +271 -0
  6. package/dist/check/copy.d.ts +27 -0
  7. package/dist/check/copy.js +139 -0
  8. package/dist/check/index.d.ts +54 -0
  9. package/dist/check/index.js +492 -0
  10. package/dist/color/contrast.d.ts +41 -0
  11. package/dist/color/contrast.js +84 -0
  12. package/dist/color/palette.d.ts +27 -0
  13. package/dist/color/palette.js +114 -0
  14. package/dist/color/wcag.d.ts +22 -0
  15. package/dist/color/wcag.js +50 -0
  16. package/dist/draft/draft.d.ts +61 -0
  17. package/dist/draft/draft.js +383 -0
  18. package/dist/editor/history.d.ts +78 -0
  19. package/dist/editor/history.js +221 -0
  20. package/dist/editor/operations.d.ts +112 -0
  21. package/dist/editor/operations.js +621 -0
  22. package/dist/files/docs.d.ts +49 -0
  23. package/dist/files/docs.js +392 -0
  24. package/dist/files/generate.d.ts +47 -0
  25. package/dist/files/generate.js +254 -0
  26. package/dist/files/png.d.ts +40 -0
  27. package/dist/files/png.js +168 -0
  28. package/dist/files/raster.d.ts +14 -0
  29. package/dist/files/raster.js +0 -0
  30. package/dist/files/templates.d.ts +162 -0
  31. package/dist/files/templates.js +390 -0
  32. package/dist/files/tokens.d.ts +22 -0
  33. package/dist/files/tokens.js +204 -0
  34. package/dist/fonts/catalog.d.ts +3 -0
  35. package/dist/fonts/catalog.js +345 -0
  36. package/dist/fonts/ofl.d.ts +5 -0
  37. package/dist/fonts/ofl.js +5 -0
  38. package/dist/fonts/pick.d.ts +33 -0
  39. package/dist/fonts/pick.js +73 -0
  40. package/dist/imports/folder.d.ts +80 -0
  41. package/dist/imports/folder.js +124 -0
  42. package/dist/imports/unzip.d.ts +32 -0
  43. package/dist/imports/unzip.js +128 -0
  44. package/dist/index.d.ts +59 -0
  45. package/dist/index.js +34 -0
  46. package/dist/marks/draw.d.ts +48 -0
  47. package/dist/marks/draw.js +149 -0
  48. package/dist/marks/icons.data.d.ts +5 -0
  49. package/dist/marks/icons.data.js +600 -0
  50. package/dist/marks/svg.d.ts +46 -0
  51. package/dist/marks/svg.js +341 -0
  52. package/dist/node/index.d.ts +13 -0
  53. package/dist/node/index.js +41 -0
  54. package/dist/posts/pix.d.ts +19 -0
  55. package/dist/posts/pix.js +49 -0
  56. package/dist/posts/templates.d.ts +45 -0
  57. package/dist/posts/templates.js +868 -0
  58. package/dist/resvg/index.d.ts +2 -0
  59. package/dist/resvg/index.js +22 -0
  60. package/dist/scene/brand-art.d.ts +49 -0
  61. package/dist/scene/brand-art.js +154 -0
  62. package/dist/scene/frame.d.ts +89 -0
  63. package/dist/scene/frame.js +213 -0
  64. package/dist/scene/grain.d.ts +7 -0
  65. package/dist/scene/grain.js +7 -0
  66. package/dist/scene/qr.d.ts +10 -0
  67. package/dist/scene/qr.js +359 -0
  68. package/dist/scene/render.d.ts +76 -0
  69. package/dist/scene/render.js +557 -0
  70. package/dist/scene/schema.d.ts +271 -0
  71. package/dist/scene/schema.js +257 -0
  72. package/dist/sign.d.ts +47 -0
  73. package/dist/sign.js +88 -0
  74. package/dist/text/engine.d.ts +128 -0
  75. package/dist/text/engine.js +245 -0
  76. package/dist/text/fonts.d.ts +56 -0
  77. package/dist/text/fonts.js +65 -0
  78. package/dist/version.d.ts +2 -0
  79. package/dist/version.js +2 -0
  80. package/dist/wasm/index.d.ts +15 -0
  81. package/dist/wasm/index.js +46 -0
  82. package/package.json +78 -5
  83. package/schema/brand.v1.json +442 -0
@@ -0,0 +1,392 @@
1
+ import { COLOR_ROLES, LINK_KINDS } from "../brand/schema.js";
2
+ import { contrast, formatRatio } from "../color/wcag.js";
3
+ import { cleanMarkSvg } from "../marks/svg.js";
4
+ import { cssString, fontStack } from "./tokens.js";
5
+ /**
6
+ * The words around the brand: brand.md for outside agents (Google's design.md format), the
7
+ * public brand.json, the head tags, a prompt for AI site builders, the web manifest and the
8
+ * email signature.
9
+ */
10
+ const ROLE_WORDS = {
11
+ ground: "the page behind everything",
12
+ surface: "cards, panels and fields",
13
+ ink: "text and icons",
14
+ muted: "quiet text: captions, hints, dates",
15
+ line: "field borders and dividers that must be seen",
16
+ primary: "the brand color: main buttons and key marks",
17
+ onPrimary: "text and icons on the brand color",
18
+ accent: "a second color for highlights and illustrations",
19
+ onAccent: "text on the accent",
20
+ focus: "the keyboard focus ring",
21
+ danger: "errors and destructive actions, always with an icon and a word",
22
+ success: "success, always with an icon and a word",
23
+ warning: "warnings, always with an icon and a word",
24
+ };
25
+ function kebab(role) {
26
+ return role.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
27
+ }
28
+ function yamlString(value) {
29
+ return JSON.stringify(value);
30
+ }
31
+ /** The brand's slug, or one made from its name for a draft. */
32
+ export function slugOf(brand) {
33
+ return (brand.slug ??
34
+ brand.name
35
+ .toLowerCase()
36
+ .normalize("NFD")
37
+ .replace(/\p{M}/gu, "")
38
+ .replace(/[^a-z0-9]+/g, "-")
39
+ .replace(/^-|-$/g, ""));
40
+ }
41
+ /** The brand's copy in a language, falling back to the default language. */
42
+ export function copyFor(brand, locale) {
43
+ const copy = brand.copy[locale ?? brand.defaultLocale] ?? brand.copy[brand.defaultLocale];
44
+ if (!copy)
45
+ throw new TypeError(`The brand has no copy for ${locale ?? brand.defaultLocale}`);
46
+ return copy;
47
+ }
48
+ /** brand.md: YAML tokens, then the design.md sections in their fixed order. */
49
+ export function brandMd(brand) {
50
+ const light = brand.colors.light;
51
+ const dark = brand.colors.dark;
52
+ const copy = copyFor(brand);
53
+ const colorYaml = [
54
+ ...COLOR_ROLES.map((role) => ` ${kebab(role)}: ${yamlString(light[role])}`),
55
+ ...COLOR_ROLES.map((role) => ` dark-${kebab(role)}: ${yamlString(dark[role])}`),
56
+ ...Object.entries(brand.paints).map(([name, hex]) => ` paint-${name}: ${yamlString(hex)}`),
57
+ ];
58
+ const display = brand.fonts.display;
59
+ const text = brand.fonts.text;
60
+ const lines = [
61
+ "---",
62
+ 'version: "alpha"',
63
+ `name: ${yamlString(brand.name)}`,
64
+ `description: ${yamlString(copy.tagline)}`,
65
+ "colors:",
66
+ ...colorYaml,
67
+ "typography:",
68
+ " display-lg:",
69
+ ` fontFamily: ${yamlString(display.family)}`,
70
+ " fontSize: 3rem",
71
+ ` fontWeight: ${brand.wordmark.font === "display" ? brand.wordmark.weight : display.weights[0]}`,
72
+ " lineHeight: 1.1",
73
+ " headline-md:",
74
+ ` fontFamily: ${yamlString(display.family)}`,
75
+ " fontSize: 1.75rem",
76
+ " lineHeight: 1.2",
77
+ " body-md:",
78
+ ` fontFamily: ${yamlString(text.family)}`,
79
+ " fontSize: 1rem",
80
+ " fontWeight: 400",
81
+ " lineHeight: 1.5",
82
+ " label-md:",
83
+ ` fontFamily: ${yamlString(text.family)}`,
84
+ " fontSize: 0.875rem",
85
+ ` fontWeight: ${text.weights.includes(600) ? 600 : (text.weights.at(-1) ?? 400)}`,
86
+ " lineHeight: 1.3",
87
+ "rounded:",
88
+ ` control: ${brand.radii.control}px`,
89
+ ` card: ${brand.radii.card}px`,
90
+ "spacing:",
91
+ " sm: 8px",
92
+ " md: 16px",
93
+ " lg: 24px",
94
+ " xl: 40px",
95
+ "components:",
96
+ " button-primary:",
97
+ ' backgroundColor: "{colors.primary}"',
98
+ ' textColor: "{colors.on-primary}"',
99
+ ' rounded: "{rounded.control}"',
100
+ " padding: 12px",
101
+ " card:",
102
+ ' backgroundColor: "{colors.surface}"',
103
+ ' textColor: "{colors.ink}"',
104
+ ' rounded: "{rounded.card}"',
105
+ " padding: 24px",
106
+ " input:",
107
+ ' backgroundColor: "{colors.ground}"',
108
+ ' textColor: "{colors.ink}"',
109
+ ' rounded: "{rounded.control}"',
110
+ "---",
111
+ "",
112
+ `# ${brand.name}`,
113
+ "",
114
+ "Made by ideare from brand.json. Do not edit this file: change the brand and make the files again.",
115
+ "",
116
+ "## Overview",
117
+ "",
118
+ `${copy.description}`,
119
+ "",
120
+ `Tagline: "${copy.tagline}". Voice: ${brand.voice.tone.join(", ") || "plain"}.`,
121
+ ...(brand.voice.notesPublic && brand.voice.notes ? ["", brand.voice.notes] : []),
122
+ "",
123
+ ...brand.locales.flatMap((lang) => {
124
+ const local = brand.copy[lang];
125
+ return local
126
+ ? [
127
+ `- ${lang}: "${local.tagline}" ${local.card.subtitle ? `(${local.card.subtitle})` : ""}`.trimEnd(),
128
+ ]
129
+ : [];
130
+ }),
131
+ "",
132
+ "## Colors",
133
+ "",
134
+ "Each color has one job. Light theme first; the `dark-` tokens are the same jobs on a dark ground.",
135
+ "",
136
+ ...COLOR_ROLES.map((role) => `- **${kebab(role)}** (${light[role]}, dark ${dark[role]}): ${ROLE_WORDS[role]}.`),
137
+ "",
138
+ ...(Object.keys(brand.paints).length
139
+ ? [
140
+ "",
141
+ `Paints, for illustrations, bands and accents, never for text roles: ${Object.entries(brand.paints)
142
+ .map(([name, hex]) => `${name} ${hex}`)
143
+ .join(", ")}.`,
144
+ "",
145
+ ]
146
+ : []),
147
+ `Text on ground reads at ${formatRatio(contrast(light.ink, light.ground))} in light and ${formatRatio(contrast(dark.ink, dark.ground))} in dark. Keep every text pair at 4.5:1 or more and every border, icon and focus ring at 3:1 or more.`,
148
+ "",
149
+ "## Typography",
150
+ "",
151
+ `- ${display.family} for the name, headlines and big statements (\`${fontStack(brand, "display")}\`).`,
152
+ `- ${text.family} for everything else (\`${fontStack(brand, "text")}\`).`,
153
+ "- Sentence case everywhere. No exclamation marks.",
154
+ "",
155
+ "## Layout",
156
+ "",
157
+ "Generous space: 8 px steps, 24 px between groups, 40 px between sections.",
158
+ "",
159
+ "## Shapes",
160
+ "",
161
+ `Controls round at ${brand.radii.control} px and cards at ${brand.radii.card} px.`,
162
+ "",
163
+ "## Components",
164
+ "",
165
+ "- Main button: the brand color with its on-color text.",
166
+ "- Cards: the surface color on the ground, no border needed.",
167
+ "- Fields: the ground with a line-colored border (3:1).",
168
+ "",
169
+ "## Do's and Don'ts",
170
+ "",
171
+ "- Do use the logo files as they are: `logo.svg` on light grounds, `logo-dark.svg` on dark ones.",
172
+ "- Do put status colors next to an icon and a word, never alone.",
173
+ "- Don't stretch, recolor or redraw the logo.",
174
+ "- Don't write the em dash, and don't use exclamation marks.",
175
+ ...(brand.voice.neverSay.length
176
+ ? [`- Don't use these words: ${brand.voice.neverSay.join(", ")}.`]
177
+ : []),
178
+ ...(brand.art.avoid.length ? [`- In pictures, avoid: ${brand.art.avoid.join(", ")}.`] : []),
179
+ "",
180
+ ];
181
+ return lines.join("\n");
182
+ }
183
+ /**
184
+ * The public brand.json: what anyone may read from the CDN and the brand page (plan D5). Voice
185
+ * notes, art references and every link without a public tick are left out, and the marks are
186
+ * written again by the kit, so only clean SVG is ever published.
187
+ */
188
+ export function publicBrand(brand) {
189
+ const links = Object.fromEntries(LINK_KINDS.flatMap((kind) => {
190
+ const link = brand.links[kind];
191
+ return link?.public ? [[kind, link]] : [];
192
+ }));
193
+ const { svg, small, dark, lockups } = brand.mark;
194
+ const clean = (value) => value === undefined ? undefined : cleanMarkSvg(value);
195
+ return {
196
+ ...brand,
197
+ mark: {
198
+ ...brand.mark,
199
+ svg: cleanMarkSvg(svg),
200
+ small: clean(small),
201
+ dark: clean(dark),
202
+ lockups: lockups && {
203
+ horizontal: clean(lockups.horizontal),
204
+ stacked: clean(lockups.stacked),
205
+ wordmark: clean(lockups.wordmark),
206
+ },
207
+ },
208
+ voice: {
209
+ ...brand.voice,
210
+ notes: brand.voice.notesPublic ? brand.voice.notes : undefined,
211
+ },
212
+ art: {
213
+ ...brand.art,
214
+ references: brand.art.referencesPublic ? brand.art.references : [],
215
+ },
216
+ links,
217
+ };
218
+ }
219
+ function escapeHtml(value) {
220
+ return value
221
+ .replaceAll("&", "&")
222
+ .replaceAll('"', """)
223
+ .replaceAll("<", "&lt;")
224
+ .replaceAll(">", "&gt;");
225
+ }
226
+ /** The head tags for the icons, the manifest, the theme colors and the share card. */
227
+ export function headTags(brand, options = {}) {
228
+ const base = (options.base ?? "/").replace(/\/?$/, "/");
229
+ const copy = copyFor(brand, options.locale);
230
+ const title = options.title ?? brand.name;
231
+ // A light-only page under a dark system setting would get dark browser chrome around a
232
+ // white page, so it names one color with no media query.
233
+ const themeColors = options.dark === "none"
234
+ ? [`<meta name="theme-color" content="${brand.colors.light.ground}">`]
235
+ : [
236
+ `<meta name="theme-color" content="${brand.colors.light.ground}" media="(prefers-color-scheme: light)">`,
237
+ `<meta name="theme-color" content="${brand.colors.dark.ground}" media="(prefers-color-scheme: dark)">`,
238
+ ];
239
+ const tags = [
240
+ `<link rel="icon" href="${base}favicon.ico" sizes="32x32">`,
241
+ `<link rel="icon" href="${base}icon.svg" type="image/svg+xml">`,
242
+ `<link rel="apple-touch-icon" href="${base}apple-touch-icon.png">`,
243
+ `<link rel="manifest" href="${base}site.webmanifest">`,
244
+ ...themeColors,
245
+ `<meta property="og:site_name" content="${escapeHtml(brand.name)}">`,
246
+ `<meta property="og:title" content="${escapeHtml(title)}">`,
247
+ `<meta property="og:description" content="${escapeHtml(copy.description)}">`,
248
+ ];
249
+ if (options.ogImage) {
250
+ tags.push(`<meta property="og:image" content="${escapeHtml(options.ogImage)}">`, '<meta property="og:image:width" content="1200">', '<meta property="og:image:height" content="630">', `<meta property="og:image:alt" content="${escapeHtml(ogAlt(brand, options.locale))}">`, '<meta name="twitter:card" content="summary_large_image">');
251
+ }
252
+ return tags;
253
+ }
254
+ /** The share card's alt text, in the card's language. */
255
+ export function ogAlt(brand, locale, title) {
256
+ const copy = copyFor(brand, locale);
257
+ return `${brand.name}: ${title ?? copy.card.title}`;
258
+ }
259
+ /** site.webmanifest, with the fleet's icon names so a sibling can switch with no head edits. */
260
+ export function webManifest(brand, options = {}) {
261
+ const base = (options.base ?? "/").replace(/\/?$/, "/");
262
+ return {
263
+ name: brand.name,
264
+ short_name: brand.shortName,
265
+ description: copyFor(brand).description,
266
+ lang: brand.defaultLocale,
267
+ start_url: base,
268
+ scope: base,
269
+ display: "standalone",
270
+ background_color: brand.colors.light.ground,
271
+ theme_color: brand.colors.light.ground,
272
+ icons: [
273
+ {
274
+ src: `${base}web-app-manifest-192x192.png`,
275
+ sizes: "192x192",
276
+ type: "image/png",
277
+ purpose: "any",
278
+ },
279
+ {
280
+ src: `${base}web-app-manifest-512x512.png`,
281
+ sizes: "512x512",
282
+ type: "image/png",
283
+ purpose: "any",
284
+ },
285
+ {
286
+ src: `${base}web-app-manifest-maskable-512x512.png`,
287
+ sizes: "512x512",
288
+ type: "image/png",
289
+ purpose: "maskable",
290
+ },
291
+ ],
292
+ };
293
+ }
294
+ function paletteLines(name, palette) {
295
+ return [
296
+ `${name}:`,
297
+ ...COLOR_ROLES.map((role) => `- ${kebab(role)}: ${palette[role]} (${ROLE_WORDS[role]})`),
298
+ ];
299
+ }
300
+ /** prompt.md: one paste-ready prompt for an AI site builder (Lovable, v0, Bolt and the like). */
301
+ export function promptMd(brand, options = {}) {
302
+ const copy = copyFor(brand);
303
+ const site = brand.links.site?.public
304
+ ? brand.links.site.value
305
+ : brand.domain
306
+ ? `https://${brand.domain}`
307
+ : undefined;
308
+ const files = options.filesUrl?.replace(/\/?$/, "/");
309
+ const lines = [
310
+ `Build the site for ${brand.name}${site ? ` (${site})` : ""}. Use this brand exactly.`,
311
+ "",
312
+ `What it is: ${copy.description}`,
313
+ `Tagline: ${copy.tagline}`,
314
+ `Languages: ${brand.locales.join(", ")} (default ${brand.defaultLocale}). Write each language natively, never as a translation.`,
315
+ "",
316
+ ...paletteLines("Colors, light theme", brand.colors.light),
317
+ "",
318
+ ...paletteLines("Colors, dark theme", brand.colors.dark),
319
+ "",
320
+ `Fonts: ${brand.fonts.display.family} for headings, ${brand.fonts.text.family} for text (both free on Google Fonts and Fontsource).`,
321
+ `Corners: ${brand.radii.control} px on buttons and fields, ${brand.radii.card} px on cards.`,
322
+ "",
323
+ ...(files
324
+ ? [
325
+ "Files (use them as they are):",
326
+ `- Logo: ${files}logo/logo.svg (dark grounds: ${files}logo/logo-dark.svg)`,
327
+ `- Icons: ${files}web/favicon.ico, ${files}web/icon.svg, ${files}web/apple-touch-icon.png, ${files}web/site.webmanifest`,
328
+ `- Share card: ${files}og/og.png`,
329
+ `- Tokens: ${files}tokens/tokens.css`,
330
+ "",
331
+ ]
332
+ : []),
333
+ `Voice: ${brand.voice.tone.join(", ") || "plain"}. Short sentences, everyday words, sentence case, no exclamation marks, no em dash.`,
334
+ ...(brand.voice.neverSay.length
335
+ ? [`Never use these words: ${brand.voice.neverSay.join(", ")}.`]
336
+ : []),
337
+ "Keep text at 4.5:1 contrast or more, and borders and icons at 3:1. Never recolor or redraw the logo.",
338
+ "",
339
+ ];
340
+ return lines.join("\n");
341
+ }
342
+ /** signature.html: a table-based email signature with the opaque logo bar (Gmail's dark mode). */
343
+ export function signatureHtml(brand, options) {
344
+ const light = brand.colors.light;
345
+ const copy = copyFor(brand);
346
+ const links = LINK_KINDS.flatMap((kind) => {
347
+ const link = brand.links[kind];
348
+ if (!link?.public)
349
+ return [];
350
+ const value = link.value;
351
+ const href = kind === "site"
352
+ ? value
353
+ : kind === "email"
354
+ ? `mailto:${value}`
355
+ : kind === "phone"
356
+ ? `tel:${value.replace(/[^\d+]/g, "")}`
357
+ : kind === "whatsapp"
358
+ ? `https://wa.me/${value.replace(/\D/g, "")}`
359
+ : kind === "instagram"
360
+ ? `https://instagram.com/${value.replace(/^@/, "")}`
361
+ : kind === "x"
362
+ ? `https://x.com/${value.replace(/^@/, "")}`
363
+ : kind === "tiktok"
364
+ ? `https://tiktok.com/@${value.replace(/^@/, "")}`
365
+ : kind === "youtube"
366
+ ? `https://youtube.com/${value.startsWith("@") ? value : `@${value}`}`
367
+ : kind === "linkedin"
368
+ ? `https://linkedin.com/${value}`
369
+ : `https://facebook.com/${value}`;
370
+ const label = kind === "site" ? value.replace(/^https?:\/\//, "").replace(/\/$/, "") : value;
371
+ return [
372
+ `<a href="${escapeHtml(href)}" style="color:${light.primary};text-decoration:none">${escapeHtml(label)}</a>`,
373
+ ];
374
+ });
375
+ // Single quotes, the form every mail client reads inside a double-quoted style.
376
+ const font = escapeHtml(`${cssString(brand.fonts.text.family, "'")}, Arial, sans-serif`);
377
+ return [
378
+ `<table cellpadding="0" cellspacing="0" border="0" style="font-family:${font};color:${light.ink};font-size:14px;line-height:1.4">`,
379
+ '<tr><td style="padding-bottom:8px">',
380
+ `<img src="${escapeHtml(options.logoUrl)}" width="300" height="80" alt="${escapeHtml(brand.name)}" style="display:block;border:0">`,
381
+ "</td></tr>",
382
+ ...(options.person
383
+ ? [
384
+ `<tr><td style="font-weight:600">${escapeHtml(options.person)}${options.role ? `<span style="font-weight:400;color:${light.muted}"> · ${escapeHtml(options.role)}</span>` : ""}</td></tr>`,
385
+ ]
386
+ : []),
387
+ `<tr><td style="color:${light.muted}">${escapeHtml(copy.tagline)}</td></tr>`,
388
+ ...(links.length ? [`<tr><td style="padding-top:4px">${links.join(" · ")}</td></tr>`] : []),
389
+ "</table>",
390
+ "",
391
+ ].join("\n");
392
+ }
@@ -0,0 +1,47 @@
1
+ import type { Brand, Locale } from "../brand/schema.ts";
2
+ import { type SceneCheck } from "../scene/render.ts";
3
+ import type { FontResolver } from "../text/fonts.ts";
4
+ import type { Rasterizer } from "./raster.ts";
5
+ import { type CardCopy } from "./templates.ts";
6
+ /** The groups of files `generate()` can make. Each file's path starts with its group. */
7
+ export declare const TARGETS: readonly ["web", "logo", "og", "social", "tokens", "registry", "docs", "email", "print"];
8
+ export type TargetId = (typeof TARGETS)[number];
9
+ export type DerivedFile = {
10
+ bytes: Uint8Array;
11
+ mime: string;
12
+ sha256: string;
13
+ width?: number;
14
+ height?: number;
15
+ };
16
+ /** A page listed in the repo's ideare.json, which gets its own share card per language. */
17
+ export type CardPage = {
18
+ slug: string;
19
+ copy: Partial<Record<Locale, CardCopy>>;
20
+ };
21
+ export type GenerateOptions = {
22
+ targets?: readonly TargetId[];
23
+ fonts: FontResolver;
24
+ rasterize: Rasterizer;
25
+ pages?: CardPage[];
26
+ /** Where the files will be served from, for links inside signature.html and prompt.md. */
27
+ filesUrl?: string;
28
+ /** tokens.css dark blocks and head.html's theme colors: "auto" (default), "attribute" or "none". */
29
+ dark?: "auto" | "attribute" | "none";
30
+ };
31
+ /** A file that would cut or overflow text, or drop a character, is refused rather than made. */
32
+ export declare class GenerateRefusal extends Error {
33
+ readonly problems: {
34
+ path: string;
35
+ check: SceneCheck;
36
+ }[];
37
+ constructor(problems: {
38
+ path: string;
39
+ check: SceneCheck;
40
+ }[]);
41
+ }
42
+ /**
43
+ * Every derived file of a brand, by path. Pure: nothing is written; the caller (the CLI, the
44
+ * worker, the browser zip) decides where bytes go. PNGs carry a tEXt provenance chunk; print
45
+ * files also say 300 dpi.
46
+ */
47
+ export declare function generate(brand: Brand, options: GenerateOptions): Promise<Map<string, DerivedFile>>;