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.
- package/README.md +108 -0
- package/bin/radtools.js +5 -0
- package/dist/cli/index.js +427 -0
- package/package.json +55 -0
- package/templates/api-routes/assets/optimize/route.ts +94 -0
- package/templates/api-routes/assets/route.ts +159 -0
- package/templates/api-routes/components/create-folder/route.ts +55 -0
- package/templates/api-routes/components/route.ts +156 -0
- package/templates/api-routes/fonts/route.ts +96 -0
- package/templates/api-routes/fonts/upload/route.ts +79 -0
- package/templates/api-routes/read-css/route.ts +29 -0
- package/templates/api-routes/write-css/route.ts +423 -0
- package/templates/components/Rad_os/AppWindow.tsx +423 -0
- package/templates/components/Rad_os/MobileAppModal.tsx +76 -0
- package/templates/components/Rad_os/WindowTitleBar.tsx +290 -0
- package/templates/components/icons/Icon.tsx +224 -0
- package/templates/components/icons/README.md +85 -0
- package/templates/components/icons/index.ts +20 -0
- package/templates/components/icons.tsx +164 -0
- package/templates/components/ui/Accordion.tsx +268 -0
- package/templates/components/ui/Alert.tsx +111 -0
- package/templates/components/ui/Badge.tsx +87 -0
- package/templates/components/ui/Breadcrumbs.tsx +88 -0
- package/templates/components/ui/Button.tsx +249 -0
- package/templates/components/ui/Card.tsx +137 -0
- package/templates/components/ui/Checkbox.tsx +137 -0
- package/templates/components/ui/ContextMenu.tsx +220 -0
- package/templates/components/ui/Dialog.tsx +264 -0
- package/templates/components/ui/Divider.tsx +70 -0
- package/templates/components/ui/DropdownMenu.tsx +301 -0
- package/templates/components/ui/HelpPanel.tsx +119 -0
- package/templates/components/ui/Input.tsx +176 -0
- package/templates/components/ui/Popover.tsx +211 -0
- package/templates/components/ui/Progress.tsx +158 -0
- package/templates/components/ui/Select.tsx +134 -0
- package/templates/components/ui/Sheet.tsx +316 -0
- package/templates/components/ui/Slider.tsx +223 -0
- package/templates/components/ui/Switch.tsx +155 -0
- package/templates/components/ui/Tabs.tsx +253 -0
- package/templates/components/ui/Toast.tsx +192 -0
- package/templates/components/ui/Tooltip.tsx +129 -0
- package/templates/components/ui/hooks/useModalBehavior.ts +66 -0
- package/templates/components/ui/index.ts +84 -0
- package/templates/devtools/DevToolsPanel.tsx +261 -0
- package/templates/devtools/DevToolsProvider.tsx +43 -0
- package/templates/devtools/components/BreakpointIndicator.tsx +49 -0
- package/templates/devtools/components/ColorPicker.tsx +33 -0
- package/templates/devtools/components/ComponentsSecondaryNav.tsx +44 -0
- package/templates/devtools/components/ContextualFooter.tsx +56 -0
- package/templates/devtools/components/DraggablePanel.tsx +43 -0
- package/templates/devtools/components/PrimaryNavigationFooter.tsx +254 -0
- package/templates/devtools/components/SearchableColorDropdown.tsx +253 -0
- package/templates/devtools/components/SecondaryNavigation.tsx +36 -0
- package/templates/devtools/components/TokenDropdown.tsx +47 -0
- package/templates/devtools/components/TypographyFooter.tsx +145 -0
- package/templates/devtools/hooks/useMockState.ts +16 -0
- package/templates/devtools/index.ts +17 -0
- package/templates/devtools/lib/componentScanner.ts +78 -0
- package/templates/devtools/lib/cssParser.ts +465 -0
- package/templates/devtools/lib/searchIndexes.ts +45 -0
- package/templates/devtools/lib/selectorGenerator.ts +86 -0
- package/templates/devtools/store/index.ts +66 -0
- package/templates/devtools/store/slices/assetsSlice.ts +106 -0
- package/templates/devtools/store/slices/componentsSlice.ts +59 -0
- package/templates/devtools/store/slices/mockStatesSlice.ts +77 -0
- package/templates/devtools/store/slices/panelSlice.ts +17 -0
- package/templates/devtools/store/slices/typographySlice.ts +538 -0
- package/templates/devtools/store/slices/variablesSlice.ts +167 -0
- package/templates/devtools/tabs/AssetsTab/AssetGrid.tsx +76 -0
- package/templates/devtools/tabs/AssetsTab/FolderTree.tsx +53 -0
- package/templates/devtools/tabs/AssetsTab/UploadDropzone.tsx +76 -0
- package/templates/devtools/tabs/AssetsTab/index.tsx +182 -0
- package/templates/devtools/tabs/ComponentsTab/AddTabButton.tsx +63 -0
- package/templates/devtools/tabs/ComponentsTab/ComponentList.tsx +153 -0
- package/templates/devtools/tabs/ComponentsTab/DesignSystemTab.tsx +1515 -0
- package/templates/devtools/tabs/ComponentsTab/DynamicFolderTab.tsx +113 -0
- package/templates/devtools/tabs/ComponentsTab/PropDisplay.tsx +55 -0
- package/templates/devtools/tabs/ComponentsTab/index.tsx +167 -0
- package/templates/devtools/tabs/ComponentsTab/previews/.gitkeep +4 -0
- package/templates/devtools/tabs/ComponentsTab/previews/Rad_os.tsx +262 -0
- package/templates/devtools/tabs/ComponentsTab/tabConfig.ts +53 -0
- package/templates/devtools/tabs/MockStatesTab/index.tsx +29 -0
- package/templates/devtools/tabs/TypographyTab/FontManager.tsx +421 -0
- package/templates/devtools/tabs/TypographyTab/TypographyStylesDisplay.tsx +290 -0
- package/templates/devtools/tabs/TypographyTab/index.tsx +98 -0
- package/templates/devtools/tabs/VariablesTab/BaseColorEditor.tsx +267 -0
- package/templates/devtools/tabs/VariablesTab/BorderRadiusEditor.tsx +37 -0
- package/templates/devtools/tabs/VariablesTab/ColorModeSelector.tsx +235 -0
- package/templates/devtools/tabs/VariablesTab/index.tsx +100 -0
- package/templates/devtools/types/index.ts +99 -0
- package/templates/globals.css +574 -0
- package/templates/hooks/index.ts +1 -0
- package/templates/hooks/useWindowManager.ts +212 -0
- package/templates/public/assets/icons/avatar.svg +18 -0
- package/templates/public/assets/icons/checkmark-filled.svg +14 -0
- package/templates/public/assets/icons/checkmark.svg +14 -0
- package/templates/public/assets/icons/chevron-down.svg +14 -0
- package/templates/public/assets/icons/close.svg +14 -0
- package/templates/public/assets/icons/copy.svg +14 -0
- package/templates/public/assets/icons/download.svg +14 -0
- package/templates/public/assets/icons/expand.svg +31 -0
- package/templates/public/assets/icons/file-blank.svg +17 -0
- package/templates/public/assets/icons/file-image.svg +19 -0
- package/templates/public/assets/icons/file-written.svg +17 -0
- package/templates/public/assets/icons/folder-closed.svg +17 -0
- package/templates/public/assets/icons/folder-open.svg +17 -0
- package/templates/public/assets/icons/hamburger.svg +18 -0
- package/templates/public/assets/icons/home-outline.svg +28 -0
- package/templates/public/assets/icons/home.svg +30 -0
- package/templates/public/assets/icons/hourglass.svg +25 -0
- package/templates/public/assets/icons/information-circle.svg +14 -0
- package/templates/public/assets/icons/information.svg +17 -0
- package/templates/public/assets/icons/lightning.svg +14 -0
- package/templates/public/assets/icons/locked.svg +17 -0
- package/templates/public/assets/icons/not-allowed.svg +14 -0
- package/templates/public/assets/icons/plus.svg +5 -0
- package/templates/public/assets/icons/power-thin.svg +17 -0
- package/templates/public/assets/icons/power.svg +17 -0
- package/templates/public/assets/icons/question-block.svg +14 -0
- package/templates/public/assets/icons/question.svg +17 -0
- package/templates/public/assets/icons/refresh-block.svg +14 -0
- package/templates/public/assets/icons/refresh.svg +17 -0
- package/templates/public/assets/icons/save.svg +14 -0
- package/templates/public/assets/icons/search.svg +25 -0
- package/templates/public/assets/icons/settings.svg +14 -0
- package/templates/public/assets/icons/trash-full.svg +21 -0
- package/templates/public/assets/icons/trash-open.svg +23 -0
- package/templates/public/assets/icons/trash.svg +18 -0
- package/templates/public/assets/icons/unlocked.svg +17 -0
- package/templates/public/assets/icons/waring-triangle-filled.svg +17 -0
- package/templates/public/assets/icons/warning-triangle-filled-2.svg +30 -0
- package/templates/public/assets/icons/warning-triangle-lines.svg +29 -0
- 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
|
+
}
|