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.
- package/dist/agent-prompts.js +22 -3
- package/dist/agent.js +731 -313
- package/dist/init.js +1 -1
- package/dist/shell/assets/index-Dfn29Bkt.js +108 -0
- package/dist/shell/assets/{index-CVEnWuGV.css → index-WNbOHPBj.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/dist/vitePlugins.js +3 -2
- package/kits/basic-2d/CLAUDE.md +29 -8
- package/kits/basic-2d/behaviors/Collider.jsx +6 -4
- package/kits/basic-2d/behaviors/Layout.jsx +2 -2
- package/kits/basic-2d/behaviors/Sprite.jsx +210 -0
- package/kits/basic-2d/behaviors/tint.js +47 -0
- package/kits/basic-2d/docs/pxart-format.md +298 -0
- package/kits/basic-2d/drawings/pig.pxart +59 -0
- package/kits/basic-2d/editors/App.jsx +125 -76
- package/kits/basic-2d/editors/CodeEditor.jsx +9 -45
- package/kits/basic-2d/editors/FileBrowser.jsx +234 -47
- package/kits/basic-2d/editors/PlayOnly.jsx +9 -7
- package/kits/basic-2d/editors/PxArtEditor.jsx +662 -0
- package/kits/basic-2d/editors/SceneEditor.jsx +587 -221
- package/kits/basic-2d/editors/SelectionOverlay.jsx +808 -0
- package/kits/basic-2d/editors/SingleEditor.jsx +38 -20
- package/kits/basic-2d/editors/codeTheme.js +135 -0
- package/kits/basic-2d/editors/editorHistory.js +44 -17
- package/kits/basic-2d/editors/inspectorSheet.js +23 -0
- package/kits/basic-2d/editors/pixelCanvas.js +11 -0
- package/kits/basic-2d/editors/pixelEditorChrome.jsx +55 -0
- package/kits/basic-2d/editors/pixelGeometry.js +45 -0
- package/kits/basic-2d/editors/pixelInspector.jsx +416 -0
- package/kits/basic-2d/editors/pxArtEditorModel.js +718 -0
- package/kits/basic-2d/editors/pxArtPlayback.js +92 -0
- package/kits/basic-2d/editors/pxArtTimeline.jsx +752 -0
- package/kits/basic-2d/editors/pxArtTimeline.module.css +506 -0
- package/kits/basic-2d/editors/pxArtTools.js +124 -0
- package/kits/basic-2d/editors/useArtboardFit.js +102 -0
- package/kits/basic-2d/engine/ScenePlayer.jsx +10 -4
- package/kits/basic-2d/engine/SceneUI.jsx +3 -11
- package/kits/basic-2d/engine/assets.js +15 -0
- package/kits/basic-2d/engine/files.js +57 -2
- package/kits/basic-2d/engine/pxart.js +985 -0
- package/kits/basic-2d/engine/scene.js +222 -41
- package/kits/basic-2d/engine/ui.jsx +155 -26
- package/kits/basic-2d/engine/ui.module.css +1280 -344
- package/kits/basic-2d/eslint.config.js +21 -0
- package/kits/basic-2d/index.html +13 -0
- package/kits/basic-2d/package.json +1 -0
- package/kits/basic-2d/pnpm-lock.yaml +5 -5
- package/kits/basic-2d/scenes/main.scene +19 -26
- package/kits/basic-2d/scripts/draw.mjs +121 -0
- package/kits/basic-3d/editors/PlayOnly.jsx +9 -1
- package/kits/basic-3d/engine/ScenePlayer.jsx +7 -1
- package/package.json +1 -1
- package/dist/shell/assets/index-BY21Og40.js +0 -106
- package/kits/basic-2d/behaviors/Drawing.jsx +0 -142
- package/kits/basic-2d/drawings/block.drawing +0 -70
- package/kits/basic-2d/drawings/default.drawing +0 -70
- 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
|
+
}
|