@buoy-gg/shared-ui 7.0.15 → 7.0.16

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.
Files changed (57) hide show
  1. package/lib/commonjs/JsModal.web.js +1588 -0
  2. package/lib/commonjs/env/EnvironmentIndicator.web.js +114 -0
  3. package/lib/commonjs/env/EnvironmentSelectorInline.web.js +178 -0
  4. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  5. package/lib/commonjs/icons/index.js +14 -0
  6. package/lib/commonjs/index.js +3 -3
  7. package/lib/commonjs/stores/ignoredPatternsCore.js +68 -0
  8. package/lib/commonjs/stores/ignoredPatternsStore.js +16 -52
  9. package/lib/commonjs/stores/ignoredPatternsStore.web.js +236 -0
  10. package/lib/commonjs/stores/index.js +1 -1
  11. package/lib/commonjs/ui/gameUI/constants/gameUIColors.js +5 -3
  12. package/lib/module/JsModal.web.js +1586 -0
  13. package/lib/module/env/EnvironmentIndicator.web.js +108 -0
  14. package/lib/module/env/EnvironmentSelectorInline.web.js +173 -0
  15. package/lib/module/hooks/safe-area-impl.js +1 -1
  16. package/lib/module/icons/index.js +1 -0
  17. package/lib/module/index.js +3 -3
  18. package/lib/module/stores/ignoredPatternsCore.js +62 -0
  19. package/lib/module/stores/ignoredPatternsStore.js +5 -51
  20. package/lib/module/stores/ignoredPatternsStore.web.js +221 -0
  21. package/lib/module/stores/index.js +1 -1
  22. package/lib/module/ui/gameUI/constants/gameUIColors.js +4 -2
  23. package/lib/typescript/commonjs/JsModal.web.d.ts +138 -0
  24. package/lib/typescript/commonjs/JsModal.web.d.ts.map +1 -0
  25. package/lib/typescript/commonjs/env/EnvironmentIndicator.web.d.ts +16 -0
  26. package/lib/typescript/commonjs/env/EnvironmentIndicator.web.d.ts.map +1 -0
  27. package/lib/typescript/commonjs/env/EnvironmentSelectorInline.web.d.ts +8 -0
  28. package/lib/typescript/commonjs/env/EnvironmentSelectorInline.web.d.ts.map +1 -0
  29. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  30. package/lib/typescript/commonjs/icons/index.d.ts +1 -0
  31. package/lib/typescript/commonjs/icons/index.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/stores/ignoredPatternsCore.d.ts +37 -0
  33. package/lib/typescript/commonjs/stores/ignoredPatternsCore.d.ts.map +1 -0
  34. package/lib/typescript/commonjs/stores/ignoredPatternsStore.d.ts +3 -27
  35. package/lib/typescript/commonjs/stores/ignoredPatternsStore.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/stores/ignoredPatternsStore.web.d.ts +72 -0
  37. package/lib/typescript/commonjs/stores/ignoredPatternsStore.web.d.ts.map +1 -0
  38. package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts +56 -0
  39. package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
  40. package/lib/typescript/module/JsModal.web.d.ts +138 -0
  41. package/lib/typescript/module/JsModal.web.d.ts.map +1 -0
  42. package/lib/typescript/module/env/EnvironmentIndicator.web.d.ts +16 -0
  43. package/lib/typescript/module/env/EnvironmentIndicator.web.d.ts.map +1 -0
  44. package/lib/typescript/module/env/EnvironmentSelectorInline.web.d.ts +8 -0
  45. package/lib/typescript/module/env/EnvironmentSelectorInline.web.d.ts.map +1 -0
  46. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  47. package/lib/typescript/module/icons/index.d.ts +1 -0
  48. package/lib/typescript/module/icons/index.d.ts.map +1 -1
  49. package/lib/typescript/module/stores/ignoredPatternsCore.d.ts +37 -0
  50. package/lib/typescript/module/stores/ignoredPatternsCore.d.ts.map +1 -0
  51. package/lib/typescript/module/stores/ignoredPatternsStore.d.ts +3 -27
  52. package/lib/typescript/module/stores/ignoredPatternsStore.d.ts.map +1 -1
  53. package/lib/typescript/module/stores/ignoredPatternsStore.web.d.ts +72 -0
  54. package/lib/typescript/module/stores/ignoredPatternsStore.web.d.ts.map +1 -0
  55. package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts +56 -0
  56. package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
  57. package/package.json +4 -4
