@mr.dj2u/cli 0.1.7 → 0.1.8

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.
@@ -0,0 +1,1374 @@
1
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const STYLE_THEME_BLOCK_START = '<!-- MDS_STYLIST_THEME_START -->';
4
+ const STYLE_THEME_BLOCK_END = '<!-- MDS_STYLIST_THEME_END -->';
5
+ const GLOBAL_CSS_THEME_BLOCK_START = '/* MDS_STYLIST_THEME_START */';
6
+ const GLOBAL_CSS_THEME_BLOCK_END = '/* MDS_STYLIST_THEME_END */';
7
+ const NATIVEWIND_UI_THEME_BLOCK_START = '/* MDS_STYLIST_NATIVEWINDUI_THEME_START */';
8
+ const NATIVEWIND_UI_THEME_BLOCK_END = '/* MDS_STYLIST_NATIVEWINDUI_THEME_END */';
9
+ const UNISTYLES_THEME_BLOCK_START = '// MDS_STYLIST_UNISTYLES_THEME_START';
10
+ const UNISTYLES_THEME_BLOCK_END = '// MDS_STYLIST_UNISTYLES_THEME_END';
11
+ const RESTYLE_THEME_BLOCK_START = '// MDS_STYLIST_RESTYLE_THEME_START';
12
+ const RESTYLE_THEME_BLOCK_END = '// MDS_STYLIST_RESTYLE_THEME_END';
13
+ const TAMAGUI_THEME_BLOCK_START = '// MDS_STYLIST_TAMAGUI_THEME_START';
14
+ const TAMAGUI_THEME_BLOCK_END = '// MDS_STYLIST_TAMAGUI_THEME_END';
15
+ const TODO_THEME_TASK = '- [ ] Apply Stylist synced theme tokens to production UI components and screens.';
16
+ export const DEFAULT_STYLIST_THEME = {
17
+ version: 1,
18
+ colorSystem: {
19
+ mode: 'bg',
20
+ previewScheme: 'light',
21
+ familyMode: 'one',
22
+ },
23
+ families: {
24
+ light: {
25
+ primary: 'blue',
26
+ secondary: 'violet',
27
+ success: 'emerald',
28
+ warning: 'amber',
29
+ },
30
+ dark: {
31
+ primary: 'blue',
32
+ secondary: 'violet',
33
+ success: 'emerald',
34
+ warning: 'amber',
35
+ },
36
+ },
37
+ palettes: {
38
+ bg: {
39
+ light: {
40
+ background: '#f8fafc',
41
+ surface: '#e2e8f0',
42
+ text: '#111827',
43
+ primary: '#2563eb',
44
+ secondary: '#7c3aed',
45
+ success: '#16a34a',
46
+ warning: '#f97316',
47
+ },
48
+ dark: {
49
+ background: '#09090b',
50
+ surface: '#18181b',
51
+ text: '#f8fafc',
52
+ primary: '#60a5fa',
53
+ secondary: '#a78bfa',
54
+ success: '#4ade80',
55
+ warning: '#fb923c',
56
+ },
57
+ },
58
+ automatic: {
59
+ light: {
60
+ background: '#eff6ff',
61
+ surface: '#dbeafe',
62
+ text: '#1e3a8a',
63
+ primary: '#3b82f6',
64
+ secondary: '#8b5cf6',
65
+ success: '#10b981',
66
+ warning: '#f59e0b',
67
+ },
68
+ dark: {
69
+ background: '#172554',
70
+ surface: '#1e3a8a',
71
+ text: '#eff6ff',
72
+ primary: '#60a5fa',
73
+ secondary: '#a78bfa',
74
+ success: '#34d399',
75
+ warning: '#fbbf24',
76
+ },
77
+ },
78
+ },
79
+ colors: {
80
+ light: {
81
+ background: '#f8fafc',
82
+ surface: '#e2e8f0',
83
+ text: '#111827',
84
+ primary: '#2563eb',
85
+ secondary: '#7c3aed',
86
+ success: '#16a34a',
87
+ warning: '#f97316',
88
+ },
89
+ dark: {
90
+ background: '#09090b',
91
+ surface: '#18181b',
92
+ text: '#f8fafc',
93
+ primary: '#60a5fa',
94
+ secondary: '#a78bfa',
95
+ success: '#4ade80',
96
+ warning: '#fb923c',
97
+ },
98
+ },
99
+ typography: {
100
+ fontFamily: 'System',
101
+ fontDisplay: 'System',
102
+ fontTitle: 'System',
103
+ fontSubtitle: 'System',
104
+ fontBody: 'System',
105
+ fontCaption: 'System',
106
+ fontMono: 'monospace',
107
+ displaySize: 32,
108
+ headingSize: 20,
109
+ bodySize: 15,
110
+ captionSize: 12,
111
+ },
112
+ layout: {
113
+ radius: 12,
114
+ spacing: {
115
+ xs: 4,
116
+ sm: 8,
117
+ md: 16,
118
+ lg: 24,
119
+ xl: 32,
120
+ },
121
+ },
122
+ };
123
+ export async function syncStylistTheme(projectPathInput, payload, options = {}) {
124
+ const projectPath = path.resolve(projectPathInput);
125
+ const projectDir = path.join(projectPath, 'project');
126
+ const updatedFiles = [];
127
+ await mkdir(projectDir, { recursive: true });
128
+ const theme = normalizeStylistTheme(payload);
129
+ const resolved = await resolveStylistContext(projectPath, options);
130
+ const styleLibrary = resolved.styleLibrary;
131
+ const writePolicy = resolved.writePolicy;
132
+ const themePath = path.join(projectDir, 'theme.json');
133
+ const wroteTheme = await writeTextFileIfChanged(themePath, `${JSON.stringify(theme, null, 2)}\n`);
134
+ if (wroteTheme) {
135
+ updatedFiles.push(themePath);
136
+ }
137
+ const stylePath = path.join(projectDir, 'style.md');
138
+ const styleExisting = await readOptionalText(stylePath);
139
+ const styleNext = upsertManagedBlock(styleExisting ?? renderDefaultStyleMarkdown(), STYLE_THEME_BLOCK_START, STYLE_THEME_BLOCK_END, renderStyleThemeBlock(theme));
140
+ const wroteStyle = await writeTextFileIfChanged(stylePath, styleNext);
141
+ if (wroteStyle) {
142
+ updatedFiles.push(stylePath);
143
+ }
144
+ const tokensPath = path.join(projectPath, 'src', 'theme', 'tokens.ts');
145
+ const wroteTokens = await writeTextFileIfChanged(tokensPath, renderThemeTokensFile(theme));
146
+ if (wroteTokens) {
147
+ updatedFiles.push(tokensPath);
148
+ }
149
+ const fontAssetUpdates = await syncThemeFontAssets(projectPath, theme);
150
+ updatedFiles.push(...fontAssetUpdates);
151
+ const adapterUpdates = await syncStyleLibraryOutputs(projectPath, theme, styleLibrary, writePolicy);
152
+ updatedFiles.push(...adapterUpdates);
153
+ const todoPath = path.join(projectDir, 'todo.md');
154
+ const todoExisting = await readOptionalText(todoPath);
155
+ if (todoExisting) {
156
+ const todoNext = ensureThemeTodoTask(todoExisting);
157
+ if (todoNext !== todoExisting) {
158
+ await writeFile(todoPath, todoNext, 'utf8');
159
+ updatedFiles.push(todoPath);
160
+ }
161
+ }
162
+ const stylistConfigPath = path.join(projectDir, 'stylist.config.json');
163
+ const wroteConfig = await writeTextFileIfChanged(stylistConfigPath, `${JSON.stringify({ styleLibrary, writePolicy }, null, 2)}\n`);
164
+ if (wroteConfig) {
165
+ updatedFiles.push(stylistConfigPath);
166
+ }
167
+ return {
168
+ projectPath,
169
+ theme,
170
+ updatedFiles,
171
+ styleLibrary,
172
+ writePolicy,
173
+ };
174
+ }
175
+ const SYSTEM_FONT_FAMILIES = new Set([
176
+ 'system',
177
+ 'monospace',
178
+ 'sans-serif',
179
+ 'serif',
180
+ 'ui-sans-serif',
181
+ 'ui-serif',
182
+ 'ui-monospace',
183
+ 'arial',
184
+ 'helvetica',
185
+ 'times new roman',
186
+ 'georgia',
187
+ 'courier new',
188
+ ]);
189
+ function normalizeFontFamilyName(value) {
190
+ return value.trim().replace(/\s+/g, ' ');
191
+ }
192
+ function isSystemFontFamily(fontFamily) {
193
+ const normalized = normalizeFontFamilyName(fontFamily).toLowerCase();
194
+ if (!normalized) {
195
+ return true;
196
+ }
197
+ return SYSTEM_FONT_FAMILIES.has(normalized);
198
+ }
199
+ function toFontAssetBaseKey(fontFamily) {
200
+ const normalized = normalizeFontFamilyName(fontFamily);
201
+ if (!normalized) {
202
+ return 'System';
203
+ }
204
+ return normalized.replace(/\s+/g, '_').replace(/[^\w\-]/g, '');
205
+ }
206
+ function toFontAssetFileName(fontFamily, weight) {
207
+ const base = toFontAssetBaseKey(fontFamily);
208
+ return `${base}-${weight}.ttf`;
209
+ }
210
+ async function syncThemeFontAssets(projectPath, theme) {
211
+ const fontAssetsPath = path.join(projectPath, 'src', 'theme', 'font-assets.ts');
212
+ const families = new Set();
213
+ for (const family of [
214
+ theme.typography.fontDisplay,
215
+ theme.typography.fontTitle,
216
+ theme.typography.fontSubtitle,
217
+ theme.typography.fontBody,
218
+ theme.typography.fontCaption,
219
+ theme.typography.fontMono,
220
+ ]) {
221
+ const normalized = normalizeFontFamilyName(family);
222
+ if (normalized && !isSystemFontFamily(normalized)) {
223
+ families.add(normalized);
224
+ }
225
+ }
226
+ if (families.size === 0) {
227
+ const wrote = await writeTextFileIfChanged(fontAssetsPath, renderFontAssetsFile(new Map()));
228
+ return wrote ? [fontAssetsPath] : [];
229
+ }
230
+ const assetsDir = path.join(projectPath, 'assets', 'fonts');
231
+ await mkdir(assetsDir, { recursive: true });
232
+ const fontAssetEntries = new Map();
233
+ const updated = [];
234
+ for (const family of families) {
235
+ const css = await fetchGoogleFontsCss(family);
236
+ const urls = parseTtfUrlsByWeight(css);
237
+ const preferredWeights = [400, 700];
238
+ const availableWeight = preferredWeights.find((weight) => urls.has(weight));
239
+ if (!availableWeight) {
240
+ continue;
241
+ }
242
+ const url = urls.get(availableWeight);
243
+ if (!url) {
244
+ continue;
245
+ }
246
+ const fileName = toFontAssetFileName(family, availableWeight);
247
+ const destPath = path.join(assetsDir, fileName);
248
+ if (!(await pathExists(destPath))) {
249
+ const buffer = await downloadFontFile(url);
250
+ await writeFile(destPath, buffer);
251
+ updated.push(destPath);
252
+ }
253
+ const key = normalizeFontFamilyName(family);
254
+ if (key) {
255
+ fontAssetEntries.set(key, `../../assets/fonts/${fileName}`);
256
+ }
257
+ }
258
+ const wroteAssetsFile = await writeTextFileIfChanged(fontAssetsPath, renderFontAssetsFile(fontAssetEntries));
259
+ if (wroteAssetsFile) {
260
+ updated.push(fontAssetsPath);
261
+ }
262
+ return updated;
263
+ }
264
+ async function fetchGoogleFontsCss(fontFamily) {
265
+ const familyParam = normalizeFontFamilyName(fontFamily).replace(/\s+/g, '+');
266
+ const url = `https://fonts.googleapis.com/css2?family=${familyParam}:wght@400;700&display=swap`;
267
+ const fetchFn = globalThis.fetch;
268
+ if (typeof fetchFn !== 'function') {
269
+ throw new Error('Global fetch is unavailable. Update Node to 18+ and retry.');
270
+ }
271
+ // Google Fonts varies the returned formats (woff2 vs ttf) based on user agent.
272
+ // For native apps we need truetype/otf sources, so we try a couple of UAs until we see them.
273
+ const userAgents = [null, 'curl/8.0.1', 'Mozilla/5.0'];
274
+ let lastError = null;
275
+ for (const userAgent of userAgents) {
276
+ try {
277
+ const response = await fetchFn(url, {
278
+ headers: userAgent ? { 'user-agent': userAgent } : undefined,
279
+ });
280
+ if (!response.ok) {
281
+ lastError = new Error(`Failed to download Google Fonts stylesheet for ${fontFamily}.`);
282
+ continue;
283
+ }
284
+ const css = await response.text();
285
+ if (css.includes('.ttf') || css.includes('.otf')) {
286
+ return css;
287
+ }
288
+ lastError = new Error(`Google Fonts stylesheet for ${fontFamily} did not include truetype/otf sources.`);
289
+ }
290
+ catch (error) {
291
+ lastError = error;
292
+ }
293
+ }
294
+ throw lastError instanceof Error ? lastError : new Error('Failed to load Google Fonts CSS.');
295
+ }
296
+ function parseTtfUrlsByWeight(css) {
297
+ const result = new Map();
298
+ const faceBlocks = css.match(/@font-face\s*\{[^}]*\}/g) ?? [];
299
+ for (const block of faceBlocks) {
300
+ const weightMatch = block.match(/font-weight:\s*(\d+)/);
301
+ const weightValue = weightMatch ? Number.parseInt(weightMatch[1] ?? '', 10) : NaN;
302
+ if (weightValue !== 400 && weightValue !== 700) {
303
+ continue;
304
+ }
305
+ if (result.has(weightValue)) {
306
+ continue;
307
+ }
308
+ const urlMatches = [...block.matchAll(/url\(([^)]+)\)/g)];
309
+ const urls = urlMatches
310
+ .map((match) => (match[1] ?? '').replace(/^['\"]|['\"]$/g, '').trim())
311
+ .filter(Boolean);
312
+ const asset = urls.find((value) => {
313
+ const lowered = value.toLowerCase();
314
+ return lowered.includes('.ttf') || lowered.includes('.otf');
315
+ });
316
+ if (asset) {
317
+ result.set(weightValue, asset);
318
+ }
319
+ }
320
+ return result;
321
+ }
322
+ async function downloadFontFile(url) {
323
+ const fetchFn = globalThis.fetch;
324
+ if (typeof fetchFn !== 'function') {
325
+ throw new Error('Global fetch is unavailable. Update Node to 18+ and retry.');
326
+ }
327
+ const response = await fetchFn(url);
328
+ if (!response.ok) {
329
+ throw new Error(`Failed to download font file: ${url}`);
330
+ }
331
+ const arrayBuffer = await response.arrayBuffer();
332
+ return Buffer.from(arrayBuffer);
333
+ }
334
+ function renderFontAssetsFile(fontAssets) {
335
+ const entries = [...fontAssets.entries()].sort(([a], [b]) => a.localeCompare(b));
336
+ const lines = entries.map(([key, relativePath]) => {
337
+ const quotedKey = JSON.stringify(key);
338
+ const quotedPath = JSON.stringify(relativePath);
339
+ return ` ${quotedKey}: require(${quotedPath}),`;
340
+ });
341
+ return [
342
+ 'export const THEME_FONT_ASSETS: Record<string, number> = {',
343
+ ...(lines.length ? lines : []),
344
+ '};',
345
+ '',
346
+ 'export default THEME_FONT_ASSETS;',
347
+ '',
348
+ ].join('\n');
349
+ }
350
+ export async function resolveStylistContext(projectPathInput, options = {}) {
351
+ const projectPath = path.resolve(projectPathInput);
352
+ const stylistConfig = await loadStylistConfig(projectPath);
353
+ const writePolicy = options.writePolicy ?? stylistConfig?.writePolicy ?? 'managed';
354
+ let styleLibrary;
355
+ if (options.styleLibrary && options.styleLibrary !== 'auto') {
356
+ styleLibrary = options.styleLibrary;
357
+ }
358
+ else if (stylistConfig?.styleLibrary) {
359
+ styleLibrary = stylistConfig.styleLibrary;
360
+ }
361
+ else {
362
+ styleLibrary = await detectStyleLibrary(projectPath);
363
+ }
364
+ return { styleLibrary, writePolicy };
365
+ }
366
+ export async function loadStylistConfig(projectPathInput) {
367
+ const projectPath = path.resolve(projectPathInput);
368
+ const configPath = path.join(projectPath, 'project', 'stylist.config.json');
369
+ const raw = await readOptionalText(configPath);
370
+ if (!raw) {
371
+ return null;
372
+ }
373
+ try {
374
+ const parsed = JSON.parse(raw);
375
+ if (!isRecord(parsed)) {
376
+ return null;
377
+ }
378
+ const styleLibrary = parseStyleLibrary(parsed.styleLibrary);
379
+ const writePolicy = parseWritePolicy(parsed.writePolicy);
380
+ if (!styleLibrary || !writePolicy) {
381
+ return null;
382
+ }
383
+ return { styleLibrary, writePolicy };
384
+ }
385
+ catch {
386
+ return null;
387
+ }
388
+ }
389
+ export async function detectStyleLibrary(projectPathInput) {
390
+ const projectPath = path.resolve(projectPathInput);
391
+ const fromCesConfig = await detectStyleLibraryFromCesConfig(projectPath);
392
+ if (fromCesConfig) {
393
+ return fromCesConfig;
394
+ }
395
+ const fromDeps = await detectStyleLibraryFromDependencies(projectPath);
396
+ if (fromDeps) {
397
+ return fromDeps;
398
+ }
399
+ const fromFiles = await detectStyleLibraryFromFiles(projectPath);
400
+ if (fromFiles) {
401
+ return fromFiles;
402
+ }
403
+ return 'stylesheet';
404
+ }
405
+ export async function loadStylistTheme(projectPathInput) {
406
+ const result = await loadStylistThemeWithDiagnostics(projectPathInput);
407
+ return result.theme;
408
+ }
409
+ export async function loadStylistThemeWithDiagnostics(projectPathInput) {
410
+ const projectPath = path.resolve(projectPathInput);
411
+ const themePath = path.join(projectPath, 'project', 'theme.json');
412
+ const stylePath = path.join(projectPath, 'project', 'style.md');
413
+ const themeRaw = await readOptionalText(themePath);
414
+ const styleRaw = await readOptionalText(stylePath);
415
+ const fromThemeJson = parseThemeJson(themeRaw);
416
+ const fromStyleManaged = parseThemeFromStyleMarkdown(styleRaw);
417
+ const mismatchDetected = fromThemeJson !== null &&
418
+ fromStyleManaged !== null &&
419
+ JSON.stringify(fromThemeJson) !== JSON.stringify(fromStyleManaged);
420
+ if (fromThemeJson) {
421
+ return {
422
+ theme: fromThemeJson,
423
+ diagnostics: { source: 'theme.json', mismatchDetected },
424
+ };
425
+ }
426
+ if (fromStyleManaged) {
427
+ return {
428
+ theme: fromStyleManaged,
429
+ diagnostics: { source: 'style.md', mismatchDetected: false },
430
+ };
431
+ }
432
+ return {
433
+ theme: DEFAULT_STYLIST_THEME,
434
+ diagnostics: { source: 'default', mismatchDetected: false },
435
+ };
436
+ }
437
+ export function normalizeStylistTheme(value) {
438
+ if (!isRecord(value)) {
439
+ throw new Error('Theme payload must be an object.');
440
+ }
441
+ if (value.version !== 1) {
442
+ throw new Error('version must be 1.');
443
+ }
444
+ const colorSystem = ensureRecord(value.colorSystem, 'colorSystem');
445
+ const families = ensureRecord(value.families, 'families');
446
+ const familiesLight = ensureRecord(families.light, 'families.light');
447
+ const familiesDark = ensureRecord(families.dark, 'families.dark');
448
+ const palettes = ensureRecord(value.palettes, 'palettes');
449
+ const paletteBg = ensureRecord(palettes.bg, 'palettes.bg');
450
+ const paletteAutomatic = ensureRecord(palettes.automatic, 'palettes.automatic');
451
+ const paletteBgLight = ensureRecord(paletteBg.light, 'palettes.bg.light');
452
+ const paletteBgDark = ensureRecord(paletteBg.dark, 'palettes.bg.dark');
453
+ const paletteAutomaticLight = ensureRecord(paletteAutomatic.light, 'palettes.automatic.light');
454
+ const paletteAutomaticDark = ensureRecord(paletteAutomatic.dark, 'palettes.automatic.dark');
455
+ const colors = ensureRecord(value.colors, 'colors');
456
+ const colorsLight = ensureRecord(colors.light, 'colors.light');
457
+ const colorsDark = ensureRecord(colors.dark, 'colors.dark');
458
+ const typography = ensureRecord(value.typography, 'typography');
459
+ const layout = ensureRecord(value.layout, 'layout');
460
+ const spacing = ensureRecord(layout.spacing, 'layout.spacing');
461
+ const theme = {
462
+ version: 1,
463
+ colorSystem: {
464
+ mode: ensureEnumValue(colorSystem.mode, 'colorSystem.mode', ['bg', 'automatic']),
465
+ previewScheme: ensureEnumValue(colorSystem.previewScheme, 'colorSystem.previewScheme', [
466
+ 'light',
467
+ 'dark',
468
+ ]),
469
+ familyMode: ensureEnumValue(colorSystem.familyMode, 'colorSystem.familyMode', ['one', 'two']),
470
+ },
471
+ families: {
472
+ light: ensureSemanticFamilies(familiesLight, 'families.light'),
473
+ dark: ensureSemanticFamilies(familiesDark, 'families.dark'),
474
+ },
475
+ palettes: {
476
+ bg: {
477
+ light: ensureColorPalette(paletteBgLight, 'palettes.bg.light'),
478
+ dark: ensureColorPalette(paletteBgDark, 'palettes.bg.dark'),
479
+ },
480
+ automatic: {
481
+ light: ensureColorPalette(paletteAutomaticLight, 'palettes.automatic.light'),
482
+ dark: ensureColorPalette(paletteAutomaticDark, 'palettes.automatic.dark'),
483
+ },
484
+ },
485
+ colors: {
486
+ light: ensureColorPalette(colorsLight, 'colors.light'),
487
+ dark: ensureColorPalette(colorsDark, 'colors.dark'),
488
+ },
489
+ typography: {
490
+ fontFamily: ensureNonEmptyString(typography.fontFamily, 'typography.fontFamily'),
491
+ fontDisplay: ensureOptionalNonEmptyString(typography.fontDisplay) ??
492
+ ensureNonEmptyString(typography.fontFamily, 'typography.fontFamily'),
493
+ fontTitle: ensureOptionalNonEmptyString(typography.fontTitle) ??
494
+ ensureNonEmptyString(typography.fontFamily, 'typography.fontFamily'),
495
+ fontSubtitle: ensureOptionalNonEmptyString(typography.fontSubtitle) ??
496
+ ensureNonEmptyString(typography.fontFamily, 'typography.fontFamily'),
497
+ fontBody: ensureOptionalNonEmptyString(typography.fontBody) ??
498
+ ensureNonEmptyString(typography.fontFamily, 'typography.fontFamily'),
499
+ fontCaption: ensureOptionalNonEmptyString(typography.fontCaption) ??
500
+ ensureNonEmptyString(typography.fontFamily, 'typography.fontFamily'),
501
+ fontMono: ensureOptionalNonEmptyString(typography.fontMono) ?? 'monospace',
502
+ displaySize: ensureNumberInRange(typography.displaySize, 'typography.displaySize', 18, 72),
503
+ headingSize: ensureNumberInRange(typography.headingSize, 'typography.headingSize', 14, 48),
504
+ bodySize: ensureNumberInRange(typography.bodySize, 'typography.bodySize', 10, 24),
505
+ captionSize: ensureNumberInRange(typography.captionSize, 'typography.captionSize', 10, 20),
506
+ },
507
+ layout: {
508
+ radius: ensureNumberInRange(layout.radius, 'layout.radius', 0, 48),
509
+ spacing: {
510
+ xs: ensureNumberInRange(spacing.xs, 'layout.spacing.xs', 0, 64),
511
+ sm: ensureNumberInRange(spacing.sm, 'layout.spacing.sm', 0, 96),
512
+ md: ensureNumberInRange(spacing.md, 'layout.spacing.md', 0, 128),
513
+ lg: ensureNumberInRange(spacing.lg, 'layout.spacing.lg', 0, 160),
514
+ xl: ensureNumberInRange(spacing.xl, 'layout.spacing.xl', 0, 192),
515
+ },
516
+ },
517
+ };
518
+ ensureDistinctPalette(theme.palettes.bg.light, 'palettes.bg.light');
519
+ ensureDistinctPalette(theme.palettes.bg.dark, 'palettes.bg.dark');
520
+ ensureDistinctPalette(theme.palettes.automatic.light, 'palettes.automatic.light');
521
+ ensureDistinctPalette(theme.palettes.automatic.dark, 'palettes.automatic.dark');
522
+ const activePalettes = theme.colorSystem.mode === 'automatic' ? theme.palettes.automatic : theme.palettes.bg;
523
+ theme.colors = {
524
+ light: activePalettes.light,
525
+ dark: activePalettes.dark,
526
+ };
527
+ ensureDistinctPalette(theme.colors.light, 'colors.light');
528
+ ensureDistinctPalette(theme.colors.dark, 'colors.dark');
529
+ return theme;
530
+ }
531
+ function renderStyleThemeBlock(theme) {
532
+ return [
533
+ STYLE_THEME_BLOCK_START,
534
+ '## Canonical Theme Tokens (Managed by Stylist)',
535
+ '',
536
+ 'The block below mirrors `project/theme.json` and is managed by `mds stylist sync`.',
537
+ '',
538
+ '```json',
539
+ JSON.stringify(theme, null, 2),
540
+ '```',
541
+ STYLE_THEME_BLOCK_END,
542
+ ].join('\n');
543
+ }
544
+ function parseThemeJson(raw) {
545
+ if (!raw) {
546
+ return null;
547
+ }
548
+ try {
549
+ return normalizeStylistTheme(JSON.parse(raw));
550
+ }
551
+ catch {
552
+ return null;
553
+ }
554
+ }
555
+ function parseThemeFromStyleMarkdown(raw) {
556
+ if (!raw) {
557
+ return null;
558
+ }
559
+ const startIndex = raw.indexOf(STYLE_THEME_BLOCK_START);
560
+ const endIndex = raw.indexOf(STYLE_THEME_BLOCK_END);
561
+ if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
562
+ return null;
563
+ }
564
+ const block = raw.slice(startIndex, endIndex + STYLE_THEME_BLOCK_END.length);
565
+ const codeFenceMatch = block.match(/```json\s*([\s\S]*?)\s*```/i);
566
+ if (!codeFenceMatch?.[1]) {
567
+ return null;
568
+ }
569
+ try {
570
+ return normalizeStylistTheme(JSON.parse(codeFenceMatch[1]));
571
+ }
572
+ catch {
573
+ return null;
574
+ }
575
+ }
576
+ export function renderGlobalCssThemeBlock(theme) {
577
+ const light = theme.colors.light;
578
+ const dark = theme.colors.dark;
579
+ return [
580
+ GLOBAL_CSS_THEME_BLOCK_START,
581
+ ':root {',
582
+ ` --color-background: ${light.background};`,
583
+ ` --color-surface: ${light.surface};`,
584
+ ` --color-typography: ${light.text};`,
585
+ ` --color-primary: ${light.primary};`,
586
+ ` --color-secondary: ${light.secondary};`,
587
+ ` --color-success: ${light.success};`,
588
+ ` --color-warning: ${light.warning};`,
589
+ ` --radius-md: ${theme.layout.radius}px;`,
590
+ ` --spacing-1: ${theme.layout.spacing.xs}px;`,
591
+ ` --spacing-2: ${theme.layout.spacing.sm}px;`,
592
+ ` --spacing-4: ${theme.layout.spacing.md}px;`,
593
+ ` --spacing-6: ${theme.layout.spacing.lg}px;`,
594
+ ` --spacing-8: ${theme.layout.spacing.xl}px;`,
595
+ ` --font-size-display: ${theme.typography.displaySize}px;`,
596
+ ` --font-size-heading: ${theme.typography.headingSize}px;`,
597
+ ` --font-size-body: ${theme.typography.bodySize}px;`,
598
+ ` --font-size-caption: ${theme.typography.captionSize}px;`,
599
+ '}',
600
+ '',
601
+ '@media (prefers-color-scheme: dark) {',
602
+ ' :root {',
603
+ ` --color-background: ${dark.background};`,
604
+ ` --color-surface: ${dark.surface};`,
605
+ ` --color-typography: ${dark.text};`,
606
+ ` --color-primary: ${dark.primary};`,
607
+ ` --color-secondary: ${dark.secondary};`,
608
+ ` --color-success: ${dark.success};`,
609
+ ` --color-warning: ${dark.warning};`,
610
+ ' }',
611
+ '}',
612
+ '',
613
+ '.stylist-theme-root {',
614
+ ' background-color: var(--color-background);',
615
+ ' color: var(--color-typography);',
616
+ '}',
617
+ GLOBAL_CSS_THEME_BLOCK_END,
618
+ ].join('\n');
619
+ }
620
+ export function renderThemeTokensFile(theme) {
621
+ return [
622
+ "export type StylistColorScheme = 'light' | 'dark';",
623
+ "export type StylistColorMode = 'bg' | 'automatic';",
624
+ "export type StylistFamilyMode = 'one' | 'two';",
625
+ '',
626
+ 'export interface StylistColorPalette {',
627
+ ' background: string;',
628
+ ' surface: string;',
629
+ ' text: string;',
630
+ ' primary: string;',
631
+ ' secondary: string;',
632
+ ' success: string;',
633
+ ' warning: string;',
634
+ '}',
635
+ '',
636
+ 'export interface StylistSemanticFamilies {',
637
+ ' primary: string;',
638
+ ' secondary: string;',
639
+ ' success: string;',
640
+ ' warning: string;',
641
+ '}',
642
+ '',
643
+ 'export interface StylistThemeTokens {',
644
+ ' version: 1;',
645
+ ' colorSystem: {',
646
+ ' mode: StylistColorMode;',
647
+ ' previewScheme: StylistColorScheme;',
648
+ ' familyMode: StylistFamilyMode;',
649
+ ' };',
650
+ ' families: {',
651
+ ' light: StylistSemanticFamilies;',
652
+ ' dark: StylistSemanticFamilies;',
653
+ ' };',
654
+ ' palettes: {',
655
+ ' bg: {',
656
+ ' light: StylistColorPalette;',
657
+ ' dark: StylistColorPalette;',
658
+ ' };',
659
+ ' automatic: {',
660
+ ' light: StylistColorPalette;',
661
+ ' dark: StylistColorPalette;',
662
+ ' };',
663
+ ' };',
664
+ ' colors: {',
665
+ ' light: StylistColorPalette;',
666
+ ' dark: StylistColorPalette;',
667
+ ' };',
668
+ ' typography: {',
669
+ ' fontFamily: string;',
670
+ ' fontDisplay: string;',
671
+ ' fontTitle: string;',
672
+ ' fontSubtitle: string;',
673
+ ' fontBody: string;',
674
+ ' fontCaption: string;',
675
+ ' fontMono: string;',
676
+ ' displaySize: number;',
677
+ ' headingSize: number;',
678
+ ' bodySize: number;',
679
+ ' captionSize: number;',
680
+ ' };',
681
+ ' layout: {',
682
+ ' radius: number;',
683
+ ' spacing: {',
684
+ ' xs: number;',
685
+ ' sm: number;',
686
+ ' md: number;',
687
+ ' lg: number;',
688
+ ' xl: number;',
689
+ ' };',
690
+ ' };',
691
+ '}',
692
+ '',
693
+ `export const stylistThemeTokens: StylistThemeTokens = ${renderTsLiteral(theme)};`,
694
+ '',
695
+ 'export default stylistThemeTokens;',
696
+ '',
697
+ ].join('\n');
698
+ }
699
+ function renderTsLiteral(value, indent = 0) {
700
+ if (typeof value === 'string') {
701
+ return `'${value
702
+ .replace(/\\/g, '\\\\')
703
+ .replace(/'/g, "\\'")
704
+ .replace(/\r/g, '\\r')
705
+ .replace(/\n/g, '\\n')}'`;
706
+ }
707
+ if (typeof value === 'number' || typeof value === 'boolean') {
708
+ return String(value);
709
+ }
710
+ if (value === null) {
711
+ return 'null';
712
+ }
713
+ if (Array.isArray(value)) {
714
+ if (value.length === 0) {
715
+ return '[]';
716
+ }
717
+ const nextIndent = indent + 2;
718
+ const entries = value.map((item) => `${' '.repeat(nextIndent)}${renderTsLiteral(item, nextIndent)},`);
719
+ return `[\n${entries.join('\n')}\n${' '.repeat(indent)}]`;
720
+ }
721
+ if (isRecord(value)) {
722
+ const entries = Object.entries(value);
723
+ if (entries.length === 0) {
724
+ return '{}';
725
+ }
726
+ const nextIndent = indent + 2;
727
+ const rendered = entries.map(([key, item]) => {
728
+ return `${' '.repeat(nextIndent)}${formatTsObjectKey(key)}: ${renderTsLiteral(item, nextIndent)},`;
729
+ });
730
+ return `{\n${rendered.join('\n')}\n${' '.repeat(indent)}}`;
731
+ }
732
+ return 'undefined';
733
+ }
734
+ function formatTsObjectKey(key) {
735
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? key : renderTsLiteral(key);
736
+ }
737
+ async function syncStyleLibraryOutputs(projectPath, theme, styleLibrary, writePolicy) {
738
+ switch (styleLibrary) {
739
+ case 'uniwind':
740
+ return [await writeCssAdapter(projectPath, theme, styleLibrary, writePolicy)];
741
+ case 'nativewind':
742
+ return [await writeCssAdapter(projectPath, theme, styleLibrary, writePolicy)];
743
+ case 'nativewindui':
744
+ return [await writeCssAdapter(projectPath, theme, styleLibrary, writePolicy)];
745
+ case 'unistyles':
746
+ return [await writeUnistylesThemeFile(projectPath, theme, writePolicy)];
747
+ case 'restyle':
748
+ return [await writeRestyleThemeFile(projectPath, theme, writePolicy)];
749
+ case 'tamagui':
750
+ return [await writeTamaguiThemeFile(projectPath, theme, writePolicy)];
751
+ case 'stylesheet':
752
+ default:
753
+ return [];
754
+ }
755
+ }
756
+ async function writeCssAdapter(projectPath, theme, styleLibrary, writePolicy) {
757
+ const globalCssPath = path.join(projectPath, 'global.css');
758
+ const existing = (await readOptionalText(globalCssPath)) ?? '';
759
+ const defaultScaffold = renderDefaultCssScaffold(styleLibrary);
760
+ let next = existing;
761
+ if (writePolicy === 'overwrite') {
762
+ if (styleLibrary === 'nativewindui') {
763
+ next = `${defaultScaffold}\n\n${renderNativewindUiGlobalCssThemeBlock(theme)}\n`;
764
+ }
765
+ else {
766
+ next = `${defaultScaffold}\n\n${renderGlobalCssThemeBlock(theme)}\n`;
767
+ }
768
+ await writeTextFileIfChanged(globalCssPath, normalizeTrailingNewline(next));
769
+ return globalCssPath;
770
+ }
771
+ if (!next.trim()) {
772
+ next = defaultScaffold;
773
+ }
774
+ if (styleLibrary === 'nativewindui') {
775
+ next = upsertManagedBlock(next, NATIVEWIND_UI_THEME_BLOCK_START, NATIVEWIND_UI_THEME_BLOCK_END, renderNativewindUiGlobalCssThemeBlock(theme));
776
+ }
777
+ else {
778
+ next = upsertManagedBlock(next, GLOBAL_CSS_THEME_BLOCK_START, GLOBAL_CSS_THEME_BLOCK_END, renderGlobalCssThemeBlock(theme));
779
+ }
780
+ await writeTextFileIfChanged(globalCssPath, normalizeTrailingNewline(next));
781
+ return globalCssPath;
782
+ }
783
+ async function writeUnistylesThemeFile(projectPath, theme, writePolicy) {
784
+ const themePath = path.join(projectPath, 'theme.ts');
785
+ const existing = (await readOptionalText(themePath)) ?? '';
786
+ const block = renderUnistylesManagedBlock(theme);
787
+ let next = existing;
788
+ if (writePolicy === 'overwrite' || !existing.trim()) {
789
+ next = renderUnistylesThemeFile(theme);
790
+ }
791
+ else {
792
+ next = upsertManagedBlock(next, UNISTYLES_THEME_BLOCK_START, UNISTYLES_THEME_BLOCK_END, block);
793
+ }
794
+ await writeTextFileIfChanged(themePath, normalizeTrailingNewline(next));
795
+ return themePath;
796
+ }
797
+ async function writeRestyleThemeFile(projectPath, theme, writePolicy) {
798
+ const themePath = path.join(projectPath, 'theme.ts');
799
+ const existing = (await readOptionalText(themePath)) ?? '';
800
+ const block = renderRestyleManagedBlock(theme);
801
+ let next = existing;
802
+ if (writePolicy === 'overwrite' || !existing.trim()) {
803
+ next = renderRestyleThemeFile(theme);
804
+ }
805
+ else {
806
+ next = upsertManagedBlock(next, RESTYLE_THEME_BLOCK_START, RESTYLE_THEME_BLOCK_END, block);
807
+ }
808
+ await writeTextFileIfChanged(themePath, normalizeTrailingNewline(next));
809
+ return themePath;
810
+ }
811
+ async function writeTamaguiThemeFile(projectPath, theme, writePolicy) {
812
+ const targetPath = path.join(projectPath, 'tamagui.tokens.ts');
813
+ const existing = (await readOptionalText(targetPath)) ?? '';
814
+ let next = existing;
815
+ if (writePolicy === 'overwrite' || !existing.trim()) {
816
+ next = renderTamaguiTokenFile(theme);
817
+ }
818
+ else {
819
+ next = upsertManagedBlock(next, TAMAGUI_THEME_BLOCK_START, TAMAGUI_THEME_BLOCK_END, renderTamaguiManagedBlock(theme));
820
+ }
821
+ await writeTextFileIfChanged(targetPath, normalizeTrailingNewline(next));
822
+ return targetPath;
823
+ }
824
+ function renderNativewindUiGlobalCssThemeBlock(theme) {
825
+ const light = theme.colors.light;
826
+ const dark = theme.colors.dark;
827
+ return [
828
+ NATIVEWIND_UI_THEME_BLOCK_START,
829
+ '@layer base {',
830
+ ' :root {',
831
+ ` --background: ${toRgbSpaceSeparated(light.background)};`,
832
+ ` --foreground: ${toRgbSpaceSeparated(light.text)};`,
833
+ ` --card: ${toRgbSpaceSeparated(light.surface)};`,
834
+ ` --card-foreground: ${toRgbSpaceSeparated(light.text)};`,
835
+ ` --popover: ${toRgbSpaceSeparated(light.surface)};`,
836
+ ` --popover-foreground: ${toRgbSpaceSeparated(light.text)};`,
837
+ ` --primary: ${toRgbSpaceSeparated(light.primary)};`,
838
+ ' --primary-foreground: 255 255 255;',
839
+ ` --secondary: ${toRgbSpaceSeparated(light.secondary)};`,
840
+ ' --secondary-foreground: 255 255 255;',
841
+ ` --muted: ${toRgbSpaceSeparated(light.surface)};`,
842
+ ` --muted-foreground: ${toRgbSpaceSeparated(light.text)};`,
843
+ ` --accent: ${toRgbSpaceSeparated(light.secondary)};`,
844
+ ' --accent-foreground: 255 255 255;',
845
+ ` --destructive: ${toRgbSpaceSeparated(light.warning)};`,
846
+ ' --destructive-foreground: 255 255 255;',
847
+ ` --border: ${toRgbSpaceSeparated(light.surface)};`,
848
+ ` --input: ${toRgbSpaceSeparated(light.surface)};`,
849
+ ` --ring: ${toRgbSpaceSeparated(light.primary)};`,
850
+ '',
851
+ ` --android-background: ${toRgbSpaceSeparated(light.background)};`,
852
+ ` --android-foreground: ${toRgbSpaceSeparated(light.text)};`,
853
+ ` --android-card: ${toRgbSpaceSeparated(light.surface)};`,
854
+ ` --android-card-foreground: ${toRgbSpaceSeparated(light.text)};`,
855
+ ` --android-popover: ${toRgbSpaceSeparated(light.surface)};`,
856
+ ` --android-popover-foreground: ${toRgbSpaceSeparated(light.text)};`,
857
+ ` --android-primary: ${toRgbSpaceSeparated(light.primary)};`,
858
+ ' --android-primary-foreground: 255 255 255;',
859
+ ` --android-secondary: ${toRgbSpaceSeparated(light.secondary)};`,
860
+ ' --android-secondary-foreground: 255 255 255;',
861
+ ` --android-muted: ${toRgbSpaceSeparated(light.surface)};`,
862
+ ` --android-muted-foreground: ${toRgbSpaceSeparated(light.text)};`,
863
+ ` --android-accent: ${toRgbSpaceSeparated(light.secondary)};`,
864
+ ' --android-accent-foreground: 255 255 255;',
865
+ ` --android-destructive: ${toRgbSpaceSeparated(light.warning)};`,
866
+ ' --android-destructive-foreground: 255 255 255;',
867
+ ` --android-border: ${toRgbSpaceSeparated(light.surface)};`,
868
+ ` --android-input: ${toRgbSpaceSeparated(light.surface)};`,
869
+ ` --android-ring: ${toRgbSpaceSeparated(light.primary)};`,
870
+ ' }',
871
+ '',
872
+ ' @media (prefers-color-scheme: dark) {',
873
+ ' :root {',
874
+ ` --background: ${toRgbSpaceSeparated(dark.background)};`,
875
+ ` --foreground: ${toRgbSpaceSeparated(dark.text)};`,
876
+ ` --card: ${toRgbSpaceSeparated(dark.surface)};`,
877
+ ` --card-foreground: ${toRgbSpaceSeparated(dark.text)};`,
878
+ ` --popover: ${toRgbSpaceSeparated(dark.surface)};`,
879
+ ` --popover-foreground: ${toRgbSpaceSeparated(dark.text)};`,
880
+ ` --primary: ${toRgbSpaceSeparated(dark.primary)};`,
881
+ ' --primary-foreground: 255 255 255;',
882
+ ` --secondary: ${toRgbSpaceSeparated(dark.secondary)};`,
883
+ ' --secondary-foreground: 255 255 255;',
884
+ ` --muted: ${toRgbSpaceSeparated(dark.surface)};`,
885
+ ` --muted-foreground: ${toRgbSpaceSeparated(dark.text)};`,
886
+ ` --accent: ${toRgbSpaceSeparated(dark.secondary)};`,
887
+ ' --accent-foreground: 255 255 255;',
888
+ ` --destructive: ${toRgbSpaceSeparated(dark.warning)};`,
889
+ ' --destructive-foreground: 255 255 255;',
890
+ ` --border: ${toRgbSpaceSeparated(dark.surface)};`,
891
+ ` --input: ${toRgbSpaceSeparated(dark.surface)};`,
892
+ ` --ring: ${toRgbSpaceSeparated(dark.primary)};`,
893
+ '',
894
+ ` --android-background: ${toRgbSpaceSeparated(dark.background)};`,
895
+ ` --android-foreground: ${toRgbSpaceSeparated(dark.text)};`,
896
+ ` --android-card: ${toRgbSpaceSeparated(dark.surface)};`,
897
+ ` --android-card-foreground: ${toRgbSpaceSeparated(dark.text)};`,
898
+ ` --android-popover: ${toRgbSpaceSeparated(dark.surface)};`,
899
+ ` --android-popover-foreground: ${toRgbSpaceSeparated(dark.text)};`,
900
+ ` --android-primary: ${toRgbSpaceSeparated(dark.primary)};`,
901
+ ' --android-primary-foreground: 255 255 255;',
902
+ ` --android-secondary: ${toRgbSpaceSeparated(dark.secondary)};`,
903
+ ' --android-secondary-foreground: 255 255 255;',
904
+ ` --android-muted: ${toRgbSpaceSeparated(dark.surface)};`,
905
+ ` --android-muted-foreground: ${toRgbSpaceSeparated(dark.text)};`,
906
+ ` --android-accent: ${toRgbSpaceSeparated(dark.secondary)};`,
907
+ ' --android-accent-foreground: 255 255 255;',
908
+ ` --android-destructive: ${toRgbSpaceSeparated(dark.warning)};`,
909
+ ' --android-destructive-foreground: 255 255 255;',
910
+ ` --android-border: ${toRgbSpaceSeparated(dark.surface)};`,
911
+ ` --android-input: ${toRgbSpaceSeparated(dark.surface)};`,
912
+ ` --android-ring: ${toRgbSpaceSeparated(dark.primary)};`,
913
+ ' }',
914
+ ' }',
915
+ '}',
916
+ NATIVEWIND_UI_THEME_BLOCK_END,
917
+ ].join('\n');
918
+ }
919
+ function renderUnistylesManagedBlock(theme) {
920
+ const light = theme.colors.light;
921
+ const dark = theme.colors.dark;
922
+ return [
923
+ UNISTYLES_THEME_BLOCK_START,
924
+ 'export const lightTheme = {',
925
+ ' colors: {',
926
+ ` typography: '${light.text}',`,
927
+ ` background: '${light.background}',`,
928
+ ` primary: '${light.primary}',`,
929
+ ` secondary: '${light.secondary}',`,
930
+ ` success: '${light.success}',`,
931
+ ` warning: '${light.warning}',`,
932
+ ` surface: '${light.surface}',`,
933
+ ' },',
934
+ ' spacing: {',
935
+ ` xs: ${theme.layout.spacing.xs},`,
936
+ ` sm: ${theme.layout.spacing.sm},`,
937
+ ` md: ${theme.layout.spacing.md},`,
938
+ ` lg: ${theme.layout.spacing.lg},`,
939
+ ` xl: ${theme.layout.spacing.xl},`,
940
+ ' },',
941
+ ' radius: {',
942
+ ` md: ${theme.layout.radius},`,
943
+ ' },',
944
+ '} as const;',
945
+ '',
946
+ 'export const darkTheme = {',
947
+ ' colors: {',
948
+ ` typography: '${dark.text}',`,
949
+ ` background: '${dark.background}',`,
950
+ ` primary: '${dark.primary}',`,
951
+ ` secondary: '${dark.secondary}',`,
952
+ ` success: '${dark.success}',`,
953
+ ` warning: '${dark.warning}',`,
954
+ ` surface: '${dark.surface}',`,
955
+ ' },',
956
+ ' spacing: {',
957
+ ` xs: ${theme.layout.spacing.xs},`,
958
+ ` sm: ${theme.layout.spacing.sm},`,
959
+ ` md: ${theme.layout.spacing.md},`,
960
+ ` lg: ${theme.layout.spacing.lg},`,
961
+ ` xl: ${theme.layout.spacing.xl},`,
962
+ ' },',
963
+ ' radius: {',
964
+ ` md: ${theme.layout.radius},`,
965
+ ' },',
966
+ '} as const;',
967
+ UNISTYLES_THEME_BLOCK_END,
968
+ ].join('\n');
969
+ }
970
+ function renderUnistylesThemeFile(theme) {
971
+ return `${renderUnistylesManagedBlock(theme)}\n`;
972
+ }
973
+ function renderRestyleManagedBlock(theme) {
974
+ const light = theme.colors.light;
975
+ return [
976
+ RESTYLE_THEME_BLOCK_START,
977
+ "import { createTheme } from '@shopify/restyle';",
978
+ '',
979
+ 'export const theme = createTheme({',
980
+ ' colors: {',
981
+ ` background: '${light.background}',`,
982
+ ` text: '${light.text}',`,
983
+ ` muted: '${light.surface}',`,
984
+ ` primary: '${light.primary}',`,
985
+ ` border: '${light.secondary}',`,
986
+ ` success: '${light.success}',`,
987
+ ` warning: '${light.warning}',`,
988
+ ' },',
989
+ ' spacing: {',
990
+ ` xs: ${theme.layout.spacing.xs},`,
991
+ ` sm: ${theme.layout.spacing.sm},`,
992
+ ` md: ${theme.layout.spacing.md},`,
993
+ ` lg: ${theme.layout.spacing.lg},`,
994
+ ` xl: ${theme.layout.spacing.xl},`,
995
+ ' },',
996
+ ' borderRadii: {',
997
+ ` md: ${theme.layout.radius},`,
998
+ ' },',
999
+ ' textVariants: {',
1000
+ ' defaults: {',
1001
+ " color: 'text',",
1002
+ ` fontSize: ${theme.typography.bodySize},`,
1003
+ ' },',
1004
+ ' header: {',
1005
+ " color: 'text',",
1006
+ ` fontSize: ${theme.typography.headingSize},`,
1007
+ " fontWeight: '700',",
1008
+ ' },',
1009
+ ' body: {',
1010
+ " color: 'text',",
1011
+ ` fontSize: ${theme.typography.bodySize},`,
1012
+ ' },',
1013
+ ' muted: {',
1014
+ " color: 'muted',",
1015
+ ` fontSize: ${theme.typography.captionSize},`,
1016
+ ' },',
1017
+ ' },',
1018
+ ' breakpoints: {',
1019
+ ' phone: 0,',
1020
+ ' tablet: 768,',
1021
+ ' },',
1022
+ '});',
1023
+ '',
1024
+ 'export type Theme = typeof theme;',
1025
+ RESTYLE_THEME_BLOCK_END,
1026
+ ].join('\n');
1027
+ }
1028
+ function renderRestyleThemeFile(theme) {
1029
+ return `${renderRestyleManagedBlock(theme)}\n`;
1030
+ }
1031
+ function renderTamaguiManagedBlock(theme) {
1032
+ const light = theme.colors.light;
1033
+ const dark = theme.colors.dark;
1034
+ return [
1035
+ TAMAGUI_THEME_BLOCK_START,
1036
+ 'export const mdsTamaguiThemeTokens = {',
1037
+ ' radius: {',
1038
+ ` md: ${theme.layout.radius},`,
1039
+ ' },',
1040
+ ' size: {',
1041
+ ` display: ${theme.typography.displaySize},`,
1042
+ ` heading: ${theme.typography.headingSize},`,
1043
+ ` body: ${theme.typography.bodySize},`,
1044
+ ` caption: ${theme.typography.captionSize},`,
1045
+ ' },',
1046
+ ' space: {',
1047
+ ` xs: ${theme.layout.spacing.xs},`,
1048
+ ` sm: ${theme.layout.spacing.sm},`,
1049
+ ` md: ${theme.layout.spacing.md},`,
1050
+ ` lg: ${theme.layout.spacing.lg},`,
1051
+ ` xl: ${theme.layout.spacing.xl},`,
1052
+ ' },',
1053
+ ' color: {',
1054
+ ' light: {',
1055
+ ` background: '${light.background}',`,
1056
+ ` surface: '${light.surface}',`,
1057
+ ` text: '${light.text}',`,
1058
+ ` primary: '${light.primary}',`,
1059
+ ` secondary: '${light.secondary}',`,
1060
+ ` success: '${light.success}',`,
1061
+ ` warning: '${light.warning}',`,
1062
+ ' },',
1063
+ ' dark: {',
1064
+ ` background: '${dark.background}',`,
1065
+ ` surface: '${dark.surface}',`,
1066
+ ` text: '${dark.text}',`,
1067
+ ` primary: '${dark.primary}',`,
1068
+ ` secondary: '${dark.secondary}',`,
1069
+ ` success: '${dark.success}',`,
1070
+ ` warning: '${dark.warning}',`,
1071
+ ' },',
1072
+ ' },',
1073
+ '} as const;',
1074
+ TAMAGUI_THEME_BLOCK_END,
1075
+ ].join('\n');
1076
+ }
1077
+ function renderTamaguiTokenFile(theme) {
1078
+ return `${renderTamaguiManagedBlock(theme)}\n`;
1079
+ }
1080
+ function renderDefaultCssScaffold(styleLibrary) {
1081
+ if (styleLibrary === 'uniwind') {
1082
+ return ["@import 'tailwindcss';", "@import 'uniwind';"].join('\n');
1083
+ }
1084
+ return ['@tailwind base;', '@tailwind components;', '@tailwind utilities;'].join('\n');
1085
+ }
1086
+ function toRgbSpaceSeparated(hex) {
1087
+ const normalized = hex.replace('#', '').trim();
1088
+ const r = Number.parseInt(normalized.slice(0, 2), 16);
1089
+ const g = Number.parseInt(normalized.slice(2, 4), 16);
1090
+ const b = Number.parseInt(normalized.slice(4, 6), 16);
1091
+ return `${r} ${g} ${b}`;
1092
+ }
1093
+ function ensureThemeTodoTask(todo) {
1094
+ if (todo.includes(TODO_THEME_TASK)) {
1095
+ return todo;
1096
+ }
1097
+ const lines = todo.split(/\r?\n/);
1098
+ const phaseOneIndex = lines.findIndex((line) => /^##\s+Phase 1\b/i.test(line.trim()));
1099
+ if (phaseOneIndex === -1) {
1100
+ return `${todo.trimEnd()}\n${TODO_THEME_TASK}\n`;
1101
+ }
1102
+ const insertionIndex = findSectionEnd(lines, phaseOneIndex);
1103
+ lines.splice(insertionIndex, 0, TODO_THEME_TASK);
1104
+ return `${lines.join('\n').replace(/\s+$/, '')}\n`;
1105
+ }
1106
+ function findSectionEnd(lines, startHeadingIndex) {
1107
+ for (let i = startHeadingIndex + 1; i < lines.length; i += 1) {
1108
+ if (/^##\s+/.test(lines[i] ?? '')) {
1109
+ return i;
1110
+ }
1111
+ }
1112
+ return lines.length;
1113
+ }
1114
+ function renderDefaultStyleMarkdown() {
1115
+ return [
1116
+ '# Style',
1117
+ '',
1118
+ '## Visual Direction',
1119
+ '',
1120
+ '- Managed collaboratively through `project/theme.json` and the Stylist page.',
1121
+ '',
1122
+ '## Colors',
1123
+ '',
1124
+ '- Add additional brand constraints here when needed.',
1125
+ '',
1126
+ '## Typography',
1127
+ '',
1128
+ '- Add preferred font families and readability guidance.',
1129
+ '',
1130
+ '## Layout/Spacing',
1131
+ '',
1132
+ '- Capture density, spacing rhythm, and border radius guidance.',
1133
+ '',
1134
+ ].join('\n');
1135
+ }
1136
+ function upsertManagedBlock(source, startToken, endToken, replacementBlock) {
1137
+ const startIndex = source.indexOf(startToken);
1138
+ const endIndex = source.indexOf(endToken);
1139
+ if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
1140
+ const before = source.slice(0, startIndex).trimEnd();
1141
+ const after = source.slice(endIndex + endToken.length).trimStart();
1142
+ const merged = `${before}\n\n${replacementBlock}\n\n${after}`.trim();
1143
+ return `${merged}\n`;
1144
+ }
1145
+ const trimmed = source.trimEnd();
1146
+ if (!trimmed) {
1147
+ return `${replacementBlock}\n`;
1148
+ }
1149
+ return `${trimmed}\n\n${replacementBlock}\n`;
1150
+ }
1151
+ async function detectStyleLibraryFromCesConfig(projectPath) {
1152
+ const cesPath = path.join(projectPath, 'cesconfig.jsonc');
1153
+ const raw = await readOptionalText(cesPath);
1154
+ if (!raw) {
1155
+ return null;
1156
+ }
1157
+ try {
1158
+ const parsed = JSON.parse(stripJsonComments(raw));
1159
+ if (!isRecord(parsed)) {
1160
+ return null;
1161
+ }
1162
+ const packages = Array.isArray(parsed.packages) ? parsed.packages : [];
1163
+ for (const item of packages) {
1164
+ if (!isRecord(item)) {
1165
+ continue;
1166
+ }
1167
+ if (item.type !== 'styling') {
1168
+ continue;
1169
+ }
1170
+ const detected = parseStyleLibrary(item.name);
1171
+ if (detected) {
1172
+ return detected;
1173
+ }
1174
+ }
1175
+ }
1176
+ catch {
1177
+ return null;
1178
+ }
1179
+ return null;
1180
+ }
1181
+ async function detectStyleLibraryFromDependencies(projectPath) {
1182
+ const packagePath = path.join(projectPath, 'package.json');
1183
+ const raw = await readOptionalText(packagePath);
1184
+ if (!raw) {
1185
+ return null;
1186
+ }
1187
+ try {
1188
+ const parsed = JSON.parse(raw);
1189
+ if (!isRecord(parsed)) {
1190
+ return null;
1191
+ }
1192
+ const dependencies = isRecord(parsed.dependencies) ? parsed.dependencies : {};
1193
+ const devDependencies = isRecord(parsed.devDependencies) ? parsed.devDependencies : {};
1194
+ const depKeys = new Set([
1195
+ ...Object.keys(dependencies),
1196
+ ...Object.keys(devDependencies),
1197
+ ]);
1198
+ if (depKeys.has('uniwind'))
1199
+ return 'uniwind';
1200
+ if (depKeys.has('@shopify/restyle'))
1201
+ return 'restyle';
1202
+ if (depKeys.has('tamagui') || depKeys.has('@tamagui/config'))
1203
+ return 'tamagui';
1204
+ if (depKeys.has('react-native-unistyles'))
1205
+ return 'unistyles';
1206
+ if (depKeys.has('nativewindui') || depKeys.has('@roninoss/nativewindui'))
1207
+ return 'nativewindui';
1208
+ if (depKeys.has('nativewind')) {
1209
+ const hasNativewindUiTree = (await pathExists(path.join(projectPath, 'components', 'nativewindui'))) ||
1210
+ (await pathExists(path.join(projectPath, 'src', 'components', 'nativewindui')));
1211
+ return hasNativewindUiTree ? 'nativewindui' : 'nativewind';
1212
+ }
1213
+ }
1214
+ catch {
1215
+ return null;
1216
+ }
1217
+ return null;
1218
+ }
1219
+ async function detectStyleLibraryFromFiles(projectPath) {
1220
+ const globalCss = await readOptionalText(path.join(projectPath, 'global.css'));
1221
+ if (globalCss) {
1222
+ if (globalCss.includes("@import 'uniwind'") || globalCss.includes('@import "uniwind"')) {
1223
+ return 'uniwind';
1224
+ }
1225
+ if (globalCss.includes('@tailwind base') || globalCss.includes('@tailwind components')) {
1226
+ if (globalCss.includes('--android-background') || globalCss.includes('--android-primary')) {
1227
+ return 'nativewindui';
1228
+ }
1229
+ return 'nativewind';
1230
+ }
1231
+ }
1232
+ if (await pathExists(path.join(projectPath, 'tamagui.config.ts'))) {
1233
+ return 'tamagui';
1234
+ }
1235
+ if (await pathExists(path.join(projectPath, 'theme.ts'))) {
1236
+ const themeFile = await readOptionalText(path.join(projectPath, 'theme.ts'));
1237
+ if (themeFile?.includes('createTheme(') && themeFile.includes('@shopify/restyle')) {
1238
+ return 'restyle';
1239
+ }
1240
+ if (themeFile?.includes('lightTheme') && themeFile.includes('darkTheme')) {
1241
+ return 'unistyles';
1242
+ }
1243
+ }
1244
+ return null;
1245
+ }
1246
+ function stripJsonComments(raw) {
1247
+ return raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
1248
+ }
1249
+ function parseStyleLibrary(value) {
1250
+ if (typeof value !== 'string') {
1251
+ return null;
1252
+ }
1253
+ const normalized = value.trim().toLowerCase();
1254
+ switch (normalized) {
1255
+ case 'uniwind':
1256
+ case 'nativewind':
1257
+ case 'nativewindui':
1258
+ case 'unistyles':
1259
+ case 'restyle':
1260
+ case 'tamagui':
1261
+ case 'stylesheet':
1262
+ return normalized;
1263
+ default:
1264
+ return null;
1265
+ }
1266
+ }
1267
+ function parseWritePolicy(value) {
1268
+ if (value === 'managed' || value === 'overwrite') {
1269
+ return value;
1270
+ }
1271
+ return null;
1272
+ }
1273
+ function normalizeTrailingNewline(value) {
1274
+ return `${value.replace(/\s+$/, '')}\n`;
1275
+ }
1276
+ async function readOptionalText(filePath) {
1277
+ try {
1278
+ return await readFile(filePath, 'utf8');
1279
+ }
1280
+ catch {
1281
+ return null;
1282
+ }
1283
+ }
1284
+ async function writeTextFileIfChanged(filePath, contents) {
1285
+ const existing = await readOptionalText(filePath);
1286
+ if (existing === contents) {
1287
+ return false;
1288
+ }
1289
+ await mkdir(path.dirname(filePath), { recursive: true });
1290
+ await writeFile(filePath, contents, 'utf8');
1291
+ return true;
1292
+ }
1293
+ async function pathExists(filePath) {
1294
+ try {
1295
+ await access(filePath);
1296
+ return true;
1297
+ }
1298
+ catch {
1299
+ return false;
1300
+ }
1301
+ }
1302
+ function ensureRecord(value, label) {
1303
+ if (!isRecord(value)) {
1304
+ throw new Error(`${label} must be an object.`);
1305
+ }
1306
+ return value;
1307
+ }
1308
+ function ensureHexColor(value, label) {
1309
+ if (typeof value !== 'string') {
1310
+ throw new Error(`${label} must be a string hex color.`);
1311
+ }
1312
+ const normalized = value.trim();
1313
+ if (!/^#[0-9a-fA-F]{6}$/.test(normalized)) {
1314
+ throw new Error(`${label} must use #RRGGBB format.`);
1315
+ }
1316
+ return normalized.toLowerCase();
1317
+ }
1318
+ function ensureNonEmptyString(value, label) {
1319
+ if (typeof value !== 'string' || !value.trim()) {
1320
+ throw new Error(`${label} must be a non-empty string.`);
1321
+ }
1322
+ return value.trim();
1323
+ }
1324
+ function ensureOptionalNonEmptyString(value) {
1325
+ if (typeof value !== 'string') {
1326
+ return undefined;
1327
+ }
1328
+ const trimmed = value.trim();
1329
+ return trimmed.length > 0 ? trimmed : undefined;
1330
+ }
1331
+ function ensureNumberInRange(value, label, minInclusive, maxInclusive) {
1332
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
1333
+ throw new Error(`${label} must be a number.`);
1334
+ }
1335
+ if (value < minInclusive || value > maxInclusive) {
1336
+ throw new Error(`${label} must be between ${minInclusive} and ${maxInclusive}.`);
1337
+ }
1338
+ return Math.round(value * 1000) / 1000;
1339
+ }
1340
+ function ensureColorPalette(value, label) {
1341
+ const palette = {
1342
+ background: ensureHexColor(value.background, `${label}.background`),
1343
+ surface: ensureHexColor(value.surface, `${label}.surface`),
1344
+ text: ensureHexColor(value.text, `${label}.text`),
1345
+ primary: ensureHexColor(value.primary, `${label}.primary`),
1346
+ secondary: ensureHexColor(value.secondary, `${label}.secondary`),
1347
+ success: ensureHexColor(value.success, `${label}.success`),
1348
+ warning: ensureHexColor(value.warning, `${label}.warning`),
1349
+ };
1350
+ return palette;
1351
+ }
1352
+ function ensureDistinctPalette(palette, label) {
1353
+ if (palette.background === palette.surface) {
1354
+ throw new Error(`${label}.background and ${label}.surface cannot match.`);
1355
+ }
1356
+ }
1357
+ function ensureSemanticFamilies(value, label) {
1358
+ return {
1359
+ primary: ensureNonEmptyString(value.primary, `${label}.primary`),
1360
+ secondary: ensureNonEmptyString(value.secondary, `${label}.secondary`),
1361
+ success: ensureNonEmptyString(value.success, `${label}.success`),
1362
+ warning: ensureNonEmptyString(value.warning, `${label}.warning`),
1363
+ };
1364
+ }
1365
+ function ensureEnumValue(value, label, allowed) {
1366
+ if (typeof value !== 'string' || !allowed.includes(value)) {
1367
+ throw new Error(`${label} must be one of: ${allowed.join(', ')}.`);
1368
+ }
1369
+ return value;
1370
+ }
1371
+ function isRecord(value) {
1372
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1373
+ }
1374
+ //# sourceMappingURL=stylist-theme.js.map