@jxrstudios/jxr 1.0.10 → 1.1.11

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 (87) hide show
  1. package/bin/jxr.js +6 -0
  2. package/dist/index.js +57 -2
  3. package/dist/jxr-server-manager.d.ts.map +1 -1
  4. package/package.json +1 -1
  5. package/src/jxr-server-manager.ts +65 -2
  6. package/zzz_react_template/App.tsx +43 -156
  7. package/zzz_react_template/components/ErrorBoundary.tsx +62 -0
  8. package/zzz_react_template/components/ManusDialog.tsx +85 -0
  9. package/zzz_react_template/components/Map.tsx +155 -0
  10. package/zzz_react_template/components/jxr/CodeEditor.tsx +313 -0
  11. package/zzz_react_template/components/jxr/FileExplorer.tsx +230 -0
  12. package/zzz_react_template/components/jxr/IDEShell.tsx +159 -0
  13. package/zzz_react_template/components/jxr/LandingPage.tsx +414 -0
  14. package/zzz_react_template/components/jxr/LivePreview.tsx +169 -0
  15. package/zzz_react_template/components/jxr/PerformanceDashboard.tsx +379 -0
  16. package/zzz_react_template/components/jxr/TopBar.tsx +149 -0
  17. package/zzz_react_template/components/ui/accordion.tsx +64 -0
  18. package/zzz_react_template/components/ui/alert-dialog.tsx +155 -0
  19. package/zzz_react_template/components/ui/alert.tsx +66 -0
  20. package/zzz_react_template/components/ui/aspect-ratio.tsx +9 -0
  21. package/zzz_react_template/components/ui/avatar.tsx +51 -0
  22. package/zzz_react_template/components/ui/badge.tsx +46 -0
  23. package/zzz_react_template/components/ui/breadcrumb.tsx +109 -0
  24. package/zzz_react_template/components/ui/button-group.tsx +83 -0
  25. package/zzz_react_template/components/ui/button.tsx +60 -0
  26. package/zzz_react_template/components/ui/calendar.tsx +211 -0
  27. package/zzz_react_template/components/ui/card.tsx +92 -0
  28. package/zzz_react_template/components/ui/carousel.tsx +239 -0
  29. package/zzz_react_template/components/ui/chart.tsx +355 -0
  30. package/zzz_react_template/components/ui/checkbox.tsx +30 -0
  31. package/zzz_react_template/components/ui/collapsible.tsx +31 -0
  32. package/zzz_react_template/components/ui/command.tsx +184 -0
  33. package/zzz_react_template/components/ui/context-menu.tsx +250 -0
  34. package/zzz_react_template/components/ui/dialog.tsx +209 -0
  35. package/zzz_react_template/components/ui/drawer.tsx +133 -0
  36. package/zzz_react_template/components/ui/dropdown-menu.tsx +255 -0
  37. package/zzz_react_template/components/ui/empty.tsx +104 -0
  38. package/zzz_react_template/components/ui/field.tsx +242 -0
  39. package/zzz_react_template/components/ui/form.tsx +168 -0
  40. package/zzz_react_template/components/ui/hover-card.tsx +42 -0
  41. package/zzz_react_template/components/ui/input-group.tsx +168 -0
  42. package/zzz_react_template/components/ui/input-otp.tsx +75 -0
  43. package/zzz_react_template/components/ui/input.tsx +70 -0
  44. package/zzz_react_template/components/ui/item.tsx +193 -0
  45. package/zzz_react_template/components/ui/kbd.tsx +28 -0
  46. package/zzz_react_template/components/ui/label.tsx +22 -0
  47. package/zzz_react_template/components/ui/menubar.tsx +274 -0
  48. package/zzz_react_template/components/ui/navigation-menu.tsx +168 -0
  49. package/zzz_react_template/components/ui/pagination.tsx +127 -0
  50. package/zzz_react_template/components/ui/popover.tsx +46 -0
  51. package/zzz_react_template/components/ui/progress.tsx +29 -0
  52. package/zzz_react_template/components/ui/radio-group.tsx +43 -0
  53. package/zzz_react_template/components/ui/resizable.tsx +54 -0
  54. package/zzz_react_template/components/ui/scroll-area.tsx +56 -0
  55. package/zzz_react_template/components/ui/select.tsx +185 -0
  56. package/zzz_react_template/components/ui/separator.tsx +26 -0
  57. package/zzz_react_template/components/ui/sheet.tsx +139 -0
  58. package/zzz_react_template/components/ui/sidebar.tsx +734 -0
  59. package/zzz_react_template/components/ui/skeleton.tsx +13 -0
  60. package/zzz_react_template/components/ui/slider.tsx +61 -0
  61. package/zzz_react_template/components/ui/sonner.tsx +23 -0
  62. package/zzz_react_template/components/ui/spinner.tsx +16 -0
  63. package/zzz_react_template/components/ui/switch.tsx +29 -0
  64. package/zzz_react_template/components/ui/table.tsx +114 -0
  65. package/zzz_react_template/components/ui/tabs.tsx +64 -0
  66. package/zzz_react_template/components/ui/textarea.tsx +67 -0
  67. package/zzz_react_template/components/ui/toggle-group.tsx +73 -0
  68. package/zzz_react_template/components/ui/toggle.tsx +45 -0
  69. package/zzz_react_template/components/ui/tooltip.tsx +59 -0
  70. package/zzz_react_template/const.ts +17 -0
  71. package/zzz_react_template/contexts/JXRContext.tsx +264 -0
  72. package/zzz_react_template/contexts/ThemeContext.tsx +64 -0
  73. package/zzz_react_template/hooks/useComposition.ts +81 -0
  74. package/zzz_react_template/hooks/useMobile.tsx +21 -0
  75. package/zzz_react_template/hooks/usePersistFn.ts +20 -0
  76. package/zzz_react_template/index.css +518 -11
  77. package/zzz_react_template/lib/jxr-runtime/index.ts +201 -0
  78. package/zzz_react_template/lib/jxr-runtime/module-resolver.ts +520 -0
  79. package/zzz_react_template/lib/jxr-runtime/moq-transport.ts +267 -0
  80. package/zzz_react_template/lib/jxr-runtime/web-crypto.ts +279 -0
  81. package/zzz_react_template/lib/jxr-runtime/worker-pool.ts +321 -0
  82. package/zzz_react_template/lib/utils.ts +6 -0
  83. package/zzz_react_template/main.tsx +4 -9
  84. package/zzz_react_template/pages/Docs.tsx +955 -0
  85. package/zzz_react_template/pages/Home.tsx +1080 -0
  86. package/zzz_react_template/pages/NotFound.tsx +105 -0
  87. package/zzz_react_template/tsconfig.json +24 -0
