castle-web-cli 0.4.71 → 0.4.73

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 (57) hide show
  1. package/dist/agent-prompts.js +22 -3
  2. package/dist/agent.js +731 -313
  3. package/dist/init.js +1 -1
  4. package/dist/shell/assets/index-Dfn29Bkt.js +108 -0
  5. package/dist/shell/assets/{index-CVEnWuGV.css → index-WNbOHPBj.css} +1 -1
  6. package/dist/shell/index.html +2 -2
  7. package/dist/vitePlugins.js +3 -2
  8. package/kits/basic-2d/CLAUDE.md +29 -8
  9. package/kits/basic-2d/behaviors/Collider.jsx +6 -4
  10. package/kits/basic-2d/behaviors/Layout.jsx +2 -2
  11. package/kits/basic-2d/behaviors/Sprite.jsx +210 -0
  12. package/kits/basic-2d/behaviors/tint.js +47 -0
  13. package/kits/basic-2d/docs/pxart-format.md +298 -0
  14. package/kits/basic-2d/drawings/pig.pxart +59 -0
  15. package/kits/basic-2d/editors/App.jsx +125 -76
  16. package/kits/basic-2d/editors/CodeEditor.jsx +9 -45
  17. package/kits/basic-2d/editors/FileBrowser.jsx +234 -47
  18. package/kits/basic-2d/editors/PlayOnly.jsx +9 -7
  19. package/kits/basic-2d/editors/PxArtEditor.jsx +662 -0
  20. package/kits/basic-2d/editors/SceneEditor.jsx +587 -221
  21. package/kits/basic-2d/editors/SelectionOverlay.jsx +808 -0
  22. package/kits/basic-2d/editors/SingleEditor.jsx +38 -20
  23. package/kits/basic-2d/editors/codeTheme.js +135 -0
  24. package/kits/basic-2d/editors/editorHistory.js +44 -17
  25. package/kits/basic-2d/editors/inspectorSheet.js +23 -0
  26. package/kits/basic-2d/editors/pixelCanvas.js +11 -0
  27. package/kits/basic-2d/editors/pixelEditorChrome.jsx +55 -0
  28. package/kits/basic-2d/editors/pixelGeometry.js +45 -0
  29. package/kits/basic-2d/editors/pixelInspector.jsx +416 -0
  30. package/kits/basic-2d/editors/pxArtEditorModel.js +718 -0
  31. package/kits/basic-2d/editors/pxArtPlayback.js +92 -0
  32. package/kits/basic-2d/editors/pxArtTimeline.jsx +752 -0
  33. package/kits/basic-2d/editors/pxArtTimeline.module.css +506 -0
  34. package/kits/basic-2d/editors/pxArtTools.js +124 -0
  35. package/kits/basic-2d/editors/useArtboardFit.js +102 -0
  36. package/kits/basic-2d/engine/ScenePlayer.jsx +10 -4
  37. package/kits/basic-2d/engine/SceneUI.jsx +3 -11
  38. package/kits/basic-2d/engine/assets.js +15 -0
  39. package/kits/basic-2d/engine/files.js +57 -2
  40. package/kits/basic-2d/engine/pxart.js +985 -0
  41. package/kits/basic-2d/engine/scene.js +222 -41
  42. package/kits/basic-2d/engine/ui.jsx +155 -26
  43. package/kits/basic-2d/engine/ui.module.css +1280 -344
  44. package/kits/basic-2d/eslint.config.js +21 -0
  45. package/kits/basic-2d/index.html +13 -0
  46. package/kits/basic-2d/package.json +1 -0
  47. package/kits/basic-2d/pnpm-lock.yaml +5 -5
  48. package/kits/basic-2d/scenes/main.scene +19 -26
  49. package/kits/basic-2d/scripts/draw.mjs +121 -0
  50. package/kits/basic-3d/editors/PlayOnly.jsx +9 -1
  51. package/kits/basic-3d/engine/ScenePlayer.jsx +7 -1
  52. package/package.json +1 -1
  53. package/dist/shell/assets/index-BY21Og40.js +0 -106
  54. package/kits/basic-2d/behaviors/Drawing.jsx +0 -142
  55. package/kits/basic-2d/drawings/block.drawing +0 -70
  56. package/kits/basic-2d/drawings/default.drawing +0 -70
  57. package/kits/basic-2d/editors/DrawingEditor.jsx +0 -224
