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,163 +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
- normalizeDrawing,
9
- parseJsonFile,
10
- } from '../engine/files';
11
- import type { DrawingData, FileMap } from '../engine/files';
12
- import { AppShell, cx, MainEditor, styles } from '../engine/ui';
13
- import { CodeEditor } from './CodeEditor';
14
- import { DrawingEditor } from './DrawingEditor';
15
- import { FileBrowser } from './FileBrowser';
16
- import { SceneEditor } from './SceneEditor';
17
-
18
- export function App() {
19
- const [files, setFiles] = useState<FileMap>(initialFiles);
20
- const [selectedPath, setSelectedPath] = useState('scenes/main.scene');
21
- const [filesSheetOpen, setFilesSheetOpen] = useState(false);
22
- const [selectedActorIds, setSelectedActorIds] = useState<string[]>([]);
23
- const [multiSelectMode, setMultiSelectMode] = useState(false);
24
- const saveTimersRef = useRef<Record<string, number>>({});
25
- const saveVersionsRef = useRef<Record<string, number>>({});
26
-
27
- const drawings: Record<string, DrawingData> = {};
28
- for (const [path, text] of Object.entries(files)) {
29
- if (!path.endsWith('.drawing')) continue;
30
- const parsed = parseJsonFile<unknown>(path, text);
31
- if (parsed.value) drawings[path] = normalizeDrawing(parsed.value);
32
- }
33
-
34
- function updateSelectedFile(nextText: string): void {
35
- const path = selectedPath;
36
- setFiles((current) => ({
37
- ...current,
38
- [path]: nextText,
39
- }));
40
- scheduleFileWrite(path, nextText);
41
- }
42
-
43
- function scheduleFileWrite(path: string, nextText: string): void {
44
- const version = (saveVersionsRef.current[path] ?? 0) + 1;
45
- saveVersionsRef.current[path] = version;
46
- const existingTimer = saveTimersRef.current[path];
47
- if (existingTimer) window.clearTimeout(existingTimer);
48
- saveTimersRef.current[path] = window.setTimeout(() => {
49
- delete saveTimersRef.current[path];
50
- writeFile(path, nextText).catch((error: unknown) => {
51
- if (saveVersionsRef.current[path] !== version) return;
52
- const message = error instanceof Error ? error.message : String(error);
53
- console.error(`Failed to save ${path}: ${message}`);
54
- });
55
- }, 1500);
56
- }
57
-
58
- const kind = getFileKind(selectedPath);
59
- const text = files[selectedPath] ?? '';
60
-
61
- function selectFile(path: string): void {
62
- setSelectedPath(path);
63
- setFilesSheetOpen(false);
64
- setSelectedActorIds([]);
65
- setMultiSelectMode(false);
66
- }
67
-
68
- const onToggleFiles = (): void => {
69
- setFilesSheetOpen((previous) => !previous);
70
- setSelectedActorIds([]);
71
- setMultiSelectMode(false);
72
- };
73
-
74
- // Alt+Up / Alt+Down steps through files in file-browser sidebar order.
75
- useEffect(() => {
76
- function onKeyDown(event: KeyboardEvent): void {
77
- if (!event.altKey || event.metaKey || event.ctrlKey) return;
78
- if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
79
- const active = document.activeElement as HTMLElement | null;
80
- if (
81
- active &&
82
- (active.tagName === 'INPUT' ||
83
- active.tagName === 'TEXTAREA' ||
84
- active.tagName === 'SELECT' ||
85
- active.isContentEditable ||
86
- active.closest('.cm-editor'))
87
- ) {
88
- return;
89
- }
90
- const order = flatFileOrder(Object.keys(files));
91
- const index = order.indexOf(selectedPath);
92
- if (index === -1) return;
93
- const nextIndex = event.key === 'ArrowUp' ? index - 1 : index + 1;
94
- if (nextIndex < 0 || nextIndex >= order.length) return;
95
- event.preventDefault();
96
- selectFile(order[nextIndex]);
97
- }
98
- window.addEventListener('keydown', onKeyDown);
99
- return () => window.removeEventListener('keydown', onKeyDown);
100
- }, [files, selectedPath]);
101
-
102
- return (
103
- <AppShell>
104
- <FileBrowser
105
- files={files}
106
- selectedPath={selectedPath}
107
- onSelect={selectFile}
108
- sheetOpen={filesSheetOpen}
109
- onSheetOpenChange={setFilesSheetOpen}
110
- />
111
- {filesSheetOpen ? (
112
- <div
113
- className={cx(styles.sheetBackdrop, styles.mobileOnly)}
114
- onClick={() => setFilesSheetOpen(false)}
115
- />
116
- ) : null}
117
- <MainEditor>
118
- {kind === 'scene' ? (
119
- <SceneEditor
120
- path={selectedPath}
121
- text={text}
122
- files={files}
123
- drawings={drawings}
124
- onChange={updateSelectedFile}
125
- onToggleFiles={onToggleFiles}
126
- filesOpen={filesSheetOpen}
127
- selectedActorIds={selectedActorIds}
128
- onSelectActorIds={setSelectedActorIds}
129
- multiSelectMode={multiSelectMode}
130
- onSetMultiSelectMode={setMultiSelectMode}
131
- />
132
- ) : null}
133
- {kind === 'drawing' ? (
134
- <DrawingEditor
135
- path={selectedPath}
136
- text={text}
137
- onChange={updateSelectedFile}
138
- onToggleFiles={onToggleFiles}
139
- filesOpen={filesSheetOpen}
140
- />
141
- ) : null}
142
- {kind === 'code' ? (
143
- <CodeEditor
144
- path={selectedPath}
145
- text={text}
146
- onChange={updateSelectedFile}
147
- onToggleFiles={onToggleFiles}
148
- filesOpen={filesSheetOpen}
149
- />
150
- ) : null}
151
- {kind === 'text' ? (
152
- <CodeEditor
153
- path={selectedPath}
154
- text={formatJson(text)}
155
- onChange={updateSelectedFile}
156
- onToggleFiles={onToggleFiles}
157
- filesOpen={filesSheetOpen}
158
- />
159
- ) : null}
160
- </MainEditor>
161
- </AppShell>
162
- );
163
- }
@@ -1,120 +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
- import type { FileEditorProps } from './editorProps';
11
-
12
- const castleHighlightStyle = HighlightStyle.define([
13
- { tag: tags.strong, color: '#285CC4' },
14
- { tag: tags.namespace, color: '#BB7547' },
15
- { tag: tags.keyword, color: '#BC4A9B' },
16
- { tag: [tags.literal, tags.inserted], color: '#5DAF8D' },
17
- { tag: [tags.string, tags.deleted], color: '#E86A73' },
18
- { tag: tags.comment, color: '#8B93AF', fontStyle: 'italic' },
19
- ]);
20
-
21
- const castleCodeTheme = EditorView.theme({
22
- '&': {
23
- height: '100%',
24
- fontSize: '9pt',
25
- backgroundColor: '#fff',
26
- },
27
- '&.cm-editor.cm-focused': {
28
- outline: 'none',
29
- },
30
- '.cm-scroller': {
31
- overflow: 'auto',
32
- fontFamily: 'Menlo, Monaco, Lucida Console, monospace',
33
- },
34
- '.cm-content': {
35
- minHeight: '100%',
36
- color: '#322b28',
37
- paddingBottom: '400px',
38
- paddingRight: '80px',
39
- },
40
- '.cm-gutters': {
41
- display: 'none',
42
- },
43
- '.cm-activeLine': {
44
- backgroundColor: '#eeeeeea0',
45
- },
46
- '.cm-activeLineGutter': {
47
- color: '#000',
48
- backgroundColor: '#ddd',
49
- },
50
- });
51
-
52
- export function CodeEditor({ path, text, onChange, onToggleFiles, filesOpen }: FileEditorProps) {
53
- const editorRef = useRef<HTMLDivElement | null>(null);
54
- const viewRef = useRef<EditorView | null>(null);
55
- const onChangeRef = useRef(onChange);
56
- const initialTextRef = useRef(text);
57
- const applyingExternalTextRef = useRef(false);
58
-
59
- useEffect(() => {
60
- onChangeRef.current = onChange;
61
- }, [onChange]);
62
-
63
- // The editor is created once per `path`; the initial doc comes from a ref
64
- // so `text` is not a dep of this effect (subsequent `text` updates flow
65
- // through the second effect below). On `path` change we read whatever the
66
- // latest `text` is at the moment of mount.
67
- initialTextRef.current = text;
68
- useEffect(() => {
69
- if (!editorRef.current) return undefined;
70
- const view = new EditorView({
71
- parent: editorRef.current,
72
- state: EditorState.create({
73
- doc: initialTextRef.current,
74
- extensions: [
75
- basicSetup,
76
- javascript({ jsx: true, typescript: true }),
77
- indentUnit.of(' '),
78
- castleCodeTheme,
79
- syntaxHighlighting(castleHighlightStyle),
80
- EditorView.updateListener.of((update) => {
81
- if (!update.docChanged || applyingExternalTextRef.current) return;
82
- onChangeRef.current(update.state.doc.toString());
83
- }),
84
- ],
85
- }),
86
- });
87
- viewRef.current = view;
88
- return () => {
89
- view.destroy();
90
- viewRef.current = null;
91
- };
92
- }, [path]);
93
-
94
- useEffect(() => {
95
- const view = viewRef.current;
96
- if (!view) return;
97
- const currentText = view.state.doc.toString();
98
- if (currentText === text) return;
99
- applyingExternalTextRef.current = true;
100
- view.dispatch({
101
- changes: {
102
- from: 0,
103
- to: currentText.length,
104
- insert: text,
105
- },
106
- });
107
- applyingExternalTextRef.current = false;
108
- }, [text]);
109
-
110
- return (
111
- <>
112
- <EditorHeader title={basename(path)} onToggleFiles={onToggleFiles} filesOpen={filesOpen} />
113
- <EditorBody>
114
- <div className={styles.codeEditor}>
115
- <div ref={editorRef} className={styles.codeMirrorHost} />
116
- </div>
117
- </EditorBody>
118
- </>
119
- );
120
- }
@@ -1,278 +0,0 @@
1
- import type { CSSProperties, PointerEvent } from 'react';
2
- import React, { useEffect, useRef, useState } from 'react';
3
- import { basename, formatJson, normalizeDrawing, parseJsonFile } from '../engine/files';
4
- import type { DrawingData } from '../engine/files';
5
-
6
- // The editor treats layer 0, frame 0 as the editable surface -- multi-layer +
7
- // multi-frame authoring is a future feature. Helpers below shim between that
8
- // flat-pixel mental model and the layered DrawingData on disk.
9
- function getEditPixels(drawing: DrawingData): string[] {
10
- return drawing.layers[0]?.frames[0] ?? [];
11
- }
12
- function withEditPixels(drawing: DrawingData, pixels: string[]): DrawingData {
13
- const layers = drawing.layers.map((layer, layerIndex) =>
14
- layerIndex === 0
15
- ? { ...layer, frames: layer.frames.map((frame, frameIndex) => (frameIndex === 0 ? pixels : frame)) }
16
- : layer
17
- );
18
- return { ...drawing, layers };
19
- }
20
- import {
21
- cx,
22
- EditorBody,
23
- EditorHeader,
24
- IconButton,
25
- NumberField,
26
- Panel,
27
- SheetGrabHandle,
28
- styles,
29
- theme,
30
- useMobileSheet,
31
- } from '../engine/ui';
32
- import type { SheetSnap } from '../engine/ui';
33
- import type { FileEditorProps } from './editorProps';
34
- import { useEditHistory } from './editorHistory';
35
-
36
- const palette = [
37
- '#00000000',
38
- '#000000FF',
39
- '#050505FF',
40
- '#111111FF',
41
- '#242234FF',
42
- '#444444FF',
43
- '#777777FF',
44
- '#ffffffFF',
45
- '#e3e6ffFF',
46
- '#8db7ffFF',
47
- '#ff5d5dFF',
48
- '#ffe17aFF',
49
- ];
50
-
51
- interface StrokeState {
52
- active: boolean;
53
- recorded: boolean;
54
- }
55
-
56
- export function DrawingEditor({ path, text, onChange, onToggleFiles, filesOpen }: FileEditorProps) {
57
- const canvasRef = useRef<HTMLCanvasElement | null>(null);
58
- const strokeRef = useRef<StrokeState>({ active: false, recorded: false });
59
- const [color, setColor] = useState('#ffffffFF');
60
- const [inspectorOpen, setInspectorOpen] = useState(true);
61
- const history = useEditHistory(text, onChange);
62
- const inspectorSheet = useInspectorSheet(inspectorOpen);
63
- const parsed = parseJsonFile<unknown>(path, text);
64
- const drawing: DrawingData | null = parsed.value ? normalizeDrawing(parsed.value) : null;
65
- const error = parsed.error;
66
- const isWide = drawing ? drawing.width >= drawing.height : true;
67
-
68
- useEffect(() => {
69
- if (!drawing || !canvasRef.current) return;
70
- const canvas = canvasRef.current;
71
- const ctx = canvas.getContext('2d');
72
- if (!ctx) return;
73
- canvas.width = drawing.width;
74
- canvas.height = drawing.height;
75
- ctx.clearRect(0, 0, drawing.width, drawing.height);
76
- const editPixels = getEditPixels(drawing);
77
- for (let y = 0; y < drawing.height; y++) {
78
- for (let x = 0; x < drawing.width; x++) {
79
- const pixel = editPixels[y * drawing.width + x];
80
- if (!pixel || pixel.endsWith('00')) continue;
81
- ctx.fillStyle = pixel;
82
- ctx.fillRect(x, y, 1, 1);
83
- }
84
- }
85
- }, [drawing, text]);
86
-
87
- function commitDrawing(nextDrawing: DrawingData): void {
88
- history.commit(formatJson(nextDrawing));
89
- }
90
-
91
- function resizeDrawing(nextWidth: number, nextHeight: number): void {
92
- if (!drawing) return;
93
- const width = clampInteger(nextWidth, 1, 128);
94
- const height = clampInteger(nextHeight, 1, 128);
95
- if (width === drawing.width && height === drawing.height) return;
96
- const oldPixels = getEditPixels(drawing);
97
- const pixels = Array<string>(width * height).fill('#00000000');
98
- const copiedWidth = Math.min(width, drawing.width);
99
- const copiedHeight = Math.min(height, drawing.height);
100
- for (let y = 0; y < copiedHeight; y++) {
101
- for (let x = 0; x < copiedWidth; x++) {
102
- pixels[y * width + x] = oldPixels[y * drawing.width + x] ?? '#00000000';
103
- }
104
- }
105
- commitDrawing(withEditPixels({ ...drawing, width, height }, pixels));
106
- }
107
-
108
- function paintAt(event: PointerEvent<HTMLCanvasElement>): void {
109
- if (!drawing) return;
110
- const rect = event.currentTarget.getBoundingClientRect();
111
- const x = Math.floor(((event.clientX - rect.left) / rect.width) * drawing.width);
112
- const y = Math.floor(((event.clientY - rect.top) / rect.height) * drawing.height);
113
- if (x < 0 || y < 0 || x >= drawing.width || y >= drawing.height) return;
114
- const editPixels = getEditPixels(drawing);
115
- if (editPixels[y * drawing.width + x] === color) return;
116
- const nextPixels = editPixels.slice();
117
- nextPixels[y * drawing.width + x] = color;
118
- const next = withEditPixels(drawing, nextPixels);
119
- // Record one undo snapshot per pointer stroke (not per painted pixel).
120
- if (!strokeRef.current.recorded) {
121
- history.recordSnapshot();
122
- strokeRef.current.recorded = true;
123
- }
124
- onChange(formatJson(next));
125
- }
126
-
127
- function startPaint(event: PointerEvent<HTMLCanvasElement>): void {
128
- strokeRef.current = { active: true, recorded: false };
129
- event.currentTarget.setPointerCapture(event.pointerId);
130
- paintAt(event);
131
- }
132
-
133
- function finishPaint(): void {
134
- strokeRef.current = { active: false, recorded: false };
135
- }
136
-
137
- const canvasStyle: CSSProperties | undefined = drawing
138
- ? {
139
- width: isWide ? 'min(70vh, 520px)' : 'auto',
140
- height: isWide ? 'auto' : 'min(70vh, 520px)',
141
- }
142
- : undefined;
143
-
144
- const sharedActionButtons = (
145
- <>
146
- <IconButton icon="undo" label="Undo" onClick={history.undo} disabled={!history.canUndo} />
147
- <IconButton icon="redo" label="Redo" onClick={history.redo} disabled={!history.canRedo} />
148
- </>
149
- );
150
-
151
- return (
152
- <>
153
- <EditorHeader
154
- title={basename(path)}
155
- subtitle={drawing ? `${drawing.width} x ${drawing.height} pixels` : error}
156
- right={
157
- <span className={styles.mobileOnly}>
158
- {sharedActionButtons}
159
- <IconButton
160
- icon="palette"
161
- label="Tools"
162
- active={inspectorOpen}
163
- onClick={() => setInspectorOpen((previous) => !previous)}
164
- />
165
- </span>
166
- }
167
- onToggleFiles={onToggleFiles}
168
- filesOpen={filesOpen}
169
- />
170
- <EditorBody>
171
- <div className={styles.drawingEditor}>
172
- <div className={styles.drawingTools}>{sharedActionButtons}</div>
173
- <div className={styles.drawingCanvasWrap}>
174
- <canvas
175
- ref={canvasRef}
176
- className={styles.drawingCanvas}
177
- style={canvasStyle}
178
- onPointerDown={startPaint}
179
- onPointerMove={(event) => {
180
- if (strokeRef.current.active && event.buttons === 1) paintAt(event);
181
- }}
182
- onPointerUp={finishPaint}
183
- onPointerCancel={finishPaint}
184
- />
185
- </div>
186
- <DrawingInspector
187
- sheet={inspectorSheet}
188
- drawing={drawing}
189
- color={color}
190
- onSelectColor={setColor}
191
- onResize={resizeDrawing}
192
- />
193
- </div>
194
- </EditorBody>
195
- </>
196
- );
197
- }
198
-
199
- function useInspectorSheet(inspectorOpen: boolean): ReturnType<typeof useMobileSheet> {
200
- const [snap, setSnap] = useState<'high' | 'low'>('high');
201
- useEffect(() => {
202
- if (inspectorOpen) setSnap('high');
203
- }, [inspectorOpen]);
204
- const effectiveSnap: SheetSnap = inspectorOpen ? snap : 'hidden';
205
- return useMobileSheet({
206
- snap: effectiveSnap,
207
- baseClassName: styles.inspector,
208
- onTransition: (direction) => {
209
- if (!inspectorOpen) return;
210
- if (direction === 'tap') setSnap((previous) => (previous === 'high' ? 'low' : 'high'));
211
- else if (direction === 'down') setSnap('low');
212
- else if (direction === 'up') setSnap('high');
213
- },
214
- });
215
- }
216
-
217
- function DrawingInspector({
218
- sheet,
219
- drawing,
220
- color,
221
- onSelectColor,
222
- onResize,
223
- }: {
224
- sheet: ReturnType<typeof useMobileSheet>;
225
- drawing: DrawingData | null;
226
- color: string;
227
- onSelectColor: (color: string) => void;
228
- onResize: (width: number, height: number) => void;
229
- }) {
230
- return (
231
- <aside {...sheet.rootProps} className={cx(sheet.rootProps.className, styles.inspectorAuto)}>
232
- <div {...sheet.grabProps}>
233
- <SheetGrabHandle
234
- label="Inspector"
235
- hint={drawing ? `${drawing.width}x${drawing.height} · color` : 'drawing tools'}
236
- />
237
- </div>
238
- <div className={cx(styles.sheetBody, styles.inspectorBody)}>
239
- <Panel title="Size">
240
- <NumberField
241
- label="Width"
242
- value={drawing?.width ?? 1}
243
- min={1}
244
- max={128}
245
- onChange={(width) => onResize(width, drawing?.height ?? 1)}
246
- />
247
- <NumberField
248
- label="Height"
249
- value={drawing?.height ?? 1}
250
- min={1}
251
- max={128}
252
- onChange={(height) => onResize(drawing?.width ?? 1, height)}
253
- />
254
- </Panel>
255
- <Panel title="Palette">
256
- <div className={styles.palette}>
257
- {palette.map((swatch) => (
258
- <button
259
- key={swatch}
260
- className={cx(styles.swatch, color === swatch && styles.swatchSelected)}
261
- title={swatch}
262
- style={{
263
- background: swatch.endsWith('00') ? theme.transparentChecker : swatch,
264
- }}
265
- onClick={() => onSelectColor(swatch)}
266
- />
267
- ))}
268
- </div>
269
- </Panel>
270
- </div>
271
- </aside>
272
- );
273
- }
274
-
275
- function clampInteger(value: number, min: number, max: number): number {
276
- if (!Number.isFinite(value)) return min;
277
- return Math.max(min, Math.min(max, Math.round(value)));
278
- }