radtools 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +108 -0
  2. package/bin/radtools.js +5 -0
  3. package/dist/cli/index.js +427 -0
  4. package/package.json +55 -0
  5. package/templates/api-routes/assets/optimize/route.ts +94 -0
  6. package/templates/api-routes/assets/route.ts +159 -0
  7. package/templates/api-routes/components/create-folder/route.ts +55 -0
  8. package/templates/api-routes/components/route.ts +156 -0
  9. package/templates/api-routes/fonts/route.ts +96 -0
  10. package/templates/api-routes/fonts/upload/route.ts +79 -0
  11. package/templates/api-routes/read-css/route.ts +29 -0
  12. package/templates/api-routes/write-css/route.ts +423 -0
  13. package/templates/components/Rad_os/AppWindow.tsx +423 -0
  14. package/templates/components/Rad_os/MobileAppModal.tsx +76 -0
  15. package/templates/components/Rad_os/WindowTitleBar.tsx +290 -0
  16. package/templates/components/icons/Icon.tsx +224 -0
  17. package/templates/components/icons/README.md +85 -0
  18. package/templates/components/icons/index.ts +20 -0
  19. package/templates/components/icons.tsx +164 -0
  20. package/templates/components/ui/Accordion.tsx +268 -0
  21. package/templates/components/ui/Alert.tsx +111 -0
  22. package/templates/components/ui/Badge.tsx +87 -0
  23. package/templates/components/ui/Breadcrumbs.tsx +88 -0
  24. package/templates/components/ui/Button.tsx +249 -0
  25. package/templates/components/ui/Card.tsx +137 -0
  26. package/templates/components/ui/Checkbox.tsx +137 -0
  27. package/templates/components/ui/ContextMenu.tsx +220 -0
  28. package/templates/components/ui/Dialog.tsx +264 -0
  29. package/templates/components/ui/Divider.tsx +70 -0
  30. package/templates/components/ui/DropdownMenu.tsx +301 -0
  31. package/templates/components/ui/HelpPanel.tsx +119 -0
  32. package/templates/components/ui/Input.tsx +176 -0
  33. package/templates/components/ui/Popover.tsx +211 -0
  34. package/templates/components/ui/Progress.tsx +158 -0
  35. package/templates/components/ui/Select.tsx +134 -0
  36. package/templates/components/ui/Sheet.tsx +316 -0
  37. package/templates/components/ui/Slider.tsx +223 -0
  38. package/templates/components/ui/Switch.tsx +155 -0
  39. package/templates/components/ui/Tabs.tsx +253 -0
  40. package/templates/components/ui/Toast.tsx +192 -0
  41. package/templates/components/ui/Tooltip.tsx +129 -0
  42. package/templates/components/ui/hooks/useModalBehavior.ts +66 -0
  43. package/templates/components/ui/index.ts +84 -0
  44. package/templates/devtools/DevToolsPanel.tsx +261 -0
  45. package/templates/devtools/DevToolsProvider.tsx +43 -0
  46. package/templates/devtools/components/BreakpointIndicator.tsx +49 -0
  47. package/templates/devtools/components/ColorPicker.tsx +33 -0
  48. package/templates/devtools/components/ComponentsSecondaryNav.tsx +44 -0
  49. package/templates/devtools/components/ContextualFooter.tsx +56 -0
  50. package/templates/devtools/components/DraggablePanel.tsx +43 -0
  51. package/templates/devtools/components/PrimaryNavigationFooter.tsx +254 -0
  52. package/templates/devtools/components/SearchableColorDropdown.tsx +253 -0
  53. package/templates/devtools/components/SecondaryNavigation.tsx +36 -0
  54. package/templates/devtools/components/TokenDropdown.tsx +47 -0
  55. package/templates/devtools/components/TypographyFooter.tsx +145 -0
  56. package/templates/devtools/hooks/useMockState.ts +16 -0
  57. package/templates/devtools/index.ts +17 -0
  58. package/templates/devtools/lib/componentScanner.ts +78 -0
  59. package/templates/devtools/lib/cssParser.ts +465 -0
  60. package/templates/devtools/lib/searchIndexes.ts +45 -0
  61. package/templates/devtools/lib/selectorGenerator.ts +86 -0
  62. package/templates/devtools/store/index.ts +66 -0
  63. package/templates/devtools/store/slices/assetsSlice.ts +106 -0
  64. package/templates/devtools/store/slices/componentsSlice.ts +59 -0
  65. package/templates/devtools/store/slices/mockStatesSlice.ts +77 -0
  66. package/templates/devtools/store/slices/panelSlice.ts +17 -0
  67. package/templates/devtools/store/slices/typographySlice.ts +538 -0
  68. package/templates/devtools/store/slices/variablesSlice.ts +167 -0
  69. package/templates/devtools/tabs/AssetsTab/AssetGrid.tsx +76 -0
  70. package/templates/devtools/tabs/AssetsTab/FolderTree.tsx +53 -0
  71. package/templates/devtools/tabs/AssetsTab/UploadDropzone.tsx +76 -0
  72. package/templates/devtools/tabs/AssetsTab/index.tsx +182 -0
  73. package/templates/devtools/tabs/ComponentsTab/AddTabButton.tsx +63 -0
  74. package/templates/devtools/tabs/ComponentsTab/ComponentList.tsx +153 -0
  75. package/templates/devtools/tabs/ComponentsTab/DesignSystemTab.tsx +1515 -0
  76. package/templates/devtools/tabs/ComponentsTab/DynamicFolderTab.tsx +113 -0
  77. package/templates/devtools/tabs/ComponentsTab/PropDisplay.tsx +55 -0
  78. package/templates/devtools/tabs/ComponentsTab/index.tsx +167 -0
  79. package/templates/devtools/tabs/ComponentsTab/previews/.gitkeep +4 -0
  80. package/templates/devtools/tabs/ComponentsTab/previews/Rad_os.tsx +262 -0
  81. package/templates/devtools/tabs/ComponentsTab/tabConfig.ts +53 -0
  82. package/templates/devtools/tabs/MockStatesTab/index.tsx +29 -0
  83. package/templates/devtools/tabs/TypographyTab/FontManager.tsx +421 -0
  84. package/templates/devtools/tabs/TypographyTab/TypographyStylesDisplay.tsx +290 -0
  85. package/templates/devtools/tabs/TypographyTab/index.tsx +98 -0
  86. package/templates/devtools/tabs/VariablesTab/BaseColorEditor.tsx +267 -0
  87. package/templates/devtools/tabs/VariablesTab/BorderRadiusEditor.tsx +37 -0
  88. package/templates/devtools/tabs/VariablesTab/ColorModeSelector.tsx +235 -0
  89. package/templates/devtools/tabs/VariablesTab/index.tsx +100 -0
  90. package/templates/devtools/types/index.ts +99 -0
  91. package/templates/globals.css +574 -0
  92. package/templates/hooks/index.ts +1 -0
  93. package/templates/hooks/useWindowManager.ts +212 -0
  94. package/templates/public/assets/icons/avatar.svg +18 -0
  95. package/templates/public/assets/icons/checkmark-filled.svg +14 -0
  96. package/templates/public/assets/icons/checkmark.svg +14 -0
  97. package/templates/public/assets/icons/chevron-down.svg +14 -0
  98. package/templates/public/assets/icons/close.svg +14 -0
  99. package/templates/public/assets/icons/copy.svg +14 -0
  100. package/templates/public/assets/icons/download.svg +14 -0
  101. package/templates/public/assets/icons/expand.svg +31 -0
  102. package/templates/public/assets/icons/file-blank.svg +17 -0
  103. package/templates/public/assets/icons/file-image.svg +19 -0
  104. package/templates/public/assets/icons/file-written.svg +17 -0
  105. package/templates/public/assets/icons/folder-closed.svg +17 -0
  106. package/templates/public/assets/icons/folder-open.svg +17 -0
  107. package/templates/public/assets/icons/hamburger.svg +18 -0
  108. package/templates/public/assets/icons/home-outline.svg +28 -0
  109. package/templates/public/assets/icons/home.svg +30 -0
  110. package/templates/public/assets/icons/hourglass.svg +25 -0
  111. package/templates/public/assets/icons/information-circle.svg +14 -0
  112. package/templates/public/assets/icons/information.svg +17 -0
  113. package/templates/public/assets/icons/lightning.svg +14 -0
  114. package/templates/public/assets/icons/locked.svg +17 -0
  115. package/templates/public/assets/icons/not-allowed.svg +14 -0
  116. package/templates/public/assets/icons/plus.svg +5 -0
  117. package/templates/public/assets/icons/power-thin.svg +17 -0
  118. package/templates/public/assets/icons/power.svg +17 -0
  119. package/templates/public/assets/icons/question-block.svg +14 -0
  120. package/templates/public/assets/icons/question.svg +17 -0
  121. package/templates/public/assets/icons/refresh-block.svg +14 -0
  122. package/templates/public/assets/icons/refresh.svg +17 -0
  123. package/templates/public/assets/icons/save.svg +14 -0
  124. package/templates/public/assets/icons/search.svg +25 -0
  125. package/templates/public/assets/icons/settings.svg +14 -0
  126. package/templates/public/assets/icons/trash-full.svg +21 -0
  127. package/templates/public/assets/icons/trash-open.svg +23 -0
  128. package/templates/public/assets/icons/trash.svg +18 -0
  129. package/templates/public/assets/icons/unlocked.svg +17 -0
  130. package/templates/public/assets/icons/waring-triangle-filled.svg +17 -0
  131. package/templates/public/assets/icons/warning-triangle-filled-2.svg +30 -0
  132. package/templates/public/assets/icons/warning-triangle-lines.svg +29 -0
  133. package/templates/public/assets/icons/wrench.svg +17 -0
