castle-web-cli 0.4.181 → 0.4.182

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.
@@ -0,0 +1,327 @@
1
+ /* Styles for the field widgets beside this file. The canonical copy is
2
+ shared/fields/fields.module.css; scripts/sync-shared.mjs copies it into each
3
+ consumer of the widgets, and `check` fails when a copy drifts. Edit it there. */
4
+ .fieldRow {
5
+ display: grid;
6
+ grid-template-columns: minmax(82px, 0.5fr) minmax(0, 1fr);
7
+ align-items: center;
8
+ gap: 12px;
9
+ padding-bottom: 12px;
10
+ margin-bottom: 0;
11
+ min-height: 28px;
12
+ }
13
+
14
+ .fieldRow:last-child {
15
+ margin-bottom: 0;
16
+ }
17
+
18
+ .fieldLabel {
19
+ color: var(--castle-inspector-text);
20
+ font-size: 14px;
21
+ overflow: hidden;
22
+ text-overflow: ellipsis;
23
+ }
24
+
25
+ /* Instance override: purple label + input border, plus a "Default: X [Reset]"
26
+ sub-line rendered below the control (see FieldRow). */
27
+ .fieldLabelOverridden {
28
+ color: var(--castle-override);
29
+ }
30
+
31
+ /* A custom property rather than `.fieldRowOverridden .input`, so a control styled
32
+ in another CSS module (ui.module.css's .select and .textarea) takes the border
33
+ too -- a class selector can't reach a class from a different module. */
34
+ .fieldRowOverridden {
35
+ --field-border: var(--castle-override-border);
36
+ }
37
+
38
+ .fieldOverride {
39
+ padding-bottom: 12px;
40
+ }
41
+
42
+ .fieldOverride .fieldRow {
43
+ padding-bottom: 4px;
44
+ }
45
+
46
+ /* A muted line of explanation under a field, sitting in the control column the
47
+ same way the override sub-line above does. For a prop whose name can't carry
48
+ its own meaning -- see `hint` in a behavior's propertyMeta. */
49
+ .fieldNote {
50
+ display: grid;
51
+ grid-template-columns: minmax(82px, 0.5fr) minmax(0, 1fr);
52
+ gap: 12px;
53
+ margin-top: -6px;
54
+ padding-bottom: 12px;
55
+ }
56
+
57
+ .fieldNoteInner {
58
+ grid-column: 2;
59
+ min-width: 0;
60
+ font-size: 12px;
61
+ line-height: 1.35;
62
+ color: var(--castle-inspector-muted);
63
+ }
64
+
65
+ .fieldDefault {
66
+ display: grid;
67
+ grid-template-columns: minmax(82px, 0.5fr) minmax(0, 1fr);
68
+ gap: 12px;
69
+ }
70
+
71
+ .fieldDefaultInner {
72
+ grid-column: 2;
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 8px;
76
+ min-width: 0;
77
+ font-size: 12px;
78
+ color: var(--castle-inspector-muted);
79
+ }
80
+
81
+ .fieldDefaultInner > span {
82
+ min-width: 0;
83
+ overflow: hidden;
84
+ text-overflow: ellipsis;
85
+ white-space: nowrap;
86
+ }
87
+
88
+ .fieldResetBtn {
89
+ margin-left: auto;
90
+ border: 0;
91
+ padding: 0;
92
+ background: transparent;
93
+ color: var(--castle-override);
94
+ font-size: 12px;
95
+ cursor: pointer;
96
+ }
97
+
98
+ .fieldResetBtn:hover {
99
+ color: var(--castle-inspector-text);
100
+ }
101
+
102
+ .input {
103
+ width: 100%;
104
+ border: 1px solid var(--field-border, var(--castle-inspector-border));
105
+ border-radius: var(--castle-radius);
106
+ background: var(--castle-inspector-input-bg);
107
+ color: var(--castle-inspector-text);
108
+ padding: 5px 8px;
109
+ outline: 0;
110
+ font: inherit;
111
+ }
112
+
113
+ .numberField {
114
+ position: relative;
115
+ width: 100%;
116
+ }
117
+
118
+ .numberInput {
119
+ appearance: textfield;
120
+ -moz-appearance: textfield;
121
+ }
122
+
123
+ .numberInput::-webkit-inner-spin-button,
124
+ .numberInput::-webkit-outer-spin-button {
125
+ -webkit-appearance: none;
126
+ appearance: none;
127
+ margin: 0;
128
+ }
129
+
130
+ /* Number fields are drag scrubbers with a tap-to-open custom numpad (no native
131
+ keyboard). A bounded field (min AND max) is a slider with fill + thumb; an
132
+ unbounded one shows a grip and scrubs into negatives. touch-action: pan-y (set
133
+ inline) lets a vertical drag scroll the field list while horizontal scrubs. */
134
+ .numberScrub {
135
+ position: relative;
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 6px;
139
+ overflow: hidden;
140
+ padding: 5px 8px;
141
+ cursor: ew-resize;
142
+ user-select: none;
143
+ -webkit-user-select: none;
144
+ }
145
+
146
+ .numberSlider {
147
+ cursor: pointer;
148
+ }
149
+
150
+ .numberValue {
151
+ position: relative;
152
+ z-index: 1;
153
+ flex: 1 1 auto;
154
+ min-width: 0;
155
+ text-align: right;
156
+ font-variant-numeric: tabular-nums;
157
+ }
158
+
159
+ .scrubGrip {
160
+ position: relative;
161
+ z-index: 1;
162
+ flex: 0 0 auto;
163
+ color: var(--castle-inspector-muted);
164
+ font-size: 12px;
165
+ line-height: 1;
166
+ letter-spacing: -2px;
167
+ opacity: 0.4;
168
+ }
169
+
170
+ /* Value bubble that floats above the finger while scrubbing (the finger covers the
171
+ field, so the live number needs to show somewhere visible). */
172
+ .scrubBubble {
173
+ position: fixed;
174
+ z-index: 1001;
175
+ transform: translate(-50%, -100%);
176
+ padding: 3px 10px;
177
+ border-radius: 8px;
178
+ background: rgba(20, 24, 30, 0.96);
179
+ border: 1px solid var(--castle-inspector-border);
180
+ color: var(--castle-inspector-text);
181
+ font-size: 15px;
182
+ font-variant-numeric: tabular-nums;
183
+ pointer-events: none;
184
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
185
+ }
186
+
187
+ .sliderFill {
188
+ position: absolute;
189
+ left: 0;
190
+ top: 0;
191
+ bottom: 0;
192
+ background: rgba(90, 140, 210, 0.3);
193
+ pointer-events: none;
194
+ z-index: 0;
195
+ }
196
+
197
+ .sliderThumb {
198
+ position: absolute;
199
+ top: 50%;
200
+ width: 3px;
201
+ height: 60%;
202
+ border-radius: 2px;
203
+ background: rgba(150, 190, 240, 0.95);
204
+ transform: translate(-50%, -50%);
205
+ pointer-events: none;
206
+ z-index: 1;
207
+ }
208
+
209
+ /* Custom numeric pad, shown as a bottom sheet. */
210
+ .numpadBackdrop {
211
+ position: fixed;
212
+ inset: 0;
213
+ z-index: 1000;
214
+ display: flex;
215
+ flex-direction: column;
216
+ justify-content: flex-end;
217
+ background: rgba(0, 0, 0, 0.35);
218
+ }
219
+
220
+ .numpadSheet {
221
+ background: var(--castle-inspector-bg, #16181d);
222
+ border-top: 1px solid var(--castle-inspector-border);
223
+ border-radius: 14px 14px 0 0;
224
+ padding: 10px 10px calc(10px + env(safe-area-inset-bottom, 0px));
225
+ box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.4);
226
+ }
227
+
228
+ .numpadValueRow {
229
+ display: flex;
230
+ align-items: baseline;
231
+ justify-content: space-between;
232
+ gap: 12px;
233
+ padding: 6px 8px 12px;
234
+ }
235
+
236
+ /* Label and number are one value line, not a caption plus a value — same size
237
+ and color. A long label (e.g. "Friction Static") truncates rather than
238
+ pushing the number off, which is why the number never shrinks. */
239
+ .numpadValueLabel {
240
+ color: var(--castle-inspector-text);
241
+ font-size: 22px;
242
+ min-width: 0;
243
+ overflow: hidden;
244
+ text-overflow: ellipsis;
245
+ white-space: nowrap;
246
+ }
247
+
248
+ .numpadValueNum {
249
+ flex: 0 0 auto;
250
+ color: var(--castle-inspector-text);
251
+ font-size: 22px;
252
+ font-variant-numeric: tabular-nums;
253
+ }
254
+
255
+ .numpadGrid {
256
+ display: grid;
257
+ grid-template-columns: repeat(4, 1fr);
258
+ gap: 8px;
259
+ }
260
+
261
+ .numpadKey {
262
+ height: 52px;
263
+ border: 1px solid var(--castle-inspector-border);
264
+ border-radius: 10px;
265
+ background: var(--castle-inspector-input-bg);
266
+ color: var(--castle-inspector-text);
267
+ font-size: 20px;
268
+ cursor: pointer;
269
+ touch-action: manipulation;
270
+ }
271
+
272
+ .numpadKey:active {
273
+ background: var(--castle-inspector-divider);
274
+ }
275
+
276
+ .numpadKeyZero {
277
+ grid-column: span 3;
278
+ }
279
+
280
+ .numpadKeyDone {
281
+ background: rgba(90, 140, 210, 0.9);
282
+ border-color: transparent;
283
+ color: #fff;
284
+ font-size: 16px;
285
+ }
286
+
287
+ .colorInput {
288
+ width: 100%;
289
+ height: 28px;
290
+ border: 1px solid var(--field-border, var(--castle-inspector-border));
291
+ border-radius: var(--castle-radius);
292
+ background: var(--castle-inspector-input-bg);
293
+ padding: 2px;
294
+ outline: 0;
295
+ cursor: pointer;
296
+ }
297
+
298
+ .checkbox {
299
+ width: 20px;
300
+ height: 20px;
301
+ flex: 0 0 auto;
302
+ justify-self: end;
303
+ border: 1px solid var(--field-border, var(--castle-inspector-border));
304
+ border-radius: var(--castle-radius);
305
+ background: var(--castle-inspector-input-bg);
306
+ padding: 0;
307
+ cursor: pointer;
308
+ display: flex;
309
+ align-items: center;
310
+ justify-content: center;
311
+ box-shadow: none;
312
+ color: var(--castle-text);
313
+ }
314
+
315
+ .checkboxOn {
316
+ background: var(--castle-black);
317
+ }
318
+
319
+ .checkboxMark {
320
+ display: none;
321
+ width: 12px;
322
+ height: 12px;
323
+ }
324
+
325
+ .checkboxOn .checkboxMark {
326
+ display: block;
327
+ }
@@ -5,7 +5,7 @@ import {
5
5
  pickerPageIndexFor,
6
6
  pickerPagesFor,
7
7
  } from './palettes';
