claudeship 0.2.12 → 0.2.15

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 (128) hide show
  1. package/README.md +18 -0
  2. package/apps/server/dist/app.module.js +10 -0
  3. package/apps/server/dist/app.module.js.map +1 -1
  4. package/apps/server/dist/chat/prompts/fullstack-express-prompt.d.ts +1 -1
  5. package/apps/server/dist/chat/prompts/fullstack-express-prompt.js +109 -1
  6. package/apps/server/dist/chat/prompts/fullstack-express-prompt.js.map +1 -1
  7. package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.d.ts +1 -1
  8. package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.js +109 -1
  9. package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.js.map +1 -1
  10. package/apps/server/dist/chat/prompts/web-system-prompt.d.ts +1 -1
  11. package/apps/server/dist/chat/prompts/web-system-prompt.js +156 -0
  12. package/apps/server/dist/chat/prompts/web-system-prompt.js.map +1 -1
  13. package/apps/server/dist/checkpoint/checkpoint.controller.d.ts +19 -0
  14. package/apps/server/dist/checkpoint/checkpoint.controller.js +93 -0
  15. package/apps/server/dist/checkpoint/checkpoint.controller.js.map +1 -0
  16. package/apps/server/dist/checkpoint/checkpoint.module.d.ts +2 -0
  17. package/apps/server/dist/checkpoint/checkpoint.module.js +25 -0
  18. package/apps/server/dist/checkpoint/checkpoint.module.js.map +1 -0
  19. package/apps/server/dist/checkpoint/checkpoint.service.d.ts +41 -0
  20. package/apps/server/dist/checkpoint/checkpoint.service.js +261 -0
  21. package/apps/server/dist/checkpoint/checkpoint.service.js.map +1 -0
  22. package/apps/server/dist/database/database.controller.d.ts +23 -0
  23. package/apps/server/dist/database/database.controller.js +109 -0
  24. package/apps/server/dist/database/database.controller.js.map +1 -0
  25. package/apps/server/dist/database/database.module.d.ts +2 -0
  26. package/apps/server/dist/database/database.module.js +25 -0
  27. package/apps/server/dist/database/database.module.js.map +1 -0
  28. package/apps/server/dist/database/database.service.d.ts +32 -0
  29. package/apps/server/dist/database/database.service.js +238 -0
  30. package/apps/server/dist/database/database.service.js.map +1 -0
  31. package/apps/server/dist/env/env.controller.d.ts +14 -0
  32. package/apps/server/dist/env/env.controller.js +84 -0
  33. package/apps/server/dist/env/env.controller.js.map +1 -0
  34. package/apps/server/dist/env/env.module.d.ts +2 -0
  35. package/apps/server/dist/env/env.module.js +25 -0
  36. package/apps/server/dist/env/env.module.js.map +1 -0
  37. package/apps/server/dist/env/env.service.d.ts +21 -0
  38. package/apps/server/dist/env/env.service.js +194 -0
  39. package/apps/server/dist/env/env.service.js.map +1 -0
  40. package/apps/server/dist/preview/preview.controller.d.ts +5 -0
  41. package/apps/server/dist/preview/preview.controller.js +41 -0
  42. package/apps/server/dist/preview/preview.controller.js.map +1 -1
  43. package/apps/server/dist/preview/preview.service.d.ts +20 -0
  44. package/apps/server/dist/preview/preview.service.js +51 -2
  45. package/apps/server/dist/preview/preview.service.js.map +1 -1
  46. package/apps/server/dist/project/project.controller.d.ts +10 -1
  47. package/apps/server/dist/project/project.controller.js +57 -0
  48. package/apps/server/dist/project/project.controller.js.map +1 -1
  49. package/apps/server/dist/project/project.service.d.ts +15 -0
  50. package/apps/server/dist/project/project.service.js +111 -0
  51. package/apps/server/dist/project/project.service.js.map +1 -1
  52. package/apps/server/dist/project-context/project-context.controller.d.ts +42 -0
  53. package/apps/server/dist/project-context/project-context.controller.js +127 -0
  54. package/apps/server/dist/project-context/project-context.controller.js.map +1 -0
  55. package/apps/server/dist/project-context/project-context.module.d.ts +2 -0
  56. package/apps/server/dist/project-context/project-context.module.js +25 -0
  57. package/apps/server/dist/project-context/project-context.module.js.map +1 -0
  58. package/apps/server/dist/project-context/project-context.service.d.ts +36 -0
  59. package/apps/server/dist/project-context/project-context.service.js +260 -0
  60. package/apps/server/dist/project-context/project-context.service.js.map +1 -0
  61. package/apps/server/dist/testing/testing.controller.d.ts +24 -0
  62. package/apps/server/dist/testing/testing.controller.js +126 -0
  63. package/apps/server/dist/testing/testing.controller.js.map +1 -0
  64. package/apps/server/dist/testing/testing.module.d.ts +2 -0
  65. package/apps/server/dist/testing/testing.module.js +26 -0
  66. package/apps/server/dist/testing/testing.module.js.map +1 -0
  67. package/apps/server/dist/testing/testing.service.d.ts +62 -0
  68. package/apps/server/dist/testing/testing.service.js +269 -0
  69. package/apps/server/dist/testing/testing.service.js.map +1 -0
  70. package/apps/server/dist/tsconfig.tsbuildinfo +1 -1
  71. package/apps/server/package.json +1 -1
  72. package/apps/web/.next/BUILD_ID +1 -1
  73. package/apps/web/.next/app-build-manifest.json +5 -5
  74. package/apps/web/.next/build-manifest.json +2 -2
  75. package/apps/web/.next/cache/.previewinfo +1 -1
  76. package/apps/web/.next/cache/.rscinfo +1 -1
  77. package/apps/web/.next/cache/.tsbuildinfo +1 -1
  78. package/apps/web/.next/cache/config.json +3 -3
  79. package/apps/web/.next/cache/eslint/.cache_j3uhuz +1 -1
  80. package/apps/web/.next/cache/webpack/client-production/0.pack +0 -0
  81. package/apps/web/.next/cache/webpack/client-production/index.pack +0 -0
  82. package/apps/web/.next/cache/webpack/edge-server-production/index.pack +0 -0
  83. package/apps/web/.next/cache/webpack/server-production/0.pack +0 -0
  84. package/apps/web/.next/cache/webpack/server-production/index.pack +0 -0
  85. package/apps/web/.next/prerender-manifest.json +10 -10
  86. package/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  87. package/apps/web/.next/server/app/_not-found.html +1 -1
  88. package/apps/web/.next/server/app/_not-found.rsc +2 -2
  89. package/apps/web/.next/server/app/index.html +1 -1
  90. package/apps/web/.next/server/app/index.rsc +3 -3
  91. package/apps/web/.next/server/app/page.js +2 -2
  92. package/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  93. package/apps/web/.next/server/app/project/[id]/page.js +2 -2
  94. package/apps/web/.next/server/app/project/[id]/page_client-reference-manifest.js +1 -1
  95. package/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  96. package/apps/web/.next/server/app/settings.html +1 -1
  97. package/apps/web/.next/server/app/settings.rsc +3 -3
  98. package/apps/web/.next/server/chunks/392.js +1 -1
  99. package/apps/web/.next/server/pages/404.html +1 -1
  100. package/apps/web/.next/server/pages/500.html +1 -1
  101. package/apps/web/.next/server/server-reference-manifest.json +1 -1
  102. package/apps/web/.next/static/chunks/574-1fe2bcd6cfb41646.js +1 -0
  103. package/apps/web/.next/static/chunks/app/page-f19cfa58541ca83d.js +1 -0
  104. package/apps/web/.next/static/chunks/app/project/[id]/page-dffaa1d02f012216.js +1 -0
  105. package/apps/web/.next/static/chunks/app/settings/page-d1318c2fd58729a5.js +1 -0
  106. package/apps/web/.next/static/css/0a24552d9794f8c8.css +3 -0
  107. package/apps/web/.next/trace +18 -17
  108. package/apps/web/node_modules/.bin/eslint +2 -2
  109. package/apps/web/package.json +2 -1
  110. package/apps/web/src/components/checkpoint/CheckpointPanel.tsx +384 -0
  111. package/apps/web/src/components/database/DatabasePanel.tsx +405 -0
  112. package/apps/web/src/components/env/EnvPanel.tsx +356 -0
  113. package/apps/web/src/components/preview/ConsoleViewer.tsx +270 -0
  114. package/apps/web/src/components/preview/ErrorOverlay.tsx +189 -0
  115. package/apps/web/src/components/preview/PreviewPanel.tsx +148 -6
  116. package/apps/web/src/components/testing/TestRunner.tsx +481 -0
  117. package/apps/web/src/components/ui/tabs.tsx +55 -0
  118. package/apps/web/src/components/visual-editor/VisualEditor.tsx +382 -0
  119. package/apps/web/src/components/workspace/WorkspaceLayout.tsx +66 -4
  120. package/apps/web/src/lib/api.ts +5 -2
  121. package/package.json +1 -1
  122. package/apps/web/.next/static/chunks/298-6f3d6b321c288cd3.js +0 -1
  123. package/apps/web/.next/static/chunks/app/page-3d093f7f480a8599.js +0 -1
  124. package/apps/web/.next/static/chunks/app/project/[id]/page-e5cda6f9050b0a52.js +0 -1
  125. package/apps/web/.next/static/chunks/app/settings/page-92d28565c3d8c755.js +0 -1
  126. package/apps/web/.next/static/css/8f946046a2047594.css +0 -3
  127. /package/apps/web/.next/static/{aXT20mSdxaem1-z8VH2F1 → mkY_TTl_ho_ehDKiX10AN}/_buildManifest.js +0 -0
  128. /package/apps/web/.next/static/{aXT20mSdxaem1-z8VH2F1 → mkY_TTl_ho_ehDKiX10AN}/_ssgManifest.js +0 -0
