@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,271 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * brand.json v1, the masters. Everything else (icons, cards, tokens, brand.md) is derived from
4
+ * this file by `generate()`, so a field belongs here only when a person chooses it. The kit
5
+ * outlines the wordmark and draws every file; neither is stored.
6
+ */
7
+ export const BRAND_SCHEMA_VERSION = 1;
8
+ export const BRAND_SCHEMA_URL = "https://ideare.app/schema/brand.v1.json";
9
+ /** The languages a brand can write its copy in. Widening this list is not a breaking change. */
10
+ export const LOCALES = ["pt-BR", "en", "es"];
11
+ /** Every color role, in the order tokens and brand.md list them. */
12
+ export const COLOR_ROLES = [
13
+ "ground",
14
+ "surface",
15
+ "ink",
16
+ "muted",
17
+ "line",
18
+ "primary",
19
+ "onPrimary",
20
+ "accent",
21
+ "onAccent",
22
+ "focus",
23
+ "danger",
24
+ "success",
25
+ "warning",
26
+ ];
27
+ export const FONT_ROLES = ["display", "text"];
28
+ /** The links a brand can list. `site` is a URL, the socials are handles, whatsapp is E.164. */
29
+ export const LINK_KINDS = [
30
+ "site",
31
+ "instagram",
32
+ "facebook",
33
+ "x",
34
+ "linkedin",
35
+ "youtube",
36
+ "tiktok",
37
+ "whatsapp",
38
+ "email",
39
+ "phone",
40
+ ];
41
+ /** The links that reach a person. ideare publishes them only when the owner shares contacts. */
42
+ export const CONTACT_LINK_KINDS = ["whatsapp", "email", "phone"];
43
+ const hex = z
44
+ .string()
45
+ .regex(/^#[0-9a-fA-F]{6}$/, "Use a 6-digit hex color, like #1A2B3C.")
46
+ .transform((value) => value.toLowerCase());
47
+ const sha256 = z.string().regex(/^[0-9a-f]{64}$/, "Use a lowercase sha256 hex digest.");
48
+ const svgMarkup = z
49
+ .string()
50
+ .max(65_536, "Keep the SVG under 64 KB.")
51
+ .refine((value) => /^\s*<svg[\s>]/.test(value), "Start the SVG with an <svg> element.");
52
+ const locale = z.enum(LOCALES);
53
+ const colorRole = z.enum(COLOR_ROLES);
54
+ const paletteSchema = z.object(Object.fromEntries(COLOR_ROLES.map((role) => [role, hex])));
55
+ const copySchema = z.object({
56
+ tagline: z.string().min(1).max(80),
57
+ description: z.string().min(1).max(300),
58
+ card: z.object({
59
+ title: z.string().min(1).max(70),
60
+ subtitle: z.string().max(140).optional(),
61
+ }),
62
+ });
63
+ const voiceSchema = z.object({
64
+ /** A few words for the tone, like "warm" or "direct". */
65
+ tone: z.array(z.string().min(1).max(40)).max(8),
66
+ /** Free notes for whoever writes in the brand's name. Private unless notesPublic. */
67
+ notes: z.string().max(2000).optional(),
68
+ notesPublic: z.boolean().default(false),
69
+ /** Words the brand never uses. The copy check flags them. */
70
+ neverSay: z.array(z.string().min(1).max(60)).max(60).default([]),
71
+ examples: z
72
+ .array(z.object({ context: z.string().min(1).max(60), text: z.string().min(1).max(400) }))
73
+ .max(20)
74
+ .default([]),
75
+ });
76
+ const tileSchema = z.object({
77
+ /** The color role behind the mark on icons and avatars. */
78
+ fill: colorRole.default("primary"),
79
+ shape: z.enum(["square", "rounded", "circle"]).default("rounded"),
80
+ /** Corner radius as a fraction of the tile's side (rounded only). */
81
+ radius: z.number().min(0).max(0.5).default(0.22),
82
+ /** Space around the mark as a fraction of the tile's side. */
83
+ padding: z.number().min(0).max(0.4).default(0.18),
84
+ });
85
+ const markSchema = z.object({
86
+ svg: svgMarkup,
87
+ /** A simpler drawing for 16 to 32 px. Falls back to svg. */
88
+ small: svgMarkup.optional(),
89
+ /** The version for dark grounds. Falls back to svg with the darkest color swapped. */
90
+ dark: svgMarkup.optional(),
91
+ /**
92
+ * The owner's own logo versions, drawn as they are instead of composed from the mark and the
93
+ * wordmark: a brand brought in from its files keeps the lockups it already has. Each one's
94
+ * viewBox hugs the drawing.
95
+ */
96
+ lockups: z
97
+ .object({
98
+ horizontal: svgMarkup.optional(),
99
+ stacked: svgMarkup.optional(),
100
+ wordmark: svgMarkup.optional(),
101
+ })
102
+ .optional(),
103
+ tile: tileSchema.prefault({}),
104
+ /** Where the mark came from. Icon marks come from an open set, so they are not exclusive. */
105
+ source: z
106
+ .object({
107
+ kind: z.enum(["monogram", "icon", "ai", "upload"]),
108
+ /** For icon marks: the set and name, like "lucide:scissors". */
109
+ ref: z.string().max(80).optional(),
110
+ })
111
+ .optional(),
112
+ });
113
+ const wordmarkSchema = z.object({
114
+ text: z.string().min(1).max(40),
115
+ font: z.enum(FONT_ROLES).default("display"),
116
+ weight: z.number().int().min(100).max(900).default(700),
117
+ /** Letter spacing in em, like -0.02. */
118
+ tracking: z.number().min(-0.2).max(0.5).default(0),
119
+ });
120
+ const fontSchema = z.object({
121
+ /** The family name people see, like "Instrument Sans". Letters, digits, spaces and . ' & -
122
+ * only: it is written into CSS and HTML, so a quote or a brace could end the string there. */
123
+ family: z
124
+ .string()
125
+ .min(1)
126
+ .max(80)
127
+ .regex(/^[\p{L}\p{N} .'&-]+$/u, "Use the font's name, like Instrument Sans."),
128
+ /** The Fontsource id, like "instrument-sans". */
129
+ id: z.string().regex(/^[a-z0-9-]+$/, "Use the Fontsource id, like instrument-sans."),
130
+ weights: z.array(z.number().int().min(100).max(900)).min(1).max(9),
131
+ license: z.enum(["OFL-1.1", "Apache-2.0"]),
132
+ /** sha256 of each weight's TTF, keyed by weight. Written by the kit when it fetches them. */
133
+ files: z.record(z.string().regex(/^[1-9]00$/), sha256).default({}),
134
+ });
135
+ const radiiSchema = z.object({
136
+ /** Buttons and fields, in px. */
137
+ control: z.number().min(0).max(64).default(10),
138
+ /** Cards and panels, in px. */
139
+ card: z.number().min(0).max(64).default(16),
140
+ });
141
+ const artSchema = z.object({
142
+ /** The style every AI picture follows. */
143
+ prompt: z.string().max(2000).default(""),
144
+ references: z
145
+ .array(z.object({
146
+ sha256,
147
+ mime: z.enum(["image/png", "image/jpeg", "image/webp"]),
148
+ width: z.number().int().positive(),
149
+ height: z.number().int().positive(),
150
+ }))
151
+ .max(10)
152
+ .default([]),
153
+ referencesPublic: z.boolean().default(false),
154
+ avoid: z.array(z.string().min(1).max(80)).max(30).default([]),
155
+ });
156
+ const linkSchema = z.object({
157
+ value: z.string().min(1).max(300),
158
+ /** Shown in the public brand.json and brand page only when ticked. */
159
+ public: z.boolean().default(false),
160
+ });
161
+ const linksShape = Object.fromEntries(LINK_KINDS.map((kind) => [kind, linkSchema.optional()]));
162
+ export const brandSchema = z
163
+ .object({
164
+ $schema: z.literal(BRAND_SCHEMA_URL).default(BRAND_SCHEMA_URL),
165
+ schemaVersion: z.literal(BRAND_SCHEMA_VERSION),
166
+ /** The public address part, ideare.app/b/<slug>. Set by ideare on the first approval. */
167
+ slug: z
168
+ .string()
169
+ .regex(/^[a-z0-9](?:[a-z0-9-]{0,46}[a-z0-9])?$/)
170
+ .optional(),
171
+ /** The approved version this file came from. Absent on drafts. */
172
+ version: z.number().int().positive().optional(),
173
+ name: z.string().min(1).max(60),
174
+ /** For tight spots: the app name under a home-screen icon. */
175
+ shortName: z.string().min(1).max(12),
176
+ domain: z
177
+ .string()
178
+ .regex(/^(?:[a-z0-9-]+\.)+[a-z]{2,}$/, "Use a bare domain, like padaria.com.br.")
179
+ .optional(),
180
+ locales: z.array(locale).min(1),
181
+ defaultLocale: locale,
182
+ copy: z.partialRecord(locale, copySchema),
183
+ voice: voiceSchema.prefault({ tone: [] }),
184
+ mark: markSchema,
185
+ wordmark: wordmarkSchema,
186
+ colors: z.object({ light: paletteSchema, dark: paletteSchema }),
187
+ /**
188
+ * Extra named colors for illustrations, bands and accents, like "peach" or "trio-red".
189
+ * They never take a role: text and controls use `colors`.
190
+ */
191
+ paints: z
192
+ .record(z
193
+ .string()
194
+ .regex(/^[a-z][a-z0-9-]{0,31}$/, "Name a paint in lowercase, like peach or trio-red."), hex)
195
+ .default({}),
196
+ fonts: z.object({ display: fontSchema, text: fontSchema }),
197
+ radii: radiiSchema.prefault({}),
198
+ art: artSchema.prefault({}),
199
+ links: z.object(linksShape).default({}),
200
+ })
201
+ .superRefine((brand, ctx) => {
202
+ if (new Set(brand.locales).size !== brand.locales.length) {
203
+ ctx.addIssue({
204
+ code: "custom",
205
+ path: ["locales"],
206
+ message: "List each language once.",
207
+ });
208
+ }
209
+ if (!brand.locales.includes(brand.defaultLocale)) {
210
+ ctx.addIssue({
211
+ code: "custom",
212
+ path: ["defaultLocale"],
213
+ message: "The default language must be one of the brand's languages.",
214
+ });
215
+ }
216
+ for (const lang of brand.locales) {
217
+ if (!brand.copy[lang]) {
218
+ ctx.addIssue({
219
+ code: "custom",
220
+ path: ["copy", lang],
221
+ message: `Add the copy for ${lang}, or take ${lang} out of locales.`,
222
+ });
223
+ }
224
+ }
225
+ for (const lang of Object.keys(brand.copy)) {
226
+ if (!brand.locales.includes(lang)) {
227
+ ctx.addIssue({
228
+ code: "custom",
229
+ path: ["copy", lang],
230
+ message: `${lang} has copy but is not in locales.`,
231
+ });
232
+ }
233
+ }
234
+ });
235
+ /**
236
+ * Read a brand.json. A file from a newer kit is refused with "Update ideare" instead of being
237
+ * half-read, so a CLI never writes files from a format it does not know.
238
+ */
239
+ export function parseBrand(input) {
240
+ const declared = typeof input === "object" && input !== null && "schemaVersion" in input
241
+ ? input.schemaVersion
242
+ : undefined;
243
+ if (typeof declared === "number" && declared > BRAND_SCHEMA_VERSION) {
244
+ return {
245
+ ok: false,
246
+ reason: "newer",
247
+ issues: [
248
+ {
249
+ path: "schemaVersion",
250
+ message: `This brand.json is format ${declared}, and this ideare reads up to ${BRAND_SCHEMA_VERSION}. Update ideare.`,
251
+ },
252
+ ],
253
+ };
254
+ }
255
+ const result = brandSchema.safeParse(input);
256
+ if (result.success)
257
+ return { ok: true, brand: result.data };
258
+ return {
259
+ ok: false,
260
+ reason: "invalid",
261
+ issues: result.error.issues.map((issue) => ({
262
+ path: issue.path.map(String).join(".") || "(root)",
263
+ message: issue.message,
264
+ })),
265
+ };
266
+ }
267
+ /** The JSON Schema for editors and outside tools, generated from the zod schema. */
268
+ export function brandJsonSchema() {
269
+ const schema = z.toJSONSchema(brandSchema, { io: "input", unrepresentable: "any" });
270
+ return { ...schema, $id: BRAND_SCHEMA_URL, title: "ideare brand.json v1" };
271
+ }
@@ -0,0 +1,27 @@
1
+ import type { Locale } from "../brand/schema.ts";
2
+ /**
3
+ * The copy check: the house rules for every word a person reads, run on the brand's own copy.
4
+ * No em dash, none of the words that make text read as written by a machine, sentence case, no
5
+ * exclamation marks, and lengths that fit where the words go.
6
+ */
7
+ export type CopyField = "tagline" | "title" | "subtitle" | "description";
8
+ export type CopyIssue = {
9
+ rule: "emDash" | "dashBreak" | "bannedWord" | "exclamation" | "sentenceCase" | "length" | "spacing";
10
+ level: "error" | "warning";
11
+ message: string;
12
+ details?: Record<string, string | number>;
13
+ };
14
+ /** Words and phrases that read as machine-written, per language, without accents. */
15
+ export declare const BANNED_WORDS: Record<Locale, string[]>;
16
+ /** Where each field shows, and how much fits there. */
17
+ export declare const COPY_LIMITS: Record<CopyField, number>;
18
+ /**
19
+ * Check one piece of copy. `names` are words allowed to start with a capital anywhere (the brand
20
+ * name, places); `neverSay` is the brand's own list.
21
+ */
22
+ export declare function checkCopy(text: string, options: {
23
+ locale: Locale;
24
+ field: CopyField;
25
+ neverSay?: string[];
26
+ names?: string[];
27
+ }): CopyIssue[];
@@ -0,0 +1,139 @@
1
+ /** Words and phrases that read as machine-written, per language, without accents. */
2
+ export const BANNED_WORDS = {
3
+ en: [
4
+ "unlock",
5
+ "elevate",
6
+ "seamless",
7
+ "seamlessly",
8
+ "effortless",
9
+ "effortlessly",
10
+ "empower",
11
+ "leverage",
12
+ "robust",
13
+ "comprehensive",
14
+ "cutting-edge",
15
+ "delve",
16
+ "game-changing",
17
+ "game changer",
18
+ "next-level",
19
+ "revolutionize",
20
+ "revolutionary",
21
+ "supercharge",
22
+ "unleash",
23
+ "synergy",
24
+ "in today's fast-paced world",
25
+ "designed to help you",
26
+ ],
27
+ "pt-BR": [
28
+ "revolucionario",
29
+ "revolucionar",
30
+ "alavancar",
31
+ "potencializar",
32
+ "sem esforco",
33
+ "de ponta",
34
+ "desbloquear",
35
+ "sinergia",
36
+ "no mundo acelerado de hoje",
37
+ "projetado para ajudar",
38
+ "eleve o seu",
39
+ "eleve sua",
40
+ ],
41
+ es: [
42
+ "revolucionario",
43
+ "revolucionar",
44
+ "potenciar",
45
+ "sin esfuerzo",
46
+ "de vanguardia",
47
+ "desbloquear",
48
+ "sinergia",
49
+ "apalancar",
50
+ "en el mundo acelerado de hoy",
51
+ "disenado para ayudarte",
52
+ ],
53
+ };
54
+ /** Where each field shows, and how much fits there. */
55
+ export const COPY_LIMITS = {
56
+ tagline: 60,
57
+ title: 60,
58
+ subtitle: 110,
59
+ description: 160,
60
+ };
61
+ function plain(text) {
62
+ return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
63
+ }
64
+ function hasWord(haystack, needle) {
65
+ const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
66
+ return new RegExp(`(^|[^\\p{L}\\p{N}])${escaped}($|[^\\p{L}\\p{N}])`, "u").test(haystack);
67
+ }
68
+ /**
69
+ * Check one piece of copy. `names` are words allowed to start with a capital anywhere (the brand
70
+ * name, places); `neverSay` is the brand's own list.
71
+ */
72
+ export function checkCopy(text, options) {
73
+ const issues = [];
74
+ if (text.includes("\u2014")) {
75
+ issues.push({
76
+ rule: "emDash",
77
+ level: "error",
78
+ message: "Take out the long dash. Use a period, a comma or a colon.",
79
+ });
80
+ }
81
+ if (/\s[\u2013-]\s/.test(text)) {
82
+ issues.push({
83
+ rule: "dashBreak",
84
+ level: "warning",
85
+ message: "A dash between spaces reads like a pause. Use a period, a comma or a colon.",
86
+ });
87
+ }
88
+ if (/[!¡]/.test(text)) {
89
+ issues.push({
90
+ rule: "exclamation",
91
+ level: "error",
92
+ message: "Take out the exclamation mark. Calm words read as more sure.",
93
+ });
94
+ }
95
+ const flat = plain(text);
96
+ const banned = [...BANNED_WORDS[options.locale], ...(options.neverSay ?? []).map(plain)];
97
+ const found = [...new Set(banned.filter((word) => hasWord(flat, plain(word))))];
98
+ if (found.length) {
99
+ issues.push({
100
+ rule: "bannedWord",
101
+ level: "error",
102
+ message: `Say it in plain words instead of "${found.join('", "')}".`,
103
+ details: { words: found.join(", ") },
104
+ });
105
+ }
106
+ if (/\s{2,}|^\s|\s$/.test(text)) {
107
+ issues.push({ rule: "spacing", level: "warning", message: "Remove the extra spaces." });
108
+ }
109
+ // Sentence case: after the first word, only names and acronyms start with a capital. Three
110
+ // or more capitals that are not names, making most of the words, read as Title Case.
111
+ if (options.field !== "description") {
112
+ const names = new Set((options.names ?? []).flatMap((name) => name.split(/\s+/)).map(plain));
113
+ const words = text
114
+ .split(/\s+/)
115
+ .slice(1)
116
+ .map((word) => word.replace(/^[^\p{L}]+|[^\p{L}]+$/gu, ""))
117
+ .filter((word) => word.length >= 2 && !names.has(plain(word)) && word !== word.toUpperCase());
118
+ const capitalized = words.filter((word) => /^\p{Lu}/u.test(word));
119
+ if (capitalized.length >= 3 && capitalized.length >= words.length * 0.6) {
120
+ issues.push({
121
+ rule: "sentenceCase",
122
+ level: "warning",
123
+ message: "Use sentence case: only the first word and names start with a capital letter.",
124
+ details: { words: capitalized.join(", ") },
125
+ });
126
+ }
127
+ }
128
+ const limit = COPY_LIMITS[options.field];
129
+ const length = [...text].length;
130
+ if (length > limit) {
131
+ issues.push({
132
+ rule: "length",
133
+ level: "warning",
134
+ message: `This is ${length} characters; about ${limit} fit where it shows. Shorten it a little.`,
135
+ details: { length, limit },
136
+ });
137
+ }
138
+ return issues;
139
+ }
@@ -0,0 +1,54 @@
1
+ import type { Brand } from "../brand/schema.ts";
2
+ import type { CardPage, DerivedFile } from "../files/generate.ts";
3
+ import type { Rasterizer } from "../files/raster.ts";
4
+ import type { FontResolver } from "../text/fonts.ts";
5
+ /**
6
+ * check(): every promise a brand kit makes, measured. Each result has a plain title an owner
7
+ * reads ("Easy to read", "Hard to read on cards: fix it"), the numbers behind it for "Details",
8
+ * and, when one exists, a fix the studio shows before and after and applies only on a tap.
9
+ */
10
+ export type CheckLevel = "pass" | "warning" | "error";
11
+ export type CheckFix =
12
+ /** Set these brand.json fields. `from` is there so the studio can show before and after. */
13
+ {
14
+ kind: "set";
15
+ label: string;
16
+ changes: {
17
+ path: string;
18
+ from: unknown;
19
+ to: unknown;
20
+ }[];
21
+ }
22
+ /** Ask for new words (the "Write it again" button). */
23
+ | {
24
+ kind: "rewrite";
25
+ label: string;
26
+ path: string;
27
+ };
28
+ export type CheckResult = {
29
+ /** Stable, for translations and tests: like "contrast.light.muted.surface". */
30
+ id: string;
31
+ area: "colors" | "logo" | "text" | "fonts" | "files" | "copy";
32
+ level: CheckLevel;
33
+ title: string;
34
+ message?: string;
35
+ fix?: CheckFix;
36
+ details?: Record<string, string | number>;
37
+ };
38
+ export type CheckContext = {
39
+ /** Needed for text fit and glyph coverage. Without it those checks are skipped, and say so. */
40
+ fonts?: FontResolver;
41
+ /** Lets the 16 px legibility check look at real pixels. */
42
+ rasterize?: Rasterizer;
43
+ /** Made files, for the size limits. */
44
+ files?: Map<string, DerivedFile>;
45
+ pages?: CardPage[];
46
+ };
47
+ /** Run every check. Errors come first, then warnings, then what passed. */
48
+ export declare function check(brand: Brand, ctx?: CheckContext): Promise<CheckResult[]>;
49
+ /** "2 to fix" and friends, for the folded summary. */
50
+ export declare function summarize(results: CheckResult[]): {
51
+ errors: number;
52
+ warnings: number;
53
+ passed: number;
54
+ };