castle-web-cli 0.4.182 → 0.4.184
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-failures.d.ts +1 -0
- package/dist/agent-failures.js +31 -4
- package/dist/agent-prompts.d.ts +0 -3
- package/dist/agent-prompts.js +14 -28
- package/dist/agent.d.ts +1 -13
- package/dist/agent.js +187 -938
- package/dist/byo-accounts.d.ts +4 -4
- package/dist/byo-accounts.js +17 -38
- package/dist/byo-auth.d.ts +2 -5
- package/dist/byo-auth.js +4 -52
- package/dist/byo-login.d.ts +4 -5
- package/dist/byo-login.js +28 -56
- package/dist/mcpPlaytest.js +1 -2
- package/dist/metering.d.ts +31 -23
- package/dist/metering.js +45 -52
- package/dist/openrouter-catalog.d.ts +0 -1
- package/dist/openrouter-catalog.js +0 -11
- package/dist/platformDoc.d.ts +1 -1
- package/dist/platformDoc.js +9 -12
- package/dist/serve.js +3 -5
- package/dist/shell/assets/{index-EGp86HKZ.js → index-g4l7hWIQ.js} +85 -83
- package/dist/shell/assets/index-y7CK6t3j.css +1 -0
- package/dist/shell/index.html +2 -2
- package/kits/physics-2d/CLAUDE.md +5 -1
- package/kits/physics-2d/castle.json +1 -1
- package/kits/physics-2d/editors/SceneEditor.jsx +2 -1
- package/kits/physics-2d/engine/autoInspector.jsx +32 -3
- package/kits/physics-2d/engine/fields/fields.jsx +305 -5
- package/kits/physics-2d/engine/fields/fields.module.css +81 -2
- package/kits/physics-2d/engine/paletteField.jsx +8 -3
- package/kits/physics-2d/engine/spriteField.jsx +3 -3
- package/kits/physics-2d/engine/ui.jsx +5 -277
- package/kits/physics-2d/engine/ui.module.css +0 -69
- package/package.json +1 -1
- package/dist/shell/assets/index-C5D4GoKe.css +0 -1
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
// The inspector's field widgets: one source for every place that edits a value
|
|
5
5
|
// with them, so a param in the shell scrubs exactly like a behavior prop in a
|
|
6
|
-
// kit's scene inspector.
|
|
6
|
+
// kit's scene inspector. Text, number, checkbox, color, select and file, plus
|
|
7
|
+
// the row chrome and the note line under a field.
|
|
7
8
|
//
|
|
8
9
|
// The canonical copy of this file is shared/fields/fields.tsx. Edit it there and
|
|
9
10
|
// run `npm run sync:shared`: scripts/sync-shared.mjs copies it byte-for-byte to
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
// The host supplies the --castle-* tokens the CSS reads. Anything portaled to
|
|
15
16
|
// document.body leaves the host's tree, so a host whose tokens live on a panel
|
|
16
17
|
// root names a class for the portal root through FieldsChrome.
|
|
17
|
-
import { createContext, useContext, useEffect, useRef, useState } from 'react';
|
|
18
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
18
19
|
import { createPortal } from 'react-dom';
|
|
19
20
|
import styles from './fields.module.css';
|
|
20
21
|
function cx(...parts) {
|
|
@@ -102,6 +103,12 @@ export function TextField({ value, onChange, ...row }) {
|
|
|
102
103
|
function numberText(v) {
|
|
103
104
|
return Number.isFinite(v) ? String(v) : '0';
|
|
104
105
|
}
|
|
106
|
+
// `0.08 s`. The space is a nbsp because a value and its unit are one token to
|
|
107
|
+
// read and must never wrap apart.
|
|
108
|
+
function UnitSuffix({ unit }) {
|
|
109
|
+
if (!unit) return null;
|
|
110
|
+
return <span className={styles.unitSuffix}>{`\u00a0${unit}`}</span>;
|
|
111
|
+
}
|
|
105
112
|
function clamp(v, min, max) {
|
|
106
113
|
if (typeof min === 'number' && v < min) v = min;
|
|
107
114
|
if (typeof max === 'number' && v > max) v = max;
|
|
@@ -124,7 +131,7 @@ const NUMPAD_GLYPH = {
|
|
|
124
131
|
// minus on mobile (the native numpad has none, and there's no web API to add one).
|
|
125
132
|
// The field has no <input>, so no native keyboard ever appears; this edits a draft
|
|
126
133
|
// string and commits on Done / dismiss.
|
|
127
|
-
function NumpadSheet({ label, value, min, max, onCommit, onClose }) {
|
|
134
|
+
function NumpadSheet({ label, value, min, max, unit, onCommit, onClose }) {
|
|
128
135
|
const [draft, setDraft] = useState(() => numberText(value));
|
|
129
136
|
const pristineRef = useRef(true); // first digit replaces the seeded value
|
|
130
137
|
function finish() {
|
|
@@ -169,6 +176,7 @@ function NumpadSheet({ label, value, min, max, onCommit, onClose }) {
|
|
|
169
176
|
<span className={styles.numpadValueLabel}>{label}</span>
|
|
170
177
|
<span className={styles.numpadValueNum}>
|
|
171
178
|
{draft === '' || draft === '-' ? '0' : draft}
|
|
179
|
+
<UnitSuffix unit={unit} />
|
|
172
180
|
</span>
|
|
173
181
|
</div>
|
|
174
182
|
<div className={styles.numpadGrid}>
|
|
@@ -256,7 +264,7 @@ function snapToStep(v, step) {
|
|
|
256
264
|
function sliderRange(min, max) {
|
|
257
265
|
return typeof min === 'number' && typeof max === 'number' && max > min ? { min, max } : null;
|
|
258
266
|
}
|
|
259
|
-
export function NumberField({ value, onChange, min, max, step = 1, ...row }) {
|
|
267
|
+
export function NumberField({ value, onChange, min, max, step = 1, unit, ...row }) {
|
|
260
268
|
const current = Number.isFinite(value) ? (value ?? 0) : 0;
|
|
261
269
|
const range = sliderRange(min, max);
|
|
262
270
|
// Drag-to-scrub is for every pointer, but what a plain CLICK/TAP opens differs:
|
|
@@ -366,6 +374,7 @@ export function NumberField({ value, onChange, min, max, step = 1, ...row }) {
|
|
|
366
374
|
range ? styles.numberSlider : styles.numberGrip
|
|
367
375
|
)}
|
|
368
376
|
data-field="number"
|
|
377
|
+
data-slider={range ? '' : undefined}
|
|
369
378
|
style={{ touchAction: 'pan-y' }}
|
|
370
379
|
onPointerDown={onPointerDown}
|
|
371
380
|
onPointerMove={onPointerMove}
|
|
@@ -389,7 +398,10 @@ export function NumberField({ value, onChange, min, max, step = 1, ...row }) {
|
|
|
389
398
|
⋮⋮
|
|
390
399
|
</span>
|
|
391
400
|
)}
|
|
392
|
-
<span className={styles.numberValue}>
|
|
401
|
+
<span className={styles.numberValue}>
|
|
402
|
+
{numberText(current)}
|
|
403
|
+
<UnitSuffix unit={unit} />
|
|
404
|
+
</span>
|
|
393
405
|
</div>
|
|
394
406
|
{scrubPos ? (
|
|
395
407
|
<Overlay>
|
|
@@ -398,6 +410,7 @@ export function NumberField({ value, onChange, min, max, step = 1, ...row }) {
|
|
|
398
410
|
style={{ left: scrubPos.x, top: scrubPos.y - 40 }}
|
|
399
411
|
aria-hidden="true">
|
|
400
412
|
{numberText(current)}
|
|
413
|
+
<UnitSuffix unit={unit} />
|
|
401
414
|
</div>
|
|
402
415
|
</Overlay>
|
|
403
416
|
) : null}
|
|
@@ -407,6 +420,7 @@ export function NumberField({ value, onChange, min, max, step = 1, ...row }) {
|
|
|
407
420
|
value={current}
|
|
408
421
|
min={min}
|
|
409
422
|
max={max}
|
|
423
|
+
unit={unit}
|
|
410
424
|
onCommit={commit}
|
|
411
425
|
onClose={() => setPadOpen(false)}
|
|
412
426
|
/>
|
|
@@ -438,6 +452,38 @@ export function CheckboxField({ checked, onChange, ...row }) {
|
|
|
438
452
|
</FieldRow>
|
|
439
453
|
);
|
|
440
454
|
}
|
|
455
|
+
function optionPair(option) {
|
|
456
|
+
if (typeof option === 'object') return { value: String(option.value), label: option.label };
|
|
457
|
+
return { value: String(option), label: String(option) };
|
|
458
|
+
}
|
|
459
|
+
// A select's value is a string in the DOM whatever the underlying type is; a
|
|
460
|
+
// host that holds numbers converts on the way back out.
|
|
461
|
+
export function SelectField({ value, onChange, options, ...row }) {
|
|
462
|
+
const current = value ?? '';
|
|
463
|
+
const listed = options.map(optionPair);
|
|
464
|
+
// A current value the options don't have is prepended rather than dropped: a
|
|
465
|
+
// select that renders blank for it reads as unset, and a stale or misspelled
|
|
466
|
+
// value is exactly the case where you need to see what it actually says.
|
|
467
|
+
const shown =
|
|
468
|
+
current === '' || listed.some((option) => option.value === current)
|
|
469
|
+
? listed
|
|
470
|
+
: [{ value: current, label: current }, ...listed];
|
|
471
|
+
return (
|
|
472
|
+
<FieldRow {...row}>
|
|
473
|
+
<select
|
|
474
|
+
className={cx(styles.input, styles.select)}
|
|
475
|
+
data-field="select"
|
|
476
|
+
value={current}
|
|
477
|
+
onChange={(event) => onChange(event.target.value)}>
|
|
478
|
+
{shown.map((option) => (
|
|
479
|
+
<option key={option.value} value={option.value}>
|
|
480
|
+
{option.label}
|
|
481
|
+
</option>
|
|
482
|
+
))}
|
|
483
|
+
</select>
|
|
484
|
+
</FieldRow>
|
|
485
|
+
);
|
|
486
|
+
}
|
|
441
487
|
// Explanatory text under a field, for a prop whose name doesn't carry its
|
|
442
488
|
// meaning. Rendered inside the panel's field grid, so it lands under the control
|
|
443
489
|
// rather than under the label.
|
|
@@ -473,3 +519,257 @@ function normalizeHex(value) {
|
|
|
473
519
|
if (/^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(raw)) return raw;
|
|
474
520
|
return '#000000';
|
|
475
521
|
}
|
|
522
|
+
// ── file paths ───────────────────────────────────────────────────────────────
|
|
523
|
+
// Which extensions are an image, a sound or a video. A browser fact rather than
|
|
524
|
+
// any one kit's taxonomy -- the same kind the CLI's own unsupportedMedia list
|
|
525
|
+
// holds -- so the host that asks for `file: 'image'` need not carry its own.
|
|
526
|
+
export const FILE_KINDS = {
|
|
527
|
+
image: ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'],
|
|
528
|
+
audio: ['.mp3', '.wav', '.m4a'],
|
|
529
|
+
video: ['.mp4'],
|
|
530
|
+
};
|
|
531
|
+
export function isFileKind(path, kind) {
|
|
532
|
+
if (kind === 'any') return true;
|
|
533
|
+
const exts = FILE_KINDS[kind];
|
|
534
|
+
return exts ? exts.some((ext) => path.toLowerCase().endsWith(ext)) : false;
|
|
535
|
+
}
|
|
536
|
+
/** Which kind a path is, or null for a file that is none of them. */
|
|
537
|
+
export function mediaKindOf(path) {
|
|
538
|
+
for (const kind of Object.keys(FILE_KINDS)) {
|
|
539
|
+
if (isFileKind(path, kind)) return kind;
|
|
540
|
+
}
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
function basenameOf(path) {
|
|
544
|
+
if (!path) return '';
|
|
545
|
+
return path.slice(path.lastIndexOf('/') + 1);
|
|
546
|
+
}
|
|
547
|
+
let measureCanvasCtx = null;
|
|
548
|
+
function measureTextWidth(text, font) {
|
|
549
|
+
if (!measureCanvasCtx) {
|
|
550
|
+
measureCanvasCtx = document.createElement('canvas').getContext('2d');
|
|
551
|
+
}
|
|
552
|
+
if (!measureCanvasCtx) return 0;
|
|
553
|
+
measureCanvasCtx.font = font;
|
|
554
|
+
return measureCanvasCtx.measureText(text).width;
|
|
555
|
+
}
|
|
556
|
+
// `getComputedStyle().font` is empty in some browsers; rebuild it when it is.
|
|
557
|
+
function fontOf(style) {
|
|
558
|
+
return style.font && style.font !== '0px'
|
|
559
|
+
? style.font
|
|
560
|
+
: `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
|
|
561
|
+
}
|
|
562
|
+
// Keep the filename intact; the directory is what yields. Binary-search the
|
|
563
|
+
// prefix of the directory until `{prefix}…/{basename}` fits `maxWidth`.
|
|
564
|
+
function fitPathKeepBasename(path, maxWidth, font) {
|
|
565
|
+
const measure = (text) => measureTextWidth(text, font);
|
|
566
|
+
if (maxWidth <= 0 || measure(path) <= maxWidth) return path;
|
|
567
|
+
const slash = path.lastIndexOf('/');
|
|
568
|
+
const dir = slash > 0 ? path.slice(0, slash) : '';
|
|
569
|
+
const base = slash > 0 ? path.slice(slash + 1) : path;
|
|
570
|
+
const ellipsed = `…/${base}`;
|
|
571
|
+
if (!dir || measure(ellipsed) > maxWidth) return fitKeepEnd(base, maxWidth, measure);
|
|
572
|
+
let lo = 0;
|
|
573
|
+
let hi = dir.length;
|
|
574
|
+
let best = ellipsed;
|
|
575
|
+
while (lo <= hi) {
|
|
576
|
+
const mid = (lo + hi) >> 1;
|
|
577
|
+
const candidate = `${dir.slice(0, mid)}…/${base}`;
|
|
578
|
+
if (measure(candidate) <= maxWidth) {
|
|
579
|
+
best = candidate;
|
|
580
|
+
lo = mid + 1;
|
|
581
|
+
} else {
|
|
582
|
+
hi = mid - 1;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return best;
|
|
586
|
+
}
|
|
587
|
+
function fitKeepEnd(text, maxWidth, measure) {
|
|
588
|
+
if (measure(text) <= maxWidth) return text;
|
|
589
|
+
if (measure('…') > maxWidth) return '';
|
|
590
|
+
let lo = 0;
|
|
591
|
+
let hi = text.length;
|
|
592
|
+
let best = '…';
|
|
593
|
+
while (lo <= hi) {
|
|
594
|
+
const mid = (lo + hi) >> 1;
|
|
595
|
+
const candidate = `…${text.slice(text.length - mid)}`;
|
|
596
|
+
if (measure(candidate) <= maxWidth) {
|
|
597
|
+
best = candidate;
|
|
598
|
+
lo = mid + 1;
|
|
599
|
+
} else {
|
|
600
|
+
hi = mid - 1;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return best;
|
|
604
|
+
}
|
|
605
|
+
function FilePathLabel({ path }) {
|
|
606
|
+
const ref = useRef(null);
|
|
607
|
+
const [shown, setShown] = useState(path);
|
|
608
|
+
useLayoutEffect(() => {
|
|
609
|
+
const node = ref.current;
|
|
610
|
+
if (!node) return undefined;
|
|
611
|
+
function fit() {
|
|
612
|
+
const max = node.clientWidth;
|
|
613
|
+
if (max <= 0) return;
|
|
614
|
+
setShown(fitPathKeepBasename(path, max, fontOf(getComputedStyle(node))));
|
|
615
|
+
}
|
|
616
|
+
fit();
|
|
617
|
+
const observer = new ResizeObserver(fit);
|
|
618
|
+
observer.observe(node);
|
|
619
|
+
return () => observer.disconnect();
|
|
620
|
+
}, [path]);
|
|
621
|
+
return (
|
|
622
|
+
<span ref={ref} className={styles.filePickerPath} title={path}>
|
|
623
|
+
{shown}
|
|
624
|
+
</span>
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
function pickerRowChrome(list) {
|
|
628
|
+
if (!list) return 26;
|
|
629
|
+
const box = getComputedStyle(list);
|
|
630
|
+
const opt = list.querySelector('button');
|
|
631
|
+
const op = opt ? getComputedStyle(opt) : null;
|
|
632
|
+
return (
|
|
633
|
+
(parseFloat(box.paddingLeft) || 0) +
|
|
634
|
+
(parseFloat(box.paddingRight) || 0) +
|
|
635
|
+
(parseFloat(box.borderLeftWidth) || 0) +
|
|
636
|
+
(parseFloat(box.borderRightWidth) || 0) +
|
|
637
|
+
(op ? (parseFloat(op.paddingLeft) || 0) + (parseFloat(op.paddingRight) || 0) : 16)
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
function longestPathWidth(list, paths) {
|
|
641
|
+
if (!list || !paths.length) return 0;
|
|
642
|
+
const probe = document.createElement('span');
|
|
643
|
+
probe.style.cssText = 'position:absolute;left:-9999px;white-space:nowrap;pointer-events:none;';
|
|
644
|
+
const opt = list.querySelector('button');
|
|
645
|
+
if (opt) probe.style.font = fontOf(getComputedStyle(opt));
|
|
646
|
+
list.appendChild(probe);
|
|
647
|
+
let max = 0;
|
|
648
|
+
for (const path of paths) {
|
|
649
|
+
probe.textContent = path || 'None';
|
|
650
|
+
max = Math.max(max, probe.offsetWidth);
|
|
651
|
+
}
|
|
652
|
+
probe.remove();
|
|
653
|
+
return max;
|
|
654
|
+
}
|
|
655
|
+
// Place a body-portaled popover now and again on every resize or scroll, so it
|
|
656
|
+
// keeps to its anchor. Scroll is captured because it doesn't bubble, and the
|
|
657
|
+
// anchor can sit in any scrolling ancestor. Returns the cleanup.
|
|
658
|
+
export function followAnchor(place) {
|
|
659
|
+
place();
|
|
660
|
+
window.addEventListener('resize', place);
|
|
661
|
+
window.addEventListener('scroll', place, true);
|
|
662
|
+
return () => {
|
|
663
|
+
window.removeEventListener('resize', place);
|
|
664
|
+
window.removeEventListener('scroll', place, true);
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
// File path field. Closed chrome shows the basename; the open list shows full
|
|
668
|
+
// paths. Callers pass the candidate paths — this does not decide what a sprite
|
|
669
|
+
// or sound is. `allowEmpty` adds a None row that writes ''.
|
|
670
|
+
export function FileField({ value, onChange, files = [], allowEmpty = false, ...row }) {
|
|
671
|
+
const current = value ?? '';
|
|
672
|
+
const [open, setOpen] = useState(false);
|
|
673
|
+
const buttonRef = useRef(null);
|
|
674
|
+
const listRef = useRef(null);
|
|
675
|
+
const [pos, setPos] = useState(null);
|
|
676
|
+
const paths = [];
|
|
677
|
+
if (allowEmpty) paths.push('');
|
|
678
|
+
for (const path of files) {
|
|
679
|
+
if (path && !paths.includes(path)) paths.push(path);
|
|
680
|
+
}
|
|
681
|
+
if (current && !paths.includes(current)) paths.splice(allowEmpty ? 1 : 0, 0, current);
|
|
682
|
+
useLayoutEffect(() => {
|
|
683
|
+
if (!open || !buttonRef.current) return undefined;
|
|
684
|
+
function place() {
|
|
685
|
+
const anchor = buttonRef.current.getBoundingClientRect();
|
|
686
|
+
const list = listRef.current;
|
|
687
|
+
const margin = 8;
|
|
688
|
+
const maxWidth = window.innerWidth - margin * 2;
|
|
689
|
+
const needed = longestPathWidth(list, paths) + pickerRowChrome(list) + 4;
|
|
690
|
+
const width = Math.min(Math.max(needed, anchor.width), maxWidth);
|
|
691
|
+
// Grow left from the field if the paths need more than the control width.
|
|
692
|
+
let left = anchor.right - width;
|
|
693
|
+
if (left < margin) left = margin;
|
|
694
|
+
if (left + width > window.innerWidth - margin) {
|
|
695
|
+
left = Math.max(margin, window.innerWidth - width - margin);
|
|
696
|
+
}
|
|
697
|
+
const gap = 4;
|
|
698
|
+
const height = list?.getBoundingClientRect().height ?? 0;
|
|
699
|
+
let top = anchor.bottom + gap;
|
|
700
|
+
if (height && top + height > window.innerHeight - margin) {
|
|
701
|
+
top = Math.max(margin, anchor.top - gap - height);
|
|
702
|
+
}
|
|
703
|
+
setPos({ top, left, width });
|
|
704
|
+
}
|
|
705
|
+
return followAnchor(place);
|
|
706
|
+
}, [open, paths.length]);
|
|
707
|
+
useEffect(() => {
|
|
708
|
+
if (!open) return undefined;
|
|
709
|
+
function onKeyDown(event) {
|
|
710
|
+
if (event.key === 'Escape') setOpen(false);
|
|
711
|
+
}
|
|
712
|
+
function onPointerDown(event) {
|
|
713
|
+
const target = event.target;
|
|
714
|
+
if (listRef.current?.contains(target) || buttonRef.current?.contains(target)) return;
|
|
715
|
+
setOpen(false);
|
|
716
|
+
}
|
|
717
|
+
window.addEventListener('keydown', onKeyDown);
|
|
718
|
+
window.addEventListener('pointerdown', onPointerDown);
|
|
719
|
+
return () => {
|
|
720
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
721
|
+
window.removeEventListener('pointerdown', onPointerDown);
|
|
722
|
+
};
|
|
723
|
+
}, [open]);
|
|
724
|
+
return (
|
|
725
|
+
<FieldRow
|
|
726
|
+
{...row}
|
|
727
|
+
defaultValue={row.defaultValue ? basenameOf(String(row.defaultValue)) : row.defaultValue}>
|
|
728
|
+
<button
|
|
729
|
+
ref={buttonRef}
|
|
730
|
+
type="button"
|
|
731
|
+
className={cx(styles.input, styles.select, styles.fileField)}
|
|
732
|
+
data-field="file"
|
|
733
|
+
aria-haspopup="listbox"
|
|
734
|
+
aria-expanded={open}
|
|
735
|
+
onClick={() => setOpen((next) => !next)}>
|
|
736
|
+
<span className={cx(styles.fileFieldLabel, !current && styles.fileFieldEmpty)}>
|
|
737
|
+
{current ? basenameOf(current) : 'None'}
|
|
738
|
+
</span>
|
|
739
|
+
</button>
|
|
740
|
+
{open ? (
|
|
741
|
+
<Overlay>
|
|
742
|
+
<div
|
|
743
|
+
ref={listRef}
|
|
744
|
+
className={styles.filePicker}
|
|
745
|
+
role="listbox"
|
|
746
|
+
aria-label={row.label}
|
|
747
|
+
style={pos ?? { visibility: 'hidden' }}>
|
|
748
|
+
{paths.map((path) => {
|
|
749
|
+
const selected = path === current;
|
|
750
|
+
return (
|
|
751
|
+
<button
|
|
752
|
+
key={path || '__none'}
|
|
753
|
+
type="button"
|
|
754
|
+
role="option"
|
|
755
|
+
aria-selected={selected}
|
|
756
|
+
className={cx(
|
|
757
|
+
styles.filePickerOption,
|
|
758
|
+
selected && styles.filePickerOptionSelected,
|
|
759
|
+
!path && styles.fileFieldEmpty
|
|
760
|
+
)}
|
|
761
|
+
title={path || 'None'}
|
|
762
|
+
onClick={() => {
|
|
763
|
+
onChange(path);
|
|
764
|
+
setOpen(false);
|
|
765
|
+
}}>
|
|
766
|
+
{path ? <FilePathLabel path={path} /> : 'None'}
|
|
767
|
+
</button>
|
|
768
|
+
);
|
|
769
|
+
})}
|
|
770
|
+
</div>
|
|
771
|
+
</Overlay>
|
|
772
|
+
) : null}
|
|
773
|
+
</FieldRow>
|
|
774
|
+
);
|
|
775
|
+
}
|
|
@@ -15,11 +15,14 @@
|
|
|
15
15
|
margin-bottom: 0;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/* `MAX_ENEMY_SPEED` is a single unbreakable token to the line breaker -- no
|
|
19
|
+
space, and an underscore is not a break opportunity -- so a long param name
|
|
20
|
+
has to be allowed to break mid-word or it overflows the label column. */
|
|
18
21
|
.fieldLabel {
|
|
19
22
|
color: var(--castle-inspector-text);
|
|
20
23
|
font-size: 14px;
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
min-width: 0;
|
|
25
|
+
overflow-wrap: anywhere;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/* Instance override: purple label + input border, plus a "Default: X [Reset]"
|
|
@@ -110,6 +113,18 @@
|
|
|
110
113
|
font: inherit;
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
/* Worn with .input, which carries the box; this is only what makes it a select:
|
|
117
|
+
the native chrome off and our own arrow in its place. */
|
|
118
|
+
.select {
|
|
119
|
+
appearance: none;
|
|
120
|
+
-webkit-appearance: none;
|
|
121
|
+
padding-right: 26px;
|
|
122
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5L6 8l3.5-3.5' fill='none' stroke='%23888888' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
123
|
+
background-repeat: no-repeat;
|
|
124
|
+
background-position: right 8px center;
|
|
125
|
+
text-overflow: ellipsis;
|
|
126
|
+
}
|
|
127
|
+
|
|
113
128
|
.numberField {
|
|
114
129
|
position: relative;
|
|
115
130
|
width: 100%;
|
|
@@ -156,6 +171,10 @@
|
|
|
156
171
|
font-variant-numeric: tabular-nums;
|
|
157
172
|
}
|
|
158
173
|
|
|
174
|
+
.unitSuffix {
|
|
175
|
+
color: var(--castle-inspector-muted);
|
|
176
|
+
}
|
|
177
|
+
|
|
159
178
|
.scrubGrip {
|
|
160
179
|
position: relative;
|
|
161
180
|
z-index: 1;
|
|
@@ -325,3 +344,63 @@
|
|
|
325
344
|
.checkboxOn .checkboxMark {
|
|
326
345
|
display: block;
|
|
327
346
|
}
|
|
347
|
+
|
|
348
|
+
/* The file picker's closed chrome wears .input and .select as well; this is the
|
|
349
|
+
part that makes it a button rather than a box with text in it. */
|
|
350
|
+
.fileField {
|
|
351
|
+
display: flex;
|
|
352
|
+
align-items: center;
|
|
353
|
+
font: inherit;
|
|
354
|
+
box-shadow: none;
|
|
355
|
+
cursor: pointer;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.fileFieldLabel {
|
|
359
|
+
min-width: 0;
|
|
360
|
+
overflow: hidden;
|
|
361
|
+
text-overflow: ellipsis;
|
|
362
|
+
white-space: nowrap;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.fileFieldEmpty {
|
|
366
|
+
color: var(--castle-inspector-muted);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.filePicker {
|
|
370
|
+
position: fixed;
|
|
371
|
+
z-index: 200;
|
|
372
|
+
box-sizing: border-box;
|
|
373
|
+
max-height: min(240px, calc(100vh - 16px));
|
|
374
|
+
overflow: auto;
|
|
375
|
+
padding: 4px;
|
|
376
|
+
border: 1px solid var(--castle-inspector-border);
|
|
377
|
+
border-radius: var(--castle-radius);
|
|
378
|
+
background: var(--castle-workspace-bg);
|
|
379
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.filePickerOption {
|
|
383
|
+
display: block;
|
|
384
|
+
width: 100%;
|
|
385
|
+
overflow: hidden;
|
|
386
|
+
text-align: left;
|
|
387
|
+
border: 0;
|
|
388
|
+
border-radius: 3px;
|
|
389
|
+
background: transparent;
|
|
390
|
+
color: var(--castle-inspector-text);
|
|
391
|
+
font: inherit;
|
|
392
|
+
font-size: 13px;
|
|
393
|
+
padding: 6px 8px;
|
|
394
|
+
cursor: pointer;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.filePickerPath {
|
|
398
|
+
display: block;
|
|
399
|
+
overflow: hidden;
|
|
400
|
+
white-space: nowrap;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.filePickerOption:hover,
|
|
404
|
+
.filePickerOptionSelected {
|
|
405
|
+
background: var(--castle-inspector-input-bg);
|
|
406
|
+
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
pickerPageIndexFor,
|
|
6
6
|
pickerPagesFor,
|
|
7
7
|
} from './palettes';
|
|
8
|
-
import { cx, FieldRow, followAnchor, IconButton, styles } from './ui';
|
|
8
|
+
import { cx, FieldRow, fieldStyles, followAnchor, IconButton, styles } from './ui';
|
|
9
9
|
import { attachDismissListeners } from './popoverDismiss';
|
|
10
10
|
|
|
11
11
|
export function sameHex(a, b) {
|
|
@@ -180,7 +180,12 @@ export function PaletteColorField({
|
|
|
180
180
|
<button
|
|
181
181
|
ref={buttonRef}
|
|
182
182
|
type="button"
|
|
183
|
-
className={cx(
|
|
183
|
+
className={cx(
|
|
184
|
+
fieldStyles.input,
|
|
185
|
+
fieldStyles.select,
|
|
186
|
+
fieldStyles.fileField,
|
|
187
|
+
styles.paletteColorField
|
|
188
|
+
)}
|
|
184
189
|
aria-haspopup="dialog"
|
|
185
190
|
aria-expanded={open}
|
|
186
191
|
onClick={() => setOpen((next) => !next)}>
|
|
@@ -200,7 +205,7 @@ export function PaletteColorField({
|
|
|
200
205
|
}
|
|
201
206
|
}
|
|
202
207
|
/>
|
|
203
|
-
<span className={cx(
|
|
208
|
+
<span className={cx(fieldStyles.fileFieldLabel, !current && fieldStyles.fileFieldEmpty)}>
|
|
204
209
|
{current || 'None'}
|
|
205
210
|
</span>
|
|
206
211
|
</button>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { ArtThumbnail } from './artThumbnail';
|
|
4
|
-
import { cx, FieldRow, followAnchor, Icon, styles } from './ui';
|
|
4
|
+
import { cx, FieldRow, fieldStyles, followAnchor, Icon, styles } from './ui';
|
|
5
5
|
import { attachDismissListeners } from './popoverDismiss';
|
|
6
6
|
|
|
7
7
|
// Visual sprite / image picker for inspector fields that point at art files.
|
|
@@ -124,12 +124,12 @@ export function SpriteField({
|
|
|
124
124
|
<button
|
|
125
125
|
ref={buttonRef}
|
|
126
126
|
type="button"
|
|
127
|
-
className={cx(
|
|
127
|
+
className={cx(fieldStyles.input, fieldStyles.select, fieldStyles.fileField, styles.spriteField)}
|
|
128
128
|
aria-haspopup="listbox"
|
|
129
129
|
aria-expanded={open}
|
|
130
130
|
onClick={() => setOpen((next) => !next)}>
|
|
131
131
|
<ArtThumbnail art={art} className={styles.spriteFieldThumb} />
|
|
132
|
-
<span className={cx(
|
|
132
|
+
<span className={cx(fieldStyles.fileFieldLabel, !current && fieldStyles.fileFieldEmpty)}>
|
|
133
133
|
{current ? name : 'None'}
|
|
134
134
|
</span>
|
|
135
135
|
</button>
|