castle-web-cli 0.4.108 → 0.4.110

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Castle Editor</title>
7
- <script type="module" crossorigin src="/__castle/ide/assets/index-BU9_JpPe.js"></script>
7
+ <script type="module" crossorigin src="/__castle/ide/assets/index-BK2M69q4.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/__castle/ide/assets/index-wU4ol--C.css">
9
9
  </head>
10
10
  <body>
@@ -53,5 +53,5 @@
53
53
  "deckId": "ckRZGFW4iPrx",
54
54
  "cardId": "_oBFbAW6DxsO",
55
55
  "title": "physics-2d",
56
- "publishedVersion": "2026-07-30T22:00:16.046Z"
56
+ "publishedVersion": "2026-08-03T21:51:41.748Z"
57
57
  }
@@ -67,22 +67,39 @@ export function useElementSize(ref) {
67
67
  }, [ref]);
68
68
  return size;
69
69
  }
70
- // True when the viewport is in the compact (≤600px) layout, where the file
71
- // list and inspector become bottom sheets. Mirrors the `@media (max-width:
72
- // 600px)` breakpoint so JS behavior (toggle target) tracks the CSS layout.
73
- export function useCompactViewport() {
74
- const query = '(max-width: 600px)';
75
- const [compact, setCompact] = useState(
70
+ // Live `matchMedia` as a hook: re-renders when the query starts/stops matching
71
+ // (plugging in a mouse, rotating, resizing a panel).
72
+ function useMediaQuery(query) {
73
+ const [matches, setMatches] = useState(
76
74
  () => typeof window !== 'undefined' && window.matchMedia(query).matches
77
75
  );
78
76
  useEffect(() => {
79
77
  const mql = window.matchMedia(query);
80
- const onChange = (event) => setCompact(event.matches);
78
+ const onChange = (event) => setMatches(event.matches);
81
79
  mql.addEventListener('change', onChange);
82
- setCompact(mql.matches);
80
+ setMatches(mql.matches);
83
81
  return () => mql.removeEventListener('change', onChange);
84
- }, []);
85
- return compact;
82
+ }, [query]);
83
+ return matches;
84
+ }
85
+ // True when the viewport is in the compact (≤600px) layout, where the file
86
+ // list and inspector become bottom sheets. Mirrors the `@media (max-width:
87
+ // 600px)` breakpoint so JS behavior (toggle target) tracks the CSS layout.
88
+ //
89
+ // NOTE: this is a question about SPACE, not about the input device. The editor
90
+ // renders inside an iframe whose viewport is the PANEL's width, so a narrow
91
+ // Flow card trips this on a desktop machine — which is correct for layout
92
+ // density and wrong for anything touch-shaped. For that, use useCoarsePointer.
93
+ export function useCompactViewport() {
94
+ return useMediaQuery('(max-width: 600px)');
95
+ }
96
+ // True when the primary pointer is a finger rather than a mouse/trackpad/stylus
97
+ // — i.e. the person can't reliably hit a small target, has no hover, and gets a
98
+ // native keyboard instead of a physical one. The right gate for touch-first
99
+ // affordances (the numpad sheet, big hit areas), since it stays false on a
100
+ // desktop no matter how narrow the panel gets.
101
+ export function useCoarsePointer() {
102
+ return useMediaQuery('(pointer: coarse)');
86
103
  }
