json-pane.tsx 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. "use client";
  2. import { useMemo, useState, useEffect, type ReactNode } from "react";
  3. import { stringify } from "yaml";
  4. import { useEditorStore } from "@/lib/store";
  5. import { sceneToSpec } from "@/lib/scene-to-spec";
  6. const YAML_TOKEN_RE =
  7. /(^[ \t]*[\w][\w.-]*:(?=\s|$))|("(?:\\.|[^"\\])*"|'(?:\\'|[^'\\])*')|(true|false)|(null|~)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?(?=\s|$))/gm;
  8. function highlightYaml(yaml: string): ReactNode[] {
  9. const parts: ReactNode[] = [];
  10. let lastIndex = 0;
  11. let match: RegExpExecArray | null;
  12. while ((match = YAML_TOKEN_RE.exec(yaml)) !== null) {
  13. if (match.index > lastIndex) {
  14. parts.push(yaml.slice(lastIndex, match.index));
  15. }
  16. const [full, key, str, bool, nil, num] = match;
  17. let color: string;
  18. if (key) color = "#c4a7e7";
  19. else if (str) color = "#a8d4a2";
  20. else if (bool) color = "#f6c177";
  21. else if (nil) color = "#6e6a86";
  22. else if (num) color = "#ebbcba";
  23. else color = "#8a8a8a";
  24. parts.push(
  25. <span key={match.index} style={{ color }}>
  26. {full}
  27. </span>,
  28. );
  29. lastIndex = match.index + full.length;
  30. }
  31. if (lastIndex < yaml.length) {
  32. parts.push(yaml.slice(lastIndex));
  33. }
  34. return parts;
  35. }
  36. export function JsonPane() {
  37. const [mounted, setMounted] = useState(false);
  38. const scenes = useEditorStore((s) => s.scenes);
  39. const activeSceneId = useEditorStore((s) => s.activeSceneId);
  40. useEffect(() => setMounted(true), []);
  41. const activeScene = useMemo(
  42. () => scenes.find((s) => s.id === activeSceneId) || scenes[0],
  43. [scenes, activeSceneId],
  44. );
  45. const spec = useMemo(() => {
  46. if (!activeScene) return null;
  47. return sceneToSpec(activeScene);
  48. }, [activeScene]);
  49. const yaml = useMemo(() => {
  50. if (!spec) return "";
  51. return stringify(spec, { indent: 2, lineWidth: 0 });
  52. }, [spec]);
  53. const highlighted = useMemo(() => {
  54. if (!yaml) return null;
  55. return highlightYaml(yaml);
  56. }, [yaml]);
  57. return (
  58. <div className="flex flex-col h-full">
  59. <div className="h-9 flex items-center px-3 border-b border-[#1e1e1e] flex-shrink-0">
  60. <span className="text-[10px] font-semibold text-[#555] uppercase tracking-wider font-mono">
  61. Spec
  62. </span>
  63. </div>
  64. <div className="flex-1 overflow-auto">
  65. <pre className="p-3 text-[11px] leading-[1.5] text-[#555] font-mono whitespace-pre select-all">
  66. {mounted ? highlighted : ""}
  67. </pre>
  68. </div>
  69. </div>
  70. );
  71. }