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,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
+