@react-trace-enhancer/plugin-preview 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,691 @@
1
+ import { IS_MAC, settingsPluginAtom, useDeactivateInspector, useProjectRoot, useSelectedSource, useWidgetPortalContainer } from "@react-trace-enhancer/core";
2
+ import { Button, CollapseIcon, Combobox, ExpandIcon, FolderIcon, IconButton, Kbd, KbdGroup, Popover, SaveIcon, ToolbarButton, Tooltip, panelPopupStyle } from "@react-trace-enhancer/ui-components";
3
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { useAtom } from "jotai";
6
+ import { bundledThemes, bundledThemesInfo, createHighlighter } from "shiki";
7
+ import Editor, { loader } from "@monaco-editor/react";
8
+ import { shikiToMonaco } from "@shikijs/monaco";
9
+
10
+ //#region src/FolderAccessPrompt.tsx
11
+ function FolderAccessPrompt({ root, onGrant, onCancel }) {
12
+ return /* @__PURE__ */ jsxs("div", {
13
+ style: {
14
+ display: "flex",
15
+ flexDirection: "column",
16
+ alignItems: "center",
17
+ justifyContent: "center",
18
+ gap: 12,
19
+ padding: "20px 16px",
20
+ height: "100%",
21
+ boxSizing: "border-box",
22
+ textAlign: "center",
23
+ fontFamily: "system-ui, sans-serif"
24
+ },
25
+ children: [
26
+ /* @__PURE__ */ jsx("span", {
27
+ style: { color: "#52525b" },
28
+ children: /* @__PURE__ */ jsx(FolderIcon, {})
29
+ }),
30
+ /* @__PURE__ */ jsxs("div", {
31
+ style: {
32
+ display: "flex",
33
+ flexDirection: "column",
34
+ gap: 12
35
+ },
36
+ children: [
37
+ /* @__PURE__ */ jsx("span", {
38
+ style: {
39
+ fontSize: 13,
40
+ fontWeight: 600,
41
+ color: "#fafafa"
42
+ },
43
+ children: "Folder access needed"
44
+ }),
45
+ /* @__PURE__ */ jsx("span", {
46
+ style: {
47
+ fontSize: 12,
48
+ color: "#71717a",
49
+ lineHeight: 1.5
50
+ },
51
+ children: root ? /* @__PURE__ */ jsxs("span", {
52
+ style: {
53
+ display: "flex",
54
+ flexDirection: "column",
55
+ gap: 8
56
+ },
57
+ children: [/* @__PURE__ */ jsx("span", { children: "The project root path will be copied to your clipboard. Navigate to it in the folder picker." }), IS_MAC && /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsxs(KbdGroup, { children: [
58
+ /* @__PURE__ */ jsx(Kbd, { children: "⌘" }),
59
+ /* @__PURE__ */ jsx(Kbd, { children: "⇧" }),
60
+ /* @__PURE__ */ jsx(Kbd, { children: "G" })
61
+ ] }), /* @__PURE__ */ jsx("span", { children: " to paste the path directly." })] })]
62
+ }) : "Grant access to your project folder to preview source files."
63
+ }),
64
+ root && /* @__PURE__ */ jsx("span", {
65
+ style: {
66
+ fontSize: 11,
67
+ fontFamily: "ui-monospace, monospace",
68
+ color: "#3b82f6",
69
+ wordBreak: "break-all"
70
+ },
71
+ children: root
72
+ })
73
+ ]
74
+ }),
75
+ /* @__PURE__ */ jsxs("div", {
76
+ style: {
77
+ display: "flex",
78
+ gap: 8
79
+ },
80
+ children: [/* @__PURE__ */ jsx(Button, {
81
+ variant: "secondary",
82
+ onClick: onCancel,
83
+ children: "Cancel"
84
+ }), /* @__PURE__ */ jsx(Button, {
85
+ variant: "primary",
86
+ onClick: onGrant,
87
+ children: "Grant access"
88
+ })]
89
+ })
90
+ ]
91
+ });
92
+ }
93
+ /**
94
+ * Copies the root path to clipboard then calls requestAccess().
95
+ */
96
+ async function handleGrantAccess(root, requestAccess) {
97
+ await navigator.clipboard.writeText(root).catch(() => {});
98
+ return requestAccess();
99
+ }
100
+
101
+ //#endregion
102
+ //#region src/fs.ts
103
+ const IDB_NAME = "react-trace";
104
+ const IDB_STORE = "handles";
105
+ const IDB_KEY = "root-directory";
106
+ function openDB() {
107
+ return new Promise((resolve, reject) => {
108
+ const req = indexedDB.open(IDB_NAME, 1);
109
+ req.onupgradeneeded = () => req.result.createObjectStore(IDB_STORE);
110
+ req.onsuccess = () => resolve(req.result);
111
+ req.onerror = () => reject(req.error);
112
+ });
113
+ }
114
+ async function saveHandle(handle) {
115
+ const db = await openDB();
116
+ return new Promise((resolve, reject) => {
117
+ const tx = db.transaction(IDB_STORE, "readwrite");
118
+ tx.objectStore(IDB_STORE).put(handle, IDB_KEY);
119
+ tx.oncomplete = () => resolve();
120
+ tx.onerror = () => reject(tx.error);
121
+ });
122
+ }
123
+ async function loadHandle() {
124
+ try {
125
+ const db = await openDB();
126
+ return new Promise((resolve, reject) => {
127
+ const req = db.transaction(IDB_STORE, "readonly").objectStore(IDB_STORE).get(IDB_KEY);
128
+ req.onsuccess = () => resolve(req.result ?? null);
129
+ req.onerror = () => reject(req.error);
130
+ });
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+ /**
136
+ * Traverses the directory handle tree to reach the target file.
137
+ * Returns null if any segment of the path doesn't exist.
138
+ */
139
+ async function getFileHandle(dir, relativePath, create = false) {
140
+ const parts = relativePath.split("/").filter(Boolean);
141
+ if (parts.length === 0) return null;
142
+ let current = dir;
143
+ for (let i = 0; i < parts.length - 1; i++) try {
144
+ current = await current.getDirectoryHandle(parts[i], { create });
145
+ } catch {
146
+ return null;
147
+ }
148
+ try {
149
+ return await current.getFileHandle(parts.at(-1), { create });
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+ var FileSystemServiceImpl = class {
155
+ _handle = null;
156
+ _listeners = /* @__PURE__ */ new Set();
157
+ get isSupported() {
158
+ return typeof window !== "undefined" && "showDirectoryPicker" in window;
159
+ }
160
+ get hasAccess() {
161
+ return this._handle !== null;
162
+ }
163
+ subscribe(listener) {
164
+ this._listeners.add(listener);
165
+ return () => this._listeners.delete(listener);
166
+ }
167
+ notify() {
168
+ this._listeners.forEach((l) => l());
169
+ }
170
+ async tryRestore() {
171
+ if (!this.isSupported) return false;
172
+ try {
173
+ const handle = await loadHandle();
174
+ if (!handle) return false;
175
+ if (await handle.requestPermission({ mode: "readwrite" }) === "granted") {
176
+ this._handle = handle;
177
+ this.notify();
178
+ return true;
179
+ }
180
+ } catch {}
181
+ return false;
182
+ }
183
+ async requestAccess() {
184
+ if (!this.isSupported) return false;
185
+ try {
186
+ const handle = await window.showDirectoryPicker({ mode: "readwrite" });
187
+ await saveHandle(handle);
188
+ this._handle = handle;
189
+ this.notify();
190
+ return true;
191
+ } catch {
192
+ return false;
193
+ }
194
+ }
195
+ /** Ensure we have access — try restore silently first, then prompt. */
196
+ async ensureAccess() {
197
+ if (this._handle) return true;
198
+ if (await this.tryRestore()) return true;
199
+ return this.requestAccess();
200
+ }
201
+ async read(relativePath) {
202
+ if (!await this.ensureAccess() || !this._handle) throw new Error("[react-trace] File system access denied");
203
+ const file = await getFileHandle(this._handle, relativePath);
204
+ if (!file) throw new Error(`[react-trace] File not found: ${relativePath}`);
205
+ return (await file.getFile()).text();
206
+ }
207
+ async write(relativePath, content) {
208
+ if (!await this.ensureAccess() || !this._handle) throw new Error("[react-trace] File system access denied");
209
+ const file = await getFileHandle(this._handle, relativePath, true);
210
+ if (!file) throw new Error(`[react-trace] Cannot open file for writing: ${relativePath}`);
211
+ const writable = await file.createWritable();
212
+ await writable.write(content);
213
+ await writable.close();
214
+ }
215
+ };
216
+ const fileSystemService = new FileSystemServiceImpl();
217
+
218
+ //#endregion
219
+ //#region src/store.ts
220
+ const previewSettingsAtom = settingsPluginAtom("preview");
221
+
222
+ //#endregion
223
+ //#region src/PreviewSettings.tsx
224
+ const themes = bundledThemesInfo.map((theme) => ({
225
+ value: theme.id,
226
+ label: theme.displayName
227
+ }));
228
+ const themesLookup = themes.reduce((acc, theme) => {
229
+ acc[theme.value] = theme;
230
+ return acc;
231
+ }, {});
232
+ const LABEL_STYLE = {
233
+ fontSize: 12,
234
+ color: "#d4d4d8",
235
+ fontFamily: "system-ui, sans-serif"
236
+ };
237
+ function PreviewSettings() {
238
+ const portalContainer = useWidgetPortalContainer();
239
+ const [themesOpen, setThemesOpen] = useState(false);
240
+ const [previewSettings = {
241
+ disabled: false,
242
+ theme: "one-dark-pro"
243
+ }, setPreviewSettings] = useAtom(previewSettingsAtom);
244
+ const anchorRef = useRef(null);
245
+ return /* @__PURE__ */ jsxs("div", {
246
+ style: {
247
+ display: "flex",
248
+ flexDirection: "column",
249
+ gap: 12
250
+ },
251
+ children: [/* @__PURE__ */ jsx("div", {
252
+ style: {
253
+ display: "flex",
254
+ flexDirection: "column",
255
+ gap: 6
256
+ },
257
+ children: /* @__PURE__ */ jsxs("label", {
258
+ style: LABEL_STYLE,
259
+ children: [
260
+ /* @__PURE__ */ jsx("input", {
261
+ type: "checkbox",
262
+ checked: previewSettings.disabled ?? false,
263
+ onChange: (e) => setPreviewSettings({
264
+ ...previewSettings,
265
+ disabled: e.target.checked
266
+ })
267
+ }),
268
+ " ",
269
+ "Code editing disabled"
270
+ ]
271
+ })
272
+ }), /* @__PURE__ */ jsxs("div", {
273
+ style: {
274
+ display: "flex",
275
+ flexDirection: "column",
276
+ gap: 6
277
+ },
278
+ children: [/* @__PURE__ */ jsx("label", {
279
+ style: LABEL_STYLE,
280
+ children: "Theme"
281
+ }), /* @__PURE__ */ jsxs(Combobox.Root, {
282
+ value: themesLookup[previewSettings.theme],
283
+ onValueChange: (theme) => {
284
+ if (theme) setPreviewSettings({
285
+ ...previewSettings,
286
+ theme: theme.value
287
+ });
288
+ },
289
+ items: themes,
290
+ open: themesOpen,
291
+ onOpenChange: setThemesOpen,
292
+ children: [/* @__PURE__ */ jsx("div", {
293
+ ref: anchorRef,
294
+ children: /* @__PURE__ */ jsx(Combobox.Trigger, {
295
+ onClick: (e) => e.stopPropagation(),
296
+ onKeyDown: (e) => e.stopPropagation(),
297
+ children: /* @__PURE__ */ jsx(Combobox.Input, {})
298
+ })
299
+ }), /* @__PURE__ */ jsx(Combobox.Portal, {
300
+ container: portalContainer,
301
+ children: /* @__PURE__ */ jsx(Combobox.Positioner, {
302
+ style: {
303
+ zIndex: 1e8,
304
+ pointerEvents: "auto"
305
+ },
306
+ anchor: anchorRef,
307
+ children: /* @__PURE__ */ jsxs(Combobox.Popup, { children: [/* @__PURE__ */ jsx(Combobox.Empty, { children: "No themes found" }), /* @__PURE__ */ jsx(Combobox.List, {
308
+ style: {
309
+ overscrollBehavior: "contain",
310
+ maxHeight: 500,
311
+ overflowY: "auto"
312
+ },
313
+ children: (option) => /* @__PURE__ */ jsx(Combobox.Item, {
314
+ value: option,
315
+ onClick: (e) => e.stopPropagation(),
316
+ children: /* @__PURE__ */ jsx("span", { children: option.label })
317
+ }, option.value)
318
+ })] })
319
+ })
320
+ })]
321
+ })]
322
+ })]
323
+ });
324
+ }
325
+
326
+ //#endregion
327
+ //#region src/highlight.ts
328
+ /** Injects the CSS animation for the highlighted source line — idempotent. */
329
+ function ensureHighlightStyle() {
330
+ if (document.getElementById("react-trace-highlighted-line")) return;
331
+ const style = document.createElement("style");
332
+ style.id = "react-trace-highlighted-line";
333
+ style.textContent = `
334
+ .react-trace-highlighted-line {
335
+ background-color: rgba(0, 200, 255, 0.25);
336
+ animation: react-trace-highlight-flash 1.2s ease-out;
337
+ }
338
+
339
+ @keyframes react-trace-highlight-flash {
340
+ from { background-color: rgba(0, 200, 255, 0.6); }
341
+ to { background-color: rgba(0, 200, 255, 0.25); }
342
+ }
343
+ `;
344
+ document.head.appendChild(style);
345
+ }
346
+
347
+ //#endregion
348
+ //#region src/monaco.ts
349
+ loader.init();
350
+ const langs = [
351
+ "typescript",
352
+ "javascript",
353
+ "graphql",
354
+ "css"
355
+ ];
356
+ const highlighterPromise = createHighlighter({
357
+ themes: Object.values(bundledThemes),
358
+ langs
359
+ });
360
+ /**
361
+ * Monaco `beforeMount` callback — disables the built-in TS/JS language server
362
+ * (no project types available in the browser) and wires Shiki for highlighting.
363
+ */
364
+ function configureBefore(monaco) {
365
+ const noValidation = {
366
+ noSemanticValidation: true,
367
+ noSyntaxValidation: true
368
+ };
369
+ const { typescriptDefaults, javascriptDefaults } = monaco.languages.typescript;
370
+ typescriptDefaults.setDiagnosticsOptions(noValidation);
371
+ javascriptDefaults.setDiagnosticsOptions(noValidation);
372
+ const compilerOptions = {
373
+ jsx: monaco.languages.typescript.JsxEmit.ReactJSX,
374
+ allowJs: true
375
+ };
376
+ typescriptDefaults.setCompilerOptions(compilerOptions);
377
+ javascriptDefaults.setCompilerOptions(compilerOptions);
378
+ for (const id of langs) monaco.languages.register({ id });
379
+ highlighterPromise.then((h) => shikiToMonaco(h, monaco));
380
+ }
381
+
382
+ //#endregion
383
+ //#region src/styles.ts
384
+ const LINE_HEIGHT = 19;
385
+ const INLINE_LINES = 12;
386
+ const INLINE_HEIGHT = INLINE_LINES * LINE_HEIGHT;
387
+ const EDITOR_WIDTH = 480;
388
+ const TOOLBAR_HEIGHT = 33;
389
+
390
+ //#endregion
391
+ //#region src/utils.ts
392
+ function detectLanguage(fileName) {
393
+ return {
394
+ ts: "typescript",
395
+ tsx: "typescript",
396
+ js: "javascript",
397
+ jsx: "javascript",
398
+ mjs: "javascript",
399
+ cjs: "javascript"
400
+ }[fileName.split(".").pop()?.toLowerCase() ?? ""] ?? "plaintext";
401
+ }
402
+ /** Convert a file path (URL or absolute) to a monaco file:// URI. */
403
+ function pathToUri(monaco, path) {
404
+ try {
405
+ const { pathname } = new URL(path);
406
+ return monaco.Uri.parse(`file://${pathname}`);
407
+ } catch {
408
+ const normalized = path.replace(/\\/g, "/");
409
+ return monaco.Uri.parse(normalized.startsWith("/") ? `file://${normalized}` : `file:///${normalized}`);
410
+ }
411
+ }
412
+
413
+ //#endregion
414
+ //#region src/SourcePreview.tsx
415
+ function SourcePreview({ options }) {
416
+ let { theme = "one-dark-pro", disabled = false } = options;
417
+ const [persistedOptions, setPreviewSettings] = useAtom(previewSettingsAtom);
418
+ if (!persistedOptions) setPreviewSettings({
419
+ theme,
420
+ disabled
421
+ });
422
+ theme = persistedOptions?.theme || theme;
423
+ disabled = persistedOptions?.disabled ?? disabled;
424
+ const root = useProjectRoot();
425
+ const source = useSelectedSource();
426
+ const [expanded, setExpanded] = useState(false);
427
+ const [dirty, setDirty] = useState(false);
428
+ const [loadState, setLoadState] = useState(fileSystemService.hasAccess ? "loading" : "needs-access");
429
+ const editorRef = useRef(null);
430
+ if (!source) return null;
431
+ const currentPath = source.relativePath;
432
+ const handleGrant = async () => {
433
+ if (await handleGrantAccess(root, () => fileSystemService.requestAccess())) setLoadState("loading");
434
+ };
435
+ const handleSave = () => {
436
+ const val = editorRef.current?.getValue();
437
+ if (val != null) fileSystemService.write(currentPath, val).then(() => setDirty(false)).catch(() => {});
438
+ };
439
+ const editorHeight = expanded ? `calc(80vh - ${!disabled ? TOOLBAR_HEIGHT : 0}px)` : INLINE_HEIGHT - (!disabled ? TOOLBAR_HEIGHT : 0);
440
+ const toolbar = !disabled ? /* @__PURE__ */ jsxs("div", {
441
+ style: {
442
+ display: "flex",
443
+ alignItems: "center",
444
+ justifyContent: "space-between",
445
+ height: TOOLBAR_HEIGHT,
446
+ padding: "0 10px",
447
+ borderBottom: "1px solid #27272a",
448
+ flexShrink: 0
449
+ },
450
+ children: [/* @__PURE__ */ jsxs("span", {
451
+ style: {
452
+ fontSize: 11,
453
+ fontFamily: "ui-monospace, monospace",
454
+ color: "#52525b"
455
+ },
456
+ children: [source.relativePath.split("/").slice(-2).join("/"), /* @__PURE__ */ jsxs("span", {
457
+ style: { color: "#3f3f46" },
458
+ children: [":", source.lineNumber]
459
+ })]
460
+ }), /* @__PURE__ */ jsxs("div", {
461
+ style: {
462
+ display: "flex",
463
+ alignItems: "center",
464
+ gap: 4
465
+ },
466
+ children: [dirty && /* @__PURE__ */ jsxs(Button, {
467
+ variant: "accent",
468
+ onClick: handleSave,
469
+ title: "Save (⌘S)",
470
+ children: [/* @__PURE__ */ jsx(SaveIcon, {}), " Save"]
471
+ }), /* @__PURE__ */ jsx(IconButton, {
472
+ onClick: () => setExpanded((prev) => !prev),
473
+ title: expanded ? "Collapse (Esc)" : "Expand",
474
+ children: expanded ? /* @__PURE__ */ jsx(CollapseIcon, {}) : /* @__PURE__ */ jsx(ExpandIcon, {})
475
+ })]
476
+ })]
477
+ }) : null;
478
+ const [dismissed, setDismissed] = useState(false);
479
+ const panel = /* @__PURE__ */ jsxs("div", {
480
+ style: {
481
+ display: "flex",
482
+ flexDirection: "column",
483
+ width: "100%",
484
+ height: "100%",
485
+ background: "#18181b",
486
+ overflow: "hidden"
487
+ },
488
+ onKeyDown: (e) => e.stopPropagation(),
489
+ children: [toolbar, loadState === "needs-access" && !dismissed ? /* @__PURE__ */ jsx(FolderAccessPrompt, {
490
+ root,
491
+ onGrant: handleGrant,
492
+ onCancel: () => setDismissed(true)
493
+ }) : /* @__PURE__ */ jsx(Editor, {
494
+ height: editorHeight,
495
+ width: "100%",
496
+ theme,
497
+ options: {
498
+ readOnly: disabled,
499
+ minimap: { enabled: false },
500
+ lineNumbers: "on",
501
+ scrollBeyondLastLine: false,
502
+ fontSize: 12,
503
+ lineHeight: LINE_HEIGHT,
504
+ folding: false,
505
+ glyphMargin: false,
506
+ automaticLayout: true,
507
+ padding: {
508
+ top: 6,
509
+ bottom: 6
510
+ },
511
+ fixedOverflowWidgets: true
512
+ },
513
+ beforeMount: configureBefore,
514
+ onMount: (editor, monaco) => {
515
+ editorRef.current = editor;
516
+ if (!disabled) {
517
+ editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, handleSave);
518
+ editor.addCommand(monaco.KeyCode.Escape, () => setExpanded(false));
519
+ }
520
+ editor.updateOptions({ theme });
521
+ const key = source.relativePath;
522
+ const uri = pathToUri(monaco, key);
523
+ fileSystemService.read(key).then((fileContent) => {
524
+ if (!monaco.editor.getModel(uri)) monaco.editor.createModel(fileContent, detectLanguage(key), uri);
525
+ const model = monaco.editor.getModel(uri);
526
+ if (model) {
527
+ const { lineNumber } = source;
528
+ editor.setModel(model);
529
+ editor.revealLineInCenter(lineNumber);
530
+ ensureHighlightStyle();
531
+ editor.createDecorationsCollection().set([{
532
+ range: new monaco.Range(lineNumber, 1, lineNumber, 1),
533
+ options: {
534
+ isWholeLine: true,
535
+ className: "react-trace-highlighted-line"
536
+ }
537
+ }]);
538
+ }
539
+ }).catch(() => {});
540
+ },
541
+ onChange: () => setDirty(true),
542
+ loading: /* @__PURE__ */ jsx("div", {
543
+ style: {
544
+ padding: "8px 12px",
545
+ color: "#52525b",
546
+ fontSize: 11
547
+ },
548
+ children: "Loading…"
549
+ })
550
+ })]
551
+ });
552
+ if (!expanded) return /* @__PURE__ */ jsx("div", {
553
+ onClick: (e) => e.stopPropagation(),
554
+ style: { width: EDITOR_WIDTH },
555
+ children: panel
556
+ });
557
+ return /* @__PURE__ */ jsx("div", {
558
+ ref: (el) => {
559
+ const parentElement = el?.parentElement?.parentElement;
560
+ if (!parentElement) return;
561
+ const transform = parentElement.style.transform;
562
+ parentElement.style.transform = "";
563
+ return () => {
564
+ parentElement.style.transform = transform;
565
+ };
566
+ },
567
+ style: {
568
+ position: "absolute",
569
+ inset: 0,
570
+ zIndex: 9999999,
571
+ display: "flex",
572
+ alignItems: "center",
573
+ justifyContent: "center"
574
+ },
575
+ onClick: () => setExpanded(false),
576
+ children: /* @__PURE__ */ jsx("div", {
577
+ style: {
578
+ ...panelPopupStyle,
579
+ position: "fixed",
580
+ top: "10vw",
581
+ left: "10vw",
582
+ overflow: "hidden",
583
+ width: "80vw",
584
+ height: "80vh"
585
+ },
586
+ onClick: (e) => e.stopPropagation(),
587
+ children: panel
588
+ })
589
+ });
590
+ }
591
+
592
+ //#endregion
593
+ //#region src/index.tsx
594
+ function FolderToolbarIcon({ hasAccess }) {
595
+ return /* @__PURE__ */ jsxs("span", {
596
+ style: {
597
+ position: "relative",
598
+ display: "inline-flex"
599
+ },
600
+ children: [/* @__PURE__ */ jsx(FolderIcon, {}), hasAccess && /* @__PURE__ */ jsx("span", { style: {
601
+ position: "absolute",
602
+ top: -4,
603
+ right: -4,
604
+ width: 6,
605
+ height: 6,
606
+ borderRadius: "50%",
607
+ background: "#22c55e",
608
+ border: "1.5px solid #18181b",
609
+ pointerEvents: "none"
610
+ } })]
611
+ });
612
+ }
613
+ function PreviewToolbar() {
614
+ const projectRoot = useProjectRoot();
615
+ const portalContainer = useWidgetPortalContainer();
616
+ const deactivateInspector = useDeactivateInspector();
617
+ const buttonRef = useRef(null);
618
+ const [isPromptOpen, setIsPromptOpen] = useState(false);
619
+ const hasAccess = useSyncExternalStore(fileSystemService.subscribe.bind(fileSystemService), () => fileSystemService.hasAccess, () => false);
620
+ useEffect(() => {
621
+ fileSystemService.tryRestore().catch(() => {});
622
+ }, []);
623
+ const handleClick = () => {
624
+ deactivateInspector();
625
+ if (fileSystemService.hasAccess) {
626
+ fileSystemService.requestAccess().catch(() => {});
627
+ return;
628
+ }
629
+ setIsPromptOpen(true);
630
+ };
631
+ const handleGrant = async () => {
632
+ if (await handleGrantAccess(projectRoot, () => fileSystemService.requestAccess())) setIsPromptOpen(false);
633
+ };
634
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Tooltip, {
635
+ label: hasAccess ? "Project folder connected" : "Select project folder",
636
+ container: portalContainer,
637
+ render: /* @__PURE__ */ jsx(ToolbarButton, { ref: buttonRef }),
638
+ "aria-label": "Project folder",
639
+ style: { color: hasAccess ? "#22c55e" : "#52525b" },
640
+ onClick: handleClick,
641
+ children: /* @__PURE__ */ jsx(FolderToolbarIcon, { hasAccess })
642
+ }), /* @__PURE__ */ jsx(Popover.Root, {
643
+ open: isPromptOpen,
644
+ onOpenChange: (open) => {
645
+ if (!open) setIsPromptOpen(false);
646
+ },
647
+ children: /* @__PURE__ */ jsx(Popover.Portal, {
648
+ container: portalContainer,
649
+ children: /* @__PURE__ */ jsx(Popover.Positioner, {
650
+ anchor: buttonRef.current,
651
+ side: "top",
652
+ align: "end",
653
+ sideOffset: 8,
654
+ collisionPadding: 8,
655
+ positionMethod: "fixed",
656
+ style: {
657
+ zIndex: 9999999,
658
+ pointerEvents: "auto"
659
+ },
660
+ children: /* @__PURE__ */ jsx(Popover.Popup, {
661
+ style: {
662
+ width: 280,
663
+ overflow: "hidden"
664
+ },
665
+ onClick: (event) => event.stopPropagation(),
666
+ onKeyDown: (event) => event.stopPropagation(),
667
+ children: /* @__PURE__ */ jsx(FolderAccessPrompt, {
668
+ root: projectRoot,
669
+ onGrant: handleGrant,
670
+ onCancel: () => setIsPromptOpen(false)
671
+ })
672
+ })
673
+ })
674
+ })
675
+ })] });
676
+ }
677
+ function PreviewPlugin(options = {}) {
678
+ function PreviewActionPanel() {
679
+ return /* @__PURE__ */ jsx(SourcePreview, { options });
680
+ }
681
+ return {
682
+ name: "preview",
683
+ toolbar: PreviewToolbar,
684
+ actionPanel: PreviewActionPanel,
685
+ settings: PreviewSettings
686
+ };
687
+ }
688
+
689
+ //#endregion
690
+ export { PreviewPlugin };
691
+ //# sourceMappingURL=index.js.map