diff --git a/bun.lock b/bun.lock index 97946082..1b1ea09a 100644 --- a/bun.lock +++ b/bun.lock @@ -46,6 +46,8 @@ "@types/three": "^0.182.0", "@typescript/native-preview": "^7.0.0-dev.20260107.1", "@uiw/react-textarea-code-editor": "^3.1.1", + "@visual-json/core": "^0.1.1", + "@visual-json/react": "^0.1.1", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "argon2": "^0.44.0", @@ -1051,6 +1053,10 @@ "@use-gesture/react": ["@use-gesture/react@10.3.1", "", { "dependencies": { "@use-gesture/core": "10.3.1" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g=="], + "@visual-json/core": ["@visual-json/core@0.1.1", "", {}, "sha512-VLARpQnjJU1SnnYk1NiXScGfp2FyAwyZS2kA8hIJV5OzkaH2oS1dTHsaB8vOiyi14/L7jsbpA4g/EwzRWbxQmg=="], + + "@visual-json/react": ["@visual-json/react@0.1.1", "", { "dependencies": { "@visual-json/core": "0.1.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-KCpFYmBhH5F+xctfvxxVX7mVZK/iwvDwh/ITNkGPzEKXTIJQ16fau+hrG7m7QoTAOxvsdhHtHoCUiEjNHXboUQ=="], + "@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="], "@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="], diff --git a/package.json b/package.json index 7d3ddab7..1c89b08b 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,8 @@ "@types/three": "^0.182.0", "@typescript/native-preview": "^7.0.0-dev.20260107.1", "@uiw/react-textarea-code-editor": "^3.1.1", + "@visual-json/core": "^0.1.1", + "@visual-json/react": "^0.1.1", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "argon2": "^0.44.0", @@ -100,6 +102,7 @@ "node-pty": "^1.1.0", "nodemailer": "^7.0.12", "officerdb": "workspace:*", + "officerdev": "workspace:*", "pg": "^8.16.3", "postgres": "^3.4.5", "react": "^19", @@ -125,7 +128,6 @@ "three": "^0.182.0", "types": "workspace:*", "vaul": "^1.1.2", - "officerdev": "workspace:*", "widgets": "workspace:*", "ws": "^8.18.1", "zod": "^4.2.1" diff --git a/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx b/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx index eed6d0fb..47801dee 100644 --- a/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx +++ b/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx @@ -1,4 +1,4 @@ -import { useRef } from 'react'; +import { useMemo, useRef } from 'react'; import { Loader2, FolderArchive } from 'lucide-react'; import { getLang, getRawUrl, getTranscodeUrl, needsTranscode, getArchiveBaseName } from './file-types'; import { useFileViewer } from './FileViewerContext'; @@ -11,6 +11,7 @@ import { MarkdownRenderer } from './renderers/MarkdownRenderer'; import { TextRenderer } from './renderers/TextRenderer'; import { ScrollToTopButton } from './renderers/ScrollToTopButton'; import { EditorRenderer } from './renderers/EditorRenderer'; +import { JsonEditorRenderer } from './renderers/JsonEditorRenderer'; export const FileViewerBody = () => { const { filePath, fileName, root, fileType, content, loading, error, autoPlay, editing, setContent } = useFileViewer(); @@ -19,6 +20,17 @@ export const FileViewerBody = () => { const videoSrc = fileType === 'video' ? (needsTranscode(fileName) ? getTranscodeUrl(filePath, root) : getRawUrl(filePath, root)) : ''; + const isJson = useMemo(() => { + if (!content) return false; + if (/\.jsonc?$/i.test(fileName)) return true; + try { + const parsed = JSON.parse(content); + return typeof parsed === 'object' && parsed !== null; + } catch { + return false; + } + }, [content, fileName]); + return (
{loading ? ( @@ -43,8 +55,15 @@ export const FileViewerBody = () => { ) : fileType === 'audio' ? ( + ) : content !== null && editing && isJson ? ( + ) : content !== null && editing ? ( + ) : content !== null && isJson ? ( +
+ + +
) : content !== null ? ( fileType === 'code' ? (
diff --git a/src/workspaces/officerdev/src/apps/FileViewer/FileViewerContext.tsx b/src/workspaces/officerdev/src/apps/FileViewer/FileViewerContext.tsx index f25aa376..65892954 100644 --- a/src/workspaces/officerdev/src/apps/FileViewer/FileViewerContext.tsx +++ b/src/workspaces/officerdev/src/apps/FileViewer/FileViewerContext.tsx @@ -16,6 +16,7 @@ export type FileViewerContextValue = { extractAudioLoading: boolean; extractLoading: boolean; autoPlay: boolean; + editable: boolean; editing: boolean; setEditing: (editing: boolean) => void; setContent: (content: string) => void; diff --git a/src/workspaces/officerdev/src/apps/FileViewer/FileViewerHeader.tsx b/src/workspaces/officerdev/src/apps/FileViewer/FileViewerHeader.tsx index e4ec43ba..cf199935 100644 --- a/src/workspaces/officerdev/src/apps/FileViewer/FileViewerHeader.tsx +++ b/src/workspaces/officerdev/src/apps/FileViewer/FileViewerHeader.tsx @@ -22,6 +22,7 @@ export const FileViewerHeader = () => { handleDownload, handleSaveResult, saveResultLoading, + editable, editing, setEditing, } = useFileViewer(); @@ -48,7 +49,7 @@ export const FileViewerHeader = () => { {fileType === 'code' ? getLang(fileName) : fileType} - {(fileType === 'markdown' || fileType === 'code' || fileType === 'text') && content && ( + {editable && content && (