castle-web-cli 0.4.74 → 0.4.76
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/dist/agent-prompts.d.ts +4 -0
- package/dist/agent-prompts.js +20 -8
- package/dist/agent.js +301 -28
- package/dist/castle-host/host.js +59 -5
- package/dist/commonInstructions.d.ts +1 -1
- package/dist/commonInstructions.js +5 -1
- package/dist/ide.d.ts +1 -0
- package/dist/ide.js +250 -1
- package/dist/init.js +46 -4
- package/dist/save-deck.js +8 -1
- package/dist/serve.js +27 -1
- package/dist/shell/assets/index-DNWEQd4R.js +141 -0
- package/dist/shell/assets/{index-WNbOHPBj.css → index-DuKq-Grp.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/kits/basic-2d/CLAUDE.md +12 -4
- package/kits/basic-2d/castle.json +15 -0
- package/kits/basic-2d/drawings/pig.pxart +22 -55
- package/kits/basic-2d/editors/PxArtEditor.jsx +237 -4
- package/kits/basic-2d/editors/SceneEditor.jsx +0 -3
- package/kits/basic-2d/editors/SingleEditor.jsx +6 -51
- package/kits/basic-2d/editors/pixelEditorChrome.jsx +17 -11
- package/kits/basic-2d/editors/pixelGeometry.js +95 -0
- package/kits/basic-2d/editors/pixelInspector.jsx +229 -51
- package/kits/basic-2d/editors/pxArtTools.js +109 -1
- package/kits/basic-2d/engine/ScenePlayer.jsx +11 -163
- package/kits/basic-2d/engine/ui.jsx +15 -0
- package/kits/basic-2d/engine/ui.module.css +44 -117
- package/kits/basic-2d/main.jsx +6 -10
- package/kits/basic-2d/pnpm-workspace.yaml +3 -0
- package/package.json +10 -1
- package/dist/shell/assets/index-Dfn29Bkt.js +0 -108
- package/kits/basic-2d/editors/App.jsx +0 -226
- package/kits/basic-2d/editors/CodeEditor.jsx +0 -79
- package/kits/basic-2d/editors/FileBrowser.jsx +0 -349
- package/kits/basic-2d/editors/codeTheme.js +0 -135
- package/kits/basic-2d/engine/TouchControls.jsx +0 -136
- package/kits/basic-2d/engine/playConsole.js +0 -66
- package/kits/basic-3d/.prettierrc +0 -8
- package/kits/basic-3d/CLAUDE.md +0 -162
- package/kits/basic-3d/behaviors/Camera.jsx +0 -56
- package/kits/basic-3d/behaviors/Collider.jsx +0 -78
- package/kits/basic-3d/behaviors/Mesh.jsx +0 -82
- package/kits/basic-3d/behaviors/Model.jsx +0 -61
- package/kits/basic-3d/behaviors/Transform.jsx +0 -35
- package/kits/basic-3d/editors/App.jsx +0 -147
- package/kits/basic-3d/editors/CodeEditor.jsx +0 -112
- package/kits/basic-3d/editors/FileBrowser.jsx +0 -143
- package/kits/basic-3d/editors/ModelEditor.jsx +0 -400
- package/kits/basic-3d/editors/PlayOnly.jsx +0 -22
- package/kits/basic-3d/editors/SceneEditor.jsx +0 -1087
- package/kits/basic-3d/editors/behaviorRegistry.js +0 -24
- package/kits/basic-3d/editors/editorHistory.js +0 -52
- package/kits/basic-3d/editors/viewportRig.js +0 -90
- package/kits/basic-3d/engine/ScenePlayer.jsx +0 -61
- package/kits/basic-3d/engine/SceneUI.jsx +0 -67
- package/kits/basic-3d/engine/SceneViewport.jsx +0 -102
- package/kits/basic-3d/engine/TouchControls.jsx +0 -136
- package/kits/basic-3d/engine/autoInspector.jsx +0 -51
- package/kits/basic-3d/engine/files.js +0 -73
- package/kits/basic-3d/engine/scene.js +0 -502
- package/kits/basic-3d/engine/threeUtil.js +0 -260
- package/kits/basic-3d/engine/ui.jsx +0 -352
- package/kits/basic-3d/engine/ui.module.css +0 -944
- package/kits/basic-3d/eslint.config.js +0 -51
- package/kits/basic-3d/index.html +0 -11
- package/kits/basic-3d/main.jsx +0 -10
- package/kits/basic-3d/models/block.model +0 -14
- package/kits/basic-3d/package-lock.json +0 -2713
- package/kits/basic-3d/package.json +0 -41
- package/kits/basic-3d/pnpm-lock.yaml +0 -1769
- package/kits/basic-3d/scenes/main.scene +0 -76
- package/kits/basic-3d/vite.config.js +0 -1
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import * as THREE from 'three';
|
|
3
|
-
import { Panel, SelectField } from '../engine/ui';
|
|
4
|
-
import { AutoFields } from '../engine/autoInspector';
|
|
5
|
-
import {
|
|
6
|
-
applyStandardMaterial,
|
|
7
|
-
buildGeometry,
|
|
8
|
-
buildStandardMaterial,
|
|
9
|
-
geometryKinds,
|
|
10
|
-
renderTarget,
|
|
11
|
-
replaceGroupChild,
|
|
12
|
-
} from '../engine/threeUtil';
|
|
13
|
-
|
|
14
|
-
// A single primitive mesh: sized geometry + a standard PBR material. For
|
|
15
|
-
// multi-part shapes use a Model behavior pointing at a `.model` file instead.
|
|
16
|
-
export class Mesh {
|
|
17
|
-
static behaviorName = 'Mesh';
|
|
18
|
-
|
|
19
|
-
static defaultProps = {
|
|
20
|
-
geometry: 'box',
|
|
21
|
-
width: 1,
|
|
22
|
-
height: 1,
|
|
23
|
-
depth: 1,
|
|
24
|
-
color: '#8db7ffff',
|
|
25
|
-
roughness: 0.8,
|
|
26
|
-
metalness: 0,
|
|
27
|
-
emissive: '#00000000',
|
|
28
|
-
opacity: 1,
|
|
29
|
-
castShadow: true,
|
|
30
|
-
receiveShadow: true,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
constructor(props) {
|
|
34
|
-
this.props = props;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
sync(actor, scene) {
|
|
38
|
-
const target = renderTarget(actor, scene);
|
|
39
|
-
if (!target) return;
|
|
40
|
-
const { group, cache } = target;
|
|
41
|
-
const geometryKey = JSON.stringify([
|
|
42
|
-
this.props.geometry,
|
|
43
|
-
this.props.width,
|
|
44
|
-
this.props.height,
|
|
45
|
-
this.props.depth,
|
|
46
|
-
]);
|
|
47
|
-
if (!cache._mesh || cache._meshGeometryKey !== geometryKey) {
|
|
48
|
-
const geometry = buildGeometry(
|
|
49
|
-
this.props.geometry,
|
|
50
|
-
this.props.width,
|
|
51
|
-
this.props.height,
|
|
52
|
-
this.props.depth
|
|
53
|
-
);
|
|
54
|
-
const mesh = new THREE.Mesh(geometry, buildStandardMaterial(this.props));
|
|
55
|
-
cache._mesh = replaceGroupChild(group, cache._mesh, mesh);
|
|
56
|
-
cache._meshGeometryKey = geometryKey;
|
|
57
|
-
}
|
|
58
|
-
// Material props are cheap to apply in place every frame.
|
|
59
|
-
applyStandardMaterial(cache._mesh.material, this.props);
|
|
60
|
-
cache._mesh.castShadow = !!this.props.castShadow;
|
|
61
|
-
cache._mesh.receiveShadow = !!this.props.receiveShadow;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
static Inspector({ component, setComponent }) {
|
|
65
|
-
return (
|
|
66
|
-
<Panel title="Mesh">
|
|
67
|
-
<SelectField
|
|
68
|
-
label="Geometry"
|
|
69
|
-
value={component.geometry}
|
|
70
|
-
onChange={(geometry) => setComponent({ geometry })}
|
|
71
|
-
options={geometryKinds}
|
|
72
|
-
/>
|
|
73
|
-
<AutoFields
|
|
74
|
-
defaultProps={Mesh.defaultProps}
|
|
75
|
-
component={component}
|
|
76
|
-
setComponent={setComponent}
|
|
77
|
-
exclude={['geometry']}
|
|
78
|
-
/>
|
|
79
|
-
</Panel>
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Panel, SelectField } from '../engine/ui';
|
|
3
|
-
import { AutoFields } from '../engine/autoInspector';
|
|
4
|
-
import { buildModelGroup, renderTarget, replaceGroupChild } from '../engine/threeUtil';
|
|
5
|
-
|
|
6
|
-
// Renders a `.model` file (a list of primitive parts -- the 3d analog of the
|
|
7
|
-
// 2d kit's `.drawing`) scaled by the actor's Transform. `tint` multiplies
|
|
8
|
-
// every part color; white or omitted keeps the original colors.
|
|
9
|
-
export class Model {
|
|
10
|
-
static behaviorName = 'Model';
|
|
11
|
-
|
|
12
|
-
static defaultProps = {
|
|
13
|
-
file: 'models/block.model',
|
|
14
|
-
tint: '#ffffffff',
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
constructor(props) {
|
|
18
|
-
this.props = props;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
sync(actor, scene) {
|
|
22
|
-
const target = renderTarget(actor, scene);
|
|
23
|
-
if (!target) return;
|
|
24
|
-
const { group, cache } = target;
|
|
25
|
-
const modelData = scene.models[this.props.file];
|
|
26
|
-
if (!modelData) {
|
|
27
|
-
if (cache._model) {
|
|
28
|
-
cache._model = replaceGroupChild(group, cache._model, null);
|
|
29
|
-
cache._modelKey = null;
|
|
30
|
-
}
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
// Model files are parsed once per text change (a fresh object identity),
|
|
34
|
-
// so identity + tint is a sufficient rebuild key.
|
|
35
|
-
if (cache._modelData !== modelData || cache._modelTint !== this.props.tint) {
|
|
36
|
-
cache._model = replaceGroupChild(group, cache._model, buildModelGroup(modelData, this.props.tint));
|
|
37
|
-
cache._modelData = modelData;
|
|
38
|
-
cache._modelTint = this.props.tint;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
static Inspector({ component, setComponent, files }) {
|
|
43
|
-
const modelFiles = Object.keys(files).filter((path) => path.endsWith('.model'));
|
|
44
|
-
return (
|
|
45
|
-
<Panel title="Model">
|
|
46
|
-
<SelectField
|
|
47
|
-
label="File"
|
|
48
|
-
value={component.file}
|
|
49
|
-
onChange={(file) => setComponent({ file })}
|
|
50
|
-
options={modelFiles}
|
|
51
|
-
/>
|
|
52
|
-
<AutoFields
|
|
53
|
-
defaultProps={Model.defaultProps}
|
|
54
|
-
component={component}
|
|
55
|
-
setComponent={setComponent}
|
|
56
|
-
only={['tint']}
|
|
57
|
-
/>
|
|
58
|
-
</Panel>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { degreesToRadians } from '../engine/threeUtil';
|
|
2
|
-
|
|
3
|
-
// Position / rotation / scale of the actor's group in world units. `x`/`y`/`z`
|
|
4
|
-
// is the actor's CENTER; rotations are degrees; the ground plane is XZ, +Y up.
|
|
5
|
-
export class Transform {
|
|
6
|
-
static behaviorName = 'Transform';
|
|
7
|
-
|
|
8
|
-
static defaultProps = {
|
|
9
|
-
x: 0,
|
|
10
|
-
y: 0,
|
|
11
|
-
z: 0,
|
|
12
|
-
rotationX: 0,
|
|
13
|
-
rotationY: 0,
|
|
14
|
-
rotationZ: 0,
|
|
15
|
-
scaleX: 1,
|
|
16
|
-
scaleY: 1,
|
|
17
|
-
scaleZ: 1,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
constructor(props) {
|
|
21
|
-
this.props = props;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
sync(actor, scene) {
|
|
25
|
-
const group = scene.actorGroup(actor);
|
|
26
|
-
if (!group) return;
|
|
27
|
-
group.position.set(this.props.x, this.props.y, this.props.z);
|
|
28
|
-
group.rotation.set(
|
|
29
|
-
degreesToRadians(this.props.rotationX),
|
|
30
|
-
degreesToRadians(this.props.rotationY),
|
|
31
|
-
degreesToRadians(this.props.rotationZ)
|
|
32
|
-
);
|
|
33
|
-
group.scale.set(this.props.scaleX || 1, this.props.scaleY || 1, this.props.scaleZ || 1);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { writeFile } from 'castle-web-sdk';
|
|
3
|
-
import {
|
|
4
|
-
flatFileOrder,
|
|
5
|
-
formatJson,
|
|
6
|
-
getFileKind,
|
|
7
|
-
initialFiles,
|
|
8
|
-
parseModels,
|
|
9
|
-
} from '../engine/files';
|
|
10
|
-
import { AppShell, cx, MainEditor, styles } from '../engine/ui';
|
|
11
|
-
import { CodeEditor } from './CodeEditor';
|
|
12
|
-
import { ModelEditor } from './ModelEditor';
|
|
13
|
-
import { FileBrowser } from './FileBrowser';
|
|
14
|
-
import { SceneEditor } from './SceneEditor';
|
|
15
|
-
export function App() {
|
|
16
|
-
const [files, setFiles] = useState(initialFiles);
|
|
17
|
-
const [selectedPath, setSelectedPath] = useState('scenes/main.scene');
|
|
18
|
-
const [filesSheetOpen, setFilesSheetOpen] = useState(false);
|
|
19
|
-
const [selectedActorIds, setSelectedActorIds] = useState([]);
|
|
20
|
-
const [multiSelectMode, setMultiSelectMode] = useState(false);
|
|
21
|
-
const saveTimersRef = useRef({});
|
|
22
|
-
const saveVersionsRef = useRef({});
|
|
23
|
-
const models = useMemo(() => parseModels(files), [files]);
|
|
24
|
-
function updateSelectedFile(nextText) {
|
|
25
|
-
const path = selectedPath;
|
|
26
|
-
setFiles((current) => ({
|
|
27
|
-
...current,
|
|
28
|
-
[path]: nextText,
|
|
29
|
-
}));
|
|
30
|
-
scheduleFileWrite(path, nextText);
|
|
31
|
-
}
|
|
32
|
-
function scheduleFileWrite(path, nextText) {
|
|
33
|
-
const version = (saveVersionsRef.current[path] ?? 0) + 1;
|
|
34
|
-
saveVersionsRef.current[path] = version;
|
|
35
|
-
const existingTimer = saveTimersRef.current[path];
|
|
36
|
-
if (existingTimer) window.clearTimeout(existingTimer);
|
|
37
|
-
saveTimersRef.current[path] = window.setTimeout(() => {
|
|
38
|
-
delete saveTimersRef.current[path];
|
|
39
|
-
writeFile(path, nextText).catch((error) => {
|
|
40
|
-
if (saveVersionsRef.current[path] !== version) return;
|
|
41
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
-
console.error(`Failed to save ${path}: ${message}`);
|
|
43
|
-
});
|
|
44
|
-
}, 1500);
|
|
45
|
-
}
|
|
46
|
-
const kind = getFileKind(selectedPath);
|
|
47
|
-
const text = files[selectedPath] ?? '';
|
|
48
|
-
function selectFile(path) {
|
|
49
|
-
setSelectedPath(path);
|
|
50
|
-
setFilesSheetOpen(false);
|
|
51
|
-
setSelectedActorIds([]);
|
|
52
|
-
setMultiSelectMode(false);
|
|
53
|
-
}
|
|
54
|
-
const onToggleFiles = () => {
|
|
55
|
-
setFilesSheetOpen((previous) => !previous);
|
|
56
|
-
setSelectedActorIds([]);
|
|
57
|
-
setMultiSelectMode(false);
|
|
58
|
-
};
|
|
59
|
-
// Alt+Up / Alt+Down steps through files in file-browser sidebar order.
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
function onKeyDown(event) {
|
|
62
|
-
if (!event.altKey || event.metaKey || event.ctrlKey) return;
|
|
63
|
-
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
|
|
64
|
-
const active = document.activeElement;
|
|
65
|
-
if (
|
|
66
|
-
active &&
|
|
67
|
-
(active.tagName === 'INPUT' ||
|
|
68
|
-
active.tagName === 'TEXTAREA' ||
|
|
69
|
-
active.tagName === 'SELECT' ||
|
|
70
|
-
active.isContentEditable ||
|
|
71
|
-
active.closest('.cm-editor'))
|
|
72
|
-
) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const order = flatFileOrder(Object.keys(files));
|
|
76
|
-
const index = order.indexOf(selectedPath);
|
|
77
|
-
if (index === -1) return;
|
|
78
|
-
const nextIndex = event.key === 'ArrowUp' ? index - 1 : index + 1;
|
|
79
|
-
if (nextIndex < 0 || nextIndex >= order.length) return;
|
|
80
|
-
event.preventDefault();
|
|
81
|
-
selectFile(order[nextIndex]);
|
|
82
|
-
}
|
|
83
|
-
window.addEventListener('keydown', onKeyDown);
|
|
84
|
-
return () => window.removeEventListener('keydown', onKeyDown);
|
|
85
|
-
}, [files, selectedPath]);
|
|
86
|
-
return (
|
|
87
|
-
<AppShell>
|
|
88
|
-
<FileBrowser
|
|
89
|
-
files={files}
|
|
90
|
-
selectedPath={selectedPath}
|
|
91
|
-
onSelect={selectFile}
|
|
92
|
-
sheetOpen={filesSheetOpen}
|
|
93
|
-
onSheetOpenChange={setFilesSheetOpen}
|
|
94
|
-
/>
|
|
95
|
-
{filesSheetOpen ? (
|
|
96
|
-
<div
|
|
97
|
-
className={cx(styles.sheetBackdrop, styles.mobileOnly)}
|
|
98
|
-
onClick={() => setFilesSheetOpen(false)}
|
|
99
|
-
/>
|
|
100
|
-
) : null}
|
|
101
|
-
<MainEditor>
|
|
102
|
-
{kind === 'scene' ? (
|
|
103
|
-
<SceneEditor
|
|
104
|
-
path={selectedPath}
|
|
105
|
-
text={text}
|
|
106
|
-
files={files}
|
|
107
|
-
models={models}
|
|
108
|
-
onChange={updateSelectedFile}
|
|
109
|
-
onToggleFiles={onToggleFiles}
|
|
110
|
-
filesOpen={filesSheetOpen}
|
|
111
|
-
selectedActorIds={selectedActorIds}
|
|
112
|
-
onSelectActorIds={setSelectedActorIds}
|
|
113
|
-
multiSelectMode={multiSelectMode}
|
|
114
|
-
onSetMultiSelectMode={setMultiSelectMode}
|
|
115
|
-
/>
|
|
116
|
-
) : null}
|
|
117
|
-
{kind === 'model' ? (
|
|
118
|
-
<ModelEditor
|
|
119
|
-
path={selectedPath}
|
|
120
|
-
text={text}
|
|
121
|
-
onChange={updateSelectedFile}
|
|
122
|
-
onToggleFiles={onToggleFiles}
|
|
123
|
-
filesOpen={filesSheetOpen}
|
|
124
|
-
/>
|
|
125
|
-
) : null}
|
|
126
|
-
{kind === 'code' ? (
|
|
127
|
-
<CodeEditor
|
|
128
|
-
path={selectedPath}
|
|
129
|
-
text={text}
|
|
130
|
-
onChange={updateSelectedFile}
|
|
131
|
-
onToggleFiles={onToggleFiles}
|
|
132
|
-
filesOpen={filesSheetOpen}
|
|
133
|
-
/>
|
|
134
|
-
) : null}
|
|
135
|
-
{kind === 'text' ? (
|
|
136
|
-
<CodeEditor
|
|
137
|
-
path={selectedPath}
|
|
138
|
-
text={formatJson(text)}
|
|
139
|
-
onChange={updateSelectedFile}
|
|
140
|
-
onToggleFiles={onToggleFiles}
|
|
141
|
-
filesOpen={filesSheetOpen}
|
|
142
|
-
/>
|
|
143
|
-
) : null}
|
|
144
|
-
</MainEditor>
|
|
145
|
-
</AppShell>
|
|
146
|
-
);
|
|
147
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useRef } from 'react';
|
|
2
|
-
import { javascript } from '@codemirror/lang-javascript';
|
|
3
|
-
import { HighlightStyle, indentUnit, syntaxHighlighting } from '@codemirror/language';
|
|
4
|
-
import { EditorState } from '@codemirror/state';
|
|
5
|
-
import { EditorView } from '@codemirror/view';
|
|
6
|
-
import { tags } from '@lezer/highlight';
|
|
7
|
-
import { basicSetup } from 'codemirror';
|
|
8
|
-
import { basename } from '../engine/files';
|
|
9
|
-
import { EditorBody, EditorHeader, styles } from '../engine/ui';
|
|
10
|
-
const castleHighlightStyle = HighlightStyle.define([
|
|
11
|
-
{ tag: tags.strong, color: '#285CC4' },
|
|
12
|
-
{ tag: tags.namespace, color: '#BB7547' },
|
|
13
|
-
{ tag: tags.keyword, color: '#BC4A9B' },
|
|
14
|
-
{ tag: [tags.literal, tags.inserted], color: '#5DAF8D' },
|
|
15
|
-
{ tag: [tags.string, tags.deleted], color: '#E86A73' },
|
|
16
|
-
{ tag: tags.comment, color: '#8B93AF', fontStyle: 'italic' },
|
|
17
|
-
]);
|
|
18
|
-
const castleCodeTheme = EditorView.theme({
|
|
19
|
-
'&': {
|
|
20
|
-
height: '100%',
|
|
21
|
-
fontSize: '9pt',
|
|
22
|
-
backgroundColor: '#fff',
|
|
23
|
-
},
|
|
24
|
-
'&.cm-editor.cm-focused': {
|
|
25
|
-
outline: 'none',
|
|
26
|
-
},
|
|
27
|
-
'.cm-scroller': {
|
|
28
|
-
overflow: 'auto',
|
|
29
|
-
fontFamily: 'Menlo, Monaco, Lucida Console, monospace',
|
|
30
|
-
},
|
|
31
|
-
'.cm-content': {
|
|
32
|
-
minHeight: '100%',
|
|
33
|
-
color: '#322b28',
|
|
34
|
-
paddingBottom: '400px',
|
|
35
|
-
paddingRight: '80px',
|
|
36
|
-
},
|
|
37
|
-
'.cm-gutters': {
|
|
38
|
-
display: 'none',
|
|
39
|
-
},
|
|
40
|
-
'.cm-activeLine': {
|
|
41
|
-
backgroundColor: '#eeeeeea0',
|
|
42
|
-
},
|
|
43
|
-
'.cm-activeLineGutter': {
|
|
44
|
-
color: '#000',
|
|
45
|
-
backgroundColor: '#ddd',
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
export function CodeEditor({ path, text, onChange, onToggleFiles, filesOpen }) {
|
|
49
|
-
const editorRef = useRef(null);
|
|
50
|
-
const viewRef = useRef(null);
|
|
51
|
-
const onChangeRef = useRef(onChange);
|
|
52
|
-
const initialTextRef = useRef(text);
|
|
53
|
-
const applyingExternalTextRef = useRef(false);
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
onChangeRef.current = onChange;
|
|
56
|
-
}, [onChange]);
|
|
57
|
-
// The editor is created once per `path`; the initial doc comes from a ref
|
|
58
|
-
// so `text` is not a dep of this effect (subsequent `text` updates flow
|
|
59
|
-
// through the second effect below). On `path` change we read whatever the
|
|
60
|
-
// latest `text` is at the moment of mount.
|
|
61
|
-
initialTextRef.current = text;
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (!editorRef.current) return undefined;
|
|
64
|
-
const view = new EditorView({
|
|
65
|
-
parent: editorRef.current,
|
|
66
|
-
state: EditorState.create({
|
|
67
|
-
doc: initialTextRef.current,
|
|
68
|
-
extensions: [
|
|
69
|
-
basicSetup,
|
|
70
|
-
javascript({ jsx: true, typescript: true }),
|
|
71
|
-
indentUnit.of(' '),
|
|
72
|
-
castleCodeTheme,
|
|
73
|
-
syntaxHighlighting(castleHighlightStyle),
|
|
74
|
-
EditorView.updateListener.of((update) => {
|
|
75
|
-
if (!update.docChanged || applyingExternalTextRef.current) return;
|
|
76
|
-
onChangeRef.current(update.state.doc.toString());
|
|
77
|
-
}),
|
|
78
|
-
],
|
|
79
|
-
}),
|
|
80
|
-
});
|
|
81
|
-
viewRef.current = view;
|
|
82
|
-
return () => {
|
|
83
|
-
view.destroy();
|
|
84
|
-
viewRef.current = null;
|
|
85
|
-
};
|
|
86
|
-
}, [path]);
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
const view = viewRef.current;
|
|
89
|
-
if (!view) return;
|
|
90
|
-
const currentText = view.state.doc.toString();
|
|
91
|
-
if (currentText === text) return;
|
|
92
|
-
applyingExternalTextRef.current = true;
|
|
93
|
-
view.dispatch({
|
|
94
|
-
changes: {
|
|
95
|
-
from: 0,
|
|
96
|
-
to: currentText.length,
|
|
97
|
-
insert: text,
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
applyingExternalTextRef.current = false;
|
|
101
|
-
}, [text]);
|
|
102
|
-
return (
|
|
103
|
-
<>
|
|
104
|
-
<EditorHeader title={basename(path)} onToggleFiles={onToggleFiles} filesOpen={filesOpen} />
|
|
105
|
-
<EditorBody>
|
|
106
|
-
<div className={styles.codeEditor}>
|
|
107
|
-
<div ref={editorRef} className={styles.codeMirrorHost} />
|
|
108
|
-
</div>
|
|
109
|
-
</EditorBody>
|
|
110
|
-
</>
|
|
111
|
-
);
|
|
112
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { basename } from '../engine/files';
|
|
3
|
-
import { cx, Icon, SheetGrabHandle, styles, useMobileSheet } from '../engine/ui';
|
|
4
|
-
export function FileBrowser({
|
|
5
|
-
files,
|
|
6
|
-
selectedPath,
|
|
7
|
-
onSelect,
|
|
8
|
-
sheetOpen = false,
|
|
9
|
-
onSheetOpenChange,
|
|
10
|
-
}) {
|
|
11
|
-
const tree = buildFileTree(Object.keys(files));
|
|
12
|
-
const [expanded, setExpanded] = useState(() => new Set(collectDirectoryPaths(tree)));
|
|
13
|
-
const sheet = useMobileSheet({
|
|
14
|
-
snap: sheetOpen ? 'high' : 'hidden',
|
|
15
|
-
baseClassName: styles.fileBrowser,
|
|
16
|
-
onTransition: (direction) => {
|
|
17
|
-
if (direction !== 'up') onSheetOpenChange?.(false);
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
function toggle(path) {
|
|
21
|
-
setExpanded((current) => {
|
|
22
|
-
const next = new Set(current);
|
|
23
|
-
if (next.has(path)) next.delete(path);
|
|
24
|
-
else next.add(path);
|
|
25
|
-
return next;
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
return (
|
|
29
|
-
<aside {...sheet.rootProps}>
|
|
30
|
-
<div {...sheet.grabProps}>
|
|
31
|
-
<SheetGrabHandle label="Files" hint={basename(selectedPath)} />
|
|
32
|
-
</div>
|
|
33
|
-
<div className={styles.fileBrowserHeader}>
|
|
34
|
-
<div>
|
|
35
|
-
<div className={styles.fileBrowserTitle}>Deck Files</div>
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
<div className={styles.fileTree}>
|
|
39
|
-
{tree.children.map((node) => (
|
|
40
|
-
<FileNode
|
|
41
|
-
key={node.path}
|
|
42
|
-
node={node}
|
|
43
|
-
depth={0}
|
|
44
|
-
expanded={expanded}
|
|
45
|
-
selectedPath={selectedPath}
|
|
46
|
-
onSelect={onSelect}
|
|
47
|
-
onToggle={toggle}
|
|
48
|
-
/>
|
|
49
|
-
))}
|
|
50
|
-
</div>
|
|
51
|
-
</aside>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
function FileNode({ node, depth, expanded, selectedPath, onSelect, onToggle }) {
|
|
55
|
-
const depthStyle = { '--file-depth': depth };
|
|
56
|
-
if (node.type === 'directory') {
|
|
57
|
-
const isExpanded = expanded.has(node.path);
|
|
58
|
-
return (
|
|
59
|
-
<div className={styles.fileBranch}>
|
|
60
|
-
<button
|
|
61
|
-
className={styles.fileDirRow}
|
|
62
|
-
style={depthStyle}
|
|
63
|
-
onClick={() => onToggle(node.path)}>
|
|
64
|
-
<span className={styles.fileDisclosure}>
|
|
65
|
-
<Icon name={isExpanded ? 'chevron-down' : 'chevron-right'} />
|
|
66
|
-
</span>
|
|
67
|
-
<span>{node.name}</span>
|
|
68
|
-
</button>
|
|
69
|
-
{isExpanded ? (
|
|
70
|
-
<div>
|
|
71
|
-
{node.children.map((child) => (
|
|
72
|
-
<FileNode
|
|
73
|
-
key={child.path}
|
|
74
|
-
node={child}
|
|
75
|
-
depth={depth + 1}
|
|
76
|
-
expanded={expanded}
|
|
77
|
-
selectedPath={selectedPath}
|
|
78
|
-
onSelect={onSelect}
|
|
79
|
-
onToggle={onToggle}
|
|
80
|
-
/>
|
|
81
|
-
))}
|
|
82
|
-
</div>
|
|
83
|
-
) : null}
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
// File rows nest under their directory header: extra left padding so
|
|
88
|
-
// file text sits to the right of the parent directory header.
|
|
89
|
-
const fileRowStyle = {
|
|
90
|
-
...depthStyle,
|
|
91
|
-
paddingLeft: `calc(9px + ${depth} * 16px + 7px)`,
|
|
92
|
-
};
|
|
93
|
-
return (
|
|
94
|
-
<button
|
|
95
|
-
className={cx(styles.fileRow, selectedPath === node.path && styles.fileRowSelected)}
|
|
96
|
-
style={fileRowStyle}
|
|
97
|
-
onClick={() => onSelect(node.path)}>
|
|
98
|
-
<span>{basename(node.path)}</span>
|
|
99
|
-
</button>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
function buildFileTree(paths) {
|
|
103
|
-
const root = {
|
|
104
|
-
type: 'directory',
|
|
105
|
-
name: '',
|
|
106
|
-
path: '',
|
|
107
|
-
children: [],
|
|
108
|
-
childMap: new Map(),
|
|
109
|
-
};
|
|
110
|
-
for (const path of paths) {
|
|
111
|
-
const parts = path.split('/');
|
|
112
|
-
let parent = root;
|
|
113
|
-
for (let index = 0; index < parts.length; index++) {
|
|
114
|
-
const name = parts[index];
|
|
115
|
-
const nodePath = parts.slice(0, index + 1).join('/');
|
|
116
|
-
const isFile = index === parts.length - 1;
|
|
117
|
-
if (!parent.childMap?.has(name)) {
|
|
118
|
-
const node = isFile
|
|
119
|
-
? { type: 'file', name, path: nodePath }
|
|
120
|
-
: { type: 'directory', name, path: nodePath, children: [], childMap: new Map() };
|
|
121
|
-
parent.childMap?.set(name, node);
|
|
122
|
-
parent.children.push(node);
|
|
123
|
-
}
|
|
124
|
-
const child = parent.childMap?.get(name);
|
|
125
|
-
if (!child || child.type !== 'directory') break;
|
|
126
|
-
parent = child;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
pruneChildMaps(root);
|
|
130
|
-
return root;
|
|
131
|
-
}
|
|
132
|
-
function pruneChildMaps(node) {
|
|
133
|
-
if (node.type !== 'directory') return;
|
|
134
|
-
for (const child of node.children) pruneChildMaps(child);
|
|
135
|
-
delete node.childMap;
|
|
136
|
-
}
|
|
137
|
-
function collectDirectoryPaths(node) {
|
|
138
|
-
if (node.type !== 'directory') return [];
|
|
139
|
-
return [
|
|
140
|
-
...(node.path ? [node.path] : []),
|
|
141
|
-
...node.children.flatMap((child) => collectDirectoryPaths(child)),
|
|
142
|
-
];
|
|
143
|
-
}
|