castle-web-cli 0.4.2 → 0.4.4

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 (96) hide show
  1. package/dist/index.js +8 -2
  2. package/dist/init.js +9 -6
  3. package/package.json +1 -1
  4. package/kits/basic-2d-frozen/.prettierrc +0 -8
  5. package/kits/basic-2d-frozen/CLAUDE.md +0 -131
  6. package/kits/basic-2d-frozen/behaviors/Camera.jsx +0 -43
  7. package/kits/basic-2d-frozen/behaviors/Collider.jsx +0 -71
  8. package/kits/basic-2d-frozen/behaviors/Drawing.jsx +0 -139
  9. package/kits/basic-2d-frozen/behaviors/Layout.jsx +0 -16
  10. package/kits/basic-2d-frozen/drawings/floor.drawing +0 -70
  11. package/kits/basic-2d-frozen/editors/App.jsx +0 -152
  12. package/kits/basic-2d-frozen/editors/CodeEditor.jsx +0 -112
  13. package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +0 -222
  14. package/kits/basic-2d-frozen/editors/FileBrowser.jsx +0 -143
  15. package/kits/basic-2d-frozen/editors/PlayOnly.jsx +0 -21
  16. package/kits/basic-2d-frozen/editors/SceneEditor.jsx +0 -1012
  17. package/kits/basic-2d-frozen/editors/behaviorRegistry.js +0 -24
  18. package/kits/basic-2d-frozen/editors/editorHistory.js +0 -52
  19. package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +0 -83
  20. package/kits/basic-2d-frozen/engine/SceneUI.jsx +0 -67
  21. package/kits/basic-2d-frozen/engine/TouchControls.jsx +0 -136
  22. package/kits/basic-2d-frozen/engine/autoInspector.jsx +0 -51
  23. package/kits/basic-2d-frozen/engine/files.js +0 -62
  24. package/kits/basic-2d-frozen/engine/scene.js +0 -420
  25. package/kits/basic-2d-frozen/engine/ui.jsx +0 -344
  26. package/kits/basic-2d-frozen/engine/ui.module.css +0 -928
  27. package/kits/basic-2d-frozen/eslint.config.js +0 -50
  28. package/kits/basic-2d-frozen/index.html +0 -11
  29. package/kits/basic-2d-frozen/main.jsx +0 -10
  30. package/kits/basic-2d-frozen/package-lock.json +0 -2706
  31. package/kits/basic-2d-frozen/package.json +0 -41
  32. package/kits/basic-2d-frozen/scenes/main.scene +0 -108
  33. package/kits/basic-2d-frozen/vite.config.js +0 -1
  34. package/kits/rpg-2d/.prettierrc +0 -8
  35. package/kits/rpg-2d/behaviors/Camera.tsx +0 -52
  36. package/kits/rpg-2d/behaviors/Collider.tsx +0 -98
  37. package/kits/rpg-2d/behaviors/Dialog.tsx +0 -184
  38. package/kits/rpg-2d/behaviors/Drawing.tsx +0 -161
  39. package/kits/rpg-2d/behaviors/Friend.tsx +0 -45
  40. package/kits/rpg-2d/behaviors/Layout.tsx +0 -29
  41. package/kits/rpg-2d/behaviors/PlayerController.tsx +0 -255
  42. package/kits/rpg-2d/behaviors/Portal.tsx +0 -60
  43. package/kits/rpg-2d/behaviors/QuestLog.tsx +0 -90
  44. package/kits/rpg-2d/behaviors/SaveMenu.tsx +0 -123
  45. package/kits/rpg-2d/behaviors/Tilemap.tsx +0 -90
  46. package/kits/rpg-2d/drawings/bld-home.drawing +0 -8136
  47. package/kits/rpg-2d/drawings/env-crate.drawing +0 -509
  48. package/kits/rpg-2d/drawings/env-fence.drawing +0 -536
  49. package/kits/rpg-2d/drawings/env-flower-bed.drawing +0 -607
  50. package/kits/rpg-2d/drawings/env-fountain.drawing +0 -2622
  51. package/kits/rpg-2d/drawings/env-hedge.drawing +0 -601
  52. package/kits/rpg-2d/drawings/env-house-blue.drawing +0 -1
  53. package/kits/rpg-2d/drawings/env-house-green.drawing +0 -1
  54. package/kits/rpg-2d/drawings/env-tree-oak.drawing +0 -1540
  55. package/kits/rpg-2d/drawings/env-tree-pine.drawing +0 -1315
  56. package/kits/rpg-2d/drawings/floor.drawing +0 -70
  57. package/kits/rpg-2d/drawings/fx-sparkle.drawing +0 -926
  58. package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +0 -1099
  59. package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +0 -4177
  60. package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +0 -1099
  61. package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +0 -4177
  62. package/kits/rpg-2d/drawings/player-idle-down.drawing +0 -1070
  63. package/kits/rpg-2d/drawings/player-idle-left.drawing +0 -1070
  64. package/kits/rpg-2d/drawings/player-idle-right.drawing +0 -1070
  65. package/kits/rpg-2d/drawings/player-idle-up.drawing +0 -1070
  66. package/kits/rpg-2d/drawings/player-walk-down.drawing +0 -4148
  67. package/kits/rpg-2d/drawings/player-walk-left.drawing +0 -4148
  68. package/kits/rpg-2d/drawings/player-walk-right.drawing +0 -4148
  69. package/kits/rpg-2d/drawings/player-walk-up.drawing +0 -4148
  70. package/kits/rpg-2d/editors/App.tsx +0 -163
  71. package/kits/rpg-2d/editors/CodeEditor.tsx +0 -120
  72. package/kits/rpg-2d/editors/DrawingEditor.tsx +0 -278
  73. package/kits/rpg-2d/editors/FileBrowser.tsx +0 -191
  74. package/kits/rpg-2d/editors/PlayOnly.tsx +0 -26
  75. package/kits/rpg-2d/editors/SceneEditor.tsx +0 -1093
  76. package/kits/rpg-2d/editors/behaviorRegistry.ts +0 -33
  77. package/kits/rpg-2d/editors/editorHistory.ts +0 -75
  78. package/kits/rpg-2d/editors/editorProps.ts +0 -10
  79. package/kits/rpg-2d/engine/ScenePlayer.tsx +0 -130
  80. package/kits/rpg-2d/engine/SceneUI.tsx +0 -74
  81. package/kits/rpg-2d/engine/TouchControls.tsx +0 -157
  82. package/kits/rpg-2d/engine/autoInspector.tsx +0 -111
  83. package/kits/rpg-2d/engine/drawing.ts +0 -81
  84. package/kits/rpg-2d/engine/files.ts +0 -215
  85. package/kits/rpg-2d/engine/scene.ts +0 -484
  86. package/kits/rpg-2d/engine/ui.module.css +0 -928
  87. package/kits/rpg-2d/engine/ui.tsx +0 -483
  88. package/kits/rpg-2d/eslint.config.js +0 -46
  89. package/kits/rpg-2d/index.html +0 -11
  90. package/kits/rpg-2d/main.tsx +0 -14
  91. package/kits/rpg-2d/package-lock.json +0 -3149
  92. package/kits/rpg-2d/package.json +0 -46
  93. package/kits/rpg-2d/scenes/main.scene +0 -203
  94. package/kits/rpg-2d/tsconfig.json +0 -17
  95. package/kits/rpg-2d/vite-env.d.ts +0 -7
  96. package/kits/rpg-2d/vite.config.js +0 -1