@@ -0,0 +1,416 @@
1
+ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { RESOLUTION_STEPS } from '../engine/pxart';
4
+ import { cx, Icon, IconButton, Panel, styles } from '../engine/ui';
5
+
6
+ // Brush/erase diameters offered by the pixel editors' size sliders.
7
+ export const BRUSH_SIZES = [1, 2, 3, 4, 6, 8, 12, 16, 24, 32];
8
+
9
+ // The tool strip for the pixel (PxArt) editor. Eyedropper is not in the strip —
10
+ // it lives in the palette panel / popover action button.
11
+ export const PIXEL_TOOLS = [
12
+ { id: 'brush', label: 'Brush', icon: 'pencil' },
13
+ { id: 'fill', label: 'Fill', icon: 'fill' },
14
+ { id: 'move-all', label: 'Move all', icon: 'move' },
15
+ { id: 'erase', label: 'Erase', icon: 'eraser' },
16
+ ];
17
+
18
+ // Tools whose work is driven by the active palette color, so the palette panel
19
+ // (and its eyedropper action) shows.
20
+ export const PALETTE_TOOLS = new Set(['brush', 'fill', 'eyedropper']);
21
+
22
+ // Shared tool-mode state for the pixel editors: the selected tool plus the
23
+ // brush/erase sizes and the fill/erase mode toggles. The active color/key stays
24
+ // editor-local (free hex vs fixed palette key).
25
+ //
26
+ // `picking` is a TRANSIENT eyedropper overlay, kept separate from `tool` so the
27
+ // real tool (and its settings panel) never changes when the picker is engaged —
28
+ // it just sits on top of whatever tool is active until you pick or toggle it off.
29
+ export function usePixelToolState() {
30
+ const [tool, setTool] = useState('brush');
31
+ const [picking, setPicking] = useState(false);
32
+ const [brushSize, setBrushSize] = useState(1);
33
+ const [eraseSize, setEraseSize] = useState(4);
34
+ const [eraseFillMode, setEraseFillMode] = useState(false);
35
+ const [fillSwapMode, setFillSwapMode] = useState(false);
36
+ return {
37
+ tool,
38
+ setTool,
39
+ picking,
40
+ setPicking,
41
+ brushSize,
42
+ setBrushSize,
43
+ eraseSize,
44
+ setEraseSize,
45
+ eraseFillMode,
46
+ setEraseFillMode,
47
+ fillSwapMode,
48
+ setFillSwapMode,
49
+ };
50
+ }
51
+
52
+ // Vertical icon tool-strip shared by the pixel editors. `tools` is a list of
53
+ // { id, label, icon }; `disabled` greys out the whole strip (read-only files).
54
+ export function PixelToolStrip({ tools, tool, onSelectTool, ariaLabel, disabled = false }) {
55
+ return (
56
+ <div className={styles.drawingToolStrip} aria-label={ariaLabel}>
57
+ {tools.map((candidate) => (
58
+ <button
59
+ key={candidate.id}
60
+ type="button"
61
+ title={candidate.label}
62
+ aria-label={candidate.label}
63
+ aria-pressed={tool === candidate.id ? 'true' : 'false'}
64
+ disabled={disabled}
65
+ className={cx(
66
+ styles.drawingToolButton,
67
+ tool === candidate.id && styles.drawingToolSelected
68
+ )}
69
+ onClick={() => onSelectTool(candidate.id)}>
70
+ <Icon name={candidate.icon} />
71
+ </button>
72
+ ))}
73
+ </div>
74
+ );
75
+ }
76
+
77
+ // Compact W × H selects above the artboard (not a sidebar panel).
78
+ export function CanvasSizeBar({ width, height, onResize }) {
79
+ return (
80
+ <div className={styles.canvasSizeBar} aria-label="Canvas size">
81
+ <ResolutionSelect label="Width" value={width} onChange={(w) => onResize(w, height)} />
82
+ <span className={styles.canvasSizeSep} aria-hidden="true">
83
+ ×
84
+ </span>
85
+ <ResolutionSelect label="Height" value={height} onChange={(h) => onResize(width, h)} />
86
+ </div>
87
+ );
88
+ }
89
+
90
+ function ResolutionSelect({ label, value, onChange }) {
91
+ const options = RESOLUTION_STEPS.includes(value)
92
+ ? RESOLUTION_STEPS
93
+ : [...RESOLUTION_STEPS, value].sort((a, b) => a - b);
94
+ return (
95
+ <select
96
+ className={styles.canvasSizeSelect}
97
+ aria-label={label}
98
+ value={value}
99
+ onChange={(event) => onChange(Number(event.target.value))}>
100
+ {options.map((step) => (
101
+ <option key={step} value={step}>
102
+ {step}
103
+ </option>
104
+ ))}
105
+ </select>
106
+ );
107
+ }
108
+
109
+ // 12-column palette grid shared by the docked sidebar and the compact popover.
110
+ export function PaletteGrid({ keys, palette, activeKey, onSelectKey }) {
111
+ return (
112
+ <div className={styles.palette}>
113
+ {keys.map((key) => (
114
+ <button
115
+ key={key}
116
+ type="button"
117
+ className={cx(styles.swatch, activeKey === key && styles.swatchSelected)}
118
+ title={palette[key]}
119
+ style={{ background: palette[key] }}
120
+ onClick={() => onSelectKey(key)}
121
+ />
122
+ ))}
123
+ </div>
124
+ );
125
+ }
126
+
127
+ // Anchored palette + eyedropper popover for compact/mobile paint-strip color tap.
128
+ export function PalettePopover({ open, anchorRef, onClose, children }) {
129
+ const popoverRef = useRef(null);
130
+ const [position, setPosition] = useState({ top: 0, left: 0 });
131
+
132
+ useLayoutEffect(() => {
133
+ if (!open || !anchorRef.current) return;
134
+ const anchor = anchorRef.current.getBoundingClientRect();
135
+ const popoverWidth = 280;
136
+ const margin = 8;
137
+ let left = anchor.left - popoverWidth - margin;
138
+ if (left < margin) left = anchor.right + margin;
139
+ let top = anchor.top;
140
+ const maxTop = window.innerHeight - margin;
141
+ if (top + 320 > maxTop) top = Math.max(margin, maxTop - 320);
142
+ setPosition({ top, left });
143
+ }, [open, anchorRef]);
144
+
145
+ useEffect(() => {
146
+ if (!open) return undefined;
147
+ function onKeyDown(event) {
148
+ if (event.key === 'Escape') onClose();
149
+ }
150
+ function onPointerDown(event) {
151
+ if (popoverRef.current?.contains(event.target) || anchorRef.current?.contains(event.target)) {
152
+ return;
153
+ }
154
+ onClose();
155
+ }
156
+ window.addEventListener('keydown', onKeyDown);
157
+ window.addEventListener('pointerdown', onPointerDown);
158
+ return () => {
159
+ window.removeEventListener('keydown', onKeyDown);
160
+ window.removeEventListener('pointerdown', onPointerDown);
161
+ };
162
+ }, [open, onClose, anchorRef]);
163
+
164
+ if (!open) return null;
165
+ return createPortal(
166
+ <div
167
+ ref={popoverRef}
168
+ className={styles.palettePopover}
169
+ style={{ top: position.top, left: position.left }}
170
+ role="dialog"
171
+ aria-label="Palette">
172
+ {children}
173
+ </div>,
174
+ document.body
175
+ );
176
+ }
177
+
178
+ // Right-side paint strip (compact layout): color swatch, vertical size slider, and
179
+ // tool-contextual mode toggles. Color tap opens the palette popover.
180
+ export function PaintStrip({
181
+ toolState,
182
+ activeKey,
183
+ activeColor,
184
+ paletteOpen,
185
+ onTogglePalette,
186
+ colorButtonRef,
187
+ onSelectKey,
188
+ paletteKeys,
189
+ palette,
190
+ }) {
191
+ const {
192
+ tool,
193
+ picking,
194
+ setPicking,
195
+ brushSize,
196
+ eraseSize,
197
+ eraseFillMode,
198
+ fillSwapMode,
199
+ setBrushSize,
200
+ setEraseSize,
201
+ setEraseFillMode,
202
+ setFillSwapMode,
203
+ } = toolState;
204
+
205
+ const showBrushSize = tool === 'brush';
206
+ const showEraseSize = tool === 'erase';
207
+ const size = showBrushSize ? brushSize : eraseSize;
208
+ const onSizeChange = showBrushSize ? setBrushSize : setEraseSize;
209
+ const sliderIndex = Math.max(0, BRUSH_SIZES.indexOf(size));
210
+ const sliderDisabled = showEraseSize && eraseFillMode;
211
+
212
+ return (
213
+ <>
214
+ <div className={styles.paintStrip} aria-label="Paint controls">
215
+ <button
216
+ ref={colorButtonRef}
217
+ type="button"
218
+ className={styles.paintStripColor}
219
+ title={activeColor}
220
+ aria-label={`Active color ${activeColor}. Open palette.`}
221
+ aria-expanded={paletteOpen ? 'true' : 'false'}
222
+ style={{ background: activeColor }}
223
+ onClick={onTogglePalette}
224
+ />
225
+ {showBrushSize || showEraseSize ? (
226
+ <div className={styles.paintStripSlider}>
227
+ <input
228
+ className={styles.drawingSizeSliderVertical}
229
+ type="range"
230
+ min="0"
231
+ max={BRUSH_SIZES.length - 1}
232
+ step="1"
233
+ value={sliderIndex}
234
+ disabled={sliderDisabled}
235
+ aria-label={showBrushSize ? 'Brush size' : 'Erase size'}
236
+ title={`${showBrushSize ? 'Brush' : 'Erase'} size ${size}`}
237
+ onChange={(event) => onSizeChange(BRUSH_SIZES[Number(event.target.value)])}
238
+ />
239
+ </div>
240
+ ) : null}
241
+ {tool === 'erase' ? (
242
+ <button
243
+ type="button"
244
+ className={cx(
245
+ styles.paintStripModeButton,
246
+ eraseFillMode && styles.drawingToolSelected
247
+ )}
248
+ title="Fill mode"
249
+ aria-label="Fill mode"
250
+ aria-pressed={eraseFillMode ? 'true' : 'false'}
251
+ onClick={() => setEraseFillMode(!eraseFillMode)}>
252
+ <Icon name="fill" />
253
+ </button>
254
+ ) : null}
255
+ {tool === 'fill' ? (
256
+ <button
257
+ type="button"
258
+ className={cx(
259
+ styles.paintStripModeButton,
260
+ fillSwapMode && styles.drawingToolSelected
261
+ )}
262
+ title="Swap mode"
263
+ aria-label="Swap mode"
264
+ aria-pressed={fillSwapMode ? 'true' : 'false'}
265
+ onClick={() => setFillSwapMode(!fillSwapMode)}>
266
+ <Icon name="rotate" />
267
+ </button>
268
+ ) : null}
269
+ </div>
270
+ <PalettePopover open={paletteOpen} anchorRef={colorButtonRef} onClose={() => onTogglePalette(false)}>
271
+ <div className={styles.palettePopoverHeader}>
272
+ <span className={styles.palettePopoverTitle}>Palette</span>
273
+ <IconButton
274
+ icon="eyedropper"
275
+ label="Pick color"
276
+ active={picking}
277
+ aria-pressed={picking ? 'true' : 'false'}
278
+ onClick={() => setPicking(!picking)}
279
+ />
280
+ </div>
281
+ <PaletteGrid
282
+ keys={paletteKeys}
283
+ palette={palette}
284
+ activeKey={activeKey}
285
+ onSelectKey={(key) => {
286
+ onSelectKey(key);
287
+ onTogglePalette(false);
288
+ }}
289
+ />
290
+ </PalettePopover>
291
+ </>
292
+ );
293
+ }
294
+
295
+ // Wide-layout docked inspector: palette (when relevant) + tool settings only.
296
+ export function PxArtInspectorDock({ toolState, activeKey, onSelectKey, paletteKeys, palette }) {
297
+ const { tool, picking, setPicking } = toolState;
298
+ return (
299
+ <aside className={cx(styles.pxArtInspector, styles.drawingToolsPanel)}>
300
+ <div className={cx(styles.sheetBody, styles.inspectorBody, styles.drawingSettingsColumn)}>
301
+ {PALETTE_TOOLS.has(tool) || picking ? (
302
+ <Panel
303
+ title="Palette"
304
+ action={
305
+ <IconButton
306
+ icon="eyedropper"
307
+ label="Pick color"
308
+ active={picking}
309
+ aria-pressed={picking ? 'true' : 'false'}
310
+ onClick={() => setPicking(!picking)}
311
+ />
312
+ }>
313
+ <PaletteGrid
314
+ keys={paletteKeys}
315
+ palette={palette}
316
+ activeKey={activeKey}
317
+ onSelectKey={onSelectKey}
318
+ />
319
+ </Panel>
320
+ ) : null}
321
+ <ToolSettingsPanel toolState={toolState} />
322
+ </div>
323
+ </aside>
324
+ );
325
+ }
326
+
327
+ // A labeled on/off switch used for the editors' boolean tool modes.
328
+ export function ModeToggle({ label, active, onChange }) {
329
+ return (
330
+ <div className={styles.drawingModeRow}>
331
+ <span>{label}</span>
332
+ <button
333
+ type="button"
334
+ role="switch"
335
+ className={cx(styles.drawingSwitch, active && styles.drawingSwitchOn)}
336
+ aria-checked={active ? 'true' : 'false'}
337
+ onClick={() => onChange(!active)}>
338
+ <span className={styles.drawingSwitchKnob} />
339
+ </button>
340
+ </div>
341
+ );
342
+ }
343
+
344
+ // The "Tool Settings" panel for the pixel editor: brush/erase size sliders plus
345
+ // the per-tool mode toggle / hint. It reads the whole usePixelToolState bundle
346
+ // to keep callers from re-spreading the same prop list.
347
+ export function ToolSettingsPanel({ toolState }) {
348
+ const {
349
+ tool,
350
+ brushSize,
351
+ eraseSize,
352
+ eraseFillMode,
353
+ fillSwapMode,
354
+ setBrushSize,
355
+ setEraseSize,
356
+ setEraseFillMode,
357
+ setFillSwapMode,
358
+ } = toolState;
359
+ return (
360
+ <Panel title="Tool Settings">
361
+ <div className={styles.drawingToolSettings}>
362
+ <ToolSizePicker
363
+ label="Brush"
364
+ value={brushSize}
365
+ visible={tool === 'brush'}
366
+ onChange={setBrushSize}
367
+ />
368
+ <ToolSizePicker
369
+ label="Erase"
370
+ value={eraseSize}
371
+ visible={tool === 'erase'}
372
+ onChange={setEraseSize}
373
+ fill={eraseFillMode}
374
+ onFillChange={setEraseFillMode}
375
+ />
376
+ {tool === 'fill' ? (
377
+ <ModeToggle label="Swap Mode" active={fillSwapMode} onChange={setFillSwapMode} />
378
+ ) : null}
379
+ {tool === 'move-all' ? (
380
+ <div className={styles.drawingToolHint}>Drag the canvas to shift all pixels.</div>
381
+ ) : null}
382
+ {tool === 'eyedropper' ? (
383
+ <div className={styles.drawingToolHint}>Click a pixel to make it the active color.</div>
384
+ ) : null}
385
+ </div>
386
+ </Panel>
387
+ );
388
+ }
389
+
390
+ // Discrete brush/erase size slider (steps through BRUSH_SIZES). Hidden unless
391
+ // `visible`. An optional fill toggle disables the slider when fill mode is on.
392
+ export function ToolSizePicker({ label, value, visible, onChange, fill = false, onFillChange }) {
393
+ if (!visible) return null;
394
+ const sliderIndex = Math.max(0, BRUSH_SIZES.indexOf(value));
395
+ return (
396
+ <div className={styles.drawingSizePicker}>
397
+ <div className={styles.drawingSizeLabel}>
398
+ <span>{label} size</span>
399
+ <span>{value}</span>
400
+ </div>
401
+ <div className={styles.drawingSizeControlRow}>
402
+ <input
403
+ className={styles.drawingSizeSlider}
404
+ type="range"
405
+ min="0"
406
+ max={BRUSH_SIZES.length - 1}
407
+ step="1"
408
+ value={sliderIndex}
409
+ disabled={fill}
410
+ onChange={(event) => onChange(BRUSH_SIZES[Number(event.target.value)])}
411
+ />
412
+ </div>
413
+ {onFillChange ? <ModeToggle label="Fill Mode" active={fill} onChange={onFillChange} /> : null}
414
+ </div>
415
+ );
416
+ }