@jgengine/editor 0.11.0 → 0.12.0

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.
Files changed (169) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +12 -0
  3. package/dist/AssetBrowser.d.ts +26 -1
  4. package/dist/AssetBrowser.js +54 -1
  5. package/dist/AssetDropZone.d.ts +9 -0
  6. package/dist/AssetDropZone.js +65 -0
  7. package/dist/CatalogsPanel.d.ts +4 -2
  8. package/dist/CatalogsPanel.js +162 -9
  9. package/dist/CityPreview.d.ts +11 -0
  10. package/dist/CityPreview.js +45 -0
  11. package/dist/CollectionsPanel.js +5 -5
  12. package/dist/DebugDraw.d.ts +3 -1
  13. package/dist/DebugDraw.js +37 -19
  14. package/dist/EditorApp.d.ts +9 -1
  15. package/dist/EditorApp.js +147 -35
  16. package/dist/EditorChrome.d.ts +25 -6
  17. package/dist/EditorChrome.js +310 -130
  18. package/dist/InspectorPanel.d.ts +17 -7
  19. package/dist/InspectorPanel.js +173 -27
  20. package/dist/LightingPanel.d.ts +11 -0
  21. package/dist/LightingPanel.js +99 -0
  22. package/dist/MaterialsWorkspacePanel.d.ts +12 -0
  23. package/dist/MaterialsWorkspacePanel.js +86 -0
  24. package/dist/ParentPickerMenu.d.ts +13 -0
  25. package/dist/ParentPickerMenu.js +33 -0
  26. package/dist/PerfProbe.js +33 -0
  27. package/dist/PrefabsPanel.js +5 -5
  28. package/dist/RuntimePlayBridge.d.ts +5 -3
  29. package/dist/RuntimePlayBridge.js +6 -13
  30. package/dist/ScatterPreview.d.ts +3 -1
  31. package/dist/ScatterPreview.js +10 -7
  32. package/dist/SchemaInspector.js +21 -8
  33. package/dist/ScriptingPanel.d.ts +15 -0
  34. package/dist/ScriptingPanel.js +46 -0
  35. package/dist/SelectionGizmo.js +114 -30
  36. package/dist/StandaloneEditor.d.ts +16 -9
  37. package/dist/StandaloneEditor.js +19 -20
  38. package/dist/TerrainPanel.d.ts +3 -2
  39. package/dist/TerrainPanel.js +16 -5
  40. package/dist/TerrainSculpt.js +98 -59
  41. package/dist/agent/AgentPanel.d.ts +5 -2
  42. package/dist/agent/AgentPanel.js +11 -6
  43. package/dist/agent/toolBridge.js +5 -0
  44. package/dist/assetImport.d.ts +36 -0
  45. package/dist/assetImport.js +66 -0
  46. package/dist/authoredComponentMeta.d.ts +25 -0
  47. package/dist/authoredComponentMeta.js +65 -0
  48. package/dist/gizmoPivot.d.ts +4 -0
  49. package/dist/gizmoPivot.js +34 -0
  50. package/dist/handlers/catalogs.d.ts +3 -0
  51. package/dist/handlers/catalogs.js +137 -0
  52. package/dist/handlers/collections.d.ts +3 -0
  53. package/dist/handlers/collections.js +57 -0
  54. package/dist/handlers/context.d.ts +36 -0
  55. package/dist/handlers/context.js +1 -0
  56. package/dist/handlers/document.d.ts +3 -0
  57. package/dist/handlers/document.js +350 -0
  58. package/dist/handlers/grids.d.ts +3 -0
  59. package/dist/handlers/grids.js +142 -0
  60. package/dist/handlers/hierarchy.d.ts +3 -0
  61. package/dist/handlers/hierarchy.js +55 -0
  62. package/dist/handlers/index.d.ts +8 -0
  63. package/dist/handlers/index.js +23 -0
  64. package/dist/handlers/minimap.d.ts +9 -0
  65. package/dist/handlers/minimap.js +32 -0
  66. package/dist/handlers/runtime.d.ts +3 -0
  67. package/dist/handlers/runtime.js +191 -0
  68. package/dist/handlers/terrain.d.ts +3 -0
  69. package/dist/handlers/terrain.js +204 -0
  70. package/dist/index.d.ts +3 -2
  71. package/dist/index.js +3 -2
  72. package/dist/listDocumentTriggers.d.ts +42 -0
  73. package/dist/listDocumentTriggers.js +49 -0
  74. package/dist/materialAssignments.d.ts +63 -0
  75. package/dist/materialAssignments.js +84 -0
  76. package/dist/mcp/fieldSpec.d.ts +32 -0
  77. package/dist/mcp/fieldSpec.js +173 -0
  78. package/dist/mcp/rpcRequest.d.ts +8 -5
  79. package/dist/mcp/rpcRequest.js +72 -93
  80. package/dist/mcp/tools.js +104 -912
  81. package/dist/networkSnapshot.d.ts +82 -0
  82. package/dist/networkSnapshot.js +73 -0
  83. package/dist/parentCandidates.d.ts +12 -0
  84. package/dist/parentCandidates.js +25 -0
  85. package/dist/session.d.ts +77 -4
  86. package/dist/session.js +36 -1017
  87. package/dist/shell/AnimationPanel.d.ts +17 -0
  88. package/dist/shell/AnimationPanel.js +134 -0
  89. package/dist/shell/AssetThumbnail.d.ts +12 -0
  90. package/dist/shell/AssetThumbnail.js +23 -0
  91. package/dist/shell/BottomDock.d.ts +25 -0
  92. package/dist/shell/BottomDock.js +40 -0
  93. package/dist/shell/CommandPalette.d.ts +12 -0
  94. package/dist/shell/CommandPalette.js +56 -0
  95. package/dist/shell/ConsolePanel.d.ts +8 -0
  96. package/dist/shell/ConsolePanel.js +45 -0
  97. package/dist/shell/ContentBrowser.d.ts +21 -0
  98. package/dist/shell/ContentBrowser.js +130 -0
  99. package/dist/shell/HierarchyPanel.d.ts +26 -0
  100. package/dist/shell/HierarchyPanel.js +387 -0
  101. package/dist/shell/NetworkWorkspacePanel.d.ts +11 -0
  102. package/dist/shell/NetworkWorkspacePanel.js +49 -0
  103. package/dist/shell/PlayModeBar.d.ts +12 -0
  104. package/dist/shell/PlayModeBar.js +27 -0
  105. package/dist/shell/ProfilerPanel.d.ts +10 -0
  106. package/dist/shell/ProfilerPanel.js +41 -0
  107. package/dist/shell/SceneToolbar.d.ts +43 -0
  108. package/dist/shell/SceneToolbar.js +122 -0
  109. package/dist/shell/StatusBar.d.ts +18 -0
  110. package/dist/shell/StatusBar.js +41 -0
  111. package/dist/shell/TopAppBar.d.ts +33 -0
  112. package/dist/shell/TopAppBar.js +64 -0
  113. package/dist/shell/ViewportOverlays.d.ts +23 -0
  114. package/dist/shell/ViewportOverlays.js +136 -0
  115. package/dist/shell/WorkspaceRail.d.ts +20 -0
  116. package/dist/shell/WorkspaceRail.js +31 -0
  117. package/dist/shell/cameraTelemetry.d.ts +27 -0
  118. package/dist/shell/cameraTelemetry.js +24 -0
  119. package/dist/shell/commandRegistry.d.ts +58 -0
  120. package/dist/shell/commandRegistry.js +133 -0
  121. package/dist/shell/consoleSink.d.ts +11 -0
  122. package/dist/shell/consoleSink.js +17 -0
  123. package/dist/shell/consoleStore.d.ts +27 -0
  124. package/dist/shell/consoleStore.js +43 -0
  125. package/dist/shell/fields.d.ts +39 -0
  126. package/dist/shell/fields.js +104 -0
  127. package/dist/shell/formatSavedRelative.d.ts +2 -0
  128. package/dist/shell/formatSavedRelative.js +14 -0
  129. package/dist/shell/glbThumbnailCache.d.ts +45 -0
  130. package/dist/shell/glbThumbnailCache.js +241 -0
  131. package/dist/shell/hierarchyA11y.d.ts +21 -0
  132. package/dist/shell/hierarchyA11y.js +35 -0
  133. package/dist/shell/icons.d.ts +13 -0
  134. package/dist/shell/icons.js +85 -0
  135. package/dist/shell/layoutStore.d.ts +68 -0
  136. package/dist/shell/layoutStore.js +156 -0
  137. package/dist/shell/pathFlythrough.d.ts +37 -0
  138. package/dist/shell/pathFlythrough.js +54 -0
  139. package/dist/shell/perfHistory.d.ts +70 -0
  140. package/dist/shell/perfHistory.js +130 -0
  141. package/dist/shell/renameObject.d.ts +3 -0
  142. package/dist/shell/renameObject.js +25 -0
  143. package/dist/shell/theme.d.ts +36 -0
  144. package/dist/shell/theme.js +37 -0
  145. package/dist/shell/ui.d.ts +84 -0
  146. package/dist/shell/ui.js +90 -0
  147. package/dist/shell/useGlbThumbnail.d.ts +8 -0
  148. package/dist/shell/useGlbThumbnail.js +19 -0
  149. package/dist/uiStore.d.ts +27 -0
  150. package/dist/uiStore.js +10 -0
  151. package/dist/viewportContextMenu.d.ts +4 -2
  152. package/dist/viewportContextMenu.js +5 -1
  153. package/package.json +4 -4
  154. package/skills/jgengine/SKILL.md +7 -4
  155. package/skills/jgengine/api.md +172 -79
  156. package/skills/jgengine/capabilities.md +104 -2
  157. package/skills/jgengine/recipes/minimal-game.md +88 -0
  158. package/skills/jgengine-editor/SKILL.md +18 -3
  159. package/skills/jgengine-editor/api.md +397 -18
  160. package/skills/jgengine-editor/capabilities.md +46 -0
  161. package/skills/jgengine-editor/reference.md +87 -0
  162. package/skills/level-design/SKILL.md +54 -0
  163. package/skills/level-design/agents/openai.yaml +4 -0
  164. package/skills/level-design/references/application-playbook.md +192 -0
  165. package/skills/level-design/references/field-guide.md +337 -0
  166. package/skills/level-design/references/live-editor-loop.md +179 -0
  167. package/dist/OutlinerPanel.d.ts +0 -14
  168. package/dist/OutlinerPanel.js +0 -84
  169. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -0,0 +1,17 @@