8
- import { cx, FieldRow, IconButton, styles } from './ui';
8
+ import { cx, FieldRow, followAnchor, IconButton, styles } from './ui';
9
9
  import { attachDismissListeners } from './popoverDismiss';
10
10
 
11
11
  export function sameHex(a, b) {
@@ -118,13 +118,7 @@ export function PalettePopover({ open, anchorRef, onClose, children }) {
118
118
  setPosition({ top, left });
119
119
  }
120
120
 
121
- positionPopover();
122
- window.addEventListener('resize', positionPopover);
123
- window.addEventListener('scroll', positionPopover, true);
124
- return () => {
125
- window.removeEventListener('resize', positionPopover);
126
- window.removeEventListener('scroll', positionPopover, true);
127
- };
121
+ return followAnchor(positionPopover);
128
122
  }, [open, anchorRef]);
129
123
 
130
124
  useEffect(() => {
@@ -33,6 +33,15 @@ function resolveSceneFileKey(name) {
33
33
  return candidates.find((candidate) => initialFiles[candidate] !== undefined) ?? key;
34
34
  }
35
35
 
36
+ // A behavior instance for one actor. `this.props` is what the constructor
37
+ // assigns; a behavior written without one would read `this.props` as undefined
38
+ // and throw on its first frame, so it is filled in here when missing.
39
+ function makeBehavior(Behavior, props) {
40
+ const instance = new Behavior(props);
41
+ if (instance.props === undefined) instance.props = props;
42
+ return instance;
43
+ }
44
+
36
45
  // `$enabled: false` turns a behavior off while keeping its props. Absent means
37
46
  // on, so it is only ever stored when false, and every existing scene reads the
38
47
  // same. `$` marks a prop the ENGINE owns -- behaviors must not define one.
@@ -371,7 +380,7 @@ export class SceneRuntime {
371
380
  if (!Behavior) return;
372
381
  ctx.save();
373
382
  ctx.globalAlpha *= 0.5;
374
- new Behavior(props).draw?.(actor, this, ctx, options);
383
+ makeBehavior(Behavior, props).draw?.(actor, this, ctx, options);
375
384
  ctx.restore();
376
385
  drawDisabledSpriteLabel(ctx, actor);
377
386
  }
@@ -389,7 +398,7 @@ export class SceneRuntime {
389
398
  const hook = on ? 'onEnable' : 'onDisable';
390
399
  // Constructed from the props it still has -- the whole point of
391
400
  // disabling is that they survive.
392
- new Behavior(props ?? {})[hook]?.(actor, this);
401
+ makeBehavior(Behavior, props ?? {})[hook]?.(actor, this);
393
402
  }
394
403
  }
395
404
 
@@ -422,7 +431,7 @@ export class SceneRuntime {
422
431
  if (!isBehaviorEnabled(props)) continue;
423
432
  const Behavior = this.behaviors.get(behaviorName);
424
433
  if (!Behavior) continue;
425
- callback(new Behavior(props));
434
+ callback(makeBehavior(Behavior, props));
426
435
  }
427
436
  }
428
437
 
@@ -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, Icon, styles } from './ui';
4
+ import { cx, FieldRow, 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.
@@ -56,16 +56,7 @@ function SpritePickerPopover({ paths, current, sprites, anchorRef, onPick, onClo
56
56
 
57
57
  useLayoutEffect(() => {
58
58
  if (!anchorRef.current || !listRef.current) return undefined;
59
- function place() {
60
- setPos(placePopover(anchorRef.current, listRef.current));
61
- }
62
- place();
63
- window.addEventListener('resize', place);
64
- window.addEventListener('scroll', place, true);
65
- return () => {
66
- window.removeEventListener('resize', place);
67
- window.removeEventListener('scroll', place, true);
68
- };
59
+ return followAnchor(() => setPos(placePopover(anchorRef.current, listRef.current)));
69
60
  }, [anchorRef, paths.length]);
70
61
 
71
62
  useEffect(() => {