castle-web-cli 0.4.166 → 0.4.168
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.d.ts +1 -2
- package/dist/agent.js +22 -13
- package/dist/api.js +16 -1
- package/dist/castle-host/host.d.ts +2 -0
- package/dist/castle-host/host.js +17 -3
- package/dist/imports.js +19 -4
- package/dist/localPaths.d.ts +2 -0
- package/dist/localPaths.js +6 -0
- package/dist/platformDoc.js +1 -1
- package/dist/save-deck.js +13 -1
- package/dist/serve.js +7 -1
- package/dist/serveSecurity.d.ts +12 -0
- package/dist/serveSecurity.js +42 -0
- package/dist/shell/assets/index-CVHj503j.css +1 -0
- package/dist/shell/assets/index-D-qJ5VGp.js +425 -0
- package/dist/shell/index.html +2 -2
- package/kits/base/castle.json +64 -2
- package/kits/base/editors/ImageViewer.js +259 -0
- package/kits/base/editors/MediaPlayer.js +90 -0
- package/kits/base/editors/imageViewer.css +95 -0
- package/kits/base/editors/mediaPlayer.css +58 -0
- package/kits/base/sdk/README.md +3 -1
- package/kits/base/sdk/castle.d.ts +1 -0
- package/kits/base/sdk/commands.d.ts +7 -0
- package/kits/base/sdk/multiplayer.d.ts +6 -0
- package/kits/base/sdk/multiplayer.js +6 -0
- package/kits/blank/.castle/agent/children.json +1 -0
- package/kits/blank/.castle/agent/messages.json +9 -0
- package/kits/blank/.castle/agent/pending-sends.json +1 -0
- package/kits/blank/.castle/agent/tasks/index.md +2 -0
- package/kits/blank/.castle/docs/platform.md +36 -0
- package/kits/blank/node_modules/.vite/deps/_metadata.json +8 -0
- package/kits/blank/node_modules/.vite/deps/package.json +3 -0
- package/kits/multiplayer-2d/CLAUDE.md +6 -0
- package/kits/multiplayer-2d/castle.json +3 -3
- package/kits/multiplayer-2d/code/systems/multiplayer.js +5 -0
- package/kits/multiplayer-2d/package-lock.json +4 -1
- package/kits/multiplayer-3d/CLAUDE.md +6 -1
- package/kits/multiplayer-3d/castle.json +3 -3
- package/kits/multiplayer-3d/code/systems/multiplayer.js +5 -0
- package/kits/multiplayer-3d/package-lock.json +1 -1
- package/kits/physics-2d/behaviors/Joints.jsx +11 -5
- package/kits/physics-2d/behaviors/Sprite.jsx +6 -21
- package/kits/physics-2d/castle.json +1 -61
- package/kits/physics-2d/editors/BlueprintLibrary.jsx +8 -68
- package/kits/physics-2d/editors/SingleEditor.jsx +3 -10
- package/kits/physics-2d/editors/editorRegistry.jsx +36 -2
- package/kits/physics-2d/engine/art.js +25 -1
- package/kits/physics-2d/engine/artThumbnail.jsx +92 -0
- package/kits/physics-2d/engine/files.js +4 -15
- package/kits/physics-2d/engine/spriteField.jsx +176 -0
- package/kits/physics-2d/engine/ui.module.css +136 -0
- package/kits/physics-2d/package-lock.json +5 -0
- package/kits/physics-2d/pnpm-lock.yaml +96 -3
- package/kits/physics-3d/castle.json +1 -1
- package/kits/physics-3d/package-lock.json +1 -1
- package/kits/physics-3d/pnpm-lock.yaml +88 -3
- package/kits/real-time/CLAUDE.md +5 -0
- package/kits/real-time/castle.json +2 -2
- package/kits/real-time/code/client/connection.js +4 -0
- package/kits/real-time/code/client/gameHooks.js +3 -0
- package/kits/real-time/code/client/soloBadge.js +71 -0
- package/kits/real-time/package.json +3 -0
- package/kits/turn-based/castle.json +1 -1
- package/kits/turn-based/package-lock.json +1 -1
- package/package.json +1 -1
- package/dist/shell/assets/index-CG9TrvCm.js +0 -444
- package/dist/shell/assets/index-CIRBt1qf.css +0 -1
- package/kits/base/node_modules/.package-lock.json +0 -28
- package/kits/base/node_modules/castle-web-fonts/README.md +0 -114
- package/kits/base/node_modules/castle-web-fonts/dist/catalog.d.ts +0 -18
- package/kits/base/node_modules/castle-web-fonts/dist/catalog.js +0 -142
- package/kits/base/node_modules/castle-web-fonts/dist/face.d.ts +0 -1
- package/kits/base/node_modules/castle-web-fonts/dist/face.js +0 -52
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Bore.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Bore.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/CourierPrime.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/CourierPrime.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/DMSans.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/DMSans.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Glacier.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Glacier.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/HelicoCentrica.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/HelicoCentrica.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Piazzolla.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Piazzolla.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Synco.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Synco.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Tektur.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Tektur.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/YatraOne.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/YatraOne.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/index.d.ts +0 -16
- package/kits/base/node_modules/castle-web-fonts/dist/index.js +0 -15
- package/kits/base/node_modules/castle-web-fonts/licenses/CC-BY-ND-4.0.txt +0 -393
- package/kits/base/node_modules/castle-web-fonts/licenses/FONTS.md +0 -64
- package/kits/base/node_modules/castle-web-fonts/licenses/OFL-1.1.txt +0 -95
- package/kits/base/node_modules/castle-web-fonts/package.json +0 -47
- package/kits/base/node_modules/castle-web-fonts/woff2/Bore.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/CourierPrime.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/DMSans.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Glacier.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/HelicoCentrica.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Piazzolla.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/SOURCE.txt +0 -2
- package/kits/base/node_modules/castle-web-fonts/woff2/Synco.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Tektur.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/YatraOne.woff2 +0 -0
- package/kits/physics-2d/editors/ImageViewer.jsx +0 -206
- package/kits/physics-2d/editors/MediaPlayer.jsx +0 -57
- package/kits/physics-2d/editors/mediaFile.js +0 -31
- package/kits/physics-2d/editors/mediaViewer.module.css +0 -128
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
// really do differ -- rendering a frame, and how many frames there are -- are
|
|
12
12
|
// `renderArtFrame` / `artFrameCount` here.
|
|
13
13
|
|
|
14
|
-
import { frameCount, renderSpriteFrame } from './pxart';
|
|
14
|
+
import { displayFinish, frameCount, renderSpriteFrame } from './pxart';
|
|
15
|
+
import { renderSmoothCompositeFrame, renderSmoothSpriteFrame } from './pxartSmooth';
|
|
15
16
|
|
|
16
17
|
// An image is one frame, held for as long as anyone asks (a still).
|
|
17
18
|
const STILL_FRAME = Object.freeze({ durationMs: 0 });
|
|
@@ -89,6 +90,10 @@ export function artFrameCount(art) {
|
|
|
89
90
|
// Draw one frame into `canvas` at the art's native resolution, resizing it to
|
|
90
91
|
// match. Returns false when there was nothing to draw yet (an image still
|
|
91
92
|
// loading), so callers don't cache the blank result.
|
|
93
|
+
//
|
|
94
|
+
// Pixel finish only — 1px/cell. Prefer `renderDisplayFrame` anywhere the art
|
|
95
|
+
// is shown to a person (belt, picker, scene), so Smooth / vector sprites keep
|
|
96
|
+
// their finish instead of looking blocky.
|
|
92
97
|
export function renderArtFrame(art, frameIndex, canvas) {
|
|
93
98
|
if (!isImageArt(art)) {
|
|
94
99
|
renderSpriteFrame(art, frameIndex, canvas);
|
|
@@ -103,3 +108,22 @@ export function renderArtFrame(art, frameIndex, canvas) {
|
|
|
103
108
|
ctx.drawImage(art.image, 0, 0);
|
|
104
109
|
return true;
|
|
105
110
|
}
|
|
111
|
+
|
|
112
|
+
// Same contract as `renderArtFrame`, but honours the sprite's display finish
|
|
113
|
+
// (vector / rounded corners → supersampled smooth render). Use this for any
|
|
114
|
+
// on-screen thumbnail or blit that should match what Sprite draws in the scene.
|
|
115
|
+
export function renderDisplayFrame(art, frameIndex, canvas) {
|
|
116
|
+
if (isImageArt(art)) return renderArtFrame(art, frameIndex, canvas);
|
|
117
|
+
if (!art) return false;
|
|
118
|
+
const finish = displayFinish(art);
|
|
119
|
+
if (finish.vector) {
|
|
120
|
+
renderSmoothCompositeFrame(art, frameIndex, canvas);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (finish.cornerRadius > 0) {
|
|
124
|
+
renderSmoothSpriteFrame(art, frameIndex, canvas, { cornerRadius: finish.cornerRadius });
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
renderSpriteFrame(art, frameIndex, canvas);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { isImageArt, onArtLoaded, renderDisplayFrame } from './art';
|
|
3
|
+
import { displayFinish } from './pxart';
|
|
4
|
+
import { cx, Icon, styles } from './ui';
|
|
5
|
+
|
|
6
|
+
// Asked of the CANVAS, not the source art: counting non-transparent grid chars
|
|
7
|
+
// would call a path-drawn sprite blank. Skipped for image art, whose native
|
|
8
|
+
// resolution is whatever was uploaded.
|
|
9
|
+
export function artRendersNothing(art, canvas) {
|
|
10
|
+
if (isImageArt(art)) return false;
|
|
11
|
+
try {
|
|
12
|
+
// Must match the options the render path already used to create the
|
|
13
|
+
// context. Asking for `{ willReadFrequently: true }` after a plain
|
|
14
|
+
// `getContext('2d')` returns null in browsers, and the catch below would
|
|
15
|
+
// then treat every transparent sprite as "has art" — blank names never
|
|
16
|
+
// appear in the belt or the picker.
|
|
17
|
+
const ctx = canvas.getContext('2d');
|
|
18
|
+
if (!ctx || canvas.width < 1 || canvas.height < 1) return true;
|
|
19
|
+
const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
20
|
+
for (let i = 3; i < data.length; i += 4) if (data[i] !== 0) return false;
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
// Tainted canvas: assume it draws something rather than mislabeling real
|
|
24
|
+
// art as blank.
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Shared art thumb used by the blueprint belt and the sprite picker. Falls
|
|
30
|
+
// back to `name` when there is no art, or the art is entirely transparent
|
|
31
|
+
// (a "Draw new" sprite nobody has drawn in yet) — same treatment the belt
|
|
32
|
+
// has always given undrawn blueprints.
|
|
33
|
+
//
|
|
34
|
+
// `className` sizes the outer slot (belt defaults to 36px via CSS; the picker
|
|
35
|
+
// stretches it to fill its tile). `fallbackIcon` is what shows when there is
|
|
36
|
+
// neither art nor a name (`clone` on the belt, `image` in the picker).
|
|
37
|
+
export function ArtThumbnail({
|
|
38
|
+
art,
|
|
39
|
+
name = '',
|
|
40
|
+
className,
|
|
41
|
+
preview = false,
|
|
42
|
+
fallbackIcon = 'image',
|
|
43
|
+
}) {
|
|
44
|
+
const canvasRef = useRef(null);
|
|
45
|
+
const [loadTick, setLoadTick] = useState(0);
|
|
46
|
+
const [blank, setBlank] = useState(false);
|
|
47
|
+
useEffect(() => onArtLoaded(() => setLoadTick((tick) => tick + 1)), []);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const canvas = canvasRef.current;
|
|
50
|
+
if (!canvas || !art) return;
|
|
51
|
+
const drew = renderDisplayFrame(art, 0, canvas);
|
|
52
|
+
// `false` means "not here yet" (an image still loading), not blank.
|
|
53
|
+
if (drew !== false) setBlank(artRendersNothing(art, canvas));
|
|
54
|
+
}, [art, loadTick]);
|
|
55
|
+
|
|
56
|
+
// The inner span is load-bearing, not decoration: bare text inside a flex
|
|
57
|
+
// container becomes an anonymous item sized to max-content, so it never
|
|
58
|
+
// "overflows a line" and `overflow-wrap` never fires -- a long single-word
|
|
59
|
+
// name silently clipped at both edges instead of wrapping. A real child can
|
|
60
|
+
// be held to the box's width, which is what gives the wrap something to
|
|
61
|
+
// break against.
|
|
62
|
+
const label = name ? (
|
|
63
|
+
<span className={cx(styles.bpSlotName, preview && styles.bpSlotNamePreview)} title={name}>
|
|
64
|
+
<span className={styles.bpSlotNameText}>{name}</span>
|
|
65
|
+
</span>
|
|
66
|
+
) : (
|
|
67
|
+
<span className={styles.bpSlotFallback}>
|
|
68
|
+
<Icon name={fallbackIcon} />
|
|
69
|
+
</span>
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (!art) {
|
|
73
|
+
return className ? <span className={className}>{label}</span> : label;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const smooth = displayFinish(art).smooth;
|
|
77
|
+
return (
|
|
78
|
+
<span className={cx(styles.bpSlotArt, className)}>
|
|
79
|
+
<canvas
|
|
80
|
+
ref={canvasRef}
|
|
81
|
+
className={cx(
|
|
82
|
+
styles.bpSlotThumb,
|
|
83
|
+
smooth && styles.bpSlotThumbSmooth,
|
|
84
|
+
preview && styles.bpPreviewThumb,
|
|
85
|
+
blank && styles.bpSlotThumbHidden
|
|
86
|
+
)}
|
|
87
|
+
aria-hidden="true"
|
|
88
|
+
/>
|
|
89
|
+
{blank ? label : null}
|
|
90
|
+
</span>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -117,26 +117,15 @@ export function mediaFilesOfKind(kind) {
|
|
|
117
117
|
const matches = { image: isImageFile, audio: isAudioFile, video: isVideoFile }[kind];
|
|
118
118
|
return Object.keys(initialMedia).filter((path) => matches(path));
|
|
119
119
|
}
|
|
120
|
-
// Which editor a file opens in
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
// `endsWith` per `return`.
|
|
120
|
+
// Which editor a file opens in when no `editorModule` was resolved. Media
|
|
121
|
+
// viewing (image / audio / video) lives on castle.base now; this only covers
|
|
122
|
+
// types this kit still routes itself (scene / sprite / style). Each extension
|
|
123
|
+
// gets its own line: legacy tooling matched one `endsWith` per `return`.
|
|
125
124
|
export function getFileKind(path) {
|
|
126
125
|
if (path.endsWith('.scene')) return 'scene';
|
|
127
126
|
if (path.endsWith('.sprite')) return 'pxart';
|
|
128
127
|
if (path.endsWith('.pxart')) return 'pxart';
|
|
129
128
|
if (path.endsWith('.style')) return 'style';
|
|
130
|
-
if (path.endsWith('.png')) return 'image';
|
|
131
|
-
if (path.endsWith('.jpg')) return 'image';
|
|
132
|
-
if (path.endsWith('.jpeg')) return 'image';
|
|
133
|
-
if (path.endsWith('.gif')) return 'image';
|
|
134
|
-
if (path.endsWith('.webp')) return 'image';
|
|
135
|
-
if (path.endsWith('.svg')) return 'image';
|
|
136
|
-
if (path.endsWith('.mp3')) return 'audio';
|
|
137
|
-
if (path.endsWith('.wav')) return 'audio';
|
|
138
|
-
if (path.endsWith('.m4a')) return 'audio';
|
|
139
|
-
if (path.endsWith('.mp4')) return 'video';
|
|
140
129
|
if (path.endsWith('.js') || path.endsWith('.jsx')) return 'code';
|
|
141
130
|
return 'text';
|
|
142
131
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { ArtThumbnail } from './artThumbnail';
|
|
4
|
+
import { cx, FieldRow, Icon, styles } from './ui';
|
|
5
|
+
|
|
6
|
+
// Visual sprite / image picker for inspector fields that point at art files.
|
|
7
|
+
// Anchored popover (same family as FileField), not a modal: thumbnails only,
|
|
8
|
+
// name-without-extension on hover, pick closes. Blank art reuses ArtThumbnail's
|
|
9
|
+
// belt fallback (title in place of the empty canvas).
|
|
10
|
+
|
|
11
|
+
function basenameOf(path) {
|
|
12
|
+
if (!path) return '';
|
|
13
|
+
return path.slice(path.lastIndexOf('/') + 1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function displayNameOf(path) {
|
|
17
|
+
const base = basenameOf(path);
|
|
18
|
+
if (!base) return 'None';
|
|
19
|
+
return base.replace(/\.[^.]+$/, '') || base;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function candidatePaths(files, sprites, current, allowEmpty) {
|
|
23
|
+
const paths = [];
|
|
24
|
+
if (allowEmpty) paths.push('');
|
|
25
|
+
const list = files ?? Object.keys(sprites ?? {}).sort();
|
|
26
|
+
for (const path of list) {
|
|
27
|
+
if (path && !paths.includes(path)) paths.push(path);
|
|
28
|
+
}
|
|
29
|
+
if (current && !paths.includes(current)) {
|
|
30
|
+
paths.splice(allowEmpty ? 1 : 0, 0, current);
|
|
31
|
+
}
|
|
32
|
+
return paths;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function placePopover(anchor, popover) {
|
|
36
|
+
const margin = 8;
|
|
37
|
+
const gap = 4;
|
|
38
|
+
const rect = anchor.getBoundingClientRect();
|
|
39
|
+
const box = popover.getBoundingClientRect();
|
|
40
|
+
const width = Math.min(Math.max(rect.width, 220), window.innerWidth - margin * 2);
|
|
41
|
+
let left = rect.left;
|
|
42
|
+
if (left + width > window.innerWidth - margin) {
|
|
43
|
+
left = Math.max(margin, window.innerWidth - width - margin);
|
|
44
|
+
}
|
|
45
|
+
let top = rect.bottom + gap;
|
|
46
|
+
if (top + box.height > window.innerHeight - margin) {
|
|
47
|
+
top = Math.max(margin, rect.top - gap - box.height);
|
|
48
|
+
}
|
|
49
|
+
return { top, left, width };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function SpritePickerPopover({ paths, current, sprites, anchorRef, onPick, onClose }) {
|
|
53
|
+
const listRef = useRef(null);
|
|
54
|
+
const [pos, setPos] = useState(null);
|
|
55
|
+
|
|
56
|
+
useLayoutEffect(() => {
|
|
57
|
+
if (!anchorRef.current || !listRef.current) return undefined;
|
|
58
|
+
function place() {
|
|
59
|
+
setPos(placePopover(anchorRef.current, listRef.current));
|
|
60
|
+
}
|
|
61
|
+
place();
|
|
62
|
+
window.addEventListener('resize', place);
|
|
63
|
+
window.addEventListener('scroll', place, true);
|
|
64
|
+
return () => {
|
|
65
|
+
window.removeEventListener('resize', place);
|
|
66
|
+
window.removeEventListener('scroll', place, true);
|
|
67
|
+
};
|
|
68
|
+
}, [anchorRef, paths.length]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
function onKeyDown(event) {
|
|
72
|
+
if (event.key === 'Escape') onClose();
|
|
73
|
+
}
|
|
74
|
+
function onPointerDown(event) {
|
|
75
|
+
if (listRef.current?.contains(event.target) || anchorRef.current?.contains(event.target)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
onClose();
|
|
79
|
+
}
|
|
80
|
+
window.addEventListener('keydown', onKeyDown);
|
|
81
|
+
window.addEventListener('pointerdown', onPointerDown);
|
|
82
|
+
return () => {
|
|
83
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
84
|
+
window.removeEventListener('pointerdown', onPointerDown);
|
|
85
|
+
};
|
|
86
|
+
}, [anchorRef, onClose]);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
ref={listRef}
|
|
91
|
+
className={styles.spritePicker}
|
|
92
|
+
role="listbox"
|
|
93
|
+
aria-label="Sprite"
|
|
94
|
+
style={pos ?? { visibility: 'hidden' }}>
|
|
95
|
+
{paths.map((path) => {
|
|
96
|
+
const selected = path === current;
|
|
97
|
+
const name = displayNameOf(path);
|
|
98
|
+
return (
|
|
99
|
+
<button
|
|
100
|
+
key={path || '__none'}
|
|
101
|
+
type="button"
|
|
102
|
+
role="option"
|
|
103
|
+
aria-selected={selected}
|
|
104
|
+
aria-label={name}
|
|
105
|
+
className={cx(styles.spritePickerTile, selected && styles.spritePickerTileSelected)}
|
|
106
|
+
data-name={name}
|
|
107
|
+
onClick={() => onPick(path)}>
|
|
108
|
+
{path ? (
|
|
109
|
+
<ArtThumbnail art={sprites?.[path]} name={name} className={styles.spritePickerThumb} />
|
|
110
|
+
) : (
|
|
111
|
+
<span className={cx(styles.bpSlotFallback, styles.spritePickerThumb)}>
|
|
112
|
+
<Icon name="times" />
|
|
113
|
+
</span>
|
|
114
|
+
)}
|
|
115
|
+
</button>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function SpriteField({
|
|
123
|
+
label,
|
|
124
|
+
value,
|
|
125
|
+
onChange,
|
|
126
|
+
sprites,
|
|
127
|
+
files,
|
|
128
|
+
allowEmpty = false,
|
|
129
|
+
overridden,
|
|
130
|
+
defaultValue,
|
|
131
|
+
onReset,
|
|
132
|
+
}) {
|
|
133
|
+
const current = value ?? '';
|
|
134
|
+
const [open, setOpen] = useState(false);
|
|
135
|
+
const buttonRef = useRef(null);
|
|
136
|
+
const paths = candidatePaths(files, sprites, current, allowEmpty);
|
|
137
|
+
const art = current ? sprites?.[current] : null;
|
|
138
|
+
const name = current ? displayNameOf(current) : '';
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<FieldRow
|
|
142
|
+
label={label}
|
|
143
|
+
overridden={overridden}
|
|
144
|
+
defaultValue={defaultValue ? displayNameOf(defaultValue) : defaultValue}
|
|
145
|
+
onReset={onReset}>
|
|
146
|
+
<button
|
|
147
|
+
ref={buttonRef}
|
|
148
|
+
type="button"
|
|
149
|
+
className={cx(styles.select, styles.fileField, styles.spriteField)}
|
|
150
|
+
aria-haspopup="listbox"
|
|
151
|
+
aria-expanded={open}
|
|
152
|
+
onClick={() => setOpen((next) => !next)}>
|
|
153
|
+
<ArtThumbnail art={art} className={styles.spriteFieldThumb} />
|
|
154
|
+
<span className={cx(styles.fileFieldLabel, !current && styles.fileFieldEmpty)}>
|
|
155
|
+
{current ? name : 'None'}
|
|
156
|
+
</span>
|
|
157
|
+
</button>
|
|
158
|
+
{open
|
|
159
|
+
? createPortal(
|
|
160
|
+
<SpritePickerPopover
|
|
161
|
+
paths={paths}
|
|
162
|
+
current={current}
|
|
163
|
+
sprites={sprites}
|
|
164
|
+
anchorRef={buttonRef}
|
|
165
|
+
onPick={(path) => {
|
|
166
|
+
onChange(path);
|
|
167
|
+
setOpen(false);
|
|
168
|
+
}}
|
|
169
|
+
onClose={() => setOpen(false)}
|
|
170
|
+
/>,
|
|
171
|
+
document.body
|
|
172
|
+
)
|
|
173
|
+
: null}
|
|
174
|
+
</FieldRow>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -465,6 +465,11 @@
|
|
|
465
465
|
object-fit: contain;
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
+
/* Smooth / vector finish is supersampled — nearest-neighbour would re-blockify it. */
|
|
469
|
+
.bpSlotThumbSmooth {
|
|
470
|
+
image-rendering: auto;
|
|
471
|
+
}
|
|
472
|
+
|
|
468
473
|
.bpSlotFallback {
|
|
469
474
|
display: inline-flex;
|
|
470
475
|
align-items: center;
|
|
@@ -1871,6 +1876,137 @@
|
|
|
1871
1876
|
color: var(--castle-inspector-muted);
|
|
1872
1877
|
}
|
|
1873
1878
|
|
|
1879
|
+
/* SpriteField: closed chrome carries a tiny thumb beside the basename so you
|
|
1880
|
+
can see which art is selected without opening the picker. */
|
|
1881
|
+
.spriteField {
|
|
1882
|
+
gap: 8px;
|
|
1883
|
+
padding-left: 6px;
|
|
1884
|
+
min-height: 32px;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
.spriteFieldThumb {
|
|
1888
|
+
width: 22px;
|
|
1889
|
+
height: 22px;
|
|
1890
|
+
flex: 0 0 22px;
|
|
1891
|
+
border-radius: 3px;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/* Anchored art grid — same shell family as FileField's path list, not a modal. */
|
|
1895
|
+
.spritePicker {
|
|
1896
|
+
position: fixed;
|
|
1897
|
+
z-index: 200;
|
|
1898
|
+
box-sizing: border-box;
|
|
1899
|
+
display: grid;
|
|
1900
|
+
grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
|
|
1901
|
+
gap: 6px;
|
|
1902
|
+
max-height: min(280px, calc(100vh - 16px));
|
|
1903
|
+
overflow-x: hidden;
|
|
1904
|
+
overflow-y: auto;
|
|
1905
|
+
overscroll-behavior-y: contain;
|
|
1906
|
+
padding: 8px;
|
|
1907
|
+
border: 1px solid var(--castle-inspector-border);
|
|
1908
|
+
border-radius: var(--castle-radius);
|
|
1909
|
+
background: var(--castle-workspace-bg);
|
|
1910
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
|
|
1911
|
+
scrollbar-width: thin;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
.spritePickerTile {
|
|
1915
|
+
position: relative;
|
|
1916
|
+
display: flex;
|
|
1917
|
+
align-items: center;
|
|
1918
|
+
justify-content: center;
|
|
1919
|
+
min-width: 0;
|
|
1920
|
+
aspect-ratio: 1;
|
|
1921
|
+
padding: 4px;
|
|
1922
|
+
border: 1px solid var(--castle-inspector-border);
|
|
1923
|
+
border-radius: 6px;
|
|
1924
|
+
background: var(--castle-inspector-input-bg);
|
|
1925
|
+
color: var(--castle-text);
|
|
1926
|
+
cursor: pointer;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
.spritePickerTile:hover {
|
|
1930
|
+
border-color: var(--castle-inspector-muted);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
.spritePickerTileSelected,
|
|
1934
|
+
.spritePickerTileSelected:hover {
|
|
1935
|
+
border-color: var(--castle-selected);
|
|
1936
|
+
box-shadow: 0 0 0 1px var(--castle-selected);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
/* Name without extension — hover only, so the grid stays pictures.
|
|
1940
|
+
Anchored to the bottom of the tile (not above it) so the top row stays
|
|
1941
|
+
readable and the scrollable picker never clips the label. */
|
|
1942
|
+
.spritePickerTile::after {
|
|
1943
|
+
content: attr(data-name);
|
|
1944
|
+
position: absolute;
|
|
1945
|
+
left: 0;
|
|
1946
|
+
right: 0;
|
|
1947
|
+
bottom: 0;
|
|
1948
|
+
z-index: 1;
|
|
1949
|
+
padding: 3px 4px;
|
|
1950
|
+
border-radius: 0 0 5px 5px;
|
|
1951
|
+
background: rgba(0, 0, 0, 0.72);
|
|
1952
|
+
color: var(--castle-text);
|
|
1953
|
+
font-size: 10px;
|
|
1954
|
+
line-height: 1.2;
|
|
1955
|
+
text-align: center;
|
|
1956
|
+
white-space: nowrap;
|
|
1957
|
+
overflow: hidden;
|
|
1958
|
+
text-overflow: ellipsis;
|
|
1959
|
+
pointer-events: none;
|
|
1960
|
+
opacity: 0;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
.spritePickerTile:hover::after,
|
|
1964
|
+
.spritePickerTile:focus-visible::after {
|
|
1965
|
+
opacity: 1;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
.spritePickerThumb {
|
|
1969
|
+
width: 100%;
|
|
1970
|
+
height: 100%;
|
|
1971
|
+
flex: 0 0 auto;
|
|
1972
|
+
max-width: 44px;
|
|
1973
|
+
max-height: 44px;
|
|
1974
|
+
border-radius: 4px;
|
|
1975
|
+
object-fit: contain;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
/* Picker tiles are larger than the belt's 36px slot — stretch the shared
|
|
1979
|
+
ArtThumbnail chrome (canvas / blank-name / fallback) to fill the tile. */
|
|
1980
|
+
.spritePickerThumb.bpSlotArt,
|
|
1981
|
+
.spritePickerThumb .bpSlotThumb,
|
|
1982
|
+
.spritePickerThumb .bpSlotName,
|
|
1983
|
+
.spritePickerThumb .bpSlotFallback {
|
|
1984
|
+
width: 100%;
|
|
1985
|
+
height: 100%;
|
|
1986
|
+
flex: none;
|
|
1987
|
+
max-width: none;
|
|
1988
|
+
max-height: none;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
.spritePickerThumb .bpSlotName {
|
|
1992
|
+
font-size: 9px;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
.spriteFieldThumb.bpSlotArt,
|
|
1996
|
+
.spriteFieldThumb .bpSlotThumb,
|
|
1997
|
+
.spriteFieldThumb .bpSlotName,
|
|
1998
|
+
.spriteFieldThumb .bpSlotFallback {
|
|
1999
|
+
width: 22px;
|
|
2000
|
+
height: 22px;
|
|
2001
|
+
flex: 0 0 22px;
|
|
2002
|
+
max-width: none;
|
|
2003
|
+
max-height: none;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
.spriteFieldThumb .bpSlotName {
|
|
2007
|
+
font-size: 5.5px;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1874
2010
|
.filePicker {
|
|
1875
2011
|
position: fixed;
|
|
1876
2012
|
z-index: 200;
|
|
@@ -216,6 +216,7 @@
|
|
|
216
216
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
|
217
217
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
|
218
218
|
"license": "MIT",
|
|
219
|
+
"peer": true,
|
|
219
220
|
"dependencies": {
|
|
220
221
|
"@dnd-kit/accessibility": "^3.1.1",
|
|
221
222
|
"@dnd-kit/utilities": "^3.2.2",
|
|
@@ -669,6 +670,7 @@
|
|
|
669
670
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
|
670
671
|
"dev": true,
|
|
671
672
|
"license": "MIT",
|
|
673
|
+
"peer": true,
|
|
672
674
|
"bin": {
|
|
673
675
|
"acorn": "bin/acorn"
|
|
674
676
|
},
|
|
@@ -1129,6 +1131,7 @@
|
|
|
1129
1131
|
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
|
1130
1132
|
"dev": true,
|
|
1131
1133
|
"license": "MIT",
|
|
1134
|
+
"peer": true,
|
|
1132
1135
|
"dependencies": {
|
|
1133
1136
|
"@eslint-community/eslint-utils": "^4.8.0",
|
|
1134
1137
|
"@eslint-community/regexpp": "^4.12.1",
|
|
@@ -2438,6 +2441,7 @@
|
|
|
2438
2441
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
|
2439
2442
|
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
|
2440
2443
|
"license": "MIT",
|
|
2444
|
+
"peer": true,
|
|
2441
2445
|
"engines": {
|
|
2442
2446
|
"node": ">=0.10.0"
|
|
2443
2447
|
}
|
|
@@ -2447,6 +2451,7 @@
|
|
|
2447
2451
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
|
2448
2452
|
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
|
2449
2453
|
"license": "MIT",
|
|
2454
|
+
"peer": true,
|
|
2450
2455
|
"dependencies": {
|
|
2451
2456
|
"scheduler": "^0.27.0"
|
|
2452
2457
|
},
|