1
+ import type { EditorSession } from "@jgengine/core/editor/index";
2
+ import type { EditorHostApi } from "../session.js";
3
+ /**
4
+ * Animation dock: path flythrough scrubber over authored scene polylines.
5
+ *
6
+ * Scope (honest, no fake NLE):
7
+ * - Scrub / play the editor orbit camera along document paths via {@link EditorHostApi.setFocusTarget}.
8
+ * - Uses real path points + `@jgengine/core` path-follow sampling — not invented keyframes.
9
+ * - Out of scope: clip authoring, keyframe curves, scene-kind param animation, director cinematics
10
+ * stored outside the scene document.
11
+ *
12
+ * @internal
13
+ */
14
+ export declare function AnimationPanel({ session, api }: {
15
+ session: EditorSession;
16
+ api: EditorHostApi;
17
+ }): import("react").JSX.Element;
@@ -0,0 +1,134 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { useStoreSelector } from "../useStoreSelector.js";
4
+ import { listScrubbablePaths, samplePathAt } from "./pathFlythrough.js";
5
+ import { FOCUS_RING, INPUT_CLS, NUMERIC } from "./theme.js";
6
+ import { EmptyState, IconButton } from "./ui.js";
7
+ /** Default world-units/sec when playing a path flythrough preview. */
8
+ const DEFAULT_PLAY_SPEED = 8;
9
+ /**
10
+ * Animation dock: path flythrough scrubber over authored scene polylines.
11
+ *
12
+ * Scope (honest, no fake NLE):
13
+ * - Scrub / play the editor orbit camera along document paths via {@link EditorHostApi.setFocusTarget}.
14
+ * - Uses real path points + `@jgengine/core` path-follow sampling — not invented keyframes.
15
+ * - Out of scope: clip authoring, keyframe curves, scene-kind param animation, director cinematics
16
+ * stored outside the scene document.
17
+ *
18
+ * @internal
19
+ */
20
+ export function AnimationPanel({ session, api }) {
21
+ const paths = useStoreSelector(session, (state) => state.document.paths);
22
+ const scrubbable = useMemo(() => listScrubbablePaths(paths), [paths]);
23
+ const [selectedId, setSelectedId] = useState(null);
24
+ const [progress, setProgress] = useState(0);
25
+ const [playing, setPlaying] = useState(false);
26
+ const [loop, setLoop] = useState(true);
27
+ const playRef = useRef({ playing: false, loop: true, progress: 0, pathId: null });
28
+ const selected = scrubbable.find((entry) => entry.id === selectedId) ?? scrubbable[0] ?? null;
29
+ const selectedPath = selected === null ? undefined : paths.find((path) => path.id === selected.id);
30
+ useEffect(() => {
31
+ if (selected !== null && selectedId !== selected.id)
32
+ setSelectedId(selected.id);
33
+ if (selected === null && selectedId !== null)
34
+ setSelectedId(null);
35
+ }, [selected, selectedId]);
36
+ useEffect(() => {
37
+ playRef.current = {
38
+ playing,
39
+ loop,
40
+ progress,
41
+ pathId: selected?.id ?? null,
42
+ };
43
+ }, [playing, loop, progress, selected?.id]);
44
+ const applyProgress = useCallback((next, pathId) => {
45
+ const path = session.getState().document.paths.find((entry) => entry.id === pathId);
46
+ if (path === undefined)
47
+ return;
48
+ const sample = samplePathAt(path, next);
49
+ if (sample === null)
50
+ return;
51
+ playRef.current.progress = next;
52
+ setProgress(next);
53
+ api.setFocusTarget({ x: sample.x, y: sample.y, z: sample.z });
54
+ }, [api, session]);
55
+ const onSelectPath = useCallback((id) => {
56
+ playRef.current.playing = false;
57
+ setPlaying(false);
58
+ setSelectedId(id);
59
+ playRef.current.pathId = id;
60
+ applyProgress(0, id);
61
+ session.dispatch({ type: "select", ids: [id] });
62
+ }, [applyProgress, session]);
63
+ const onScrub = useCallback((value) => {
64
+ if (selected === null)
65
+ return;
66
+ playRef.current.playing = false;
67
+ setPlaying(false);
68
+ applyProgress(value, selected.id);
69
+ }, [applyProgress, selected]);
70
+ useEffect(() => {
71
+ if (!playing || selected === null || selected.length <= 0)
72
+ return;
73
+ playRef.current.playing = true;
74
+ playRef.current.pathId = selected.id;
75
+ let frame = 0;
76
+ let last = performance.now();
77
+ const tick = (now) => {
78
+ const dt = Math.min(0.05, Math.max(0, (now - last) / 1000));
79
+ last = now;
80
+ const current = playRef.current;
81
+ if (!current.playing || current.pathId === null)
82
+ return;
83
+ const path = session.getState().document.paths.find((entry) => entry.id === current.pathId);
84
+ if (path === undefined) {
85
+ playRef.current.playing = false;
86
+ setPlaying(false);
87
+ return;
88
+ }
89
+ const sample = samplePathAt(path, current.progress);
90
+ if (sample === null || sample.length <= 0) {
91
+ playRef.current.playing = false;
92
+ setPlaying(false);
93
+ return;
94
+ }
95
+ let next = current.progress + (DEFAULT_PLAY_SPEED * dt) / sample.length;
96
+ if (next >= 1) {
97
+ if (current.loop) {
98
+ next = next % 1;
99
+ }
100
+ else {
101
+ applyProgress(1, path.id);
102
+ playRef.current.playing = false;
103
+ setPlaying(false);
104
+ return;
105
+ }
106
+ }
107
+ applyProgress(next, path.id);
108
+ frame = requestAnimationFrame(tick);
109
+ };
110
+ frame = requestAnimationFrame(tick);
111
+ return () => {
112
+ cancelAnimationFrame(frame);
113
+ playRef.current.playing = false;
114
+ };
115
+ }, [playing, selected, applyProgress, session]);
116
+ if (scrubbable.length === 0) {
117
+ return (_jsx(EmptyState, { icon: "film", title: "Path flythrough", badge: "Scoped", description: "Scope: scrub the orbit camera along authored scene paths (routes, roads, patrols). Draw a path with 2+ points via Add \u2192 path/road, then open this tab. Keyframe clip authoring and scene-kind param animation are not in scope." }));
118
+ }
119
+ const lengthLabel = selected === null ? "—" : selected.length < 10 ? `${selected.length.toFixed(1)} m` : `${Math.round(selected.length)} m`;
120
+ const distance = selected === null || selected.length <= 0 ? 0 : progress * selected.length;
121
+ return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex h-8 shrink-0 items-center gap-1.5 border-b border-white/[0.06] px-2", children: [_jsx("span", { className: "text-[10px] uppercase tracking-wider text-neutral-500", children: "Path flythrough" }), _jsxs("span", { className: `text-[10px] text-neutral-600 ${NUMERIC}`, children: [scrubbable.length, " path", scrubbable.length === 1 ? "" : "s", " \u00B7 camera follows scrub"] }), _jsxs("div", { className: "ml-auto flex items-center gap-1", children: [_jsx(IconButton, { icon: playing ? "pause" : "play", label: playing ? "Pause flythrough" : "Play flythrough", size: 12, active: playing, disabled: selected === null || selected.length <= 0, onClick: () => setPlaying((value) => !value) }), _jsx("button", { type: "button", className: `rounded-[5px] border px-2 py-0.5 text-[10px] uppercase tracking-wider transition-colors ${FOCUS_RING} ${loop
122
+ ? "border-cyan-400/30 bg-cyan-500/15 text-cyan-200"
123
+ : "border-white/[0.07] bg-white/[0.02] text-neutral-500 hover:text-neutral-300"}`, "aria-pressed": loop, "aria-label": loop ? "Loop on" : "Loop off", onClick: () => setLoop((value) => !value), children: "Loop" })] })] }), _jsxs("div", { className: "flex min-h-0 flex-1 gap-3 overflow-auto p-3", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: [_jsxs("label", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-[9px] uppercase tracking-wider text-neutral-600", children: "Path" }), _jsx("select", { className: `${INPUT_CLS} h-8 px-2 text-[12px]`, value: selected?.id ?? "", "aria-label": "Flythrough path", onChange: (event) => onSelectPath(event.target.value), children: scrubbable.map((entry) => (_jsxs("option", { value: entry.id, children: [entry.label, " \u00B7 ", entry.kind, " \u00B7 ", entry.pointCount, " pts"] }, entry.id))) })] }), _jsxs("label", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-center justify-between text-[9px] uppercase tracking-wider text-neutral-600", children: [_jsx("span", { children: "Progress" }), _jsxs("span", { className: NUMERIC, children: [(progress * 100).toFixed(0), "% \u00B7 ", distance.toFixed(1), " / ", lengthLabel.replace(" m", ""), " m"] })] }), _jsx("input", { type: "range", min: 0, max: 1, step: 0.001, value: progress, "aria-label": "Flythrough progress", disabled: selected === null || selected.length <= 0, className: `h-2 w-full accent-cyan-400 ${FOCUS_RING}`, onChange: (event) => onScrub(Number(event.target.value)) })] }), _jsxs("p", { className: "text-[10px] leading-relaxed text-neutral-600", children: ["Scope: real path points only \u2014 no fabricated keyframes. Play moves the orbit focus along the polyline at", " ", DEFAULT_PLAY_SPEED, " u/s. Edit points in the viewport or inspector."] })] }), _jsxs("div", { className: "flex w-40 shrink-0 flex-col gap-1.5", children: [_jsx(Meta, { label: "Kind", value: selected?.kind ?? "—" }), _jsx(Meta, { label: "Points", value: selected === null ? "—" : String(selected.pointCount) }), _jsx(Meta, { label: "Length", value: lengthLabel }), _jsx(Meta, { label: "At", value: selectedPath === undefined
124
+ ? "—"
125
+ : (() => {
126
+ const sample = samplePathAt(selectedPath, progress);
127
+ if (sample === null)
128
+ return "—";
129
+ return `${sample.x.toFixed(1)}, ${sample.y.toFixed(1)}, ${sample.z.toFixed(1)}`;
130
+ })() })] })] })] }));
131
+ }
132
+ function Meta({ label, value }) {
133
+ return (_jsxs("div", { className: "min-w-0 rounded-[6px] border border-white/[0.07] bg-white/[0.02] px-2.5 py-1.5", children: [_jsx("div", { className: "text-[9px] uppercase tracking-wider text-neutral-600", children: label }), _jsx("div", { className: `truncate text-[12px] font-medium text-neutral-100 ${NUMERIC}`, children: value })] }));
134
+ }
@@ -0,0 +1,12 @@
1
+ import type { EditorAssetEntry } from "../AssetBrowser.js";
2
+ /**
3
+ * Content Browser tile face: real offscreen-rendered GLB preview when a model URL
4
+ * is available and the render succeeds; otherwise the typed kind glyph (honest
5
+ * fallback — never a fabricated screenshot).
6
+ * @internal
7
+ */
8
+ export declare function AssetThumbnail({ asset, size, className, }: {
9
+ asset: EditorAssetEntry;
10
+ size?: number;
11
+ className?: string;
12
+ }): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Icon } from "./icons.js";
3
+ import { useGlbThumbnail } from "./useGlbThumbnail.js";
4
+ const KIND_ICON = {
5
+ model: "cube",
6
+ catalog: "layers",
7
+ marker: "pin",
8
+ };
9
+ /**
10
+ * Content Browser tile face: real offscreen-rendered GLB preview when a model URL
11
+ * is available and the render succeeds; otherwise the typed kind glyph (honest
12
+ * fallback — never a fabricated screenshot).
13
+ * @internal
14
+ */
15
+ export function AssetThumbnail({ asset, size = 22, className = "", }) {
16
+ const canRender = asset.kind === "model" && typeof asset.url === "string" && asset.url.length > 0;
17
+ const thumb = useGlbThumbnail(canRender ? asset.url : undefined);
18
+ if (thumb.status === "ready" && thumb.dataUrl !== null) {
19
+ return (_jsx("img", { src: thumb.dataUrl, alt: "", draggable: false, className: `h-full w-full object-cover ${className}` }));
20
+ }
21
+ const loading = canRender && thumb.status === "loading";
22
+ return (_jsx("span", { className: `flex h-full w-full items-center justify-center text-neutral-500 ${loading ? "animate-pulse" : ""} ${className}`, "aria-hidden": true, children: _jsx(Icon, { name: KIND_ICON[asset.kind], size: size }) }));
23
+ }
@@ -0,0 +1,25 @@
1
+ import type { EditorSession } from "@jgengine/core/editor/index";
2
+ import type { EditorAssetEntry } from "../AssetBrowser.js";
3
+ import type { EditorHostApi } from "../session.js";
4
+ import type { EditorConsoleStore } from "./consoleStore.js";
5
+ import type { BottomDockTab, BrowserViewMode } from "./layoutStore.js";
6
+ import type { PerfHistoryStore } from "./perfHistory.js";
7
+ /**
8
+ * Tabbed bottom dock: Content Browser, Console, Profiler, Animation (path flythrough scrub),
9
+ * and the AI Assistant. Only the active tab's panel mounts, so hidden tools cost nothing per frame.
10
+ */
11
+ export declare function BottomDock({ tab, onSelectTab, onClose, assets, session, api, consoleStore, perfHistory, browserView, onSetBrowserView, onPlaceAsset, onImportModels, importBusy, }: {
12
+ tab: BottomDockTab;
13
+ onSelectTab: (tab: BottomDockTab) => void;
14
+ onClose: () => void;
15
+ assets: readonly EditorAssetEntry[];
16
+ session: EditorSession;
17
+ api: EditorHostApi;
18
+ consoleStore: EditorConsoleStore;
19
+ perfHistory: PerfHistoryStore;
20
+ browserView: BrowserViewMode;
21
+ onSetBrowserView: (view: BrowserViewMode) => void;
22
+ onPlaceAsset: (entry: EditorAssetEntry) => void;
23
+ onImportModels?: (files: readonly File[]) => void | Promise<void>;
24
+ importBusy?: boolean;
25
+ }): import("react").JSX.Element;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useSyncExternalStore } from "react";
3
+ import { AgentPanel } from "../agent/AgentPanel.js";
4
+ import { AnimationPanel } from "./AnimationPanel.js";
5
+ import { ConsolePanel } from "./ConsolePanel.js";
6
+ import { ContentBrowser } from "./ContentBrowser.js";
7
+ import { ProfilerPanel } from "./ProfilerPanel.js";
8
+ import { BORDER } from "./theme.js";
9
+ import { IconButton, PanelTabs } from "./ui.js";
10
+ /**
11
+ * Tabbed bottom dock: Content Browser, Console, Profiler, Animation (path flythrough scrub),
12
+ * and the AI Assistant. Only the active tab's panel mounts, so hidden tools cost nothing per frame.
13
+ */
14
+ export function BottomDock({ tab, onSelectTab, onClose, assets, session, api, consoleStore, perfHistory, browserView, onSetBrowserView, onPlaceAsset, onImportModels, importBusy, }) {
15
+ const consoleEntries = useSyncExternalStore(consoleStore.subscribe, consoleStore.getEntries, consoleStore.getEntries);
16
+ const consoleErrorCount = consoleEntries.reduce((sum, entry) => (entry.severity === "error" ? sum + 1 : sum), 0);
17
+ let content;
18
+ if (tab === "content") {
19
+ content = (_jsx(ContentBrowser, { assets: assets, session: session, onPlace: onPlaceAsset, view: browserView, onSetView: onSetBrowserView, ...(onImportModels === undefined ? {} : { onImportModels, importBusy }) }));
20
+ }
21
+ else if (tab === "console") {
22
+ content = _jsx(ConsolePanel, { store: consoleStore });
23
+ }
24
+ else if (tab === "profiler") {
25
+ content = _jsx(ProfilerPanel, { history: perfHistory });
26
+ }
27
+ else if (tab === "animation") {
28
+ content = _jsx(AnimationPanel, { session: session, api: api });
29
+ }
30
+ else {
31
+ content = _jsx(AgentPanel, { api: api, embedded: true });
32
+ }
33
+ return (_jsxs("section", { className: `pointer-events-auto flex min-h-0 flex-1 flex-col border-t ${BORDER} bg-[#111318]`, "aria-label": "Bottom tool dock", children: [_jsx(PanelTabs, { ariaLabel: "Bottom dock tools", active: tab, onSelect: onSelectTab, tabs: [
34
+ { id: "content", label: "Content Browser", icon: "image", badge: assets.length },
35
+ { id: "console", label: "Console", icon: "terminal", ...(consoleErrorCount > 0 ? { badge: consoleErrorCount } : {}) },
36
+ { id: "profiler", label: "Profiler", icon: "gauge" },
37
+ { id: "animation", label: "Animation", icon: "film" },
38
+ { id: "assistant", label: "AI Assistant", icon: "sparkle" },
39
+ ], trailing: _jsx(IconButton, { icon: "chevronDown", label: "Collapse bottom dock", size: 12, tone: "ghost", onClick: onClose }) }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: content })] }));
40
+ }
@@ -0,0 +1,12 @@
1
+ import { type PaletteCommand } from "./commandRegistry.js";
2
+ /**
3
+ * Modal command palette (Ctrl/Cmd+K): fuzzy-filters every executable editor command (plus scene
4
+ * object jump rows), surfaces recent commands when the query is empty, and runs the highlighted
5
+ * entry on Enter. Esc or backdrop click cancels without side effects.
6
+ */
7
+ export declare function CommandPalette({ commands, onClose, initialQuery, }: {
8
+ commands: readonly PaletteCommand[];
9
+ onClose: () => void;
10
+ /** Pre-filled filter (e.g. "add " to open straight into placement commands). */
11
+ initialQuery?: string;
12
+ }): import("react").JSX.Element;
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { Icon } from "./icons.js";
4
+ import { filterPaletteCommands, loadRecentCommandIds, pushRecentCommandId, } from "./commandRegistry.js";
5
+ import { FOCUS_RING } from "./theme.js";
6
+ import { Kbd } from "./ui.js";
7
+ /**
8
+ * Modal command palette (Ctrl/Cmd+K): fuzzy-filters every executable editor command (plus scene
9
+ * object jump rows), surfaces recent commands when the query is empty, and runs the highlighted
10
+ * entry on Enter. Esc or backdrop click cancels without side effects.
11
+ */
12
+ export function CommandPalette({ commands, onClose, initialQuery = "", }) {
13
+ const [query, setQuery] = useState(initialQuery);
14
+ const [cursor, setCursor] = useState(0);
15
+ const [recentIds, setRecentIds] = useState(() => loadRecentCommandIds());
16
+ const inputRef = useRef(null);
17
+ const listRef = useRef(null);
18
+ const filtered = useMemo(() => filterPaletteCommands(commands, query, recentIds).slice(0, 40), [commands, query, recentIds]);
19
+ const clampedCursor = Math.min(cursor, Math.max(0, filtered.length - 1));
20
+ useEffect(() => {
21
+ inputRef.current?.focus();
22
+ }, []);
23
+ useEffect(() => {
24
+ const row = listRef.current?.children[clampedCursor];
25
+ if (row instanceof HTMLElement)
26
+ row.scrollIntoView({ block: "nearest" });
27
+ }, [clampedCursor]);
28
+ const run = (command) => {
29
+ if (command === undefined)
30
+ return;
31
+ setRecentIds(pushRecentCommandId(command.id));
32
+ onClose();
33
+ command.run();
34
+ };
35
+ return (_jsx("div", { className: "pointer-events-auto absolute inset-0 z-[70] flex items-start justify-center bg-black/50 pt-[12vh] backdrop-blur-[2px]", onClick: onClose, children: _jsxs("div", { role: "dialog", "aria-label": "Command palette", className: "w-[520px] max-w-[92vw] overflow-hidden rounded-[8px] border border-white/10 bg-[#14171d] shadow-2xl shadow-black/60", onClick: (event) => event.stopPropagation(), children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-white/[0.07] px-3", children: [_jsx(Icon, { name: "search", size: 14, className: "shrink-0 text-neutral-500" }), _jsx("input", { ref: inputRef, value: query, onChange: (event) => {
36
+ setQuery(event.target.value);
37
+ setCursor(0);
38
+ }, onKeyDown: (event) => {
39
+ if (event.key === "Escape") {
40
+ event.preventDefault();
41
+ onClose();
42
+ }
43
+ else if (event.key === "ArrowDown") {
44
+ event.preventDefault();
45
+ setCursor((value) => Math.min(filtered.length - 1, value + 1));
46
+ }
47
+ else if (event.key === "ArrowUp") {
48
+ event.preventDefault();
49
+ setCursor((value) => Math.max(0, value - 1));
50
+ }
51
+ else if (event.key === "Enter") {
52
+ event.preventDefault();
53
+ run(filtered[clampedCursor]);
54
+ }
55
+ }, placeholder: "Search commands, tools, or objects\u2026", "aria-label": "Search commands and objects", className: "h-10 w-full bg-transparent text-[13px] text-neutral-100 outline-none placeholder:text-neutral-600" }), _jsx(Kbd, { children: "Esc" })] }), _jsx("div", { ref: listRef, role: "listbox", "aria-label": "Commands", className: "max-h-[46vh] overflow-auto p-1.5", children: filtered.length === 0 ? (_jsxs("div", { className: "p-4 text-center text-[12px] text-neutral-500", children: ["No commands match \u201C", query, "\u201D."] })) : (filtered.map((command, index) => (_jsxs("button", { type: "button", role: "option", "aria-selected": index === clampedCursor, onMouseEnter: () => setCursor(index), onClick: () => run(command), className: `flex w-full items-center gap-2 rounded-[5px] px-2.5 py-1.5 text-left text-[12px] transition-colors ${FOCUS_RING} ${index === clampedCursor ? "bg-cyan-500/15 text-cyan-100" : "text-neutral-300 hover:bg-white/[0.05]"}`, children: [_jsx("span", { className: "w-16 shrink-0 truncate text-[10px] uppercase tracking-wider text-neutral-600", children: command.group }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: command.title }), command.kbd !== undefined ? _jsx(Kbd, { children: command.kbd }) : null] }, command.id)))) })] }) }));
56
+ }
@@ -0,0 +1,8 @@
1
+ import type { EditorConsoleStore } from "./consoleStore.js";
2
+ /**
3
+ * Console dock tab over the real editor event log — notifications, save/import results, RPC and
4
+ * agent errors. Severity filters, counts, search, timestamps, and clear; no fabricated entries.
5
+ */
6
+ export declare function ConsolePanel({ store }: {
7
+ store: EditorConsoleStore;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState, useSyncExternalStore } from "react";
3
+ import { Icon } from "./icons.js";
4
+ import { FOCUS_RING, INPUT_CLS, NUMERIC } from "./theme.js";
5
+ import { EmptyState, IconButton } from "./ui.js";
6
+ const SEVERITY_META = {
7
+ info: { icon: "info", row: "text-neutral-300", chip: "text-neutral-300" },
8
+ warning: { icon: "warning", row: "text-amber-200", chip: "text-amber-300" },
9
+ error: { icon: "error", row: "text-rose-200", chip: "text-rose-300" },
10
+ };
11
+ function formatTime(at) {
12
+ const date = new Date(at);
13
+ const pad = (value) => String(value).padStart(2, "0");
14
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
15
+ }
16
+ /**
17
+ * Console dock tab over the real editor event log — notifications, save/import results, RPC and
18
+ * agent errors. Severity filters, counts, search, timestamps, and clear; no fabricated entries.
19
+ */
20
+ export function ConsolePanel({ store }) {
21
+ const entries = useSyncExternalStore(store.subscribe, store.getEntries, store.getEntries);
22
+ const [enabled, setEnabled] = useState({ info: true, warning: true, error: true });
23
+ const [query, setQuery] = useState("");
24
+ const counts = useMemo(() => {
25
+ const totals = { info: 0, warning: 0, error: 0 };
26
+ for (const entry of entries)
27
+ totals[entry.severity] += 1;
28
+ return totals;
29
+ }, [entries]);
30
+ const visible = useMemo(() => {
31
+ const needle = query.trim().toLowerCase();
32
+ return entries.filter((entry) => enabled[entry.severity] &&
33
+ (needle.length === 0 || entry.message.toLowerCase().includes(needle) || entry.source.toLowerCase().includes(needle)));
34
+ }, [entries, enabled, query]);
35
+ return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex h-8 shrink-0 items-center gap-1.5 border-b border-white/[0.06] px-2", children: [Object.keys(SEVERITY_META).map((severity) => {
36
+ const meta = SEVERITY_META[severity];
37
+ const active = enabled[severity];
38
+ return (_jsxs("button", { type: "button", "aria-pressed": active, "aria-label": `${active ? "Hide" : "Show"} ${severity} entries`, onClick: () => setEnabled((previous) => ({ ...previous, [severity]: !previous[severity] })), className: `flex h-6 items-center gap-1 rounded-[5px] border px-1.5 text-[10px] transition-colors ${FOCUS_RING} ${active ? `border-white/[0.1] bg-white/[0.05] ${meta.chip}` : "border-transparent text-neutral-600 hover:text-neutral-400"}`, children: [_jsx(Icon, { name: meta.icon, size: 11 }), _jsx("span", { className: NUMERIC, children: counts[severity] })] }, severity));
39
+ }), _jsx("input", { type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: "Filter messages\u2026", "aria-label": "Filter console messages", className: `ml-2 h-6 w-56 px-2 ${INPUT_CLS}` }), _jsx("div", { className: "ml-auto", children: _jsx(IconButton, { icon: "trash", label: "Clear console", size: 12, onClick: () => store.clear(), disabled: entries.length === 0 }) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-1 font-mono text-[10.5px] leading-relaxed", children: visible.length === 0 ? (_jsx(EmptyState, { icon: "terminal", title: entries.length === 0 ? "No editor events yet" : "No entries match the filter", description: entries.length === 0
40
+ ? "Editor actions, save results, and agent activity will appear here as they happen."
41
+ : "Adjust the severity filters or search to see more." })) : (visible.map((entry) => {
42
+ const meta = SEVERITY_META[entry.severity];
43
+ return (_jsxs("div", { className: `flex items-start gap-2 rounded-[4px] px-1.5 py-0.5 hover:bg-white/[0.03] ${meta.row}`, children: [_jsxs("span", { className: `shrink-0 text-neutral-600 ${NUMERIC}`, children: ["[", formatTime(entry.at), "]"] }), _jsx(Icon, { name: meta.icon, size: 11, className: "mt-0.5 shrink-0 opacity-70" }), _jsx("span", { className: "shrink-0 rounded-[3px] bg-white/[0.05] px-1 text-[9px] uppercase tracking-wider text-neutral-500", children: entry.source }), _jsx("span", { className: "min-w-0 flex-1 break-words", children: entry.message })] }, entry.id));
44
+ })) })] }));
45
+ }
@@ -0,0 +1,21 @@
1
+ import type { EditorSession } from "@jgengine/core/editor/index";
2
+ import { type EditorAssetEntry } from "../AssetBrowser.js";
3
+ import type { BrowserViewMode } from "./layoutStore.js";
4
+ /**
5
+ * Content Browser dock tab: folder rail + searchable asset grid/list over the game's real asset
6
+ * catalog, plus the terrain material palette (drag chips onto objects or the viewport). Assets are
7
+ * draggable into the viewport for placement (and still double-click / Place). Model cards with a URL
8
+ * get a real offscreen-GLB thumbnail (cached); catalog/marker rows and failed loads keep typed
9
+ * glyphs — never a fabricated screenshot. Import reuses the standalone editor's host importer.
10
+ */
11
+ export declare function ContentBrowser({ assets, session, onPlace, view, onSetView, onImportModels, importBusy, }: {
12
+ assets: readonly EditorAssetEntry[];
13
+ session: EditorSession;
14
+ onPlace: (entry: EditorAssetEntry) => void;
15
+ view: BrowserViewMode;
16
+ onSetView: (view: BrowserViewMode) => void;
17
+ /** When set, enables Import + drop-to-import for `.glb`/`.gltf` model files. */
18
+ onImportModels?: (files: readonly File[]) => void | Promise<void>;
19
+ /** True while an import is in flight (disables the Import control). */
20
+ importBusy?: boolean;
21
+ }): import("react").JSX.Element;
@@ -0,0 +1,130 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useRef, useState } from "react";
3
+ import { ASSET_DRAG_MIME, MATERIAL_DRAG_MIME, encodeAssetDragPayload, } from "../AssetBrowser.js";
4
+ import { TERRAIN_MATERIALS } from "../uiStore.js";
5
+ import { AssetThumbnail } from "./AssetThumbnail.js";
6
+ import { Icon } from "./icons.js";
7
+ import { FOCUS_RING, INPUT_CLS, MICRO_LABEL } from "./theme.js";
8
+ import { EmptyState, IconButton, Segmented } from "./ui.js";
9
+ const ASSET_KIND_ICON = {
10
+ model: "cube",
11
+ catalog: "layers",
12
+ marker: "pin",
13
+ };
14
+ const MODEL_ACCEPT = ".glb,.gltf,model/gltf-binary,model/gltf+json";
15
+ /**
16
+ * Content Browser dock tab: folder rail + searchable asset grid/list over the game's real asset
17
+ * catalog, plus the terrain material palette (drag chips onto objects or the viewport). Assets are
18
+ * draggable into the viewport for placement (and still double-click / Place). Model cards with a URL
19
+ * get a real offscreen-GLB thumbnail (cached); catalog/marker rows and failed loads keep typed
20
+ * glyphs — never a fabricated screenshot. Import reuses the standalone editor's host importer.
21
+ */
22
+ export function ContentBrowser({ assets, session, onPlace, view, onSetView, onImportModels, importBusy = false, }) {
23
+ const [query, setQuery] = useState("");
24
+ const [folder, setFolder] = useState("all");
25
+ const [selectedId, setSelectedId] = useState(null);
26
+ const [dropActive, setDropActive] = useState(false);
27
+ const importInputRef = useRef(null);
28
+ const folders = useMemo(() => {
29
+ const kinds = new Map();
30
+ for (const asset of assets)
31
+ kinds.set(asset.kind, (kinds.get(asset.kind) ?? 0) + 1);
32
+ return [
33
+ { id: "all", label: "All assets", icon: "folder", count: assets.length },
34
+ ...[...kinds.entries()].map(([kind, count]) => ({
35
+ id: `kind:${kind}`,
36
+ label: kind === "model" ? "Models" : kind === "catalog" ? "Catalog" : "Markers",
37
+ icon: ASSET_KIND_ICON[kind] ?? "cube",
38
+ count,
39
+ })),
40
+ { id: "materials", label: "Materials", icon: "sphere", count: TERRAIN_MATERIALS.length },
41
+ ];
42
+ }, [assets]);
43
+ const filtered = useMemo(() => {
44
+ const needle = query.trim().toLowerCase();
45
+ let list = assets;
46
+ if (folder.startsWith("kind:")) {
47
+ const kind = folder.slice(5);
48
+ list = list.filter((asset) => asset.kind === kind);
49
+ }
50
+ if (needle.length === 0)
51
+ return list;
52
+ return list.filter((asset) => asset.id.toLowerCase().includes(needle) || asset.label.toLowerCase().includes(needle));
53
+ }, [assets, folder, query]);
54
+ const selected = filtered.find((asset) => asset.id === selectedId) ?? null;
55
+ const activeFolder = folders.find((entry) => entry.id === folder) ?? folders[0];
56
+ const showMaterials = folder === "materials";
57
+ const canImport = onImportModels !== undefined;
58
+ const takeDroppedFiles = (event) => {
59
+ if (!canImport || event.dataTransfer === null)
60
+ return;
61
+ const files = Array.from(event.dataTransfer.files);
62
+ if (files.length === 0)
63
+ return;
64
+ event.preventDefault();
65
+ event.stopPropagation();
66
+ setDropActive(false);
67
+ void onImportModels(files);
68
+ };
69
+ return (_jsxs("div", { className: `flex min-h-0 flex-1 ${dropActive ? "ring-2 ring-inset ring-cyan-400/40" : ""}`, onDragEnter: (event) => {
70
+ if (!canImport || event.dataTransfer === null)
71
+ return;
72
+ if (![...event.dataTransfer.types].includes("Files"))
73
+ return;
74
+ event.preventDefault();
75
+ setDropActive(true);
76
+ }, onDragOver: (event) => {
77
+ if (!canImport || event.dataTransfer === null)
78
+ return;
79
+ if (![...event.dataTransfer.types].includes("Files"))
80
+ return;
81
+ event.preventDefault();
82
+ event.dataTransfer.dropEffect = "copy";
83
+ setDropActive(true);
84
+ }, onDragLeave: (event) => {
85
+ if (event.currentTarget.contains(event.relatedTarget))
86
+ return;
87
+ setDropActive(false);
88
+ }, onDrop: takeDroppedFiles, children: [_jsxs("nav", { "aria-label": "Asset folders", className: "flex w-40 shrink-0 flex-col gap-0.5 overflow-auto border-r border-white/[0.06] p-1.5", children: [_jsx("div", { className: `px-1.5 pb-1 ${MICRO_LABEL}`, children: "Folders" }), folders.map((entry) => (_jsxs("button", { type: "button", onClick: () => {
89
+ setFolder(entry.id);
90
+ setSelectedId(null);
91
+ }, "aria-current": entry.id === folder, className: `flex items-center gap-1.5 rounded-[5px] px-1.5 py-1 text-left text-[11px] transition-colors ${FOCUS_RING} ${entry.id === folder ? "bg-cyan-500/15 text-cyan-100" : "text-neutral-400 hover:bg-white/[0.05] hover:text-neutral-200"}`, children: [_jsx(Icon, { name: entry.icon, size: 13, className: "shrink-0" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: entry.label }), _jsx("span", { className: "text-[9px] tabular-nums text-neutral-600", children: entry.count })] }, entry.id)))] }), _jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex h-9 shrink-0 items-center gap-2 border-b border-white/[0.06] px-2", children: [_jsxs("div", { className: "flex items-center gap-1 text-[11px] text-neutral-500", children: [_jsx("span", { children: "Assets" }), _jsx(Icon, { name: "chevronRight", size: 9 }), _jsx("span", { className: "text-neutral-300", children: activeFolder.label })] }), canImport ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", disabled: importBusy, onClick: () => importInputRef.current?.click(), className: `ml-auto flex h-6.5 items-center gap-1 rounded-[5px] bg-cyan-500/15 px-2 text-[11px] text-cyan-100 ring-1 ring-inset ring-cyan-400/30 transition-colors hover:bg-cyan-500/25 disabled:pointer-events-none disabled:opacity-50 ${FOCUS_RING}`, title: "Import .glb / .gltf models into the placeable catalog", children: [_jsx(Icon, { name: "import", size: 12 }), importBusy ? "Importing…" : "Import"] }), _jsx("input", { ref: importInputRef, type: "file", accept: MODEL_ACCEPT, multiple: true, className: "hidden", onChange: (event) => {
92
+ const files = event.target.files;
93
+ if (files !== null && files.length > 0)
94
+ void onImportModels(Array.from(files));
95
+ event.target.value = "";
96
+ } })] })) : null, _jsx("input", { type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: "Search assets\u2026", "aria-label": "Search assets", className: `${canImport ? "" : "ml-auto "}h-6.5 w-52 px-2 ${INPUT_CLS}` }), _jsx(Segmented, { ariaLabel: "Browser layout", value: view, onChange: onSetView, options: [
97
+ { value: "grid", label: "Grid view", icon: "gridView", iconOnly: true },
98
+ { value: "list", label: "List view", icon: "list", iconOnly: true },
99
+ ] })] }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx("div", { className: "min-h-0 min-w-0 flex-1 overflow-auto p-2", children: showMaterials ? (_jsx("div", { className: "flex flex-wrap gap-1.5", children: TERRAIN_MATERIALS.map((material) => (_jsxs("div", { draggable: true, onDragStart: (event) => {
100
+ event.dataTransfer.setData(MATERIAL_DRAG_MIME, material.id);
101
+ event.dataTransfer.effectAllowed = "copy";
102
+ }, title: `Drag ${material.label} onto an object or the viewport to assign/paint it`, className: "flex cursor-grab items-center gap-2 rounded-[6px] border border-white/[0.08] bg-[#191d24] px-2.5 py-1.5 text-[11px] text-neutral-300 active:cursor-grabbing", children: [_jsx("span", { className: "h-4 w-4 rounded-full ring-1 ring-inset ring-white/20", style: { backgroundColor: material.color } }), material.label] }, material.id))) })) : filtered.length === 0 ? (_jsx(EmptyState, { icon: "image", title: query.length > 0 ? "No assets match" : "No assets in this catalog", description: query.length > 0
103
+ ? `Nothing in ${activeFolder.label} matches “${query}”.`
104
+ : canImport
105
+ ? "Drop .glb / .gltf files here, or use Import, to add placeable models."
106
+ : "This game registers no placeable assets. Register models in the game's asset catalog, or drop .glb files into the standalone editor." })) : view === "grid" ? (_jsx("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-1.5", children: filtered.map((asset) => {
107
+ const isSelected = asset.id === selectedId;
108
+ return (_jsxs("button", { type: "button", draggable: true, onDragStart: (event) => {
109
+ event.dataTransfer.setData(ASSET_DRAG_MIME, encodeAssetDragPayload(asset));
110
+ event.dataTransfer.effectAllowed = "copy";
111
+ }, onClick: () => setSelectedId(asset.id), onDoubleClick: () => onPlace(asset), title: `${asset.label} — drag into viewport or double-click to place`, className: `group flex cursor-grab flex-col overflow-hidden rounded-[6px] border text-left transition-colors active:cursor-grabbing ${FOCUS_RING} ${isSelected
112
+ ? "border-cyan-400/50 bg-cyan-500/10"
113
+ : "border-white/[0.07] bg-white/[0.02] hover:border-white/[0.14] hover:bg-white/[0.04]"}`, children: [_jsx("div", { className: "flex h-14 items-center justify-center overflow-hidden border-b border-white/[0.05] bg-black/25 text-neutral-500 group-hover:text-neutral-300", children: _jsx(AssetThumbnail, { asset: asset, size: 22 }) }), _jsxs("div", { className: "p-1.5", children: [_jsx("div", { className: "truncate text-[10px] text-neutral-200", children: asset.label }), _jsx("div", { className: "mt-0.5 flex items-center gap-1", children: _jsx("span", { className: "rounded-[3px] bg-white/[0.06] px-1 py-px text-[8px] uppercase tracking-wider text-neutral-500", children: asset.kind }) })] })] }, asset.id));
114
+ }) })) : (_jsx("div", { className: "space-y-0.5", children: filtered.map((asset) => {
115
+ const isSelected = asset.id === selectedId;
116
+ return (_jsxs("div", { draggable: true, onDragStart: (event) => {
117
+ event.dataTransfer.setData(ASSET_DRAG_MIME, encodeAssetDragPayload(asset));
118
+ event.dataTransfer.effectAllowed = "copy";
119
+ }, className: `group flex cursor-grab items-center gap-2 rounded-[5px] px-1.5 py-1 transition-colors active:cursor-grabbing ${isSelected ? "bg-cyan-500/10 ring-1 ring-inset ring-cyan-400/30" : "hover:bg-white/[0.04]"}`, children: [_jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-[4px] border border-white/[0.06] bg-black/30", children: _jsx(AssetThumbnail, { asset: asset, size: 14 }) }), _jsxs("button", { type: "button", onClick: () => setSelectedId(asset.id), onDoubleClick: () => onPlace(asset), className: `min-w-0 flex-1 truncate text-left text-[11px] text-neutral-200 ${FOCUS_RING}`, children: [asset.label, _jsx("span", { className: "ml-2 text-[10px] text-neutral-600", children: asset.id })] }), _jsx("span", { className: "rounded-[3px] bg-white/[0.06] px-1 py-px text-[8px] uppercase tracking-wider text-neutral-500", children: asset.kind }), _jsx("button", { type: "button", onClick: () => onPlace(asset), className: `shrink-0 rounded-[4px] bg-cyan-500/20 px-2 py-0.5 text-[10px] text-cyan-200 opacity-0 ring-1 ring-inset ring-cyan-400/30 transition-all hover:bg-cyan-500/30 focus-visible:opacity-100 group-hover:opacity-100 ${FOCUS_RING}`, children: "Place" })] }, asset.id));
120
+ }) })) }), selected !== null && !showMaterials ? (_jsxs("aside", { className: "w-48 shrink-0 overflow-auto border-l border-white/[0.06] p-2.5", "aria-label": "Asset details", children: [_jsx("div", { className: "flex h-16 items-center justify-center overflow-hidden rounded-[6px] border border-white/[0.07] bg-black/25 text-neutral-400", children: _jsx(AssetThumbnail, { asset: selected, size: 26 }) }), _jsx("div", { className: "mt-2 truncate text-[12px] font-medium text-neutral-100", children: selected.label }), _jsxs("dl", { className: "mt-1.5 space-y-1 text-[10px]", children: [_jsxs("div", { className: "flex justify-between gap-2", children: [_jsx("dt", { className: "text-neutral-600", children: "Type" }), _jsx("dd", { className: "text-neutral-400", children: selected.kind })] }), _jsxs("div", { className: "flex justify-between gap-2", children: [_jsx("dt", { className: "text-neutral-600", children: "Id" }), _jsx("dd", { className: "truncate text-neutral-400", children: selected.id })] }), selected.url !== undefined ? (_jsxs("div", { className: "flex justify-between gap-2", children: [_jsx("dt", { className: "text-neutral-600", children: "Source" }), _jsx("dd", { className: "truncate text-neutral-400", title: selected.url, children: selected.url.split("/").pop() })] })) : null] }), _jsx("button", { type: "button", onClick: () => onPlace(selected), className: `mt-2.5 w-full rounded-[5px] bg-cyan-600 px-2 py-1.5 text-[11px] font-medium text-white transition-colors hover:bg-cyan-500 ${FOCUS_RING}`, children: "Place in scene" })] })) : null] }), _jsxs("div", { className: "flex h-7 shrink-0 items-center gap-2 border-t border-white/[0.06] px-2 text-[10px] text-neutral-600", children: [_jsx("span", { className: "tabular-nums", children: showMaterials ? `${TERRAIN_MATERIALS.length} materials` : `${filtered.length} of ${assets.length} assets` }), selected !== null ? _jsxs("span", { className: "truncate text-neutral-500", children: [selected.id, " selected"] }) : null, canImport && !showMaterials ? (_jsx("span", { className: "truncate text-neutral-600", children: "Drop .glb here or Import" })) : null, _jsx("div", { className: "ml-auto", children: _jsx(IconButton, { icon: "pin", label: "Add empty marker at origin", size: 12, onClick: () => session.dispatch({
121
+ type: "addMarker",
122
+ marker: {
123
+ id: `marker_${Date.now().toString(36)}`,
124
+ kind: "prop",
125
+ position: { x: 0, y: 0, z: 0 },
126
+ label: "New marker",
127
+ color: "#e2e8f0",
128
+ },
129
+ }) }) })] })] })] }));
130
+ }
@@ -0,0 +1,26 @@
1
+ import { type EditorSession } from "@jgengine/core/editor/index";
2
+ import type { EditorHostApi } from "../session.js";
3
+ export { hierarchyActiveDescendantId, hierarchyRowDomId, selectableIds, } from "./hierarchyA11y.js";
4
+ /** HTML5 drag payload for hierarchy reparenting via `set_parent`. */
5
+ export declare const OBJECT_DRAG_MIME = "application/x-jgengine-editor-object";
6
+ /**
7
+ * Redesigned world outliner: searchable, virtualized, kind-iconed rows generated from the live
8
+ * document. Group headers carry real per-kind visibility toggles (the editor's layer system);
9
+ * rows expose per-object eye/lock toggles (`setObjectFlags` → document `hidden`/`locked`) and show
10
+ * a lock affordance when a locked collection also owns the id. Tree view supports drag-and-drop
11
+ * reparenting through the existing `set_parent` RPC. Keyboard navigation uses a single tab stop
12
+ * on the list (roving active row via `aria-activedescendant`; arrows / Home / End / Enter / F2),
13
+ * double-click rename, and row context menus (frame / duplicate / delete / prefab / parent to… /
14
+ * unparent) are supported. Selector-subscribed and memoized, so UI-only churn never rerenders it.
15
+ */
16
+ export declare const HierarchyPanel: import("react").MemoExoticComponent<({ session, api, onAdd, onRowContextMenu, }: {
17
+ session: EditorSession;
18
+ api: EditorHostApi;
19
+ /** Opens the add flow (command palette pre-filtered to Add commands). */
20
+ onAdd: () => void;
21
+ /** Right-click a row — host opens the shared editor context menu at the pointer. */
22
+ onRowContextMenu?: (event: {
23
+ clientX: number;
24
+ clientY: number;
25
+ }, id: string) => void;
26
+ }) => import("react").JSX.Element>;