@buoy-gg/shared-ui 7.0.15 → 7.0.17
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/lib/commonjs/JsModal.web.js +1588 -0
- package/lib/commonjs/env/EnvironmentIndicator.web.js +114 -0
- package/lib/commonjs/env/EnvironmentSelectorInline.web.js +178 -0
- package/lib/commonjs/hooks/index.js +7 -0
- package/lib/commonjs/hooks/safe-area-impl.js +1 -1
- package/lib/commonjs/hooks/usePersistedState.js +71 -0
- package/lib/commonjs/icons/index.js +28 -0
- package/lib/commonjs/index.js +45 -3
- package/lib/commonjs/storage/devToolsStorageKeys.js +22 -0
- package/lib/commonjs/stores/ignoredPatternsCore.js +68 -0
- package/lib/commonjs/stores/ignoredPatternsStore.js +16 -52
- package/lib/commonjs/stores/ignoredPatternsStore.web.js +236 -0
- package/lib/commonjs/stores/index.js +1 -1
- package/lib/commonjs/ui/components/ModalHeader.js +69 -0
- package/lib/commonjs/ui/components/ToolButton.js +90 -0
- package/lib/commonjs/ui/components/index.js +7 -0
- package/lib/commonjs/ui/gameUI/constants/gameUIColors.js +5 -3
- package/lib/commonjs/ui/index.js +8 -0
- package/lib/commonjs/ui/toolTheme.js +96 -0
- package/lib/commonjs/utils/index.js +38 -0
- package/lib/commonjs/utils/stateSnapshotRegistry.js +62 -0
- package/lib/module/JsModal.web.js +1586 -0
- package/lib/module/env/EnvironmentIndicator.web.js +108 -0
- package/lib/module/env/EnvironmentSelectorInline.web.js +173 -0
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/safe-area-impl.js +1 -1
- package/lib/module/hooks/usePersistedState.js +68 -0
- package/lib/module/icons/index.js +2 -0
- package/lib/module/index.js +7 -3
- package/lib/module/storage/devToolsStorageKeys.js +22 -0
- package/lib/module/stores/ignoredPatternsCore.js +62 -0
- package/lib/module/stores/ignoredPatternsStore.js +5 -51
- package/lib/module/stores/ignoredPatternsStore.web.js +221 -0
- package/lib/module/stores/index.js +1 -1
- package/lib/module/ui/components/ModalHeader.js +70 -2
- package/lib/module/ui/components/ToolButton.js +86 -0
- package/lib/module/ui/components/index.js +1 -0
- package/lib/module/ui/gameUI/constants/gameUIColors.js +4 -2
- package/lib/module/ui/index.js +3 -0
- package/lib/module/ui/toolTheme.js +93 -0
- package/lib/module/utils/index.js +2 -0
- package/lib/module/utils/stateSnapshotRegistry.js +55 -0
- package/lib/typescript/commonjs/JsModal.web.d.ts +138 -0
- package/lib/typescript/commonjs/JsModal.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/env/EnvironmentIndicator.web.d.ts +16 -0
- package/lib/typescript/commonjs/env/EnvironmentIndicator.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/env/EnvironmentSelectorInline.web.d.ts +8 -0
- package/lib/typescript/commonjs/env/EnvironmentSelectorInline.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/index.d.ts +1 -0
- package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
- package/lib/typescript/commonjs/hooks/usePersistedState.d.ts +16 -0
- package/lib/typescript/commonjs/hooks/usePersistedState.d.ts.map +1 -0
- package/lib/typescript/commonjs/icons/index.d.ts +2 -0
- package/lib/typescript/commonjs/icons/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts +19 -0
- package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts.map +1 -1
- package/lib/typescript/commonjs/stores/ignoredPatternsCore.d.ts +37 -0
- package/lib/typescript/commonjs/stores/ignoredPatternsCore.d.ts.map +1 -0
- package/lib/typescript/commonjs/stores/ignoredPatternsStore.d.ts +3 -27
- package/lib/typescript/commonjs/stores/ignoredPatternsStore.d.ts.map +1 -1
- package/lib/typescript/commonjs/stores/ignoredPatternsStore.web.d.ts +72 -0
- package/lib/typescript/commonjs/stores/ignoredPatternsStore.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts +13 -1
- package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ToolButton.d.ts +24 -0
- package/lib/typescript/commonjs/ui/components/ToolButton.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/index.d.ts +4 -0
- package/lib/typescript/commonjs/ui/components/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts +56 -0
- package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts +1 -0
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/toolTheme.d.ts +68 -0
- package/lib/typescript/commonjs/ui/toolTheme.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/index.d.ts +2 -0
- package/lib/typescript/commonjs/utils/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/stateSnapshotRegistry.d.ts +67 -0
- package/lib/typescript/commonjs/utils/stateSnapshotRegistry.d.ts.map +1 -0
- package/lib/typescript/module/JsModal.web.d.ts +138 -0
- package/lib/typescript/module/JsModal.web.d.ts.map +1 -0
- package/lib/typescript/module/env/EnvironmentIndicator.web.d.ts +16 -0
- package/lib/typescript/module/env/EnvironmentIndicator.web.d.ts.map +1 -0
- package/lib/typescript/module/env/EnvironmentSelectorInline.web.d.ts +8 -0
- package/lib/typescript/module/env/EnvironmentSelectorInline.web.d.ts.map +1 -0
- package/lib/typescript/module/hooks/index.d.ts +1 -0
- package/lib/typescript/module/hooks/index.d.ts.map +1 -1
- package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
- package/lib/typescript/module/hooks/usePersistedState.d.ts +16 -0
- package/lib/typescript/module/hooks/usePersistedState.d.ts.map +1 -0
- package/lib/typescript/module/icons/index.d.ts +2 -0
- package/lib/typescript/module/icons/index.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/storage/devToolsStorageKeys.d.ts +19 -0
- package/lib/typescript/module/storage/devToolsStorageKeys.d.ts.map +1 -1
- package/lib/typescript/module/stores/ignoredPatternsCore.d.ts +37 -0
- package/lib/typescript/module/stores/ignoredPatternsCore.d.ts.map +1 -0
- package/lib/typescript/module/stores/ignoredPatternsStore.d.ts +3 -27
- package/lib/typescript/module/stores/ignoredPatternsStore.d.ts.map +1 -1
- package/lib/typescript/module/stores/ignoredPatternsStore.web.d.ts +72 -0
- package/lib/typescript/module/stores/ignoredPatternsStore.web.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/ModalHeader.d.ts +13 -1
- package/lib/typescript/module/ui/components/ModalHeader.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ToolButton.d.ts +24 -0
- package/lib/typescript/module/ui/components/ToolButton.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/index.d.ts +4 -0
- package/lib/typescript/module/ui/components/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts +56 -0
- package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
- package/lib/typescript/module/ui/index.d.ts +1 -0
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/toolTheme.d.ts +68 -0
- package/lib/typescript/module/ui/toolTheme.d.ts.map +1 -0
- package/lib/typescript/module/utils/index.d.ts +2 -0
- package/lib/typescript/module/utils/index.d.ts.map +1 -1
- package/lib/typescript/module/utils/stateSnapshotRegistry.d.ts +67 -0
- package/lib/typescript/module/utils/stateSnapshotRegistry.d.ts.map +1 -0
- 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);
|