shapes-ui 0.5.0 → 0.6.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/.github/workflows/pr-preview.yml +9 -2
- package/CHANGELOG.md +11 -0
- package/README.md +13 -0
- package/content/components/accordion.mdx +13 -0
- package/content/components/alert-dialog.mdx +34 -0
- package/content/components/autocomplete.mdx +62 -0
- package/content/components/avatar.mdx +11 -0
- package/content/components/button.mdx +8 -0
- package/content/components/checkbox.mdx +11 -0
- package/content/components/collapsible.mdx +11 -0
- package/content/components/combobox.mdx +33 -0
- package/content/components/context-menu.mdx +29 -0
- package/content/components/dialog.mdx +33 -0
- package/content/components/drawer.mdx +38 -0
- package/content/components/field.mdx +21 -0
- package/content/components/fieldset.mdx +10 -0
- package/content/components/form.mdx +8 -0
- package/content/components/input.mdx +4 -0
- package/content/components/menu.mdx +27 -0
- package/content/components/menubar.mdx +31 -0
- package/content/components/meter.mdx +14 -0
- package/content/components/navigation-menu.mdx +28 -0
- package/content/components/number-field.mdx +25 -0
- package/content/components/popover.mdx +22 -0
- package/content/components/preview-card.mdx +14 -2
- package/content/components/progress.mdx +15 -1
- package/content/components/radio.mdx +11 -0
- package/content/components/scroll-area.mdx +23 -0
- package/content/components/select.mdx +27 -0
- package/content/components/separator.mdx +29 -0
- package/content/components/slider.mdx +4 -0
- package/content/components/switch.mdx +4 -0
- package/content/components/tabs.mdx +15 -0
- package/content/components/toast.mdx +10 -0
- package/content/components/toggle-group.mdx +37 -0
- package/content/components/toggle.mdx +12 -0
- package/content/components/toolbar.mdx +22 -0
- package/content/components/tooltip.mdx +13 -0
- package/content/docs/installation.mdx +30 -0
- package/content-collections.ts +65 -1
- package/dist/cli.js +947 -101
- package/examples/__index.tsx +136 -68
- package/examples/autocomplete-align.tsx +39 -0
- package/examples/autocomplete-controlled.tsx +44 -0
- package/examples/autocomplete-groups.tsx +65 -0
- package/examples/autocomplete-no-clear.tsx +40 -0
- package/examples/avatar-demo.tsx +3 -3
- package/examples/input-group-with-button.tsx +1 -1
- package/examples/separator-demo.tsx +13 -0
- package/examples/separator-horizontal.tsx +18 -0
- package/package.json +19 -18
- package/public/base-ui.svg +1 -0
- package/src/assets/base-ui.svg +1 -0
- package/src/commands/add.ts +79 -38
- package/src/commands/cli.ts +50 -3
- package/src/commands/create.ts +262 -0
- package/src/commands/init.ts +45 -12
- package/src/commands/palette.ts +55 -0
- package/src/components/docs/layout/footer.tsx +2 -2
- package/src/components/docs/layout/header.tsx +7 -9
- package/src/components/docs/layout/mobile-menu.tsx +0 -1
- package/src/components/docs/layout/nav-list.tsx +2 -2
- package/src/components/docs/layout/page-header.tsx +52 -7
- package/src/components/docs/layout/split-layout.tsx +9 -10
- package/src/components/docs/layout/table-of-content.tsx +145 -0
- package/src/components/docs/markdown/components.tsx +142 -29
- package/src/components/docs/markdown/copy-button.tsx +41 -0
- package/src/components/docs/markdown/installation-block.tsx +5 -24
- package/src/components/docs/markdown/render-preview.tsx +1 -1
- package/src/components/ui/button-group.tsx +1 -1
- package/src/components/ui/scroll-area.tsx +11 -2
- package/src/lib/docs-headings.ts +72 -0
- package/src/routeTree.gen.ts +60 -3
- package/src/routes/__root.tsx +2 -2
- package/src/routes/components.$slug.tsx +20 -4
- package/src/routes/docs.$slug.tsx +78 -0
- package/src/routes/docs.tsx +15 -0
- package/src/styles/styles.css +1 -1
- package/src/utils/cli-utils.ts +8 -8
- package/src/utils/dependency-installer.ts +30 -0
- package/src/utils/package-manager.ts +4 -4
- package/src/utils/palette.ts +666 -0
- package/src/utils/schema.ts +6 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
|
|
6
|
+
const CACHE_VERSION = 1;
|
|
7
|
+
const TAILWIND_COLORS_URL = "https://tailwindcss.com/docs/colors";
|
|
8
|
+
const MARKER_START = "/* shapes-ui:tokens:start v1 */";
|
|
9
|
+
const MARKER_END = "/* shapes-ui:tokens:end */";
|
|
10
|
+
const SHADE_STEPS = [
|
|
11
|
+
"50",
|
|
12
|
+
"100",
|
|
13
|
+
"200",
|
|
14
|
+
"300",
|
|
15
|
+
"400",
|
|
16
|
+
"500",
|
|
17
|
+
"600",
|
|
18
|
+
"700",
|
|
19
|
+
"800",
|
|
20
|
+
"900",
|
|
21
|
+
"950",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
const DEFAULT_BRAND_PALETTES = [
|
|
25
|
+
"red",
|
|
26
|
+
"orange",
|
|
27
|
+
"amber",
|
|
28
|
+
"yellow",
|
|
29
|
+
"lime",
|
|
30
|
+
"green",
|
|
31
|
+
"emerald",
|
|
32
|
+
"teal",
|
|
33
|
+
"cyan",
|
|
34
|
+
"sky",
|
|
35
|
+
"blue",
|
|
36
|
+
"indigo",
|
|
37
|
+
"violet",
|
|
38
|
+
"purple",
|
|
39
|
+
"fuchsia",
|
|
40
|
+
"pink",
|
|
41
|
+
"rose",
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
const DEFAULT_NEUTRAL_PALETTES = ["slate", "gray", "zinc", "neutral", "stone"] as const;
|
|
45
|
+
|
|
46
|
+
type ShadeStep = (typeof SHADE_STEPS)[number];
|
|
47
|
+
type ColorScale = Record<ShadeStep, string>;
|
|
48
|
+
type PaletteMap = Record<string, ColorScale>;
|
|
49
|
+
type ThemeMode = "light" | "dark";
|
|
50
|
+
|
|
51
|
+
type TokenRole =
|
|
52
|
+
| "background"
|
|
53
|
+
| "foreground"
|
|
54
|
+
| "card"
|
|
55
|
+
| "card-foreground"
|
|
56
|
+
| "popup"
|
|
57
|
+
| "popup-foreground"
|
|
58
|
+
| "primary"
|
|
59
|
+
| "primary-foreground"
|
|
60
|
+
| "secondary"
|
|
61
|
+
| "secondary-foreground"
|
|
62
|
+
| "muted"
|
|
63
|
+
| "muted-foreground"
|
|
64
|
+
| "accent"
|
|
65
|
+
| "accent-foreground"
|
|
66
|
+
| "destructive"
|
|
67
|
+
| "destructive-foreground"
|
|
68
|
+
| "success"
|
|
69
|
+
| "success-foreground"
|
|
70
|
+
| "warning"
|
|
71
|
+
| "warning-foreground"
|
|
72
|
+
| "info"
|
|
73
|
+
| "info-foreground"
|
|
74
|
+
| "border"
|
|
75
|
+
| "input"
|
|
76
|
+
| "ring";
|
|
77
|
+
|
|
78
|
+
type TokenTheme = Record<TokenRole, string>;
|
|
79
|
+
|
|
80
|
+
type SemanticAssignments = {
|
|
81
|
+
light: TokenTheme;
|
|
82
|
+
dark: TokenTheme;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type TokenRef = {
|
|
86
|
+
family: string;
|
|
87
|
+
shade: ShadeStep;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type CachePayload = {
|
|
91
|
+
version: number;
|
|
92
|
+
fetchedAt: string;
|
|
93
|
+
palettes: PaletteMap;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type ContrastMode = "deterministic" | "dynamic";
|
|
97
|
+
|
|
98
|
+
export type PaletteWriteResult = {
|
|
99
|
+
source: "remote" | "cache" | "package";
|
|
100
|
+
paletteName: string;
|
|
101
|
+
neutralPalette: string;
|
|
102
|
+
contrastMode: ContrastMode;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const FOREGROUND_PAIRS: Array<{ background: TokenRole; foreground: TokenRole }> = [
|
|
106
|
+
{ background: "card", foreground: "card-foreground" },
|
|
107
|
+
{ background: "popup", foreground: "popup-foreground" },
|
|
108
|
+
{ background: "primary", foreground: "primary-foreground" },
|
|
109
|
+
{ background: "secondary", foreground: "secondary-foreground" },
|
|
110
|
+
{ background: "muted", foreground: "muted-foreground" },
|
|
111
|
+
{ background: "accent", foreground: "accent-foreground" },
|
|
112
|
+
{ background: "destructive", foreground: "destructive-foreground" },
|
|
113
|
+
{ background: "success", foreground: "success-foreground" },
|
|
114
|
+
{ background: "warning", foreground: "warning-foreground" },
|
|
115
|
+
{ background: "info", foreground: "info-foreground" },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const DETERMINISTIC_FOREGROUND_SHADE: Record<ThemeMode, Record<TokenRole, ShadeStep>> = {
|
|
119
|
+
light: {
|
|
120
|
+
background: "950",
|
|
121
|
+
foreground: "950",
|
|
122
|
+
card: "950",
|
|
123
|
+
"card-foreground": "950",
|
|
124
|
+
popup: "950",
|
|
125
|
+
"popup-foreground": "950",
|
|
126
|
+
primary: "50",
|
|
127
|
+
"primary-foreground": "50",
|
|
128
|
+
secondary: "900",
|
|
129
|
+
"secondary-foreground": "900",
|
|
130
|
+
muted: "500",
|
|
131
|
+
"muted-foreground": "500",
|
|
132
|
+
accent: "900",
|
|
133
|
+
"accent-foreground": "900",
|
|
134
|
+
destructive: "50",
|
|
135
|
+
"destructive-foreground": "50",
|
|
136
|
+
success: "50",
|
|
137
|
+
"success-foreground": "50",
|
|
138
|
+
warning: "950",
|
|
139
|
+
"warning-foreground": "950",
|
|
140
|
+
info: "50",
|
|
141
|
+
"info-foreground": "50",
|
|
142
|
+
border: "900",
|
|
143
|
+
input: "900",
|
|
144
|
+
ring: "50",
|
|
145
|
+
},
|
|
146
|
+
dark: {
|
|
147
|
+
background: "50",
|
|
148
|
+
foreground: "50",
|
|
149
|
+
card: "100",
|
|
150
|
+
"card-foreground": "100",
|
|
151
|
+
popup: "100",
|
|
152
|
+
"popup-foreground": "100",
|
|
153
|
+
primary: "950",
|
|
154
|
+
"primary-foreground": "950",
|
|
155
|
+
secondary: "100",
|
|
156
|
+
"secondary-foreground": "100",
|
|
157
|
+
muted: "400",
|
|
158
|
+
"muted-foreground": "400",
|
|
159
|
+
accent: "100",
|
|
160
|
+
"accent-foreground": "100",
|
|
161
|
+
destructive: "950",
|
|
162
|
+
"destructive-foreground": "950",
|
|
163
|
+
success: "950",
|
|
164
|
+
"success-foreground": "950",
|
|
165
|
+
warning: "950",
|
|
166
|
+
"warning-foreground": "950",
|
|
167
|
+
info: "950",
|
|
168
|
+
"info-foreground": "950",
|
|
169
|
+
border: "100",
|
|
170
|
+
input: "100",
|
|
171
|
+
ring: "950",
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
function normalizeName(value: string) {
|
|
176
|
+
return value.trim().toLowerCase();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeColorValue(value: string) {
|
|
180
|
+
const trimmed = value.trim();
|
|
181
|
+
if (trimmed.startsWith("oklch(")) {
|
|
182
|
+
return trimmed.slice(6, -1).trim();
|
|
183
|
+
}
|
|
184
|
+
return trimmed;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isValidScale(value: unknown): value is ColorScale {
|
|
188
|
+
if (!value || typeof value !== "object") return false;
|
|
189
|
+
return SHADE_STEPS.every((step) => typeof (value as Record<string, unknown>)[step] === "string");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getCacheFilePath() {
|
|
193
|
+
return path.join(os.homedir(), ".shapes-ui", "cache", "tailwind-colors-v4.json");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function readCache(): Promise<PaletteMap | null> {
|
|
197
|
+
const cachePath = getCacheFilePath();
|
|
198
|
+
if (!(await fs.pathExists(cachePath))) return null;
|
|
199
|
+
|
|
200
|
+
const payload = (await fs.readJSON(cachePath)) as CachePayload;
|
|
201
|
+
if (
|
|
202
|
+
payload.version !== CACHE_VERSION ||
|
|
203
|
+
!payload.palettes ||
|
|
204
|
+
typeof payload.palettes !== "object"
|
|
205
|
+
) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const validated: PaletteMap = {};
|
|
210
|
+
for (const [name, scale] of Object.entries(payload.palettes)) {
|
|
211
|
+
if (isValidScale(scale)) {
|
|
212
|
+
validated[normalizeName(name)] = scale;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return Object.keys(validated).length > 0 ? validated : null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function writeCache(palettes: PaletteMap) {
|
|
220
|
+
const cachePath = getCacheFilePath();
|
|
221
|
+
await fs.ensureDir(path.dirname(cachePath));
|
|
222
|
+
const payload: CachePayload = {
|
|
223
|
+
version: CACHE_VERSION,
|
|
224
|
+
fetchedAt: new Date().toISOString(),
|
|
225
|
+
palettes,
|
|
226
|
+
};
|
|
227
|
+
await fs.writeJSON(cachePath, payload, { spaces: 2 });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function loadFromTailwindPackage(): Promise<PaletteMap | null> {
|
|
231
|
+
try {
|
|
232
|
+
const colorsModule = await import("tailwindcss/colors");
|
|
233
|
+
const source = (colorsModule.default ?? colorsModule) as Record<string, unknown>;
|
|
234
|
+
const palettes: PaletteMap = {};
|
|
235
|
+
|
|
236
|
+
for (const [familyName, rawScale] of Object.entries(source)) {
|
|
237
|
+
if (!rawScale || typeof rawScale !== "object") continue;
|
|
238
|
+
const family = normalizeName(familyName);
|
|
239
|
+
|
|
240
|
+
const scale = rawScale as Record<string, unknown>;
|
|
241
|
+
const next: Partial<ColorScale> = {};
|
|
242
|
+
for (const step of SHADE_STEPS) {
|
|
243
|
+
const value = scale[step];
|
|
244
|
+
if (typeof value !== "string") {
|
|
245
|
+
next[step] = undefined;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
next[step] = normalizeColorValue(value);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (isValidScale(next)) {
|
|
252
|
+
palettes[family] = next;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return Object.keys(palettes).length > 0 ? palettes : null;
|
|
257
|
+
} catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function parseTailwindColorsFromDocs(markup: string): PaletteMap {
|
|
263
|
+
const regex =
|
|
264
|
+
/--color-([a-z]+)-(50|100|200|300|400|500|600|700|800|900|950)\s*:\s*(oklch\([^)]+\))/g;
|
|
265
|
+
const palettes: Record<string, Partial<ColorScale>> = {};
|
|
266
|
+
|
|
267
|
+
let match: RegExpExecArray | null = regex.exec(markup);
|
|
268
|
+
while (match) {
|
|
269
|
+
const family = normalizeName(match[1]);
|
|
270
|
+
const shade = match[2] as ShadeStep;
|
|
271
|
+
const value = normalizeColorValue(match[3]);
|
|
272
|
+
|
|
273
|
+
palettes[family] ??= {};
|
|
274
|
+
palettes[family][shade] = value;
|
|
275
|
+
match = regex.exec(markup);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const validated: PaletteMap = {};
|
|
279
|
+
for (const [family, scale] of Object.entries(palettes)) {
|
|
280
|
+
if (isValidScale(scale)) {
|
|
281
|
+
validated[family] = scale;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return validated;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function loadFromRemote(): Promise<PaletteMap | null> {
|
|
289
|
+
try {
|
|
290
|
+
const response = await fetch(TAILWIND_COLORS_URL);
|
|
291
|
+
if (!response.ok) return null;
|
|
292
|
+
const markup = await response.text();
|
|
293
|
+
const palettes = parseTailwindColorsFromDocs(markup);
|
|
294
|
+
if (Object.keys(palettes).length === 0) return null;
|
|
295
|
+
return palettes;
|
|
296
|
+
} catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function resolvePaletteSource(
|
|
302
|
+
refresh = false,
|
|
303
|
+
): Promise<{ source: "remote" | "cache" | "package"; palettes: PaletteMap }> {
|
|
304
|
+
if (!refresh) {
|
|
305
|
+
const fromCache = await readCache();
|
|
306
|
+
if (fromCache) {
|
|
307
|
+
return { source: "cache", palettes: fromCache };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const fromRemote = await loadFromRemote();
|
|
312
|
+
if (fromRemote) {
|
|
313
|
+
await writeCache(fromRemote);
|
|
314
|
+
return { source: "remote", palettes: fromRemote };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const fromCache = await readCache();
|
|
318
|
+
if (fromCache) {
|
|
319
|
+
return { source: "cache", palettes: fromCache };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const fromPackage = await loadFromTailwindPackage();
|
|
323
|
+
if (fromPackage) {
|
|
324
|
+
return { source: "package", palettes: fromPackage };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
throw new Error("Could not load Tailwind color palettes from docs, cache, or local package.");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function resolveFamily(palettes: PaletteMap, familyName: string) {
|
|
331
|
+
const family = normalizeName(familyName);
|
|
332
|
+
const scale = palettes[family];
|
|
333
|
+
if (!scale) {
|
|
334
|
+
const available = Object.keys(palettes).sort().join(", ");
|
|
335
|
+
throw new Error(`Palette "${familyName}" is not available. Available palettes: ${available}`);
|
|
336
|
+
}
|
|
337
|
+
return { family, scale };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function parseLightness(value: string) {
|
|
341
|
+
const first = value.split(/\s+/)[0]?.trim() ?? "";
|
|
342
|
+
if (!first) return Number.NaN;
|
|
343
|
+
if (first.endsWith("%")) {
|
|
344
|
+
return Number.parseFloat(first.slice(0, -1));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const numeric = Number.parseFloat(first);
|
|
348
|
+
if (Number.isNaN(numeric)) return Number.NaN;
|
|
349
|
+
return numeric <= 1 ? numeric * 100 : numeric;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function pickDynamicForegroundShade(scale: ColorScale, backgroundShade: ShadeStep) {
|
|
353
|
+
const backgroundValue = scale[backgroundShade];
|
|
354
|
+
const backgroundLightness = parseLightness(backgroundValue);
|
|
355
|
+
|
|
356
|
+
if (!Number.isFinite(backgroundLightness)) {
|
|
357
|
+
return backgroundShade === "950" ? "50" : "950";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let bestShade: ShadeStep = "950";
|
|
361
|
+
let bestDelta = -1;
|
|
362
|
+
|
|
363
|
+
for (const shade of SHADE_STEPS) {
|
|
364
|
+
const delta = Math.abs(parseLightness(scale[shade]) - backgroundLightness);
|
|
365
|
+
if (delta > bestDelta) {
|
|
366
|
+
bestDelta = delta;
|
|
367
|
+
bestShade = shade;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return bestShade;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function buildAssignments(
|
|
375
|
+
brandFamily: string,
|
|
376
|
+
neutralFamily: string,
|
|
377
|
+
): Record<ThemeMode, Record<TokenRole, TokenRef>> {
|
|
378
|
+
return {
|
|
379
|
+
light: {
|
|
380
|
+
background: { family: neutralFamily, shade: "50" },
|
|
381
|
+
foreground: { family: neutralFamily, shade: "950" },
|
|
382
|
+
card: { family: neutralFamily, shade: "50" },
|
|
383
|
+
"card-foreground": { family: neutralFamily, shade: "950" },
|
|
384
|
+
popup: { family: neutralFamily, shade: "50" },
|
|
385
|
+
"popup-foreground": { family: neutralFamily, shade: "950" },
|
|
386
|
+
primary: { family: brandFamily, shade: "600" },
|
|
387
|
+
"primary-foreground": { family: brandFamily, shade: "50" },
|
|
388
|
+
secondary: { family: neutralFamily, shade: "100" },
|
|
389
|
+
"secondary-foreground": { family: neutralFamily, shade: "900" },
|
|
390
|
+
muted: { family: neutralFamily, shade: "100" },
|
|
391
|
+
"muted-foreground": { family: neutralFamily, shade: "500" },
|
|
392
|
+
accent: { family: brandFamily, shade: "100" },
|
|
393
|
+
"accent-foreground": { family: brandFamily, shade: "900" },
|
|
394
|
+
destructive: { family: "red", shade: "600" },
|
|
395
|
+
"destructive-foreground": { family: "red", shade: "50" },
|
|
396
|
+
success: { family: "green", shade: "600" },
|
|
397
|
+
"success-foreground": { family: "green", shade: "50" },
|
|
398
|
+
warning: { family: "amber", shade: "500" },
|
|
399
|
+
"warning-foreground": { family: "amber", shade: "950" },
|
|
400
|
+
info: { family: brandFamily, shade: "600" },
|
|
401
|
+
"info-foreground": { family: brandFamily, shade: "50" },
|
|
402
|
+
border: { family: neutralFamily, shade: "200" },
|
|
403
|
+
input: { family: neutralFamily, shade: "300" },
|
|
404
|
+
ring: { family: brandFamily, shade: "500" },
|
|
405
|
+
},
|
|
406
|
+
dark: {
|
|
407
|
+
background: { family: neutralFamily, shade: "950" },
|
|
408
|
+
foreground: { family: neutralFamily, shade: "50" },
|
|
409
|
+
card: { family: neutralFamily, shade: "900" },
|
|
410
|
+
"card-foreground": { family: neutralFamily, shade: "100" },
|
|
411
|
+
popup: { family: neutralFamily, shade: "900" },
|
|
412
|
+
"popup-foreground": { family: neutralFamily, shade: "100" },
|
|
413
|
+
primary: { family: brandFamily, shade: "500" },
|
|
414
|
+
"primary-foreground": { family: brandFamily, shade: "950" },
|
|
415
|
+
secondary: { family: neutralFamily, shade: "800" },
|
|
416
|
+
"secondary-foreground": { family: neutralFamily, shade: "100" },
|
|
417
|
+
muted: { family: neutralFamily, shade: "800" },
|
|
418
|
+
"muted-foreground": { family: neutralFamily, shade: "400" },
|
|
419
|
+
accent: { family: brandFamily, shade: "800" },
|
|
420
|
+
"accent-foreground": { family: brandFamily, shade: "100" },
|
|
421
|
+
destructive: { family: "red", shade: "500" },
|
|
422
|
+
"destructive-foreground": { family: "red", shade: "950" },
|
|
423
|
+
success: { family: "green", shade: "500" },
|
|
424
|
+
"success-foreground": { family: "green", shade: "950" },
|
|
425
|
+
warning: { family: "amber", shade: "500" },
|
|
426
|
+
"warning-foreground": { family: "amber", shade: "950" },
|
|
427
|
+
info: { family: brandFamily, shade: "500" },
|
|
428
|
+
"info-foreground": { family: brandFamily, shade: "950" },
|
|
429
|
+
border: { family: neutralFamily, shade: "800" },
|
|
430
|
+
input: { family: neutralFamily, shade: "700" },
|
|
431
|
+
ring: { family: brandFamily, shade: "400" },
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function assignForegrounds(
|
|
437
|
+
palettes: PaletteMap,
|
|
438
|
+
assignments: Record<ThemeMode, Record<TokenRole, TokenRef>>,
|
|
439
|
+
contrastMode: ContrastMode,
|
|
440
|
+
) {
|
|
441
|
+
if (contrastMode === "deterministic") {
|
|
442
|
+
for (const mode of ["light", "dark"] as const) {
|
|
443
|
+
for (const pair of FOREGROUND_PAIRS) {
|
|
444
|
+
const bg = assignments[mode][pair.background];
|
|
445
|
+
assignments[mode][pair.foreground] = {
|
|
446
|
+
family: bg.family,
|
|
447
|
+
shade: DETERMINISTIC_FOREGROUND_SHADE[mode][pair.background],
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for (const mode of ["light", "dark"] as const) {
|
|
455
|
+
for (const pair of FOREGROUND_PAIRS) {
|
|
456
|
+
const background = assignments[mode][pair.background];
|
|
457
|
+
const scale = palettes[background.family];
|
|
458
|
+
if (!scale) continue;
|
|
459
|
+
assignments[mode][pair.foreground] = {
|
|
460
|
+
family: background.family,
|
|
461
|
+
shade: pickDynamicForegroundShade(scale, background.shade),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function resolveTokenThemes(
|
|
468
|
+
palettes: PaletteMap,
|
|
469
|
+
brandFamily: string,
|
|
470
|
+
neutralFamily: string,
|
|
471
|
+
contrastMode: ContrastMode,
|
|
472
|
+
): SemanticAssignments {
|
|
473
|
+
const assignments = buildAssignments(brandFamily, neutralFamily);
|
|
474
|
+
assignForegrounds(palettes, assignments, contrastMode);
|
|
475
|
+
|
|
476
|
+
const result: SemanticAssignments = {
|
|
477
|
+
light: {} as TokenTheme,
|
|
478
|
+
dark: {} as TokenTheme,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
for (const mode of ["light", "dark"] as const) {
|
|
482
|
+
for (const [role, ref] of Object.entries(assignments[mode]) as [TokenRole, TokenRef][]) {
|
|
483
|
+
const scale = palettes[ref.family];
|
|
484
|
+
if (!scale) {
|
|
485
|
+
throw new Error(`Palette "${ref.family}" is missing required shades.`);
|
|
486
|
+
}
|
|
487
|
+
result[mode][role] = scale[ref.shade];
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function formatThemeBlock(theme: TokenTheme) {
|
|
495
|
+
return [
|
|
496
|
+
` --background: oklch(${theme.background});`,
|
|
497
|
+
` --foreground: oklch(${theme.foreground});`,
|
|
498
|
+
` --card: oklch(${theme.card});`,
|
|
499
|
+
` --card-foreground: oklch(${theme["card-foreground"]});`,
|
|
500
|
+
` --popup: oklch(${theme.popup});`,
|
|
501
|
+
` --popup-foreground: oklch(${theme["popup-foreground"]});`,
|
|
502
|
+
` --primary: oklch(${theme.primary});`,
|
|
503
|
+
` --primary-foreground: oklch(${theme["primary-foreground"]});`,
|
|
504
|
+
` --secondary: oklch(${theme.secondary});`,
|
|
505
|
+
` --secondary-foreground: oklch(${theme["secondary-foreground"]});`,
|
|
506
|
+
` --muted: oklch(${theme.muted});`,
|
|
507
|
+
` --muted-foreground: oklch(${theme["muted-foreground"]});`,
|
|
508
|
+
` --accent: oklch(${theme.accent});`,
|
|
509
|
+
` --accent-foreground: oklch(${theme["accent-foreground"]});`,
|
|
510
|
+
` --destructive: oklch(${theme.destructive});`,
|
|
511
|
+
` --destructive-foreground: oklch(${theme["destructive-foreground"]});`,
|
|
512
|
+
` --success: oklch(${theme.success});`,
|
|
513
|
+
` --success-foreground: oklch(${theme["success-foreground"]});`,
|
|
514
|
+
` --warning: oklch(${theme.warning});`,
|
|
515
|
+
` --warning-foreground: oklch(${theme["warning-foreground"]});`,
|
|
516
|
+
` --info: oklch(${theme.info});`,
|
|
517
|
+
` --info-foreground: oklch(${theme["info-foreground"]});`,
|
|
518
|
+
` --border: oklch(${theme.border});`,
|
|
519
|
+
` --input: oklch(${theme.input});`,
|
|
520
|
+
` --ring: oklch(${theme.ring});`,
|
|
521
|
+
].join("\n");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function buildTokenBlock(assignments: SemanticAssignments, result: PaletteWriteResult) {
|
|
525
|
+
return [
|
|
526
|
+
MARKER_START,
|
|
527
|
+
`/* source=${result.source} palette=${result.paletteName} neutral=${result.neutralPalette} contrast=${result.contrastMode} */`,
|
|
528
|
+
":root {",
|
|
529
|
+
formatThemeBlock(assignments.light),
|
|
530
|
+
"}",
|
|
531
|
+
"",
|
|
532
|
+
".dark {",
|
|
533
|
+
formatThemeBlock(assignments.dark),
|
|
534
|
+
"}",
|
|
535
|
+
"",
|
|
536
|
+
"@theme inline {",
|
|
537
|
+
" --color-background: var(--background);",
|
|
538
|
+
" --color-foreground: var(--foreground);",
|
|
539
|
+
" --color-card: var(--card);",
|
|
540
|
+
" --color-card-foreground: var(--card-foreground);",
|
|
541
|
+
" --color-popup: var(--popup);",
|
|
542
|
+
" --color-popup-foreground: var(--popup-foreground);",
|
|
543
|
+
" --color-primary: var(--primary);",
|
|
544
|
+
" --color-primary-foreground: var(--primary-foreground);",
|
|
545
|
+
" --color-secondary: var(--secondary);",
|
|
546
|
+
" --color-secondary-foreground: var(--secondary-foreground);",
|
|
547
|
+
" --color-muted: var(--muted);",
|
|
548
|
+
" --color-muted-foreground: var(--muted-foreground);",
|
|
549
|
+
" --color-accent: var(--accent);",
|
|
550
|
+
" --color-accent-foreground: var(--accent-foreground);",
|
|
551
|
+
" --color-destructive: var(--destructive);",
|
|
552
|
+
" --color-destructive-foreground: var(--destructive-foreground);",
|
|
553
|
+
" --color-success: var(--success);",
|
|
554
|
+
" --color-success-foreground: var(--success-foreground);",
|
|
555
|
+
" --color-warning: var(--warning);",
|
|
556
|
+
" --color-warning-foreground: var(--warning-foreground);",
|
|
557
|
+
" --color-info: var(--info);",
|
|
558
|
+
" --color-info-foreground: var(--info-foreground);",
|
|
559
|
+
" --color-border: var(--border);",
|
|
560
|
+
" --color-input: var(--input);",
|
|
561
|
+
" --color-ring: var(--ring);",
|
|
562
|
+
"}",
|
|
563
|
+
MARKER_END,
|
|
564
|
+
].join("\n");
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function upsertGeneratedBlock(current: string, generatedBlock: string) {
|
|
568
|
+
const hasStart = current.includes(MARKER_START);
|
|
569
|
+
const hasEnd = current.includes(MARKER_END);
|
|
570
|
+
|
|
571
|
+
if (hasStart !== hasEnd) {
|
|
572
|
+
throw new Error("Found an incomplete generated token block. Please fix it manually and retry.");
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (!hasStart) {
|
|
576
|
+
const trimmed = current.trimEnd();
|
|
577
|
+
return `${trimmed}\n\n${generatedBlock}\n`;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const startIndex = current.indexOf(MARKER_START);
|
|
581
|
+
const endIndex = current.indexOf(MARKER_END);
|
|
582
|
+
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
|
583
|
+
throw new Error("Could not safely update generated token block.");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const endMarkerIndex = endIndex + MARKER_END.length;
|
|
587
|
+
const before = current.slice(0, startIndex).trimEnd();
|
|
588
|
+
const after = current.slice(endMarkerIndex).trimStart();
|
|
589
|
+
|
|
590
|
+
if (!before && !after) {
|
|
591
|
+
return `${generatedBlock}\n`;
|
|
592
|
+
}
|
|
593
|
+
if (!after) {
|
|
594
|
+
return `${before}\n\n${generatedBlock}\n`;
|
|
595
|
+
}
|
|
596
|
+
if (!before) {
|
|
597
|
+
return `${generatedBlock}\n\n${after}`;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return `${before}\n\n${generatedBlock}\n\n${after}`;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export function getDefaultBrandPaletteOptions() {
|
|
604
|
+
return [...DEFAULT_BRAND_PALETTES];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function getDefaultNeutralPaletteOptions() {
|
|
608
|
+
return [...DEFAULT_NEUTRAL_PALETTES];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export function normalizeContrastMode(value: string | null | undefined): ContrastMode {
|
|
612
|
+
const normalized = value ? value.trim().toLowerCase() : "deterministic";
|
|
613
|
+
if (normalized === "deterministic" || normalized === "dynamic") {
|
|
614
|
+
return normalized;
|
|
615
|
+
}
|
|
616
|
+
throw new Error(`Invalid contrast mode "${value}". Use deterministic or dynamic.`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export async function getAvailablePalettes(refresh = false) {
|
|
620
|
+
const resolved = await resolvePaletteSource(refresh);
|
|
621
|
+
return {
|
|
622
|
+
source: resolved.source,
|
|
623
|
+
names: Object.keys(resolved.palettes).sort(),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export async function writePaletteTokens(options: {
|
|
628
|
+
cwd?: string;
|
|
629
|
+
cssPath: string;
|
|
630
|
+
paletteName: string;
|
|
631
|
+
neutralPalette: string;
|
|
632
|
+
contrastMode: ContrastMode;
|
|
633
|
+
refresh?: boolean;
|
|
634
|
+
}) {
|
|
635
|
+
const cwd = options.cwd ?? process.cwd();
|
|
636
|
+
const cssFilePath = path.join(cwd, options.cssPath);
|
|
637
|
+
|
|
638
|
+
const resolved = await resolvePaletteSource(options.refresh);
|
|
639
|
+
const brand = resolveFamily(resolved.palettes, options.paletteName);
|
|
640
|
+
const neutral = resolveFamily(resolved.palettes, options.neutralPalette);
|
|
641
|
+
resolveFamily(resolved.palettes, "red");
|
|
642
|
+
resolveFamily(resolved.palettes, "green");
|
|
643
|
+
resolveFamily(resolved.palettes, "amber");
|
|
644
|
+
|
|
645
|
+
const result: PaletteWriteResult = {
|
|
646
|
+
source: resolved.source,
|
|
647
|
+
paletteName: brand.family,
|
|
648
|
+
neutralPalette: neutral.family,
|
|
649
|
+
contrastMode: options.contrastMode,
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const assignments = resolveTokenThemes(
|
|
653
|
+
resolved.palettes,
|
|
654
|
+
brand.family,
|
|
655
|
+
neutral.family,
|
|
656
|
+
options.contrastMode,
|
|
657
|
+
);
|
|
658
|
+
const generatedBlock = buildTokenBlock(assignments, result);
|
|
659
|
+
|
|
660
|
+
const current = (await fs.pathExists(cssFilePath)) ? await fs.readFile(cssFilePath, "utf-8") : "";
|
|
661
|
+
const updated = upsertGeneratedBlock(current, generatedBlock);
|
|
662
|
+
await fs.ensureDir(path.dirname(cssFilePath));
|
|
663
|
+
await fs.writeFile(cssFilePath, updated);
|
|
664
|
+
|
|
665
|
+
return result;
|
|
666
|
+
}
|
package/src/utils/schema.ts
CHANGED
|
@@ -5,6 +5,12 @@ import { zodToJsonSchema } from "zod-to-json-schema";
|
|
|
5
5
|
export const configSchema = z.object({
|
|
6
6
|
$schema: z.string().optional(),
|
|
7
7
|
style: z.enum(["default", "brutalist", "minimal"]).default("default"),
|
|
8
|
+
palette: z
|
|
9
|
+
.object({
|
|
10
|
+
name: z.string().default("blue"),
|
|
11
|
+
contrastMode: z.enum(["deterministic", "dynamic"]).default("deterministic"),
|
|
12
|
+
})
|
|
13
|
+
.default({ name: "blue", contrastMode: "deterministic" }),
|
|
8
14
|
tailwind: z.object({
|
|
9
15
|
config: z.string().optional(),
|
|
10
16
|
css: z.string().default("src/styles/globals.css"),
|