@@ -0,0 +1,189 @@
1
+ "use client";
2
+
3
+ import { X, ExternalLink, Copy, Check } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { useState } from "react";
6
+
7
+ export interface ErrorInfo {
8
+ type: "compile" | "runtime" | "network";
9
+ message: string;
10
+ stack?: string;
11
+ location?: {
12
+ file: string;
13
+ line: number;
14
+ column: number;
15
+ };
16
+ }
17
+
18
+ interface ErrorOverlayProps {
19
+ error: ErrorInfo;
20
+ onDismiss: () => void;
21
+ }
22
+
23
+ export function ErrorOverlay({ error, onDismiss }: ErrorOverlayProps) {
24
+ const [copied, setCopied] = useState(false);
25
+
26
+ const handleCopy = async () => {
27
+ const text = [
28
+ error.message,
29
+ error.location ? `at ${error.location.file}:${error.location.line}:${error.location.column}` : "",
30
+ error.stack || "",
31
+ ].filter(Boolean).join("\n\n");
32
+
33
+ await navigator.clipboard.writeText(text);
34
+ setCopied(true);
35
+ setTimeout(() => setCopied(false), 2000);
36
+ };
37
+
38
+ const errorTypeLabels = {
39
+ compile: "Compilation Error",
40
+ runtime: "Runtime Error",
41
+ network: "Network Error",
42
+ };
43
+
44
+ const errorTypeColors = {
45
+ compile: "text-red-500",
46
+ runtime: "text-orange-500",
47
+ network: "text-yellow-500",
48
+ };
49
+
50
+ return (
51
+ <div className="absolute inset-0 bg-black/80 backdrop-blur-sm z-50 overflow-auto">
52
+ <div className="min-h-full p-6">
53
+ <div className="max-w-2xl mx-auto bg-zinc-900 rounded-lg border border-red-500/50 overflow-hidden">
54
+ {/* Header */}
55
+ <div className="flex items-center justify-between px-4 py-3 bg-red-500/10 border-b border-red-500/30">
56
+ <div className="flex items-center gap-2">
57
+ <div className="w-3 h-3 rounded-full bg-red-500" />
58
+ <span className={`font-medium ${errorTypeColors[error.type]}`}>
59
+ {errorTypeLabels[error.type]}
60
+ </span>
61
+ </div>
62
+ <div className="flex items-center gap-1">
63
+ <Button
64
+ variant="ghost"
65
+ size="sm"
66
+ onClick={handleCopy}
67
+ className="h-7 px-2 text-zinc-400 hover:text-white"
68
+ >
69
+ {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
70
+ </Button>
71
+ <Button
72
+ variant="ghost"
73
+ size="sm"
74
+ onClick={onDismiss}
75
+ className="h-7 px-2 text-zinc-400 hover:text-white"
76
+ >
77
+ <X className="h-4 w-4" />
78
+ </Button>
79
+ </div>
80
+ </div>
81
+
82
+ {/* Error Message */}
83
+ <div className="p-4 space-y-4">
84
+ <p className="text-lg font-mono text-red-400 whitespace-pre-wrap">
85
+ {error.message}
86
+ </p>
87
+
88
+ {/* Location */}
89
+ {error.location && (
90
+ <div className="flex items-center gap-2 text-sm text-zinc-400">
91
+ <ExternalLink className="h-4 w-4" />
92
+ <code className="bg-zinc-800 px-2 py-0.5 rounded">
93
+ {error.location.file}:{error.location.line}:{error.location.column}
94
+ </code>
95
+ </div>
96
+ )}
97
+
98
+ {/* Stack Trace */}
99
+ {error.stack && (
100
+ <div className="mt-4">
101
+ <p className="text-xs text-zinc-500 mb-2 uppercase tracking-wide">Stack Trace</p>
102
+ <pre className="bg-zinc-950 p-3 rounded-lg text-xs text-zinc-400 overflow-x-auto font-mono">
103
+ {error.stack}
104
+ </pre>
105
+ </div>
106
+ )}
107
+ </div>
108
+
109
+ {/* Footer */}
110
+ <div className="px-4 py-3 bg-zinc-800/50 border-t border-zinc-700 flex justify-end">
111
+ <Button
112
+ variant="outline"
113
+ size="sm"
114
+ onClick={onDismiss}
115
+ className="text-zinc-300"
116
+ >
117
+ Dismiss
118
+ </Button>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ );
124
+ }
125
+
126
+ /**
127
+ * Parse error from stderr log message
128
+ */
129
+ export function parseErrorFromLog(message: string): ErrorInfo | null {
130
+ // Skip common non-error messages
131
+ if (message.includes("Compiling...") ||
132
+ message.includes("Ready in") ||
133
+ message.includes("Compiled successfully") ||
134
+ message.includes("Fast Refresh") ||
135
+ message.includes("○ Compiling") ||
136
+ message.includes("✓ Compiled")) {
137
+ return null;
138
+ }
139
+
140
+ // Next.js / TypeScript error pattern
141
+ // Example: "Error: Cannot find module 'xxx'"
142
+ // Example: "./src/components/Test.tsx:10:5"
143
+ const nextjsErrorPattern = /(?:Error|TypeError|SyntaxError|ReferenceError):\s*(.+)/;
144
+ const locationPattern = /(?:\.\/)?([^:]+):(\d+):(\d+)/;
145
+
146
+ const errorMatch = message.match(nextjsErrorPattern);
147
+
148
+ if (errorMatch) {
149
+ const locationMatch = message.match(locationPattern);
150
+
151
+ return {
152
+ type: "compile",
153
+ message: errorMatch[1],
154
+ stack: message,
155
+ location: locationMatch ? {
156
+ file: locationMatch[1],
157
+ line: parseInt(locationMatch[2], 10),
158
+ column: parseInt(locationMatch[3], 10),
159
+ } : undefined,
160
+ };
161
+ }
162
+
163
+ // Check for "Failed to compile" message
164
+ if (message.includes("Failed to compile") || message.includes("Compilation failed")) {
165
+ return {
166
+ type: "compile",
167
+ message: "Failed to compile. Check the console for more details.",
168
+ stack: message,
169
+ };
170
+ }
171
+
172
+ // Check for module not found errors
173
+ const moduleNotFound = message.match(/Module not found:\s*(.+)/);
174
+ if (moduleNotFound) {
175
+ const locationMatch = message.match(locationPattern);
176
+ return {
177
+ type: "compile",
178
+ message: moduleNotFound[1],
179
+ stack: message,
180
+ location: locationMatch ? {
181
+ file: locationMatch[1],
182
+ line: parseInt(locationMatch[2], 10),
183
+ column: parseInt(locationMatch[3], 10),
184
+ } : undefined,
185
+ };
186
+ }
187
+
188
+ return null;
189
+ }
@@ -1,13 +1,25 @@
1
1
  "use client";
2
2
 
3
3
  import { useEffect, useState, useRef, useCallback } from "react";
4
- import { Play, Square, RefreshCw, ExternalLink, Package, Loader2, Zap, RotateCcw } from "lucide-react";
4
+ import { Play, Square, RefreshCw, ExternalLink, Package, Loader2, Zap, RotateCcw, Terminal, Monitor, Smartphone, Tablet } from "lucide-react";
5
5
  import { Button } from "@/components/ui/button";
6
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
6
7
  import { usePreviewStore } from "@/stores/usePreviewStore";
7
8
  import { useTranslation } from "@/lib/i18n";
9
+ import { ConsoleViewer } from "./ConsoleViewer";
10
+ import { ErrorOverlay, ErrorInfo, parseErrorFromLog } from "./ErrorOverlay";
8
11
 
9
12
  const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:14000";
10
13
 
14
+ // Device presets for responsive preview
15
+ const devicePresets = {
16
+ desktop: { width: "100%", height: "100%", label: "Desktop" },
17
+ tablet: { width: "768px", height: "1024px", label: "Tablet" },
18
+ mobile: { width: "375px", height: "667px", label: "Mobile" },
19
+ } as const;
20
+
21
+ type DeviceType = keyof typeof devicePresets;
22
+
11
23
  interface PreviewPanelProps {
12
24
  projectId: string;
13
25
  }
@@ -196,6 +208,63 @@ export function PreviewPanel({ projectId }: PreviewPanelProps) {
196
208
  }
197
209
  };