@@ -0,0 +1,155 @@
1
+ /**
2
+ * GOOGLE MAPS FRONTEND INTEGRATION - ESSENTIAL GUIDE
3
+ *
4
+ * USAGE FROM PARENT COMPONENT:
5
+ * ======
6
+ *
7
+ * const mapRef = useRef<google.maps.Map | null>(null);
8
+ *
9
+ * <MapView
10
+ * initialCenter={{ lat: 40.7128, lng: -74.0060 }}
11
+ * initialZoom={15}
12
+ * onMapReady={(map) => {
13
+ * mapRef.current = map; // Store to control map from parent anytime, google map itself is in charge of the re-rendering, not react state.
14
+ * </MapView>
15
+ *
16
+ * ======
17
+ * Available Libraries and Core Features:
18
+ * -------------------------------
19
+ * 📍 MARKER (from `marker` library)
20
+ * - Attaches to map using { map, position }
21
+ * new google.maps.marker.AdvancedMarkerElement({
22
+ * map,
23
+ * position: { lat: 37.7749, lng: -122.4194 },
24
+ * title: "San Francisco",
25
+ * });
26
+ *
27
+ * -------------------------------
28
+ * 🏢 PLACES (from `places` library)
29
+ * - Does not attach directly to map; use data with your map manually.
30
+ * const place = new google.maps.places.Place({ id: PLACE_ID });
31
+ * await place.fetchFields({ fields: ["displayName", "location"] });
32
+ * map.setCenter(place.location);
33
+ * new google.maps.marker.AdvancedMarkerElement({ map, position: place.location });
34
+ *
35
+ * -------------------------------
36
+ * 🧭 GEOCODER (from `geocoding` library)
37
+ * - Standalone service; manually apply results to map.
38
+ * const geocoder = new google.maps.Geocoder();
39
+ * geocoder.geocode({ address: "New York" }, (results, status) => {
40
+ * if (status === "OK" && results[0]) {
41
+ * map.setCenter(results[0].geometry.location);
42
+ * new google.maps.marker.AdvancedMarkerElement({
43
+ * map,
44
+ * position: results[0].geometry.location,
45
+ * });
46
+ * }
47
+ * });
48
+ *
49
+ * -------------------------------
50
+ * 📐 GEOMETRY (from `geometry` library)
51
+ * - Pure utility functions; not attached to map.
52
+ * const dist = google.maps.geometry.spherical.computeDistanceBetween(p1, p2);
53
+ *
54
+ * -------------------------------
55
+ * 🛣️ ROUTES (from `routes` library)
56
+ * - Combines DirectionsService (standalone) + DirectionsRenderer (map-attached)
57
+ * const directionsService = new google.maps.DirectionsService();
58
+ * const directionsRenderer = new google.maps.DirectionsRenderer({ map });
59
+ * directionsService.route(
60
+ * { origin, destination, travelMode: "DRIVING" },
61
+ * (res, status) => status === "OK" && directionsRenderer.setDirections(res)
62
+ * );
63
+ *
64
+ * -------------------------------
65
+ * 🌦️ MAP LAYERS (attach directly to map)
66
+ * - new google.maps.TrafficLayer().setMap(map);
67
+ * - new google.maps.TransitLayer().setMap(map);
68
+ * - new google.maps.BicyclingLayer().setMap(map);
69
+ *
70
+ * -------------------------------
71
+ * ✅ SUMMARY
72
+ * - “map-attached” → AdvancedMarkerElement, DirectionsRenderer, Layers.
73
+ * - “standalone” → Geocoder, DirectionsService, DistanceMatrixService, ElevationService.
74
+ * - “data-only” → Place, Geometry utilities.
75
+ */
76
+
77
+ /// <reference types="@types/google.maps" />
78
+
79
+ import { useEffect, useRef } from "react";
80
+ import { usePersistFn } from "@/hooks/usePersistFn";
81
+ import { cn } from "@/lib/utils";
82
+
83
+ declare global {
84
+ interface Window {
85
+ google?: typeof google;
86
+ }
87
+ }
88
+
89
+ const API_KEY = import.meta.env.VITE_FRONTEND_FORGE_API_KEY;
90
+ const FORGE_BASE_URL =
91
+ import.meta.env.VITE_FRONTEND_FORGE_API_URL ||
92
+ "https://forge.butterfly-effect.dev";
93
+ const MAPS_PROXY_URL = `${FORGE_BASE_URL}/v1/maps/proxy`;
94
+
95
+ function loadMapScript() {
96
+ return new Promise(resolve => {
97
+ const script = document.createElement("script");
98
+ script.src = `${MAPS_PROXY_URL}/maps/api/js?key=${API_KEY}&v=weekly&libraries=marker,places,geocoding,geometry`;
99
+ script.async = true;
100
+ script.crossOrigin = "anonymous";
101
+ script.onload = () => {
102
+ resolve(null);
103
+ script.remove(); // Clean up immediately
104
+ };
105
+ script.onerror = () => {
106
+ console.error("Failed to load Google Maps script");
107
+ };
108
+ document.head.appendChild(script);
109
+ });
110
+ }
111
+
112
+ interface MapViewProps {
113
+ className?: string;
114
+ initialCenter?: google.maps.LatLngLiteral;
115
+ initialZoom?: number;
116
+ onMapReady?: (map: google.maps.Map) => void;
117
+ }
118
+
119
+ export function MapView({
120
+ className,
121
+ initialCenter = { lat: 37.7749, lng: -122.4194 },
122
+ initialZoom = 12,
123
+ onMapReady,
124
+ }: MapViewProps) {
125
+ const mapContainer = useRef<HTMLDivElement>(null);
126
+ const map = useRef<google.maps.Map | null>(null);
127
+
128
+ const init = usePersistFn(async () => {
129
+ await loadMapScript();
130
+ if (!mapContainer.current) {
131
+ console.error("Map container not found");
132
+ return;
133
+ }
134
+ map.current = new window.google.maps.Map(mapContainer.current, {
135
+ zoom: initialZoom,
136
+ center: initialCenter,
137
+ mapTypeControl: true,
138
+ fullscreenControl: true,
139
+ zoomControl: true,
140
+ streetViewControl: true,
141
+ mapId: "DEMO_MAP_ID",
142
+ });
143
+ if (onMapReady) {
144
+ onMapReady(map.current);
145
+ }
146
+ });
147
+
148
+ useEffect(() => {
149
+ init();
150
+ }, [init]);
151
+
152
+ return (
153
+ <div ref={mapContainer} className={cn("w-full h-[500px]", className)} />
154
+ );
155
+ }
@@ -0,0 +1,313 @@
1
+ /**
2
+ * JXR.js — Code Editor Component
3
+ * Design: LavaFlow OS — Thermal Precision + Edge Command
4
+ * Lightweight code editor with syntax highlighting and tab management
5
+ */
6
+
7
+ import { useState, useCallback, useRef, useEffect } from 'react';
8
+ import { useJXR } from '@/contexts/JXRContext';
9
+ import { X, Save, FileCode2 } from 'lucide-react';
10
+ import { cn } from '@/lib/utils';
11
+ import { toast } from 'sonner';
12
+
13
+ // ─── Syntax highlighter ───────────────────────────────────────────────────────
14
+
15
+ function highlight(code: string, language: string): string {
16
+ if (!['tsx', 'ts', 'jsx', 'js'].includes(language)) {
17
+ return escapeHtml(code);
18
+ }
19
+
20
+ let result = escapeHtml(code);
21
+
22
+ // Keywords
23
+ result = result.replace(
24
+ /\b(import|export|from|default|const|let|var|function|return|if|else|for|while|class|extends|new|typeof|instanceof|async|await|try|catch|throw|interface|type|enum|implements|abstract|readonly|private|public|protected|static|override|declare|namespace|module|as|in|of|void|null|undefined|true|false|this|super)\b/g,
25
+ '<span class="syn-keyword">$1</span>'
26
+ );
27
+
28
+ // JSX tags
29
+ result = result.replace(
30
+ /(&lt;\/?[A-Z][A-Za-z0-9.]*)/g,
31
+ '<span class="syn-component">$1</span>'
32
+ );
33
+ result = result.replace(
34
+ /(&lt;\/?[a-z][a-z0-9-]*)/g,
35
+ '<span class="syn-tag">$1</span>'
36
+ );
37
+
38
+ // Strings
39
+ result = result.replace(
40
+ /(&quot;[^&]*&quot;|&#39;[^&]*&#39;|`[^`]*`)/g,
41
+ '<span class="syn-string">$1</span>'
42
+ );
43
+
44
+ // Comments
45
+ result = result.replace(
46
+ /(\/\/[^\n]*)/g,
47
+ '<span class="syn-comment">$1</span>'
48
+ );
49
+ result = result.replace(
50
+ /(\/\*[\s\S]*?\*\/)/g,
51
+ '<span class="syn-comment">$1</span>'
52
+ );
53
+
54
+ // Numbers
55
+ result = result.replace(
56
+ /\b(\d+\.?\d*)\b/g,
57
+ '<span class="syn-number">$1</span>'
58
+ );
59
+
60
+ // Function names
61
+ result = result.replace(
62
+ /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?=\()/g,
63
+ '<span class="syn-function">$1</span>'
64
+ );
65
+
66
+ // Types (PascalCase)
67
+ result = result.replace(
68
+ /\b([A-Z][A-Za-z0-9]*)\b/g,
69
+ '<span class="syn-type">$1</span>'
70
+ );
71
+
72
+ return result;
73
+ }
74
+
75
+ function escapeHtml(str: string): string {
76
+ return str
77
+ .replace(/&/g, '&amp;')
78
+ .replace(/</g, '&lt;')
79
+ .replace(/>/g, '&gt;')
80
+ .replace(/"/g, '&quot;')
81
+ .replace(/'/g, '&#39;');
82
+ }
83
+
84
+ // ─── Tab bar ──────────────────────────────────────────────────────────────────
85
+
86
+ function TabBar() {
87
+ const { openTabs, activeFile, openFile, closeFile } = useJXR();
88
+
89
+ return (
90
+ <div className="flex items-center overflow-x-auto border-b border-border bg-card scrollbar-thin">
91
+ {openTabs.map((tab) => {
92
+ const isActive = tab.path === activeFile?.path;
93
+ const name = tab.path.split('/').pop() ?? tab.path;
94
+ return (
95
+ <div
96
+ key={tab.path}
97
+ className={cn(
98
+ 'group flex items-center gap-1.5 px-3 py-2 border-r border-border cursor-pointer',
99
+ 'text-xs font-mono whitespace-nowrap transition-colors duration-100 shrink-0',
100
+ isActive
101
+ ? 'bg-background text-foreground border-t-2 border-t-lava'
102
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted/40'
103
+ )}
104
+ onClick={() => openFile(tab.path)}
105
+ >
106
+ <FileCode2 className="w-3 h-3 shrink-0" />
107
+ <span>{name}</span>
108
+ {tab.dirty && (
109
+ <span className="w-1.5 h-1.5 rounded-full bg-lava shrink-0" />
110
+ )}
111
+ <button
112
+ className={cn(
113
+ 'ml-0.5 p-0.5 rounded hover:bg-muted transition-opacity',
114
+ isActive ? 'opacity-60 hover:opacity-100' : 'opacity-0 group-hover:opacity-60 hover:!opacity-100'
115
+ )}
116
+ onClick={(e) => {
117
+ e.stopPropagation();
118
+ closeFile(tab.path);
119
+ }}
120
+ >
121
+ <X className="w-3 h-3" />
122
+ </button>
123
+ </div>
124
+ );
125
+ })}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ // ─── Editor ───────────────────────────────────────────────────────────────────
131
+
132
+ export function CodeEditor() {
133
+ const { activeFile, saveFile, openTabs } = useJXR();
134
+ const [content, setContent] = useState('');
135
+ const [isDirty, setIsDirty] = useState(false);
136
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
137
+ const highlightRef = useRef<HTMLDivElement>(null);
138
+ const saveTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
139
+
140
+ // Sync content when active file changes
141
+ useEffect(() => {
142
+ if (activeFile) {
143
+ setContent(activeFile.content);
144
+ setIsDirty(false);
145
+ }
146
+ }, [activeFile?.path]);
147
+
148
+ // Auto-save after 1.5s of inactivity
149
+ useEffect(() => {
150
+ if (!isDirty || !activeFile) return;
151
+ if (saveTimeout.current) clearTimeout(saveTimeout.current);
152
+ saveTimeout.current = setTimeout(() => {
153
+ saveFile(activeFile.path, content);
154
+ setIsDirty(false);
155
+ }, 1500);
156
+ return () => {
157
+ if (saveTimeout.current) clearTimeout(saveTimeout.current);
158
+ };
159
+ }, [content, isDirty, activeFile, saveFile]);
160
+
161
+ const handleChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
162
+ setContent(e.target.value);
163
+ setIsDirty(true);
164
+ }, []);
165
+
166
+ const handleKeyDown = useCallback(
167
+ (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
168
+ // Tab key → insert 2 spaces
169
+ if (e.key === 'Tab') {
170
+ e.preventDefault();
171
+ const start = e.currentTarget.selectionStart;
172
+ const end = e.currentTarget.selectionEnd;
173
+ const newContent = content.substring(0, start) + ' ' + content.substring(end);
174
+ setContent(newContent);
175
+ setIsDirty(true);
176
+ requestAnimationFrame(() => {
177
+ if (textareaRef.current) {
178
+ textareaRef.current.selectionStart = start + 2;
179
+ textareaRef.current.selectionEnd = start + 2;
180
+ }
181
+ });
182
+ }
183
+ // Ctrl/Cmd+S → save
184
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
185
+ e.preventDefault();
186
+ if (activeFile) {
187
+ saveFile(activeFile.path, content);
188
+ setIsDirty(false);
189
+ toast.success('Saved', { duration: 1000 });
190
+ }
191
+ }
192
+ },
193
+ [content, activeFile, saveFile]
194
+ );
195
+
196
+ // Sync scroll between textarea and highlight layer
197
+ const handleScroll = useCallback(() => {
198
+ if (highlightRef.current && textareaRef.current) {
199
+ highlightRef.current.scrollTop = textareaRef.current.scrollTop;
200
+ highlightRef.current.scrollLeft = textareaRef.current.scrollLeft;
201
+ }
202
+ }, []);
203
+
204
+ const lines = content.split('\n');
205
+ const highlighted = activeFile
206
+ ? highlight(content, activeFile.language)
207
+ : escapeHtml(content);
208
+
209
+ if (openTabs.length === 0) {
210
+ return (
211
+ <div className="flex flex-col h-full">
212
+ <div className="flex-1 flex items-center justify-center bg-background">
213
+ <div className="text-center space-y-3">
214
+ <FileCode2 className="w-12 h-12 text-muted-foreground/30 mx-auto" />
215
+ <p className="text-sm text-muted-foreground">Select a file to edit</p>
216
+ </div>
217
+ </div>
218
+ </div>
219
+ );
220
+ }
221
+
222
+ return (
223
+ <div className="flex flex-col h-full bg-background">
224
+ <TabBar />
225
+
226
+ {/* Editor area */}
227
+ <div className="flex-1 overflow-hidden relative">
228
+ {activeFile ? (
229
+ <div className="flex h-full">
230
+ {/* Line numbers */}
231
+ <div
232
+ className="select-none text-right pr-3 pl-3 py-4 text-xs font-mono text-muted-foreground/40 bg-card/50 border-r border-border overflow-hidden"
233
+ style={{ minWidth: '3rem', lineHeight: '1.6' }}
234
+ >
235
+ {lines.map((_, i) => (
236
+ <div key={i}>{i + 1}</div>
237
+ ))}
238
+ </div>
239
+
240
+ {/* Code area */}
241
+ <div className="flex-1 relative overflow-hidden">
242
+ {/* Syntax highlight layer */}
243
+ <div
244
+ ref={highlightRef}
245
+ className="absolute inset-0 px-4 py-4 text-xs font-mono overflow-auto pointer-events-none whitespace-pre"
246
+ style={{ lineHeight: '1.6', color: 'transparent' }}
247
+ dangerouslySetInnerHTML={{ __html: highlighted }}
248
+ />
249
+
250
+ {/* Actual textarea */}
251
+ <textarea
252
+ ref={textareaRef}
253
+ className={cn(
254
+ 'absolute inset-0 w-full h-full px-4 py-4',
255
+ 'text-xs font-mono bg-transparent text-foreground',
256
+ 'resize-none outline-none border-none',
257
+ 'caret-lava selection:bg-lava/30',
258
+ 'overflow-auto whitespace-pre'
259
+ )}
260
+ style={{ lineHeight: '1.6', caretColor: 'var(--lava)' }}
261
+ value={content}
262
+ onChange={handleChange}
263
+ onKeyDown={handleKeyDown}
264
+ onScroll={handleScroll}
265
+ spellCheck={false}
266
+ autoComplete="off"
267
+ autoCorrect="off"
268
+ autoCapitalize="off"
269
+ />
270
+ </div>
271
+ </div>
272
+ ) : (
273
+ <div className="flex items-center justify-center h-full">
274
+ <p className="text-sm text-muted-foreground">No file selected</p>
275
+ </div>
276
+ )}
277
+ </div>
278
+
279
+ {/* Status bar */}
280
+ {activeFile && (
281
+ <div className="flex items-center justify-between px-4 py-1 border-t border-border bg-card text-[10px] text-muted-foreground">
282
+ <div className="flex items-center gap-3">
283
+ <span className="font-mono">{activeFile.language.toUpperCase()}</span>
284
+ <span>{lines.length} lines</span>
285
+ <span>{(activeFile.size / 1024).toFixed(1)} KB</span>
286
+ </div>
287
+ <div className="flex items-center gap-2">
288
+ {isDirty && (
289
+ <span className="text-lava flex items-center gap-1">
290
+ <span className="w-1.5 h-1.5 rounded-full bg-lava inline-block" />
291
+ Unsaved
292
+ </span>
293
+ )}
294
+ <span>UTF-8</span>
295
+ <span>LF</span>
296
+ </div>
297
+ </div>
298
+ )}
299
+
300
+ {/* Syntax highlight CSS */}
301
+ <style>{`
302
+ .syn-keyword { color: #c792ea; }
303
+ .syn-component { color: #82aaff; }
304
+ .syn-tag { color: #f07178; }
305
+ .syn-string { color: #c3e88d; }
306
+ .syn-comment { color: #546e7a; font-style: italic; }
307
+ .syn-number { color: #f78c6c; }
308
+ .syn-function { color: #82aaff; }
309
+ .syn-type { color: #ffcb6b; }
310
+ `}</style>
311
+ </div>
312
+ );
313
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * JXR.js — File Explorer Component
3
+ * Design: LavaFlow OS — Thermal Precision + Edge Command
4
+ * Sidebar file tree with context menu, drag-and-drop ready
5
+ */
6
+
7
+ import { useState, useCallback } from 'react';
8
+ import { useJXR } from '@/contexts/JXRContext';
9
+ import type { VirtualFile, VirtualDirectory } from '@/lib/jxr-runtime';
10
+ import {
11
+ ChevronRight,
12
+ ChevronDown,
13
+ FileCode2,
14
+ FileJson,
15
+ FileText,
16
+ Folder,
17
+ FolderOpen,
18
+ Plus,
19
+ Trash2,
20
+ RefreshCw,
21
+ } from 'lucide-react';
22
+ import { cn } from '@/lib/utils';
23
+ import { toast } from 'sonner';
24
+
25
+ function isVirtualFile(node: VirtualFile | VirtualDirectory): node is VirtualFile {
26
+ return 'content' in node;
27
+ }
28
+
29
+ function getFileIcon(file: VirtualFile) {
30
+ const iconClass = 'w-3.5 h-3.5 shrink-0';
31
+ switch (file.language) {
32
+ case 'tsx':
33
+ case 'jsx':
34
+ return <FileCode2 className={cn(iconClass, 'text-cyan-400')} />;
35
+ case 'ts':
36
+ case 'js':
37
+ return <FileCode2 className={cn(iconClass, 'text-yellow-400')} />;
38
+ case 'json':
39
+ return <FileJson className={cn(iconClass, 'text-green-400')} />;
40
+ case 'css':
41
+ return <FileCode2 className={cn(iconClass, 'text-pink-400')} />;
42
+ default:
43
+ return <FileText className={cn(iconClass, 'text-muted-foreground')} />;
44
+ }
45
+ }
46
+
47
+ function getFileName(path: string): string {
48
+ return path.split('/').pop() ?? path;
49
+ }
50
+
51
+ interface TreeNodeProps {
52
+ node: VirtualFile | VirtualDirectory;
53
+ depth: number;
54
+ activePath: string | null;
55
+ onFileClick: (file: VirtualFile) => void;
56
+ onDelete: (path: string) => void;
57
+ }
58
+
59
+ function TreeNode({ node, depth, activePath, onFileClick, onDelete }: TreeNodeProps) {
60
+ const [expanded, setExpanded] = useState(true);
61
+ const indent = depth * 12;
62
+
63
+ if (isVirtualFile(node)) {
64
+ const isActive = node.path === activePath;
65
+ return (
66
+ <div
67
+ className={cn(
68
+ 'group flex items-center gap-1.5 px-2 py-[3px] cursor-pointer rounded-sm text-xs',
69
+ 'hover:bg-sidebar-accent/60 transition-colors duration-100',
70
+ isActive && 'bg-sidebar-accent text-lava border-l-2 border-lava'
71
+ )}
72
+ style={{ paddingLeft: `${indent + 8}px` }}
73
+ onClick={() => onFileClick(node)}
74
+ >
75
+ {getFileIcon(node)}
76
+ <span className={cn(
77
+ 'flex-1 truncate font-mono',
78
+ isActive ? 'text-lava' : 'text-sidebar-foreground/80'
79
+ )}>
80
+ {getFileName(node.path)}
81
+ </span>
82
+ <button
83
+ className="opacity-0 group-hover:opacity-100 p-0.5 hover:text-destructive transition-opacity"
84
+ onClick={(e) => {
85
+ e.stopPropagation();
86
+ onDelete(node.path);
87
+ }}
88
+ >
89
+ <Trash2 className="w-3 h-3" />
90
+ </button>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ // Directory node
96
+ return (
97
+ <div>
98
+ <div
99
+ className={cn(
100
+ 'flex items-center gap-1.5 px-2 py-[3px] cursor-pointer rounded-sm text-xs',
101
+ 'hover:bg-sidebar-accent/40 transition-colors duration-100'
102
+ )}
103
+ style={{ paddingLeft: `${indent + 4}px` }}
104
+ onClick={() => setExpanded((e) => !e)}
105
+ >
106
+ {expanded ? (
107
+ <ChevronDown className="w-3 h-3 text-muted-foreground shrink-0" />
108
+ ) : (
109
+ <ChevronRight className="w-3 h-3 text-muted-foreground shrink-0" />
110
+ )}
111
+ {expanded ? (
112
+ <FolderOpen className="w-3.5 h-3.5 text-lava shrink-0" />
113
+ ) : (
114
+ <Folder className="w-3.5 h-3.5 text-lava/70 shrink-0" />
115
+ )}
116
+ <span className="text-sidebar-foreground/90 font-medium">
117
+ {node.name}
118
+ </span>
119
+ </div>
120
+ {expanded && (
121
+ <div>
122
+ {node.children.map((child) => (
123
+ <TreeNode
124
+ key={isVirtualFile(child) ? child.path : child.path}
125
+ node={child}
126
+ depth={depth + 1}
127
+ activePath={activePath}
128
+ onFileClick={onFileClick}
129
+ onDelete={onDelete}
130
+ />
131
+ ))}
132
+ </div>
133
+ )}
134
+ </div>
135
+ );
136
+ }
137
+
138
+ export function FileExplorer() {
139
+ const { fileTree, openFile, deleteFile, createFile, activeFile } = useJXR();
140
+ const [newFileName, setNewFileName] = useState('');
141
+ const [showNewFile, setShowNewFile] = useState(false);
142
+
143
+ const handleFileClick = useCallback(
144
+ (file: VirtualFile) => {
145
+ openFile(file.path);
146
+ },
147
+ [openFile]
148
+ );
149
+
150
+ const handleDelete = useCallback(
151
+ (path: string) => {
152
+ deleteFile(path);
153
+ toast.success(`Deleted ${path.split('/').pop()}`);
154
+ },
155
+ [deleteFile]
156
+ );
157
+
158
+ const handleCreateFile = useCallback(() => {
159
+ if (!newFileName.trim()) return;
160
+ const path = `/src/${newFileName.trim()}`;
161
+ const ext = newFileName.split('.').pop() ?? 'ts';
162
+ const templates: Record<string, string> = {
163
+ tsx: `export default function ${newFileName.replace('.tsx', '')}() {\n return <div>Hello from ${newFileName}</div>;\n}\n`,
164
+ ts: `// ${newFileName}\nexport {};\n`,
165
+ css: `/* ${newFileName} */\n`,
166
+ json: `{}\n`,
167
+ };
168
+ createFile(path, templates[ext] ?? '');
169
+ setNewFileName('');
170
+ setShowNewFile(false);
171
+ }, [newFileName, createFile]);
172
+
173
+ return (
174
+ <div className="flex flex-col h-full bg-sidebar">
175
+ {/* Header */}
176
+ <div className="flex items-center justify-between px-3 py-2 border-b border-sidebar-border">
177
+ <span className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
178
+ Explorer
179
+ </span>
180
+ <div className="flex items-center gap-1">
181
+ <button
182
+ className="p-1 hover:text-lava transition-colors rounded"
183
+ onClick={() => setShowNewFile((v) => !v)}
184
+ title="New file"
185
+ >
186
+ <Plus className="w-3.5 h-3.5" />
187
+ </button>
188
+ <button
189
+ className="p-1 hover:text-lava transition-colors rounded"
190
+ title="Refresh"
191
+ >
192
+ <RefreshCw className="w-3.5 h-3.5" />
193
+ </button>
194
+ </div>
195
+ </div>
196
+
197
+ {/* New file input */}
198
+ {showNewFile && (
199
+ <div className="px-3 py-2 border-b border-sidebar-border">
200
+ <input
201
+ autoFocus
202
+ className="w-full bg-input text-xs font-mono px-2 py-1 rounded border border-border focus:border-lava outline-none"
203
+ placeholder="filename.tsx"
204
+ value={newFileName}
205
+ onChange={(e) => setNewFileName(e.target.value)}
206
+ onKeyDown={(e) => {
207
+ if (e.key === 'Enter') handleCreateFile();
208
+ if (e.key === 'Escape') setShowNewFile(false);
209
+ }}
210
+ />
211
+ </div>
212
+ )}
213
+
214
+ {/* Tree */}
215
+ <div className="flex-1 overflow-y-auto py-1 scrollbar-thin">
216
+ {fileTree ? (
217
+ <TreeNode
218
+ node={fileTree}
219
+ depth={0}
220
+ activePath={activeFile?.path ?? null}
221
+ onFileClick={handleFileClick}
222
+ onDelete={handleDelete}
223
+ />
224
+ ) : (
225
+ <div className="px-4 py-3 text-xs text-muted-foreground">Loading...</div>
226
+ )}
227
+ </div>
228
+ </div>
229
+ );
230
+ }