castle-web-cli 0.4.3 → 0.4.5

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 (97) hide show
  1. package/dist/index.js +8 -2
  2. package/dist/init.js +1 -1
  3. package/kits/basic-2d/CLAUDE.md +3 -3
  4. package/package.json +1 -1
  5. package/kits/basic-2d-frozen/.prettierrc +0 -8
  6. package/kits/basic-2d-frozen/CLAUDE.md +0 -131
  7. package/kits/basic-2d-frozen/behaviors/Camera.jsx +0 -43
  8. package/kits/basic-2d-frozen/behaviors/Collider.jsx +0 -71
  9. package/kits/basic-2d-frozen/behaviors/Drawing.jsx +0 -139
  10. package/kits/basic-2d-frozen/behaviors/Layout.jsx +0 -16
  11. package/kits/basic-2d-frozen/drawings/floor.drawing +0 -70
  12. package/kits/basic-2d-frozen/editors/App.jsx +0 -152
  13. package/kits/basic-2d-frozen/editors/CodeEditor.jsx +0 -112
  14. package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +0 -222
  15. package/kits/basic-2d-frozen/editors/FileBrowser.jsx +0 -143
  16. package/kits/basic-2d-frozen/editors/PlayOnly.jsx +0 -21
  17. package/kits/basic-2d-frozen/editors/SceneEditor.jsx +0 -1012
  18. package/kits/basic-2d-frozen/editors/behaviorRegistry.js +0 -24
  19. package/kits/basic-2d-frozen/editors/editorHistory.js +0 -52
  20. package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +0 -83
  21. package/kits/basic-2d-frozen/engine/SceneUI.jsx +0 -67
  22. package/kits/basic-2d-frozen/engine/TouchControls.jsx +0 -136
  23. package/kits/basic-2d-frozen/engine/autoInspector.jsx +0 -51
  24. package/kits/basic-2d-frozen/engine/files.js +0 -62
  25. package/kits/basic-2d-frozen/engine/scene.js +0 -420
  26. package/kits/basic-2d-frozen/engine/ui.jsx +0 -344
  27. package/kits/basic-2d-frozen/engine/ui.module.css +0 -928
  28. package/kits/basic-2d-frozen/eslint.config.js +0 -50
  29. package/kits/basic-2d-frozen/index.html +0 -11
  30. package/kits/basic-2d-frozen/main.jsx +0 -10
  31. package/kits/basic-2d-frozen/package-lock.json +0 -2706
  32. package/kits/basic-2d-frozen/package.json +0 -41
  33. package/kits/basic-2d-frozen/scenes/main.scene +0 -108
  34. package/kits/basic-2d-frozen/vite.config.js +0 -1
  35. package/kits/rpg-2d/.prettierrc +0 -8
  36. package/kits/rpg-2d/behaviors/Camera.tsx +0 -52
  37. package/kits/rpg-2d/behaviors/Collider.tsx +0 -98
  38. package/kits/rpg-2d/behaviors/Dialog.tsx +0 -184
  39. package/kits/rpg-2d/behaviors/Drawing.tsx +0 -161
  40. package/kits/rpg-2d/behaviors/Friend.tsx +0 -45
  41. package/kits/rpg-2d/behaviors/Layout.tsx +0 -29
  42. package/kits/rpg-2d/behaviors/PlayerController.tsx +0 -255
  43. package/kits/rpg-2d/behaviors/Portal.tsx +0 -60
  44. package/kits/rpg-2d/behaviors/QuestLog.tsx +0 -90
  45. package/kits/rpg-2d/behaviors/SaveMenu.tsx +0 -123
  46. package/kits/rpg-2d/behaviors/Tilemap.tsx +0 -90
  47. package/kits/rpg-2d/drawings/bld-home.drawing +0 -8136
  48. package/kits/rpg-2d/drawings/env-crate.drawing +0 -509
  49. package/kits/rpg-2d/drawings/env-fence.drawing +0 -536
  50. package/kits/rpg-2d/drawings/env-flower-bed.drawing +0 -607
  51. package/kits/rpg-2d/drawings/env-fountain.drawing +0 -2622
  52. package/kits/rpg-2d/drawings/env-hedge.drawing +0 -601
  53. package/kits/rpg-2d/drawings/env-house-blue.drawing +0 -1
  54. package/kits/rpg-2d/drawings/env-house-green.drawing +0 -1
  55. package/kits/rpg-2d/drawings/env-tree-oak.drawing +0 -1540
  56. package/kits/rpg-2d/drawings/env-tree-pine.drawing +0 -1315
  57. package/kits/rpg-2d/drawings/floor.drawing +0 -70
  58. package/kits/rpg-2d/drawings/fx-sparkle.drawing +0 -926
  59. package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +0 -1099
  60. package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +0 -4177
  61. package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +0 -1099
  62. package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +0 -4177
  63. package/kits/rpg-2d/drawings/player-idle-down.drawing +0 -1070
  64. package/kits/rpg-2d/drawings/player-idle-left.drawing +0 -1070
  65. package/kits/rpg-2d/drawings/player-idle-right.drawing +0 -1070
  66. package/kits/rpg-2d/drawings/player-idle-up.drawing +0 -1070
  67. package/kits/rpg-2d/drawings/player-walk-down.drawing +0 -4148
  68. package/kits/rpg-2d/drawings/player-walk-left.drawing +0 -4148
  69. package/kits/rpg-2d/drawings/player-walk-right.drawing +0 -4148
  70. package/kits/rpg-2d/drawings/player-walk-up.drawing +0 -4148
  71. package/kits/rpg-2d/editors/App.tsx +0 -163
  72. package/kits/rpg-2d/editors/CodeEditor.tsx +0 -120
  73. package/kits/rpg-2d/editors/DrawingEditor.tsx +0 -278
  74. package/kits/rpg-2d/editors/FileBrowser.tsx +0 -191
  75. package/kits/rpg-2d/editors/PlayOnly.tsx +0 -26
  76. package/kits/rpg-2d/editors/SceneEditor.tsx +0 -1093
  77. package/kits/rpg-2d/editors/behaviorRegistry.ts +0 -33
  78. package/kits/rpg-2d/editors/editorHistory.ts +0 -75
  79. package/kits/rpg-2d/editors/editorProps.ts +0 -10
  80. package/kits/rpg-2d/engine/ScenePlayer.tsx +0 -130
  81. package/kits/rpg-2d/engine/SceneUI.tsx +0 -74
  82. package/kits/rpg-2d/engine/TouchControls.tsx +0 -157
  83. package/kits/rpg-2d/engine/autoInspector.tsx +0 -111
  84. package/kits/rpg-2d/engine/drawing.ts +0 -81
  85. package/kits/rpg-2d/engine/files.ts +0 -215
  86. package/kits/rpg-2d/engine/scene.ts +0 -484
  87. package/kits/rpg-2d/engine/ui.module.css +0 -928
  88. package/kits/rpg-2d/engine/ui.tsx +0 -483
  89. package/kits/rpg-2d/eslint.config.js +0 -46
  90. package/kits/rpg-2d/index.html +0 -11
  91. package/kits/rpg-2d/main.tsx +0 -14
  92. package/kits/rpg-2d/package-lock.json +0 -3149
  93. package/kits/rpg-2d/package.json +0 -46
  94. package/kits/rpg-2d/scenes/main.scene +0 -203
  95. package/kits/rpg-2d/tsconfig.json +0 -17
  96. package/kits/rpg-2d/vite-env.d.ts +0 -7
  97. 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
- }