198
210
 
211
+ const [activeTab, setActiveTab] = useState<"preview" | "console">("preview");
212
+ const [device, setDevice] = useState<DeviceType>("desktop");
213
+ const [buildError, setBuildError] = useState<ErrorInfo | null>(null);
214
+ const logEventSourceRef = useRef<EventSource | null>(null);
215
+
216
+ // Subscribe to log stream for error detection
217
+ useEffect(() => {
218
+ if (!isMounted || status !== "running") {
219
+ if (logEventSourceRef.current) {
220
+ logEventSourceRef.current.close();
221
+ logEventSourceRef.current = null;
222
+ }
223
+ return;
224
+ }
225
+
226
+ const eventSource = new EventSource(
227
+ `${API_BASE}/projects/${projectId}/preview/logs/stream`
228
+ );
229
+
230
+ eventSource.onmessage = (event) => {
231
+ try {
232
+ const data = JSON.parse(event.data);
233
+ if (data.type === "log" && data.entry && data.entry.level === "stderr") {
234
+ const errorInfo = parseErrorFromLog(data.entry.message);
235
+ if (errorInfo) {
236
+ setBuildError(errorInfo);
237
+ }
238
+ }
239
+ } catch {
240
+ // Ignore parse errors
241
+ }
242
+ };
243
+
244
+ eventSource.onerror = () => {
245
+ eventSource.close();
246
+ logEventSourceRef.current = null;
247
+ };
248
+
249
+ logEventSourceRef.current = eventSource;
250
+
251
+ return () => {
252
+ eventSource.close();
253
+ logEventSourceRef.current = null;
254
+ };
255
+ }, [isMounted, status, projectId]);
256
+
257
+ // Clear error when preview restarts
258
+ useEffect(() => {
259
+ if (status === "starting") {
260
+ setBuildError(null);
261
+ }
262
+ }, [status]);
263
+
264
+ const handleDismissError = useCallback(() => {
265
+ setBuildError(null);
266
+ }, []);
267
+
199
268
  return (
200
269
  <div className="flex h-full flex-col border-l">
201
270
  {/* Controls */}
@@ -228,6 +297,61 @@ export function PreviewPanel({ projectId }: PreviewPanelProps) {
228
297
  </div>
229
298
 
230
299
  <div className="flex items-center gap-1">
300
+ {/* Device Selector (only visible in preview tab when running) */}
301
+ {activeTab === "preview" && status === "running" && (
302
+ <div className="flex items-center gap-0.5 mr-2 border-r pr-2">
303
+ <Button
304
+ variant={device === "desktop" ? "secondary" : "ghost"}
305
+ size="sm"
306
+ onClick={() => setDevice("desktop")}
307
+ className="h-7 px-2"
308
+ title="Desktop"
309
+ >
310
+ <Monitor className="h-4 w-4" />
311
+ </Button>
312
+ <Button
313
+ variant={device === "tablet" ? "secondary" : "ghost"}
314
+ size="sm"
315
+ onClick={() => setDevice("tablet")}
316
+ className="h-7 px-2"
317
+ title="Tablet (768x1024)"
318
+ >
319
+ <Tablet className="h-4 w-4" />
320
+ </Button>
321
+ <Button
322
+ variant={device === "mobile" ? "secondary" : "ghost"}
323
+ size="sm"
324
+ onClick={() => setDevice("mobile")}
325
+ className="h-7 px-2"
326
+ title="Mobile (375x667)"
327
+ >
328
+ <Smartphone className="h-4 w-4" />
329
+ </Button>
330
+ </div>
331
+ )}
332
+
333
+ {/* Tab Switcher */}
334
+ <div className="flex items-center gap-0.5 mr-2 border-r pr-2">
335
+ <Button
336
+ variant={activeTab === "preview" ? "secondary" : "ghost"}
337
+ size="sm"
338
+ onClick={() => setActiveTab("preview")}
339
+ className="h-7 px-2"
340
+ title="Preview"
341
+ >
342
+ <Monitor className="h-4 w-4" />
343
+ </Button>
344
+ <Button
345
+ variant={activeTab === "console" ? "secondary" : "ghost"}
346
+ size="sm"
347
+ onClick={() => setActiveTab("console")}
348
+ className="h-7 px-2"
349
+ title="Console"
350
+ >
351
+ <Terminal className="h-4 w-4" />
352
+ </Button>
353
+ </div>
354
+
231
355
  {status === "running" ? (
232
356
  <>
233
357
  <Button
@@ -294,20 +418,38 @@ export function PreviewPanel({ projectId }: PreviewPanelProps) {
294
418
  </div>
295
419
  </div>
296
420
 
297
- {/* Preview Frame */}
298
- <div className="flex-1 bg-muted/30">
299
- {!isMounted ? (
421
+ {/* Tab Content */}
422
+ <div className="flex-1 bg-muted/30 overflow-hidden">
423
+ {activeTab === "console" ? (
424
+ <ConsoleViewer projectId={projectId} isRunning={status === "running"} />
425
+ ) : !isMounted ? (
300
426
  <div className="flex h-full items-center justify-center">
301
427
  <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
302
428
  </div>
303
429
  ) : status === "running" && url ? (
304
- <div className="relative h-full w-full">
430
+ <div className={`relative h-full w-full ${device !== "desktop" ? "flex items-center justify-center bg-muted/50 p-4" : ""}`}>
305
431
  <iframe
306
432
  ref={iframeRef}
307
433
  key={url}
308
434
  src={url}
309
- className="h-full w-full border-0"
435
+ style={{
436
+ width: devicePresets[device].width,
437
+ height: devicePresets[device].height,
438
+ maxWidth: "100%",
439
+ maxHeight: "100%",
440
+ }}
441
+ className={`border-0 ${device !== "desktop" ? "border rounded-lg shadow-lg bg-white" : ""}`}
310
442
  />
443
+ {/* Error Overlay */}
444
+ {buildError && (
445
+ <ErrorOverlay error={buildError} onDismiss={handleDismissError} />
446
+ )}
447
+ {/* Device size indicator */}
448
+ {device !== "desktop" && (
449
+ <div className="absolute bottom-4 left-4 px-2 py-1 bg-black/70 text-white text-xs rounded">
450
+ {devicePresets[device].label} ({devicePresets[device].width} x {devicePresets[device].height})
451
+ </div>
452
+ )}
311
453
  {/* File change notification */}
312
454
  {lastChange && (
313
455
  <div className="absolute bottom-4 right-4 flex items-center gap-2 px-3 py-2 bg-green-500/90 text-white text-xs rounded-lg shadow-lg animate-pulse">