@@ -1,152 +0,0 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { writeFile } from 'castle-web-sdk';
3
- import {
4
- flatFileOrder,
5
- formatJson,
6
- getFileKind,
7
- initialFiles,
8
- parseJsonFile,
9
- } from '../engine/files';
10
- import { AppShell, cx, MainEditor, styles } from '../engine/ui';
11
- import { CodeEditor } from './CodeEditor';
12
- import { DrawingEditor } from './DrawingEditor';
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 drawings = {};
24
- for (const [path, text] of Object.entries(files)) {
25
- if (!path.endsWith('.drawing')) continue;
26
- const parsed = parseJsonFile(path, text);
27
- if (parsed.value) drawings[path] = parsed.value;
28
- }
29
- function updateSelectedFile(nextText) {
30
- const path = selectedPath;
31
- setFiles((current) => ({
32
- ...current,
33
- [path]: nextText,
34
- }));
35
- scheduleFileWrite(path, nextText);
36
- }
37
- function scheduleFileWrite(path, nextText) {
38
- const version = (saveVersionsRef.current[path] ?? 0) + 1;
39
- saveVersionsRef.current[path] = version;
40
- const existingTimer = saveTimersRef.current[path];
41
- if (existingTimer) window.clearTimeout(existingTimer);
42
- saveTimersRef.current[path] = window.setTimeout(() => {
43
- delete saveTimersRef.current[path];
44
- writeFile(path, nextText).catch((error) => {
45
- if (saveVersionsRef.current[path] !== version) return;
46
- const message = error instanceof Error ? error.message : String(error);
47
- console.error(`Failed to save ${path}: ${message}`);
48
- });
49
- }, 1500);
50
- }
51
- const kind = getFileKind(selectedPath);
52
- const text = files[selectedPath] ?? '';
53
- function selectFile(path) {
54
- setSelectedPath(path);
55
- setFilesSheetOpen(false);
56
- setSelectedActorIds([]);
57
- setMultiSelectMode(false);
58
- }
59
- const onToggleFiles = () => {
60
- setFilesSheetOpen((previous) => !previous);
61
- setSelectedActorIds([]);
62
- setMultiSelectMode(false);
63
- };
64
- // Alt+Up / Alt+Down steps through files in file-browser sidebar order.
65
- useEffect(() => {
66
- function onKeyDown(event) {
67
- if (!event.altKey || event.metaKey || event.ctrlKey) return;
68
- if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
69
- const active = document.activeElement;
70
- if (
71
- active &&
72
- (active.tagName === 'INPUT' ||
73
- active.tagName === 'TEXTAREA' ||
74
- active.tagName === 'SELECT' ||
75
- active.isContentEditable ||
76
- active.closest('.cm-editor'))
77
- ) {
78
- return;
79
- }
80
- const order = flatFileOrder(Object.keys(files));
81
- const index = order.indexOf(selectedPath);
82
- if (index === -1) return;
83
- const nextIndex = event.key === 'ArrowUp' ? index - 1 : index + 1;
84
- if (nextIndex < 0 || nextIndex >= order.length) return;
85
- event.preventDefault();
86
- selectFile(order[nextIndex]);
87
- }
88
- window.addEventListener('keydown', onKeyDown);
89
- return () => window.removeEventListener('keydown', onKeyDown);
90
- }, [files, selectedPath]);
91
- return (
92
- <AppShell>
93
- <FileBrowser
94
- files={files}
95
- selectedPath={selectedPath}
96
- onSelect={selectFile}
97
- sheetOpen={filesSheetOpen}
98
- onSheetOpenChange={setFilesSheetOpen}
99
- />
100
- {filesSheetOpen ? (
101
- <div
102
- className={cx(styles.sheetBackdrop, styles.mobileOnly)}
103
- onClick={() => setFilesSheetOpen(false)}
104
- />
105
- ) : null}
106
- <MainEditor>
107
- {kind === 'scene' ? (
108
- <SceneEditor
109
- path={selectedPath}
110
- text={text}
111
- files={files}
112
- drawings={drawings}
113
- onChange={updateSelectedFile}
114
- onToggleFiles={onToggleFiles}
115
- filesOpen={filesSheetOpen}
116
- selectedActorIds={selectedActorIds}
117
- onSelectActorIds={setSelectedActorIds}
118
- multiSelectMode={multiSelectMode}
119
- onSetMultiSelectMode={setMultiSelectMode}
120
- />
121
- ) : null}
122
- {kind === 'drawing' ? (
123
- <DrawingEditor
124
- path={selectedPath}
125
- text={text}
126
- onChange={updateSelectedFile}
127
- onToggleFiles={onToggleFiles}
128
- filesOpen={filesSheetOpen}
129
- />
130
- ) : null}
131
- {kind === 'code' ? (
132
- <CodeEditor
133
- path={selectedPath}
134
- text={text}
135
- onChange={updateSelectedFile}
136
- onToggleFiles={onToggleFiles}
137
- filesOpen={filesSheetOpen}
138
- />
139
- ) : null}
140
- {kind === 'text' ? (
141
- <CodeEditor
142
- path={selectedPath}
143
- text={formatJson(text)}
144
- onChange={updateSelectedFile}
145
- onToggleFiles={onToggleFiles}
146
- filesOpen={filesSheetOpen}
147
- />
148
- ) : null}
149
- </MainEditor>
150
- </AppShell>
151
- );
152
- }
@@ -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,222 +0,0 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { basename, formatJson, parseJsonFile } from '../engine/files';
3
- import {
4
- cx,
5
- EditorBody,
6
- EditorHeader,
7
- IconButton,
8
- NumberField,
9
- Panel,
10
- SheetGrabHandle,
11
- styles,
12
- theme,
13
- useMobileSheet,
14
- } from '../engine/ui';
15
- import { useEditHistory } from './editorHistory';
16
- const palette = [
17
- '#00000000',
18
- '#000000FF',
19
- '#050505FF',
20
- '#111111FF',
21
- '#242234FF',
22
- '#444444FF',
23
- '#777777FF',
24
- '#ffffffFF',
25
- '#e3e6ffFF',
26
- '#8db7ffFF',
27
- '#ff5d5dFF',
28
- '#ffe17aFF',
29
- ];
30
- export function DrawingEditor({ path, text, onChange, onToggleFiles, filesOpen }) {
31
- const canvasRef = useRef(null);
32
- const strokeRef = useRef({ active: false, recorded: false });
33
- const [color, setColor] = useState('#ffffffFF');
34
- const [inspectorOpen, setInspectorOpen] = useState(true);
35
- const history = useEditHistory(text, onChange);
36
- const inspectorSheet = useInspectorSheet(inspectorOpen);
37
- const { value: drawing, error } = parseJsonFile(path, text);
38
- const isWide = drawing ? drawing.width >= drawing.height : true;
39
- useEffect(() => {
40
- if (!drawing || !canvasRef.current) return;
41
- const canvas = canvasRef.current;
42
- const ctx = canvas.getContext('2d');
43
- if (!ctx) return;
44
- canvas.width = drawing.width;
45
- canvas.height = drawing.height;
46
- ctx.clearRect(0, 0, drawing.width, drawing.height);
47
- for (let y = 0; y < drawing.height; y++) {
48
- for (let x = 0; x < drawing.width; x++) {
49
- const pixel = drawing.pixels[y * drawing.width + x];
50
- if (!pixel || pixel.endsWith('00')) continue;
51
- ctx.fillStyle = pixel;
52
- ctx.fillRect(x, y, 1, 1);
53
- }
54
- }
55
- }, [drawing, text]);
56
- function commitDrawing(nextDrawing) {
57
- history.commit(formatJson(nextDrawing));
58
- }
59
- function resizeDrawing(nextWidth, nextHeight) {
60
- if (!drawing) return;
61
- const width = clampInteger(nextWidth, 1, 128);
62
- const height = clampInteger(nextHeight, 1, 128);
63
- if (width === drawing.width && height === drawing.height) return;
64
- const pixels = Array(width * height).fill('#00000000');
65
- const copiedWidth = Math.min(width, drawing.width);
66
- const copiedHeight = Math.min(height, drawing.height);
67
- for (let y = 0; y < copiedHeight; y++) {
68
- for (let x = 0; x < copiedWidth; x++) {
69
- pixels[y * width + x] = drawing.pixels[y * drawing.width + x] ?? '#00000000';
70
- }
71
- }
72
- commitDrawing({ ...drawing, width, height, pixels });
73
- }
74
- function paintAt(event) {
75
- if (!drawing) return;
76
- const rect = event.currentTarget.getBoundingClientRect();
77
- const x = Math.floor(((event.clientX - rect.left) / rect.width) * drawing.width);
78
- const y = Math.floor(((event.clientY - rect.top) / rect.height) * drawing.height);
79
- if (x < 0 || y < 0 || x >= drawing.width || y >= drawing.height) return;
80
- if (drawing.pixels[y * drawing.width + x] === color) return;
81
- const next = structuredClone(drawing);
82
- next.pixels[y * next.width + x] = color;
83
- // Record one undo snapshot per pointer stroke (not per painted pixel).
84
- if (!strokeRef.current.recorded) {
85
- history.recordSnapshot();
86
- strokeRef.current.recorded = true;
87
- }
88
- onChange(formatJson(next));
89
- }
90
- function startPaint(event) {
91
- strokeRef.current = { active: true, recorded: false };
92
- event.currentTarget.setPointerCapture(event.pointerId);
93
- paintAt(event);
94
- }
95
- function finishPaint() {
96
- strokeRef.current = { active: false, recorded: false };
97
- }
98
- const canvasStyle = drawing
99
- ? {
100
- width: isWide ? 'min(70vh, 520px)' : 'auto',
101
- height: isWide ? 'auto' : 'min(70vh, 520px)',
102
- }
103
- : undefined;
104
- const sharedActionButtons = (
105
- <>
106
- <IconButton icon="undo" label="Undo" onClick={history.undo} disabled={!history.canUndo} />
107
- <IconButton icon="redo" label="Redo" onClick={history.redo} disabled={!history.canRedo} />
108
- </>
109
- );
110
- return (
111
- <>
112
- <EditorHeader
113
- title={basename(path)}
114
- subtitle={drawing ? `${drawing.width} x ${drawing.height} pixels` : error}
115
- right={
116
- <span className={styles.mobileOnly}>
117
- {sharedActionButtons}
118
- <IconButton
119
- icon="palette"
120
- label="Tools"
121
- active={inspectorOpen}
122
- onClick={() => setInspectorOpen((previous) => !previous)}
123
- />
124
- </span>
125
- }
126
- onToggleFiles={onToggleFiles}
127
- filesOpen={filesOpen}
128
- />
129
- <EditorBody>
130
- <div className={styles.drawingEditor}>
131
- <div className={styles.drawingTools}>{sharedActionButtons}</div>
132
- <div className={styles.drawingCanvasWrap}>
133
- <canvas
134
- ref={canvasRef}
135
- className={styles.drawingCanvas}
136
- style={canvasStyle}
137
- onPointerDown={startPaint}
138
- onPointerMove={(event) => {
139
- if (strokeRef.current.active && event.buttons === 1) paintAt(event);
140
- }}
141
- onPointerUp={finishPaint}
142
- onPointerCancel={finishPaint}
143
- />
144
- </div>
145
- <DrawingInspector
146
- sheet={inspectorSheet}
147
- drawing={drawing}
148
- color={color}
149
- onSelectColor={setColor}
150
- onResize={resizeDrawing}
151
- />
152
- </div>
153
- </EditorBody>
154
- </>
155
- );
156
- }
157
- function useInspectorSheet(inspectorOpen) {
158
- const [snap, setSnap] = useState('high');
159
- useEffect(() => {
160
- if (inspectorOpen) setSnap('high');
161
- }, [inspectorOpen]);
162
- const effectiveSnap = inspectorOpen ? snap : 'hidden';
163
- return useMobileSheet({
164
- snap: effectiveSnap,
165
- baseClassName: styles.inspector,
166
- onTransition: (direction) => {
167
- if (!inspectorOpen) return;
168
- if (direction === 'tap') setSnap((previous) => (previous === 'high' ? 'low' : 'high'));
169
- else if (direction === 'down') setSnap('low');
170
- else if (direction === 'up') setSnap('high');
171
- },
172
- });
173
- }
174
- function DrawingInspector({ sheet, drawing, color, onSelectColor, onResize }) {
175
- return (
176
- <aside {...sheet.rootProps} className={cx(sheet.rootProps.className, styles.inspectorAuto)}>
177
- <div {...sheet.grabProps}>
178
- <SheetGrabHandle
179
- label="Inspector"
180
- hint={drawing ? `${drawing.width}x${drawing.height} · color` : 'drawing tools'}
181
- />
182
- </div>
183
- <div className={cx(styles.sheetBody, styles.inspectorBody)}>
184
- <Panel title="Size">
185
- <NumberField
186
- label="Width"
187
- value={drawing?.width ?? 1}
188
- min={1}
189
- max={128}
190
- onChange={(width) => onResize(width, drawing?.height ?? 1)}
191
- />
192
- <NumberField
193
- label="Height"
194
- value={drawing?.height ?? 1}
195
- min={1}
196
- max={128}
197
- onChange={(height) => onResize(drawing?.width ?? 1, height)}
198
- />
199
- </Panel>
200
- <Panel title="Palette">
201
- <div className={styles.palette}>
202
- {palette.map((swatch) => (
203
- <button
204
- key={swatch}
205
- className={cx(styles.swatch, color === swatch && styles.swatchSelected)}
206
- title={swatch}
207
- style={{
208
- background: swatch.endsWith('00') ? theme.transparentChecker : swatch,
209
- }}
210
- onClick={() => onSelectColor(swatch)}
211
- />
212
- ))}
213
- </div>
214
- </Panel>
215
- </div>
216
- </aside>
217
- );
218
- }
219
- function clampInteger(value, min, max) {
220
- if (!Number.isFinite(value)) return min;
221
- return Math.max(min, Math.min(max, Math.round(value)));
222
- }
@@ -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
- }
@@ -1,21 +0,0 @@
1
- import React from 'react';
2
- import { initialFiles, parseJsonFile } from '../engine/files';
3
- import { ScenePlayer } from '../engine/ScenePlayer';
4
- import { behaviorClasses } from './behaviorRegistry';
5
- // Play-mode entry point. Intentionally thin: it locates the start scene and
6
- // hands it to the engine's `ScenePlayer`, which owns the runtime and input.
7
- // Deck/game logic belongs in `scenes/` and `behaviors/`, not here.
8
- export function PlayOnly() {
9
- const sceneText = initialFiles['scenes/main.scene'] ?? '';
10
- const { value: sceneData } = parseJsonFile('scenes/main.scene', sceneText);
11
- if (!sceneData) return null;
12
- const drawings = {};
13
- for (const [path, text] of Object.entries(initialFiles)) {
14
- if (!path.endsWith('.drawing')) continue;
15
- const parsed = parseJsonFile(path, text);
16
- if (parsed.value) drawings[path] = parsed.value;
17
- }
18
- return (
19
- <ScenePlayer sceneData={sceneData} drawings={drawings} behaviorClasses={behaviorClasses} />
20
- );
21
- }