@@ -0,0 +1,423 @@
1
+ 'use client';
2
+
3
+ import React, { useCallback, useRef, useState, useEffect } from 'react';
4
+ import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
5
+ import { useWindowManager } from '@/hooks/useWindowManager';
6
+ import { WindowTitleBar } from './WindowTitleBar';
7
+
8
+ // ============================================================================
9
+ // Types
10
+ // ============================================================================
11
+
12
+ interface AppWindowProps {
13
+ id: string;
14
+ title: string;
15
+ children: React.ReactNode;
16
+ defaultPosition?: { x: number; y: number };
17
+ defaultSize?: { width: string; height: string };
18
+ minWidth?: string;
19
+ minHeight?: string;
20
+ maxWidth?: string;
21
+ maxHeight?: string;
22
+ resizable?: boolean;
23
+ className?: string;
24
+ }
25
+
26
+ // ============================================================================
27
+ // Component
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Draggable and resizable window container for apps
32
+ *
33
+ * Features:
34
+ * - Draggable via title bar
35
+ * - Resizable via handles on edges and corners
36
+ * - Click-to-focus (z-index management)
37
+ * - Close button
38
+ * - Copy link button
39
+ * - Responsive sizing
40
+ *
41
+ * @example
42
+ * <AppWindow id="brand" title="Brand & Press">
43
+ * <BrandAssetsContent />
44
+ * </AppWindow>
45
+ */
46
+ export function AppWindow({
47
+ id,
48
+ title,
49
+ children,
50
+ defaultPosition = { x: 100, y: 50 },
51
+ defaultSize = { width: '900px', height: '700px' },
52
+ minWidth = '400px',
53
+ minHeight = '300px',
54
+ maxWidth = '95vw',
55
+ maxHeight = '85vh',
56
+ resizable = false,
57
+ className = '',
58
+ }: AppWindowProps) {
59
+ const nodeRef = useRef<HTMLDivElement>(null);
60
+ const contentRef = useRef<HTMLDivElement>(null);
61
+ const titleBarRef = useRef<HTMLDivElement>(null);
62
+ const {
63
+ getWindowState,
64
+ closeWindow,
65
+ focusWindow,
66
+ updateWindowPosition,
67
+ updateWindowSize
68
+ } = useWindowManager();
69
+
70
+ const windowState = getWindowState(id);
71
+
72
+ // Get current size from state or use default
73
+ const currentSize = windowState?.size || defaultSize;
74
+
75
+ // Parse size strings to numbers for calculations
76
+ const parseSize = (size: string): number => {
77
+ if (size.endsWith('px')) {
78
+ return parseFloat(size);
79
+ } else if (size.endsWith('vw')) {
80
+ return (parseFloat(size) / 100) * (typeof window !== 'undefined' ? window.innerWidth : 1920);
81
+ } else if (size.endsWith('vh')) {
82
+ return (parseFloat(size) / 100) * (typeof window !== 'undefined' ? window.innerHeight : 1080);
83
+ }
84
+ return parseFloat(size);
85
+ };
86
+
87
+ const parseMinSize = (size: string): number => parseSize(size);
88
+ const parseMaxSize = (size: string): number => {
89
+ if (size.endsWith('vw')) {
90
+ return (parseFloat(size) / 100) * (typeof window !== 'undefined' ? window.innerWidth : 1920);
91
+ } else if (size.endsWith('vh')) {
92
+ return (parseFloat(size) / 100) * (typeof window !== 'undefined' ? window.innerHeight : 1080);
93
+ }
94
+ return parseSize(size);
95
+ };
96
+
97
+ const [isResizing, setIsResizing] = useState(false);
98
+ const [resizeStart, setResizeStart] = useState({
99
+ x: 0,
100
+ y: 0,
101
+ width: 0,
102
+ height: 0,
103
+ left: 0,
104
+ top: 0,
105
+ positionX: 0,
106
+ positionY: 0,
107
+ });
108
+ const [resizeDirection, setResizeDirection] = useState<string>('');
109
+
110
+ // Handle window focus on click
111
+ const handleFocus = useCallback(() => {
112
+ focusWindow(id);
113
+ }, [focusWindow, id]);
114
+
115
+ // Handle close
116
+ const handleClose = useCallback(() => {
117
+ closeWindow(id);
118
+ }, [closeWindow, id]);
119
+
120
+ // Handle drag stop - update position in state
121
+ const handleDragStop = useCallback(
122
+ (_e: DraggableEvent, data: DraggableData) => {
123
+ updateWindowPosition(id, { x: data.x, y: data.y });
124
+ },
125
+ [id, updateWindowPosition]
126
+ );
127
+
128
+ // Handle resize start
129
+ const handleResizeStart = useCallback((e: React.MouseEvent, direction: string) => {
130
+ e.preventDefault();
131
+ e.stopPropagation();
132
+
133
+ if (!nodeRef.current) return;
134
+
135
+ const rect = nodeRef.current.getBoundingClientRect();
136
+ const currentPos = windowState?.position || defaultPosition;
137
+
138
+ setIsResizing(true);
139
+ setResizeDirection(direction);
140
+ setResizeStart({
141
+ x: e.clientX,
142
+ y: e.clientY,
143
+ width: rect.width,
144
+ height: rect.height,
145
+ left: rect.left,
146
+ top: rect.top,
147
+ positionX: currentPos.x,
148
+ positionY: currentPos.y,
149
+ });
150
+
151
+ focusWindow(id);
152
+ }, [focusWindow, id, windowState, defaultPosition]);
153
+
154
+ // Handle resize
155
+ useEffect(() => {
156
+ if (!isResizing) return;
157
+
158
+ const handleMouseMove = (e: MouseEvent) => {
159
+ if (!nodeRef.current) return;
160
+
161
+ const deltaX = e.clientX - resizeStart.x;
162
+ const deltaY = e.clientY - resizeStart.y;
163
+
164
+ const minWidthNum = parseMinSize(minWidth);
165
+ const minHeightNum = parseMinSize(minHeight);
166
+ const maxWidthNum = parseMaxSize(maxWidth);
167
+ const maxHeightNum = parseMaxSize(maxHeight);
168
+
169
+ let newWidth = resizeStart.width;
170
+ let newHeight = resizeStart.height;
171
+ let newX = resizeStart.positionX;
172
+ let newY = resizeStart.positionY;
173
+
174
+ // Calculate new dimensions based on resize direction
175
+ if (resizeDirection.includes('e')) {
176
+ newWidth = Math.min(Math.max(resizeStart.width + deltaX, minWidthNum), maxWidthNum);
177
+ }
178
+ if (resizeDirection.includes('w')) {
179
+ const widthChange = resizeStart.width - Math.min(Math.max(resizeStart.width - deltaX, minWidthNum), maxWidthNum);
180
+ newWidth = Math.min(Math.max(resizeStart.width - deltaX, minWidthNum), maxWidthNum);
181
+ newX = resizeStart.positionX + (resizeStart.width - newWidth);
182
+ }
183
+ if (resizeDirection.includes('s')) {
184
+ newHeight = Math.min(Math.max(resizeStart.height + deltaY, minHeightNum), maxHeightNum);
185
+ }
186
+ if (resizeDirection.includes('n')) {
187
+ const heightChange = resizeStart.height - Math.min(Math.max(resizeStart.height - deltaY, minHeightNum), maxHeightNum);
188
+ newHeight = Math.min(Math.max(resizeStart.height - deltaY, minHeightNum), maxHeightNum);
189
+ newY = resizeStart.positionY + (resizeStart.height - newHeight);
190
+ }
191
+
192
+ // Update size in state
193
+ updateWindowSize(id, {
194
+ width: `${newWidth}px`,
195
+ height: `${newHeight}px`,
196
+ });
197
+
198
+ // Update position if resizing from left or top
199
+ if (resizeDirection.includes('w') || resizeDirection.includes('n')) {
200
+ updateWindowPosition(id, { x: newX, y: newY });
201
+ }
202
+ };
203
+
204
+ const handleMouseUp = () => {
205
+ setIsResizing(false);
206
+ setResizeDirection('');
207
+ };
208
+
209
+ document.addEventListener('mousemove', handleMouseMove);
210
+ document.addEventListener('mouseup', handleMouseUp);
211
+
212
+ return () => {
213
+ document.removeEventListener('mousemove', handleMouseMove);
214
+ document.removeEventListener('mouseup', handleMouseUp);
215
+ };
216
+ }, [isResizing, resizeStart, resizeDirection, minWidth, minHeight, maxWidth, maxHeight, id, updateWindowSize, updateWindowPosition]);
217
+
218
+ // Auto-size window based on content on initial mount
219
+ useEffect(() => {
220
+ // Only auto-size if window doesn't have a saved size
221
+ if (windowState?.size) {
222
+ return;
223
+ }
224
+
225
+ // Skip auto-sizing if defaultSize is fit-content (let CSS handle it)
226
+ if (defaultSize.width === 'fit-content' || defaultSize.height === 'fit-content') {
227
+ return;
228
+ }
229
+
230
+ // Wait for content to render
231
+ const timeoutId = setTimeout(() => {
232
+ if (!contentRef.current || !titleBarRef.current || !nodeRef.current) {
233
+ return;
234
+ }
235
+
236
+ // Temporarily remove size constraints to measure natural content size
237
+ const originalWindowWidth = nodeRef.current.style.width;
238
+ const originalWindowHeight = nodeRef.current.style.height;
239
+ const originalWindowMaxWidth = nodeRef.current.style.maxWidth;
240
+ const originalWindowMaxHeight = nodeRef.current.style.maxHeight;
241
+ const originalContentFlex = contentRef.current.style.flex;
242
+ const originalContentOverflow = contentRef.current.style.overflow;
243
+ const originalContentWidth = contentRef.current.style.width;
244
+ const originalContentHeight = contentRef.current.style.height;
245
+
246
+ // Set window to very large size temporarily to allow content to expand
247
+ nodeRef.current.style.width = '9999px';
248
+ nodeRef.current.style.height = '9999px';
249
+ nodeRef.current.style.maxWidth = 'none';
250
+ nodeRef.current.style.maxHeight = 'none';
251
+
252
+ // Remove flex constraints from content container to measure natural size
253
+ contentRef.current.style.flex = 'none';
254
+ contentRef.current.style.overflow = 'visible';
255
+ contentRef.current.style.width = 'auto';
256
+ contentRef.current.style.height = 'auto';
257
+
258
+ // Force layout recalculation
259
+ void nodeRef.current.offsetHeight;
260
+ void contentRef.current.offsetHeight;
261
+
262
+ // Measure content dimensions
263
+ const contentWidth = contentRef.current.scrollWidth;
264
+ const contentHeight = contentRef.current.scrollHeight;
265
+
266
+ // Measure title bar height
267
+ const titleBarHeight = titleBarRef.current.offsetHeight;
268
+
269
+ // Restore original styles
270
+ nodeRef.current.style.width = originalWindowWidth;
271
+ nodeRef.current.style.height = originalWindowHeight;
272
+ nodeRef.current.style.maxWidth = originalWindowMaxWidth;
273
+ nodeRef.current.style.maxHeight = originalWindowMaxHeight;
274
+ contentRef.current.style.flex = originalContentFlex;
275
+ contentRef.current.style.overflow = originalContentOverflow;
276
+ contentRef.current.style.width = originalContentWidth;
277
+ contentRef.current.style.height = originalContentHeight;
278
+
279
+ // Window padding: 8px on sides, 8px bottom
280
+ const horizontalPadding = 16; // 8px left + 8px right
281
+ const verticalPadding = 8; // 8px bottom (title bar has its own padding)
282
+
283
+ // Calculate required window dimensions
284
+ const requiredWidth = contentWidth + horizontalPadding;
285
+ const requiredHeight = contentHeight + titleBarHeight + verticalPadding;
286
+
287
+ // Get viewport constraints
288
+ const viewportWidth = typeof window !== 'undefined' ? window.innerWidth : 1920;
289
+ const viewportHeight = typeof window !== 'undefined' ? window.innerHeight : 1080;
290
+ const maxWidthPx = parseMaxSize(maxWidth);
291
+ const maxHeightPx = parseMaxSize(maxHeight);
292
+ const minWidthPx = parseMinSize(minWidth);
293
+ const minHeightPx = parseMinSize(minHeight);
294
+
295
+ // Cap at viewport size (95vw, 85vh) or max constraints
296
+ const effectiveMaxWidth = Math.min(maxWidthPx, viewportWidth * 0.95);
297
+ const effectiveMaxHeight = Math.min(maxHeightPx, viewportHeight * 0.85);
298
+
299
+ // Apply constraints
300
+ const finalWidth = Math.max(
301
+ minWidthPx,
302
+ Math.min(requiredWidth, effectiveMaxWidth)
303
+ );
304
+ const finalHeight = Math.max(
305
+ minHeightPx,
306
+ Math.min(requiredHeight, effectiveMaxHeight)
307
+ );
308
+
309
+ updateWindowSize(id, {
310
+ width: `${finalWidth}px`,
311
+ height: `${finalHeight}px`,
312
+ });
313
+ }, 0);
314
+
315
+ return () => clearTimeout(timeoutId);
316
+ }, [id, windowState?.size, maxWidth, maxHeight, minWidth, minHeight, updateWindowSize, defaultSize.width, defaultSize.height]);
317
+
318
+ // Don't render if window is not open or is minimized
319
+ // This check must be AFTER all hooks are called!
320
+ if (!windowState?.isOpen || windowState?.isMinimized) {
321
+ return null;
322
+ }
323
+
324
+ return (
325
+ <Draggable
326
+ nodeRef={nodeRef}
327
+ handle="[data-drag-handle]"
328
+ position={windowState?.position || defaultPosition}
329
+ onStop={handleDragStop}
330
+ bounds="parent"
331
+ disabled={isResizing}
332
+ >
333
+ <div
334
+ ref={nodeRef}
335
+ className={`
336
+ absolute
337
+ pointer-events-auto
338
+ border border-primary
339
+ rounded-lg
340
+ shadow-[4px_4px_0px_0px_var(--border-primary)]
341
+ overflow-hidden
342
+ flex flex-col
343
+ ${className}
344
+ `}
345
+ style={{
346
+ width: currentSize.width,
347
+ height: currentSize.height,
348
+ minWidth,
349
+ minHeight,
350
+ maxWidth,
351
+ maxHeight,
352
+ zIndex: windowState?.zIndex || 100,
353
+ padding: '0px 8px 8px',
354
+ background: 'linear-gradient(180deg, var(--bg-primary) 0%, var(--bg-tertiary) 100%)',
355
+ }}
356
+ onMouseDown={handleFocus}
357
+ data-app-window={id}
358
+ data-resizable={resizable}
359
+ >
360
+ {/* Title Bar */}
361
+ <div ref={titleBarRef}>
362
+ <WindowTitleBar
363
+ title={title}
364
+ windowId={id}
365
+ onClose={handleClose}
366
+ />
367
+ </div>
368
+
369
+ {/* Content */}
370
+ <div
371
+ ref={contentRef}
372
+ className="flex-1 overflow-auto rounded-sm"
373
+ >
374
+ {children}
375
+ </div>
376
+
377
+ {/* Resize Handles - only render if resizable is true */}
378
+ {resizable && (
379
+ <>
380
+ {/* Corner handles */}
381
+ <div
382
+ className="absolute top-0 left-0 w-3 h-3 cursor-nwse-resize z-10"
383
+ onMouseDown={(e) => handleResizeStart(e, 'nw')}
384
+ />
385
+ <div
386
+ className="absolute top-0 right-0 w-3 h-3 cursor-nesw-resize z-10"
387
+ onMouseDown={(e) => handleResizeStart(e, 'ne')}
388
+ />
389
+ <div
390
+ className="absolute bottom-0 left-0 w-3 h-3 cursor-nesw-resize z-10"
391
+ onMouseDown={(e) => handleResizeStart(e, 'sw')}
392
+ />
393
+ <div
394
+ className="absolute bottom-0 right-0 w-3 h-3 cursor-nwse-resize z-10"
395
+ onMouseDown={(e) => handleResizeStart(e, 'se')}
396
+ />
397
+
398
+ {/* Edge handles */}
399
+ <div
400
+ className="absolute top-0 left-3 right-3 h-1 cursor-ns-resize z-10"
401
+ onMouseDown={(e) => handleResizeStart(e, 'n')}
402
+ />
403
+ <div
404
+ className="absolute bottom-0 left-3 right-3 h-1 cursor-ns-resize z-10"
405
+ onMouseDown={(e) => handleResizeStart(e, 's')}
406
+ />
407
+ <div
408
+ className="absolute left-0 top-3 bottom-3 w-1 cursor-ew-resize z-10"
409
+ onMouseDown={(e) => handleResizeStart(e, 'w')}
410
+ />
411
+ <div
412
+ className="absolute right-0 top-3 bottom-3 w-1 cursor-ew-resize z-10"
413
+ onMouseDown={(e) => handleResizeStart(e, 'e')}
414
+ />
415
+ </>
416
+ )}
417
+ </div>
418
+ </Draggable>
419
+ );
420
+ }
421
+
422
+ export default AppWindow;
423
+
@@ -0,0 +1,76 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { useWindowManager } from '@/hooks/useWindowManager';
5
+ import { CloseIcon } from '@/components/icons';
6
+
7
+ // ============================================================================
8
+ // Types
9
+ // ============================================================================
10
+
11
+ interface MobileAppModalProps {
12
+ id: string;
13
+ title: string;
14
+ children: React.ReactNode;
15
+ }
16
+
17
+ // ============================================================================
18
+ // Component
19
+ // ============================================================================
20
+
21
+ /**
22
+ * Full-screen modal for mobile app display
23
+ * Replaces draggable windows on mobile devices
24
+ */
25
+ export function MobileAppModal({ id, title, children }: MobileAppModalProps) {
26
+ const { getWindowState, closeWindow } = useWindowManager();
27
+ const windowState = getWindowState(id);
28
+
29
+ if (!windowState?.isOpen || windowState?.isMinimized) {
30
+ return null;
31
+ }
32
+
33
+ return (
34
+ <div
35
+ className="fixed inset-0 z-[200] bg-warm-cloud flex flex-col"
36
+ style={{ zIndex: windowState.zIndex + 100 }}
37
+ >
38
+ {/* Header */}
39
+ <header
40
+ className="
41
+ flex items-center justify-between
42
+ px-4 py-3
43
+ bg-warm-cloud
44
+ border-b
45
+ flex-shrink-0
46
+ "
47
+ style={{ borderBottomColor: 'var(--border-primary-20)' }}
48
+ >
49
+ <h1 className="font-joystix text-sm text-primary uppercase">
50
+ {title}
51
+ </h1>
52
+ <button
53
+ type="button"
54
+ onClick={() => closeWindow(id)}
55
+ className="
56
+ w-10 h-10
57
+ flex items-center justify-center
58
+ hover:bg-black/5 active:bg-black/10
59
+ rounded-sm
60
+ -mr-2
61
+ "
62
+ >
63
+ <CloseIcon size={14} className="text-primary" />
64
+ </button>
65
+ </header>
66
+
67
+ {/* Content */}
68
+ <main className="flex-1 overflow-auto">
69
+ {children}
70
+ </main>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ export default MobileAppModal;
76
+