87
104
  export function AppShell({ children }) {
88
105
  return <div className={styles.appShell}>{children}</div>;
@@ -378,13 +395,60 @@ const SCRUB_THRESHOLD = 12;
378
395
  const SCRUB_PX_PER_STEP = 12;
379
396
  const SCRUB_ACCEL_RANGE = 240;
380
397
 
398
+ // The mouse/trackpad answer to "I want to type an exact number": a real input in
399
+ // place of the scrub box, focused and selected so typing replaces the value.
400
+ // Enter/blur commits, Esc reverts. The numpad sheet exists because a phone has
401
+ // no minus key and we don't want the native keyboard covering the field — with a
402
+ // physical keyboard neither is a problem, so a fine pointer never sees it.
403
+ function NumberInlineInput({ value, min, max, step, onCommit, onDone }) {
404
+ const [draft, setDraft] = useState(() => numberText(value));
405
+ const cancelRef = useRef(false);
406
+ function finish(raw) {
407
+ if (cancelRef.current) return onDone();
408
+ const next = Number(raw);
409
+ onCommit(Number.isFinite(next) ? next : value);
410
+ onDone();
411
+ }
412
+ return (
413
+ <div className={styles.numberField}>
414
+ <input
415
+ className={cx(styles.input, styles.numberInput)}
416
+ type="number"
417
+ min={min}
418
+ max={max}
419
+ step={step}
420
+ value={draft}
421
+ autoFocus
422
+ onFocus={(event) => event.currentTarget.select()}
423
+ onChange={(event) => setDraft(event.target.value)}
424
+ onBlur={(event) => finish(event.target.value)}
425
+ onKeyDown={(event) => {
426
+ if (event.key === 'Enter') {
427
+ event.preventDefault();
428
+ event.currentTarget.blur();
429
+ } else if (event.key === 'Escape') {
430
+ event.preventDefault();
431
+ cancelRef.current = true;
432
+ event.currentTarget.blur();
433
+ }
434
+ }}
435
+ />
436
+ </div>
437
+ );
438
+ }
439
+
381
440
  export function NumberField({ label, value, onChange, min, max, step = 1, overridden, defaultValue, onReset }) {
382
441
  const current = Number.isFinite(value) ? (value ?? 0) : 0;
383
442
  // A field with BOTH a min and max is a true slider (fill + thumb), the pointer's X
384
443
  // mapped onto [min,max]. Everything else is a horizontal scrubber that can pass
385
444
  // into negatives, marked with a grip so it reads as draggable.
386
445
  const bounded = min != null && max != null && max > min;
446
+ // Drag-to-scrub is for every pointer, but what a plain CLICK/TAP opens differs:
447
+ // a finger gets the numpad sheet, a mouse gets an inline text input (see
448
+ // NumberInlineInput). Both are the "type an exact value" path for their device.
449
+ const coarse = useCoarsePointer();
387
450
  const [padOpen, setPadOpen] = useState(false);
451
+ const [typing, setTyping] = useState(false);
388
452
  // While scrubbing, a value bubble floats above the finger (which occludes the
389
453
  // field itself) — { x, y } client coords, or null when not scrubbing.
390
454
  const [scrubPos, setScrubPos] = useState(null);
@@ -401,7 +465,7 @@ export function NumberField({ label, value, onChange, min, max, step = 1, overri
401
465
  if (v !== current) onChange(v);
402
466
  }
403
467
  function onPointerDown(event) {
404
- if (padOpen) return;
468
+ if (padOpen || typing) return;
405
469
  if (event.pointerType === 'mouse' && event.button !== 0) return;
406
470
  // Don't capture or preventDefault yet: `touch-action: pan-y` lets the browser
407
471
  // own a VERTICAL drag (scroll the field list); we only claim a HORIZONTAL one.
@@ -447,7 +511,8 @@ export function NumberField({ label, value, onChange, min, max, step = 1, overri
447
511
  const wasTap = drag.mode === 'pending';
448
512
  dragRef.current = null;
449
513
  setScrubPos(null);
450
- if (wasTap) setPadOpen(true); // a plain tap opens the numpad
514
+ // A plain tap/click opens the device-appropriate way to type a value.
515
+ if (wasTap) (coarse ? setPadOpen : setTyping)(true);
451
516
  }
452
517
  // A pointercancel = the browser took the gesture (a vertical scroll under pan-y).
453
518
  // Just drop the drag; it must NOT be treated as a tap.
@@ -456,6 +521,24 @@ export function NumberField({ label, value, onChange, min, max, step = 1, overri
456
521
  setScrubPos(null);
457
522
  }
458
523
  const fillPct = bounded ? Math.min(100, Math.max(0, ((current - min) / (max - min)) * 100)) : 0;
524
+ const commit = (v) => {
525
+ const next = clampVal(v);
526
+ if (next !== current) onChange(next);
527
+ };
528
+ if (typing) {
529
+ return (
530
+ <FieldRow label={label} overridden={overridden} defaultValue={defaultValue} onReset={onReset}>
531
+ <NumberInlineInput
532
+ value={current}
533
+ min={min}
534
+ max={max}
535
+ step={step}
536
+ onCommit={commit}
537
+ onDone={() => setTyping(false)}
538
+ />
539
+ </FieldRow>
540
+ );
541
+ }
459
542
  return (
460
543
  <FieldRow label={label} overridden={overridden} defaultValue={defaultValue} onReset={onReset}>
461
544
  <div
@@ -496,9 +579,7 @@ export function NumberField({ label, value, onChange, min, max, step = 1, overri
496
579
  min={min}
497
580
  max={max}
498
581
  step={step}
499
- onCommit={(v) => {
500
- if (v !== current) onChange(v);
501
- }}
582
+ onCommit={commit}
502
583
  onClose={() => setPadOpen(false)}
503
584
  />
504
585
  ) : null}
@@ -393,6 +393,14 @@
393
393
  cursor: pointer;
394
394
  font-size: 14px;
395
395
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
396
+ /* Drag-to-place is a pointer gesture we implement ourselves (see
397
+ BlueprintLibrary's onSlotPointerDown). On a touch screen the browser claims
398
+ an unqualified gesture for scrolling and fires pointercancel the moment the
399
+ finger moves, so the drag died on contact -- the slot only ever registered
400
+ as a tap. `pan-x` hands the browser exactly the axis the hotbar scrolls on
401
+ and leaves the other to us: swipe sideways to scroll through blueprints,
402
+ drag up toward the stage to place one. */
403
+ touch-action: pan-x;
396
404
  }
397
405
 
398
406
  .bpSlotBadge {
@@ -1302,12 +1310,20 @@
1302
1310
  padding: 6px 8px 12px;
1303
1311
  }
1304
1312
 
1313
+ /* Label and number are one value line, not a caption plus a value — same size
1314
+ and color. A long label (e.g. "Friction Static") truncates rather than
1315
+ pushing the number off, which is why the number never shrinks. */
1305
1316
  .numpadValueLabel {
1306
- color: var(--castle-inspector-muted);
1307
- font-size: 13px;
1317
+ color: var(--castle-inspector-text);
1318
+ font-size: 22px;
1319
+ min-width: 0;
1320
+ overflow: hidden;
1321
+ text-overflow: ellipsis;
1322
+ white-space: nowrap;
1308
1323
  }
1309
1324
 
1310
1325
  .numpadValueNum {
1326
+ flex: 0 0 auto;
1311
1327
  color: var(--castle-inspector-text);
1312
1328
  font-size: 22px;
1313
1329
  font-variant-numeric: tabular-nums;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.108",
3
+ "version": "0.4.110",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"