brandspec 0.1.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/LICENSE +21 -0
- package/README.md +230 -0
- package/dist/cli.js +1516 -0
- package/dist/index.cjs +773 -0
- package/dist/index.d.cts +402 -0
- package/dist/index.d.ts +402 -0
- package/dist/index.js +718 -0
- package/package.json +61 -0
- package/schema/spec/assets.md +219 -0
- package/schema/spec/core.md +203 -0
- package/schema/spec/guidelines.md +110 -0
- package/schema/spec/tokens.md +389 -0
- package/schema/v0.1.0.yaml +201 -0
- package/workshop/SKILL.md +218 -0
- package/workshop/flow.md +181 -0
- package/workshop/phases/01-discovery.md +156 -0
- package/workshop/phases/02-concept.md +234 -0
- package/workshop/phases/03-visual.md +271 -0
- package/workshop/phases/04-documentation.md +99 -0
- package/workshop/templates/_workshop/decisions.yml +15 -0
- package/workshop/templates/_workshop/memo.md +23 -0
- package/workshop/templates/_workshop/position.yml +9 -0
- package/workshop/templates/_workshop/session.md +28 -0
- package/workshop/templates/brand.yaml +112 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// cli/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
API_BASE: () => API_BASE,
|
|
34
|
+
ensureBrandspecrc: () => ensureBrandspecrc,
|
|
35
|
+
flattenTokens: () => flattenTokens,
|
|
36
|
+
getContrastRatio: () => getContrastRatio,
|
|
37
|
+
getCredentialsPath: () => getCredentialsPath,
|
|
38
|
+
lintBrandspec: () => lintBrandspec,
|
|
39
|
+
loadRemote: () => loadRemote,
|
|
40
|
+
loadToken: () => loadToken,
|
|
41
|
+
parse: () => parse,
|
|
42
|
+
parseColor: () => parseColor,
|
|
43
|
+
parseOrgBrand: () => parseOrgBrand,
|
|
44
|
+
saveCredentials: () => saveCredentials,
|
|
45
|
+
schema: () => schema,
|
|
46
|
+
serialize: () => serialize,
|
|
47
|
+
toCss: () => toCss,
|
|
48
|
+
toFigmaTokens: () => toFigmaTokens,
|
|
49
|
+
toStyleDictionary: () => toStyleDictionary,
|
|
50
|
+
toTailwindCss: () => toTailwindCss,
|
|
51
|
+
validate: () => validate
|
|
52
|
+
});
|
|
53
|
+
module.exports = __toCommonJS(index_exports);
|
|
54
|
+
|
|
55
|
+
// cli/parser.ts
|
|
56
|
+
var import_js_yaml = __toESM(require("js-yaml"), 1);
|
|
57
|
+
function parse(content) {
|
|
58
|
+
const errors = [];
|
|
59
|
+
const warnings = [];
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = import_js_yaml.default.load(content);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
errors: [`Invalid YAML: ${e instanceof Error ? e.message : "Unknown error"}`],
|
|
67
|
+
warnings: []
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (!parsed || typeof parsed !== "object") {
|
|
71
|
+
return { success: false, errors: ["YAML must be an object"], warnings: [] };
|
|
72
|
+
}
|
|
73
|
+
const doc = parsed;
|
|
74
|
+
if (!doc.meta || typeof doc.meta !== "object") {
|
|
75
|
+
errors.push("Missing required field: meta");
|
|
76
|
+
return { success: false, errors, warnings };
|
|
77
|
+
}
|
|
78
|
+
const meta = doc.meta;
|
|
79
|
+
if (!meta.name || typeof meta.name !== "string") {
|
|
80
|
+
errors.push("Missing required field: meta.name");
|
|
81
|
+
return { success: false, errors, warnings };
|
|
82
|
+
}
|
|
83
|
+
if (doc.assets !== void 0) {
|
|
84
|
+
if (!Array.isArray(doc.assets)) {
|
|
85
|
+
errors.push("Field 'assets' must be an array");
|
|
86
|
+
} else {
|
|
87
|
+
doc.assets.forEach((asset, i) => {
|
|
88
|
+
if (!asset || typeof asset !== "object") {
|
|
89
|
+
errors.push(`assets[${i}] must be an object`);
|
|
90
|
+
} else {
|
|
91
|
+
const a = asset;
|
|
92
|
+
if (!a.file || typeof a.file !== "string") {
|
|
93
|
+
errors.push(`assets[${i}].file is required and must be a string`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!doc.core) {
|
|
100
|
+
warnings.push("No 'core' section \u2014 brand essence, personality, and voice are recommended");
|
|
101
|
+
}
|
|
102
|
+
if (!doc.tokens) {
|
|
103
|
+
warnings.push("No 'tokens' section \u2014 design tokens are recommended");
|
|
104
|
+
}
|
|
105
|
+
if (errors.length > 0) {
|
|
106
|
+
return { success: false, errors, warnings };
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
data: doc,
|
|
111
|
+
errors: [],
|
|
112
|
+
warnings
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function serialize(data) {
|
|
116
|
+
return import_js_yaml.default.dump(data, {
|
|
117
|
+
lineWidth: -1,
|
|
118
|
+
noRefs: true,
|
|
119
|
+
quotingType: '"',
|
|
120
|
+
forceQuotes: false
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// cli/validate.ts
|
|
125
|
+
var import_ajv = __toESM(require("ajv"), 1);
|
|
126
|
+
var import_ajv_formats = __toESM(require("ajv-formats"), 1);
|
|
127
|
+
|
|
128
|
+
// cli/schema.ts
|
|
129
|
+
var schema = {
|
|
130
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
131
|
+
$id: "https://brandspec.tools/schema/v0.1.0",
|
|
132
|
+
title: "brandspec",
|
|
133
|
+
description: "Brand Identity specification format",
|
|
134
|
+
type: "object",
|
|
135
|
+
required: ["meta"],
|
|
136
|
+
properties: {
|
|
137
|
+
meta: {
|
|
138
|
+
type: "object",
|
|
139
|
+
description: "Brand metadata",
|
|
140
|
+
required: ["name"],
|
|
141
|
+
properties: {
|
|
142
|
+
name: { type: "string", description: "Brand name" },
|
|
143
|
+
version: { type: "string", description: "Brand spec version (semver)" },
|
|
144
|
+
updated: { type: "string", format: "date", description: "Last updated date" },
|
|
145
|
+
description: { type: "string", description: "Brief brand description" },
|
|
146
|
+
url: { type: "string", format: "uri", description: "Brand website" }
|
|
147
|
+
},
|
|
148
|
+
additionalProperties: true
|
|
149
|
+
},
|
|
150
|
+
core: {
|
|
151
|
+
type: "object",
|
|
152
|
+
description: "Brand essence, personality, and voice",
|
|
153
|
+
properties: {
|
|
154
|
+
essence: { type: "string" },
|
|
155
|
+
tagline: { type: "string" },
|
|
156
|
+
mission: { type: "string" },
|
|
157
|
+
vision: { type: "string" },
|
|
158
|
+
values: { type: "array", items: { type: "string" } },
|
|
159
|
+
personality: { type: "array", items: { type: "string" } },
|
|
160
|
+
voice: {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {
|
|
163
|
+
tone: { type: "array", items: { type: "string" } },
|
|
164
|
+
principles: { type: "array", items: { type: "string" } }
|
|
165
|
+
},
|
|
166
|
+
additionalProperties: true
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
additionalProperties: true
|
|
170
|
+
},
|
|
171
|
+
tokens: {
|
|
172
|
+
type: "object",
|
|
173
|
+
description: "Design tokens (W3C DTCG compliant)",
|
|
174
|
+
additionalProperties: true
|
|
175
|
+
},
|
|
176
|
+
assets: {
|
|
177
|
+
type: "array",
|
|
178
|
+
description: "Brand assets",
|
|
179
|
+
items: {
|
|
180
|
+
type: "object",
|
|
181
|
+
required: ["file"],
|
|
182
|
+
properties: {
|
|
183
|
+
file: { type: "string" },
|
|
184
|
+
id: { type: "string" },
|
|
185
|
+
role: { type: "string" },
|
|
186
|
+
variant: { type: "string" },
|
|
187
|
+
context: { type: "string" },
|
|
188
|
+
description: { type: "string" },
|
|
189
|
+
formats: {
|
|
190
|
+
type: "array",
|
|
191
|
+
items: {
|
|
192
|
+
type: "object",
|
|
193
|
+
properties: {
|
|
194
|
+
path: { type: "string" },
|
|
195
|
+
width: { type: "integer" },
|
|
196
|
+
height: { type: "integer" }
|
|
197
|
+
},
|
|
198
|
+
additionalProperties: true
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
tags: { type: "array", items: { type: "string" } }
|
|
202
|
+
},
|
|
203
|
+
additionalProperties: true
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
guidelines: {
|
|
207
|
+
type: "object",
|
|
208
|
+
description: "Usage guidelines",
|
|
209
|
+
additionalProperties: {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {
|
|
212
|
+
content: { type: "string" },
|
|
213
|
+
rules: {
|
|
214
|
+
type: "array",
|
|
215
|
+
items: { $ref: "#/$defs/guidelineRule" }
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
additionalProperties: true
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
extensions: {
|
|
222
|
+
type: "object",
|
|
223
|
+
description: "Custom extensions",
|
|
224
|
+
additionalProperties: true
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
additionalProperties: true,
|
|
228
|
+
$defs: {
|
|
229
|
+
guidelineRule: {
|
|
230
|
+
type: "object",
|
|
231
|
+
required: ["description", "severity"],
|
|
232
|
+
properties: {
|
|
233
|
+
id: { type: "string" },
|
|
234
|
+
description: { type: "string" },
|
|
235
|
+
severity: {
|
|
236
|
+
type: "string",
|
|
237
|
+
enum: ["info", "warning", "error"]
|
|
238
|
+
},
|
|
239
|
+
criteria: { type: "array", items: { type: "string" } },
|
|
240
|
+
applies_to: { type: "string" }
|
|
241
|
+
},
|
|
242
|
+
additionalProperties: true
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// cli/validate.ts
|
|
248
|
+
var ajvInstance = null;
|
|
249
|
+
function getAjv() {
|
|
250
|
+
if (!ajvInstance) {
|
|
251
|
+
ajvInstance = new import_ajv.default({ allErrors: true, strict: false });
|
|
252
|
+
(0, import_ajv_formats.default)(ajvInstance);
|
|
253
|
+
}
|
|
254
|
+
return ajvInstance;
|
|
255
|
+
}
|
|
256
|
+
function validate(data) {
|
|
257
|
+
const ajv = getAjv();
|
|
258
|
+
const { $schema: _, $id: __, ...schemaBody } = schema;
|
|
259
|
+
const valid = ajv.validate(schemaBody, data);
|
|
260
|
+
if (valid) {
|
|
261
|
+
return { valid: true, errors: [], warnings: [] };
|
|
262
|
+
}
|
|
263
|
+
const errors = (ajv.errors ?? []).map((err) => {
|
|
264
|
+
const path = err.instancePath || "/";
|
|
265
|
+
return `${path}: ${err.message}`;
|
|
266
|
+
});
|
|
267
|
+
return { valid: false, errors, warnings: [] };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// cli/tokens.ts
|
|
271
|
+
function toCss(data) {
|
|
272
|
+
var _a;
|
|
273
|
+
const lines = [];
|
|
274
|
+
if (!data.tokens) return ":root {}\n";
|
|
275
|
+
for (const [group, tokens] of Object.entries(data.tokens)) {
|
|
276
|
+
if (!tokens) continue;
|
|
277
|
+
for (const [name, token] of Object.entries(tokens)) {
|
|
278
|
+
const prefix = group === "colors" ? "" : `${group}-`;
|
|
279
|
+
const varName = `--${prefix}${name}`;
|
|
280
|
+
lines.push(` ${varName}: ${token.$value};`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const darkLines = [];
|
|
284
|
+
if (data.tokens.colors) {
|
|
285
|
+
for (const [name, token] of Object.entries(data.tokens.colors)) {
|
|
286
|
+
const dark = (_a = token.$extensions) == null ? void 0 : _a["dark"];
|
|
287
|
+
if (typeof dark === "string") {
|
|
288
|
+
darkLines.push(` --${name}: ${dark};`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
let output = `:root {
|
|
293
|
+
${lines.join("\n")}
|
|
294
|
+
}
|
|
295
|
+
`;
|
|
296
|
+
if (darkLines.length > 0) {
|
|
297
|
+
output += `
|
|
298
|
+
.dark {
|
|
299
|
+
${darkLines.join("\n")}
|
|
300
|
+
}
|
|
301
|
+
`;
|
|
302
|
+
}
|
|
303
|
+
return output;
|
|
304
|
+
}
|
|
305
|
+
function toTailwindCss(data) {
|
|
306
|
+
const lines = [];
|
|
307
|
+
if (!data.tokens) return '@import "tailwindcss";\n\n@theme {}\n';
|
|
308
|
+
if (data.tokens.colors) {
|
|
309
|
+
for (const [name, token] of Object.entries(data.tokens.colors)) {
|
|
310
|
+
lines.push(` --color-${name}: ${token.$value};`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (data.tokens.typography) {
|
|
314
|
+
for (const [name, token] of Object.entries(data.tokens.typography)) {
|
|
315
|
+
lines.push(` --font-${name}: ${token.$value};`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (data.tokens.spacing) {
|
|
319
|
+
for (const [name, token] of Object.entries(data.tokens.spacing)) {
|
|
320
|
+
lines.push(` --spacing-${name}: ${token.$value};`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (data.tokens.radius) {
|
|
324
|
+
for (const [name, token] of Object.entries(data.tokens.radius)) {
|
|
325
|
+
lines.push(` --radius-${name}: ${token.$value};`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return `@import "tailwindcss";
|
|
329
|
+
|
|
330
|
+
@theme {
|
|
331
|
+
${lines.join("\n")}
|
|
332
|
+
}
|
|
333
|
+
`;
|
|
334
|
+
}
|
|
335
|
+
function toFigmaTokens(data) {
|
|
336
|
+
if (!data.tokens) return JSON.stringify({}, null, 2);
|
|
337
|
+
const output = {};
|
|
338
|
+
for (const [group, tokens] of Object.entries(data.tokens)) {
|
|
339
|
+
if (!tokens) continue;
|
|
340
|
+
output[group] = {};
|
|
341
|
+
for (const [name, token] of Object.entries(tokens)) {
|
|
342
|
+
output[group][name] = {
|
|
343
|
+
value: token.$value,
|
|
344
|
+
type: token.$type ?? group,
|
|
345
|
+
...token.$description && { description: token.$description }
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return JSON.stringify(output, null, 2) + "\n";
|
|
350
|
+
}
|
|
351
|
+
function toStyleDictionary(data) {
|
|
352
|
+
const tokenFile = {};
|
|
353
|
+
if (data.tokens) {
|
|
354
|
+
for (const [group, tokens2] of Object.entries(data.tokens)) {
|
|
355
|
+
if (!tokens2) continue;
|
|
356
|
+
tokenFile[group] = {};
|
|
357
|
+
for (const [name, token] of Object.entries(tokens2)) {
|
|
358
|
+
tokenFile[group][name] = {
|
|
359
|
+
$value: token.$value,
|
|
360
|
+
$type: token.$type ?? group,
|
|
361
|
+
...token.$description && { $description: token.$description }
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const tokens = JSON.stringify(tokenFile, null, 2) + "\n";
|
|
367
|
+
const brandName = data.meta.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
368
|
+
const config = {
|
|
369
|
+
source: ["tokens.json"],
|
|
370
|
+
usesDtcg: true,
|
|
371
|
+
platforms: {
|
|
372
|
+
css: {
|
|
373
|
+
transformGroup: "css",
|
|
374
|
+
buildPath: `build/css/`,
|
|
375
|
+
files: [
|
|
376
|
+
{
|
|
377
|
+
destination: "variables.css",
|
|
378
|
+
format: "css/variables",
|
|
379
|
+
options: { outputReferences: true }
|
|
380
|
+
}
|
|
381
|
+
]
|
|
382
|
+
},
|
|
383
|
+
scss: {
|
|
384
|
+
transformGroup: "scss",
|
|
385
|
+
buildPath: `build/scss/`,
|
|
386
|
+
files: [
|
|
387
|
+
{
|
|
388
|
+
destination: "_variables.scss",
|
|
389
|
+
format: "scss/variables",
|
|
390
|
+
options: { outputReferences: true }
|
|
391
|
+
}
|
|
392
|
+
]
|
|
393
|
+
},
|
|
394
|
+
ios: {
|
|
395
|
+
transformGroup: "ios-swift",
|
|
396
|
+
buildPath: `build/ios/`,
|
|
397
|
+
files: [
|
|
398
|
+
{
|
|
399
|
+
destination: `${brandName}.swift`,
|
|
400
|
+
format: "ios-swift/class.swift",
|
|
401
|
+
className: data.meta.name.replace(/[^a-zA-Z0-9]/g, "")
|
|
402
|
+
}
|
|
403
|
+
]
|
|
404
|
+
},
|
|
405
|
+
android: {
|
|
406
|
+
transformGroup: "android",
|
|
407
|
+
buildPath: `build/android/`,
|
|
408
|
+
files: [
|
|
409
|
+
{
|
|
410
|
+
destination: "colors.xml",
|
|
411
|
+
format: "android/resources",
|
|
412
|
+
filter: { $type: "color" }
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
destination: "dimens.xml",
|
|
416
|
+
format: "android/resources",
|
|
417
|
+
filter: { $type: "dimension" }
|
|
418
|
+
}
|
|
419
|
+
]
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
return { tokens, config: JSON.stringify(config, null, 2) + "\n" };
|
|
424
|
+
}
|
|
425
|
+
function flattenTokens(data) {
|
|
426
|
+
const result = [];
|
|
427
|
+
if (!data.tokens) return result;
|
|
428
|
+
for (const [group, tokens] of Object.entries(data.tokens)) {
|
|
429
|
+
if (!tokens) continue;
|
|
430
|
+
for (const [name, token] of Object.entries(tokens)) {
|
|
431
|
+
result.push({ group, name, token });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// cli/remote.ts
|
|
438
|
+
var import_node_fs = require("fs");
|
|
439
|
+
var import_node_path = require("path");
|
|
440
|
+
var import_node_os = require("os");
|
|
441
|
+
var API_BASE = process.env.BRANDSPEC_API_URL ?? "https://brandspec.tools";
|
|
442
|
+
function getCredentialsPath() {
|
|
443
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "brandspec", "credentials");
|
|
444
|
+
}
|
|
445
|
+
function loadToken() {
|
|
446
|
+
const envToken = process.env.BRANDSPEC_TOKEN;
|
|
447
|
+
if (envToken) return envToken;
|
|
448
|
+
const credPath = getCredentialsPath();
|
|
449
|
+
if ((0, import_node_fs.existsSync)(credPath)) {
|
|
450
|
+
const content = (0, import_node_fs.readFileSync)(credPath, "utf-8").trim();
|
|
451
|
+
if (content) return content;
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
function parseOrgBrand(str) {
|
|
456
|
+
const parts = str.split("/");
|
|
457
|
+
if (parts.length !== 2) return null;
|
|
458
|
+
const [org, brand] = parts;
|
|
459
|
+
const slugRe = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
|
|
460
|
+
if (!slugRe.test(org) || !slugRe.test(brand)) return null;
|
|
461
|
+
return { org, brand };
|
|
462
|
+
}
|
|
463
|
+
function loadRemote(args) {
|
|
464
|
+
const positional = args.find((a) => !a.startsWith("-") && a.includes("/"));
|
|
465
|
+
if (positional) return parseOrgBrand(positional);
|
|
466
|
+
const rcPath = (0, import_node_path.resolve)(".brandspecrc");
|
|
467
|
+
if ((0, import_node_fs.existsSync)(rcPath)) {
|
|
468
|
+
const content = (0, import_node_fs.readFileSync)(rcPath, "utf-8");
|
|
469
|
+
const match = content.match(/remote:\s*(.+)/);
|
|
470
|
+
if (match) return parseOrgBrand(match[1].trim());
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
function ensureBrandspecrc(org, brand) {
|
|
475
|
+
const rcPath = (0, import_node_path.resolve)(".brandspecrc");
|
|
476
|
+
if (!(0, import_node_fs.existsSync)(rcPath)) {
|
|
477
|
+
(0, import_node_fs.writeFileSync)(rcPath, `remote: ${org}/${brand}
|
|
478
|
+
`, "utf-8");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function saveCredentials(token) {
|
|
482
|
+
const credPath = getCredentialsPath();
|
|
483
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(credPath), { recursive: true });
|
|
484
|
+
(0, import_node_fs.writeFileSync)(credPath, token, { mode: 384 });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// cli/color.ts
|
|
488
|
+
function parseColor(color) {
|
|
489
|
+
const hex = color.match(/^#([0-9a-f]{3,8})$/i);
|
|
490
|
+
if (hex) {
|
|
491
|
+
let h = hex[1];
|
|
492
|
+
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
493
|
+
return [
|
|
494
|
+
parseInt(h.slice(0, 2), 16),
|
|
495
|
+
parseInt(h.slice(2, 4), 16),
|
|
496
|
+
parseInt(h.slice(4, 6), 16)
|
|
497
|
+
];
|
|
498
|
+
}
|
|
499
|
+
const rgb = color.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
|
|
500
|
+
if (rgb) {
|
|
501
|
+
return [parseInt(rgb[1]), parseInt(rgb[2]), parseInt(rgb[3])];
|
|
502
|
+
}
|
|
503
|
+
const oklch = color.match(
|
|
504
|
+
/oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+([\d.]+)/
|
|
505
|
+
);
|
|
506
|
+
if (oklch) {
|
|
507
|
+
const L = oklch[2] === "%" ? parseFloat(oklch[1]) / 100 : parseFloat(oklch[1]);
|
|
508
|
+
const C = parseFloat(oklch[3]);
|
|
509
|
+
const H = parseFloat(oklch[4]);
|
|
510
|
+
return oklchToSrgb(L, C, H);
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
function oklchToSrgb(L, C, H) {
|
|
515
|
+
const hRad = H * Math.PI / 180;
|
|
516
|
+
const a = C * Math.cos(hRad);
|
|
517
|
+
const b = C * Math.sin(hRad);
|
|
518
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
519
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
520
|
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b;
|
|
521
|
+
const l = l_ * l_ * l_;
|
|
522
|
+
const m = m_ * m_ * m_;
|
|
523
|
+
const s = s_ * s_ * s_;
|
|
524
|
+
const rl = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
|
|
525
|
+
const gl = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
|
|
526
|
+
const bl = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
|
|
527
|
+
const gamma = (x) => x <= 31308e-7 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055;
|
|
528
|
+
const clamp = (x) => Math.max(0, Math.min(255, Math.round(x * 255)));
|
|
529
|
+
return [clamp(gamma(rl)), clamp(gamma(gl)), clamp(gamma(bl))];
|
|
530
|
+
}
|
|
531
|
+
function relativeLuminance(r, g, b) {
|
|
532
|
+
const [rs, gs, bs] = [r, g, b].map((c) => {
|
|
533
|
+
const s = c / 255;
|
|
534
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
535
|
+
});
|
|
536
|
+
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
537
|
+
}
|
|
538
|
+
function getContrastRatio(color1, color2) {
|
|
539
|
+
const c1 = parseColor(color1);
|
|
540
|
+
const c2 = parseColor(color2);
|
|
541
|
+
if (!c1 || !c2) return 21;
|
|
542
|
+
const l1 = relativeLuminance(...c1);
|
|
543
|
+
const l2 = relativeLuminance(...c2);
|
|
544
|
+
const lighter = Math.max(l1, l2);
|
|
545
|
+
const darker = Math.min(l1, l2);
|
|
546
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
547
|
+
}
|
|
548
|
+
function findColorValue(colors, name) {
|
|
549
|
+
const token = colors[name];
|
|
550
|
+
if (isToken(token)) return token.$value;
|
|
551
|
+
if (typeof token === "object" && token !== null) {
|
|
552
|
+
const nested = token;
|
|
553
|
+
if (isToken(nested)) return nested.$value;
|
|
554
|
+
}
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
function isToken(v) {
|
|
558
|
+
return typeof v === "object" && v !== null && "$value" in v;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// cli/lint.ts
|
|
562
|
+
var requiredFields = (spec) => {
|
|
563
|
+
const results = [];
|
|
564
|
+
if (!spec.meta.version) {
|
|
565
|
+
results.push({
|
|
566
|
+
rule: "meta/version-required",
|
|
567
|
+
severity: "warning",
|
|
568
|
+
message: "meta.version is recommended for tracking changes",
|
|
569
|
+
path: "meta.version"
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
if (!spec.core) {
|
|
573
|
+
results.push({
|
|
574
|
+
rule: "core/missing",
|
|
575
|
+
severity: "warning",
|
|
576
|
+
message: "core section is missing \u2014 brand identity is undefined",
|
|
577
|
+
path: "core"
|
|
578
|
+
});
|
|
579
|
+
} else {
|
|
580
|
+
if (!spec.core.essence && !spec.core.tagline) {
|
|
581
|
+
results.push({
|
|
582
|
+
rule: "core/identity-missing",
|
|
583
|
+
severity: "warning",
|
|
584
|
+
message: "Neither essence nor tagline is defined",
|
|
585
|
+
path: "core"
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
if (!spec.core.personality || spec.core.personality.length === 0) {
|
|
589
|
+
results.push({
|
|
590
|
+
rule: "core/personality-empty",
|
|
591
|
+
severity: "info",
|
|
592
|
+
message: "No personality traits defined",
|
|
593
|
+
path: "core.personality"
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (!spec.core.voice) {
|
|
597
|
+
results.push({
|
|
598
|
+
rule: "core/voice-missing",
|
|
599
|
+
severity: "info",
|
|
600
|
+
message: "Voice guidelines not defined",
|
|
601
|
+
path: "core.voice"
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (!spec.tokens) {
|
|
606
|
+
results.push({
|
|
607
|
+
rule: "tokens/missing",
|
|
608
|
+
severity: "warning",
|
|
609
|
+
message: "No design tokens defined",
|
|
610
|
+
path: "tokens"
|
|
611
|
+
});
|
|
612
|
+
} else {
|
|
613
|
+
if (!spec.tokens.colors || Object.keys(spec.tokens.colors).length === 0) {
|
|
614
|
+
results.push({
|
|
615
|
+
rule: "tokens/colors-empty",
|
|
616
|
+
severity: "warning",
|
|
617
|
+
message: "No color tokens defined",
|
|
618
|
+
path: "tokens.colors"
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
if (!spec.tokens.typography || Object.keys(spec.tokens.typography).length === 0) {
|
|
622
|
+
results.push({
|
|
623
|
+
rule: "tokens/typography-empty",
|
|
624
|
+
severity: "info",
|
|
625
|
+
message: "No typography tokens defined",
|
|
626
|
+
path: "tokens.typography"
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return results;
|
|
631
|
+
};
|
|
632
|
+
var colorContrast = (spec) => {
|
|
633
|
+
var _a;
|
|
634
|
+
const results = [];
|
|
635
|
+
const colors = (_a = spec.tokens) == null ? void 0 : _a.colors;
|
|
636
|
+
if (!colors) return results;
|
|
637
|
+
const bg = findColorValue(colors, "background");
|
|
638
|
+
const fg = findColorValue(colors, "foreground");
|
|
639
|
+
if (bg && fg) {
|
|
640
|
+
const ratio = getContrastRatio(bg, fg);
|
|
641
|
+
if (ratio < 4.5) {
|
|
642
|
+
results.push({
|
|
643
|
+
rule: "contrast/bg-fg-aa",
|
|
644
|
+
severity: "error",
|
|
645
|
+
message: `Background/foreground contrast ratio is ${ratio.toFixed(1)}:1 (WCAG AA requires 4.5:1)`,
|
|
646
|
+
path: "tokens.colors"
|
|
647
|
+
});
|
|
648
|
+
} else if (ratio < 7) {
|
|
649
|
+
results.push({
|
|
650
|
+
rule: "contrast/bg-fg-aaa",
|
|
651
|
+
severity: "info",
|
|
652
|
+
message: `Background/foreground contrast ratio is ${ratio.toFixed(1)}:1 (WCAG AAA requires 7:1)`,
|
|
653
|
+
path: "tokens.colors"
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const primary = findColorValue(colors, "primary");
|
|
658
|
+
if (primary && bg) {
|
|
659
|
+
const ratio = getContrastRatio(bg, primary);
|
|
660
|
+
if (ratio < 3) {
|
|
661
|
+
results.push({
|
|
662
|
+
rule: "contrast/primary-bg",
|
|
663
|
+
severity: "warning",
|
|
664
|
+
message: `Primary on background contrast ratio is ${ratio.toFixed(1)}:1 (minimum 3:1 for large text)`,
|
|
665
|
+
path: "tokens.colors.primary"
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return results;
|
|
670
|
+
};
|
|
671
|
+
var ASSET_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*(\.[a-z0-9]+)+$/;
|
|
672
|
+
var assetNaming = (spec) => {
|
|
673
|
+
const results = [];
|
|
674
|
+
if (!spec.assets) return results;
|
|
675
|
+
for (const asset of spec.assets) {
|
|
676
|
+
const fileName = asset.file.split("/").pop() ?? asset.file;
|
|
677
|
+
if (!ASSET_PATTERN.test(fileName)) {
|
|
678
|
+
results.push({
|
|
679
|
+
rule: "assets/naming-convention",
|
|
680
|
+
severity: "warning",
|
|
681
|
+
message: `Asset "${asset.file}" doesn't follow {role}-{variant}.{ext} naming convention`,
|
|
682
|
+
path: "assets"
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (!asset.role) {
|
|
686
|
+
results.push({
|
|
687
|
+
rule: "assets/role-missing",
|
|
688
|
+
severity: "info",
|
|
689
|
+
message: `Asset "${asset.file}" has no role defined`,
|
|
690
|
+
path: "assets"
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return results;
|
|
695
|
+
};
|
|
696
|
+
var KEBAB_CASE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
697
|
+
var tokenNaming = (spec) => {
|
|
698
|
+
const results = [];
|
|
699
|
+
if (!spec.tokens) return results;
|
|
700
|
+
for (const [group, tokens] of Object.entries(spec.tokens)) {
|
|
701
|
+
if (!tokens || typeof tokens !== "object") continue;
|
|
702
|
+
for (const name of Object.keys(tokens)) {
|
|
703
|
+
if (name.startsWith("$")) continue;
|
|
704
|
+
if (!KEBAB_CASE.test(name)) {
|
|
705
|
+
results.push({
|
|
706
|
+
rule: "tokens/naming-kebab",
|
|
707
|
+
severity: "info",
|
|
708
|
+
message: `Token "${group}.${name}" should use kebab-case`,
|
|
709
|
+
path: `tokens.${group}.${name}`
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return results;
|
|
715
|
+
};
|
|
716
|
+
var essentialColors = (spec) => {
|
|
717
|
+
var _a;
|
|
718
|
+
const results = [];
|
|
719
|
+
const colors = (_a = spec.tokens) == null ? void 0 : _a.colors;
|
|
720
|
+
if (!colors) return results;
|
|
721
|
+
const essential = ["primary", "background", "foreground"];
|
|
722
|
+
for (const name of essential) {
|
|
723
|
+
if (!findColorValue(colors, name)) {
|
|
724
|
+
results.push({
|
|
725
|
+
rule: "tokens/essential-color",
|
|
726
|
+
severity: "warning",
|
|
727
|
+
message: `Essential color token "${name}" is missing`,
|
|
728
|
+
path: `tokens.colors.${name}`
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return results;
|
|
733
|
+
};
|
|
734
|
+
var ALL_RULES = [
|
|
735
|
+
requiredFields,
|
|
736
|
+
colorContrast,
|
|
737
|
+
assetNaming,
|
|
738
|
+
tokenNaming,
|
|
739
|
+
essentialColors
|
|
740
|
+
];
|
|
741
|
+
function lintBrandspec(spec) {
|
|
742
|
+
const results = ALL_RULES.flatMap((rule) => rule(spec));
|
|
743
|
+
const errors = results.filter((r) => r.severity === "error").length;
|
|
744
|
+
const warnings = results.filter((r) => r.severity === "warning").length;
|
|
745
|
+
const infos = results.filter((r) => r.severity === "info").length;
|
|
746
|
+
const score = Math.max(
|
|
747
|
+
0,
|
|
748
|
+
Math.min(100, 100 - errors * 10 - warnings * 3 - infos)
|
|
749
|
+
);
|
|
750
|
+
return { score, results, errors, warnings, infos };
|
|
751
|
+
}
|
|
752
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
753
|
+
0 && (module.exports = {
|
|
754
|
+
API_BASE,
|
|
755
|
+
ensureBrandspecrc,
|
|
756
|
+
flattenTokens,
|
|
757
|
+
getContrastRatio,
|
|
758
|
+
getCredentialsPath,
|
|
759
|
+
lintBrandspec,
|
|
760
|
+
loadRemote,
|
|
761
|
+
loadToken,
|
|
762
|
+
parse,
|
|
763
|
+
parseColor,
|
|
764
|
+
parseOrgBrand,
|
|
765
|
+
saveCredentials,
|
|
766
|
+
schema,
|
|
767
|
+
serialize,
|
|
768
|
+
toCss,
|
|
769
|
+
toFigmaTokens,
|
|
770
|
+
toStyleDictionary,
|
|
771
|
+
toTailwindCss,
|
|
772
|
+
validate
|
|
773
|
+
});
|