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,465 @@
|
|
|
1
|
+
import type { BaseColor, ColorMode, FontDefinition, FontFile, TypographyStyle } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface ParsedCSS {
|
|
4
|
+
themeInline: Record<string, string>;
|
|
5
|
+
theme: Record<string, string>;
|
|
6
|
+
colorModes: Record<string, Record<string, string>>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Known brand color names in radOS
|
|
10
|
+
const KNOWN_BRAND_COLORS = [
|
|
11
|
+
'sun-yellow', 'sky-blue', 'warm-cloud', 'sunset-fuzz',
|
|
12
|
+
'sun-red', 'green', 'cream', 'black', 'white', 'transparent'
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse globals.css and extract theme variables
|
|
17
|
+
*/
|
|
18
|
+
export function parseGlobalsCSS(css: string): ParsedCSS {
|
|
19
|
+
const result: ParsedCSS = {
|
|
20
|
+
themeInline: {},
|
|
21
|
+
theme: {},
|
|
22
|
+
colorModes: {},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Parse @theme inline block - find the complete block including nested content
|
|
26
|
+
const themeInlineMatch = css.match(/@theme\s+inline\s*\{([\s\S]*?)(?=\n@theme\s*\{|\n\/\*|$)/);
|
|
27
|
+
if (themeInlineMatch) {
|
|
28
|
+
// Find the closing brace by counting braces
|
|
29
|
+
const content = extractBlockContent(css, themeInlineMatch.index!);
|
|
30
|
+
if (content) {
|
|
31
|
+
result.themeInline = parseVariables(content);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Parse @theme block (not inline)
|
|
36
|
+
const themeBlockStart = css.indexOf('@theme {');
|
|
37
|
+
if (themeBlockStart !== -1) {
|
|
38
|
+
const content = extractBlockContent(css, themeBlockStart);
|
|
39
|
+
if (content) {
|
|
40
|
+
result.theme = parseVariables(content);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Parse color mode classes (.dark, .light, etc.)
|
|
45
|
+
const modeMatches = css.matchAll(/\.(\w+)\s*\{([\s\S]*?)\}/g);
|
|
46
|
+
for (const match of modeMatches) {
|
|
47
|
+
const modeName = match[1];
|
|
48
|
+
// Only capture known color modes
|
|
49
|
+
if (['dark', 'light', 'contrast'].includes(modeName)) {
|
|
50
|
+
result.colorModes[modeName] = parseVariables(match[2]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extract content between matching braces
|
|
59
|
+
*/
|
|
60
|
+
function extractBlockContent(css: string, startIndex: number): string | null {
|
|
61
|
+
const openBrace = css.indexOf('{', startIndex);
|
|
62
|
+
if (openBrace === -1) return null;
|
|
63
|
+
|
|
64
|
+
let depth = 0;
|
|
65
|
+
let closeBrace = -1;
|
|
66
|
+
|
|
67
|
+
for (let i = openBrace; i < css.length; i++) {
|
|
68
|
+
if (css[i] === '{') depth++;
|
|
69
|
+
if (css[i] === '}') {
|
|
70
|
+
depth--;
|
|
71
|
+
if (depth === 0) {
|
|
72
|
+
closeBrace = i;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (closeBrace === -1) return null;
|
|
79
|
+
return css.slice(openBrace + 1, closeBrace);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse CSS variables from a block of CSS
|
|
84
|
+
*/
|
|
85
|
+
export function parseVariables(block: string): Record<string, string> {
|
|
86
|
+
const vars: Record<string, string> = {};
|
|
87
|
+
const matches = block.matchAll(/(--[\w-]+):\s*([^;]+);/g);
|
|
88
|
+
|
|
89
|
+
for (const match of matches) {
|
|
90
|
+
vars[match[1]] = match[2].trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return vars;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve a CSS variable reference (handles var() references)
|
|
98
|
+
*/
|
|
99
|
+
export function resolveVariable(
|
|
100
|
+
varName: string,
|
|
101
|
+
parsed: ParsedCSS
|
|
102
|
+
): string | null {
|
|
103
|
+
// Check theme first, then themeInline
|
|
104
|
+
const value = parsed.theme[varName] || parsed.themeInline[varName];
|
|
105
|
+
|
|
106
|
+
if (!value) return null;
|
|
107
|
+
|
|
108
|
+
// Resolve var() references
|
|
109
|
+
const varRef = value.match(/var\((--[\w-]+)\)/);
|
|
110
|
+
if (varRef) {
|
|
111
|
+
return resolveVariable(varRef[1], parsed);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create display name from variable name
|
|
119
|
+
* e.g., "sun-yellow" -> "Sun Yellow"
|
|
120
|
+
*/
|
|
121
|
+
function toDisplayName(name: string): string {
|
|
122
|
+
return name
|
|
123
|
+
.split('-')
|
|
124
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
125
|
+
.join(' ');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Convert parsed CSS to store-friendly structures (new data model)
|
|
130
|
+
*/
|
|
131
|
+
export function parsedCSSToStoreState(parsed: ParsedCSS): {
|
|
132
|
+
baseColors: BaseColor[];
|
|
133
|
+
colorModes: ColorMode[];
|
|
134
|
+
borderRadius: Record<string, string>;
|
|
135
|
+
} {
|
|
136
|
+
const baseColors: BaseColor[] = [];
|
|
137
|
+
const borderRadius: Record<string, string> = {};
|
|
138
|
+
|
|
139
|
+
// Build a map to track color IDs for reference resolution
|
|
140
|
+
const colorIdMap = new Map<string, string>(); // name -> id
|
|
141
|
+
|
|
142
|
+
// Extract base colors from @theme inline
|
|
143
|
+
for (const [key, value] of Object.entries(parsed.themeInline)) {
|
|
144
|
+
// Skip non-color variables (like fonts)
|
|
145
|
+
if (key.startsWith('--font-')) continue;
|
|
146
|
+
|
|
147
|
+
// Brand colors (--color-sun-yellow, --color-cream, etc.)
|
|
148
|
+
if (key.startsWith('--color-') && !key.includes('neutral') && !key.includes('success') && !key.includes('warning') && !key.includes('error') && !key.includes('focus')) {
|
|
149
|
+
const name = key.replace('--color-', '');
|
|
150
|
+
// Only add if it's a known brand color or looks like a hex value
|
|
151
|
+
if (KNOWN_BRAND_COLORS.includes(name) || value.startsWith('#')) {
|
|
152
|
+
const id = name; // Use name as ID for stable references
|
|
153
|
+
colorIdMap.set(name, id);
|
|
154
|
+
baseColors.push({
|
|
155
|
+
id,
|
|
156
|
+
name,
|
|
157
|
+
displayName: toDisplayName(name),
|
|
158
|
+
value: value.startsWith('var(') ? resolveVariable(key, parsed) || value : value,
|
|
159
|
+
category: 'brand',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Neutral colors (--color-neutral-lightest, --neutral-dark, etc.)
|
|
164
|
+
else if (key.startsWith('--color-neutral-') || key.startsWith('--neutral-')) {
|
|
165
|
+
const name = key.replace('--color-neutral-', '').replace('--neutral-', '');
|
|
166
|
+
const id = name;
|
|
167
|
+
colorIdMap.set(name, id);
|
|
168
|
+
colorIdMap.set(`neutral-${name}`, id); // Also map with neutral prefix
|
|
169
|
+
baseColors.push({
|
|
170
|
+
id,
|
|
171
|
+
name,
|
|
172
|
+
displayName: toDisplayName(name),
|
|
173
|
+
value: value.startsWith('var(') ? resolveVariable(key, parsed) || value : value,
|
|
174
|
+
category: 'neutral',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Extract border radius from @theme
|
|
180
|
+
for (const [key, value] of Object.entries(parsed.theme)) {
|
|
181
|
+
if (key.startsWith('--radius-')) {
|
|
182
|
+
const radiusName = key.replace('--radius-', '');
|
|
183
|
+
borderRadius[radiusName] = value;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Extract color modes
|
|
188
|
+
const colorModes: ColorMode[] = Object.entries(parsed.colorModes).map(([name, overrides]) => {
|
|
189
|
+
const processedOverrides: Record<string, string> = {};
|
|
190
|
+
|
|
191
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
192
|
+
// Process color overrides - map to base color names
|
|
193
|
+
if (key.startsWith('--color-')) {
|
|
194
|
+
const colorName = key.replace('--color-', '');
|
|
195
|
+
const reference = value.includes('var(') ? value.match(/var\(--color-([\w-]+)\)/)?.[1] || value : value;
|
|
196
|
+
processedOverrides[colorName] = reference;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
id: crypto.randomUUID(),
|
|
202
|
+
name,
|
|
203
|
+
className: `.${name}`,
|
|
204
|
+
overrides: processedOverrides,
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
baseColors,
|
|
210
|
+
colorModes,
|
|
211
|
+
borderRadius,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Font Face Parsing
|
|
218
|
+
// ============================================================================
|
|
219
|
+
|
|
220
|
+
// Weight name to number mapping
|
|
221
|
+
const WEIGHT_MAP: Record<string, number> = {
|
|
222
|
+
'thin': 100,
|
|
223
|
+
'hairline': 100,
|
|
224
|
+
'extralight': 200,
|
|
225
|
+
'ultralight': 200,
|
|
226
|
+
'light': 300,
|
|
227
|
+
'regular': 400,
|
|
228
|
+
'normal': 400,
|
|
229
|
+
'medium': 500,
|
|
230
|
+
'semibold': 600,
|
|
231
|
+
'demibold': 600,
|
|
232
|
+
'bold': 700,
|
|
233
|
+
'extrabold': 800,
|
|
234
|
+
'ultrabold': 800,
|
|
235
|
+
'black': 900,
|
|
236
|
+
'heavy': 900,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Auto-detect weight and style from filename
|
|
241
|
+
* e.g., "Mondwest-Bold.woff2" -> { weight: 700, style: 'normal' }
|
|
242
|
+
* e.g., "Mondwest-Italic.woff2" -> { weight: 400, style: 'italic' }
|
|
243
|
+
*/
|
|
244
|
+
export function detectFontPropertiesFromFilename(filename: string): { weight: number; style: string } {
|
|
245
|
+
const name = filename.toLowerCase();
|
|
246
|
+
|
|
247
|
+
// Detect style
|
|
248
|
+
const style = name.includes('italic') ? 'italic' : 'normal';
|
|
249
|
+
|
|
250
|
+
// Detect weight
|
|
251
|
+
let weight = 400; // Default to regular
|
|
252
|
+
for (const [weightName, weightValue] of Object.entries(WEIGHT_MAP)) {
|
|
253
|
+
if (name.includes(weightName)) {
|
|
254
|
+
weight = weightValue;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { weight, style };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Parse @font-face declarations from CSS
|
|
264
|
+
*/
|
|
265
|
+
export function parseFontFaces(css: string): FontDefinition[] {
|
|
266
|
+
const fontFaces: FontDefinition[] = [];
|
|
267
|
+
const fontMap = new Map<string, FontDefinition>();
|
|
268
|
+
|
|
269
|
+
// Match all @font-face blocks
|
|
270
|
+
const fontFaceRegex = /@font-face\s*\{([^}]+)\}/g;
|
|
271
|
+
let match;
|
|
272
|
+
|
|
273
|
+
while ((match = fontFaceRegex.exec(css)) !== null) {
|
|
274
|
+
const block = match[1];
|
|
275
|
+
|
|
276
|
+
// Extract font-family
|
|
277
|
+
const familyMatch = block.match(/font-family:\s*['"]?([^'";]+)['"]?;/);
|
|
278
|
+
if (!familyMatch) continue;
|
|
279
|
+
const family = familyMatch[1].trim();
|
|
280
|
+
|
|
281
|
+
// Extract src (path and format)
|
|
282
|
+
const srcMatch = block.match(/src:\s*url\(['"]?([^'"()]+)['"]?\)\s*format\(['"]?([^'"]+)['"]?\)/);
|
|
283
|
+
if (!srcMatch) continue;
|
|
284
|
+
const path = srcMatch[1];
|
|
285
|
+
const format = srcMatch[2].replace('woff2', 'woff2').replace('truetype', 'ttf').replace('opentype', 'otf') as FontFile['format'];
|
|
286
|
+
|
|
287
|
+
// Extract font-weight
|
|
288
|
+
const weightMatch = block.match(/font-weight:\s*(\d+);/);
|
|
289
|
+
const weight = weightMatch ? parseInt(weightMatch[1], 10) : 400;
|
|
290
|
+
|
|
291
|
+
// Extract font-style
|
|
292
|
+
const styleMatch = block.match(/font-style:\s*(\w+);/);
|
|
293
|
+
const style = styleMatch ? styleMatch[1] : 'normal';
|
|
294
|
+
|
|
295
|
+
// Get or create font definition
|
|
296
|
+
const fontId = family.toLowerCase().replace(/\s+/g, '-');
|
|
297
|
+
let font = fontMap.get(fontId);
|
|
298
|
+
|
|
299
|
+
if (!font) {
|
|
300
|
+
font = {
|
|
301
|
+
id: fontId,
|
|
302
|
+
name: family,
|
|
303
|
+
family: family,
|
|
304
|
+
files: [],
|
|
305
|
+
weights: [],
|
|
306
|
+
styles: [],
|
|
307
|
+
};
|
|
308
|
+
fontMap.set(fontId, font);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Add file
|
|
312
|
+
const fileId = `${fontId}-${weight}-${style}`;
|
|
313
|
+
font.files.push({
|
|
314
|
+
id: fileId,
|
|
315
|
+
weight,
|
|
316
|
+
style,
|
|
317
|
+
format: format as FontFile['format'],
|
|
318
|
+
path,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Update weights and styles
|
|
322
|
+
if (!font.weights.includes(weight)) {
|
|
323
|
+
font.weights.push(weight);
|
|
324
|
+
font.weights.sort((a, b) => a - b);
|
|
325
|
+
}
|
|
326
|
+
if (!font.styles.includes(style)) {
|
|
327
|
+
font.styles.push(style);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return Array.from(fontMap.values());
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ============================================================================
|
|
335
|
+
// Layer Base Parsing
|
|
336
|
+
// ============================================================================
|
|
337
|
+
|
|
338
|
+
// Tailwind class patterns for typography
|
|
339
|
+
const SIZE_CLASSES = ['text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl', 'text-5xl', 'text-6xl'];
|
|
340
|
+
const WEIGHT_CLASSES = ['font-thin', 'font-extralight', 'font-light', 'font-normal', 'font-medium', 'font-semibold', 'font-bold', 'font-extrabold', 'font-black'];
|
|
341
|
+
const LEADING_CLASSES = ['leading-none', 'leading-tight', 'leading-snug', 'leading-normal', 'leading-relaxed', 'leading-loose'];
|
|
342
|
+
const TRACKING_CLASSES = ['tracking-tighter', 'tracking-tight', 'tracking-normal', 'tracking-wide', 'tracking-wider', 'tracking-widest'];
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Parse @layer base block for typography styles
|
|
346
|
+
*/
|
|
347
|
+
export function parseLayerBase(css: string): TypographyStyle[] {
|
|
348
|
+
const styles: TypographyStyle[] = [];
|
|
349
|
+
|
|
350
|
+
// Find @layer base block
|
|
351
|
+
const layerMatch = css.match(/@layer\s+base\s*\{([\s\S]*?)\n\}/);
|
|
352
|
+
if (!layerMatch) return styles;
|
|
353
|
+
|
|
354
|
+
const layerContent = layerMatch[1];
|
|
355
|
+
|
|
356
|
+
// Match each element rule: h1 { @apply ... }
|
|
357
|
+
const elementRegex = /\s*(h[1-6]|p|a|ul|ol|li|small|strong|em|code|pre|kbd|mark|blockquote|cite|abbr|dfn|q|sub|sup|del|ins|caption|label|figcaption)\s*\{([^}]+)\}/g;
|
|
358
|
+
let match;
|
|
359
|
+
|
|
360
|
+
while ((match = elementRegex.exec(layerContent)) !== null) {
|
|
361
|
+
const element = match[1];
|
|
362
|
+
const ruleContent = match[2];
|
|
363
|
+
|
|
364
|
+
// Extract @apply classes
|
|
365
|
+
const applyMatch = ruleContent.match(/@apply\s+([^;]+);/);
|
|
366
|
+
if (!applyMatch) continue;
|
|
367
|
+
|
|
368
|
+
const classes = applyMatch[1].trim().split(/\s+/);
|
|
369
|
+
|
|
370
|
+
// Parse classes into typography style properties
|
|
371
|
+
const style: TypographyStyle = {
|
|
372
|
+
id: element,
|
|
373
|
+
element,
|
|
374
|
+
fontFamilyId: '', // Will be resolved based on font-* class
|
|
375
|
+
fontSize: 'text-base',
|
|
376
|
+
fontWeight: 'font-normal',
|
|
377
|
+
baseColorId: 'black', // Default to black text color
|
|
378
|
+
displayName: getElementDisplayName(element),
|
|
379
|
+
utilities: [],
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
for (const cls of classes) {
|
|
383
|
+
// Font family (font-mondwest, font-joystix)
|
|
384
|
+
if (cls.startsWith('font-') && !WEIGHT_CLASSES.includes(cls)) {
|
|
385
|
+
const fontName = cls.replace('font-', '');
|
|
386
|
+
style.fontFamilyId = fontName;
|
|
387
|
+
}
|
|
388
|
+
// Font size
|
|
389
|
+
else if (SIZE_CLASSES.includes(cls)) {
|
|
390
|
+
style.fontSize = cls;
|
|
391
|
+
}
|
|
392
|
+
// Font weight
|
|
393
|
+
else if (WEIGHT_CLASSES.includes(cls)) {
|
|
394
|
+
style.fontWeight = cls;
|
|
395
|
+
}
|
|
396
|
+
// Line height
|
|
397
|
+
else if (LEADING_CLASSES.includes(cls)) {
|
|
398
|
+
style.lineHeight = cls;
|
|
399
|
+
}
|
|
400
|
+
// Letter spacing
|
|
401
|
+
else if (TRACKING_CLASSES.includes(cls)) {
|
|
402
|
+
style.letterSpacing = cls;
|
|
403
|
+
}
|
|
404
|
+
// Text color (text-black, text-cream, etc.) - extract base color name
|
|
405
|
+
else if (cls.startsWith('text-') && !SIZE_CLASSES.includes(cls)) {
|
|
406
|
+
const colorName = cls.replace('text-', '');
|
|
407
|
+
// Map common color names to base color IDs
|
|
408
|
+
style.baseColorId = colorName === 'cream' ? 'cream' :
|
|
409
|
+
colorName === 'white' ? 'white' :
|
|
410
|
+
colorName === 'sun-yellow' ? 'sun-yellow' :
|
|
411
|
+
colorName === 'sky-blue' ? 'sky-blue' :
|
|
412
|
+
colorName === 'sun-red' ? 'sun-red' :
|
|
413
|
+
colorName === 'green' ? 'green' : 'black'; // Default to black
|
|
414
|
+
}
|
|
415
|
+
// Other utilities
|
|
416
|
+
else {
|
|
417
|
+
style.utilities = style.utilities || [];
|
|
418
|
+
style.utilities.push(cls);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
styles.push(style);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return styles;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get display name for HTML element
|
|
430
|
+
*/
|
|
431
|
+
function getElementDisplayName(element: string): string {
|
|
432
|
+
const names: Record<string, string> = {
|
|
433
|
+
h1: 'Heading 1',
|
|
434
|
+
h2: 'Heading 2',
|
|
435
|
+
h3: 'Heading 3',
|
|
436
|
+
h4: 'Heading 4',
|
|
437
|
+
h5: 'Heading 5',
|
|
438
|
+
h6: 'Heading 6',
|
|
439
|
+
p: 'Paragraph',
|
|
440
|
+
a: 'Link',
|
|
441
|
+
ul: 'Unordered List',
|
|
442
|
+
ol: 'Ordered List',
|
|
443
|
+
li: 'List Item',
|
|
444
|
+
small: 'Small Text',
|
|
445
|
+
strong: 'Strong',
|
|
446
|
+
em: 'Emphasis',
|
|
447
|
+
code: 'Inline Code',
|
|
448
|
+
pre: 'Code Block',
|
|
449
|
+
kbd: 'Keyboard Input',
|
|
450
|
+
mark: 'Highlighted Text',
|
|
451
|
+
blockquote: 'Block Quote',
|
|
452
|
+
cite: 'Citation',
|
|
453
|
+
abbr: 'Abbreviation',
|
|
454
|
+
dfn: 'Definition Term',
|
|
455
|
+
q: 'Inline Quote',
|
|
456
|
+
sub: 'Subscript',
|
|
457
|
+
sup: 'Superscript',
|
|
458
|
+
del: 'Deleted Text',
|
|
459
|
+
ins: 'Inserted Text',
|
|
460
|
+
caption: 'Caption',
|
|
461
|
+
label: 'Form Label',
|
|
462
|
+
figcaption: 'Figure Caption',
|
|
463
|
+
};
|
|
464
|
+
return names[element] || element.toUpperCase();
|
|
465
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared search indexes for devtools navigation and search
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface TypographySearchableItem {
|
|
6
|
+
text: string;
|
|
7
|
+
aliases: string[];
|
|
8
|
+
sectionId: string;
|
|
9
|
+
element: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Typography search index with aliases for searching typography elements
|
|
14
|
+
*/
|
|
15
|
+
export const TYPOGRAPHY_SEARCH_INDEX: TypographySearchableItem[] = [
|
|
16
|
+
// Headings
|
|
17
|
+
{ text: 'H1', aliases: ['Heading 1', 'h1', 'heading 1'], sectionId: 'headings', element: 'h1' },
|
|
18
|
+
{ text: 'H2', aliases: ['Heading 2', 'h2', 'heading 2'], sectionId: 'headings', element: 'h2' },
|
|
19
|
+
{ text: 'H3', aliases: ['Heading 3', 'h3', 'heading 3'], sectionId: 'headings', element: 'h3' },
|
|
20
|
+
{ text: 'H4', aliases: ['Heading 4', 'h4', 'heading 4'], sectionId: 'headings', element: 'h4' },
|
|
21
|
+
{ text: 'H5', aliases: ['Heading 5', 'h5', 'heading 5'], sectionId: 'headings', element: 'h5' },
|
|
22
|
+
{ text: 'H6', aliases: ['Heading 6', 'h6', 'heading 6'], sectionId: 'headings', element: 'h6' },
|
|
23
|
+
// Text
|
|
24
|
+
{ text: 'Paragraph', aliases: ['P', 'p', 'paragraph', 'body'], sectionId: 'text', element: 'p' },
|
|
25
|
+
{ text: 'Link', aliases: ['A', 'a', 'anchor'], sectionId: 'text', element: 'a' },
|
|
26
|
+
// Lists
|
|
27
|
+
{ text: 'Unordered List', aliases: ['UL', 'ul', 'unordered list'], sectionId: 'lists', element: 'ul' },
|
|
28
|
+
{ text: 'Ordered List', aliases: ['OL', 'ol', 'ordered list'], sectionId: 'lists', element: 'ol' },
|
|
29
|
+
{ text: 'List Item', aliases: ['LI', 'li', 'list item'], sectionId: 'lists', element: 'li' },
|
|
30
|
+
// Code
|
|
31
|
+
{ text: 'Code', aliases: ['code', 'inline code'], sectionId: 'code', element: 'code' },
|
|
32
|
+
{ text: 'Pre', aliases: ['pre', 'preformatted', 'code block'], sectionId: 'code', element: 'pre' },
|
|
33
|
+
{ text: 'Keyboard', aliases: ['KBD', 'kbd', 'keyboard'], sectionId: 'code', element: 'kbd' },
|
|
34
|
+
// Semantic
|
|
35
|
+
{ text: 'Strong', aliases: ['strong', 'bold'], sectionId: 'semantic', element: 'strong' },
|
|
36
|
+
{ text: 'Emphasis', aliases: ['EM', 'em', 'emphasis', 'italic'], sectionId: 'semantic', element: 'em' },
|
|
37
|
+
{ text: 'Mark', aliases: ['mark', 'highlight'], sectionId: 'semantic', element: 'mark' },
|
|
38
|
+
// Quotations
|
|
39
|
+
{ text: 'Blockquote', aliases: ['blockquote', 'quote'], sectionId: 'quotations', element: 'blockquote' },
|
|
40
|
+
{ text: 'Cite', aliases: ['cite', 'citation'], sectionId: 'quotations', element: 'cite' },
|
|
41
|
+
// Captions
|
|
42
|
+
{ text: 'Caption', aliases: ['caption', 'table caption'], sectionId: 'captions', element: 'caption' },
|
|
43
|
+
{ text: 'Small', aliases: ['small', 'fine print'], sectionId: 'captions', element: 'small' },
|
|
44
|
+
{ text: 'Label', aliases: ['label', 'form label'], sectionId: 'captions', element: 'label' },
|
|
45
|
+
];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a CSS selector for an element
|
|
3
|
+
* Priority: data-testid > id > built path
|
|
4
|
+
*/
|
|
5
|
+
export function generateSelector(element: HTMLElement): string {
|
|
6
|
+
// Priority 1: data-testid
|
|
7
|
+
if (element.dataset.testid) {
|
|
8
|
+
return `[data-testid="${element.dataset.testid}"]`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Priority 2: unique ID
|
|
12
|
+
if (element.id) {
|
|
13
|
+
return `#${element.id}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Priority 3: Build path
|
|
17
|
+
const path: string[] = [];
|
|
18
|
+
let current: HTMLElement | null = element;
|
|
19
|
+
|
|
20
|
+
while (current && current !== document.body) {
|
|
21
|
+
let selector = current.tagName.toLowerCase();
|
|
22
|
+
|
|
23
|
+
// Add classes if present (limit to 2 for readability)
|
|
24
|
+
if (current.className && typeof current.className === 'string') {
|
|
25
|
+
const classes = current.className
|
|
26
|
+
.split(' ')
|
|
27
|
+
.filter((c) => c && !c.includes(':') && !c.includes('['))
|
|
28
|
+
.slice(0, 2);
|
|
29
|
+
if (classes.length) {
|
|
30
|
+
selector += '.' + classes.join('.');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Add nth-child if needed for uniqueness
|
|
35
|
+
const parent = current.parentElement;
|
|
36
|
+
if (parent) {
|
|
37
|
+
const siblings = Array.from(parent.children).filter(
|
|
38
|
+
(el) => el.tagName === current!.tagName
|
|
39
|
+
);
|
|
40
|
+
if (siblings.length > 1) {
|
|
41
|
+
const index = siblings.indexOf(current) + 1;
|
|
42
|
+
selector += `:nth-child(${index})`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
path.unshift(selector);
|
|
47
|
+
current = current.parentElement;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return path.join(' > ');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get a text preview of an element's content
|
|
55
|
+
*/
|
|
56
|
+
export function getElementPreview(element: HTMLElement): string {
|
|
57
|
+
const text = element.textContent?.trim().slice(0, 50);
|
|
58
|
+
return text ? `"${text}${text.length >= 50 ? '...' : ''}"` : `<${element.tagName.toLowerCase()}>`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the DOM path of an element as an array
|
|
63
|
+
*/
|
|
64
|
+
export function getElementPath(element: HTMLElement): string[] {
|
|
65
|
+
const path: string[] = [];
|
|
66
|
+
let current: HTMLElement | null = element;
|
|
67
|
+
|
|
68
|
+
while (current && current !== document.body) {
|
|
69
|
+
path.unshift(current.tagName.toLowerCase());
|
|
70
|
+
current = current.parentElement;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return path;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Find element by selector safely
|
|
78
|
+
*/
|
|
79
|
+
export function findElementBySelector(selector: string): HTMLElement | null {
|
|
80
|
+
try {
|
|
81
|
+
return document.querySelector(selector);
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { devtools, persist } from 'zustand/middleware';
|
|
3
|
+
import { VariablesSlice, createVariablesSlice } from './slices/variablesSlice';
|
|
4
|
+
import { TypographySlice, createTypographySlice } from './slices/typographySlice';
|
|
5
|
+
import { ComponentsSlice, createComponentsSlice } from './slices/componentsSlice';
|
|
6
|
+
import { AssetsSlice, createAssetsSlice } from './slices/assetsSlice';
|
|
7
|
+
import { MockStatesSlice, createMockStatesSlice } from './slices/mockStatesSlice';
|
|
8
|
+
import { PanelSlice, createPanelSlice } from './slices/panelSlice';
|
|
9
|
+
import type { Tab } from '../types';
|
|
10
|
+
|
|
11
|
+
interface PanelState {
|
|
12
|
+
isOpen: boolean;
|
|
13
|
+
activeTab: Tab;
|
|
14
|
+
panelPosition: { x: number; y: number };
|
|
15
|
+
panelSize: { width: number; height: number };
|
|
16
|
+
togglePanel: () => void;
|
|
17
|
+
setActiveTab: (tab: Tab) => void;
|
|
18
|
+
setPanelPosition: (position: { x: number; y: number }) => void;
|
|
19
|
+
setPanelSize: (size: { width: number; height: number }) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type DevToolsState = PanelState &
|
|
23
|
+
PanelSlice &
|
|
24
|
+
VariablesSlice &
|
|
25
|
+
TypographySlice &
|
|
26
|
+
ComponentsSlice &
|
|
27
|
+
AssetsSlice &
|
|
28
|
+
MockStatesSlice;
|
|
29
|
+
|
|
30
|
+
export const useDevToolsStore = create<DevToolsState>()(
|
|
31
|
+
devtools(
|
|
32
|
+
persist(
|
|
33
|
+
(set, get, api) => ({
|
|
34
|
+
// Panel state
|
|
35
|
+
isOpen: false,
|
|
36
|
+
activeTab: 'variables' as Tab,
|
|
37
|
+
panelPosition: { x: 20, y: 20 },
|
|
38
|
+
panelSize: { width: 420, height: 600 },
|
|
39
|
+
togglePanel: () => set((state) => ({ isOpen: !state.isOpen })),
|
|
40
|
+
setActiveTab: (tab) => set({ activeTab: tab }),
|
|
41
|
+
setPanelPosition: (position) => set({ panelPosition: position }),
|
|
42
|
+
setPanelSize: (size) => set({ panelSize: size }),
|
|
43
|
+
|
|
44
|
+
// Slices
|
|
45
|
+
...createPanelSlice(set, get, api),
|
|
46
|
+
...createVariablesSlice(set, get, api),
|
|
47
|
+
...createTypographySlice(set, get, api),
|
|
48
|
+
...createComponentsSlice(set, get, api),
|
|
49
|
+
...createAssetsSlice(set, get, api),
|
|
50
|
+
...createMockStatesSlice(set, get, api),
|
|
51
|
+
}),
|
|
52
|
+
{
|
|
53
|
+
name: 'devtools-storage',
|
|
54
|
+
partialize: (state) => ({
|
|
55
|
+
// Only persist these fields
|
|
56
|
+
panelPosition: state.panelPosition,
|
|
57
|
+
panelSize: state.panelSize,
|
|
58
|
+
activeTab: state.activeTab,
|
|
59
|
+
mockStates: state.mockStates,
|
|
60
|
+
}),
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
{ name: 'RadTools DevTools' }
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
|