radtools 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.
Files changed (133) hide show
  1. package/README.md +108 -0
  2. package/bin/radtools.js +5 -0
  3. package/dist/cli/index.js +427 -0
  4. package/package.json +55 -0
  5. package/templates/api-routes/assets/optimize/route.ts +94 -0
  6. package/templates/api-routes/assets/route.ts +159 -0
  7. package/templates/api-routes/components/create-folder/route.ts +55 -0
  8. package/templates/api-routes/components/route.ts +156 -0
  9. package/templates/api-routes/fonts/route.ts +96 -0
  10. package/templates/api-routes/fonts/upload/route.ts +79 -0
  11. package/templates/api-routes/read-css/route.ts +29 -0
  12. package/templates/api-routes/write-css/route.ts +423 -0
  13. package/templates/components/Rad_os/AppWindow.tsx +423 -0
  14. package/templates/components/Rad_os/MobileAppModal.tsx +76 -0
  15. package/templates/components/Rad_os/WindowTitleBar.tsx +290 -0
  16. package/templates/components/icons/Icon.tsx +224 -0
  17. package/templates/components/icons/README.md +85 -0
  18. package/templates/components/icons/index.ts +20 -0
  19. package/templates/components/icons.tsx +164 -0
  20. package/templates/components/ui/Accordion.tsx +268 -0
  21. package/templates/components/ui/Alert.tsx +111 -0
  22. package/templates/components/ui/Badge.tsx +87 -0
  23. package/templates/components/ui/Breadcrumbs.tsx +88 -0
  24. package/templates/components/ui/Button.tsx +249 -0
  25. package/templates/components/ui/Card.tsx +137 -0
  26. package/templates/components/ui/Checkbox.tsx +137 -0
  27. package/templates/components/ui/ContextMenu.tsx +220 -0
  28. package/templates/components/ui/Dialog.tsx +264 -0
  29. package/templates/components/ui/Divider.tsx +70 -0
  30. package/templates/components/ui/DropdownMenu.tsx +301 -0
  31. package/templates/components/ui/HelpPanel.tsx +119 -0
  32. package/templates/components/ui/Input.tsx +176 -0
  33. package/templates/components/ui/Popover.tsx +211 -0
  34. package/templates/components/ui/Progress.tsx +158 -0
  35. package/templates/components/ui/Select.tsx +134 -0
  36. package/templates/components/ui/Sheet.tsx +316 -0
  37. package/templates/components/ui/Slider.tsx +223 -0
  38. package/templates/components/ui/Switch.tsx +155 -0
  39. package/templates/components/ui/Tabs.tsx +253 -0
  40. package/templates/components/ui/Toast.tsx +192 -0
  41. package/templates/components/ui/Tooltip.tsx +129 -0
  42. package/templates/components/ui/hooks/useModalBehavior.ts +66 -0
  43. package/templates/components/ui/index.ts +84 -0
  44. package/templates/devtools/DevToolsPanel.tsx +261 -0
  45. package/templates/devtools/DevToolsProvider.tsx +43 -0
  46. package/templates/devtools/components/BreakpointIndicator.tsx +49 -0
  47. package/templates/devtools/components/ColorPicker.tsx +33 -0
  48. package/templates/devtools/components/ComponentsSecondaryNav.tsx +44 -0
  49. package/templates/devtools/components/ContextualFooter.tsx +56 -0
  50. package/templates/devtools/components/DraggablePanel.tsx +43 -0
  51. package/templates/devtools/components/PrimaryNavigationFooter.tsx +254 -0
  52. package/templates/devtools/components/SearchableColorDropdown.tsx +253 -0
  53. package/templates/devtools/components/SecondaryNavigation.tsx +36 -0
  54. package/templates/devtools/components/TokenDropdown.tsx +47 -0
  55. package/templates/devtools/components/TypographyFooter.tsx +145 -0
  56. package/templates/devtools/hooks/useMockState.ts +16 -0
  57. package/templates/devtools/index.ts +17 -0
  58. package/templates/devtools/lib/componentScanner.ts +78 -0
  59. package/templates/devtools/lib/cssParser.ts +465 -0
  60. package/templates/devtools/lib/searchIndexes.ts +45 -0
  61. package/templates/devtools/lib/selectorGenerator.ts +86 -0
  62. package/templates/devtools/store/index.ts +66 -0
  63. package/templates/devtools/store/slices/assetsSlice.ts +106 -0
  64. package/templates/devtools/store/slices/componentsSlice.ts +59 -0
  65. package/templates/devtools/store/slices/mockStatesSlice.ts +77 -0
  66. package/templates/devtools/store/slices/panelSlice.ts +17 -0
  67. package/templates/devtools/store/slices/typographySlice.ts +538 -0
  68. package/templates/devtools/store/slices/variablesSlice.ts +167 -0
  69. package/templates/devtools/tabs/AssetsTab/AssetGrid.tsx +76 -0
  70. package/templates/devtools/tabs/AssetsTab/FolderTree.tsx +53 -0
  71. package/templates/devtools/tabs/AssetsTab/UploadDropzone.tsx +76 -0
  72. package/templates/devtools/tabs/AssetsTab/index.tsx +182 -0
  73. package/templates/devtools/tabs/ComponentsTab/AddTabButton.tsx +63 -0
  74. package/templates/devtools/tabs/ComponentsTab/ComponentList.tsx +153 -0
  75. package/templates/devtools/tabs/ComponentsTab/DesignSystemTab.tsx +1515 -0
  76. package/templates/devtools/tabs/ComponentsTab/DynamicFolderTab.tsx +113 -0
  77. package/templates/devtools/tabs/ComponentsTab/PropDisplay.tsx +55 -0
  78. package/templates/devtools/tabs/ComponentsTab/index.tsx +167 -0
  79. package/templates/devtools/tabs/ComponentsTab/previews/.gitkeep +4 -0
  80. package/templates/devtools/tabs/ComponentsTab/previews/Rad_os.tsx +262 -0
  81. package/templates/devtools/tabs/ComponentsTab/tabConfig.ts +53 -0
  82. package/templates/devtools/tabs/MockStatesTab/index.tsx +29 -0
  83. package/templates/devtools/tabs/TypographyTab/FontManager.tsx +421 -0
  84. package/templates/devtools/tabs/TypographyTab/TypographyStylesDisplay.tsx +290 -0
  85. package/templates/devtools/tabs/TypographyTab/index.tsx +98 -0
  86. package/templates/devtools/tabs/VariablesTab/BaseColorEditor.tsx +267 -0
  87. package/templates/devtools/tabs/VariablesTab/BorderRadiusEditor.tsx +37 -0
  88. package/templates/devtools/tabs/VariablesTab/ColorModeSelector.tsx +235 -0
  89. package/templates/devtools/tabs/VariablesTab/index.tsx +100 -0
  90. package/templates/devtools/types/index.ts +99 -0
  91. package/templates/globals.css +574 -0
  92. package/templates/hooks/index.ts +1 -0
  93. package/templates/hooks/useWindowManager.ts +212 -0
  94. package/templates/public/assets/icons/avatar.svg +18 -0
  95. package/templates/public/assets/icons/checkmark-filled.svg +14 -0
  96. package/templates/public/assets/icons/checkmark.svg +14 -0
  97. package/templates/public/assets/icons/chevron-down.svg +14 -0
  98. package/templates/public/assets/icons/close.svg +14 -0
  99. package/templates/public/assets/icons/copy.svg +14 -0
  100. package/templates/public/assets/icons/download.svg +14 -0
  101. package/templates/public/assets/icons/expand.svg +31 -0
  102. package/templates/public/assets/icons/file-blank.svg +17 -0
  103. package/templates/public/assets/icons/file-image.svg +19 -0
  104. package/templates/public/assets/icons/file-written.svg +17 -0
  105. package/templates/public/assets/icons/folder-closed.svg +17 -0
  106. package/templates/public/assets/icons/folder-open.svg +17 -0
  107. package/templates/public/assets/icons/hamburger.svg +18 -0
  108. package/templates/public/assets/icons/home-outline.svg +28 -0
  109. package/templates/public/assets/icons/home.svg +30 -0
  110. package/templates/public/assets/icons/hourglass.svg +25 -0
  111. package/templates/public/assets/icons/information-circle.svg +14 -0
  112. package/templates/public/assets/icons/information.svg +17 -0
  113. package/templates/public/assets/icons/lightning.svg +14 -0
  114. package/templates/public/assets/icons/locked.svg +17 -0
  115. package/templates/public/assets/icons/not-allowed.svg +14 -0
  116. package/templates/public/assets/icons/plus.svg +5 -0
  117. package/templates/public/assets/icons/power-thin.svg +17 -0
  118. package/templates/public/assets/icons/power.svg +17 -0
  119. package/templates/public/assets/icons/question-block.svg +14 -0
  120. package/templates/public/assets/icons/question.svg +17 -0
  121. package/templates/public/assets/icons/refresh-block.svg +14 -0
  122. package/templates/public/assets/icons/refresh.svg +17 -0
  123. package/templates/public/assets/icons/save.svg +14 -0
  124. package/templates/public/assets/icons/search.svg +25 -0
  125. package/templates/public/assets/icons/settings.svg +14 -0
  126. package/templates/public/assets/icons/trash-full.svg +21 -0
  127. package/templates/public/assets/icons/trash-open.svg +23 -0
  128. package/templates/public/assets/icons/trash.svg +18 -0
  129. package/templates/public/assets/icons/unlocked.svg +17 -0
  130. package/templates/public/assets/icons/waring-triangle-filled.svg +17 -0
  131. package/templates/public/assets/icons/warning-triangle-filled-2.svg +30 -0
  132. package/templates/public/assets/icons/warning-triangle-lines.svg +29 -0
  133. package/templates/public/assets/icons/wrench.svg +17 -0
