@advantacode/brander 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 +371 -0
- package/dist/adapters/css.js +39 -0
- package/dist/adapters/figma.js +28 -0
- package/dist/adapters/json.js +10 -0
- package/dist/adapters/scss.js +59 -0
- package/dist/adapters/tailwind.js +18 -0
- package/dist/adapters/typescript.js +13 -0
- package/dist/adapters/variables.js +21 -0
- package/dist/engine/color-parser.js +64 -0
- package/dist/engine/palette.js +46 -0
- package/dist/engine/semantics.js +145 -0
- package/dist/engine/themes.js +25 -0
- package/dist/generate-tokens.js +220 -0
- package/dist/index.js +205 -0
- package/dist/setup.js +153 -0
- package/dist/tailwind-colors.js +288 -0
- package/docs/CONTRIBUTING.md +41 -0
- package/docs/TECH_OVERVIEW.md +780 -0
- package/docs/TRADEMARKS.md +37 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AdvantaCode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# AdvantaCode Brander
|
|
2
|
+
|
|
3
|
+
AdvantaCode Brander is a design token generator that produces consistent branding tokens for modern web applications.
|
|
4
|
+
|
|
5
|
+
It converts a simple configuration into reusable outputs for multiple platforms including:
|
|
6
|
+
|
|
7
|
+
* CSS variables
|
|
8
|
+
* TypeScript tokens
|
|
9
|
+
* Tailwind presets
|
|
10
|
+
* Bootstrap / SCSS variables
|
|
11
|
+
* Figma tokens
|
|
12
|
+
|
|
13
|
+
AdvantaCode Brander uses OKLCH color space to generate perceptually consistent color scales.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -D @advantacode/brander
|
|
19
|
+
npx --package @advantacode/brander advantacode-brander setup --out src/brander --style src/style.css
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This creates `brand.config.ts`, adds a `brand:generate` script, patches your stylesheet imports, and prepares the token output folder.
|
|
23
|
+
|
|
24
|
+
AdvantaCode Brander generates design tokens and framework adapters from a single brand configuration file. It allows applications, design systems, and design tools to share a consistent source of truth for colors and semantic tokens.
|
|
25
|
+
|
|
26
|
+
For architecture, development, testing, and publishing workflows, see [docs/TECH_OVERVIEW.md](docs/TECH_OVERVIEW.md).
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
* Single source of truth for brand tokens
|
|
31
|
+
* OKLCH color scaling
|
|
32
|
+
* Multi-framework outputs
|
|
33
|
+
* CLI tool usable across any JavaScript stack
|
|
34
|
+
* Environment variable support
|
|
35
|
+
* Tailwind integration
|
|
36
|
+
* Design-tool exports for Figma
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
Node.js 20 or newer is required.
|
|
41
|
+
|
|
42
|
+
Recommended for app projects:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install -D @advantacode/brander
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
One-off usage with `npx`:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx --package @advantacode/brander advantacode-brander
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Global install:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install -g @advantacode/brander
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Run:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
advantacode-brander
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## CLI Usage
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
advantacode-brander --help
|
|
72
|
+
advantacode-brander --version
|
|
73
|
+
advantacode-brander --out src/tokens
|
|
74
|
+
advantacode-brander --format css,tailwind,figma
|
|
75
|
+
advantacode-brander --theme dark
|
|
76
|
+
advantacode-brander --prefix ac
|
|
77
|
+
advantacode-brander setup --out src/brander --style src/style.css
|
|
78
|
+
advantacode-brander init --out src/brander
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Supported flags:
|
|
82
|
+
|
|
83
|
+
* `--out <dir>` writes generated files to a custom folder instead of `dist/brander`
|
|
84
|
+
* `--format <list>` limits output to specific formats: `all`, `css`, `json`, `typescript` or `ts`, `scss`, `tailwind`, `bootstrap`, `figma`
|
|
85
|
+
* `--theme <value>` limits theme CSS output to `light`, `dark`, or `both`
|
|
86
|
+
* `--prefix <value>` applies a CSS variable prefix like `ac`, producing variables such as `--ac-primary`
|
|
87
|
+
* `--version`, `-v` prints the installed package version
|
|
88
|
+
* `--help`, `-h` prints the CLI help text
|
|
89
|
+
|
|
90
|
+
Setup commands:
|
|
91
|
+
|
|
92
|
+
* `advantacode-brander setup` configures an existing app by creating `brand.config.ts` if needed, adding a `brand:generate` script, patching a stylesheet with token imports, and generating tokens
|
|
93
|
+
* `advantacode-brander init` runs the same setup flow for a freshly created app and is intended to be called by a higher-level scaffolder such as `advantacode-init`
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
Create a `brand.config.ts` file in your project root.
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
export default {
|
|
103
|
+
name: process.env.COMPANY_NAME || "My Company",
|
|
104
|
+
css: {
|
|
105
|
+
prefix: process.env.CSS_PREFIX ?? ""
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
colors: {
|
|
109
|
+
primary: process.env.PRIMARY_COLOR || "amber-500",
|
|
110
|
+
secondary: process.env.SECONDARY_COLOR || "zinc-700",
|
|
111
|
+
neutral: process.env.NEUTRAL_COLOR || process.env.SECONDARY_COLOR || "zinc-700",
|
|
112
|
+
accent: process.env.ACCENT_COLOR || "amber-400",
|
|
113
|
+
info: process.env.INFO_COLOR || "sky-500",
|
|
114
|
+
success: process.env.SUCCESS_COLOR || "green-500",
|
|
115
|
+
warning: process.env.WARNING_COLOR || "yellow-500",
|
|
116
|
+
danger: process.env.DANGER_COLOR || "red-500"
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Supported color inputs:
|
|
122
|
+
|
|
123
|
+
* Tailwind-style tokens like `amber-500`
|
|
124
|
+
* CSS color strings like `#f59e0b` or `rgb(245 158 11)`
|
|
125
|
+
* OKLCH values like `oklch(0.76859 0.164659 70.08)`
|
|
126
|
+
|
|
127
|
+
## Environment Variables
|
|
128
|
+
|
|
129
|
+
Brander supports environment variables via `.env`.
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
|
|
133
|
+
```dotenv
|
|
134
|
+
COMPANY_NAME="My Company"
|
|
135
|
+
CSS_PREFIX=
|
|
136
|
+
PRIMARY_COLOR=amber-500
|
|
137
|
+
SECONDARY_COLOR=zinc-700
|
|
138
|
+
NEUTRAL_COLOR=zinc-700
|
|
139
|
+
ACCENT_COLOR=amber-400
|
|
140
|
+
INFO_COLOR=sky-500
|
|
141
|
+
SUCCESS_COLOR=green-500
|
|
142
|
+
WARNING_COLOR=yellow-500
|
|
143
|
+
DANGER_COLOR=red-500
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Generated Outputs
|
|
147
|
+
|
|
148
|
+
Running the CLI with no flags generates all formats into `dist/brander` and writes both light and dark theme CSS.
|
|
149
|
+
|
|
150
|
+
```text
|
|
151
|
+
dist/
|
|
152
|
+
brander/
|
|
153
|
+
tokens.css
|
|
154
|
+
tokens.scss
|
|
155
|
+
tokens.ts
|
|
156
|
+
tokens.json
|
|
157
|
+
metadata.json
|
|
158
|
+
themes/
|
|
159
|
+
light.css
|
|
160
|
+
dark.css
|
|
161
|
+
adapters/
|
|
162
|
+
tailwind.preset.ts
|
|
163
|
+
bootstrap.variables.scss
|
|
164
|
+
figma.tokens.json
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## CSS Variables
|
|
168
|
+
|
|
169
|
+
Example generated `tokens.css`:
|
|
170
|
+
|
|
171
|
+
```css
|
|
172
|
+
:root {
|
|
173
|
+
--primary-500: oklch(0.65 0.2 45);
|
|
174
|
+
--neutral-50: oklch(0.97 0.02 95);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Usage:
|
|
179
|
+
|
|
180
|
+
```css
|
|
181
|
+
:root {
|
|
182
|
+
--background: var(--neutral-50);
|
|
183
|
+
--text: var(--neutral-950);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
By default, Brander emits unprefixed variables for broader compatibility. If you want namespaced output, use `css.prefix` in `brand.config.ts`, `CSS_PREFIX` in `.env`, or `--prefix ac` on the CLI.
|
|
188
|
+
|
|
189
|
+
## Theme Tokens
|
|
190
|
+
|
|
191
|
+
Example:
|
|
192
|
+
|
|
193
|
+
```css
|
|
194
|
+
:root {
|
|
195
|
+
--background: var(--neutral-50);
|
|
196
|
+
--surface: var(--neutral-100);
|
|
197
|
+
--text: var(--neutral-950);
|
|
198
|
+
--muted: var(--neutral-100);
|
|
199
|
+
--card: var(--neutral-50);
|
|
200
|
+
--border: var(--neutral-200);
|
|
201
|
+
--primary: var(--primary-600);
|
|
202
|
+
--primary-foreground: var(--neutral-50);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
[data-theme="dark"] {
|
|
206
|
+
--background: var(--neutral-950);
|
|
207
|
+
--surface: var(--neutral-900);
|
|
208
|
+
--text: var(--neutral-50);
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Core semantic tokens include `background`, `surface`, `text`, `muted`, `card`, `popover`, `border`, `input`, `ring`, `primary`, `secondary`, `accent`, `info`, `success`, `warning`, and `danger`, each with matching foreground tokens where appropriate.
|
|
213
|
+
|
|
214
|
+
## TypeScript Tokens
|
|
215
|
+
|
|
216
|
+
Generated file:
|
|
217
|
+
|
|
218
|
+
```text
|
|
219
|
+
dist/brander/tokens.ts
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Usage:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { tokens, metadata } from "./dist/brander/tokens";
|
|
226
|
+
|
|
227
|
+
console.log(tokens.color.semantic.light.primary.value);
|
|
228
|
+
console.log(metadata.adapters);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
This output is intended for typed apps, scripts, and build-time tooling.
|
|
232
|
+
|
|
233
|
+
## SCSS Tokens
|
|
234
|
+
|
|
235
|
+
Generated file:
|
|
236
|
+
|
|
237
|
+
```text
|
|
238
|
+
dist/brander/tokens.scss
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Usage:
|
|
242
|
+
|
|
243
|
+
```scss
|
|
244
|
+
@use "./dist/brander/tokens.scss" as tokens;
|
|
245
|
+
|
|
246
|
+
.button {
|
|
247
|
+
background: tokens.$primary;
|
|
248
|
+
color: tokens.$primary-foreground;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Light semantic tokens are emitted as Sass variables like `$background`, and dark semantic tokens are emitted as `$dark-background`. Prefixing works here too when configured.
|
|
253
|
+
|
|
254
|
+
## Tailwind Integration
|
|
255
|
+
|
|
256
|
+
Generated preset:
|
|
257
|
+
|
|
258
|
+
```text
|
|
259
|
+
dist/brander/adapters/tailwind.preset.ts
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Usage:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import preset from "./dist/brander/adapters/tailwind.preset";
|
|
266
|
+
|
|
267
|
+
export default {
|
|
268
|
+
presets: [preset]
|
|
269
|
+
};
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Use tokens in Tailwind:
|
|
273
|
+
|
|
274
|
+
```text
|
|
275
|
+
bg-primary
|
|
276
|
+
text-danger
|
|
277
|
+
border-secondary
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Bootstrap / SCSS Frameworks
|
|
281
|
+
|
|
282
|
+
Generated file:
|
|
283
|
+
|
|
284
|
+
```text
|
|
285
|
+
dist/brander/adapters/bootstrap.variables.scss
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Example output:
|
|
289
|
+
|
|
290
|
+
```scss
|
|
291
|
+
@use "../tokens.scss" as tokens;
|
|
292
|
+
|
|
293
|
+
$primary: tokens.$primary;
|
|
294
|
+
$secondary: tokens.$secondary;
|
|
295
|
+
$info: tokens.$info;
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Figma Token Export
|
|
299
|
+
|
|
300
|
+
Generated:
|
|
301
|
+
|
|
302
|
+
```text
|
|
303
|
+
dist/brander/adapters/figma.tokens.json
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Example:
|
|
307
|
+
|
|
308
|
+
```json
|
|
309
|
+
{
|
|
310
|
+
"color": {
|
|
311
|
+
"primary": {
|
|
312
|
+
"value": "oklch(0.65 0.2 45)"
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
This allows importing tokens into design tools.
|
|
319
|
+
|
|
320
|
+
## Metadata
|
|
321
|
+
|
|
322
|
+
Generated file:
|
|
323
|
+
|
|
324
|
+
```text
|
|
325
|
+
dist/brander/metadata.json
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Example:
|
|
329
|
+
|
|
330
|
+
```json
|
|
331
|
+
{
|
|
332
|
+
"version": "0.1.0",
|
|
333
|
+
"generated": "2026-03-08T17:47:26.019Z",
|
|
334
|
+
"themes": ["light", "dark"],
|
|
335
|
+
"adapters": ["tailwind", "bootstrap", "figma"],
|
|
336
|
+
"artifacts": ["tokens.css", "themes/light.css", "themes/dark.css"],
|
|
337
|
+
"cssPrefix": ""
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Ecosystem
|
|
342
|
+
|
|
343
|
+
AdvantaCode Brander is part of the AdvantaCode ecosystem.
|
|
344
|
+
|
|
345
|
+
```text
|
|
346
|
+
@advantacode/brander
|
|
347
|
+
advantacode-init
|
|
348
|
+
advantacode-starter
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
This allows developers to bootstrap fully branded applications with a single command.
|
|
352
|
+
|
|
353
|
+
## Contributing
|
|
354
|
+
|
|
355
|
+
AdvantaCode Brander is maintained under a closed governance model.
|
|
356
|
+
|
|
357
|
+
Issues and feature requests are welcome, but pull requests may not be accepted.
|
|
358
|
+
|
|
359
|
+
See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for details.
|
|
360
|
+
|
|
361
|
+
## Trademark Notice
|
|
362
|
+
|
|
363
|
+
`AdvantaCode` and `AdvantaCode Brander` are trademarks of AdvantaCode.
|
|
364
|
+
|
|
365
|
+
The MIT license for this package does not grant permission to use the AdvantaCode name, logos, package names, domains, or branding, or to imply endorsement or affiliation.
|
|
366
|
+
|
|
367
|
+
See [docs/TRADEMARKS.md](docs/TRADEMARKS.md) for the trademark policy.
|
|
368
|
+
|
|
369
|
+
## License
|
|
370
|
+
|
|
371
|
+
MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { scaleSteps } from "../engine/palette.js";
|
|
4
|
+
import { semanticTokenNames } from "../engine/semantics.js";
|
|
5
|
+
import { getVariableName, getVariableReference } from "./variables.js";
|
|
6
|
+
export function writeCssArtifacts(outputDir, tokenModel, theme, variableOptions) {
|
|
7
|
+
const themesDir = path.join(outputDir, "themes");
|
|
8
|
+
const writtenArtifacts = ["tokens.css"];
|
|
9
|
+
fs.mkdirSync(themesDir, { recursive: true });
|
|
10
|
+
fs.writeFileSync(path.join(outputDir, "tokens.css"), renderPrimitiveTokens(tokenModel, variableOptions));
|
|
11
|
+
if (theme === "light" || theme === "both") {
|
|
12
|
+
fs.writeFileSync(path.join(themesDir, "light.css"), renderThemeCss(":root", tokenModel, "light", variableOptions));
|
|
13
|
+
writtenArtifacts.push("themes/light.css");
|
|
14
|
+
}
|
|
15
|
+
if (theme === "dark" || theme === "both") {
|
|
16
|
+
fs.writeFileSync(path.join(themesDir, "dark.css"), renderThemeCss('[data-theme="dark"]', tokenModel, "dark", variableOptions));
|
|
17
|
+
writtenArtifacts.push("themes/dark.css");
|
|
18
|
+
}
|
|
19
|
+
return writtenArtifacts;
|
|
20
|
+
}
|
|
21
|
+
function renderPrimitiveTokens(tokenModel, variableOptions) {
|
|
22
|
+
let css = ":root {\n";
|
|
23
|
+
for (const [colorName, scale] of Object.entries(tokenModel.color.primitive)) {
|
|
24
|
+
for (const step of scaleSteps) {
|
|
25
|
+
css += ` ${getVariableName(`${colorName}-${step}`, variableOptions)}: ${scale[step]};\n`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
css += "}\n";
|
|
29
|
+
return css;
|
|
30
|
+
}
|
|
31
|
+
function renderThemeCss(selector, tokenModel, themeName, variableOptions) {
|
|
32
|
+
let css = `${selector} {\n`;
|
|
33
|
+
for (const semanticTokenName of semanticTokenNames) {
|
|
34
|
+
const token = tokenModel.color.semantic[themeName][semanticTokenName];
|
|
35
|
+
css += ` ${getVariableName(semanticTokenName, variableOptions)}: ${getVariableReference(token.ref, variableOptions)};\n`;
|
|
36
|
+
}
|
|
37
|
+
css += "}\n";
|
|
38
|
+
return css;
|
|
39
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { scaleSteps } from "../engine/palette.js";
|
|
4
|
+
import { semanticTokenNames, themeNames } from "../engine/semantics.js";
|
|
5
|
+
export function writeFigmaAdapter(outputDir, tokenModel) {
|
|
6
|
+
const adaptersDir = path.join(outputDir, "adapters");
|
|
7
|
+
const figmaTokens = {
|
|
8
|
+
color: {
|
|
9
|
+
primitive: Object.fromEntries(Object.entries(tokenModel.color.primitive).map(([colorName, scale]) => [
|
|
10
|
+
colorName,
|
|
11
|
+
Object.fromEntries(scaleSteps.map((step) => [step, { value: scale[step] }]))
|
|
12
|
+
])),
|
|
13
|
+
semantic: Object.fromEntries(themeNames.map((themeName) => [
|
|
14
|
+
themeName,
|
|
15
|
+
Object.fromEntries(semanticTokenNames.map((semanticTokenName) => [
|
|
16
|
+
semanticTokenName,
|
|
17
|
+
{
|
|
18
|
+
value: tokenModel.color.semantic[themeName][semanticTokenName].value,
|
|
19
|
+
ref: tokenModel.color.semantic[themeName][semanticTokenName].ref
|
|
20
|
+
}
|
|
21
|
+
]))
|
|
22
|
+
]))
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
fs.mkdirSync(adaptersDir, { recursive: true });
|
|
26
|
+
fs.writeFileSync(path.join(adaptersDir, "figma.tokens.json"), JSON.stringify(figmaTokens, null, 2));
|
|
27
|
+
return ["adapters/figma.tokens.json"];
|
|
28
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export function writeTokenModelJson(outputDir, tokenModel) {
|
|
4
|
+
fs.writeFileSync(path.join(outputDir, "tokens.json"), JSON.stringify(tokenModel, null, 2));
|
|
5
|
+
return ["tokens.json"];
|
|
6
|
+
}
|
|
7
|
+
export function writeMetadataJson(outputDir, metadata) {
|
|
8
|
+
fs.writeFileSync(path.join(outputDir, "metadata.json"), JSON.stringify(metadata, null, 2));
|
|
9
|
+
return ["metadata.json"];
|
|
10
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { scaleSteps } from "../engine/palette.js";
|
|
4
|
+
import { semanticTokenNames } from "../engine/semantics.js";
|
|
5
|
+
import { getSassVariableName } from "./variables.js";
|
|
6
|
+
export function writeScssArtifacts(outputDir, tokenModel, options) {
|
|
7
|
+
const adaptersDir = path.join(outputDir, "adapters");
|
|
8
|
+
const writtenArtifacts = [];
|
|
9
|
+
const tokensScss = renderTokensScss(tokenModel, options.variableOptions);
|
|
10
|
+
const bootstrapScss = `@use "../tokens.scss" as tokens;
|
|
11
|
+
|
|
12
|
+
$body-bg: tokens.${getSassVariableName("background", options.variableOptions)};
|
|
13
|
+
$body-color: tokens.${getSassVariableName("text", options.variableOptions)};
|
|
14
|
+
$border-color: tokens.${getSassVariableName("border", options.variableOptions)};
|
|
15
|
+
$primary: tokens.${getSassVariableName("primary", options.variableOptions)};
|
|
16
|
+
$secondary: tokens.${getSassVariableName("secondary", options.variableOptions)};
|
|
17
|
+
$info: tokens.${getSassVariableName("info", options.variableOptions)};
|
|
18
|
+
$success: tokens.${getSassVariableName("success", options.variableOptions)};
|
|
19
|
+
$warning: tokens.${getSassVariableName("warning", options.variableOptions)};
|
|
20
|
+
$danger: tokens.${getSassVariableName("danger", options.variableOptions)};
|
|
21
|
+
$card-bg: tokens.${getSassVariableName("surface", options.variableOptions)};
|
|
22
|
+
`;
|
|
23
|
+
fs.mkdirSync(adaptersDir, { recursive: true });
|
|
24
|
+
if (options.includeTokensScss || options.includeBootstrapAdapter) {
|
|
25
|
+
fs.writeFileSync(path.join(outputDir, "tokens.scss"), tokensScss);
|
|
26
|
+
writtenArtifacts.push("tokens.scss");
|
|
27
|
+
}
|
|
28
|
+
if (options.includeBootstrapAdapter) {
|
|
29
|
+
fs.writeFileSync(path.join(adaptersDir, "bootstrap.variables.scss"), bootstrapScss);
|
|
30
|
+
writtenArtifacts.push("adapters/bootstrap.variables.scss");
|
|
31
|
+
}
|
|
32
|
+
return writtenArtifacts;
|
|
33
|
+
}
|
|
34
|
+
function renderTokensScss(tokenModel, variableOptions) {
|
|
35
|
+
let scss = "";
|
|
36
|
+
for (const [colorName, scale] of Object.entries(tokenModel.color.primitive)) {
|
|
37
|
+
for (const step of scaleSteps) {
|
|
38
|
+
scss += `${getSassVariableName(`${colorName}-${step}`, variableOptions)}: ${scale[step]};\n`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
scss += "\n";
|
|
42
|
+
for (const semanticTokenName of semanticTokenNames) {
|
|
43
|
+
scss += `${getSassVariableName(semanticTokenName, variableOptions)}: ${tokenModel.color.semantic.light[semanticTokenName].value};\n`;
|
|
44
|
+
}
|
|
45
|
+
scss += `\n${getSassVariableName("theme-light", variableOptions)}: (\n`;
|
|
46
|
+
for (const semanticTokenName of semanticTokenNames) {
|
|
47
|
+
scss += ` "${semanticTokenName}": ${getSassVariableName(semanticTokenName, variableOptions)},\n`;
|
|
48
|
+
}
|
|
49
|
+
scss += ");\n\n";
|
|
50
|
+
for (const semanticTokenName of semanticTokenNames) {
|
|
51
|
+
scss += `${getSassVariableName(`dark-${semanticTokenName}`, variableOptions)}: ${tokenModel.color.semantic.dark[semanticTokenName].value};\n`;
|
|
52
|
+
}
|
|
53
|
+
scss += `\n${getSassVariableName("theme-dark", variableOptions)}: (\n`;
|
|
54
|
+
for (const semanticTokenName of semanticTokenNames) {
|
|
55
|
+
scss += ` "${semanticTokenName}": ${getSassVariableName(`dark-${semanticTokenName}`, variableOptions)},\n`;
|
|
56
|
+
}
|
|
57
|
+
scss += ");\n";
|
|
58
|
+
return scss;
|
|
59
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { semanticTokenNames } from "../engine/semantics.js";
|
|
4
|
+
import { getVariableReference } from "./variables.js";
|
|
5
|
+
export function writeTailwindAdapter(outputDir, tokenModel, variableOptions) {
|
|
6
|
+
const adaptersDir = path.join(outputDir, "adapters");
|
|
7
|
+
let preset = `export default {\n theme: {\n extend: {\n colors: {\n`;
|
|
8
|
+
fs.mkdirSync(adaptersDir, { recursive: true });
|
|
9
|
+
for (const semanticTokenName of semanticTokenNames) {
|
|
10
|
+
if (!tokenModel.color.semantic.light[semanticTokenName]) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
preset += ` "${semanticTokenName}": "${getVariableReference(semanticTokenName, variableOptions)}",\n`;
|
|
14
|
+
}
|
|
15
|
+
preset += " }\n }\n }\n};\n";
|
|
16
|
+
fs.writeFileSync(path.join(adaptersDir, "tailwind.preset.ts"), preset);
|
|
17
|
+
return ["adapters/tailwind.preset.ts"];
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export function writeTypeScriptArtifacts(outputDir, tokenModel, metadata) {
|
|
4
|
+
const source = `export const metadata = ${JSON.stringify(metadata, null, 2)} as const;
|
|
5
|
+
|
|
6
|
+
export const tokens = ${JSON.stringify(tokenModel, null, 2)} as const;
|
|
7
|
+
|
|
8
|
+
export type Metadata = typeof metadata;
|
|
9
|
+
export type Tokens = typeof tokens;
|
|
10
|
+
`;
|
|
11
|
+
fs.writeFileSync(path.join(outputDir, "tokens.ts"), source);
|
|
12
|
+
return ["tokens.ts"];
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function normalizeVariablePrefix(prefix) {
|
|
2
|
+
if (!prefix) {
|
|
3
|
+
return "";
|
|
4
|
+
}
|
|
5
|
+
return prefix.trim().replace(/^-+/, "").replace(/-+$/, "");
|
|
6
|
+
}
|
|
7
|
+
export function getVariableName(tokenName, options) {
|
|
8
|
+
const normalizedTokenName = tokenName.replace(".", "-");
|
|
9
|
+
return options.prefix
|
|
10
|
+
? `--${options.prefix}-${normalizedTokenName}`
|
|
11
|
+
: `--${normalizedTokenName}`;
|
|
12
|
+
}
|
|
13
|
+
export function getVariableReference(tokenName, options) {
|
|
14
|
+
return `var(${getVariableName(tokenName, options)})`;
|
|
15
|
+
}
|
|
16
|
+
export function getSassVariableName(tokenName, options) {
|
|
17
|
+
const normalizedTokenName = tokenName.replace(".", "-");
|
|
18
|
+
return options.prefix
|
|
19
|
+
? `$${options.prefix}-${normalizedTokenName}`
|
|
20
|
+
: `$${normalizedTokenName}`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { converter } from "culori";
|
|
2
|
+
import { tailwindColors } from "../tailwind-colors.js";
|
|
3
|
+
const toOklch = converter("oklch");
|
|
4
|
+
export const baseColorNames = [
|
|
5
|
+
"primary",
|
|
6
|
+
"secondary",
|
|
7
|
+
"accent",
|
|
8
|
+
"info",
|
|
9
|
+
"success",
|
|
10
|
+
"warning",
|
|
11
|
+
"danger",
|
|
12
|
+
"neutral"
|
|
13
|
+
];
|
|
14
|
+
const defaultBaseColors = {
|
|
15
|
+
primary: "amber-500",
|
|
16
|
+
secondary: "zinc-700",
|
|
17
|
+
accent: "amber-400",
|
|
18
|
+
info: "sky-500",
|
|
19
|
+
success: "green-500",
|
|
20
|
+
warning: "yellow-500",
|
|
21
|
+
danger: "red-500",
|
|
22
|
+
neutral: "zinc-700"
|
|
23
|
+
};
|
|
24
|
+
export function resolveBaseColors(colors) {
|
|
25
|
+
const mergedColors = {
|
|
26
|
+
...defaultBaseColors,
|
|
27
|
+
...colors,
|
|
28
|
+
neutral: colors.neutral ?? colors.secondary ?? defaultBaseColors.neutral
|
|
29
|
+
};
|
|
30
|
+
return Object.fromEntries(Object.entries(mergedColors).map(([colorName, colorValue]) => [colorName, normalizeColorValue(colorName, colorValue)]));
|
|
31
|
+
}
|
|
32
|
+
function normalizeColorValue(colorName, colorValue) {
|
|
33
|
+
const resolvedColorValue = resolveTailwindColorToken(colorValue) ?? colorValue;
|
|
34
|
+
const oklchColor = toOklch(resolvedColorValue);
|
|
35
|
+
if (!oklchColor) {
|
|
36
|
+
throw new Error(`Unable to parse color "${colorName}" with value "${colorValue}". Use a valid CSS color, OKLCH string, or Tailwind-style token like "amber-500".`);
|
|
37
|
+
}
|
|
38
|
+
return formatOklchColor(oklchColor);
|
|
39
|
+
}
|
|
40
|
+
function resolveTailwindColorToken(colorValue) {
|
|
41
|
+
const match = colorValue.match(/^([a-z]+)-(\d{2,3})$/);
|
|
42
|
+
if (!match) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const [, colorName, rawScale] = match;
|
|
46
|
+
const colorScale = Number(rawScale);
|
|
47
|
+
const palette = tailwindColors[colorName];
|
|
48
|
+
return palette?.[colorScale];
|
|
49
|
+
}
|
|
50
|
+
function formatOklchColor(color) {
|
|
51
|
+
const lightness = roundComponent(color.l, 6) ?? "none";
|
|
52
|
+
const chroma = roundComponent(color.c, 6) ?? "none";
|
|
53
|
+
const hue = roundComponent(color.h, 3) ?? "none";
|
|
54
|
+
const alpha = roundComponent(color.alpha, 3);
|
|
55
|
+
return alpha !== undefined && alpha < 1
|
|
56
|
+
? `oklch(${lightness} ${chroma} ${hue} / ${alpha})`
|
|
57
|
+
: `oklch(${lightness} ${chroma} ${hue})`;
|
|
58
|
+
}
|
|
59
|
+
function roundComponent(value, precision) {
|
|
60
|
+
if (value === undefined) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return Number(value.toFixed(precision));
|
|
64
|
+
}
|