castle-web-cli 0.4.129 → 0.4.130
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 +4 -4
- package/dist/filesChanged.d.ts +2 -0
- package/dist/filesChanged.js +6 -2
- package/dist/ide.js +15 -4
- package/dist/importBrowse.js +1 -0
- package/dist/index.js +10 -4
- package/dist/init.js +63 -15
- package/dist/native/loop.js +1 -1
- package/dist/native/tools.js +1 -1
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +20 -1
- package/dist/shell/assets/index-BpOLUyVO.js +441 -0
- package/dist/shell/assets/{index-CAq6f9B5.css → index-D6K-0YDB.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/kits/physics-2d/CLAUDE.md +9 -10
- package/kits/physics-2d/castle.json +1 -1
- package/kits/physics-2d/editors/PlayOnly.jsx +15 -6
- package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
- package/kits/physics-2d/editors/SceneEditor.jsx +5 -5
- package/kits/physics-2d/editors/SingleEditor.jsx +37 -8
- package/kits/physics-2d/editors/StyleEditor.jsx +7 -4
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +7 -7
- package/kits/physics-2d/editors/pixelInspector.jsx +13 -7
- package/kits/physics-2d/editors/pxArtTimeline.jsx +29 -11
- package/kits/physics-2d/editors/pxArtTimeline.module.css +11 -0
- package/kits/physics-2d/engine/blueprint.js +15 -3
- package/kits/physics-2d/engine/liveReload.js +33 -21
- package/kits/physics-2d/engine/systemRegistry.js +1 -1
- package/kits/physics-2d/engine/ui.jsx +2 -2
- package/kits/physics-2d/engine/ui.module.css +42 -0
- package/kits/physics-3d/CLAUDE.md +109 -0
- package/kits/physics-3d/behaviors/Body.jsx +51 -0
- package/kits/physics-3d/behaviors/Door.jsx +31 -0
- package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
- package/kits/physics-3d/behaviors/Model.jsx +14 -0
- package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
- package/kits/physics-3d/behaviors/Player.jsx +172 -0
- package/kits/physics-3d/behaviors/Portal.jsx +19 -0
- package/kits/physics-3d/behaviors/Shape.jsx +75 -0
- package/kits/physics-3d/behaviors/Solid.jsx +11 -0
- package/kits/physics-3d/behaviors/Transform.jsx +24 -0
- package/kits/physics-3d/blueprints/barrel.scene +15 -0
- package/kits/physics-3d/blueprints/crate.scene +29 -0
- package/kits/physics-3d/blueprints/door.scene +35 -0
- package/kits/physics-3d/blueprints/gem.scene +26 -0
- package/kits/physics-3d/blueprints/pillar.scene +25 -0
- package/kits/physics-3d/blueprints/platform.scene +23 -0
- package/kits/physics-3d/blueprints/player.scene +29 -0
- package/kits/physics-3d/blueprints/portal.scene +21 -0
- package/kits/physics-3d/blueprints/rock.scene +14 -0
- package/kits/physics-3d/blueprints/statue.scene +26 -0
- package/kits/physics-3d/blueprints/tree.scene +14 -0
- package/kits/physics-3d/blueprints/wall.scene +26 -0
- package/kits/{basic-2d → physics-3d}/castle.json +23 -5
- package/kits/physics-3d/docs/pxmodel-format.md +111 -0
- package/kits/physics-3d/drawings/crate.pxart +26 -0
- package/kits/physics-3d/drawings/door-edge.pxart +26 -0
- package/kits/physics-3d/drawings/door.pxart +27 -0
- package/kits/physics-3d/drawings/face.pxart +26 -0
- package/kits/physics-3d/drawings/floor.pxart +27 -0
- package/kits/physics-3d/drawings/platform.pxart +27 -0
- package/kits/physics-3d/drawings/statue.pxart +27 -0
- package/kits/physics-3d/drawings/wall.pxart +27 -0
- package/kits/physics-3d/engine3d/PlayOnly3D.jsx +32 -0
- package/kits/physics-3d/engine3d/PxModelEditor.jsx +619 -0
- package/kits/physics-3d/engine3d/Scene3DEditor.jsx +618 -0
- package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +309 -0
- package/kits/physics-3d/engine3d/editor3dData.js +114 -0
- package/kits/physics-3d/engine3d/editor3dInspector.jsx +404 -0
- package/kits/physics-3d/engine3d/editorChrome.jsx +218 -0
- package/kits/physics-3d/engine3d/editorWorld.js +249 -0
- package/kits/physics-3d/engine3d/materials.js +174 -0
- package/kits/physics-3d/engine3d/meshops.js +123 -0
- package/kits/physics-3d/engine3d/modelEditorWorld.js +439 -0
- package/kits/physics-3d/engine3d/modelFiles.js +139 -0
- package/kits/physics-3d/engine3d/pxmodel.js +265 -0
- package/kits/physics-3d/engine3d/thumbnails.js +137 -0
- package/kits/physics-3d/engine3d/world3d.js +216 -0
- package/kits/{basic-2d → physics-3d}/eslint.config.js +10 -25
- package/kits/physics-3d/index.html +20 -0
- package/kits/physics-3d/main.jsx +30 -0
- package/kits/physics-3d/models/barrel.pxmodel +10 -0
- package/kits/physics-3d/models/player.pxmodel +222 -0
- package/kits/physics-3d/models/rock.pxmodel +30 -0
- package/kits/physics-3d/models/tree.pxmodel +10 -0
- package/kits/{basic-2d → physics-3d}/package-lock.json +140 -119
- package/kits/{basic-2d → physics-3d}/package.json +11 -9
- package/kits/{basic-2d → physics-3d}/pnpm-lock.yaml +162 -137
- package/kits/physics-3d/scenes/main.scene +414 -0
- package/kits/physics-3d/scenes/model-lab.scene +127 -0
- package/kits/physics-3d/systems/physics3d.js +356 -0
- package/package.json +5 -2
- package/dist/shell/assets/index-DKu9ejyh.js +0 -436
- package/kits/basic-2d/CLAUDE.md +0 -221
- package/kits/basic-2d/behaviors/Camera.jsx +0 -43
- package/kits/basic-2d/behaviors/Collider.jsx +0 -213
- package/kits/basic-2d/behaviors/Layout.jsx +0 -53
- package/kits/basic-2d/behaviors/Sprite.jsx +0 -357
- package/kits/basic-2d/behaviors/tint.js +0 -47
- package/kits/basic-2d/blueprints/cauldron.scene +0 -20
- package/kits/basic-2d/docs/pxart-format.md +0 -377
- package/kits/basic-2d/drawings/cauldron.pxart +0 -113
- package/kits/basic-2d/editors/BlueprintLibrary.jsx +0 -270
- package/kits/basic-2d/editors/ErrorBoundary.jsx +0 -59
- package/kits/basic-2d/editors/PlayOnly.jsx +0 -31
- package/kits/basic-2d/editors/PxArtEditor.jsx +0 -1092
- package/kits/basic-2d/editors/SceneEditor.jsx +0 -1780
- package/kits/basic-2d/editors/SelectionOverlay.jsx +0 -909
- package/kits/basic-2d/editors/SingleEditor.jsx +0 -122
- package/kits/basic-2d/editors/behaviorRegistry.js +0 -34
- package/kits/basic-2d/editors/editorHistory.js +0 -157
- package/kits/basic-2d/editors/inspectorSheet.js +0 -13
- package/kits/basic-2d/editors/pixelCanvas.js +0 -11
- package/kits/basic-2d/editors/pixelEditorChrome.jsx +0 -74
- package/kits/basic-2d/editors/pixelGeometry.js +0 -140
- package/kits/basic-2d/editors/pixelInspector.jsx +0 -633
- package/kits/basic-2d/editors/pxArtEditorModel.js +0 -732
- package/kits/basic-2d/editors/pxArtPlayback.js +0 -92
- package/kits/basic-2d/editors/pxArtTimeline.jsx +0 -752
- package/kits/basic-2d/editors/pxArtTimeline.module.css +0 -506
- package/kits/basic-2d/editors/pxArtTools.js +0 -232
- package/kits/basic-2d/editors/useArtboardFit.js +0 -105
- package/kits/basic-2d/engine/ScenePlayer.jsx +0 -209
- package/kits/basic-2d/engine/SceneUI.jsx +0 -59
- package/kits/basic-2d/engine/assets.js +0 -15
- package/kits/basic-2d/engine/autoInspector.jsx +0 -70
- package/kits/basic-2d/engine/behaviorExtensions.js +0 -32
- package/kits/basic-2d/engine/blueprint.js +0 -557
- package/kits/basic-2d/engine/collider.js +0 -200
- package/kits/basic-2d/engine/files.js +0 -141
- package/kits/basic-2d/engine/liveReload.js +0 -88
- package/kits/basic-2d/engine/pxart.js +0 -1032
- package/kits/basic-2d/engine/pxartSmooth.js +0 -222
- package/kits/basic-2d/engine/scene.js +0 -696
- package/kits/basic-2d/engine/spriteGeometry.js +0 -32
- package/kits/basic-2d/engine/systemRegistry.js +0 -16
- package/kits/basic-2d/engine/ui.jsx +0 -695
- package/kits/basic-2d/engine/ui.module.css +0 -2287
- package/kits/basic-2d/index.html +0 -24
- package/kits/basic-2d/main.jsx +0 -24
- package/kits/basic-2d/scenes/main.scene +0 -16
- package/kits/basic-2d/scripts/draw.mjs +0 -121
- /package/kits/{basic-2d → physics-3d}/.prettierrc +0 -0
- /package/kits/{basic-2d → physics-3d}/vite.config.js +0 -0
|
@@ -1,695 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
-
import { createPortal } from 'react-dom';
|
|
3
|
-
import {
|
|
4
|
-
faArrowsAlt,
|
|
5
|
-
faArrowsAltH,
|
|
6
|
-
faArrowsAltV,
|
|
7
|
-
faBars,
|
|
8
|
-
faChevronDown,
|
|
9
|
-
faChevronRight,
|
|
10
|
-
faChevronUp,
|
|
11
|
-
faCircle,
|
|
12
|
-
faClone,
|
|
13
|
-
faCode,
|
|
14
|
-
faCodeBranch,
|
|
15
|
-
faEraser,
|
|
16
|
-
faExpand,
|
|
17
|
-
faExternalLinkAlt,
|
|
18
|
-
faEyeDropper,
|
|
19
|
-
faFile,
|
|
20
|
-
faFilm,
|
|
21
|
-
faFillDrip,
|
|
22
|
-
faGlobe,
|
|
23
|
-
faImage,
|
|
24
|
-
faLayerGroup,
|
|
25
|
-
faObjectGroup,
|
|
26
|
-
faPalette,
|
|
27
|
-
faPencilAlt,
|
|
28
|
-
faPlay,
|
|
29
|
-
faPlus,
|
|
30
|
-
faRedo,
|
|
31
|
-
faSearchMinus,
|
|
32
|
-
faSearchPlus,
|
|
33
|
-
faShapes,
|
|
34
|
-
faSlash,
|
|
35
|
-
faVectorSquare,
|
|
36
|
-
faSquare,
|
|
37
|
-
faStamp,
|
|
38
|
-
faStop,
|
|
39
|
-
faSyncAlt,
|
|
40
|
-
faTimes,
|
|
41
|
-
faTrash,
|
|
42
|
-
faUndo,
|
|
43
|
-
faVideo,
|
|
44
|
-
} from '@fortawesome/free-solid-svg-icons';
|
|
45
|
-
import styles from './ui.module.css';
|
|
46
|
-
export { styles };
|
|
47
|
-
export const theme = {
|
|
48
|
-
transparentChecker:
|
|
49
|
-
'repeating-linear-gradient(45deg, #121212, #121212 4px, #1a1a1a 4px, #1a1a1a 8px)',
|
|
50
|
-
};
|
|
51
|
-
export function cx(...parts) {
|
|
52
|
-
return parts.filter(Boolean).join(' ');
|
|
53
|
-
}
|
|
54
|
-
// Track an element's content box via ResizeObserver. Shared by the canvas
|
|
55
|
-
// overlays (SceneUI, SelectionOverlay) that scale a card-unit layer onto the
|
|
56
|
-
// live canvas size.
|
|
57
|
-
export function useElementSize(ref) {
|
|
58
|
-
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
const node = ref.current;
|
|
61
|
-
if (!node) return undefined;
|
|
62
|
-
const measure = () => setSize({ width: node.clientWidth, height: node.clientHeight });
|
|
63
|
-
const observer = new ResizeObserver(measure);
|
|
64
|
-
observer.observe(node);
|
|
65
|
-
measure();
|
|
66
|
-
return () => observer.disconnect();
|
|
67
|
-
}, [ref]);
|
|
68
|
-
return size;
|
|
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(
|
|
76
|
-
() => typeof window !== 'undefined' && window.matchMedia(query).matches
|
|
77
|
-
);
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
const mql = window.matchMedia(query);
|
|
80
|
-
const onChange = (event) => setCompact(event.matches);
|
|
81
|
-
mql.addEventListener('change', onChange);
|
|
82
|
-
setCompact(mql.matches);
|
|
83
|
-
return () => mql.removeEventListener('change', onChange);
|
|
84
|
-
}, []);
|
|
85
|
-
return compact;
|
|
86
|
-
}
|
|
87
|
-
export function AppShell({ children }) {
|
|
88
|
-
return <div className={styles.appShell}>{children}</div>;
|
|
89
|
-
}
|
|
90
|
-
export function MainEditor({ children }) {
|
|
91
|
-
return <main className={styles.mainEditor}>{children}</main>;
|
|
92
|
-
}
|
|
93
|
-
// The header is a full-width row spanning the file list + workspace. Each
|
|
94
|
-
// editor still owns its header content (title/subtitle/right controls depend
|
|
95
|
-
// on per-editor state), so EditorHeader portals its DOM into the editor-root
|
|
96
|
-
// header slot (`headerHost`) that App renders above the `[files | workspace]`
|
|
97
|
-
// grid. The files toggle lives on the left and works on every viewport.
|
|
98
|
-
export function EditorHeader({ title, subtitle, right, onToggleFiles, filesOpen, headerHost }) {
|
|
99
|
-
const header = (
|
|
100
|
-
<header className={styles.editorHeader}>
|
|
101
|
-
<div className={styles.editorHeaderLeft}>
|
|
102
|
-
{onToggleFiles ? (
|
|
103
|
-
<IconButton
|
|
104
|
-
icon="bars"
|
|
105
|
-
label="Files"
|
|
106
|
-
title="Toggle file list"
|
|
107
|
-
active={filesOpen}
|
|
108
|
-
aria-pressed={filesOpen ? 'true' : 'false'}
|
|
109
|
-
onClick={onToggleFiles}
|
|
110
|
-
/>
|
|
111
|
-
) : null}
|
|
112
|
-
<div>
|
|
113
|
-
<div className={styles.editorTitle}>{title}</div>
|
|
114
|
-
{subtitle ? <div className={styles.muted}>{subtitle}</div> : null}
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
{right ? <div className={styles.editorHeaderRight}>{right}</div> : null}
|
|
118
|
-
</header>
|
|
119
|
-
);
|
|
120
|
-
if (!headerHost) return null;
|
|
121
|
-
return createPortal(header, headerHost);
|
|
122
|
-
}
|
|
123
|
-
// Safari only moves keyboard focus into an iframe -- at the browser level,
|
|
124
|
-
// not just document.activeElement -- when the click lands on something
|
|
125
|
-
// Safari itself focuses on click (a text input; NOT a button, div, or
|
|
126
|
-
// canvas). PxArtEditor/SceneEditor each already focus their own canvas on
|
|
127
|
-
// pointer-down for exactly this reason, but that only covers clicks that
|
|
128
|
-
// land on the canvas: clicking a tool button, a palette swatch, the
|
|
129
|
-
// timeline, etc. does nothing, so Safari's frame focus can stay wherever it
|
|
130
|
-
// was and the NEXT keyboard shortcut or Cmd+Z falls through to the browser
|
|
131
|
-
// -- reproducing right after the user just clicked in the editor. Claiming
|
|
132
|
-
// focus here, once, for every pointer-down anywhere in the editor body
|
|
133
|
-
// closes that gap for both editors without duplicating the fix. Real text
|
|
134
|
-
// inputs still win focus normally right after: the browser's own
|
|
135
|
-
// click-focuses-inputs behavior runs as mousedown's default action, which
|
|
136
|
-
// fires AFTER this pointerdown handler.
|
|
137
|
-
function claimEditorFocus(event) {
|
|
138
|
-
event.currentTarget.focus({ preventScroll: true });
|
|
139
|
-
}
|
|
140
|
-
export const EditorBody = React.forwardRef(function EditorBody({ children }, ref) {
|
|
141
|
-
return (
|
|
142
|
-
<div ref={ref} className={styles.editorBody} tabIndex={-1} onPointerDown={claimEditorFocus}>
|
|
143
|
-
{children}
|
|
144
|
-
</div>
|
|
145
|
-
);
|
|
146
|
-
});
|
|
147
|
-
export function Button({ children, active = false, variant = '', className = '', ...props }) {
|
|
148
|
-
return (
|
|
149
|
-
<button
|
|
150
|
-
className={cx(
|
|
151
|
-
styles.button,
|
|
152
|
-
active && styles.buttonActive,
|
|
153
|
-
variant === 'danger' && styles.buttonDanger,
|
|
154
|
-
className
|
|
155
|
-
)}
|
|
156
|
-
{...props}>
|
|
157
|
-
{children}
|
|
158
|
-
</button>
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
export function IconButton({ icon, label, active = false, variant = '', ...props }) {
|
|
162
|
-
return (
|
|
163
|
-
<Button
|
|
164
|
-
active={active}
|
|
165
|
-
variant={variant}
|
|
166
|
-
className={styles.iconButton}
|
|
167
|
-
aria-label={label}
|
|
168
|
-
title={label}
|
|
169
|
-
{...props}>
|
|
170
|
-
<Icon name={icon} />
|
|
171
|
-
</Button>
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
const icons = {
|
|
175
|
-
bars: faBars,
|
|
176
|
-
camera: faVideo,
|
|
177
|
-
'chevron-down': faChevronDown,
|
|
178
|
-
'chevron-right': faChevronRight,
|
|
179
|
-
'chevron-up': faChevronUp,
|
|
180
|
-
circle: faCircle,
|
|
181
|
-
clone: faClone,
|
|
182
|
-
code: faCode,
|
|
183
|
-
// FA5 has no faCodeFork (that's the FA6 name); faCodeBranch is the fork glyph.
|
|
184
|
-
'code-fork': faCodeBranch,
|
|
185
|
-
eraser: faEraser,
|
|
186
|
-
expand: faExpand,
|
|
187
|
-
external: faExternalLinkAlt,
|
|
188
|
-
eyedropper: faEyeDropper,
|
|
189
|
-
fill: faFillDrip,
|
|
190
|
-
'flip-h': faArrowsAltH,
|
|
191
|
-
'flip-v': faArrowsAltV,
|
|
192
|
-
file: faFile,
|
|
193
|
-
film: faFilm,
|
|
194
|
-
globe: faGlobe,
|
|
195
|
-
image: faImage,
|
|
196
|
-
'layer-group': faLayerGroup,
|
|
197
|
-
marquee: faVectorSquare,
|
|
198
|
-
move: faArrowsAlt,
|
|
199
|
-
'object-group': faObjectGroup,
|
|
200
|
-
palette: faPalette,
|
|
201
|
-
pencil: faPencilAlt,
|
|
202
|
-
play: faPlay,
|
|
203
|
-
plus: faPlus,
|
|
204
|
-
redo: faRedo,
|
|
205
|
-
rotate: faSyncAlt,
|
|
206
|
-
'zoom-in': faSearchPlus,
|
|
207
|
-
'zoom-out': faSearchMinus,
|
|
208
|
-
fit: faExpand,
|
|
209
|
-
shapes: faShapes,
|
|
210
|
-
slash: faSlash,
|
|
211
|
-
square: faSquare,
|
|
212
|
-
stamp: faStamp,
|
|
213
|
-
stop: faStop,
|
|
214
|
-
times: faTimes,
|
|
215
|
-
// Hand-rolled equilateral-ish triangle glyph (FA free-solid has no plain
|
|
216
|
-
// triangle); shaped to match the filled-glyph look of the imported icons.
|
|
217
|
-
triangle: { icon: [512, 512, [], '', 'M256 64 L496 448 L16 448 Z'] },
|
|
218
|
-
trash: faTrash,
|
|
219
|
-
undo: faUndo,
|
|
220
|
-
};
|
|
221
|
-
export function Icon({ name }) {
|
|
222
|
-
const def = icons[name];
|
|
223
|
-
if (!def) return null;
|
|
224
|
-
const [width, height, , , path] = def.icon;
|
|
225
|
-
const d = Array.isArray(path) ? path.join(' ') : path;
|
|
226
|
-
return (
|
|
227
|
-
<svg
|
|
228
|
-
viewBox={`0 0 ${width} ${height}`}
|
|
229
|
-
aria-hidden="true"
|
|
230
|
-
style={{ width: '1em', height: '1em', display: 'inline-block', fill: 'currentColor' }}>
|
|
231
|
-
<path d={d} />
|
|
232
|
-
</svg>
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
export function Panel({ title, action, children, overridden = false }) {
|
|
236
|
-
return (
|
|
237
|
-
<section className={styles.panel}>
|
|
238
|
-
{title ? (
|
|
239
|
-
<div className={styles.panelHeader}>
|
|
240
|
-
<span className={cx(overridden && styles.panelTitleOverridden)}>{title}</span>
|
|
241
|
-
{action}
|
|
242
|
-
</div>
|
|
243
|
-
) : null}
|
|
244
|
-
<div className={styles.panelBody}>{children}</div>
|
|
245
|
-
</section>
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
// Render the "Default: X" value in an instance override's sub-line.
|
|
249
|
-
function formatFieldDefault(value) {
|
|
250
|
-
if (typeof value === 'boolean') return value ? 'On' : 'Off';
|
|
251
|
-
if (value == null || value === '') return 'none';
|
|
252
|
-
return String(value);
|
|
253
|
-
}
|
|
254
|
-
export function FieldRow({ label, overridden, defaultValue, onReset, children }) {
|
|
255
|
-
const row = (
|
|
256
|
-
<label className={cx(styles.fieldRow, overridden && styles.fieldRowOverridden)}>
|
|
257
|
-
<span className={cx(styles.fieldLabel, overridden && styles.fieldLabelOverridden)}>{label}</span>
|
|
258
|
-
{children}
|
|
259
|
-
</label>
|
|
260
|
-
);
|
|
261
|
-
if (!overridden) return row;
|
|
262
|
-
return (
|
|
263
|
-
<div className={styles.fieldOverride}>
|
|
264
|
-
{row}
|
|
265
|
-
<div className={styles.fieldDefault}>
|
|
266
|
-
<div className={styles.fieldDefaultInner}>
|
|
267
|
-
<span>Default: {formatFieldDefault(defaultValue)}</span>
|
|
268
|
-
{onReset ? (
|
|
269
|
-
<button type="button" className={styles.fieldResetBtn} onClick={onReset}>
|
|
270
|
-
Reset
|
|
271
|
-
</button>
|
|
272
|
-
) : null}
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
275
|
-
</div>
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
export function TextField({ label, value, onChange, overridden, defaultValue, onReset }) {
|
|
279
|
-
return (
|
|
280
|
-
<FieldRow label={label} overridden={overridden} defaultValue={defaultValue} onReset={onReset}>
|
|
281
|
-
<input
|
|
282
|
-
className={styles.input}
|
|
283
|
-
value={value ?? ''}
|
|
284
|
-
onChange={(event) => onChange(event.target.value)}
|
|
285
|
-
/>
|
|
286
|
-
</FieldRow>
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
export function NumberField({ label, value, onChange, min, max, step = 1, overridden, defaultValue, onReset }) {
|
|
290
|
-
const current = Number.isFinite(value) ? (value ?? 0) : 0;
|
|
291
|
-
const [draft, setDraft] = useState(String(current));
|
|
292
|
-
const editingRef = useRef(false);
|
|
293
|
-
const cancelRef = useRef(false);
|
|
294
|
-
// keep the draft in sync with the committed value while not editing
|
|
295
|
-
if (!editingRef.current && draft !== String(current)) {
|
|
296
|
-
setDraft(String(current));
|
|
297
|
-
}
|
|
298
|
-
function commit(raw) {
|
|
299
|
-
let next = Number(raw);
|
|
300
|
-
if (!Number.isFinite(next)) next = current;
|
|
301
|
-
if (min != null && next < min) next = min;
|
|
302
|
-
if (max != null && next > max) next = max;
|
|
303
|
-
setDraft(String(next));
|
|
304
|
-
onChange(next);
|
|
305
|
-
}
|
|
306
|
-
function stepBy(delta) {
|
|
307
|
-
let next = current + delta;
|
|
308
|
-
if (min != null && next < min) next = min;
|
|
309
|
-
if (max != null && next > max) next = max;
|
|
310
|
-
setDraft(String(next));
|
|
311
|
-
onChange(next);
|
|
312
|
-
}
|
|
313
|
-
return (
|
|
314
|
-
<FieldRow label={label} overridden={overridden} defaultValue={defaultValue} onReset={onReset}>
|
|
315
|
-
<div className={styles.numberField}>
|
|
316
|
-
<input
|
|
317
|
-
className={cx(styles.input, styles.numberInput)}
|
|
318
|
-
type="number"
|
|
319
|
-
min={min}
|
|
320
|
-
max={max}
|
|
321
|
-
step={step}
|
|
322
|
-
value={draft}
|
|
323
|
-
onFocus={() => {
|
|
324
|
-
editingRef.current = true;
|
|
325
|
-
}}
|
|
326
|
-
onChange={(event) => setDraft(event.target.value)}
|
|
327
|
-
onBlur={(event) => {
|
|
328
|
-
editingRef.current = false;
|
|
329
|
-
if (cancelRef.current) {
|
|
330
|
-
cancelRef.current = false;
|
|
331
|
-
setDraft(String(current));
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
commit(event.target.value);
|
|
335
|
-
}}
|
|
336
|
-
onKeyDown={(event) => {
|
|
337
|
-
if (event.key === 'Enter') {
|
|
338
|
-
event.preventDefault();
|
|
339
|
-
commit(event.currentTarget.value);
|
|
340
|
-
event.currentTarget.blur();
|
|
341
|
-
} else if (event.key === 'Escape') {
|
|
342
|
-
event.preventDefault();
|
|
343
|
-
cancelRef.current = true;
|
|
344
|
-
setDraft(String(current));
|
|
345
|
-
event.currentTarget.blur();
|
|
346
|
-
}
|
|
347
|
-
}}
|
|
348
|
-
/>
|
|
349
|
-
<div className={styles.numberSteppers}>
|
|
350
|
-
<button
|
|
351
|
-
type="button"
|
|
352
|
-
className={styles.numberStepper}
|
|
353
|
-
tabIndex={-1}
|
|
354
|
-
aria-label="Increment"
|
|
355
|
-
disabled={max != null && current >= max}
|
|
356
|
-
onClick={() => stepBy(step)}>
|
|
357
|
-
<Icon name="chevron-up" />
|
|
358
|
-
</button>
|
|
359
|
-
<button
|
|
360
|
-
type="button"
|
|
361
|
-
className={styles.numberStepper}
|
|
362
|
-
tabIndex={-1}
|
|
363
|
-
aria-label="Decrement"
|
|
364
|
-
disabled={min != null && current <= min}
|
|
365
|
-
onClick={() => stepBy(-step)}>
|
|
366
|
-
<Icon name="chevron-down" />
|
|
367
|
-
</button>
|
|
368
|
-
</div>
|
|
369
|
-
</div>
|
|
370
|
-
</FieldRow>
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
export function CheckboxField({ label, checked, onChange, overridden, defaultValue, onReset }) {
|
|
374
|
-
return (
|
|
375
|
-
<FieldRow label={label} overridden={overridden} defaultValue={defaultValue} onReset={onReset}>
|
|
376
|
-
<button
|
|
377
|
-
type="button"
|
|
378
|
-
className={cx(styles.toggle, checked && styles.toggleOn)}
|
|
379
|
-
role="switch"
|
|
380
|
-
aria-checked={!!checked}
|
|
381
|
-
onClick={() => onChange(!checked)}>
|
|
382
|
-
<span className={styles.toggleKnob} />
|
|
383
|
-
</button>
|
|
384
|
-
</FieldRow>
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
export function ColorField({ label, value, onChange, overridden, defaultValue, onReset }) {
|
|
388
|
-
const hex = normalizeHex(value);
|
|
389
|
-
const alpha = hex.length === 9 ? hex.slice(7) : '';
|
|
390
|
-
const rgb = hex.slice(0, 7);
|
|
391
|
-
return (
|
|
392
|
-
<FieldRow label={label} overridden={overridden} defaultValue={defaultValue} onReset={onReset}>
|
|
393
|
-
<input
|
|
394
|
-
className={styles.colorInput}
|
|
395
|
-
type="color"
|
|
396
|
-
value={rgb}
|
|
397
|
-
onChange={(event) => onChange(event.target.value + alpha)}
|
|
398
|
-
/>
|
|
399
|
-
</FieldRow>
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
function normalizeHex(value) {
|
|
403
|
-
const raw = (value ?? '').trim();
|
|
404
|
-
if (/^#[0-9a-fA-F]{3}$/.test(raw)) {
|
|
405
|
-
return '#' + raw.slice(1).replace(/./g, (c) => c + c);
|
|
406
|
-
}
|
|
407
|
-
if (/^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(raw)) return raw;
|
|
408
|
-
return '#000000';
|
|
409
|
-
}
|
|
410
|
-
export function isHexColor(value) {
|
|
411
|
-
return (
|
|
412
|
-
typeof value === 'string' && /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3}([0-9a-fA-F]{2})?)?$/.test(value)
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
// --- Bottom-sheet sizing (mobile only) -------------------------------------
|
|
416
|
-
// The inspector / file sheets are docked panels on desktop; below the CSS
|
|
417
|
-
// breakpoint they become drag-resizable bottom sheets. `useMobileSheet` owns a
|
|
418
|
-
// continuous pixel height so the sheet tracks the finger 1:1 and can rest at
|
|
419
|
-
// any height (the old version only read the drag delta at release and jumped
|
|
420
|
-
// between two fixed snaps, which felt dead mid-drag).
|
|
421
|
-
const SHEET_PEEK_FRACTION = 0.18; // collapsed "peek" height, fraction of viewport
|
|
422
|
-
const SHEET_DEFAULT_FRACTION = 0.62; // expanded default height
|
|
423
|
-
const SHEET_MAX_FRACTION = 0.92; // never quite cover the whole screen
|
|
424
|
-
const SHEET_MIN_PX = 96; // floor so the grab handle always fits
|
|
425
|
-
const SHEET_TAP_PX = 6; // release movement under this reads as a tap, not a drag
|
|
426
|
-
const SHEET_FLICK_VELOCITY = 0.6; // px/ms; a faster release flings to an end
|
|
427
|
-
function sheetViewportHeight() {
|
|
428
|
-
return typeof window !== 'undefined' ? window.innerHeight : 800;
|
|
429
|
-
}
|
|
430
|
-
function sheetSizes() {
|
|
431
|
-
const vh = sheetViewportHeight();
|
|
432
|
-
const peek = Math.max(SHEET_MIN_PX, vh * SHEET_PEEK_FRACTION);
|
|
433
|
-
const max = vh * SHEET_MAX_FRACTION;
|
|
434
|
-
const expanded = Math.min(max, Math.max(peek, vh * SHEET_DEFAULT_FRACTION));
|
|
435
|
-
return { peek, expanded, max };
|
|
436
|
-
}
|
|
437
|
-
function clampSheetHeight(px) {
|
|
438
|
-
const { peek, max } = sheetSizes();
|
|
439
|
-
return Math.min(max, Math.max(peek, px));
|
|
440
|
-
}
|
|
441
|
-
function releasePointer(event, pointerId) {
|
|
442
|
-
try {
|
|
443
|
-
event.currentTarget.releasePointerCapture(pointerId);
|
|
444
|
-
} catch {
|
|
445
|
-
// Capture may already be gone after a cancel / blur.
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
// Decide where the sheet rests when the finger lifts: a stationary press toggles
|
|
449
|
-
// between the peek and the last expanded height; a fast flick flings to the
|
|
450
|
-
// nearest end; otherwise it stays exactly where it was dragged (free resize).
|
|
451
|
-
function settleSheet(drag, setHeight, expandedRef) {
|
|
452
|
-
const { peek, expanded, max } = sheetSizes();
|
|
453
|
-
if (drag.moved < SHEET_TAP_PX) {
|
|
454
|
-
const wasCollapsed = drag.startHeight <= peek + 2;
|
|
455
|
-
if (!wasCollapsed) expandedRef.current = drag.startHeight;
|
|
456
|
-
setHeight(wasCollapsed ? expandedRef.current || expanded : peek);
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
if (drag.velocity > SHEET_FLICK_VELOCITY) {
|
|
460
|
-
setHeight(peek);
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
if (drag.velocity < -SHEET_FLICK_VELOCITY) {
|
|
464
|
-
expandedRef.current = max;
|
|
465
|
-
setHeight(max);
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
const settled = setHeight(drag.startHeight - (drag.lastY - drag.startY));
|
|
469
|
-
if (settled > peek + 2) expandedRef.current = settled;
|
|
470
|
-
}
|
|
471
|
-
// Persist a sheet's resting height across reloads (an agent's CODE edit
|
|
472
|
-
// triggers requestReload, which would otherwise reset every sheet to the
|
|
473
|
-
// default size). Keyed per sheet via `storageKey`; sessionStorage-scoped and
|
|
474
|
-
// best-effort, the same degrade-to-memory-only contract as editorHistory.
|
|
475
|
-
function sheetHeightStorageKey(key) {
|
|
476
|
-
return `castle-sheet-height:${key}`;
|
|
477
|
-
}
|
|
478
|
-
function loadStoredSheetHeight(key) {
|
|
479
|
-
if (!key || typeof sessionStorage === 'undefined') return null;
|
|
480
|
-
try {
|
|
481
|
-
const raw = sessionStorage.getItem(sheetHeightStorageKey(key));
|
|
482
|
-
if (raw === null) return null;
|
|
483
|
-
const px = Number(raw);
|
|
484
|
-
return Number.isFinite(px) && px > 0 ? px : null;
|
|
485
|
-
} catch {
|
|
486
|
-
return null;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
function saveStoredSheetHeight(key, px) {
|
|
490
|
-
if (!key || typeof sessionStorage === 'undefined') return;
|
|
491
|
-
try {
|
|
492
|
-
sessionStorage.setItem(sheetHeightStorageKey(key), String(Math.round(px)));
|
|
493
|
-
} catch {
|
|
494
|
-
// storage full/unavailable -- height still works in-memory this session
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
export function useMobileSheet({ open = true, baseClassName, storageKey }) {
|
|
498
|
-
const [height, setHeightState] = useState(() =>
|
|
499
|
-
clampSheetHeight(loadStoredSheetHeight(storageKey) ?? sheetSizes().expanded)
|
|
500
|
-
);
|
|
501
|
-
const [dragging, setDragging] = useState(false);
|
|
502
|
-
const heightRef = useRef(height);
|
|
503
|
-
// Last height above the peek, for tap-restore: seed from the restored height
|
|
504
|
-
// when it isn't collapsed, else the default expanded size.
|
|
505
|
-
const expandedRef = useRef(height > sheetSizes().peek + 2 ? height : sheetSizes().expanded);
|
|
506
|
-
const dragRef = useRef(null);
|
|
507
|
-
const didOpenMountRef = useRef(false);
|
|
508
|
-
const setHeight = (px) => {
|
|
509
|
-
const next = clampSheetHeight(px);
|
|
510
|
-
heightRef.current = next;
|
|
511
|
-
setHeightState(next);
|
|
512
|
-
return next;
|
|
513
|
-
};
|
|
514
|
-
// Restore the last expanded height whenever the sheet (re)opens, and keep the
|
|
515
|
-
// height in range across viewport resize / rotation. Skip the initial mount
|
|
516
|
-
// so a height restored from storage (including a collapsed peek) is kept
|
|
517
|
-
// rather than immediately overwritten by the default expanded size.
|
|
518
|
-
useEffect(() => {
|
|
519
|
-
if (!didOpenMountRef.current) {
|
|
520
|
-
didOpenMountRef.current = true;
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
if (open) setHeight(expandedRef.current);
|
|
524
|
-
}, [open]);
|
|
525
|
-
useEffect(() => {
|
|
526
|
-
const onResize = () => setHeight(heightRef.current);
|
|
527
|
-
window.addEventListener('resize', onResize);
|
|
528
|
-
return () => window.removeEventListener('resize', onResize);
|
|
529
|
-
}, []);
|
|
530
|
-
// Persist the resting height (never mid-drag, so a drag doesn't spam storage)
|
|
531
|
-
// so it survives a reload.
|
|
532
|
-
useEffect(() => {
|
|
533
|
-
if (!dragging) saveStoredSheetHeight(storageKey, height);
|
|
534
|
-
}, [dragging, height, storageKey]);
|
|
535
|
-
function onPointerDown(event) {
|
|
536
|
-
dragRef.current = {
|
|
537
|
-
pointerId: event.pointerId,
|
|
538
|
-
startY: event.clientY,
|
|
539
|
-
startHeight: heightRef.current,
|
|
540
|
-
lastY: event.clientY,
|
|
541
|
-
lastT: event.timeStamp,
|
|
542
|
-
velocity: 0,
|
|
543
|
-
moved: 0,
|
|
544
|
-
};
|
|
545
|
-
setDragging(true);
|
|
546
|
-
try {
|
|
547
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
548
|
-
} catch {
|
|
549
|
-
// No active pointer to capture (e.g. synthetic events) -- drag still works.
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
function onPointerMove(event) {
|
|
553
|
-
const drag = dragRef.current;
|
|
554
|
-
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
555
|
-
const dy = event.clientY - drag.startY; // down is positive
|
|
556
|
-
drag.moved = Math.max(drag.moved, Math.abs(dy));
|
|
557
|
-
const dt = event.timeStamp - drag.lastT;
|
|
558
|
-
if (dt > 0) drag.velocity = (event.clientY - drag.lastY) / dt;
|
|
559
|
-
drag.lastY = event.clientY;
|
|
560
|
-
drag.lastT = event.timeStamp;
|
|
561
|
-
setHeight(drag.startHeight - dy); // dragging up grows the sheet
|
|
562
|
-
}
|
|
563
|
-
function endDrag(event) {
|
|
564
|
-
const drag = dragRef.current;
|
|
565
|
-
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
566
|
-
dragRef.current = null;
|
|
567
|
-
setDragging(false);
|
|
568
|
-
releasePointer(event, drag.pointerId);
|
|
569
|
-
settleSheet(drag, setHeight, expandedRef);
|
|
570
|
-
}
|
|
571
|
-
return {
|
|
572
|
-
rootProps: {
|
|
573
|
-
className: cx(baseClassName, dragging && styles.sheetDragging),
|
|
574
|
-
'data-sheet-open': open ? 'true' : 'false',
|
|
575
|
-
style: { '--sheet-h': `${Math.round(height)}px` },
|
|
576
|
-
},
|
|
577
|
-
grabProps: {
|
|
578
|
-
className: cx(styles.sheetGrab, styles.mobileOnly),
|
|
579
|
-
onPointerDown,
|
|
580
|
-
onPointerMove,
|
|
581
|
-
onPointerUp: endDrag,
|
|
582
|
-
onPointerCancel: endDrag,
|
|
583
|
-
},
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
// Just the drag-affordance pill for the mobile sheet. The label/hint header
|
|
587
|
-
// this used to render was dropped -- it duplicated each inspector's own header
|
|
588
|
-
// (blueprint name input, "<name> Actor", scene settings, etc.).
|
|
589
|
-
export function SheetGrabHandle() {
|
|
590
|
-
return <div className={styles.sheetGrabBar} />;
|
|
591
|
-
}
|
|
592
|
-
// Cursor-anchored right-click menu reusing the scene editor's "Arrange" menu
|
|
593
|
-
// chrome (the shared `selArrangeMenu`/`selArrangeItem` classes). Opens at the
|
|
594
|
-
// given client point, clamps into the viewport, and closes on outside-click or
|
|
595
|
-
// Escape. Items are `{ key, label, disabled?, onClick }` descriptors. The
|
|
596
|
-
// timeline keeps its own inline copy; this one backs the FileBrowser menus.
|
|
597
|
-
export function ContextMenu({ x, y, items, onClose }) {
|
|
598
|
-
const ref = useRef(null);
|
|
599
|
-
const [pos, setPos] = useState({ left: x, top: y });
|
|
600
|
-
useLayoutEffect(() => {
|
|
601
|
-
const el = ref.current;
|
|
602
|
-
if (!el) return;
|
|
603
|
-
const { width, height } = el.getBoundingClientRect();
|
|
604
|
-
const margin = 8;
|
|
605
|
-
const maxLeft = Math.max(margin, window.innerWidth - width - margin);
|
|
606
|
-
const maxTop = Math.max(margin, window.innerHeight - height - margin);
|
|
607
|
-
setPos({ left: Math.min(x, maxLeft), top: Math.min(y, maxTop) });
|
|
608
|
-
}, [x, y]);
|
|
609
|
-
useEffect(() => {
|
|
610
|
-
const dismiss = (event) => {
|
|
611
|
-
if (event.type === 'keydown' && event.key !== 'Escape') return;
|
|
612
|
-
if (event.type === 'pointerdown' && ref.current?.contains(event.target)) return;
|
|
613
|
-
onClose();
|
|
614
|
-
};
|
|
615
|
-
window.addEventListener('pointerdown', dismiss);
|
|
616
|
-
window.addEventListener('keydown', dismiss);
|
|
617
|
-
return () => {
|
|
618
|
-
window.removeEventListener('pointerdown', dismiss);
|
|
619
|
-
window.removeEventListener('keydown', dismiss);
|
|
620
|
-
};
|
|
621
|
-
}, [onClose]);
|
|
622
|
-
const pick = (item) => {
|
|
623
|
-
if (item.disabled) return;
|
|
624
|
-
item.onClick();
|
|
625
|
-
onClose();
|
|
626
|
-
};
|
|
627
|
-
const menuStyle = { position: 'fixed', left: pos.left, top: pos.top, transform: 'none' };
|
|
628
|
-
return (
|
|
629
|
-
<div
|
|
630
|
-
ref={ref}
|
|
631
|
-
role="menu"
|
|
632
|
-
style={menuStyle}
|
|
633
|
-
onContextMenu={(event) => event.preventDefault()}
|
|
634
|
-
className={styles.selArrangeMenu}>
|
|
635
|
-
{items.map((item) => (
|
|
636
|
-
<button
|
|
637
|
-
type="button"
|
|
638
|
-
key={item.key}
|
|
639
|
-
disabled={item.disabled}
|
|
640
|
-
onClick={() => pick(item)}
|
|
641
|
-
className={cx(styles.selArrangeItem, item.disabled && styles.selArrangeItemDisabled)}
|
|
642
|
-
role="menuitem">
|
|
643
|
-
{item.label}
|
|
644
|
-
</button>
|
|
645
|
-
))}
|
|
646
|
-
</div>
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
// Cursor-independent modal confirm (unlike `ContextMenu`, which anchors to a
|
|
650
|
-
// click point) -- for actions with real consequences, like the blueprint
|
|
651
|
-
// library's cascade delete. Dismisses on Escape or a backdrop click, same as
|
|
652
|
-
// cancelling.
|
|
653
|
-
export function ConfirmDialog({ title, message, confirmLabel = 'Delete', danger = true, onConfirm, onCancel }) {
|
|
654
|
-
useEffect(() => {
|
|
655
|
-
const onKeyDown = (event) => {
|
|
656
|
-
if (event.key === 'Escape') onCancel();
|
|
657
|
-
};
|
|
658
|
-
window.addEventListener('keydown', onKeyDown);
|
|
659
|
-
return () => window.removeEventListener('keydown', onKeyDown);
|
|
660
|
-
}, [onCancel]);
|
|
661
|
-
return (
|
|
662
|
-
<div className={styles.confirmBackdrop} onPointerDown={onCancel}>
|
|
663
|
-
<div className={styles.confirmDialog} role="alertdialog" onPointerDown={(event) => event.stopPropagation()}>
|
|
664
|
-
<div className={styles.confirmTitle}>{title}</div>
|
|
665
|
-
<div className={styles.confirmMessage}>{message}</div>
|
|
666
|
-
<div className={styles.confirmActions}>
|
|
667
|
-
<Button onClick={onCancel}>Cancel</Button>
|
|
668
|
-
<Button variant={danger ? 'danger' : ''} onClick={onConfirm}>
|
|
669
|
-
{confirmLabel}
|
|
670
|
-
</Button>
|
|
671
|
-
</div>
|
|
672
|
-
</div>
|
|
673
|
-
</div>
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
export function SelectField({ label, value, onChange, options, overridden, defaultValue, onReset }) {
|
|
677
|
-
return (
|
|
678
|
-
<FieldRow label={label} overridden={overridden} defaultValue={defaultValue} onReset={onReset}>
|
|
679
|
-
<select
|
|
680
|
-
className={styles.select}
|
|
681
|
-
value={value ?? ''}
|
|
682
|
-
onChange={(event) => onChange(event.target.value)}>
|
|
683
|
-
{options.map((option) => {
|
|
684
|
-
const value = typeof option === 'string' ? option : option.value;
|
|
685
|
-
const label = typeof option === 'string' ? option : option.label;
|
|
686
|
-
return (
|
|
687
|
-
<option key={value} value={value}>
|
|
688
|
-
{label}
|
|
689
|
-
</option>
|
|
690
|
-
);
|
|
691
|
-
})}
|
|
692
|
-
</select>
|
|
693
|
-
</FieldRow>
|
|
694
|
-
);
|
|
695
|
-
}
|