@@ -0,0 +1,1588 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.JsModal = void 0;
7
+ var _react = require("react");
8
+ var _devToolsStorageKeys = require("./storage/devToolsStorageKeys.js");
9
+ var _index = require("./context/index.js");
10
+ var _jsxRuntime = require("react/jsx-runtime");
11
+ /**
12
+ * JsModal.web — DOM port of JsModal.tsx (React Native).
13
+ *
14
+ * The dual-mode tool-panel container: draggable bottom sheet ⇄ floating
15
+ * window. Same exported names (`JsModal`, `ModalMode`, `ModalState`), same
16
+ * prop contract, same gesture spec and persistence key/JSON shape as the
17
+ * native file. Zero react-native imports — header chrome that lives in
18
+ * shared-ui RN components (`WindowControls`, `DraggableHeader`, `ModalHeader`,
19
+ * `ModalHintBanner`) is replicated as DOM with provenance comments, the same
20
+ * way DevToolsSettingsModal.web.tsx replicates the WindowControls close dot.
21
+ *
22
+ * Gesture spec (verbatim from native):
23
+ * - Sheet header drag: steal at |dy| > 3px, absolute finger anchoring
24
+ * (height = screenH − (pointerY − grab offset)), clamped to
25
+ * [minHeight, maxHeight ?? screenH − safeTop]. Fast downward swipe closes:
26
+ * (vy > 0.8 px/ms && dy > 50) || (dy > 150 && height <= minHeight).
27
+ * - Floating header drag (DraggableHeader.tsx): drag starts at
28
+ * |dx|+|dy| > 5, position unclamped during move, clamped on release to
29
+ * x ∈ [0, screenW − width], y ∈ [safeTop, screenH − height].
30
+ * - Corner resize (bottom corners only): moves < 0.5px ignored; rect clamped
31
+ * to safe-area bounds; min size = screenW*0.25 × 80.
32
+ * - Header taps (both modes): taps accumulate within a 500ms window and
33
+ * settle after 300ms — 2 taps toggles mode, ≥3 taps minimizes.
34
+ *
35
+ * Persistence (verbatim from native ModalStorage): the `persistenceKey` prop
36
+ * is the storage key, used verbatim (native callers pass fully-derived
37
+ * `@react_buoy…` keys or short keys — no extra derivation happens here
38
+ * either). JSON shape `{ mode, panelHeight, dimensions: {width, height, top,
39
+ * left}, isVisible }`, saved on a 500ms debounce after state loads, with the
40
+ * same in-memory cache in front of storage. Backed by a local
41
+ * localStorage adapter matching the `persistentStorage` API — importing
42
+ * `./utils/persistentStorage` is avoided on purpose: its lazy `require()`
43
+ * probing of expo-file-system/AsyncStorage doesn't survive Vite's ESM graph.
44
+ * The hint-acknowledged flag uses the real `devToolsStorageKeys` helper
45
+ * (zero-dep) so the key matches native exactly.
46
+ *
47
+ * Intentional deviations from the native file (all logged):
48
+ * - No native-driver springs: rAF drives all animations. The open/close
49
+ * spring (tension 180, friction 22, ~300ms settle) is approximated with a
50
+ * 300ms ease-out-cubic; native `Animated.timing` calls keep their exact
51
+ * durations (200ms fades, 300ms minimize) with ease-in-out.
52
+ * - No RN Modal/overlay layer: fixed-position divs with the `zIndex` prop
53
+ * (default 1000, same as the native styles).
54
+ * - `benchMotion` and `animatedHeight` (DEV-only benchmark seams) are
55
+ * accepted and no-op'd — the web build has no tool-bench harness.
56
+ * - `CyberpunkModalHeader` complete-replacement detection in ModalHeader is
57
+ * not ported (that component is RN-only); custom header content renders
58
+ * inside the standard header structure, with `onMinimize` injected via
59
+ * cloneElement like native's non-replacement path.
60
+ * - Native captures `Dimensions.get("window")` once at module load; the web
61
+ * twin reads `window.innerWidth/innerHeight` lazily so late layout (Tauri/
62
+ * WKWebView) doesn't bake in a 0×0 screen.
63
+ * - Safe-area insets come from a one-shot `env(safe-area-inset-*)` probe
64
+ * (same trick as floatingTools.web.tsx) instead of the RN hook.
65
+ * - On restore-from-minimize, the floating position re-syncs from the last
66
+ * committed dimensions when the panel becomes visible: the web AppHost
67
+ * doesn't pass `initialModalState` back, and without the re-sync the panel
68
+ * would reappear at the minimize target.
69
+ * - `JsModalProps` is additionally exported (native keeps it private) so web
70
+ * consumers can type wrappers without redeclaring the contract.
71
+ * - `visible={false}` renders children in a `display:none` div (state and
72
+ * listeners survive) instead of native's offscreen −9999 container.
73
+ * - The hidden/floating/bottomSheet root branches carry distinct React keys:
74
+ * the rAF animations write opacity/transform straight to the DOM, and DOM
75
+ * reuse across branches (e.g. an async persisted-mode flip mid-entrance)
76
+ * would leak those styles onto unrelated chrome (blank header bug).
77
+ *
78
+ * @platform React DOM Web (Vite, Tauri, Capacitor/Ionic, Electron)
79
+ */
80
+
81
+ // ============================================================================
82
+ // PALETTE — inlined from shared-ui with provenance (RN-only modules on web)
83
+ // ============================================================================
84
+
85
+ /** buoyColors from ui/gameUI/constants/gameUIColors.ts — inlined verbatim. */const buoy = {
86
+ base: "#121212",
87
+ card: "#1A1A1A",
88
+ border: "#333333",
89
+ textMuted: "#888888",
90
+ primary: "#20C997" // Buoy teal
91
+ };
92
+
93
+ /**
94
+ * gameUIColors resolved values (macOS theme active — see gameUIColors.ts
95
+ * `activeTheme = macOSTheme` and macOSDesignSystemColors.ts).
96
+ */
97
+ const gameUI = {
98
+ primary: "#F5F5F7",
99
+ // macOSColors.text.primary — header title
100
+ primaryLight: "#F5F5F7",
101
+ // macOS theme maps primaryLight to text.primary too
102
+ secondary: "#A1A1A6",
103
+ // macOSColors.text.secondary — header subtitle
104
+ muted: "#8E8E93",
105
+ // macOSColors.text.muted — hint text
106
+ success: "#34C759",
107
+ // macOSColors.semantic.success — resize grips/handles
108
+ info: "#00B8E6" // macOSColors.semantic.info — hint banner
109
+ };
110
+
111
+ // ============================================================================
112
+ // CONSTANTS — same values as native JsModal.tsx
113
+ // ============================================================================
114
+ const MIN_HEIGHT = 100;
115
+ const DEFAULT_HEIGHT = 400;
116
+ const FLOATING_HEIGHT = 500;
117
+ const FLOATING_MIN_HEIGHT = 80; // header (60) + 20px content
118
+ // native: FLOATING_WIDTH = SCREEN.width, FLOATING_MIN_WIDTH = SCREEN.width/4 —
119
+ // read lazily on web (see deviations in the file header).
120
+ const screenW = () => typeof window !== "undefined" ? window.innerWidth : 0;
121
+ const screenH = () => typeof window !== "undefined" ? window.innerHeight : 0;
122
+
123
+ // ============================================================================
124
+ // STORAGE — exact native ModalStorage semantics over localStorage
125
+ // ============================================================================
126
+
127
+ /**
128
+ * Local localStorage-backed adapter matching the shared `persistentStorage`
129
+ * API surface JsModal uses (getItem/setItem). Same recipe as the app shims
130
+ * (example-tauri/src/shims/shared-ui.web.ts).
131
+ */
132
+ const webPersistentStorage = {
133
+ async getItem(key) {
134
+ try {
135
+ return window.localStorage.getItem(key);
136
+ } catch {
137
+ return null;
138
+ }
139
+ },
140
+ async setItem(key, value) {
141
+ try {
142
+ window.localStorage.setItem(key, value);
143
+ } catch {
144
+ // Best-effort, like native's swallowed save errors.
145
+ }
146
+ }
147
+ };
148
+
149
+ /** Persisted JSON shape — identical to native `PersistedModalState`. */
150
+
151
+ /** Native `ModalStorage` twin: memory cache in front of persistent storage. */
152
+ class ModalStorage {
153
+ static memoryCache = {};
154
+ static async save(key, value) {
155
+ try {
156
+ this.memoryCache[key] = value;
157
+ await webPersistentStorage.setItem(key, JSON.stringify(value));
158
+ } catch {
159
+ // Failed to save modal state
160
+ }
161
+ }
162
+ static async load(key) {
163
+ try {
164
+ if (this.memoryCache[key]) {
165
+ return this.memoryCache[key];
166
+ }
167
+ const stored = await webPersistentStorage.getItem(key);
168
+ if (stored) {
169
+ const parsed = JSON.parse(stored);
170
+ this.memoryCache[key] = parsed;
171
+ return parsed;
172
+ }
173
+ } catch {
174
+ // Failed to load modal state
175
+ }
176
+ return null;
177
+ }
178
+ }
179
+
180
+ // ============================================================================
181
+ // TYPES — same exported names as native
182
+ // ============================================================================
183
+
184
+ /** Modal state that can be saved and restored when minimizing/restoring. */
185
+
186
+ // ============================================================================
187
+ // rAF ANIMATION HELPERS (native-driver springs/timings stand-in)
188
+ // ============================================================================
189
+
190
+ const easeOutCubic = t => 1 - Math.pow(1 - t, 3);
191
+ // Animated.timing's default easing is ease-in-out
192
+ const easeInOut = t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
193
+ function runAnim(duration, ease, onFrame, onDone) {
194
+ let raf = 0;
195
+ let cancelled = false;
196
+ const start = performance.now();
197
+ const tick = now => {
198
+ if (cancelled) return;
199
+ const t = Math.min((now - start) / duration, 1);
200
+ onFrame(ease(t));
201
+ if (t < 1) {
202
+ raf = requestAnimationFrame(tick);
203
+ } else {
204
+ onDone?.();
205
+ }
206
+ };
207
+ raf = requestAnimationFrame(tick);
208
+ return () => {
209
+ cancelled = true;
210
+ cancelAnimationFrame(raf);
211
+ };
212
+ }
213
+
214
+ // ============================================================================
215
+ // SAFE AREA — env() probe (floatingTools.web.tsx pattern), one-shot on mount
216
+ // ============================================================================
217
+
218
+ function readSafeAreaInsets() {
219
+ if (typeof document === "undefined") {
220
+ return {
221
+ top: 0,
222
+ left: 0,
223
+ right: 0,
224
+ bottom: 0
225
+ };
226
+ }
227
+ const probe = document.createElement("div");
228
+ probe.style.cssText = "position:fixed;top:env(safe-area-inset-top);left:env(safe-area-inset-left);" + "right:env(safe-area-inset-right);bottom:env(safe-area-inset-bottom);" + "visibility:hidden;pointer-events:none";
229
+ document.body.appendChild(probe);
230
+ const rect = probe.getBoundingClientRect();
231
+ document.body.removeChild(probe);
232
+ return {
233
+ top: Math.max(0, rect.top),
234
+ left: Math.max(0, rect.left),
235
+ right: Math.max(0, window.innerWidth - rect.right),
236
+ bottom: Math.max(0, window.innerHeight - rect.bottom)
237
+ };
238
+ }
239
+
240
+ /**
241
+ * Keep a floating rect fully inside the safe area. Applied on every entry
242
+ * path — initial position, persisted restore, drag release — so the window
243
+ * can never sit under a notch/Dynamic Island or the home indicator.
244
+ * (Deliberate improvement over native, whose release clamp allows the
245
+ * window to reach the raw screen bottom.)
246
+ */
247
+ function clampRectToSafeArea(rect, insets) {
248
+ const maxW = Math.max(FLOATING_MIN_HEIGHT, window.innerWidth - insets.left - insets.right);
249
+ const maxH = Math.max(FLOATING_MIN_HEIGHT, window.innerHeight - insets.top - insets.bottom);
250
+ const width = Math.min(rect.width, maxW);
251
+ const height = Math.min(rect.height, maxH);
252
+ return {
253
+ width,
254
+ height,
255
+ left: Math.max(insets.left, Math.min(rect.left, window.innerWidth - insets.right - width)),
256
+ top: Math.max(insets.top, Math.min(rect.top, window.innerHeight - insets.bottom - height))
257
+ };
258
+ }
259
+
260
+ // ============================================================================
261
+ // ICONS — DOM twins of shared-ui icons (icons/lucide-icons.tsx)
262
+ // ============================================================================
263
+
264
+ /** shared-ui X icon (lucide-icons.tsx). */
265
+ const XIcon = ({
266
+ size = 24,
267
+ color = "currentColor",
268
+ strokeWidth = 2
269
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsxs)("svg", {
270
+ width: size,
271
+ height: size,
272
+ viewBox: "0 0 24 24",
273
+ fill: "none",
274
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("line", {
275
+ x1: 18,
276
+ y1: 6,
277
+ x2: 6,
278
+ y2: 18,
279
+ stroke: color,
280
+ strokeWidth: strokeWidth,
281
+ strokeLinecap: "round"
282
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("line", {
283
+ x1: 6,
284
+ y1: 6,
285
+ x2: 18,
286
+ y2: 18,
287
+ stroke: color,
288
+ strokeWidth: strokeWidth,
289
+ strokeLinecap: "round"
290
+ })]
291
+ });
292
+
293
+ /** shared-ui Minus icon (lucide-icons.tsx). */
294
+ const MinusIcon = ({
295
+ size = 24,
296
+ color = "currentColor",
297
+ strokeWidth = 2
298
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsx)("svg", {
299
+ width: size,
300
+ height: size,
301
+ viewBox: "0 0 24 24",
302
+ fill: "none",
303
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)("line", {
304
+ x1: 5,
305
+ y1: 12,
306
+ x2: 19,
307
+ y2: 12,
308
+ stroke: color,
309
+ strokeWidth: strokeWidth,
310
+ strokeLinecap: "round"
311
+ })
312
+ });
313
+
314
+ /** shared-ui DockBottom icon — outer frame + filled bottom panel. */
315
+ const DockBottomIcon = ({
316
+ size = 24,
317
+ color = "currentColor",
318
+ strokeWidth = 2
319
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsxs)("svg", {
320
+ width: size,
321
+ height: size,
322
+ viewBox: "0 0 24 24",
323
+ fill: "none",
324
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("rect", {
325
+ x: 3,
326
+ y: 3,
327
+ width: 18,
328
+ height: 18,
329
+ rx: 2,
330
+ stroke: color,
331
+ strokeWidth: strokeWidth
332
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("rect", {
333
+ x: 3,
334
+ y: 14,
335
+ width: 18,
336
+ height: 7,
337
+ fill: color
338
+ })]
339
+ });
340
+
341
+ /** shared-ui FloatWindow icon — window with offset shadow. */
342
+ const FloatWindowIcon = ({
343
+ size = 24,
344
+ color = "currentColor",
345
+ strokeWidth = 2
346
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsxs)("svg", {
347
+ width: size,
348
+ height: size,
349
+ viewBox: "0 0 24 24",
350
+ fill: "none",
351
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("rect", {
352
+ x: 3,
353
+ y: 3,
354
+ width: 14,
355
+ height: 14,
356
+ rx: 2,
357
+ stroke: color,
358
+ strokeWidth: strokeWidth
359
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("line", {
360
+ x1: 7,
361
+ y1: 17,
362
+ x2: 7,
363
+ y2: 21,
364
+ stroke: color,
365
+ strokeWidth: strokeWidth
366
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("line", {
367
+ x1: 7,
368
+ y1: 21,
369
+ x2: 21,
370
+ y2: 21,
371
+ stroke: color,
372
+ strokeWidth: strokeWidth
373
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("line", {
374
+ x1: 21,
375
+ y1: 21,
376
+ x2: 21,
377
+ y2: 7,
378
+ stroke: color,
379
+ strokeWidth: strokeWidth
380
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("line", {
381
+ x1: 17,
382
+ y1: 7,
383
+ x2: 21,
384
+ y2: 7,
385
+ stroke: color,
386
+ strokeWidth: strokeWidth
387
+ })]
388
+ });
389
+
390
+ // ============================================================================
391
+ // WINDOW CONTROLS — DOM twin of ui/components/WindowControls.tsx.
392
+ // Native renders the non-expandable branch on web (EXPANDABLE_SUPPORTED =
393
+ // Platform.OS !== "web"), so the plain macOS dots ARE the 1:1 web look.
394
+ // ============================================================================
395
+ // native WindowControls COLORS
396
+ const WC_COLORS = {
397
+ close: "#FF5F57",
398
+ minimize: "#FEBC2E",
399
+ toggleMode: "#28C840"
400
+ };
401
+ const WC_BUTTON_SIZE = 12; // native BUTTON_SIZE
402
+ const WC_BUTTON_SPACING = 8; // native BUTTON_SPACING
403
+ const WC_ICON_SIZE = 8; // native ICON_SIZE
404
+
405
+ const wcDotStyle = bg => ({
406
+ width: WC_BUTTON_SIZE,
407
+ height: WC_BUTTON_SIZE,
408
+ borderRadius: WC_BUTTON_SIZE / 2,
409
+ display: "flex",
410
+ alignItems: "center",
411
+ justifyContent: "center",
412
+ backgroundColor: bg,
413
+ border: "none",
414
+ padding: 0,
415
+ cursor: "pointer",
416
+ flexShrink: 0
417
+ });
418
+ function WindowControls({
419
+ onClose,
420
+ onMinimize,
421
+ onToggleMode,
422
+ mode
423
+ }) {
424
+ const ToggleModeIcon = mode === "floating" ? DockBottomIcon : FloatWindowIcon;
425
+ const toggleModeLabel = mode === "floating" ? "Dock to bottom sheet" : "Make floating window";
426
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
427
+ style: {
428
+ display: "flex",
429
+ flexDirection: "row",
430
+ alignItems: "center",
431
+ gap: WC_BUTTON_SPACING
432
+ },
433
+ children: [onMinimize && /*#__PURE__*/(0, _jsxRuntime.jsx)("button", {
434
+ type: "button",
435
+ onClick: onMinimize,
436
+ style: wcDotStyle(WC_COLORS.minimize),
437
+ "aria-label": "Minimize modal",
438
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(MinusIcon, {
439
+ size: WC_ICON_SIZE,
440
+ color: "#7A5A00",
441
+ strokeWidth: 1.5
442
+ })
443
+ }), onToggleMode && /*#__PURE__*/(0, _jsxRuntime.jsx)("button", {
444
+ type: "button",
445
+ onClick: onToggleMode,
446
+ style: wcDotStyle(WC_COLORS.toggleMode),
447
+ "aria-label": toggleModeLabel,
448
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(ToggleModeIcon, {
449
+ size: WC_ICON_SIZE,
450
+ color: "#004A1A",
451
+ strokeWidth: 1.5
452
+ })
453
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("button", {
454
+ type: "button",
455
+ onClick: onClose,
456
+ style: wcDotStyle(WC_COLORS.close),
457
+ "aria-label": "Close modal",
458
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(XIcon, {
459
+ size: WC_ICON_SIZE,
460
+ color: "#4A0000",
461
+ strokeWidth: 1.5
462
+ })
463
+ })]
464
+ });
465
+ }
466
+
467
+ // ============================================================================
468
+ // DRAG INDICATOR — DOM twin of native DragIndicator
469
+ // ============================================================================
470
+ const DragIndicator = /*#__PURE__*/(0, _react.memo)(function DragIndicator({
471
+ isResizing,
472
+ mode,
473
+ hasCustomContent = false
474
+ }) {
475
+ const floating = mode === "floating";
476
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
477
+ style: {
478
+ display: "flex",
479
+ flexDirection: "column",
480
+ alignItems: "center",
481
+ position: "relative",
482
+ // native dragIndicatorContainer: paddingVertical 8;
483
+ // custom variant: paddingTop 6, paddingBottom 2
484
+ paddingTop: hasCustomContent ? 6 : 8,
485
+ paddingBottom: hasCustomContent ? 2 : 8,
486
+ backgroundColor: "transparent"
487
+ },
488
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
489
+ style: {
490
+ // native dragIndicator: 40x3 teal bar, radius 2, teal glow
491
+ // (shadowOpacity 0.6, radius 4); floating variant: 50x5 textMuted;
492
+ // active (isResizing/isDragging): teal, width 40
493
+ width: isResizing ? 40 : floating ? 50 : 40,
494
+ height: floating ? 5 : 3,
495
+ backgroundColor: isResizing ? buoy.primary : floating ? buoy.textMuted : buoy.primary,
496
+ borderRadius: 2,
497
+ boxShadow: `0 0 4px ${buoy.primary}99`
498
+ }
499
+ }), isResizing && mode === "bottomSheet" && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
500
+ style: {
501
+ position: "absolute",
502
+ display: "flex",
503
+ flexDirection: "row",
504
+ gap: 2,
505
+ // native resizeGripContainer: gap 2
506
+ marginTop: 12 // native: marginTop 12
507
+ },
508
+ children: [0, 1, 2].map(i => /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
509
+ style: {
510
+ width: 12,
511
+ // native resizeGripLine: 12x1
512
+ height: 1,
513
+ backgroundColor: gameUI.success,
514
+ opacity: 0.6
515
+ }
516
+ }, i))
517
+ })]
518
+ });
519
+ });
520
+
521
+ // ============================================================================
522
+ // MODAL HINT BANNER — DOM twin of ui/components/ModalHintBanner.tsx
523
+ // ============================================================================
524
+ const ModalHintBanner = /*#__PURE__*/(0, _react.memo)(function ModalHintBanner({
525
+ onAcknowledge
526
+ }) {
527
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
528
+ style: {
529
+ // native container: info+15 bg, 3px info left border, radius 8,
530
+ // marginHorizontal 12, mt 8, mb 4, info glow (opacity 0.3, radius 8)
531
+ backgroundColor: `${gameUI.info}15`,
532
+ borderLeft: `3px solid ${gameUI.info}`,
533
+ borderRadius: 8,
534
+ margin: "8px 12px 4px",
535
+ overflow: "hidden",
536
+ boxShadow: `0 0 8px ${gameUI.info}4D`,
537
+ flexShrink: 0
538
+ },
539
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
540
+ style: {
541
+ display: "flex",
542
+ flexDirection: "row",
543
+ alignItems: "center",
544
+ padding: 12,
545
+ // native inner: padding 12, gap 12
546
+ gap: 12
547
+ },
548
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
549
+ style: {
550
+ width: 32,
551
+ height: 32,
552
+ position: "relative",
553
+ display: "flex",
554
+ alignItems: "center",
555
+ justifyContent: "center",
556
+ flexShrink: 0
557
+ },
558
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
559
+ style: {
560
+ position: "absolute",
561
+ width: 20,
562
+ height: 20,
563
+ borderRadius: 10,
564
+ border: `2px solid ${gameUI.info}`,
565
+ opacity: 0.6,
566
+ transform: "scale(0.8)",
567
+ boxSizing: "border-box"
568
+ }
569
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
570
+ style: {
571
+ position: "absolute",
572
+ width: 20,
573
+ height: 20,
574
+ borderRadius: 10,
575
+ border: `2px solid ${gameUI.info}`,
576
+ boxSizing: "border-box"
577
+ }
578
+ })]
579
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
580
+ style: {
581
+ flex: 1,
582
+ display: "flex",
583
+ flexDirection: "column",
584
+ gap: 2
585
+ },
586
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
587
+ style: {
588
+ fontSize: 13,
589
+ fontWeight: 600,
590
+ color: gameUI.info,
591
+ letterSpacing: 0.3
592
+ },
593
+ children: "\uD83D\uDCA1 Pro Tip"
594
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
595
+ style: {
596
+ fontSize: 12,
597
+ color: gameUI.primaryLight,
598
+ lineHeight: "16px"
599
+ },
600
+ children: "Double-tap the header to transform this into a floating modal"
601
+ })]
602
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("button", {
603
+ type: "button",
604
+ onClick: onAcknowledge,
605
+ style: {
606
+ backgroundColor: `${gameUI.info}30`,
607
+ padding: "6px 12px",
608
+ // native: paddingVertical 6 / horizontal 12
609
+ borderRadius: 6,
610
+ border: `1px solid ${gameUI.info}50`,
611
+ fontSize: 12,
612
+ fontWeight: 600,
613
+ color: gameUI.info,
614
+ cursor: "pointer",
615
+ fontFamily: "inherit",
616
+ flexShrink: 0
617
+ },
618
+ children: "Got it!"
619
+ })]
620
+ })
621
+ });
622
+ });
623
+
624
+ // ============================================================================
625
+ // MODAL HEADER — DOM twin of native ModalHeader (title/subtitle/hint text)
626
+ // ============================================================================
627
+ function ModalHeaderContent({
628
+ header,
629
+ isResizing,
630
+ mode,
631
+ onMinimize
632
+ }) {
633
+ if (header?.customContent) {
634
+ // native (non-CyberpunkModalHeader path): inject onMinimize into the
635
+ // custom element so it can trigger minimize; render inside the standard
636
+ // header structure with the compact drag indicator.
637
+ let customContent = header.customContent;
638
+ if (/*#__PURE__*/(0, _react.isValidElement)(header.customContent) && onMinimize) {
639
+ customContent = /*#__PURE__*/(0, _react.cloneElement)(header.customContent, {
640
+ onMinimize
641
+ });
642
+ }
643
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
644
+ style: {
645
+ flex: 1,
646
+ display: "flex",
647
+ flexDirection: "column",
648
+ justifyContent: "center",
649
+ minWidth: 0
650
+ },
651
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(DragIndicator, {
652
+ isResizing: isResizing,
653
+ mode: mode,
654
+ hasCustomContent: true
655
+ }), customContent]
656
+ });
657
+ }
658
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
659
+ style: {
660
+ flex: 1,
661
+ display: "flex",
662
+ flexDirection: "column",
663
+ justifyContent: "center",
664
+ position: "relative",
665
+ minWidth: 0
666
+ },
667
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(DragIndicator, {
668
+ isResizing: isResizing,
669
+ mode: mode
670
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
671
+ style: {
672
+ // native headerContent: paddingHorizontal 16, alignItems center
673
+ padding: "0 16px",
674
+ display: "flex",
675
+ flexDirection: "column",
676
+ alignItems: "center"
677
+ },
678
+ children: [header?.title && /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
679
+ style: {
680
+ fontSize: 16,
681
+ // native headerTitle
682
+ fontWeight: 600,
683
+ color: gameUI.primary
684
+ },
685
+ children: header.title
686
+ }), header?.subtitle && /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
687
+ style: {
688
+ fontSize: 12,
689
+ // native headerSubtitle
690
+ color: gameUI.secondary,
691
+ paddingTop: 4
692
+ },
693
+ children: header.subtitle
694
+ })]
695
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
696
+ style: {
697
+ position: "absolute",
698
+ inset: 0,
699
+ display: "flex",
700
+ alignItems: "center",
701
+ justifyContent: "center",
702
+ pointerEvents: "none"
703
+ },
704
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
705
+ style: {
706
+ fontSize: 10,
707
+ // native hintText
708
+ color: gameUI.muted,
709
+ fontStyle: "italic"
710
+ },
711
+ children: "Double tap: Toggle \u2022 Triple tap: Close"
712
+ })
713
+ })]
714
+ });
715
+ }
716
+
717
+ // ============================================================================
718
+ // MAIN COMPONENT
719
+ // ============================================================================
720
+ const JsModalComponent = ({
721
+ visible,
722
+ onClose,
723
+ children,
724
+ header,
725
+ styles: customStyles = {},
726
+ minHeight = MIN_HEIGHT,
727
+ maxHeight,
728
+ initialHeight = DEFAULT_HEIGHT,
729
+ animatedHeight: _animatedHeight,
730
+ // no-op on web (DEV benchmark seam)
731
+ benchMotion: _benchMotion,
732
+ // no-op on web (DEV benchmark seam)
733
+ initialMode = "bottomSheet",
734
+ onModeChange,
735
+ persistenceKey,
736
+ enablePersistence = true,
737
+ initialFloatingPosition,
738
+ footer,
739
+ footerHeight = 0,
740
+ onBack: _onBack,
741
+ // native never invokes it from JsModal either
742
+ onMinimize,
743
+ minimizeTargetPosition,
744
+ initialModalState,
745
+ zIndex,
746
+ disableScrollWrapper = false
747
+ }) => {
748
+ const [insets] = (0, _react.useState)(readSafeAreaInsets);
749
+ const [isStateLoaded, setIsStateLoaded] = (0, _react.useState)(!enablePersistence);
750
+ const [mode, setMode] = (0, _react.useState)(initialMode);
751
+ const [isResizing, setIsResizing] = (0, _react.useState)(false);
752
+ const [isDragging, setIsDragging] = (0, _react.useState)(false);
753
+ const [panelHeight, setPanelHeight] = (0, _react.useState)(initialHeight);
754
+ const [dimensions, setDimensions] = (0, _react.useState)(() => clampRectToSafeArea({
755
+ width: screenW(),
756
+ // native FLOATING_WIDTH = SCREEN.width
757
+ height: FLOATING_HEIGHT,
758
+ top: initialFloatingPosition?.y ?? (screenH() - FLOATING_HEIGHT) / 2,
759
+ left: initialFloatingPosition?.x ?? 0 // flush with left edge
760
+ }, insets));
761
+ const [showHint, setShowHint] = (0, _react.useState)(false);
762
+ const [hintLoaded, setHintLoaded] = (0, _react.useState)(false);
763
+ const hintsDisabled = (0, _index.useHintsDisabled)();
764
+
765
+ // -- imperative value refs (native Animated.Value stand-ins) --------------
766
+ const currentHeightRef = (0, _react.useRef)(initialHeight);
767
+ const currentDimensionsRef = (0, _react.useRef)(dimensions);
768
+ const posRef = (0, _react.useRef)({
769
+ x: dimensions.left,
770
+ y: dimensions.top
771
+ });
772
+
773
+ // -- DOM refs -------------------------------------------------------------
774
+ const sheetWrapperRef = (0, _react.useRef)(null);
775
+ const sheetRef = (0, _react.useRef)(null);
776
+ const floatRef = (0, _react.useRef)(null);
777
+
778
+ // Active rAF animations (cancelled on new visibility runs / unmount).
779
+ const animCancelsRef = (0, _react.useRef)([]);
780
+ const cancelAnims = (0, _react.useCallback)(() => {
781
+ animCancelsRef.current.forEach(c => c());
782
+ animCancelsRef.current = [];
783
+ }, []);
784
+ (0, _react.useEffect)(() => {
785
+ currentDimensionsRef.current = dimensions;
786
+ }, [dimensions]);
787
+ (0, _react.useEffect)(() => cancelAnims, [cancelAnims]);
788
+ const effectiveMaxHeight = maxHeight || screenH() - insets.top;
789
+
790
+ // ==========================================================================
791
+ // PERSISTENCE — exact native key usage, shape and 500ms debounce
792
+ // ==========================================================================
793
+ (0, _react.useEffect)(() => {
794
+ if (!enablePersistence || !persistenceKey) {
795
+ setIsStateLoaded(true);
796
+ return;
797
+ }
798
+ let mounted = true;
799
+ const loadState = async () => {
800
+ const savedState = await ModalStorage.load(persistenceKey);
801
+ if (mounted && savedState) {
802
+ if (savedState.mode) {
803
+ setMode(savedState.mode);
804
+ onModeChange?.(savedState.mode);
805
+ }
806
+ if (savedState.panelHeight) {
807
+ setPanelHeight(savedState.panelHeight);
808
+ currentHeightRef.current = savedState.panelHeight;
809
+ }
810
+ if (savedState.dimensions) {
811
+ // Clamp persisted rects too — a position saved on a different
812
+ // viewport (or before the safe-area fix) may be out of bounds.
813
+ const clamped = clampRectToSafeArea(savedState.dimensions, insets);
814
+ setDimensions(clamped);
815
+ posRef.current = {
816
+ x: clamped.left,
817
+ y: clamped.top
818
+ };
819
+ }
820
+ }
821
+ if (mounted) setIsStateLoaded(true);
822
+ };
823
+ loadState();
824
+ return () => {
825
+ mounted = false;
826
+ };
827
+ // eslint-disable-next-line react-hooks/exhaustive-deps
828
+ }, [persistenceKey, enablePersistence, onModeChange]);
829
+ (0, _react.useEffect)(() => {
830
+ if (!enablePersistence || !persistenceKey || !isStateLoaded) return;
831
+ const timeoutId = setTimeout(() => {
832
+ ModalStorage.save(persistenceKey, {
833
+ mode,
834
+ panelHeight: currentHeightRef.current,
835
+ dimensions,
836
+ isVisible: visible
837
+ });
838
+ }, 500); // native: 500ms debounce
839
+ return () => clearTimeout(timeoutId);
840
+ }, [mode, panelHeight, dimensions, visible, persistenceKey, enablePersistence, isStateLoaded]);
841
+
842
+ // ==========================================================================
843
+ // HINT BANNER — same @react_buoy_modal_hint_acknowledged key as native
844
+ // ==========================================================================
845
+ (0, _react.useEffect)(() => {
846
+ let mounted = true;
847
+ const loadHintStatus = async () => {
848
+ try {
849
+ const hintKey = _devToolsStorageKeys.devToolsStorageKeys.modal.hintAcknowledged();
850
+ const acknowledged = await webPersistentStorage.getItem(hintKey);
851
+ if (mounted) {
852
+ setShowHint(acknowledged !== "true");
853
+ setHintLoaded(true);
854
+ }
855
+ } catch {
856
+ if (mounted) {
857
+ setShowHint(true);
858
+ setHintLoaded(true);
859
+ }
860
+ }
861
+ };
862
+ loadHintStatus();
863
+ return () => {
864
+ mounted = false;
865
+ };
866
+ }, []);
867
+ const handleHintAcknowledge = (0, _react.useCallback)(async () => {
868
+ try {
869
+ const hintKey = _devToolsStorageKeys.devToolsStorageKeys.modal.hintAcknowledged();
870
+ await webPersistentStorage.setItem(hintKey, "true");
871
+ setShowHint(false);
872
+ } catch {
873
+ setShowHint(false);
874
+ }
875
+ }, []);
876
+
877
+ // ==========================================================================
878
+ // MODE TOGGLE + MINIMIZE
879
+ // ==========================================================================
880
+ const toggleMode = (0, _react.useCallback)(() => {
881
+ setIsDragging(false);
882
+ setIsResizing(false);
883
+ setMode(prev => {
884
+ const newMode = prev === "bottomSheet" ? "floating" : "bottomSheet";
885
+ onModeChange?.(newMode);
886
+ return newMode;
887
+ });
888
+ }, [onModeChange]);
889
+
890
+ // Belt-and-suspenders: also clear flags when mode changes (native parity).
891
+ (0, _react.useEffect)(() => {
892
+ setIsDragging(false);
893
+ setIsResizing(false);
894
+ }, [mode]);
895
+ const handleMinimize = (0, _react.useCallback)(() => {
896
+ if (!onMinimize) return;
897
+ const currentState = {
898
+ mode,
899
+ panelHeight: currentHeightRef.current,
900
+ floatingPosition: {
901
+ x: currentDimensionsRef.current.left,
902
+ y: currentDimensionsRef.current.top
903
+ },
904
+ floatingDimensions: {
905
+ width: currentDimensionsRef.current.width,
906
+ height: currentDimensionsRef.current.height
907
+ }
908
+ };
909
+
910
+ // native defaults: SCREEN.width - 50 / SCREEN.height - 150
911
+ const targetX = minimizeTargetPosition?.x ?? screenW() - 50;
912
+ const targetY = minimizeTargetPosition?.y ?? screenH() - 150;
913
+ cancelAnims();
914
+ if (mode === "bottomSheet") {
915
+ const wrapper = sheetWrapperRef.current;
916
+ if (!wrapper) {
917
+ onMinimize(currentState);
918
+ return;
919
+ }
920
+ const distance = screenH();
921
+ // native: parallel timing(300) opacity→0 + translateY→SCREEN.height
922
+ animCancelsRef.current.push(runAnim(300, easeInOut, v => {
923
+ wrapper.style.opacity = String(1 - v);
924
+ wrapper.style.transform = `translateY(${v * distance}px)`;
925
+ }, () => onMinimize(currentState)));
926
+ } else {
927
+ const el = floatRef.current;
928
+ if (!el) {
929
+ onMinimize(currentState);
930
+ return;
931
+ }
932
+ const from = {
933
+ ...posRef.current
934
+ };
935
+ // native animates floatingScale too, but the floating render never
936
+ // applies scale in its transform list — so visually it's fade + move,
937
+ // which is what we replicate.
938
+ animCancelsRef.current.push(runAnim(300, easeInOut, v => {
939
+ el.style.opacity = String(1 - v);
940
+ const x = from.x + (targetX - from.x) * v;
941
+ const y = from.y + (targetY - from.y) * v;
942
+ el.style.transform = `translate(${x}px, ${y}px)`;
943
+ }, () => onMinimize(currentState)));
944
+ }
945
+ }, [mode, onMinimize, minimizeTargetPosition, cancelAnims]);
946
+
947
+ // Apply initial modal state when restoring from minimized (native parity).
948
+ (0, _react.useEffect)(() => {
949
+ if (!initialModalState) return;
950
+ if (initialModalState.mode) {
951
+ setMode(initialModalState.mode);
952
+ onModeChange?.(initialModalState.mode);
953
+ }
954
+ if (initialModalState.panelHeight) {
955
+ setPanelHeight(initialModalState.panelHeight);
956
+ currentHeightRef.current = initialModalState.panelHeight;
957
+ }
958
+ if (initialModalState.floatingDimensions && initialModalState.floatingPosition) {
959
+ const newDimensions = {
960
+ width: initialModalState.floatingDimensions.width,
961
+ height: initialModalState.floatingDimensions.height,
962
+ top: initialModalState.floatingPosition.y,
963
+ left: initialModalState.floatingPosition.x
964
+ };
965
+ setDimensions(newDimensions);
966
+ posRef.current = {
967
+ x: initialModalState.floatingPosition.x,
968
+ y: initialModalState.floatingPosition.y
969
+ };
970
+ }
971
+ // eslint-disable-next-line react-hooks/exhaustive-deps
972
+ }, [initialModalState]);
973
+
974
+ // ==========================================================================
975
+ // VISIBILITY / OPEN ANIMATIONS (mirrors native visibility effect: runs on
976
+ // visible AND mode changes; visible=false skips — the hidden tree renders)
977
+ // ==========================================================================
978
+ (0, _react.useLayoutEffect)(() => {
979
+ if (!visible) return;
980
+ cancelAnims();
981
+ if (mode === "bottomSheet") {
982
+ const wrapper = sheetWrapperRef.current;
983
+ if (!wrapper) return;
984
+ const distance = screenH();
985
+ // native: reset translateY to SCREEN.height + progress 0, then
986
+ // spring(tension 180, friction 22) → 0 and timing(200) opacity → 1.
987
+ wrapper.style.transform = `translateY(${distance}px)`;
988
+ wrapper.style.opacity = "0";
989
+ animCancelsRef.current.push(
990
+ // spring stand-in: 300ms ease-out-cubic (logged deviation)
991
+ runAnim(300, easeOutCubic, v => {
992
+ wrapper.style.transform = `translateY(${(1 - v) * distance}px)`;
993
+ }), runAnim(200, easeInOut, v => {
994
+ wrapper.style.opacity = String(v);
995
+ }));
996
+ } else {
997
+ const el = floatRef.current;
998
+ if (!el) return;
999
+ // Restore-from-minimize deviation (see header): re-sync position from
1000
+ // the last committed rect so the panel doesn't reopen at the minimize
1001
+ // target the web AppHost never restores via initialModalState.
1002
+ posRef.current = {
1003
+ x: currentDimensionsRef.current.left,
1004
+ y: currentDimensionsRef.current.top
1005
+ };
1006
+ el.style.transform = `translate(${posRef.current.x}px, ${posRef.current.y}px)`;
1007
+ // native: floatingScale jumps to 1 (no pop) + timing(200) opacity → 1
1008
+ el.style.opacity = "0";
1009
+ animCancelsRef.current.push(runAnim(200, easeInOut, v => {
1010
+ el.style.opacity = String(v);
1011
+ }));
1012
+ }
1013
+ }, [visible, mode, cancelAnims]);
1014
+
1015
+ // ==========================================================================
1016
+ // TAP COUNTING — native spec: 500ms window, 300ms settle;
1017
+ // 2 taps → toggle mode, ≥3 taps → minimize
1018
+ // ==========================================================================
1019
+ const lastTapRef = (0, _react.useRef)(0);
1020
+ const tapCountRef = (0, _react.useRef)(0);
1021
+ const tapTimeoutRef = (0, _react.useRef)(null);
1022
+ const registerTap = (0, _react.useCallback)(() => {
1023
+ const now = Date.now();
1024
+ if (now - lastTapRef.current > 500) {
1025
+ tapCountRef.current = 0;
1026
+ }
1027
+ tapCountRef.current++;
1028
+ lastTapRef.current = now;
1029
+ if (tapTimeoutRef.current) clearTimeout(tapTimeoutRef.current);
1030
+ tapTimeoutRef.current = setTimeout(() => {
1031
+ if (tapCountRef.current === 2) {
1032
+ toggleMode();
1033
+ } else if (tapCountRef.current >= 3) {
1034
+ if (onMinimize) handleMinimize();
1035
+ }
1036
+ tapCountRef.current = 0;
1037
+ }, 300);
1038
+ }, [toggleMode, onMinimize, handleMinimize]);
1039
+ (0, _react.useEffect)(() => {
1040
+ return () => {
1041
+ if (tapTimeoutRef.current) clearTimeout(tapTimeoutRef.current);
1042
+ };
1043
+ }, []);
1044
+
1045
+ // ==========================================================================
1046
+ // GESTURE: bottom-sheet header drag-resize (native bottomSheetPanResponder)
1047
+ // ==========================================================================
1048
+ const sheetGestureRef = (0, _react.useRef)({
1049
+ active: false,
1050
+ resizing: false,
1051
+ startY: 0,
1052
+ headerOffsetY: 0,
1053
+ samples: []
1054
+ });
1055
+ const handleSheetPointerDown = (0, _react.useCallback)(e => {
1056
+ if (mode !== "bottomSheet") return;
1057
+ // Ignore clicks on interactive header children (window controls live
1058
+ // outside the header, but custom content may contain buttons).
1059
+ if (e.target.closest("button")) return;
1060
+ const g = sheetGestureRef.current;
1061
+ g.active = true;
1062
+ g.resizing = false;
1063
+ g.startY = e.clientY;
1064
+ // native onPanResponderGrant: locationY — where inside the header the
1065
+ // pointer grabbed (absolute finger anchoring).
1066
+ const rect = e.currentTarget.getBoundingClientRect();
1067
+ g.headerOffsetY = e.clientY - rect.top;
1068
+ g.samples = [{
1069
+ t: performance.now(),
1070
+ y: e.clientY
1071
+ }];
1072
+ e.currentTarget.setPointerCapture(e.pointerId);
1073
+ }, [mode]);
1074
+ const handleSheetPointerMove = (0, _react.useCallback)(e => {
1075
+ const g = sheetGestureRef.current;
1076
+ if (!g.active) return;
1077
+ const dy = e.clientY - g.startY;
1078
+ // native onMoveShouldSetPanResponder: |dy| > 3
1079
+ if (!g.resizing && Math.abs(dy) > 3) {
1080
+ g.resizing = true;
1081
+ cancelAnims();
1082
+ setIsResizing(true);
1083
+ }
1084
+ if (!g.resizing) return;
1085
+ // native onPanResponderMove: sheetTop = pageY - headerTouchOffset;
1086
+ // height = SCREEN.height - sheetTop, clamped
1087
+ const sheetTop = e.clientY - g.headerOffsetY;
1088
+ let targetHeight = screenH() - sheetTop;
1089
+ targetHeight = Math.max(minHeight, Math.min(targetHeight, effectiveMaxHeight));
1090
+ currentHeightRef.current = targetHeight;
1091
+ if (sheetRef.current) sheetRef.current.style.height = `${targetHeight}px`;
1092
+ // velocity samples (~last 100ms) for the swipe-to-close check
1093
+ const now = performance.now();
1094
+ g.samples.push({
1095
+ t: now,
1096
+ y: e.clientY
1097
+ });
1098
+ while (g.samples.length > 2 && now - g.samples[0].t > 100) g.samples.shift();
1099
+ }, [minHeight, effectiveMaxHeight, cancelAnims]);
1100
+ const handleSheetPointerUp = (0, _react.useCallback)(e => {
1101
+ const g = sheetGestureRef.current;
1102
+ if (!g.active) return;
1103
+ g.active = false;
1104
+ if (!g.resizing) {
1105
+ // Tap path — native: TouchableWithoutFeedback keeps the touch when
1106
+ // movement stays under the 3px steal threshold.
1107
+ registerTap();
1108
+ return;
1109
+ }
1110
+ setIsResizing(false);
1111
+ const finalHeight = currentHeightRef.current;
1112
+ const dy = e.clientY - g.startY;
1113
+ // vy in px/ms — same unit as RN gestureState.vy
1114
+ const first = g.samples[0];
1115
+ const last = g.samples[g.samples.length - 1];
1116
+ const vy = last && first && last.t > first.t ? (last.y - first.y) / (last.t - first.t) : 0;
1117
+
1118
+ // native close spec: (vy > 0.8 && dy > 50) || (dy > 150 && h <= minHeight)
1119
+ const shouldClose = vy > 0.8 && dy > 50 || dy > 150 && finalHeight <= minHeight;
1120
+ if (shouldClose) {
1121
+ const wrapper = sheetWrapperRef.current;
1122
+ cancelAnims();
1123
+ if (wrapper) {
1124
+ const distance = screenH();
1125
+ // native: timing(200) opacity→0 + spring translateY→SCREEN.height
1126
+ animCancelsRef.current.push(runAnim(200, easeInOut, v => {
1127
+ wrapper.style.opacity = String(1 - v);
1128
+ }), runAnim(300, easeOutCubic, v => {
1129
+ wrapper.style.transform = `translateY(${v * distance}px)`;
1130
+ }, () => onClose()));
1131
+ } else {
1132
+ onClose();
1133
+ }
1134
+ return;
1135
+ }
1136
+
1137
+ // Already at the finger-tracked height; just commit state.
1138
+ setPanelHeight(finalHeight);
1139
+ }, [minHeight, onClose, registerTap, cancelAnims]);
1140
+ const handleSheetPointerCancel = (0, _react.useCallback)(() => {
1141
+ const g = sheetGestureRef.current;
1142
+ if (!g.active) return;
1143
+ g.active = false;
1144
+ g.resizing = false;
1145
+ setIsResizing(false);
1146
+ }, []);
1147
+
1148
+ // ==========================================================================
1149
+ // GESTURE: floating header drag (native DraggableHeader semantics)
1150
+ // ==========================================================================
1151
+ const dragGestureRef = (0, _react.useRef)({
1152
+ active: false,
1153
+ dragging: false,
1154
+ startX: 0,
1155
+ startY: 0,
1156
+ startPos: {
1157
+ x: 0,
1158
+ y: 0
1159
+ }
1160
+ });
1161
+ const handleFloatPointerDown = (0, _react.useCallback)(e => {
1162
+ // native: DraggableHeader enabled = mode === "floating" && !isResizing
1163
+ if (mode !== "floating" || isResizing) return;
1164
+ if (e.target.closest("button")) return;
1165
+ const g = dragGestureRef.current;
1166
+ g.active = true;
1167
+ g.dragging = false;
1168
+ g.startX = e.clientX;
1169
+ g.startY = e.clientY;
1170
+ g.startPos = {
1171
+ ...posRef.current
1172
+ };
1173
+ e.currentTarget.setPointerCapture(e.pointerId);
1174
+ }, [mode, isResizing]);
1175
+ const handleFloatPointerMove = (0, _react.useCallback)(e => {
1176
+ const g = dragGestureRef.current;
1177
+ if (!g.active) return;
1178
+ const dx = e.clientX - g.startX;
1179
+ const dy = e.clientY - g.startY;
1180
+ // native DraggableHeader: drag begins when |dx| + |dy| > 5
1181
+ if (!g.dragging && Math.abs(dx) + Math.abs(dy) > 5) {
1182
+ g.dragging = true;
1183
+ setIsDragging(true);
1184
+ }
1185
+ if (!g.dragging) return;
1186
+ // relative dx/dy motion, unclamped during move (native parity)
1187
+ const x = g.startPos.x + dx;
1188
+ const y = g.startPos.y + dy;
1189
+ posRef.current = {
1190
+ x,
1191
+ y
1192
+ };
1193
+ if (floatRef.current) {
1194
+ floatRef.current.style.transform = `translate(${x}px, ${y}px)`;
1195
+ }
1196
+ }, []);
1197
+ const handleFloatPointerUp = (0, _react.useCallback)(() => {
1198
+ const g = dragGestureRef.current;
1199
+ if (!g.active) return;
1200
+ g.active = false;
1201
+ if (!g.dragging) {
1202
+ registerTap(); // native: DraggableHeader onTap → handleFloatingTap
1203
+ return;
1204
+ }
1205
+ setIsDragging(false);
1206
+ // Release clamp keeps the whole window inside the safe area (all four
1207
+ // insets — deliberate improvement over native, which only clamps top).
1208
+ const dims = currentDimensionsRef.current;
1209
+ const clampedRect = clampRectToSafeArea({
1210
+ ...dims,
1211
+ left: posRef.current.x,
1212
+ top: posRef.current.y
1213
+ }, insets);
1214
+ const clampedX = clampedRect.left;
1215
+ const clampedY = clampedRect.top;
1216
+ posRef.current = {
1217
+ x: clampedX,
1218
+ y: clampedY
1219
+ };
1220
+ if (floatRef.current) {
1221
+ floatRef.current.style.transform = `translate(${clampedX}px, ${clampedY}px)`;
1222
+ }
1223
+ setDimensions(prev => ({
1224
+ ...prev,
1225
+ left: clampedX,
1226
+ top: clampedY
1227
+ }));
1228
+ }, [insets, registerTap]);
1229
+ const handleFloatPointerCancel = (0, _react.useCallback)(() => {
1230
+ const g = dragGestureRef.current;
1231
+ if (!g.active) return;
1232
+ g.active = false;
1233
+ g.dragging = false;
1234
+ setIsDragging(false);
1235
+ }, []);
1236
+
1237
+ // ==========================================================================
1238
+ // GESTURE: floating corner resize (native createResizeHandler — only the
1239
+ // bottom corners are rendered, to avoid conflicts with the window controls)
1240
+ // ==========================================================================
1241
+ const resizeGestureRef = (0, _react.useRef)({
1242
+ active: false,
1243
+ corner: "bottomRight",
1244
+ startX: 0,
1245
+ startY: 0,
1246
+ start: {
1247
+ width: 0,
1248
+ height: 0,
1249
+ top: 0,
1250
+ left: 0
1251
+ }
1252
+ });
1253
+ const handleResizePointerDown = (0, _react.useCallback)(corner => e => {
1254
+ if (mode !== "floating") return;
1255
+ e.stopPropagation();
1256
+ const g = resizeGestureRef.current;
1257
+ g.active = true;
1258
+ g.corner = corner;
1259
+ g.startX = e.clientX;
1260
+ g.startY = e.clientY;
1261
+ g.start = {
1262
+ ...currentDimensionsRef.current
1263
+ };
1264
+ setIsResizing(true);
1265
+ e.currentTarget.setPointerCapture(e.pointerId);
1266
+ }, [mode]);
1267
+ const handleResizePointerMove = (0, _react.useCallback)(e => {
1268
+ const g = resizeGestureRef.current;
1269
+ if (!g.active) return;
1270
+ const dx = e.clientX - g.startX;
1271
+ const dy = e.clientY - g.startY;
1272
+ // native: moves under 0.5px are ignored
1273
+ if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) return;
1274
+
1275
+ // native safe-area-aware bounds
1276
+ const minLeft = Math.max(0, insets.left);
1277
+ const maxBottom = screenH() - Math.max(0, insets.bottom);
1278
+ const maxRight = screenW() - Math.max(0, insets.right);
1279
+ const floatingMinWidth = screenW() * 0.25; // native FLOATING_MIN_WIDTH
1280
+ const start = g.start;
1281
+ const startRight = start.left + start.width;
1282
+ const startBottom = start.top + start.height;
1283
+ let left = start.left;
1284
+ const top = start.top; // bottom corners anchor the top edge
1285
+ let right = startRight;
1286
+ let bottom = startBottom;
1287
+ if (g.corner === "bottomLeft") {
1288
+ // move left & bottom; anchor right & top
1289
+ left = Math.max(minLeft, Math.min(start.left + dx, startRight - floatingMinWidth));
1290
+ bottom = Math.min(maxBottom, Math.max(startBottom + dy, start.top + FLOATING_MIN_HEIGHT));
1291
+ } else {
1292
+ // bottomRight: move right & bottom; anchor left & top
1293
+ right = Math.min(maxRight, Math.max(startRight + dx, start.left + floatingMinWidth));
1294
+ bottom = Math.min(maxBottom, Math.max(startBottom + dy, start.top + FLOATING_MIN_HEIGHT));
1295
+ }
1296
+ const updatedWidth = Math.max(floatingMinWidth, right - left);
1297
+ const updatedHeight = Math.max(FLOATING_MIN_HEIGHT, bottom - top);
1298
+ const next = {
1299
+ width: updatedWidth,
1300
+ height: updatedHeight,
1301
+ left,
1302
+ top
1303
+ };
1304
+ currentDimensionsRef.current = next;
1305
+ posRef.current = {
1306
+ x: left,
1307
+ y: top
1308
+ };
1309
+ // native pushes setDimensions per move (state drives width/height)
1310
+ setDimensions(next);
1311
+ if (floatRef.current) {
1312
+ floatRef.current.style.transform = `translate(${left}px, ${top}px)`;
1313
+ }
1314
+ }, [insets.left, insets.right, insets.bottom]);
1315
+ const handleResizePointerUp = (0, _react.useCallback)(() => {
1316
+ const g = resizeGestureRef.current;
1317
+ if (!g.active) return;
1318
+ g.active = false;
1319
+ setIsResizing(false);
1320
+ setDimensions(currentDimensionsRef.current);
1321
+ }, []);
1322
+
1323
+ // ==========================================================================
1324
+ // RENDER
1325
+ // ==========================================================================
1326
+
1327
+ // Hidden but mounted — panel state and listeners survive minimize/close-
1328
+ // to-background, same as native's offscreen container.
1329
+ // The three branches below carry distinct keys ON PURPOSE: the open/
1330
+ // minimize animations write opacity/transform imperatively (rAF, outside
1331
+ // React's style props), so if React reused a DOM node across branches —
1332
+ // e.g. a fresh mount whose async-persisted mode flips bottomSheet→floating
1333
+ // mid-entrance — the old sheet wrapper (opacity ≈ 0, translateY offscreen)
1334
+ // would be recycled as the floating header and render a blank strip.
1335
+ // React never clears styles it didn't set via props; distinct keys force a
1336
+ // clean remount of the chrome instead. (Children remount with it, matching
1337
+ // native, whose mode branches are different trees too — tool panels hold
1338
+ // their state above JsModal or in external stores.)
1339
+ if (!visible) {
1340
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
1341
+ style: {
1342
+ display: "none"
1343
+ },
1344
+ "aria-hidden": true,
1345
+ children: children
1346
+ }, "hidden");
1347
+ }
1348
+ const dialogLabel = header?.title || "Dev tool panel";
1349
+
1350
+ // Content block shared by both modes — native styles.content:
1351
+ // flex 1, bg base #121212, borderBottomRadius 16, overflow hidden.
1352
+ const contentBlock = /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
1353
+ style: {
1354
+ flex: 1,
1355
+ minHeight: 0,
1356
+ position: "relative",
1357
+ display: "flex",
1358
+ flexDirection: "column",
1359
+ backgroundColor: buoy.base,
1360
+ borderBottomLeftRadius: 16,
1361
+ borderBottomRightRadius: 16,
1362
+ overflow: "hidden",
1363
+ ...customStyles.content
1364
+ },
1365
+ children: [disableScrollWrapper ?
1366
+ /*#__PURE__*/
1367
+ // children manage their own scrolling (native: plain flex View)
1368
+ (0, _jsxRuntime.jsx)("div", {
1369
+ style: {
1370
+ flex: 1,
1371
+ minHeight: 0,
1372
+ display: "flex",
1373
+ flexDirection: "column",
1374
+ paddingBottom: footerHeight
1375
+ },
1376
+ children: children
1377
+ }) :
1378
+ /*#__PURE__*/
1379
+ // native: ScrollView with flexGrow 1 + paddingBottom footerHeight
1380
+ (0, _jsxRuntime.jsx)("div", {
1381
+ style: {
1382
+ flex: 1,
1383
+ minHeight: 0,
1384
+ overflowY: "auto",
1385
+ paddingBottom: footerHeight
1386
+ },
1387
+ children: children
1388
+ }), footer ? /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
1389
+ style: {
1390
+ // native footerStyles.footerContainer
1391
+ position: "absolute",
1392
+ left: 0,
1393
+ right: 0,
1394
+ bottom: 0,
1395
+ backgroundColor: buoy.base,
1396
+ borderBottomLeftRadius: 16,
1397
+ borderBottomRightRadius: 16
1398
+ },
1399
+ children: footer
1400
+ }) : null]
1401
+ });
1402
+ const windowControlsBlock =
1403
+ /*#__PURE__*/
1404
+ // native windowControlsContainer: absolute top 4 right 4, zIndex 10
1405
+ (0, _jsxRuntime.jsx)("div", {
1406
+ style: {
1407
+ position: "absolute",
1408
+ top: 4,
1409
+ right: 4,
1410
+ zIndex: 10
1411
+ },
1412
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(WindowControls, {
1413
+ onClose: onClose,
1414
+ onMinimize: onMinimize ? handleMinimize : undefined,
1415
+ onToggleMode: toggleMode,
1416
+ mode: mode
1417
+ })
1418
+ });
1419
+
1420
+ // ----- FLOATING MODE ------------------------------------------------------
1421
+ if (mode === "floating") {
1422
+ const active = isDragging || isResizing;
1423
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
1424
+ ref: floatRef,
1425
+ role: "dialog",
1426
+ "aria-label": dialogLabel,
1427
+ style: {
1428
+ // native floatingModal: absolute, card bg, radius 6, 1px #333
1429
+ // border, teal glow (shadowOpacity 0.3, radius 20), zIndex 1000.
1430
+ // Dragging: 2px teal border + tighter teal glow
1431
+ // (primary+"99" × shadowOpacity 0.8 ≈ 0.48 effective alpha).
1432
+ position: "fixed",
1433
+ left: 0,
1434
+ top: 0,
1435
+ width: dimensions.width,
1436
+ height: dimensions.height,
1437
+ transform: `translate(${posRef.current.x}px, ${posRef.current.y}px)`,
1438
+ display: "flex",
1439
+ flexDirection: "column",
1440
+ backgroundColor: buoy.card,
1441
+ borderRadius: 6,
1442
+ border: active ? `2px solid ${buoy.primary}` : `1px solid ${buoy.border}`,
1443
+ boxShadow: active ? `0 0 12px ${buoy.primary}7A` : `0 0 20px ${buoy.primary}4D`,
1444
+ boxSizing: "border-box",
1445
+ // RN borders draw inside the rect
1446
+ zIndex: zIndex ?? 1000,
1447
+ ...customStyles.container
1448
+ },
1449
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
1450
+ onPointerDown: handleFloatPointerDown,
1451
+ onPointerMove: handleFloatPointerMove,
1452
+ onPointerUp: handleFloatPointerUp,
1453
+ onPointerCancel: handleFloatPointerCancel,
1454
+ style: {
1455
+ // native styles.header (+ floatingHeader top radius 6)
1456
+ borderTopLeftRadius: 6,
1457
+ borderTopRightRadius: 6,
1458
+ backgroundColor: buoy.card,
1459
+ minHeight: 56,
1460
+ border: `1px solid ${buoy.border}`,
1461
+ boxSizing: "border-box",
1462
+ display: "flex",
1463
+ flexDirection: "column",
1464
+ justifyContent: "center",
1465
+ flexShrink: 0,
1466
+ touchAction: "none",
1467
+ userSelect: "none",
1468
+ cursor: isDragging ? "grabbing" : "grab" // web-only affordance
1469
+ },
1470
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(ModalHeaderContent, {
1471
+ header: header,
1472
+ isResizing: isDragging || isResizing,
1473
+ mode: mode,
1474
+ onMinimize: onMinimize ? handleMinimize : undefined
1475
+ })
1476
+ }), windowControlsBlock, contentBlock, ["bottomLeft", "bottomRight"].map(corner => /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
1477
+ onPointerDown: handleResizePointerDown(corner),
1478
+ onPointerMove: handleResizePointerMove,
1479
+ onPointerUp: handleResizePointerUp,
1480
+ onPointerCancel: handleResizePointerUp,
1481
+ style: {
1482
+ position: "absolute",
1483
+ width: 30,
1484
+ height: 30,
1485
+ bottom: 4,
1486
+ ...(corner === "bottomLeft" ? {
1487
+ left: 4
1488
+ } : {
1489
+ right: 4
1490
+ }),
1491
+ zIndex: 1000,
1492
+ touchAction: "none",
1493
+ cursor: corner === "bottomLeft" ? "nesw-resize" : "nwse-resize"
1494
+ },
1495
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
1496
+ style: {
1497
+ width: 20,
1498
+ height: 20,
1499
+ borderRadius: 10,
1500
+ boxSizing: "border-box",
1501
+ backgroundColor: active ? `${gameUI.success}1A` : "transparent",
1502
+ border: active ? `2px solid ${gameUI.success}` : "none",
1503
+ boxShadow: active ? `0 0 8px ${gameUI.success}99` : "none"
1504
+ }
1505
+ })
1506
+ }, corner))]
1507
+ }, "floating");
1508
+ }
1509
+
1510
+ // ----- BOTTOM SHEET MODE --------------------------------------------------
1511
+ return (
1512
+ /*#__PURE__*/
1513
+ // native fullScreenContainer: absolute fill, zIndex 1000, box-none
1514
+ (0, _jsxRuntime.jsx)("div", {
1515
+ style: {
1516
+ position: "fixed",
1517
+ inset: 0,
1518
+ zIndex: zIndex ?? 1000,
1519
+ pointerEvents: "none"
1520
+ },
1521
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
1522
+ ref: sheetWrapperRef,
1523
+ style: {
1524
+ position: "absolute",
1525
+ bottom: 0,
1526
+ left: 0,
1527
+ right: 0,
1528
+ pointerEvents: "auto"
1529
+ },
1530
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
1531
+ ref: sheetRef,
1532
+ role: "dialog",
1533
+ "aria-label": dialogLabel,
1534
+ style: {
1535
+ // native bottomSheet: card bg, top radius 6, 1px #333 border,
1536
+ // teal shadow (offset 0/-4, opacity 0.2, radius 12)
1537
+ height: currentHeightRef.current,
1538
+ display: "flex",
1539
+ flexDirection: "column",
1540
+ backgroundColor: buoy.card,
1541
+ borderTopLeftRadius: 6,
1542
+ borderTopRightRadius: 6,
1543
+ border: `1px solid ${buoy.border}`,
1544
+ boxShadow: `0 -4px 12px ${buoy.primary}33`,
1545
+ boxSizing: "border-box",
1546
+ position: "relative",
1547
+ ...customStyles.container
1548
+ },
1549
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
1550
+ onPointerDown: handleSheetPointerDown,
1551
+ onPointerMove: handleSheetPointerMove,
1552
+ onPointerUp: handleSheetPointerUp,
1553
+ onPointerCancel: handleSheetPointerCancel,
1554
+ style: {
1555
+ // native styles.header
1556
+ borderTopLeftRadius: 6,
1557
+ borderTopRightRadius: 6,
1558
+ backgroundColor: buoy.card,
1559
+ minHeight: 56,
1560
+ border: `1px solid ${buoy.border}`,
1561
+ boxSizing: "border-box",
1562
+ display: "flex",
1563
+ flexDirection: "column",
1564
+ justifyContent: "center",
1565
+ flexShrink: 0,
1566
+ touchAction: "none",
1567
+ userSelect: "none",
1568
+ cursor: "ns-resize" // web-only affordance
1569
+ },
1570
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(ModalHeaderContent, {
1571
+ header: header,
1572
+ isResizing: isResizing,
1573
+ mode: mode,
1574
+ onMinimize: onMinimize ? handleMinimize : undefined
1575
+ })
1576
+ }), windowControlsBlock, hintLoaded && showHint && !hintsDisabled && /*#__PURE__*/(0, _jsxRuntime.jsx)(ModalHintBanner, {
1577
+ onAcknowledge: handleHintAcknowledge
1578
+ }), contentBlock]
1579
+ })
1580
+ })
1581
+ }, "bottomSheet")
1582
+ );
1583
+ };
1584
+
1585
+ // ============================================================================
1586
+ // EXPORT — memoized like native
1587
+ // ============================================================================
1588
+ const JsModal = exports.JsModal = /*#__PURE__*/(0, _react.memo)(JsModalComponent);