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,538 @@
1
+ import { StateCreator } from 'zustand';
2
+ import type { FontDefinition, FontFile, TypographyStyle } from '../../types';
3
+
4
+ export interface TypographySlice {
5
+ // State
6
+ fonts: FontDefinition[];
7
+ typographyStyles: TypographyStyle[];
8
+
9
+ // Actions - Fonts
10
+ addFont: (font: Omit<FontDefinition, 'id'>) => void;
11
+ updateFont: (id: string, updates: Partial<FontDefinition>) => void;
12
+ deleteFont: (id: string) => void;
13
+ addFontFile: (fontId: string, file: Omit<FontFile, 'id'>) => void;
14
+ removeFontFile: (fontId: string, fileId: string) => void;
15
+
16
+ // Actions - Typography Styles
17
+ addTypographyStyle: (style: Omit<TypographyStyle, 'id'>) => void;
18
+ updateTypographyStyle: (id: string, updates: Partial<TypographyStyle>) => void;
19
+ deleteTypographyStyle: (id: string) => void;
20
+
21
+ // Helpers
22
+ getFontById: (id: string) => FontDefinition | undefined;
23
+ getFontByFamily: (family: string) => FontDefinition | undefined;
24
+
25
+ // Sync
26
+ syncTypographyToCSS: () => Promise<void>;
27
+ loadTypographyFromCSS: () => Promise<void>;
28
+ loadFontsFromFilesystem: () => Promise<void>;
29
+ }
30
+
31
+ // Default fonts matching radOS
32
+ const defaultFonts: FontDefinition[] = [
33
+ {
34
+ id: 'mondwest',
35
+ name: 'Mondwest',
36
+ family: 'Mondwest',
37
+ files: [
38
+ { id: 'mondwest-regular', weight: 400, style: 'normal', format: 'woff2', path: '/fonts/Mondwest-Regular.woff2' },
39
+ { id: 'mondwest-bold', weight: 700, style: 'normal', format: 'woff2', path: '/fonts/Mondwest-Bold.woff2' },
40
+ ],
41
+ weights: [400, 700],
42
+ styles: ['normal'],
43
+ },
44
+ {
45
+ id: 'joystix',
46
+ name: 'Joystix Monospace',
47
+ family: 'Joystix Monospace',
48
+ files: [
49
+ { id: 'joystix-regular', weight: 400, style: 'normal', format: 'ttf', path: '/fonts/joystix_monospace.ttf' },
50
+ ],
51
+ weights: [400],
52
+ styles: ['normal'],
53
+ },
54
+ ];
55
+
56
+ // Default typography styles matching Webflow style guide with @layer base
57
+ const defaultTypographyStyles: TypographyStyle[] = [
58
+ {
59
+ id: 'h1',
60
+ element: 'h1',
61
+ fontFamilyId: 'mondwest',
62
+ fontSize: 'text-4xl',
63
+ lineHeight: 'leading-tight',
64
+ fontWeight: 'font-bold',
65
+ baseColorId: 'black',
66
+ displayName: 'Heading 1',
67
+ },
68
+ {
69
+ id: 'h2',
70
+ element: 'h2',
71
+ fontFamilyId: 'mondwest',
72
+ fontSize: 'text-3xl',
73
+ lineHeight: 'leading-tight',
74
+ fontWeight: 'font-normal',
75
+ baseColorId: 'black',
76
+ displayName: 'Heading 2',
77
+ },
78
+ {
79
+ id: 'h3',
80
+ element: 'h3',
81
+ fontFamilyId: 'mondwest',
82
+ fontSize: 'text-2xl',
83
+ lineHeight: 'leading-snug',
84
+ fontWeight: 'font-semibold',
85
+ baseColorId: 'black',
86
+ displayName: 'Heading 3',
87
+ },
88
+ {
89
+ id: 'h4',
90
+ element: 'h4',
91
+ fontFamilyId: 'mondwest',
92
+ fontSize: 'text-xl',
93
+ lineHeight: 'leading-snug',
94
+ fontWeight: 'font-medium',
95
+ baseColorId: 'black',
96
+ displayName: 'Heading 4',
97
+ },
98
+ {
99
+ id: 'h5',
100
+ element: 'h5',
101
+ fontFamilyId: 'mondwest',
102
+ fontSize: 'text-lg',
103
+ lineHeight: 'leading-normal',
104
+ fontWeight: 'font-medium',
105
+ baseColorId: 'black',
106
+ displayName: 'Heading 5',
107
+ },
108
+ {
109
+ id: 'h6',
110
+ element: 'h6',
111
+ fontFamilyId: 'mondwest',
112
+ fontSize: 'text-base',
113
+ lineHeight: 'leading-normal',
114
+ fontWeight: 'font-medium',
115
+ baseColorId: 'black',
116
+ displayName: 'Heading 6',
117
+ },
118
+ {
119
+ id: 'p',
120
+ element: 'p',
121
+ fontFamilyId: 'mondwest',
122
+ fontSize: 'text-base',
123
+ lineHeight: 'leading-relaxed',
124
+ fontWeight: 'font-normal',
125
+ baseColorId: 'black',
126
+ displayName: 'Paragraph',
127
+ },
128
+ {
129
+ id: 'a',
130
+ element: 'a',
131
+ fontFamilyId: 'mondwest',
132
+ fontSize: 'text-base',
133
+ lineHeight: 'leading-normal',
134
+ fontWeight: 'font-normal',
135
+ baseColorId: 'sky-blue',
136
+ displayName: 'Link',
137
+ utilities: ['underline', 'hover:opacity-80'],
138
+ },
139
+ {
140
+ id: 'ul',
141
+ element: 'ul',
142
+ fontFamilyId: 'mondwest',
143
+ fontSize: 'text-base',
144
+ lineHeight: 'leading-relaxed',
145
+ fontWeight: 'font-normal',
146
+ baseColorId: 'black',
147
+ displayName: 'Unordered List',
148
+ utilities: ['pl-6'],
149
+ },
150
+ {
151
+ id: 'ol',
152
+ element: 'ol',
153
+ fontFamilyId: 'mondwest',
154
+ fontSize: 'text-base',
155
+ lineHeight: 'leading-relaxed',
156
+ fontWeight: 'font-normal',
157
+ baseColorId: 'black',
158
+ displayName: 'Ordered List',
159
+ utilities: ['pl-6'],
160
+ },
161
+ {
162
+ id: 'li',
163
+ element: 'li',
164
+ fontFamilyId: 'mondwest',
165
+ fontSize: 'text-base',
166
+ lineHeight: 'leading-relaxed',
167
+ fontWeight: 'font-normal',
168
+ baseColorId: 'black',
169
+ displayName: 'List Item',
170
+ utilities: ['mb-2'],
171
+ },
172
+ {
173
+ id: 'small',
174
+ element: 'small',
175
+ fontFamilyId: 'mondwest',
176
+ fontSize: 'text-sm',
177
+ lineHeight: 'leading-normal',
178
+ fontWeight: 'font-normal',
179
+ baseColorId: 'black',
180
+ displayName: 'Small Text',
181
+ },
182
+ {
183
+ id: 'strong',
184
+ element: 'strong',
185
+ fontFamilyId: 'mondwest',
186
+ fontSize: 'text-base',
187
+ lineHeight: 'leading-normal',
188
+ fontWeight: 'font-bold',
189
+ baseColorId: 'black',
190
+ displayName: 'Strong',
191
+ },
192
+ {
193
+ id: 'em',
194
+ element: 'em',
195
+ fontFamilyId: 'mondwest',
196
+ fontSize: 'text-base',
197
+ lineHeight: 'leading-normal',
198
+ fontWeight: 'font-normal',
199
+ baseColorId: 'black',
200
+ displayName: 'Emphasis',
201
+ utilities: ['italic'],
202
+ },
203
+ {
204
+ id: 'code',
205
+ element: 'code',
206
+ fontFamilyId: 'joystix',
207
+ fontSize: 'text-sm',
208
+ lineHeight: 'leading-normal',
209
+ fontWeight: 'font-normal',
210
+ baseColorId: 'black',
211
+ displayName: 'Inline Code',
212
+ utilities: ['bg-black/10', 'px-1', 'py-0.5', 'rounded-sm'],
213
+ },
214
+ {
215
+ id: 'pre',
216
+ element: 'pre',
217
+ fontFamilyId: 'joystix',
218
+ fontSize: 'text-sm',
219
+ lineHeight: 'leading-relaxed',
220
+ fontWeight: 'font-normal',
221
+ baseColorId: 'black',
222
+ displayName: 'Code Block',
223
+ utilities: ['bg-black/10', 'p-4', 'rounded-sm', 'overflow-x-auto'],
224
+ },
225
+ {
226
+ id: 'kbd',
227
+ element: 'kbd',
228
+ fontFamilyId: 'joystix',
229
+ fontSize: 'text-xs',
230
+ lineHeight: 'leading-normal',
231
+ fontWeight: 'font-normal',
232
+ baseColorId: 'cream',
233
+ displayName: 'Keyboard Input',
234
+ utilities: ['bg-black', 'px-1', 'py-0.5', 'rounded-sm'],
235
+ },
236
+ {
237
+ id: 'mark',
238
+ element: 'mark',
239
+ fontFamilyId: 'mondwest',
240
+ fontSize: 'text-base',
241
+ lineHeight: 'leading-normal',
242
+ fontWeight: 'font-normal',
243
+ baseColorId: 'black',
244
+ displayName: 'Highlighted Text',
245
+ utilities: ['bg-sun-yellow'],
246
+ },
247
+ {
248
+ id: 'blockquote',
249
+ element: 'blockquote',
250
+ fontFamilyId: 'mondwest',
251
+ fontSize: 'text-base',
252
+ lineHeight: 'leading-relaxed',
253
+ fontWeight: 'font-normal',
254
+ baseColorId: 'black',
255
+ displayName: 'Block Quote',
256
+ utilities: ['border-l-4', 'border-black', 'pl-4', 'italic'],
257
+ },
258
+ {
259
+ id: 'cite',
260
+ element: 'cite',
261
+ fontFamilyId: 'mondwest',
262
+ fontSize: 'text-sm',
263
+ lineHeight: 'leading-normal',
264
+ fontWeight: 'font-normal',
265
+ baseColorId: 'black',
266
+ displayName: 'Citation',
267
+ utilities: ['italic'],
268
+ },
269
+ {
270
+ id: 'abbr',
271
+ element: 'abbr',
272
+ fontFamilyId: 'mondwest',
273
+ fontSize: 'text-base',
274
+ lineHeight: 'leading-normal',
275
+ fontWeight: 'font-normal',
276
+ baseColorId: 'black',
277
+ displayName: 'Abbreviation',
278
+ utilities: ['underline', 'decoration-dotted'],
279
+ },
280
+ {
281
+ id: 'dfn',
282
+ element: 'dfn',
283
+ fontFamilyId: 'mondwest',
284
+ fontSize: 'text-base',
285
+ lineHeight: 'leading-normal',
286
+ fontWeight: 'font-normal',
287
+ baseColorId: 'black',
288
+ displayName: 'Definition Term',
289
+ utilities: ['italic'],
290
+ },
291
+ {
292
+ id: 'q',
293
+ element: 'q',
294
+ fontFamilyId: 'mondwest',
295
+ fontSize: 'text-base',
296
+ lineHeight: 'leading-normal',
297
+ fontWeight: 'font-normal',
298
+ baseColorId: 'black',
299
+ displayName: 'Inline Quote',
300
+ utilities: ['italic'],
301
+ },
302
+ {
303
+ id: 'sub',
304
+ element: 'sub',
305
+ fontFamilyId: 'mondwest',
306
+ fontSize: 'text-xs',
307
+ lineHeight: 'leading-none',
308
+ fontWeight: 'font-normal',
309
+ baseColorId: 'black',
310
+ displayName: 'Subscript',
311
+ },
312
+ {
313
+ id: 'sup',
314
+ element: 'sup',
315
+ fontFamilyId: 'mondwest',
316
+ fontSize: 'text-xs',
317
+ lineHeight: 'leading-none',
318
+ fontWeight: 'font-normal',
319
+ baseColorId: 'black',
320
+ displayName: 'Superscript',
321
+ },
322
+ {
323
+ id: 'del',
324
+ element: 'del',
325
+ fontFamilyId: 'mondwest',
326
+ fontSize: 'text-base',
327
+ lineHeight: 'leading-normal',
328
+ fontWeight: 'font-normal',
329
+ baseColorId: 'black',
330
+ displayName: 'Deleted Text',
331
+ utilities: ['line-through'],
332
+ },
333
+ {
334
+ id: 'ins',
335
+ element: 'ins',
336
+ fontFamilyId: 'mondwest',
337
+ fontSize: 'text-base',
338
+ lineHeight: 'leading-normal',
339
+ fontWeight: 'font-normal',
340
+ baseColorId: 'black',
341
+ displayName: 'Inserted Text',
342
+ utilities: ['underline'],
343
+ },
344
+ {
345
+ id: 'caption',
346
+ element: 'caption',
347
+ fontFamilyId: 'mondwest',
348
+ fontSize: 'text-xs',
349
+ lineHeight: 'leading-normal',
350
+ fontWeight: 'font-normal',
351
+ baseColorId: 'black',
352
+ displayName: 'Caption',
353
+ },
354
+ {
355
+ id: 'label',
356
+ element: 'label',
357
+ fontFamilyId: 'mondwest',
358
+ fontSize: 'text-xs',
359
+ lineHeight: 'leading-normal',
360
+ fontWeight: 'font-medium',
361
+ baseColorId: 'black',
362
+ displayName: 'Form Label',
363
+ },
364
+ {
365
+ id: 'figcaption',
366
+ element: 'figcaption',
367
+ fontFamilyId: 'mondwest',
368
+ fontSize: 'text-xs',
369
+ lineHeight: 'leading-normal',
370
+ fontWeight: 'font-normal',
371
+ baseColorId: 'black',
372
+ displayName: 'Figure Caption',
373
+ },
374
+ ];
375
+
376
+ export const createTypographySlice: StateCreator<TypographySlice, [], [], TypographySlice> = (set, get) => ({
377
+ fonts: defaultFonts,
378
+ typographyStyles: defaultTypographyStyles,
379
+
380
+ // Font Actions
381
+ addFont: (font) => set((state) => ({
382
+ fonts: [...state.fonts, { ...font, id: crypto.randomUUID() }]
383
+ })),
384
+
385
+ updateFont: (id, updates) => set((state) => ({
386
+ fonts: state.fonts.map((f) =>
387
+ f.id === id ? { ...f, ...updates } : f
388
+ )
389
+ })),
390
+
391
+ deleteFont: (id) => set((state) => ({
392
+ fonts: state.fonts.filter((f) => f.id !== id)
393
+ })),
394
+
395
+ addFontFile: (fontId, file) => set((state) => ({
396
+ fonts: state.fonts.map((f) => {
397
+ if (f.id !== fontId) return f;
398
+ const newFile = { ...file, id: crypto.randomUUID() };
399
+ return {
400
+ ...f,
401
+ files: [...f.files, newFile],
402
+ weights: Array.from(new Set([...f.weights, file.weight])).sort((a, b) => a - b),
403
+ styles: Array.from(new Set([...f.styles, file.style])),
404
+ };
405
+ })
406
+ })),
407
+
408
+ removeFontFile: (fontId, fileId) => set((state) => ({
409
+ fonts: state.fonts.map((f) => {
410
+ if (f.id !== fontId) return f;
411
+ const newFiles = f.files.filter((file) => file.id !== fileId);
412
+ return {
413
+ ...f,
414
+ files: newFiles,
415
+ weights: Array.from(new Set(newFiles.map(file => file.weight))).sort((a, b) => a - b),
416
+ styles: Array.from(new Set(newFiles.map(file => file.style))),
417
+ };
418
+ })
419
+ })),
420
+
421
+ // Typography Style Actions
422
+ addTypographyStyle: (style) => set((state) => ({
423
+ typographyStyles: [...state.typographyStyles, { ...style, id: crypto.randomUUID() }]
424
+ })),
425
+
426
+ updateTypographyStyle: (id, updates) => set((state) => ({
427
+ typographyStyles: state.typographyStyles.map((s) =>
428
+ s.id === id ? { ...s, ...updates } : s
429
+ )
430
+ })),
431
+
432
+ deleteTypographyStyle: (id) => set((state) => ({
433
+ typographyStyles: state.typographyStyles.filter((s) => s.id !== id)
434
+ })),
435
+
436
+ // Helpers
437
+ getFontById: (id) => get().fonts.find((f) => f.id === id),
438
+ getFontByFamily: (family) => get().fonts.find((f) => f.family === family),
439
+
440
+ // Sync typography to CSS
441
+ syncTypographyToCSS: async () => {
442
+ const state = get();
443
+ try {
444
+ const response = await fetch('/api/devtools/write-css', {
445
+ method: 'POST',
446
+ headers: { 'Content-Type': 'application/json' },
447
+ body: JSON.stringify({
448
+ fonts: state.fonts,
449
+ typographyStyles: state.typographyStyles,
450
+ }),
451
+ });
452
+
453
+ if (!response.ok) {
454
+ const error = await response.json();
455
+ throw new Error(error.error || 'Failed to sync typography CSS');
456
+ }
457
+ } catch (error) {
458
+ throw error;
459
+ }
460
+ },
461
+
462
+ // Load typography from CSS (only typography styles, not fonts)
463
+ loadTypographyFromCSS: async () => {
464
+ try {
465
+ const res = await fetch('/api/devtools/read-css');
466
+ if (!res.ok) {
467
+ throw new Error('Failed to fetch CSS');
468
+ }
469
+ const css = await res.text();
470
+
471
+ // Import parser dynamically to avoid SSR issues
472
+ const { parseLayerBase } = await import('../../lib/cssParser');
473
+
474
+ // Only load typography styles, not fonts (fonts should come from filesystem)
475
+ const typographyStyles = parseLayerBase(css);
476
+
477
+ if (typographyStyles.length > 0) {
478
+ set((state) => ({ ...state, typographyStyles }));
479
+ }
480
+ } catch (error) {
481
+ // Failed to load typography from CSS
482
+ }
483
+ },
484
+
485
+ // Load fonts from filesystem
486
+ loadFontsFromFilesystem: async () => {
487
+ try {
488
+ const res = await fetch('/api/devtools/fonts');
489
+ if (!res.ok) {
490
+ throw new Error('Failed to fetch fonts from filesystem');
491
+ }
492
+ const data = await res.json();
493
+
494
+ // Import parser dynamically to avoid SSR issues
495
+ const { detectFontPropertiesFromFilename } = await import('../../lib/cssParser');
496
+
497
+ // Convert filesystem fonts to FontDefinition format
498
+ const fontMap = new Map<string, FontDefinition>();
499
+
500
+ for (const { family, files } of data.fonts || []) {
501
+ const fontId = family.toLowerCase().replace(/\s+/g, '-');
502
+ const fontFiles: FontFile[] = [];
503
+ const weights = new Set<number>();
504
+ const styles = new Set<string>();
505
+
506
+ for (const file of files) {
507
+ const { weight, style } = detectFontPropertiesFromFilename(file.filename);
508
+ weights.add(weight);
509
+ styles.add(style);
510
+
511
+ fontFiles.push({
512
+ id: `${fontId}-${file.filename.replace(/\.[^.]+$/, '')}`,
513
+ weight,
514
+ style,
515
+ format: file.format as FontFile['format'],
516
+ path: file.path,
517
+ });
518
+ }
519
+
520
+ fontMap.set(fontId, {
521
+ id: fontId,
522
+ name: family,
523
+ family: family,
524
+ files: fontFiles,
525
+ weights: Array.from(weights).sort((a, b) => a - b),
526
+ styles: Array.from(styles),
527
+ });
528
+ }
529
+
530
+ if (fontMap.size > 0) {
531
+ set({ fonts: Array.from(fontMap.values()) });
532
+ }
533
+ } catch (error) {
534
+ // Failed to load fonts from filesystem
535
+ }
536
+ },
537
+ });
538
+
@@ -0,0 +1,167 @@
1
+ import { StateCreator } from 'zustand';
2
+ import type { BaseColor, ColorMode } from '../../types';
3
+
4
+ export interface VariablesSlice {
5
+ // State
6
+ baseColors: BaseColor[];
7
+ colorModes: ColorMode[];
8
+ activeColorMode: string | null;
9
+ borderRadius: Record<string, string>;
10
+
11
+ // Actions - Base Colors
12
+ addBaseColor: (color: Omit<BaseColor, 'id'>) => void;
13
+ updateBaseColor: (id: string, updates: Partial<BaseColor>) => void;
14
+ deleteBaseColor: (id: string) => void;
15
+
16
+ // Actions - Color Modes
17
+ addColorMode: (mode: Omit<ColorMode, 'id'>) => void;
18
+ updateColorMode: (id: string, updates: Partial<ColorMode>) => void;
19
+ deleteColorMode: (id: string) => void;
20
+ setActiveColorMode: (id: string | null) => void;
21
+
22
+ // Actions - Border Radius
23
+ updateBorderRadius: (key: string, value: string) => void;
24
+ addBorderRadius: (key: string, value: string) => void;
25
+ deleteBorderRadius: (key: string) => void;
26
+
27
+ // Helper - Get base color by ID
28
+ getBaseColorById: (id: string) => BaseColor | undefined;
29
+
30
+ // Sync
31
+ syncToCSS: () => Promise<void>;
32
+ loadFromCSS: () => Promise<void>;
33
+ }
34
+
35
+ // Default base colors matching radOS
36
+ const defaultBaseColors: BaseColor[] = [
37
+ // Brand colors
38
+ { id: 'cream', name: 'cream', displayName: 'Cream', value: '#FEF8E2', category: 'brand' },
39
+ { id: 'black', name: 'black', displayName: 'Black', value: '#0F0E0C', category: 'brand' },
40
+ { id: 'sun-yellow', name: 'sun-yellow', displayName: 'Sun Yellow', value: '#FCE184', category: 'brand' },
41
+ { id: 'sky-blue', name: 'sky-blue', displayName: 'Sky Blue', value: '#95BAD2', category: 'brand' },
42
+ { id: 'warm-cloud', name: 'warm-cloud', displayName: 'Warm Cloud', value: '#FEF8E2', category: 'brand' },
43
+ { id: 'sunset-fuzz', name: 'sunset-fuzz', displayName: 'Sunset Fuzz', value: '#FCC383', category: 'brand' },
44
+ { id: 'sun-red', name: 'sun-red', displayName: 'Sun Red', value: '#FF6B63', category: 'brand' },
45
+ { id: 'green', name: 'green', displayName: 'Green', value: '#CEF5CA', category: 'brand' },
46
+ { id: 'white', name: 'white', displayName: 'White', value: '#FFFFFF', category: 'brand' },
47
+ // Neutral colors
48
+ { id: 'lightest', name: 'lightest', displayName: 'Lightest', value: '#FEF8E2', category: 'neutral' },
49
+ { id: 'lighter', name: 'lighter', displayName: 'Lighter', value: '#CCCCCC', category: 'neutral' },
50
+ { id: 'light', name: 'light', displayName: 'Light', value: '#AAAAAA', category: 'neutral' },
51
+ { id: 'dark', name: 'dark', displayName: 'Dark', value: '#444444', category: 'neutral' },
52
+ { id: 'darker', name: 'darker', displayName: 'Darker', value: '#222222', category: 'neutral' },
53
+ { id: 'darkest', name: 'darkest', displayName: 'Darkest', value: '#000000', category: 'neutral' },
54
+ ];
55
+
56
+ export const createVariablesSlice: StateCreator<VariablesSlice, [], [], VariablesSlice> = (set, get) => ({
57
+ baseColors: defaultBaseColors,
58
+ colorModes: [],
59
+ activeColorMode: null,
60
+ borderRadius: {
61
+ none: '0',
62
+ xs: '0.125rem',
63
+ sm: '0.25rem',
64
+ md: '0.5rem',
65
+ lg: '1rem',
66
+ full: '9999px',
67
+ },
68
+
69
+ // Base Color Actions
70
+ addBaseColor: (color) => set((state) => ({
71
+ baseColors: [...state.baseColors, { ...color, id: crypto.randomUUID() }]
72
+ })),
73
+
74
+ updateBaseColor: (id, updates) => set((state) => ({
75
+ baseColors: state.baseColors.map((c) =>
76
+ c.id === id ? { ...c, ...updates } : c
77
+ )
78
+ })),
79
+
80
+ deleteBaseColor: (id) => set((state) => ({
81
+ baseColors: state.baseColors.filter((c) => c.id !== id)
82
+ })),
83
+
84
+ // Color Mode Actions
85
+ addColorMode: (mode) => set((state) => ({
86
+ colorModes: [...state.colorModes, { ...mode, id: crypto.randomUUID() }]
87
+ })),
88
+
89
+ updateColorMode: (id, updates) => set((state) => ({
90
+ colorModes: state.colorModes.map((m) =>
91
+ m.id === id ? { ...m, ...updates } : m
92
+ )
93
+ })),
94
+
95
+ deleteColorMode: (id) => set((state) => ({
96
+ colorModes: state.colorModes.filter((m) => m.id !== id)
97
+ })),
98
+
99
+ setActiveColorMode: (id) => set({ activeColorMode: id }),
100
+
101
+ // Border Radius Actions
102
+ updateBorderRadius: (key, value) => set((state) => ({
103
+ borderRadius: { ...state.borderRadius, [key]: value }
104
+ })),
105
+
106
+ addBorderRadius: (key, value) => set((state) => ({
107
+ borderRadius: { ...state.borderRadius, [key]: value }
108
+ })),
109
+
110
+ deleteBorderRadius: (key) => set((state) => {
111
+ const { [key]: _, ...rest } = state.borderRadius;
112
+ return { borderRadius: rest };
113
+ }),
114
+
115
+ // Helper
116
+ getBaseColorById: (id) => {
117
+ return get().baseColors.find(c => c.id === id);
118
+ },
119
+
120
+ // Sync to CSS - sends new data model format
121
+ syncToCSS: async () => {
122
+ const state = get();
123
+ try {
124
+ const response = await fetch('/api/devtools/write-css', {
125
+ method: 'POST',
126
+ headers: { 'Content-Type': 'application/json' },
127
+ body: JSON.stringify({
128
+ baseColors: state.baseColors,
129
+ borderRadius: state.borderRadius,
130
+ colorModes: state.colorModes,
131
+ }),
132
+ });
133
+
134
+ if (!response.ok) {
135
+ const error = await response.json();
136
+ throw new Error(error.error || 'Failed to sync CSS');
137
+ }
138
+ } catch (error) {
139
+ throw error;
140
+ }
141
+ },
142
+
143
+ // Load from CSS - parses existing CSS into new data model
144
+ loadFromCSS: async () => {
145
+ try {
146
+ const res = await fetch('/api/devtools/read-css');
147
+ if (!res.ok) {
148
+ throw new Error('Failed to fetch CSS');
149
+ }
150
+ const css = await res.text();
151
+
152
+ // Import parser dynamically to avoid SSR issues
153
+ const { parseGlobalsCSS, parsedCSSToStoreState } = await import('../../lib/cssParser');
154
+
155
+ const parsed = parseGlobalsCSS(css);
156
+ const state = parsedCSSToStoreState(parsed);
157
+
158
+ set({
159
+ baseColors: state.baseColors,
160
+ colorModes: state.colorModes,
161
+ borderRadius: state.borderRadius,
162
+ });
163
+ } catch (error) {
164
+ // Failed to load CSS
165
+ }
166
+ },
167
+ });