diff --git a/src/features/editor/components/EditorControls.tsx b/src/features/editor/components/EditorControls.tsx index de7ee84..9e8157c 100644 --- a/src/features/editor/components/EditorControls.tsx +++ b/src/features/editor/components/EditorControls.tsx @@ -1,5 +1,6 @@ import { Box, + Braces, Download, Expand, Keyboard, @@ -11,12 +12,13 @@ import { Save, Undo2, } from "lucide-react"; -import type { TransformMode } from "@/types/editor"; +import type { MapNode, TransformMode } from "@/types/editor"; interface EditorControlsProps { transformMode: TransformMode; onTransformModeChange: (mode: TransformMode) => void; selectedNodeIndex: number | null; + mapNodes: MapNode[]; nodesCount: number; selectedNodeName: string | null; undoCount: number; @@ -33,6 +35,7 @@ export function EditorControls({ transformMode, onTransformModeChange, selectedNodeIndex, + mapNodes, nodesCount, selectedNodeName, undoCount, @@ -46,6 +49,7 @@ export function EditorControls({ }: EditorControlsProps): React.JSX.Element { const cameraPosition = [0, 50, 100]; const viewModeLabel = isPlayerMode ? "View locked" : "Lock view"; + const jsonPreview = getJsonPreview(mapNodes, selectedNodeIndex); return ( <> @@ -230,7 +234,100 @@ export function EditorControls({ + +
+
+

JSON

+ {jsonPreview.label} +
+ +
+            {jsonPreview.lines.map((line) => (
+              
+                {line.number}
+                {line.content || " "}
+              
+            ))}
+          
+ +
+
+
); } + +interface JsonPreviewLine { + number: number; + content: string; + isSelected: boolean; +} + +interface JsonPreview { + label: string; + lines: JsonPreviewLine[]; +} + +function getJsonPreview( + mapNodes: MapNode[], + selectedNodeIndex: number | null, +): JsonPreview { + const { lines, ranges } = formatMapNodesWithRanges(mapNodes); + + if (selectedNodeIndex === null || !ranges[selectedNodeIndex]) { + return { + label: `${lines.length} raw lines`, + lines: lines.map((content, index) => ({ + number: index + 1, + content, + isSelected: false, + })), + }; + } + + const range = ranges[selectedNodeIndex]; + const selectedLines = lines.slice(range.start - 1, range.end); + + return { + label: `Lines ${range.start}-${range.end}`, + lines: selectedLines.map((content, index) => ({ + number: range.start + index, + content, + isSelected: true, + })), + }; +} + +function formatMapNodesWithRanges(mapNodes: MapNode[]): { + lines: string[]; + ranges: Array<{ start: number; end: number }>; +} { + const lines = ["["]; + const ranges: Array<{ start: number; end: number }> = []; + + mapNodes.forEach((node, index) => { + const objectLines = JSON.stringify(node, null, 2) + .split("\n") + .map((line) => ` ${line}`); + + if (index < mapNodes.length - 1) { + objectLines[objectLines.length - 1] += ","; + } + + const start = lines.length + 1; + lines.push(...objectLines); + ranges.push({ start, end: lines.length }); + }); + + lines.push("]"); + + return { lines, ranges }; +} diff --git a/src/index.css b/src/index.css index 9438bd8..d59a38b 100644 --- a/src/index.css +++ b/src/index.css @@ -262,6 +262,8 @@ canvas { border-radius: 28px; box-shadow: 0 24px 90px rgba(0, 0, 0, 0.45); overflow-y: auto; + display: flex; + flex-direction: column; backdrop-filter: blur(22px); scrollbar-width: thin; scrollbar-color: #3a3a3a transparent; @@ -541,6 +543,81 @@ canvas { text-align: right; } +.editor-json-section { + display: flex; + flex-direction: column; + min-height: 240px; + padding: 14px 12px 12px; + border-top: 1px solid rgba(255, 255, 255, 0.09); +} + +.editor-json-view { + flex: 1; + max-height: 320px; + margin: 0; + padding: 8px 0; + overflow: auto; + background: #050505; + border: 1px solid #1f1f1f; + border-radius: 16px; + color: #d7d7d7; + font-family: "SFMono-Regular", "Courier New", monospace; + font-size: 0.72rem; + line-height: 1.55; + scrollbar-width: thin; + scrollbar-color: #3a3a3a transparent; +} + +.editor-json-view::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.editor-json-view::-webkit-scrollbar-thumb { + background: #3a3a3a; + border-radius: 999px; +} + +.editor-json-view code { + display: grid; + grid-template-columns: 34px max-content; + gap: 10px; + min-width: 100%; + padding: 0 12px; + background: transparent; + color: inherit; + font-family: inherit; + white-space: pre; +} + +.editor-json-view code span { + color: #5f5f5f; + text-align: right; + user-select: none; +} + +.editor-json-view code.is-selected { + background: #111111; + color: #f2f2f2; +} + +.editor-json-view code.is-selected * { + color: #f2f2f2; +} + +.editor-json-view code.is-selected span { + color: #8a8a8a; +} + +.editor-json-hint { + display: flex; + align-items: center; + gap: 7px; + margin-top: 8px; + color: #8d8d8d; + font-size: 0.74rem; +} + @media (max-width: 768px) { .editor-error h2 { font-size: 1.5rem; @@ -567,4 +644,8 @@ canvas { max-height: 46vh; border-radius: 22px; } + + .editor-json-section { + min-height: 180px; + } } diff --git a/src/pages/editor/EditorPage.tsx b/src/pages/editor/EditorPage.tsx index f9022ac..fbe5b34 100644 --- a/src/pages/editor/EditorPage.tsx +++ b/src/pages/editor/EditorPage.tsx @@ -173,6 +173,7 @@ export function EditorPage(): React.JSX.Element { transformMode={transformMode} onTransformModeChange={handleTransformModeChange} selectedNodeIndex={selectedNodeIndex} + mapNodes={sceneData.mapNodes} nodesCount={sceneData.mapNodes.length} selectedNodeName={ selectedNodeIndex !== null && sceneData.mapNodes[selectedNodeIndex]