@@ -0,0 +1,423 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { readFile, writeFile, copyFile } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import type { BaseColor, FontFile, FontDefinition, TypographyStyle, ColorMode } from '@/devtools/types';
5
+
6
+ const GLOBALS_PATH = join(process.cwd(), 'app', 'globals.css');
7
+ const BACKUP_PATH = join(process.cwd(), 'app', '.globals.css.backup');
8
+
9
+ export async function POST(req: Request) {
10
+ // Security: Block in production
11
+ if (process.env.NODE_ENV !== 'development') {
12
+ return NextResponse.json(
13
+ { error: 'Dev tools API not available in production' },
14
+ { status: 403 }
15
+ );
16
+ }
17
+
18
+ try {
19
+ const { baseColors, borderRadius, fonts, typographyStyles, colorModes } = await req.json();
20
+
21
+ // Read existing CSS content
22
+ let existingCSS: string;
23
+ try {
24
+ existingCSS = await readFile(GLOBALS_PATH, 'utf-8');
25
+ } catch {
26
+ return NextResponse.json(
27
+ { error: 'Could not read globals.css' },
28
+ { status: 500 }
29
+ );
30
+ }
31
+
32
+ // Create backup before writing
33
+ try {
34
+ await copyFile(GLOBALS_PATH, BACKUP_PATH);
35
+ } catch {
36
+ // Could not create backup - continue anyway
37
+ }
38
+
39
+ // Perform surgical update - only replace @theme blocks, @font-face, and @layer base
40
+ let updatedCSS = existingCSS;
41
+
42
+ // Update @theme blocks if color data provided
43
+ if (baseColors) {
44
+ updatedCSS = updateCSSBlocks(updatedCSS, {
45
+ baseColors,
46
+ borderRadius: borderRadius || {},
47
+ });
48
+ }
49
+
50
+ // Update @font-face declarations if fonts provided
51
+ if (fonts) {
52
+ updatedCSS = updateFontFaces(updatedCSS, fonts);
53
+ }
54
+
55
+ // Update @layer base if typography styles provided
56
+ if (typographyStyles) {
57
+ updatedCSS = updateLayerBase(updatedCSS, typographyStyles, fonts || []);
58
+ }
59
+
60
+ // Update color mode classes if provided
61
+ if (colorModes) {
62
+ updatedCSS = updateColorModeClasses(updatedCSS, colorModes);
63
+ }
64
+
65
+ // Write updated CSS
66
+ await writeFile(GLOBALS_PATH, updatedCSS, 'utf-8');
67
+
68
+ return NextResponse.json({ success: true });
69
+ } catch (error) {
70
+ return NextResponse.json(
71
+ {
72
+ error: 'Failed to write CSS',
73
+ details: String(error),
74
+ hint: 'Try restoring from backup: copy .globals.css.backup to globals.css'
75
+ },
76
+ { status: 500 }
77
+ );
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Surgically update only the @theme inline and @theme blocks,
83
+ * preserving all other CSS (fonts, base styles, scrollbar, etc.)
84
+ */
85
+ function updateCSSBlocks(
86
+ existingCSS: string,
87
+ data: {
88
+ baseColors: BaseColor[];
89
+ borderRadius: Record<string, string>;
90
+ }
91
+ ): string {
92
+ const { baseColors, borderRadius } = data;
93
+
94
+ // Build a map of baseColorId -> value for resolving references
95
+ const colorMap = new Map<string, { name: string; value: string }>();
96
+ for (const color of baseColors) {
97
+ colorMap.set(color.id, { name: color.name, value: color.value });
98
+ }
99
+
100
+ // Generate @theme inline block content
101
+ const themeInlineContent = generateThemeInlineBlock(baseColors);
102
+
103
+ // Generate @theme block content
104
+ const themeContent = generateThemeBlock(baseColors, borderRadius);
105
+
106
+ let updated = existingCSS;
107
+
108
+ // Replace @theme inline block (match from @theme inline { to the closing } )
109
+ // Use a more robust regex that handles nested content
110
+ const themeInlineRegex = /@theme\s+inline\s*\{[^}]*(?:\{[^}]*\}[^}]*)*\}/;
111
+ if (themeInlineRegex.test(updated)) {
112
+ updated = updated.replace(themeInlineRegex, themeInlineContent);
113
+ } else {
114
+ // If no @theme inline block exists, insert after @import
115
+ const importMatch = updated.match(/@import\s+["']tailwindcss["'];?\s*/);
116
+ if (importMatch) {
117
+ const insertPos = (importMatch.index || 0) + importMatch[0].length;
118
+ updated = updated.slice(0, insertPos) + '\n\n' + themeInlineContent + '\n' + updated.slice(insertPos);
119
+ }
120
+ }
121
+
122
+ // Replace @theme block (not inline) - need to match @theme { but not @theme inline {
123
+ // Look for @theme that is NOT followed by "inline"
124
+ const themeRegex = /@theme\s*(?!inline)\{[^}]*(?:\{[^}]*\}[^}]*)*\}/;
125
+ if (themeRegex.test(updated)) {
126
+ updated = updated.replace(themeRegex, themeContent);
127
+ } else {
128
+ // If no @theme block exists, insert after @theme inline
129
+ const themeInlineEndMatch = updated.match(/@theme\s+inline\s*\{[^}]*(?:\{[^}]*\}[^}]*)*\}/);
130
+ if (themeInlineEndMatch) {
131
+ const insertPos = (themeInlineEndMatch.index || 0) + themeInlineEndMatch[0].length;
132
+ updated = updated.slice(0, insertPos) + '\n\n' + themeContent + updated.slice(insertPos);
133
+ }
134
+ }
135
+
136
+ return updated;
137
+ }
138
+
139
+ /**
140
+ * Generate the @theme inline block with base colors
141
+ */
142
+ function generateThemeInlineBlock(baseColors: BaseColor[]): string {
143
+ const brandColors = baseColors.filter(c => c.category === 'brand');
144
+ const neutralColors = baseColors.filter(c => c.category === 'neutral');
145
+
146
+ const brandVars = brandColors
147
+ .map(c => ` --color-${c.name}: ${c.value};`)
148
+ .join('\n');
149
+
150
+ const neutralVars = neutralColors
151
+ .map(c => ` --color-neutral-${c.name}: ${c.value};`)
152
+ .join('\n');
153
+
154
+ return `@theme inline {
155
+ /* ============================================
156
+ BRAND COLORS (internal reference only)
157
+ ============================================ */
158
+
159
+ ${brandVars}
160
+
161
+ /* Neutral Colors */
162
+ ${neutralVars}
163
+
164
+ /* System Colors */
165
+ --color-success-green: #22C55E;
166
+ --color-success-green-dark: #87BB82;
167
+ --color-warning-yellow: var(--color-sun-yellow);
168
+ --color-warning-yellow-dark: #BE9D2B;
169
+ --color-error-red: var(--color-sun-red);
170
+ --color-error-red-dark: #9E433E;
171
+ --color-focus-state: var(--color-sky-blue);
172
+
173
+ /* Fonts */
174
+ --font-mondwest: 'Mondwest';
175
+ --font-joystix: 'Joystix Monospace', monospace;
176
+ }`;
177
+ }
178
+
179
+ /**
180
+ * Generate the @theme block with brand colors and border radius
181
+ */
182
+ function generateThemeBlock(
183
+ baseColors: BaseColor[],
184
+ borderRadius: Record<string, string>
185
+ ): string {
186
+ // Generate brand color utilities (Tailwind v4 auto-generates bg-*, text-*, border-* from these)
187
+ const brandColorUtils: string[] = [];
188
+ for (const color of baseColors) {
189
+ if (color.category === 'brand') {
190
+ brandColorUtils.push(` --color-${color.name}: ${color.value};`);
191
+ }
192
+ }
193
+
194
+ // Generate neutral color utilities
195
+ const neutralColorUtils: string[] = [];
196
+ for (const color of baseColors) {
197
+ if (color.category === 'neutral') {
198
+ neutralColorUtils.push(` --color-neutral-${color.name}: ${color.value};`);
199
+ }
200
+ }
201
+
202
+ // Generate border radius
203
+ const radiusVars = Object.entries(borderRadius)
204
+ .map(([key, value]) => ` --radius-${key}: ${value};`)
205
+ .join('\n');
206
+
207
+ return `@theme {
208
+ /* ============================================
209
+ BRAND COLORS (Tailwind v4 auto-generates utilities)
210
+ bg-sun-yellow, text-black, border-warm-cloud, etc.
211
+ ============================================ */
212
+
213
+ ${brandColorUtils.join('\n')}
214
+
215
+ /* Neutral Colors */
216
+ ${neutralColorUtils.join('\n')}
217
+
218
+ /* System Colors */
219
+ --color-success-green: #22C55E;
220
+ --color-success-green-dark: #87BB82;
221
+ --color-warning-yellow: var(--color-sun-yellow);
222
+ --color-warning-yellow-dark: #BE9D2B;
223
+ --color-error-red: var(--color-sun-red);
224
+ --color-error-red-dark: #9E433E;
225
+ --color-focus-state: var(--color-sky-blue);
226
+
227
+ /* Border Radius → rounded-sm, rounded-md, etc. */
228
+ ${radiusVars}
229
+
230
+ /* Box Shadows → shadow-btn, shadow-card, etc. */
231
+ --shadow-btn: 0 1px 0 0 var(--color-black);
232
+ --shadow-btn-hover: 0 3px 0 0 var(--color-black);
233
+ --shadow-card: 2px 2px 0 0 var(--color-black);
234
+ --shadow-card-lg: 4px 4px 0 0 var(--color-black);
235
+ --shadow-inner: inset 0 0 0 1px var(--color-black);
236
+
237
+ /* Font Families */
238
+ --font-family-mondwest: var(--font-mondwest);
239
+ --font-family-joystix: var(--font-joystix);
240
+ }`;
241
+ }
242
+
243
+ /**
244
+ * Update @font-face declarations
245
+ * Preserves existing fonts and adds new ones
246
+ */
247
+ function updateFontFaces(css: string, fonts: FontDefinition[]): string {
248
+ // Remove existing @font-face blocks
249
+ let updated = css.replace(/@font-face\s*\{[^}]+\}\s*/g, '');
250
+
251
+ // Generate new @font-face blocks
252
+ const fontFaceBlocks: string[] = [];
253
+
254
+ for (const font of fonts) {
255
+ for (const file of font.files) {
256
+ const format = file.format === 'ttf' ? 'truetype'
257
+ : file.format === 'otf' ? 'opentype'
258
+ : file.format;
259
+
260
+ fontFaceBlocks.push(`@font-face {
261
+ font-family: '${font.family}';
262
+ src: url('${file.path}') format('${format}');
263
+ font-weight: ${file.weight};
264
+ font-style: ${file.style};
265
+ font-display: swap;
266
+ }`);
267
+ }
268
+ }
269
+
270
+ // Insert after @import tailwindcss
271
+ const importMatch = updated.match(/@import\s+["']tailwindcss["'];?\s*\n?/);
272
+ if (importMatch && fontFaceBlocks.length > 0) {
273
+ const insertPos = (importMatch.index || 0) + importMatch[0].length;
274
+ const fontFaceCSS = '\n' + fontFaceBlocks.join('\n\n') + '\n';
275
+ updated = updated.slice(0, insertPos) + fontFaceCSS + updated.slice(insertPos);
276
+ }
277
+
278
+ return updated;
279
+ }
280
+
281
+ /**
282
+ * Update @layer base with typography styles
283
+ */
284
+ function updateLayerBase(
285
+ css: string,
286
+ typographyStyles: TypographyStyle[],
287
+ fonts: FontDefinition[]
288
+ ): string {
289
+ // Build font family map
290
+ const fontFamilyMap = new Map<string, string>();
291
+ for (const font of fonts) {
292
+ fontFamilyMap.set(font.id, font.family.toLowerCase().replace(/\s+/g, ''));
293
+ }
294
+
295
+ // Generate element rules
296
+ const elementRules: string[] = [];
297
+
298
+ for (const style of typographyStyles) {
299
+ const classes: string[] = [];
300
+
301
+ // Add font family class if available
302
+ const fontClass = fontFamilyMap.get(style.fontFamilyId);
303
+ if (fontClass) {
304
+ classes.push(`font-${fontClass}`);
305
+ }
306
+
307
+ // Add size, weight, and other classes
308
+ if (style.fontSize) classes.push(style.fontSize);
309
+ if (style.fontWeight) classes.push(style.fontWeight);
310
+ if (style.lineHeight) classes.push(style.lineHeight);
311
+ if (style.letterSpacing) classes.push(style.letterSpacing);
312
+
313
+ // Add color - convert baseColorId to Tailwind class
314
+ // baseColorId is the base color name (e.g., 'black', 'cream', 'sky-blue')
315
+ if (style.baseColorId) {
316
+ classes.push(`text-${style.baseColorId}`);
317
+ }
318
+
319
+ // Add utilities
320
+ if (style.utilities) {
321
+ classes.push(...style.utilities);
322
+ }
323
+
324
+ if (classes.length > 0) {
325
+ elementRules.push(` ${style.element} {
326
+ @apply ${classes.join(' ')};
327
+ }`);
328
+ }
329
+ }
330
+
331
+ const layerBaseContent = `@layer base {
332
+ ${elementRules.join('\n\n')}
333
+ }`;
334
+
335
+ // Replace existing @layer base or insert before closing content
336
+ // Match @layer base { ... } with any whitespace/newlines
337
+ const layerBaseRegex = /@layer\s+base\s*\{[\s\S]*?\}/;
338
+
339
+ if (layerBaseRegex.test(css)) {
340
+ return css.replace(layerBaseRegex, layerBaseContent);
341
+ } else {
342
+ // Insert at the end of the file
343
+ return css.trimEnd() + '\n\n' + layerBaseContent + '\n';
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Update color mode classes in CSS
349
+ * Writes color mode overrides as CSS classes (.dark, .light, etc.)
350
+ */
351
+ function updateColorModeClasses(css: string, colorModes: ColorMode[]): string {
352
+ let updated = css;
353
+
354
+ // First, remove existing color mode class blocks that we manage
355
+ // Match patterns like .dark { ... } or .light { ... }
356
+ const knownModeNames = colorModes.map(m => m.name);
357
+
358
+ for (const modeName of knownModeNames) {
359
+ // Remove existing block for this mode
360
+ const modeRegex = new RegExp(`\\.${modeName}\\s*\\{[^}]*\\}\\s*`, 'g');
361
+ updated = updated.replace(modeRegex, '');
362
+ }
363
+
364
+ // Also clean up any orphan color mode classes that might exist
365
+ // This regex matches common color mode class names
366
+ const commonModes = ['dark', 'light', 'contrast'];
367
+ for (const modeName of commonModes) {
368
+ if (!knownModeNames.includes(modeName)) {
369
+ // Only remove if it's a devtools-managed block (has CSS variable overrides)
370
+ const modeRegex = new RegExp(`\\.${modeName}\\s*\\{[^}]*--[^}]*\\}\\s*`, 'g');
371
+ updated = updated.replace(modeRegex, '');
372
+ }
373
+ }
374
+
375
+ // Generate new color mode class blocks
376
+ const colorModeBlocks: string[] = [];
377
+
378
+ for (const mode of colorModes) {
379
+ if (Object.keys(mode.overrides).length === 0) continue;
380
+
381
+ const overrideVars = Object.entries(mode.overrides)
382
+ .map(([colorName, colorRef]) => {
383
+ // colorRef is like "neutral-darkest" or "sun-yellow" (base color name)
384
+ // Convert to CSS variable reference
385
+ const varName = colorRef.startsWith('neutral-')
386
+ ? `--color-neutral-${colorRef.replace('neutral-', '')}`
387
+ : `--color-${colorRef}`;
388
+ return ` --color-${colorName}: var(${varName});`;
389
+ })
390
+ .join('\n');
391
+
392
+ colorModeBlocks.push(`.${mode.name} {\n${overrideVars}\n}`);
393
+ }
394
+
395
+ if (colorModeBlocks.length === 0) {
396
+ return updated;
397
+ }
398
+
399
+ // Find the right place to insert color mode classes
400
+ // Insert after @theme block but before :root or scrollbar styles
401
+ const colorModeCSS = `\n/* ============================================
402
+ COLOR MODES (DevTools managed)
403
+ ============================================ */\n\n${colorModeBlocks.join('\n\n')}\n`;
404
+
405
+ // Look for :root block as insertion point
406
+ const rootMatch = updated.match(/:root\s*\{/);
407
+ if (rootMatch && rootMatch.index !== undefined) {
408
+ // Insert before :root
409
+ updated = updated.slice(0, rootMatch.index) + colorModeCSS + '\n' + updated.slice(rootMatch.index);
410
+ } else {
411
+ // Look for scrollbar styles as insertion point
412
+ const scrollbarMatch = updated.match(/\/\*.*scrollbar.*\*\//i);
413
+ if (scrollbarMatch && scrollbarMatch.index !== undefined) {
414
+ // Insert before scrollbar section
415
+ updated = updated.slice(0, scrollbarMatch.index) + colorModeCSS + '\n' + updated.slice(scrollbarMatch.index);
416
+ } else {
417
+ // Append at the end
418
+ updated = updated.trimEnd() + '\n' + colorModeCSS;
419
+ }
420
+ }
421
+
422
+ return updated;
423
+ }