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