@react-spot/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/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @react-spot/plugin-preview
2
+
3
+ Monaco-based source preview plugin for `@react-spot/core`.
4
+
5
+ This package adds a toolbar control for connecting the current project folder and an action-panel preview for the selected source file.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pnpm add --dev @react-spot/core @react-spot/ui-components @react-spot/plugin-preview
11
+ ```
12
+
13
+ If you are already using `@react-spot/kit`, this plugin is included there by default.
14
+
15
+ ## Usage
16
+
17
+ ```tsx
18
+ import { Trace } from '@react-spot/core'
19
+ import { PreviewPlugin } from '@react-spot/plugin-preview'
20
+
21
+ import App from './App'
22
+
23
+ export function AppWithTrace() {
24
+ return (
25
+ <>
26
+ <App />
27
+ <Trace
28
+ root={import.meta.env.VITE_ROOT}
29
+ plugins={[PreviewPlugin({ theme: 'one-dark-pro' })]}
30
+ />
31
+ </>
32
+ )
33
+ }
34
+ ```
35
+
36
+ `root` should be the absolute project root passed to Trace so the plugin can resolve relative file paths for comments.
37
+
38
+ ## What it adds
39
+
40
+ - A toolbar button for project-folder access.
41
+ - An action-panel source preview for the currently selected component source.
42
+ - An inline Monaco editor with syntax highlighting.
43
+ - Optional save and expand controls when editing is enabled.
44
+ - A plugin settings UI for preview theme and editing mode.
45
+
46
+ ## Folder access expectations
47
+
48
+ The preview plugin reads files from the project through Trace's file-system service, so users must grant folder access before the preview can load file contents.
49
+
50
+ - Pass the absolute project root to `<Trace root="..." />`.
51
+ - When prompted, select the same project folder that contains the source files Trace resolves.
52
+ - If a root path is available, the plugin copies that path to the clipboard to make the folder picker easier to use.
53
+ - Until access is granted, the toolbar button and action panel show the access flow instead of a live file preview.
54
+ - When editing is enabled, saves are written back through the same file-system access.
55
+
56
+ If you only want inspection without file writes, start the plugin in read-only mode with `disabled: true`.
57
+
58
+ ## Options
59
+
60
+ `PreviewPlugin(options?: PreviewPluginOptions)`
61
+
62
+ ```ts
63
+ interface PreviewPluginOptions {
64
+ disabled?: boolean
65
+ theme?: BundledTheme
66
+ }
67
+ ```
68
+
69
+ - `disabled` — defaults to `false`. Starts the preview in read-only mode and removes editing controls.
70
+ - `theme` — defaults to `'one-dark-pro'`. Accepts any bundled Shiki theme id.
71
+
72
+ ## Settings
73
+
74
+ The plugin also contributes a settings panel inside Trace where users can:
75
+
76
+ - toggle code editing on or off
77
+ - switch the preview theme
78
+
79
+ These controls let users adjust preview behavior without changing the plugin registration code.
package/dist/index.cjs ADDED
@@ -0,0 +1,420 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let _react_spot_core = require("@react-spot/core");
3
+ let _react_spot_ui_components = require("@react-spot/ui-components");
4
+ let react = require("react");
5
+ let react_jsx_runtime = require("react/jsx-runtime");
6
+ let jotai = require("jotai");
7
+ let shiki = require("shiki");
8
+
9
+ //#region src/FolderAccessPrompt.tsx
10
+ function FolderAccessPrompt({ root, onGrant, onCancel }) {
11
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
12
+ style: {
13
+ display: "flex",
14
+ flexDirection: "column",
15
+ alignItems: "center",
16
+ justifyContent: "center",
17
+ gap: 12,
18
+ padding: "20px 16px",
19
+ height: "100%",
20
+ boxSizing: "border-box",
21
+ textAlign: "center",
22
+ fontFamily: "system-ui, sans-serif"
23
+ },
24
+ children: [
25
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
26
+ style: { color: "#52525b" },
27
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.FolderIcon, {})
28
+ }),
29
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
30
+ style: {
31
+ display: "flex",
32
+ flexDirection: "column",
33
+ gap: 12
34
+ },
35
+ children: [
36
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
37
+ style: {
38
+ fontSize: 13,
39
+ fontWeight: 600,
40
+ color: "#fafafa"
41
+ },
42
+ children: "Folder access needed"
43
+ }),
44
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
45
+ style: {
46
+ fontSize: 12,
47
+ color: "#71717a",
48
+ lineHeight: 1.5
49
+ },
50
+ children: root ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
51
+ style: {
52
+ display: "flex",
53
+ flexDirection: "column",
54
+ gap: 8
55
+ },
56
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "The project root path will be copied to your clipboard. Navigate to it in the folder picker." }), _react_spot_core.IS_MAC && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_react_spot_ui_components.KbdGroup, { children: [
57
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Kbd, { children: "⌘" }),
58
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Kbd, { children: "⇧" }),
59
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Kbd, { children: "G" })
60
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: " to paste the path directly." })] })]
61
+ }) : "Grant access to your project folder to preview source files."
62
+ }),
63
+ root && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
64
+ style: {
65
+ fontSize: 11,
66
+ fontFamily: "ui-monospace, monospace",
67
+ color: "#3b82f6",
68
+ wordBreak: "break-all"
69
+ },
70
+ children: root
71
+ })
72
+ ]
73
+ }),
74
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
75
+ style: {
76
+ display: "flex",
77
+ gap: 8
78
+ },
79
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Button, {
80
+ variant: "secondary",
81
+ onClick: onCancel,
82
+ children: "Cancel"
83
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Button, {
84
+ variant: "primary",
85
+ onClick: onGrant,
86
+ children: "Grant access"
87
+ })]
88
+ })
89
+ ]
90
+ });
91
+ }
92
+ /**
93
+ * Copies the root path to clipboard then calls requestAccess().
94
+ */
95
+ async function handleGrantAccess(root, requestAccess) {
96
+ await navigator.clipboard.writeText(root).catch(() => {});
97
+ return requestAccess();
98
+ }
99
+
100
+ //#endregion
101
+ //#region src/fs.ts
102
+ const IDB_NAME = "react-trace";
103
+ const IDB_STORE = "handles";
104
+ const IDB_KEY = "root-directory";
105
+ function openDB() {
106
+ return new Promise((resolve, reject) => {
107
+ const req = indexedDB.open(IDB_NAME, 1);
108
+ req.onupgradeneeded = () => req.result.createObjectStore(IDB_STORE);
109
+ req.onsuccess = () => resolve(req.result);
110
+ req.onerror = () => reject(req.error);
111
+ });
112
+ }
113
+ async function saveHandle(handle) {
114
+ const db = await openDB();
115
+ return new Promise((resolve, reject) => {
116
+ const tx = db.transaction(IDB_STORE, "readwrite");
117
+ tx.objectStore(IDB_STORE).put(handle, IDB_KEY);
118
+ tx.oncomplete = () => resolve();
119
+ tx.onerror = () => reject(tx.error);
120
+ });
121
+ }
122
+ async function loadHandle() {
123
+ try {
124
+ const db = await openDB();
125
+ return new Promise((resolve, reject) => {
126
+ const req = db.transaction(IDB_STORE, "readonly").objectStore(IDB_STORE).get(IDB_KEY);
127
+ req.onsuccess = () => resolve(req.result ?? null);
128
+ req.onerror = () => reject(req.error);
129
+ });
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+ /**
135
+ * Traverses the directory handle tree to reach the target file.
136
+ * Returns null if any segment of the path doesn't exist.
137
+ */
138
+ async function getFileHandle(dir, relativePath, create = false) {
139
+ const parts = relativePath.split("/").filter(Boolean);
140
+ if (parts.length === 0) return null;
141
+ let current = dir;
142
+ for (let i = 0; i < parts.length - 1; i++) try {
143
+ current = await current.getDirectoryHandle(parts[i], { create });
144
+ } catch {
145
+ return null;
146
+ }
147
+ try {
148
+ return await current.getFileHandle(parts.at(-1), { create });
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+ var FileSystemServiceImpl = class {
154
+ _handle = null;
155
+ _listeners = /* @__PURE__ */ new Set();
156
+ get isSupported() {
157
+ return typeof window !== "undefined" && "showDirectoryPicker" in window;
158
+ }
159
+ get hasAccess() {
160
+ return this._handle !== null;
161
+ }
162
+ subscribe(listener) {
163
+ this._listeners.add(listener);
164
+ return () => this._listeners.delete(listener);
165
+ }
166
+ notify() {
167
+ this._listeners.forEach((l) => l());
168
+ }
169
+ async tryRestore() {
170
+ if (!this.isSupported) return false;
171
+ try {
172
+ const handle = await loadHandle();
173
+ if (!handle) return false;
174
+ if (await handle.requestPermission({ mode: "readwrite" }) === "granted") {
175
+ this._handle = handle;
176
+ this.notify();
177
+ return true;
178
+ }
179
+ } catch {}
180
+ return false;
181
+ }
182
+ async requestAccess() {
183
+ if (!this.isSupported) return false;
184
+ try {
185
+ const handle = await window.showDirectoryPicker({ mode: "readwrite" });
186
+ await saveHandle(handle);
187
+ this._handle = handle;
188
+ this.notify();
189
+ return true;
190
+ } catch {
191
+ return false;
192
+ }
193
+ }
194
+ /** Ensure we have access — try restore silently first, then prompt. */
195
+ async ensureAccess() {
196
+ if (this._handle) return true;
197
+ if (await this.tryRestore()) return true;
198
+ return this.requestAccess();
199
+ }
200
+ async read(relativePath) {
201
+ if (!await this.ensureAccess() || !this._handle) throw new Error("[react-trace] File system access denied");
202
+ const file = await getFileHandle(this._handle, relativePath);
203
+ if (!file) throw new Error(`[react-trace] File not found: ${relativePath}`);
204
+ return (await file.getFile()).text();
205
+ }
206
+ async write(relativePath, content) {
207
+ if (!await this.ensureAccess() || !this._handle) throw new Error("[react-trace] File system access denied");
208
+ const file = await getFileHandle(this._handle, relativePath, true);
209
+ if (!file) throw new Error(`[react-trace] Cannot open file for writing: ${relativePath}`);
210
+ const writable = await file.createWritable();
211
+ await writable.write(content);
212
+ await writable.close();
213
+ }
214
+ };
215
+ const fileSystemService = new FileSystemServiceImpl();
216
+
217
+ //#endregion
218
+ //#region src/store.ts
219
+ const previewSettingsAtom = (0, _react_spot_core.settingsPluginAtom)("preview");
220
+
221
+ //#endregion
222
+ //#region src/PreviewSettings.tsx
223
+ const themes = shiki.bundledThemesInfo.map((theme) => ({
224
+ value: theme.id,
225
+ label: theme.displayName
226
+ }));
227
+ const themesLookup = themes.reduce((acc, theme) => {
228
+ acc[theme.value] = theme;
229
+ return acc;
230
+ }, {});
231
+ const LABEL_STYLE = {
232
+ fontSize: 12,
233
+ color: "#d4d4d8",
234
+ fontFamily: "system-ui, sans-serif"
235
+ };
236
+ function PreviewSettings() {
237
+ const portalContainer = (0, _react_spot_core.useWidgetPortalContainer)();
238
+ const [themesOpen, setThemesOpen] = (0, react.useState)(false);
239
+ const [previewSettings = {
240
+ disabled: false,
241
+ theme: "one-dark-pro"
242
+ }, setPreviewSettings] = (0, jotai.useAtom)(previewSettingsAtom);
243
+ const anchorRef = (0, react.useRef)(null);
244
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
245
+ style: {
246
+ display: "flex",
247
+ flexDirection: "column",
248
+ gap: 12
249
+ },
250
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
251
+ style: {
252
+ display: "flex",
253
+ flexDirection: "column",
254
+ gap: 6
255
+ },
256
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
257
+ style: LABEL_STYLE,
258
+ children: [
259
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
260
+ type: "checkbox",
261
+ checked: previewSettings.disabled ?? false,
262
+ onChange: (e) => setPreviewSettings({
263
+ ...previewSettings,
264
+ disabled: e.target.checked
265
+ })
266
+ }),
267
+ " ",
268
+ "Code editing disabled"
269
+ ]
270
+ })
271
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
272
+ style: {
273
+ display: "flex",
274
+ flexDirection: "column",
275
+ gap: 6
276
+ },
277
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
278
+ style: LABEL_STYLE,
279
+ children: "Theme"
280
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_react_spot_ui_components.Combobox.Root, {
281
+ value: themesLookup[previewSettings.theme],
282
+ onValueChange: (theme) => {
283
+ if (theme) setPreviewSettings({
284
+ ...previewSettings,
285
+ theme: theme.value
286
+ });
287
+ },
288
+ items: themes,
289
+ open: themesOpen,
290
+ onOpenChange: setThemesOpen,
291
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
292
+ ref: anchorRef,
293
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Combobox.Trigger, {
294
+ onClick: (e) => e.stopPropagation(),
295
+ onKeyDown: (e) => e.stopPropagation(),
296
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Combobox.Input, {})
297
+ })
298
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Combobox.Portal, {
299
+ container: portalContainer,
300
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Combobox.Positioner, {
301
+ style: {
302
+ zIndex: 1e8,
303
+ pointerEvents: "auto"
304
+ },
305
+ anchor: anchorRef,
306
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_react_spot_ui_components.Combobox.Popup, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Combobox.Empty, { children: "No themes found" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Combobox.List, {
307
+ style: {
308
+ overscrollBehavior: "contain",
309
+ maxHeight: 500,
310
+ overflowY: "auto"
311
+ },
312
+ children: (option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Combobox.Item, {
313
+ value: option,
314
+ onClick: (e) => e.stopPropagation(),
315
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: option.label })
316
+ }, option.value)
317
+ })] })
318
+ })
319
+ })]
320
+ })]
321
+ })]
322
+ });
323
+ }
324
+
325
+ //#endregion
326
+ //#region src/index.tsx
327
+ function FolderToolbarIcon({ hasAccess }) {
328
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
329
+ style: {
330
+ position: "relative",
331
+ display: "inline-flex"
332
+ },
333
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.FolderIcon, {}), hasAccess && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
334
+ position: "absolute",
335
+ top: -4,
336
+ right: -4,
337
+ width: 6,
338
+ height: 6,
339
+ borderRadius: "50%",
340
+ background: "#22c55e",
341
+ border: "1.5px solid #18181b",
342
+ pointerEvents: "none"
343
+ } })]
344
+ });
345
+ }
346
+ function PreviewToolbar() {
347
+ const projectRoot = (0, _react_spot_core.useProjectRoot)();
348
+ const portalContainer = (0, _react_spot_core.useWidgetPortalContainer)();
349
+ const deactivateInspector = (0, _react_spot_core.useDeactivateInspector)();
350
+ const buttonRef = (0, react.useRef)(null);
351
+ const [isPromptOpen, setIsPromptOpen] = (0, react.useState)(false);
352
+ const hasAccess = (0, react.useSyncExternalStore)(fileSystemService.subscribe.bind(fileSystemService), () => fileSystemService.hasAccess, () => false);
353
+ (0, react.useEffect)(() => {
354
+ fileSystemService.tryRestore().catch(() => {});
355
+ }, []);
356
+ const handleClick = () => {
357
+ deactivateInspector();
358
+ if (fileSystemService.hasAccess) {
359
+ fileSystemService.requestAccess().catch(() => {});
360
+ return;
361
+ }
362
+ setIsPromptOpen(true);
363
+ };
364
+ const handleGrant = async () => {
365
+ if (await handleGrantAccess(projectRoot, () => fileSystemService.requestAccess())) setIsPromptOpen(false);
366
+ };
367
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Tooltip, {
368
+ label: hasAccess ? "Project folder connected" : "Select project folder",
369
+ container: portalContainer,
370
+ render: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.ToolbarButton, { ref: buttonRef }),
371
+ "aria-label": "Project folder",
372
+ style: { color: hasAccess ? "#22c55e" : "#52525b" },
373
+ onClick: handleClick,
374
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FolderToolbarIcon, { hasAccess })
375
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Popover.Root, {
376
+ open: isPromptOpen,
377
+ onOpenChange: (open) => {
378
+ if (!open) setIsPromptOpen(false);
379
+ },
380
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Popover.Portal, {
381
+ container: portalContainer,
382
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Popover.Positioner, {
383
+ anchor: buttonRef.current,
384
+ side: "top",
385
+ align: "end",
386
+ sideOffset: 8,
387
+ collisionPadding: 8,
388
+ positionMethod: "fixed",
389
+ style: {
390
+ zIndex: 9999999,
391
+ pointerEvents: "auto"
392
+ },
393
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_spot_ui_components.Popover.Popup, {
394
+ style: {
395
+ width: 280,
396
+ overflow: "hidden"
397
+ },
398
+ onClick: (event) => event.stopPropagation(),
399
+ onKeyDown: (event) => event.stopPropagation(),
400
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FolderAccessPrompt, {
401
+ root: projectRoot,
402
+ onGrant: handleGrant,
403
+ onCancel: () => setIsPromptOpen(false)
404
+ })
405
+ })
406
+ })
407
+ })
408
+ })] });
409
+ }
410
+ function PreviewPlugin(_options = {}) {
411
+ return {
412
+ name: "preview",
413
+ toolbar: PreviewToolbar,
414
+ settings: PreviewSettings
415
+ };
416
+ }
417
+
418
+ //#endregion
419
+ exports.PreviewPlugin = PreviewPlugin;
420
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["FolderIcon","IS_MAC","KbdGroup","Kbd","Button","bundledThemesInfo","Combobox","FolderIcon","Tooltip","ToolbarButton","Popover"],"sources":["../src/FolderAccessPrompt.tsx","../src/fs.ts","../src/store.ts","../src/PreviewSettings.tsx","../src/index.tsx"],"sourcesContent":["import { IS_MAC } from '@react-spot/core'\nimport {\n Button,\n FolderIcon,\n Kbd,\n KbdGroup,\n} from '@react-spot/ui-components'\n\nexport function FolderAccessPrompt({\n root,\n onGrant,\n onCancel,\n}: {\n root: string | undefined\n onGrant(): void\n onCancel(): void\n}) {\n return (\n <div\n style={{\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n gap: 12,\n padding: '20px 16px',\n height: '100%',\n boxSizing: 'border-box',\n textAlign: 'center',\n fontFamily: 'system-ui, sans-serif',\n }}\n >\n <span style={{ color: '#52525b' }}>\n <FolderIcon />\n </span>\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <span style={{ fontSize: 13, fontWeight: 600, color: '#fafafa' }}>\n Folder access needed\n </span>\n <span style={{ fontSize: 12, color: '#71717a', lineHeight: 1.5 }}>\n {root ? (\n <span style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>\n <span>\n The project root path will be copied to your clipboard. Navigate\n to it in the folder picker.\n </span>\n {IS_MAC && (\n <span>\n <KbdGroup>\n <Kbd>⌘</Kbd>\n <Kbd>⇧</Kbd>\n <Kbd>G</Kbd>\n </KbdGroup>\n <span> to paste the path directly.</span>\n </span>\n )}\n </span>\n ) : (\n 'Grant access to your project folder to preview source files.'\n )}\n </span>\n {root && (\n <span\n style={{\n fontSize: 11,\n fontFamily: 'ui-monospace, monospace',\n color: '#3b82f6',\n wordBreak: 'break-all',\n }}\n >\n {root}\n </span>\n )}\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <Button variant=\"secondary\" onClick={onCancel}>\n Cancel\n </Button>\n <Button variant=\"primary\" onClick={onGrant}>\n Grant access\n </Button>\n </div>\n </div>\n )\n}\n\n/**\n * Copies the root path to clipboard then calls requestAccess().\n */\nexport async function handleGrantAccess(\n root: string,\n requestAccess: () => Promise<boolean>,\n): Promise<boolean> {\n await navigator.clipboard.writeText(root).catch(() => {})\n return requestAccess()\n}\n","export interface FileSystemService {\n /** Whether the File System Access API is available in this browser */\n isSupported: boolean\n /** Whether the user has already granted directory access */\n hasAccess: boolean\n /**\n * Silently try to restore a previously granted directory handle from\n * IndexedDB and re-request permission. Resolves true if successful.\n * Call this on app mount to avoid prompting on every reload.\n */\n tryRestore(): Promise<boolean>\n /**\n * Prompt the user to pick the project root directory via showDirectoryPicker().\n * The handle is persisted in IndexedDB for future sessions.\n */\n requestAccess(): Promise<boolean>\n /**\n * Subscribe to hasAccess changes (e.g. after requestAccess / tryRestore).\n * Returns an unsubscribe function.\n */\n subscribe(listener: () => void): () => void\n /**\n * Read a file by its relative path (relative to the granted directory).\n * If no access has been granted yet, triggers requestAccess() first.\n */\n read(relativePath: string): Promise<string>\n /**\n * Write content to a file by its relative path. Triggers requestAccess() if needed.\n * Written files trigger HMR automatically in the dev server.\n */\n write(relativePath: string, content: string): Promise<void>\n}\n\nconst IDB_NAME = 'react-trace'\nconst IDB_STORE = 'handles'\nconst IDB_KEY = 'root-directory'\n\nfunction openDB(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(IDB_NAME, 1)\n req.onupgradeneeded = () => req.result.createObjectStore(IDB_STORE)\n req.onsuccess = () => resolve(req.result)\n req.onerror = () => reject(req.error)\n })\n}\n\nasync function saveHandle(handle: FileSystemDirectoryHandle): Promise<void> {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(IDB_STORE, 'readwrite')\n tx.objectStore(IDB_STORE).put(handle, IDB_KEY)\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n })\n}\n\nasync function loadHandle(): Promise<FileSystemDirectoryHandle | null> {\n try {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(IDB_STORE, 'readonly')\n const req = tx.objectStore(IDB_STORE).get(IDB_KEY)\n req.onsuccess = () =>\n resolve((req.result as FileSystemDirectoryHandle) ?? null)\n req.onerror = () => reject(req.error)\n })\n } catch {\n return null\n }\n}\n\n/**\n * Traverses the directory handle tree to reach the target file.\n * Returns null if any segment of the path doesn't exist.\n */\nasync function getFileHandle(\n dir: FileSystemDirectoryHandle,\n relativePath: string,\n create = false,\n): Promise<FileSystemFileHandle | null> {\n const parts = relativePath.split('/').filter(Boolean)\n if (parts.length === 0) return null\n\n let current: FileSystemDirectoryHandle = dir\n\n for (let i = 0; i < parts.length - 1; i++) {\n try {\n current = await current.getDirectoryHandle(parts[i]!, { create })\n } catch {\n return null\n }\n }\n\n try {\n return await current.getFileHandle(parts.at(-1)!, { create })\n } catch {\n return null\n }\n}\n\nclass FileSystemServiceImpl implements FileSystemService {\n private _handle: FileSystemDirectoryHandle | null = null\n private _listeners = new Set<() => void>()\n\n get isSupported(): boolean {\n return typeof window !== 'undefined' && 'showDirectoryPicker' in window\n }\n\n get hasAccess(): boolean {\n return this._handle !== null\n }\n\n subscribe(listener: () => void): () => void {\n this._listeners.add(listener)\n return () => this._listeners.delete(listener)\n }\n\n private notify(): void {\n this._listeners.forEach((l) => l())\n }\n\n async tryRestore(): Promise<boolean> {\n if (!this.isSupported) return false\n try {\n const handle = await loadHandle()\n if (!handle) return false\n const perm = await handle.requestPermission({ mode: 'readwrite' })\n if (perm === 'granted') {\n this._handle = handle\n this.notify()\n return true\n }\n } catch {\n // handle gone or permission denied — fall through\n }\n return false\n }\n\n async requestAccess(): Promise<boolean> {\n if (!this.isSupported) return false\n try {\n const handle = await window.showDirectoryPicker({ mode: 'readwrite' })\n await saveHandle(handle)\n this._handle = handle\n this.notify()\n return true\n } catch {\n // User cancelled the picker\n return false\n }\n }\n\n /** Ensure we have access — try restore silently first, then prompt. */\n private async ensureAccess(): Promise<boolean> {\n if (this._handle) return true\n const restored = await this.tryRestore()\n if (restored) return true\n return this.requestAccess()\n }\n\n async read(relativePath: string): Promise<string> {\n const ok = await this.ensureAccess()\n if (!ok || !this._handle)\n throw new Error('[react-trace] File system access denied')\n\n const file = await getFileHandle(this._handle, relativePath)\n if (!file) throw new Error(`[react-trace] File not found: ${relativePath}`)\n\n return (await file.getFile()).text()\n }\n\n async write(relativePath: string, content: string): Promise<void> {\n const ok = await this.ensureAccess()\n if (!ok || !this._handle)\n throw new Error('[react-trace] File system access denied')\n\n const file = await getFileHandle(this._handle, relativePath, true)\n if (!file)\n throw new Error(\n `[react-trace] Cannot open file for writing: ${relativePath}`,\n )\n\n const writable = await file.createWritable()\n await writable.write(content)\n await writable.close()\n }\n}\n\nexport const fileSystemService: FileSystemService = new FileSystemServiceImpl()\n","import type { TraceSettings } from '@react-spot/core'\nimport { settingsPluginAtom } from '@react-spot/core'\nimport type { WritableAtom } from 'jotai'\nimport type { BundledTheme } from 'shiki'\n\ndeclare module '@react-spot/core' {\n interface TraceSettings {\n preview?: {\n disabled: boolean\n theme: BundledTheme\n }\n }\n}\n\nexport const previewSettingsAtom = settingsPluginAtom(\n 'preview',\n) as WritableAtom<TraceSettings['preview'], [TraceSettings['preview']], void>\n","import { useWidgetPortalContainer } from '@react-spot/core'\nimport { Combobox } from '@react-spot/ui-components'\nimport { useAtom } from 'jotai'\nimport type { CSSProperties } from 'react'\nimport { useRef, useState } from 'react'\nimport type { BundledTheme } from 'shiki'\nimport { bundledThemesInfo } from 'shiki'\n\nimport { previewSettingsAtom } from './store'\n\nconst themes = bundledThemesInfo.map((theme) => ({\n value: theme.id as BundledTheme,\n label: theme.displayName,\n}))\n\nconst themesLookup = themes.reduce<\n Record<(typeof themes)[number]['value'], (typeof themes)[number]>\n>(\n (acc, theme) => {\n acc[theme.value] = theme\n return acc\n },\n {} as Record<(typeof themes)[number]['value'], (typeof themes)[number]>,\n)\n\nconst LABEL_STYLE: CSSProperties = {\n fontSize: 12,\n color: '#d4d4d8',\n fontFamily: 'system-ui, sans-serif',\n}\n\nexport function PreviewSettings() {\n const portalContainer = useWidgetPortalContainer()\n const [themesOpen, setThemesOpen] = useState(false)\n\n const [\n previewSettings = { disabled: false, theme: 'one-dark-pro' },\n setPreviewSettings,\n ] = useAtom(previewSettingsAtom)\n\n const anchorRef = useRef<HTMLDivElement>(null)\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n <label style={LABEL_STYLE}>\n <input\n type=\"checkbox\"\n checked={previewSettings.disabled ?? false}\n onChange={(e) =>\n setPreviewSettings({\n ...previewSettings,\n disabled: e.target.checked,\n })\n }\n />{' '}\n Code editing disabled\n </label>\n </div>\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n <label style={LABEL_STYLE}>Theme</label>\n <Combobox.Root\n value={themesLookup[previewSettings.theme]}\n onValueChange={(theme) => {\n if (theme) {\n setPreviewSettings({ ...previewSettings, theme: theme.value })\n }\n }}\n items={themes}\n open={themesOpen}\n onOpenChange={setThemesOpen}\n >\n <div ref={anchorRef}>\n <Combobox.Trigger\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => e.stopPropagation()}\n >\n <Combobox.Input />\n </Combobox.Trigger>\n </div>\n\n <Combobox.Portal container={portalContainer}>\n <Combobox.Positioner\n style={{ zIndex: 100000000, pointerEvents: 'auto' }}\n anchor={anchorRef}\n >\n <Combobox.Popup>\n <Combobox.Empty>No themes found</Combobox.Empty>\n <Combobox.List\n style={{\n overscrollBehavior: 'contain',\n maxHeight: 500,\n overflowY: 'auto',\n }}\n >\n {(option: (typeof themes)[number]) => (\n <Combobox.Item\n key={option.value}\n value={option}\n onClick={(e) => e.stopPropagation()}\n >\n <span>{option.label}</span>\n </Combobox.Item>\n )}\n </Combobox.List>\n </Combobox.Popup>\n </Combobox.Positioner>\n </Combobox.Portal>\n </Combobox.Root>\n </div>\n </div>\n )\n}\n","import type { TracePlugin } from '@react-spot/core'\nimport {\n useDeactivateInspector,\n useProjectRoot,\n useWidgetPortalContainer,\n} from '@react-spot/core'\nimport {\n FolderIcon,\n Popover,\n ToolbarButton,\n Tooltip,\n} from '@react-spot/ui-components'\nimport { useEffect, useRef, useState, useSyncExternalStore } from 'react'\n\nimport { FolderAccessPrompt, handleGrantAccess } from './FolderAccessPrompt'\nimport { fileSystemService } from './fs'\nimport { PreviewSettings } from './PreviewSettings'\nimport { SourcePreview } from './SourcePreview'\nimport type { PreviewPluginOptions } from './types'\n\nexport type { PreviewPluginOptions }\n\nfunction FolderToolbarIcon({ hasAccess }: { hasAccess: boolean }) {\n return (\n <span style={{ position: 'relative', display: 'inline-flex' }}>\n <FolderIcon />\n {hasAccess && (\n <span\n style={{\n position: 'absolute',\n top: -4,\n right: -4,\n width: 6,\n height: 6,\n borderRadius: '50%',\n background: '#22c55e',\n border: '1.5px solid #18181b',\n pointerEvents: 'none',\n }}\n />\n )}\n </span>\n )\n}\n\nfunction PreviewToolbar() {\n const projectRoot = useProjectRoot()\n const portalContainer = useWidgetPortalContainer()\n const deactivateInspector = useDeactivateInspector()\n const buttonRef = useRef<HTMLButtonElement>(null)\n const [isPromptOpen, setIsPromptOpen] = useState(false)\n const hasAccess = useSyncExternalStore(\n fileSystemService.subscribe.bind(fileSystemService),\n () => fileSystemService.hasAccess,\n () => false,\n )\n\n // Silently try to restore a previously granted FS handle on mount\n useEffect(() => {\n fileSystemService.tryRestore().catch(() => {})\n }, [])\n\n const handleClick = () => {\n deactivateInspector()\n\n if (fileSystemService.hasAccess) {\n fileSystemService.requestAccess().catch(() => {})\n return\n }\n\n setIsPromptOpen(true)\n }\n\n const handleGrant = async () => {\n const granted = await handleGrantAccess(projectRoot, () =>\n fileSystemService.requestAccess(),\n )\n if (granted) setIsPromptOpen(false)\n }\n\n return (\n <>\n <Tooltip\n label={hasAccess ? 'Project folder connected' : 'Select project folder'}\n container={portalContainer}\n render={<ToolbarButton ref={buttonRef} />}\n aria-label=\"Project folder\"\n style={{\n color: hasAccess ? '#22c55e' : '#52525b',\n }}\n onClick={handleClick}\n >\n <FolderToolbarIcon hasAccess={hasAccess} />\n </Tooltip>\n\n <Popover.Root\n open={isPromptOpen}\n onOpenChange={(open: boolean) => {\n if (!open) setIsPromptOpen(false)\n }}\n >\n <Popover.Portal container={portalContainer}>\n <Popover.Positioner\n anchor={buttonRef.current}\n side=\"top\"\n align=\"end\"\n sideOffset={8}\n collisionPadding={8}\n positionMethod=\"fixed\"\n style={{ zIndex: 9999999, pointerEvents: 'auto' }}\n >\n <Popover.Popup\n style={{ width: 280, overflow: 'hidden' }}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n >\n <FolderAccessPrompt\n root={projectRoot}\n onGrant={handleGrant}\n onCancel={() => setIsPromptOpen(false)}\n />\n </Popover.Popup>\n </Popover.Positioner>\n </Popover.Portal>\n </Popover.Root>\n </>\n )\n}\n\nexport function PreviewPlugin(_options: PreviewPluginOptions = {}): TracePlugin {\n return {\n name: 'preview',\n toolbar: PreviewToolbar,\n settings: PreviewSettings,\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { t as PreviewPluginOptions } from "./types-DM7rB7J1.cjs";
2
+ import { TracePlugin } from "@react-spot/core";
3
+
4
+ //#region src/index.d.ts
5
+ declare function PreviewPlugin(_options?: PreviewPluginOptions): TracePlugin;
6
+ //#endregion
7
+ export { PreviewPlugin, type PreviewPluginOptions };
8
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.tsx"],"mappings":";;;;iBAiIgB,aAAA,CAAc,QAAA,GAAU,oBAAA,GAA4B,WAAA"}
@@ -0,0 +1,8 @@
1
+ import { t as PreviewPluginOptions } from "./types-uxyW_RVb.js";
2
+ import { TracePlugin } from "@react-spot/core";
3
+
4
+ //#region src/index.d.ts
5
+ declare function PreviewPlugin(_options?: PreviewPluginOptions): TracePlugin;
6
+ //#endregion
7
+ export { PreviewPlugin, type PreviewPluginOptions };
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;;;iBAiIgB,aAAA,CAAc,QAAA,GAAU,oBAAA,GAA4B,WAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,419 @@
1
+ import { IS_MAC, settingsPluginAtom, useDeactivateInspector, useProjectRoot, useWidgetPortalContainer } from "@react-spot/core";
2
+ import { Button, Combobox, FolderIcon, Kbd, KbdGroup, Popover, ToolbarButton, Tooltip } from "@react-spot/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 { bundledThemesInfo } from "shiki";
7
+
8
+ //#region src/FolderAccessPrompt.tsx
9
+ function FolderAccessPrompt({ root, onGrant, onCancel }) {
10
+ return /* @__PURE__ */ jsxs("div", {
11
+ style: {
12
+ display: "flex",
13
+ flexDirection: "column",
14
+ alignItems: "center",
15
+ justifyContent: "center",
16
+ gap: 12,
17
+ padding: "20px 16px",
18
+ height: "100%",
19
+ boxSizing: "border-box",
20
+ textAlign: "center",
21
+ fontFamily: "system-ui, sans-serif"
22
+ },
23
+ children: [
24
+ /* @__PURE__ */ jsx("span", {
25
+ style: { color: "#52525b" },
26
+ children: /* @__PURE__ */ jsx(FolderIcon, {})
27
+ }),
28
+ /* @__PURE__ */ jsxs("div", {
29
+ style: {
30
+ display: "flex",
31
+ flexDirection: "column",
32
+ gap: 12
33
+ },
34
+ children: [
35
+ /* @__PURE__ */ jsx("span", {
36
+ style: {
37
+ fontSize: 13,
38
+ fontWeight: 600,
39
+ color: "#fafafa"
40
+ },
41
+ children: "Folder access needed"
42
+ }),
43
+ /* @__PURE__ */ jsx("span", {
44
+ style: {
45
+ fontSize: 12,
46
+ color: "#71717a",
47
+ lineHeight: 1.5
48
+ },
49
+ children: root ? /* @__PURE__ */ jsxs("span", {
50
+ style: {
51
+ display: "flex",
52
+ flexDirection: "column",
53
+ gap: 8
54
+ },
55
+ 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: [
56
+ /* @__PURE__ */ jsx(Kbd, { children: "⌘" }),
57
+ /* @__PURE__ */ jsx(Kbd, { children: "⇧" }),
58
+ /* @__PURE__ */ jsx(Kbd, { children: "G" })
59
+ ] }), /* @__PURE__ */ jsx("span", { children: " to paste the path directly." })] })]
60
+ }) : "Grant access to your project folder to preview source files."
61
+ }),
62
+ root && /* @__PURE__ */ jsx("span", {
63
+ style: {
64
+ fontSize: 11,
65
+ fontFamily: "ui-monospace, monospace",
66
+ color: "#3b82f6",
67
+ wordBreak: "break-all"
68
+ },
69
+ children: root
70
+ })
71
+ ]
72
+ }),
73
+ /* @__PURE__ */ jsxs("div", {
74
+ style: {
75
+ display: "flex",
76
+ gap: 8
77
+ },
78
+ children: [/* @__PURE__ */ jsx(Button, {
79
+ variant: "secondary",
80
+ onClick: onCancel,
81
+ children: "Cancel"
82
+ }), /* @__PURE__ */ jsx(Button, {
83
+ variant: "primary",
84
+ onClick: onGrant,
85
+ children: "Grant access"
86
+ })]
87
+ })
88
+ ]
89
+ });
90
+ }
91
+ /**
92
+ * Copies the root path to clipboard then calls requestAccess().
93
+ */
94
+ async function handleGrantAccess(root, requestAccess) {
95
+ await navigator.clipboard.writeText(root).catch(() => {});
96
+ return requestAccess();
97
+ }
98
+
99
+ //#endregion
100
+ //#region src/fs.ts
101
+ const IDB_NAME = "react-trace";
102
+ const IDB_STORE = "handles";
103
+ const IDB_KEY = "root-directory";
104
+ function openDB() {
105
+ return new Promise((resolve, reject) => {
106
+ const req = indexedDB.open(IDB_NAME, 1);
107
+ req.onupgradeneeded = () => req.result.createObjectStore(IDB_STORE);
108
+ req.onsuccess = () => resolve(req.result);
109
+ req.onerror = () => reject(req.error);
110
+ });
111
+ }
112
+ async function saveHandle(handle) {
113
+ const db = await openDB();
114
+ return new Promise((resolve, reject) => {
115
+ const tx = db.transaction(IDB_STORE, "readwrite");
116
+ tx.objectStore(IDB_STORE).put(handle, IDB_KEY);
117
+ tx.oncomplete = () => resolve();
118
+ tx.onerror = () => reject(tx.error);
119
+ });
120
+ }
121
+ async function loadHandle() {
122
+ try {
123
+ const db = await openDB();
124
+ return new Promise((resolve, reject) => {
125
+ const req = db.transaction(IDB_STORE, "readonly").objectStore(IDB_STORE).get(IDB_KEY);
126
+ req.onsuccess = () => resolve(req.result ?? null);
127
+ req.onerror = () => reject(req.error);
128
+ });
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+ /**
134
+ * Traverses the directory handle tree to reach the target file.
135
+ * Returns null if any segment of the path doesn't exist.
136
+ */
137
+ async function getFileHandle(dir, relativePath, create = false) {
138
+ const parts = relativePath.split("/").filter(Boolean);
139
+ if (parts.length === 0) return null;
140
+ let current = dir;
141
+ for (let i = 0; i < parts.length - 1; i++) try {
142
+ current = await current.getDirectoryHandle(parts[i], { create });
143
+ } catch {
144
+ return null;
145
+ }
146
+ try {
147
+ return await current.getFileHandle(parts.at(-1), { create });
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+ var FileSystemServiceImpl = class {
153
+ _handle = null;
154
+ _listeners = /* @__PURE__ */ new Set();
155
+ get isSupported() {
156
+ return typeof window !== "undefined" && "showDirectoryPicker" in window;
157
+ }
158
+ get hasAccess() {
159
+ return this._handle !== null;
160
+ }
161
+ subscribe(listener) {
162
+ this._listeners.add(listener);
163
+ return () => this._listeners.delete(listener);
164
+ }
165
+ notify() {
166
+ this._listeners.forEach((l) => l());
167
+ }
168
+ async tryRestore() {
169
+ if (!this.isSupported) return false;
170
+ try {
171
+ const handle = await loadHandle();
172
+ if (!handle) return false;
173
+ if (await handle.requestPermission({ mode: "readwrite" }) === "granted") {
174
+ this._handle = handle;
175
+ this.notify();
176
+ return true;
177
+ }
178
+ } catch {}
179
+ return false;
180
+ }
181
+ async requestAccess() {
182
+ if (!this.isSupported) return false;
183
+ try {
184
+ const handle = await window.showDirectoryPicker({ mode: "readwrite" });
185
+ await saveHandle(handle);
186
+ this._handle = handle;
187
+ this.notify();
188
+ return true;
189
+ } catch {
190
+ return false;
191
+ }
192
+ }
193
+ /** Ensure we have access — try restore silently first, then prompt. */
194
+ async ensureAccess() {
195
+ if (this._handle) return true;
196
+ if (await this.tryRestore()) return true;
197
+ return this.requestAccess();
198
+ }
199
+ async read(relativePath) {
200
+ if (!await this.ensureAccess() || !this._handle) throw new Error("[react-trace] File system access denied");
201
+ const file = await getFileHandle(this._handle, relativePath);
202
+ if (!file) throw new Error(`[react-trace] File not found: ${relativePath}`);
203
+ return (await file.getFile()).text();
204
+ }
205
+ async write(relativePath, content) {
206
+ if (!await this.ensureAccess() || !this._handle) throw new Error("[react-trace] File system access denied");
207
+ const file = await getFileHandle(this._handle, relativePath, true);
208
+ if (!file) throw new Error(`[react-trace] Cannot open file for writing: ${relativePath}`);
209
+ const writable = await file.createWritable();
210
+ await writable.write(content);
211
+ await writable.close();
212
+ }
213
+ };
214
+ const fileSystemService = new FileSystemServiceImpl();
215
+
216
+ //#endregion
217
+ //#region src/store.ts
218
+ const previewSettingsAtom = settingsPluginAtom("preview");
219
+
220
+ //#endregion
221
+ //#region src/PreviewSettings.tsx
222
+ const themes = bundledThemesInfo.map((theme) => ({
223
+ value: theme.id,
224
+ label: theme.displayName
225
+ }));
226
+ const themesLookup = themes.reduce((acc, theme) => {
227
+ acc[theme.value] = theme;
228
+ return acc;
229
+ }, {});
230
+ const LABEL_STYLE = {
231
+ fontSize: 12,
232
+ color: "#d4d4d8",
233
+ fontFamily: "system-ui, sans-serif"
234
+ };
235
+ function PreviewSettings() {
236
+ const portalContainer = useWidgetPortalContainer();
237
+ const [themesOpen, setThemesOpen] = useState(false);
238
+ const [previewSettings = {
239
+ disabled: false,
240
+ theme: "one-dark-pro"
241
+ }, setPreviewSettings] = useAtom(previewSettingsAtom);
242
+ const anchorRef = useRef(null);
243
+ return /* @__PURE__ */ jsxs("div", {
244
+ style: {
245
+ display: "flex",
246
+ flexDirection: "column",
247
+ gap: 12
248
+ },
249
+ children: [/* @__PURE__ */ jsx("div", {
250
+ style: {
251
+ display: "flex",
252
+ flexDirection: "column",
253
+ gap: 6
254
+ },
255
+ children: /* @__PURE__ */ jsxs("label", {
256
+ style: LABEL_STYLE,
257
+ children: [
258
+ /* @__PURE__ */ jsx("input", {
259
+ type: "checkbox",
260
+ checked: previewSettings.disabled ?? false,
261
+ onChange: (e) => setPreviewSettings({
262
+ ...previewSettings,
263
+ disabled: e.target.checked
264
+ })
265
+ }),
266
+ " ",
267
+ "Code editing disabled"
268
+ ]
269
+ })
270
+ }), /* @__PURE__ */ jsxs("div", {
271
+ style: {
272
+ display: "flex",
273
+ flexDirection: "column",
274
+ gap: 6
275
+ },
276
+ children: [/* @__PURE__ */ jsx("label", {
277
+ style: LABEL_STYLE,
278
+ children: "Theme"
279
+ }), /* @__PURE__ */ jsxs(Combobox.Root, {
280
+ value: themesLookup[previewSettings.theme],
281
+ onValueChange: (theme) => {
282
+ if (theme) setPreviewSettings({
283
+ ...previewSettings,
284
+ theme: theme.value
285
+ });
286
+ },
287
+ items: themes,
288
+ open: themesOpen,
289
+ onOpenChange: setThemesOpen,
290
+ children: [/* @__PURE__ */ jsx("div", {
291
+ ref: anchorRef,
292
+ children: /* @__PURE__ */ jsx(Combobox.Trigger, {
293
+ onClick: (e) => e.stopPropagation(),
294
+ onKeyDown: (e) => e.stopPropagation(),
295
+ children: /* @__PURE__ */ jsx(Combobox.Input, {})
296
+ })
297
+ }), /* @__PURE__ */ jsx(Combobox.Portal, {
298
+ container: portalContainer,
299
+ children: /* @__PURE__ */ jsx(Combobox.Positioner, {
300
+ style: {
301
+ zIndex: 1e8,
302
+ pointerEvents: "auto"
303
+ },
304
+ anchor: anchorRef,
305
+ children: /* @__PURE__ */ jsxs(Combobox.Popup, { children: [/* @__PURE__ */ jsx(Combobox.Empty, { children: "No themes found" }), /* @__PURE__ */ jsx(Combobox.List, {
306
+ style: {
307
+ overscrollBehavior: "contain",
308
+ maxHeight: 500,
309
+ overflowY: "auto"
310
+ },
311
+ children: (option) => /* @__PURE__ */ jsx(Combobox.Item, {
312
+ value: option,
313
+ onClick: (e) => e.stopPropagation(),
314
+ children: /* @__PURE__ */ jsx("span", { children: option.label })
315
+ }, option.value)
316
+ })] })
317
+ })
318
+ })]
319
+ })]
320
+ })]
321
+ });
322
+ }
323
+
324
+ //#endregion
325
+ //#region src/index.tsx
326
+ function FolderToolbarIcon({ hasAccess }) {
327
+ return /* @__PURE__ */ jsxs("span", {
328
+ style: {
329
+ position: "relative",
330
+ display: "inline-flex"
331
+ },
332
+ children: [/* @__PURE__ */ jsx(FolderIcon, {}), hasAccess && /* @__PURE__ */ jsx("span", { style: {
333
+ position: "absolute",
334
+ top: -4,
335
+ right: -4,
336
+ width: 6,
337
+ height: 6,
338
+ borderRadius: "50%",
339
+ background: "#22c55e",
340
+ border: "1.5px solid #18181b",
341
+ pointerEvents: "none"
342
+ } })]
343
+ });
344
+ }
345
+ function PreviewToolbar() {
346
+ const projectRoot = useProjectRoot();
347
+ const portalContainer = useWidgetPortalContainer();
348
+ const deactivateInspector = useDeactivateInspector();
349
+ const buttonRef = useRef(null);
350
+ const [isPromptOpen, setIsPromptOpen] = useState(false);
351
+ const hasAccess = useSyncExternalStore(fileSystemService.subscribe.bind(fileSystemService), () => fileSystemService.hasAccess, () => false);
352
+ useEffect(() => {
353
+ fileSystemService.tryRestore().catch(() => {});
354
+ }, []);
355
+ const handleClick = () => {
356
+ deactivateInspector();
357
+ if (fileSystemService.hasAccess) {
358
+ fileSystemService.requestAccess().catch(() => {});
359
+ return;
360
+ }
361
+ setIsPromptOpen(true);
362
+ };
363
+ const handleGrant = async () => {
364
+ if (await handleGrantAccess(projectRoot, () => fileSystemService.requestAccess())) setIsPromptOpen(false);
365
+ };
366
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Tooltip, {
367
+ label: hasAccess ? "Project folder connected" : "Select project folder",
368
+ container: portalContainer,
369
+ render: /* @__PURE__ */ jsx(ToolbarButton, { ref: buttonRef }),
370
+ "aria-label": "Project folder",
371
+ style: { color: hasAccess ? "#22c55e" : "#52525b" },
372
+ onClick: handleClick,
373
+ children: /* @__PURE__ */ jsx(FolderToolbarIcon, { hasAccess })
374
+ }), /* @__PURE__ */ jsx(Popover.Root, {
375
+ open: isPromptOpen,
376
+ onOpenChange: (open) => {
377
+ if (!open) setIsPromptOpen(false);
378
+ },
379
+ children: /* @__PURE__ */ jsx(Popover.Portal, {
380
+ container: portalContainer,
381
+ children: /* @__PURE__ */ jsx(Popover.Positioner, {
382
+ anchor: buttonRef.current,
383
+ side: "top",
384
+ align: "end",
385
+ sideOffset: 8,
386
+ collisionPadding: 8,
387
+ positionMethod: "fixed",
388
+ style: {
389
+ zIndex: 9999999,
390
+ pointerEvents: "auto"
391
+ },
392
+ children: /* @__PURE__ */ jsx(Popover.Popup, {
393
+ style: {
394
+ width: 280,
395
+ overflow: "hidden"
396
+ },
397
+ onClick: (event) => event.stopPropagation(),
398
+ onKeyDown: (event) => event.stopPropagation(),
399
+ children: /* @__PURE__ */ jsx(FolderAccessPrompt, {
400
+ root: projectRoot,
401
+ onGrant: handleGrant,
402
+ onCancel: () => setIsPromptOpen(false)
403
+ })
404
+ })
405
+ })
406
+ })
407
+ })] });
408
+ }
409
+ function PreviewPlugin(_options = {}) {
410
+ return {
411
+ name: "preview",
412
+ toolbar: PreviewToolbar,
413
+ settings: PreviewSettings
414
+ };
415
+ }
416
+
417
+ //#endregion
418
+ export { PreviewPlugin };
419
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/FolderAccessPrompt.tsx","../src/fs.ts","../src/store.ts","../src/PreviewSettings.tsx","../src/index.tsx"],"sourcesContent":["import { IS_MAC } from '@react-spot/core'\nimport {\n Button,\n FolderIcon,\n Kbd,\n KbdGroup,\n} from '@react-spot/ui-components'\n\nexport function FolderAccessPrompt({\n root,\n onGrant,\n onCancel,\n}: {\n root: string | undefined\n onGrant(): void\n onCancel(): void\n}) {\n return (\n <div\n style={{\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n gap: 12,\n padding: '20px 16px',\n height: '100%',\n boxSizing: 'border-box',\n textAlign: 'center',\n fontFamily: 'system-ui, sans-serif',\n }}\n >\n <span style={{ color: '#52525b' }}>\n <FolderIcon />\n </span>\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <span style={{ fontSize: 13, fontWeight: 600, color: '#fafafa' }}>\n Folder access needed\n </span>\n <span style={{ fontSize: 12, color: '#71717a', lineHeight: 1.5 }}>\n {root ? (\n <span style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>\n <span>\n The project root path will be copied to your clipboard. Navigate\n to it in the folder picker.\n </span>\n {IS_MAC && (\n <span>\n <KbdGroup>\n <Kbd>⌘</Kbd>\n <Kbd>⇧</Kbd>\n <Kbd>G</Kbd>\n </KbdGroup>\n <span> to paste the path directly.</span>\n </span>\n )}\n </span>\n ) : (\n 'Grant access to your project folder to preview source files.'\n )}\n </span>\n {root && (\n <span\n style={{\n fontSize: 11,\n fontFamily: 'ui-monospace, monospace',\n color: '#3b82f6',\n wordBreak: 'break-all',\n }}\n >\n {root}\n </span>\n )}\n </div>\n <div style={{ display: 'flex', gap: 8 }}>\n <Button variant=\"secondary\" onClick={onCancel}>\n Cancel\n </Button>\n <Button variant=\"primary\" onClick={onGrant}>\n Grant access\n </Button>\n </div>\n </div>\n )\n}\n\n/**\n * Copies the root path to clipboard then calls requestAccess().\n */\nexport async function handleGrantAccess(\n root: string,\n requestAccess: () => Promise<boolean>,\n): Promise<boolean> {\n await navigator.clipboard.writeText(root).catch(() => {})\n return requestAccess()\n}\n","export interface FileSystemService {\n /** Whether the File System Access API is available in this browser */\n isSupported: boolean\n /** Whether the user has already granted directory access */\n hasAccess: boolean\n /**\n * Silently try to restore a previously granted directory handle from\n * IndexedDB and re-request permission. Resolves true if successful.\n * Call this on app mount to avoid prompting on every reload.\n */\n tryRestore(): Promise<boolean>\n /**\n * Prompt the user to pick the project root directory via showDirectoryPicker().\n * The handle is persisted in IndexedDB for future sessions.\n */\n requestAccess(): Promise<boolean>\n /**\n * Subscribe to hasAccess changes (e.g. after requestAccess / tryRestore).\n * Returns an unsubscribe function.\n */\n subscribe(listener: () => void): () => void\n /**\n * Read a file by its relative path (relative to the granted directory).\n * If no access has been granted yet, triggers requestAccess() first.\n */\n read(relativePath: string): Promise<string>\n /**\n * Write content to a file by its relative path. Triggers requestAccess() if needed.\n * Written files trigger HMR automatically in the dev server.\n */\n write(relativePath: string, content: string): Promise<void>\n}\n\nconst IDB_NAME = 'react-trace'\nconst IDB_STORE = 'handles'\nconst IDB_KEY = 'root-directory'\n\nfunction openDB(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(IDB_NAME, 1)\n req.onupgradeneeded = () => req.result.createObjectStore(IDB_STORE)\n req.onsuccess = () => resolve(req.result)\n req.onerror = () => reject(req.error)\n })\n}\n\nasync function saveHandle(handle: FileSystemDirectoryHandle): Promise<void> {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(IDB_STORE, 'readwrite')\n tx.objectStore(IDB_STORE).put(handle, IDB_KEY)\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n })\n}\n\nasync function loadHandle(): Promise<FileSystemDirectoryHandle | null> {\n try {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(IDB_STORE, 'readonly')\n const req = tx.objectStore(IDB_STORE).get(IDB_KEY)\n req.onsuccess = () =>\n resolve((req.result as FileSystemDirectoryHandle) ?? null)\n req.onerror = () => reject(req.error)\n })\n } catch {\n return null\n }\n}\n\n/**\n * Traverses the directory handle tree to reach the target file.\n * Returns null if any segment of the path doesn't exist.\n */\nasync function getFileHandle(\n dir: FileSystemDirectoryHandle,\n relativePath: string,\n create = false,\n): Promise<FileSystemFileHandle | null> {\n const parts = relativePath.split('/').filter(Boolean)\n if (parts.length === 0) return null\n\n let current: FileSystemDirectoryHandle = dir\n\n for (let i = 0; i < parts.length - 1; i++) {\n try {\n current = await current.getDirectoryHandle(parts[i]!, { create })\n } catch {\n return null\n }\n }\n\n try {\n return await current.getFileHandle(parts.at(-1)!, { create })\n } catch {\n return null\n }\n}\n\nclass FileSystemServiceImpl implements FileSystemService {\n private _handle: FileSystemDirectoryHandle | null = null\n private _listeners = new Set<() => void>()\n\n get isSupported(): boolean {\n return typeof window !== 'undefined' && 'showDirectoryPicker' in window\n }\n\n get hasAccess(): boolean {\n return this._handle !== null\n }\n\n subscribe(listener: () => void): () => void {\n this._listeners.add(listener)\n return () => this._listeners.delete(listener)\n }\n\n private notify(): void {\n this._listeners.forEach((l) => l())\n }\n\n async tryRestore(): Promise<boolean> {\n if (!this.isSupported) return false\n try {\n const handle = await loadHandle()\n if (!handle) return false\n const perm = await handle.requestPermission({ mode: 'readwrite' })\n if (perm === 'granted') {\n this._handle = handle\n this.notify()\n return true\n }\n } catch {\n // handle gone or permission denied — fall through\n }\n return false\n }\n\n async requestAccess(): Promise<boolean> {\n if (!this.isSupported) return false\n try {\n const handle = await window.showDirectoryPicker({ mode: 'readwrite' })\n await saveHandle(handle)\n this._handle = handle\n this.notify()\n return true\n } catch {\n // User cancelled the picker\n return false\n }\n }\n\n /** Ensure we have access — try restore silently first, then prompt. */\n private async ensureAccess(): Promise<boolean> {\n if (this._handle) return true\n const restored = await this.tryRestore()\n if (restored) return true\n return this.requestAccess()\n }\n\n async read(relativePath: string): Promise<string> {\n const ok = await this.ensureAccess()\n if (!ok || !this._handle)\n throw new Error('[react-trace] File system access denied')\n\n const file = await getFileHandle(this._handle, relativePath)\n if (!file) throw new Error(`[react-trace] File not found: ${relativePath}`)\n\n return (await file.getFile()).text()\n }\n\n async write(relativePath: string, content: string): Promise<void> {\n const ok = await this.ensureAccess()\n if (!ok || !this._handle)\n throw new Error('[react-trace] File system access denied')\n\n const file = await getFileHandle(this._handle, relativePath, true)\n if (!file)\n throw new Error(\n `[react-trace] Cannot open file for writing: ${relativePath}`,\n )\n\n const writable = await file.createWritable()\n await writable.write(content)\n await writable.close()\n }\n}\n\nexport const fileSystemService: FileSystemService = new FileSystemServiceImpl()\n","import type { TraceSettings } from '@react-spot/core'\nimport { settingsPluginAtom } from '@react-spot/core'\nimport type { WritableAtom } from 'jotai'\nimport type { BundledTheme } from 'shiki'\n\ndeclare module '@react-spot/core' {\n interface TraceSettings {\n preview?: {\n disabled: boolean\n theme: BundledTheme\n }\n }\n}\n\nexport const previewSettingsAtom = settingsPluginAtom(\n 'preview',\n) as WritableAtom<TraceSettings['preview'], [TraceSettings['preview']], void>\n","import { useWidgetPortalContainer } from '@react-spot/core'\nimport { Combobox } from '@react-spot/ui-components'\nimport { useAtom } from 'jotai'\nimport type { CSSProperties } from 'react'\nimport { useRef, useState } from 'react'\nimport type { BundledTheme } from 'shiki'\nimport { bundledThemesInfo } from 'shiki'\n\nimport { previewSettingsAtom } from './store'\n\nconst themes = bundledThemesInfo.map((theme) => ({\n value: theme.id as BundledTheme,\n label: theme.displayName,\n}))\n\nconst themesLookup = themes.reduce<\n Record<(typeof themes)[number]['value'], (typeof themes)[number]>\n>(\n (acc, theme) => {\n acc[theme.value] = theme\n return acc\n },\n {} as Record<(typeof themes)[number]['value'], (typeof themes)[number]>,\n)\n\nconst LABEL_STYLE: CSSProperties = {\n fontSize: 12,\n color: '#d4d4d8',\n fontFamily: 'system-ui, sans-serif',\n}\n\nexport function PreviewSettings() {\n const portalContainer = useWidgetPortalContainer()\n const [themesOpen, setThemesOpen] = useState(false)\n\n const [\n previewSettings = { disabled: false, theme: 'one-dark-pro' },\n setPreviewSettings,\n ] = useAtom(previewSettingsAtom)\n\n const anchorRef = useRef<HTMLDivElement>(null)\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n <label style={LABEL_STYLE}>\n <input\n type=\"checkbox\"\n checked={previewSettings.disabled ?? false}\n onChange={(e) =>\n setPreviewSettings({\n ...previewSettings,\n disabled: e.target.checked,\n })\n }\n />{' '}\n Code editing disabled\n </label>\n </div>\n <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>\n <label style={LABEL_STYLE}>Theme</label>\n <Combobox.Root\n value={themesLookup[previewSettings.theme]}\n onValueChange={(theme) => {\n if (theme) {\n setPreviewSettings({ ...previewSettings, theme: theme.value })\n }\n }}\n items={themes}\n open={themesOpen}\n onOpenChange={setThemesOpen}\n >\n <div ref={anchorRef}>\n <Combobox.Trigger\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => e.stopPropagation()}\n >\n <Combobox.Input />\n </Combobox.Trigger>\n </div>\n\n <Combobox.Portal container={portalContainer}>\n <Combobox.Positioner\n style={{ zIndex: 100000000, pointerEvents: 'auto' }}\n anchor={anchorRef}\n >\n <Combobox.Popup>\n <Combobox.Empty>No themes found</Combobox.Empty>\n <Combobox.List\n style={{\n overscrollBehavior: 'contain',\n maxHeight: 500,\n overflowY: 'auto',\n }}\n >\n {(option: (typeof themes)[number]) => (\n <Combobox.Item\n key={option.value}\n value={option}\n onClick={(e) => e.stopPropagation()}\n >\n <span>{option.label}</span>\n </Combobox.Item>\n )}\n </Combobox.List>\n </Combobox.Popup>\n </Combobox.Positioner>\n </Combobox.Portal>\n </Combobox.Root>\n </div>\n </div>\n )\n}\n","import type { TracePlugin } from '@react-spot/core'\nimport {\n useDeactivateInspector,\n useProjectRoot,\n useWidgetPortalContainer,\n} from '@react-spot/core'\nimport {\n FolderIcon,\n Popover,\n ToolbarButton,\n Tooltip,\n} from '@react-spot/ui-components'\nimport { useEffect, useRef, useState, useSyncExternalStore } from 'react'\n\nimport { FolderAccessPrompt, handleGrantAccess } from './FolderAccessPrompt'\nimport { fileSystemService } from './fs'\nimport { PreviewSettings } from './PreviewSettings'\nimport { SourcePreview } from './SourcePreview'\nimport type { PreviewPluginOptions } from './types'\n\nexport type { PreviewPluginOptions }\n\nfunction FolderToolbarIcon({ hasAccess }: { hasAccess: boolean }) {\n return (\n <span style={{ position: 'relative', display: 'inline-flex' }}>\n <FolderIcon />\n {hasAccess && (\n <span\n style={{\n position: 'absolute',\n top: -4,\n right: -4,\n width: 6,\n height: 6,\n borderRadius: '50%',\n background: '#22c55e',\n border: '1.5px solid #18181b',\n pointerEvents: 'none',\n }}\n />\n )}\n </span>\n )\n}\n\nfunction PreviewToolbar() {\n const projectRoot = useProjectRoot()\n const portalContainer = useWidgetPortalContainer()\n const deactivateInspector = useDeactivateInspector()\n const buttonRef = useRef<HTMLButtonElement>(null)\n const [isPromptOpen, setIsPromptOpen] = useState(false)\n const hasAccess = useSyncExternalStore(\n fileSystemService.subscribe.bind(fileSystemService),\n () => fileSystemService.hasAccess,\n () => false,\n )\n\n // Silently try to restore a previously granted FS handle on mount\n useEffect(() => {\n fileSystemService.tryRestore().catch(() => {})\n }, [])\n\n const handleClick = () => {\n deactivateInspector()\n\n if (fileSystemService.hasAccess) {\n fileSystemService.requestAccess().catch(() => {})\n return\n }\n\n setIsPromptOpen(true)\n }\n\n const handleGrant = async () => {\n const granted = await handleGrantAccess(projectRoot, () =>\n fileSystemService.requestAccess(),\n )\n if (granted) setIsPromptOpen(false)\n }\n\n return (\n <>\n <Tooltip\n label={hasAccess ? 'Project folder connected' : 'Select project folder'}\n container={portalContainer}\n render={<ToolbarButton ref={buttonRef} />}\n aria-label=\"Project folder\"\n style={{\n color: hasAccess ? '#22c55e' : '#52525b',\n }}\n onClick={handleClick}\n >\n <FolderToolbarIcon hasAccess={hasAccess} />\n </Tooltip>\n\n <Popover.Root\n open={isPromptOpen}\n onOpenChange={(open: boolean) => {\n if (!open) setIsPromptOpen(false)\n }}\n >\n <Popover.Portal container={portalContainer}>\n <Popover.Positioner\n anchor={buttonRef.current}\n side=\"top\"\n align=\"end\"\n sideOffset={8}\n collisionPadding={8}\n positionMethod=\"fixed\"\n style={{ zIndex: 9999999, pointerEvents: 'auto' }}\n >\n <Popover.Popup\n style={{ width: 280, overflow: 'hidden' }}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n >\n <FolderAccessPrompt\n root={projectRoot}\n onGrant={handleGrant}\n onCancel={() => setIsPromptOpen(false)}\n />\n </Popover.Popup>\n </Popover.Positioner>\n </Popover.Portal>\n </Popover.Root>\n </>\n )\n}\n\nexport function PreviewPlugin(_options: PreviewPluginOptions = {}): TracePlugin {\n return {\n name: 'preview',\n toolbar: PreviewToolbar,\n settings: PreviewSettings,\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/index.prod.ts
4
+ /**
5
+ * Production stub for @react-spot/plugin-preview.
6
+ * Returns a no-op plugin object — Trace itself renders null in production
7
+ * so the plugin is never used, but the import must resolve to a valid shape.
8
+ */
9
+ const PreviewPlugin = () => ({ name: "preview" });
10
+
11
+ //#endregion
12
+ exports.PreviewPlugin = PreviewPlugin;
13
+ //# sourceMappingURL=index.prod.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.prod.cjs","names":[],"sources":["../src/index.prod.ts"],"sourcesContent":["/**\n * Production stub for @react-spot/plugin-preview.\n * Returns a no-op plugin object — Trace itself renders null in production\n * so the plugin is never used, but the import must resolve to a valid shape.\n */\nexport const PreviewPlugin = () => ({ name: 'preview' as const })\n\nexport type { PreviewPluginOptions } from './types'\n"],"mappings":";;;;;;;;AAKA,MAAa,uBAAuB,EAAE,MAAM,WAAoB"}
@@ -0,0 +1,14 @@
1
+ import { t as PreviewPluginOptions } from "./types-DM7rB7J1.cjs";
2
+
3
+ //#region src/index.prod.d.ts
4
+ /**
5
+ * Production stub for @react-spot/plugin-preview.
6
+ * Returns a no-op plugin object — Trace itself renders null in production
7
+ * so the plugin is never used, but the import must resolve to a valid shape.
8
+ */
9
+ declare const PreviewPlugin: () => {
10
+ name: "preview";
11
+ };
12
+ //#endregion
13
+ export { PreviewPlugin, type PreviewPluginOptions };
14
+ //# sourceMappingURL=index.prod.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.prod.d.cts","names":[],"sources":["../src/index.prod.ts"],"mappings":";;;;;;AAKA;;cAAa,aAAA;EAAoD,IAAA;AAAA"}
@@ -0,0 +1,14 @@
1
+ import { t as PreviewPluginOptions } from "./types-uxyW_RVb.js";
2
+
3
+ //#region src/index.prod.d.ts
4
+ /**
5
+ * Production stub for @react-spot/plugin-preview.
6
+ * Returns a no-op plugin object — Trace itself renders null in production
7
+ * so the plugin is never used, but the import must resolve to a valid shape.
8
+ */
9
+ declare const PreviewPlugin: () => {
10
+ name: "preview";
11
+ };
12
+ //#endregion
13
+ export { PreviewPlugin, type PreviewPluginOptions };
14
+ //# sourceMappingURL=index.prod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.prod.d.ts","names":[],"sources":["../src/index.prod.ts"],"mappings":";;;;;;AAKA;;cAAa,aAAA;EAAoD,IAAA;AAAA"}
@@ -0,0 +1,11 @@
1
+ //#region src/index.prod.ts
2
+ /**
3
+ * Production stub for @react-spot/plugin-preview.
4
+ * Returns a no-op plugin object — Trace itself renders null in production
5
+ * so the plugin is never used, but the import must resolve to a valid shape.
6
+ */
7
+ const PreviewPlugin = () => ({ name: "preview" });
8
+
9
+ //#endregion
10
+ export { PreviewPlugin };
11
+ //# sourceMappingURL=index.prod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.prod.js","names":[],"sources":["../src/index.prod.ts"],"sourcesContent":["/**\n * Production stub for @react-spot/plugin-preview.\n * Returns a no-op plugin object — Trace itself renders null in production\n * so the plugin is never used, but the import must resolve to a valid shape.\n */\nexport const PreviewPlugin = () => ({ name: 'preview' as const })\n\nexport type { PreviewPluginOptions } from './types'\n"],"mappings":";;;;;;AAKA,MAAa,uBAAuB,EAAE,MAAM,WAAoB"}
@@ -0,0 +1,12 @@
1
+ import { BundledTheme } from "shiki";
2
+
3
+ //#region src/types.d.ts
4
+ interface PreviewPluginOptions {
5
+ /** Disable editing. @default false */
6
+ disabled?: boolean;
7
+ /** Shiki theme ID. @default 'one-dark-pro' — any https://shiki.style/themes value works. */
8
+ theme?: BundledTheme;
9
+ }
10
+ //#endregion
11
+ export { PreviewPluginOptions as t };
12
+ //# sourceMappingURL=types-DM7rB7J1.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-DM7rB7J1.d.cts","names":[],"sources":["../src/types.ts"],"mappings":";;;UAEiB,oBAAA;;EAEf,QAAA;EAFmC;EAInC,KAAA,GAAQ,YAAA;AAAA"}
@@ -0,0 +1,12 @@
1
+ import { BundledTheme } from "shiki";
2
+
3
+ //#region src/types.d.ts
4
+ interface PreviewPluginOptions {
5
+ /** Disable editing. @default false */
6
+ disabled?: boolean;
7
+ /** Shiki theme ID. @default 'one-dark-pro' — any https://shiki.style/themes value works. */
8
+ theme?: BundledTheme;
9
+ }
10
+ //#endregion
11
+ export { PreviewPluginOptions as t };
12
+ //# sourceMappingURL=types-uxyW_RVb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-uxyW_RVb.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;UAEiB,oBAAA;;EAEf,QAAA;EAFmC;EAInC,KAAA,GAAQ,YAAA;AAAA"}
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "@react-spot/plugin-preview",
3
+ "version": "0.0.1",
4
+ "description": "react-trace plugin: Monaco-powered source code preview for inspected components",
5
+ "keywords": [
6
+ "react-trace",
7
+ "react-trace-plugin"
8
+ ],
9
+ "license": "MIT",
10
+ "author": "Vitor Buzinaro",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/buzinas/react-trace",
14
+ "directory": "packages/plugin-preview"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "type": "module",
20
+ "main": "./dist/index.cjs",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "development": {
26
+ "import": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "require": {
31
+ "types": "./dist/index.d.cts",
32
+ "default": "./dist/index.cjs"
33
+ }
34
+ },
35
+ "production": {
36
+ "import": {
37
+ "types": "./dist/index.d.ts",
38
+ "default": "./dist/index.prod.js"
39
+ },
40
+ "require": {
41
+ "types": "./dist/index.d.cts",
42
+ "default": "./dist/index.prod.cjs"
43
+ }
44
+ },
45
+ "default": {
46
+ "import": {
47
+ "types": "./dist/index.d.ts",
48
+ "default": "./dist/index.js"
49
+ },
50
+ "require": {
51
+ "types": "./dist/index.d.cts",
52
+ "default": "./dist/index.cjs"
53
+ }
54
+ }
55
+ }
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "scripts": {
61
+ "build": "tsdown",
62
+ "dev": "tsdown --watch --no-clean",
63
+ "typecheck": "tsc --noEmit",
64
+ "lint": "oxlint src",
65
+ "prepublishOnly": "pnpm build"
66
+ },
67
+ "dependencies": {
68
+ "@monaco-editor/react": "^4.7.0",
69
+ "@shikijs/monaco": "^4.0.1",
70
+ "shiki": "^4.0.1"
71
+ },
72
+ "devDependencies": {
73
+ "@types/node": "^24.11.0",
74
+ "@types/react": "^19",
75
+ "@types/react-dom": "^19.2.3",
76
+ "@types/wicg-file-system-access": "^2023.10.7",
77
+ "monaco-types": "^0.1.2",
78
+ "oxlint": "latest",
79
+ "react": "^19",
80
+ "react-dom": "^19.2.4",
81
+ "tsdown": "^0.21.0-beta.2",
82
+ "typescript": "^5"
83
+ },
84
+ "peerDependencies": {
85
+ "@react-spot/core": "^0.0.1",
86
+ "@react-spot/ui-components": "^0.0.1",
87
+ "jotai": "^2.18.0",
88
+ "react": ">=18"
89
+ }
90
+ }