@1outres/dsh-plugin-mobile 0.0.0-stage → 0.6.0

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/client.js ADDED
@@ -0,0 +1,1394 @@
1
+ /* Generated by scripts/build.mjs — do not edit. */
2
+ window.__ModuleLoader__.load({
3
+ id: "@1outres/dsh-plugin-mobile",
4
+ factory: (require) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ "use strict";
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
25
+
26
+ // src/client/index.tsx
27
+ var index_exports = {};
28
+ __export(index_exports, {
29
+ HEADER_SLOT: () => HEADER_SLOT,
30
+ OVERLAY_SLOT: () => OVERLAY_SLOT,
31
+ SETTINGS_SLOT: () => SETTINGS_SLOT,
32
+ apply: () => apply,
33
+ inject: () => inject
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/client/components.tsx
38
+ var import_react2 = require("react");
39
+
40
+ // src/client/frame.ts
41
+ var import_react = require("react");
42
+ var FRAME = "div:has(> [data-rightbar-col])";
43
+ var COLLAPSED_ATTRIBUTE = "data-sidebar-collapsed";
44
+ var FULLSCREEN_ATTRIBUTE = "data-rightbar-fullscreen";
45
+ function findFrame(root) {
46
+ return root.querySelector("[data-rightbar-col]")?.parentElement ?? null;
47
+ }
48
+ function isExpanded(frame) {
49
+ return frame !== null && !frame.hasAttribute(COLLAPSED_ATTRIBUTE);
50
+ }
51
+ var NO_SWIPE = "pre, table, input, textarea, select, [contenteditable='true'], [role='slider'], [data-dsh-mobile-no-swipe]";
52
+ function swipeBlocked(target, root, overflowX) {
53
+ if (findFrame(root)?.hasAttribute(FULLSCREEN_ATTRIBUTE) === true) return true;
54
+ if (root.querySelector("[aria-modal='true']") !== null) return true;
55
+ const element = target;
56
+ if (element === null || typeof element.closest !== "function") return false;
57
+ if (element.closest(NO_SWIPE) !== null) return true;
58
+ let current = element;
59
+ for (let depth = 0; current !== null && depth < 12; depth += 1) {
60
+ if (current.scrollWidth > current.clientWidth + 1) {
61
+ const overflow = overflowX(current);
62
+ if (overflow === "auto" || overflow === "scroll") return true;
63
+ }
64
+ current = current.parentElement;
65
+ }
66
+ return false;
67
+ }
68
+ function useFrameFlag(anchor, attribute) {
69
+ const subscribe = (0, import_react.useCallback)(
70
+ (listener) => {
71
+ const frame = anchor.current?.closest(FRAME);
72
+ if (frame === void 0 || frame === null) return () => {
73
+ };
74
+ const observer = new MutationObserver(listener);
75
+ observer.observe(frame, { attributes: true, attributeFilter: [attribute] });
76
+ return () => observer.disconnect();
77
+ },
78
+ [anchor, attribute]
79
+ );
80
+ const read = (0, import_react.useCallback)(
81
+ () => anchor.current?.closest(FRAME)?.hasAttribute(attribute),
82
+ [anchor, attribute]
83
+ );
84
+ return (0, import_react.useSyncExternalStore)(subscribe, read, () => void 0);
85
+ }
86
+
87
+ // src/client/icons.tsx
88
+ var import_jsx_runtime = require("react/jsx-runtime");
89
+ function IconMenu({ size = 16, className }) {
90
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
91
+ "svg",
92
+ {
93
+ width: size,
94
+ height: size,
95
+ className,
96
+ viewBox: "0 0 16 16",
97
+ fill: "none",
98
+ "aria-hidden": "true",
99
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M2.5 4h11M2.5 8h11M2.5 12h7", stroke: "currentColor", strokeLinecap: "round" })
100
+ }
101
+ );
102
+ }
103
+
104
+ // src/client/components.tsx
105
+ var import_jsx_runtime2 = require("react/jsx-runtime");
106
+ function useObservable(observable) {
107
+ const subscribe = (0, import_react2.useCallback)(
108
+ (listener) => observable.subscribe(listener),
109
+ [observable]
110
+ );
111
+ const getSnapshot = (0, import_react2.useCallback)(() => observable.getSnapshot(), [observable]);
112
+ return (0, import_react2.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
113
+ }
114
+ function mainSessionOf(state) {
115
+ for (const [id, row] of Object.entries(state.byId)) {
116
+ if ((row?.retainedBy?.mainView ?? 0) > 0) return id;
117
+ }
118
+ return void 0;
119
+ }
120
+ function HeaderMenuButton({ mode, toggle, t }) {
121
+ const state = useObservable(mode);
122
+ const ref = (0, import_react2.useRef)(null);
123
+ const collapsed = useFrameFlag(ref, COLLAPSED_ATTRIBUTE);
124
+ const expanded = collapsed === false;
125
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
126
+ "button",
127
+ {
128
+ ref,
129
+ type: "button",
130
+ className: "dsh-mobile-menu",
131
+ hidden: !state.mobile,
132
+ "aria-label": expanded ? t("menu.close") : t("menu.open"),
133
+ "aria-expanded": state.mobile ? expanded : void 0,
134
+ onClick: toggle,
135
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IconMenu, { size: 18 })
136
+ }
137
+ );
138
+ }
139
+ function DrawerOverlay({ mode, toggle, t, usePanelInfo, useSessions }) {
140
+ const state = useObservable(mode);
141
+ const anchor = (0, import_react2.useRef)(null);
142
+ const collapsed = useFrameFlag(anchor, COLLAPSED_ATTRIBUTE);
143
+ const expanded = state.mobile && collapsed === false;
144
+ const panel = usePanelInfo((info) => info.activePanelId);
145
+ const session = useSessions(mainSessionOf);
146
+ const seen = (0, import_react2.useRef)({ panel, session });
147
+ (0, import_react2.useEffect)(() => {
148
+ const before = seen.current;
149
+ seen.current = { panel, session };
150
+ if (before.panel === panel && before.session === session) return;
151
+ if (expanded) toggle();
152
+ }, [panel, session, expanded, toggle]);
153
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
154
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { ref: anchor, hidden: true }),
155
+ state.mobile ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
156
+ "div",
157
+ {
158
+ className: "dsh-mobile-scrim",
159
+ "data-open": expanded ? "" : void 0,
160
+ "aria-hidden": "true",
161
+ onClick: toggle
162
+ }
163
+ ) : null,
164
+ state.mobile && panel !== null && !expanded ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
165
+ "button",
166
+ {
167
+ type: "button",
168
+ className: "dsh-mobile-menu dsh-mobile-floating",
169
+ "aria-label": t("menu.open"),
170
+ onClick: toggle,
171
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IconMenu, { size: 18 })
172
+ }
173
+ ) : null
174
+ ] });
175
+ }
176
+
177
+ // src/client/config.ts
178
+ var NAMESPACE = "mobile";
179
+ var MIN_BREAKPOINT = 360;
180
+ var MAX_BREAKPOINT = 1024;
181
+ var DEFAULT_CONFIG = {
182
+ mode: "auto",
183
+ breakpoint: 768,
184
+ enterKey: "newline",
185
+ swipe: true,
186
+ fitKeyboard: true,
187
+ blockPullToRefresh: true,
188
+ autoFocusComposer: false
189
+ };
190
+ var MODES = ["auto", "always", "off"];
191
+ var ENTER_KEYS = ["newline", "send"];
192
+ function oneOf(value, allowed, fallback) {
193
+ return typeof value === "string" && allowed.includes(value) ? value : fallback;
194
+ }
195
+ function flag(value, fallback) {
196
+ return typeof value === "boolean" ? value : fallback;
197
+ }
198
+ function normalizeConfig(value) {
199
+ const breakpoint = typeof value?.breakpoint === "number" && Number.isFinite(value.breakpoint) ? Math.round(Math.min(MAX_BREAKPOINT, Math.max(MIN_BREAKPOINT, value.breakpoint))) : DEFAULT_CONFIG.breakpoint;
200
+ return {
201
+ mode: oneOf(value?.mode, MODES, DEFAULT_CONFIG.mode),
202
+ breakpoint,
203
+ enterKey: oneOf(value?.enterKey, ENTER_KEYS, DEFAULT_CONFIG.enterKey),
204
+ swipe: flag(value?.swipe, DEFAULT_CONFIG.swipe),
205
+ fitKeyboard: flag(value?.fitKeyboard, DEFAULT_CONFIG.fitKeyboard),
206
+ blockPullToRefresh: flag(value?.blockPullToRefresh, DEFAULT_CONFIG.blockPullToRefresh),
207
+ autoFocusComposer: flag(value?.autoFocusComposer, DEFAULT_CONFIG.autoFocusComposer)
208
+ };
209
+ }
210
+
211
+ // src/client/enter.ts
212
+ var COMPOSER_INPUT = "[data-composer-input]";
213
+ var OPEN_MENU = "[data-composer-overlay] [role='listbox']";
214
+ function shouldInsertNewline(event, target) {
215
+ if (event.key !== "Enter") return false;
216
+ if (event.isComposing === true || event.keyCode === 229) return false;
217
+ if (event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return false;
218
+ const input = target?.closest(COMPOSER_INPUT) ?? null;
219
+ if (input === null || input.isContentEditable === false) return false;
220
+ const card = input.closest("[data-composer-card]");
221
+ if (card !== null && card.querySelector(OPEN_MENU) !== null) return false;
222
+ return true;
223
+ }
224
+ function asClosest(target) {
225
+ const candidate = target;
226
+ return candidate !== null && typeof candidate.closest === "function" ? candidate : null;
227
+ }
228
+ function installEnterNewline(target, active) {
229
+ const listener = (event) => {
230
+ if (!active()) return;
231
+ if (!shouldInsertNewline(event, asClosest(event.target))) return;
232
+ event.stopImmediatePropagation();
233
+ };
234
+ target.addEventListener("keydown", listener, true);
235
+ return () => target.removeEventListener("keydown", listener, true);
236
+ }
237
+
238
+ // src/client/focus.ts
239
+ var INTENT_WINDOW_MS = 1500;
240
+ var COMPOSER_CARD = "[data-composer-card]";
241
+ var CONTROL = "button, a, input, select, textarea, [role='button'], [role='menuitem'], [role='option']";
242
+ var COMMAND_BUTTON = "[aria-haspopup='listbox']";
243
+ function asTarget(target) {
244
+ const candidate = target;
245
+ return candidate !== null && typeof candidate?.closest === "function" ? candidate : null;
246
+ }
247
+ function tapWantsKeyboard(target) {
248
+ const element = asTarget(target);
249
+ if (element === null) return false;
250
+ if (element.closest(COMPOSER_INPUT) !== null) return true;
251
+ if (element.closest(COMPOSER_CARD) === null) return false;
252
+ return element.closest(CONTROL) === null || element.closest(COMMAND_BUTTON) !== null;
253
+ }
254
+ function initialIntent() {
255
+ return {
256
+ tapAt: Number.NEGATIVE_INFINITY,
257
+ tapWantsKeyboard: false,
258
+ keyAt: Number.NEGATIVE_INFINITY
259
+ };
260
+ }
261
+ function shouldBlockFocus(target, intent, now) {
262
+ const element = asTarget(target);
263
+ if (element === null || element.closest(COMPOSER_INPUT) === null) return false;
264
+ if (intent.tapWantsKeyboard && now - intent.tapAt <= INTENT_WINDOW_MS) return false;
265
+ if (intent.keyAt > intent.tapAt && now - intent.keyAt <= INTENT_WINDOW_MS) return false;
266
+ return true;
267
+ }
268
+ function installFocusGuard(target, active, now = Date.now) {
269
+ const intent = initialIntent();
270
+ const onPointer = (event) => {
271
+ intent.tapAt = now();
272
+ intent.tapWantsKeyboard = tapWantsKeyboard(event.target);
273
+ };
274
+ const onKey = () => {
275
+ intent.keyAt = now();
276
+ };
277
+ const onFocus = (event) => {
278
+ if (!active() || !shouldBlockFocus(event.target, intent, now())) return;
279
+ const element = event.target;
280
+ element?.blur?.();
281
+ };
282
+ target.addEventListener("pointerdown", onPointer, true);
283
+ target.addEventListener("keydown", onKey, true);
284
+ target.addEventListener("focusin", onFocus, true);
285
+ return () => {
286
+ target.removeEventListener("pointerdown", onPointer, true);
287
+ target.removeEventListener("keydown", onKey, true);
288
+ target.removeEventListener("focusin", onFocus, true);
289
+ };
290
+ }
291
+
292
+ // src/client/i18n.ts
293
+ var en = {
294
+ "menu.open": "Open sidebar",
295
+ "menu.close": "Close sidebar",
296
+ nav: "Mobile",
297
+ "settings.title": "Mobile",
298
+ "settings.description": "Adapts the Web UI to phones: below the breakpoint the sidebar becomes a drawer, the page fits above the on-screen keyboard, and touch controls get larger targets. Changes apply at once in every open page.",
299
+ "settings.statusMobile": "This page is using the phone layout ({width}px wide).",
300
+ "settings.statusDesktop": "This page is using the desktop layout ({width}px wide).",
301
+ "settings.mode": "Phone layout",
302
+ "settings.modeHint": "Auto switches below the breakpoint. Off removes every change of this plugin.",
303
+ "settings.mode.auto": "Auto",
304
+ "settings.mode.always": "Always",
305
+ "settings.mode.off": "Off",
306
+ "settings.breakpoint": "Breakpoint",
307
+ "settings.breakpointHint": "Viewport width in CSS pixels, from 360 to 1024. Below it, auto mode uses the phone layout. Most phones are 360 to 430 wide in portrait.",
308
+ "settings.breakpointInvalid": "Enter a whole number from 360 to 1024.",
309
+ "settings.enterKey": "Enter key in the composer",
310
+ "settings.enterKeyHint": "On a touch screen in the phone layout. With New line, send with the send button. IME conversion and command menus keep their Enter.",
311
+ "settings.enterKey.newline": "New line",
312
+ "settings.enterKey.send": "Send",
313
+ "settings.autoFocusComposer": "Focus the composer automatically",
314
+ "settings.autoFocusComposerHint": "Opening a session or pressing a composer button focuses the input, as on desktop, and the on-screen keyboard opens. Off: tap the input to type.",
315
+ "settings.swipe": "Swipe to open the sidebar",
316
+ "settings.swipeHint": "Swipe right from the left edge to open, swipe left to close.",
317
+ "settings.fitKeyboard": "Fit the page to the keyboard",
318
+ "settings.fitKeyboardHint": "Keep the header and the composer on screen while the on-screen keyboard is up.",
319
+ "settings.blockPullToRefresh": "Block pull-to-refresh",
320
+ "settings.blockPullToRefreshHint": "Scrolling up past the top of a long conversation does not reload the page.",
321
+ "settings.reset": "Restore defaults",
322
+ "settings.unavailable": "This configuration is not available.",
323
+ "settings.readOnly": "Configuration is read-only: a higher configuration layer sets it.",
324
+ "settings.writeError": "The Host refused the change."
325
+ };
326
+ var zh = {
327
+ "menu.open": "\u6253\u5F00\u4FA7\u8FB9\u680F",
328
+ "menu.close": "\u5173\u95ED\u4FA7\u8FB9\u680F",
329
+ nav: "\u79FB\u52A8\u7AEF",
330
+ "settings.title": "\u79FB\u52A8\u7AEF",
331
+ "settings.description": "\u8BA9 Web UI \u9002\u914D\u624B\u673A\uFF1A\u7A84\u4E8E\u65AD\u70B9\u65F6\u4FA7\u8FB9\u680F\u53D8\u4E3A\u62BD\u5C49\uFF0C\u9875\u9762\u9002\u914D\u5C4F\u5E55\u952E\u76D8\u4E0A\u65B9\u7684\u53EF\u89C1\u533A\u57DF\uFF0C\u89E6\u63A7\u63A7\u4EF6\u7684\u70B9\u51FB\u533A\u57DF\u66F4\u5927\u3002\u4FEE\u6539\u4F1A\u7ACB\u5373\u5E94\u7528\u5230\u6240\u6709\u6253\u5F00\u7684\u9875\u9762\u3002",
332
+ "settings.statusMobile": "\u5F53\u524D\u9875\u9762\u4F7F\u7528\u624B\u673A\u5E03\u5C40\uFF08\u5BBD {width}px\uFF09\u3002",
333
+ "settings.statusDesktop": "\u5F53\u524D\u9875\u9762\u4F7F\u7528\u684C\u9762\u5E03\u5C40\uFF08\u5BBD {width}px\uFF09\u3002",
334
+ "settings.mode": "\u624B\u673A\u5E03\u5C40",
335
+ "settings.modeHint": "\u81EA\u52A8\uFF1A\u7A84\u4E8E\u65AD\u70B9\u65F6\u5207\u6362\u3002\u5173\u95ED\uFF1A\u64A4\u9500\u672C\u63D2\u4EF6\u7684\u6240\u6709\u4FEE\u6539\u3002",
336
+ "settings.mode.auto": "\u81EA\u52A8",
337
+ "settings.mode.always": "\u59CB\u7EC8",
338
+ "settings.mode.off": "\u5173\u95ED",
339
+ "settings.breakpoint": "\u65AD\u70B9",
340
+ "settings.breakpointHint": "\u89C6\u53E3\u5BBD\u5EA6\uFF08CSS \u50CF\u7D20\uFF09\uFF0C360 \u5230 1024\u3002\u7A84\u4E8E\u8BE5\u503C\u65F6\u81EA\u52A8\u6A21\u5F0F\u4F7F\u7528\u624B\u673A\u5E03\u5C40\u3002\u591A\u6570\u624B\u673A\u7AD6\u5C4F\u5BBD 360 \u5230 430\u3002",
341
+ "settings.breakpointInvalid": "\u8BF7\u8F93\u5165 360 \u5230 1024 \u4E4B\u95F4\u7684\u6574\u6570\u3002",
342
+ "settings.enterKey": "\u8F93\u5165\u6846\u4E2D\u7684 Enter \u952E",
343
+ "settings.enterKeyHint": "\u4EC5\u5728\u89E6\u5C4F\u8BBE\u5907\u7684\u624B\u673A\u5E03\u5C40\u4E2D\u751F\u6548\u3002\u9009\u62E9\u6362\u884C\u65F6\u7528\u53D1\u9001\u6309\u94AE\u53D1\u9001\u3002\u8F93\u5165\u6CD5\u786E\u8BA4\u548C\u547D\u4EE4\u83DC\u5355\u4ECD\u4F7F\u7528 Enter\u3002",
344
+ "settings.enterKey.newline": "\u6362\u884C",
345
+ "settings.enterKey.send": "\u53D1\u9001",
346
+ "settings.autoFocusComposer": "\u81EA\u52A8\u805A\u7126\u8F93\u5165\u6846",
347
+ "settings.autoFocusComposerHint": "\u6253\u5F00\u4F1A\u8BDD\u6216\u6309\u4E0B\u8F93\u5165\u6846\u4E2D\u7684\u6309\u94AE\u65F6\u81EA\u52A8\u805A\u7126\u8F93\u5165\u6846\uFF08\u4E0E\u684C\u9762\u76F8\u540C\uFF09\uFF0C\u5C4F\u5E55\u952E\u76D8\u4F1A\u5F39\u51FA\u3002\u5173\u95ED\u65F6\u70B9\u6309\u8F93\u5165\u6846\u624D\u5F00\u59CB\u8F93\u5165\u3002",
348
+ "settings.swipe": "\u6ED1\u52A8\u6253\u5F00\u4FA7\u8FB9\u680F",
349
+ "settings.swipeHint": "\u4ECE\u5DE6\u8FB9\u7F18\u5411\u53F3\u6ED1\u52A8\u6253\u5F00\uFF0C\u5411\u5DE6\u6ED1\u52A8\u5173\u95ED\u3002",
350
+ "settings.fitKeyboard": "\u9875\u9762\u9002\u914D\u952E\u76D8",
351
+ "settings.fitKeyboardHint": "\u5C4F\u5E55\u952E\u76D8\u5F39\u51FA\u65F6\uFF0C\u6807\u9898\u680F\u548C\u8F93\u5165\u6846\u4FDD\u6301\u5728\u5C4F\u5E55\u5185\u3002",
352
+ "settings.blockPullToRefresh": "\u963B\u6B62\u4E0B\u62C9\u5237\u65B0",
353
+ "settings.blockPullToRefreshHint": "\u5728\u957F\u4F1A\u8BDD\u9876\u90E8\u7EE7\u7EED\u4E0A\u6ED1\u4E0D\u4F1A\u5237\u65B0\u9875\u9762\u3002",
354
+ "settings.reset": "\u6062\u590D\u9ED8\u8BA4",
355
+ "settings.unavailable": "\u6B64\u914D\u7F6E\u4E0D\u53EF\u7528\u3002",
356
+ "settings.readOnly": "\u914D\u7F6E\u4E3A\u53EA\u8BFB\uFF1A\u7531\u66F4\u9AD8\u7684\u914D\u7F6E\u5C42\u8BBE\u7F6E\u3002",
357
+ "settings.writeError": "Host \u62D2\u7EDD\u4E86\u6B64\u4FEE\u6539\u3002"
358
+ };
359
+ var ja = {
360
+ "menu.open": "\u30B5\u30A4\u30C9\u30D0\u30FC\u3092\u958B\u304F",
361
+ "menu.close": "\u30B5\u30A4\u30C9\u30D0\u30FC\u3092\u9589\u3058\u308B",
362
+ nav: "\u30E2\u30D0\u30A4\u30EB",
363
+ "settings.title": "\u30E2\u30D0\u30A4\u30EB",
364
+ "settings.description": "Web UI \u3092\u30B9\u30DE\u30FC\u30C8\u30D5\u30A9\u30F3\u306B\u5408\u308F\u305B\u307E\u3059\u3002\u30D6\u30EC\u30FC\u30AF\u30DD\u30A4\u30F3\u30C8\u3088\u308A\u72ED\u3044\u753B\u9762\u3067\u306F\u30B5\u30A4\u30C9\u30D0\u30FC\u3092\u30C9\u30ED\u30EF\u30FC\u306B\u3057\u3001\u30AA\u30F3\u30B9\u30AF\u30EA\u30FC\u30F3\u30AD\u30FC\u30DC\u30FC\u30C9\u306E\u4E0A\u306B\u53CE\u307E\u308B\u3088\u3046\u30DA\u30FC\u30B8\u3092\u7E2E\u3081\u3001\u30BF\u30C3\u30C1\u64CD\u4F5C\u306E\u5F53\u305F\u308A\u5224\u5B9A\u3092\u5E83\u3052\u307E\u3059\u3002\u5909\u66F4\u306F\u958B\u3044\u3066\u3044\u308B\u3059\u3079\u3066\u306E\u30DA\u30FC\u30B8\u306B\u3059\u3050\u53CD\u6620\u3055\u308C\u307E\u3059\u3002",
365
+ "settings.statusMobile": "\u3053\u306E\u30DA\u30FC\u30B8\u306F\u30B9\u30DE\u30DB\u8868\u793A\u3067\u3059\uFF08\u5E45 {width}px\uFF09\u3002",
366
+ "settings.statusDesktop": "\u3053\u306E\u30DA\u30FC\u30B8\u306F\u30C7\u30B9\u30AF\u30C8\u30C3\u30D7\u8868\u793A\u3067\u3059\uFF08\u5E45 {width}px\uFF09\u3002",
367
+ "settings.mode": "\u30B9\u30DE\u30DB\u8868\u793A",
368
+ "settings.modeHint": "\u81EA\u52D5\u306F\u30D6\u30EC\u30FC\u30AF\u30DD\u30A4\u30F3\u30C8\u3088\u308A\u72ED\u3044\u3068\u304D\u306B\u5207\u308A\u66FF\u3048\u307E\u3059\u3002\u30AA\u30D5\u306F\u3053\u306E\u30D7\u30E9\u30B0\u30A4\u30F3\u306E\u5909\u66F4\u3092\u3059\u3079\u3066\u5916\u3057\u307E\u3059\u3002",
369
+ "settings.mode.auto": "\u81EA\u52D5",
370
+ "settings.mode.always": "\u5E38\u306B",
371
+ "settings.mode.off": "\u30AA\u30D5",
372
+ "settings.breakpoint": "\u30D6\u30EC\u30FC\u30AF\u30DD\u30A4\u30F3\u30C8",
373
+ "settings.breakpointHint": "\u30D3\u30E5\u30FC\u30DD\u30FC\u30C8\u5E45\uFF08CSS \u30D4\u30AF\u30BB\u30EB\uFF09\u3002360\u301C1024 \u306E\u7BC4\u56F2\u3067\u3059\u3002\u3053\u308C\u3088\u308A\u72ED\u3044\u3068\u81EA\u52D5\u30E2\u30FC\u30C9\u3067\u30B9\u30DE\u30DB\u8868\u793A\u306B\u306A\u308A\u307E\u3059\u3002\u7E26\u5411\u304D\u306E\u30B9\u30DE\u30DB\u306F\u304A\u304A\u3080\u306D 360\u301C430 \u3067\u3059\u3002",
374
+ "settings.breakpointInvalid": "360\u301C1024 \u306E\u6574\u6570\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
375
+ "settings.enterKey": "\u5165\u529B\u6B04\u306E Enter \u30AD\u30FC",
376
+ "settings.enterKeyHint": "\u30BF\u30C3\u30C1\u7AEF\u672B\u306E\u30B9\u30DE\u30DB\u8868\u793A\u3067\u3060\u3051\u52B9\u304D\u307E\u3059\u3002\u300C\u6539\u884C\u300D\u306E\u3068\u304D\u306F\u9001\u4FE1\u30DC\u30BF\u30F3\u3067\u9001\u308A\u307E\u3059\u3002\u5909\u63DB\u306E\u78BA\u5B9A\u3068\u30B3\u30DE\u30F3\u30C9\u30E1\u30CB\u30E5\u30FC\u3067\u306F\u5F93\u6765\u3069\u304A\u308A Enter \u304C\u52B9\u304D\u307E\u3059\u3002",
377
+ "settings.enterKey.newline": "\u6539\u884C",
378
+ "settings.enterKey.send": "\u9001\u4FE1",
379
+ "settings.autoFocusComposer": "\u5165\u529B\u6B04\u306B\u81EA\u52D5\u3067\u30D5\u30A9\u30FC\u30AB\u30B9\u3059\u308B",
380
+ "settings.autoFocusComposerHint": "\u30BB\u30C3\u30B7\u30E7\u30F3\u3092\u958B\u3044\u305F\u3068\u304D\u3084\u5165\u529B\u6B04\u306E\u30DC\u30BF\u30F3\u3092\u62BC\u3057\u305F\u3068\u304D\u306B\u3001\u30C7\u30B9\u30AF\u30C8\u30C3\u30D7\u3068\u540C\u3058\u304F\u5165\u529B\u6B04\u306B\u30D5\u30A9\u30FC\u30AB\u30B9\u3057\u3001\u30AD\u30FC\u30DC\u30FC\u30C9\u304C\u958B\u304D\u307E\u3059\u3002\u30AA\u30D5\u306B\u3059\u308B\u3068\u3001\u5165\u529B\u6B04\u3092\u30BF\u30C3\u30D7\u3059\u308B\u307E\u3067\u30AD\u30FC\u30DC\u30FC\u30C9\u306F\u958B\u304D\u307E\u305B\u3093\u3002",
381
+ "settings.swipe": "\u30B9\u30EF\u30A4\u30D7\u3067\u30B5\u30A4\u30C9\u30D0\u30FC\u3092\u958B\u304F",
382
+ "settings.swipeHint": "\u5DE6\u7AEF\u304B\u3089\u53F3\u3078\u30B9\u30EF\u30A4\u30D7\u3067\u958B\u304D\u3001\u5DE6\u3078\u30B9\u30EF\u30A4\u30D7\u3067\u9589\u3058\u307E\u3059\u3002",
383
+ "settings.fitKeyboard": "\u30AD\u30FC\u30DC\u30FC\u30C9\u306B\u5408\u308F\u305B\u3066\u30DA\u30FC\u30B8\u3092\u7E2E\u3081\u308B",
384
+ "settings.fitKeyboardHint": "\u30AA\u30F3\u30B9\u30AF\u30EA\u30FC\u30F3\u30AD\u30FC\u30DC\u30FC\u30C9\u306E\u8868\u793A\u4E2D\u3082\u3001\u30D8\u30C3\u30C0\u30FC\u3068\u5165\u529B\u6B04\u3092\u753B\u9762\u5185\u306B\u6B8B\u3057\u307E\u3059\u3002",
385
+ "settings.blockPullToRefresh": "\u5F15\u3063\u5F35\u3063\u3066\u66F4\u65B0\u3092\u6B62\u3081\u308B",
386
+ "settings.blockPullToRefreshHint": "\u9577\u3044\u4F1A\u8A71\u306E\u5148\u982D\u3067\u3055\u3089\u306B\u4E0A\u3078\u30B9\u30AF\u30ED\u30FC\u30EB\u3057\u3066\u3082\u518D\u8AAD\u307F\u8FBC\u307F\u3057\u307E\u305B\u3093\u3002",
387
+ "settings.reset": "\u65E2\u5B9A\u5024\u306B\u623B\u3059",
388
+ "settings.unavailable": "\u3053\u306E\u8A2D\u5B9A\u306F\u5229\u7528\u3067\u304D\u307E\u305B\u3093\u3002",
389
+ "settings.readOnly": "\u4E0A\u4F4D\u306E\u8A2D\u5B9A\u30EC\u30A4\u30E4\u30FC\u304C\u5024\u3092\u6C7A\u3081\u3066\u3044\u308B\u305F\u3081\u3001\u8AAD\u307F\u53D6\u308A\u5C02\u7528\u3067\u3059\u3002",
390
+ "settings.writeError": "Host \u304C\u5909\u66F4\u3092\u62D2\u5426\u3057\u307E\u3057\u305F\u3002"
391
+ };
392
+
393
+ // src/client/mode.ts
394
+ var TOUCH_QUERY = "(hover: none) and (pointer: coarse)";
395
+ function widthQuery(breakpoint) {
396
+ return `(max-width: ${breakpoint - 0.02}px)`;
397
+ }
398
+ function resolveMobile(mode, narrow) {
399
+ if (mode === "always") return true;
400
+ if (mode === "off") return false;
401
+ return narrow;
402
+ }
403
+ function createModeStore(matchMedia, config) {
404
+ const listeners = /* @__PURE__ */ new Set();
405
+ const touch = matchMedia(TOUCH_QUERY);
406
+ let breakpoint = config().breakpoint;
407
+ let width = matchMedia(widthQuery(breakpoint));
408
+ let state = { mobile: false, touch: false };
409
+ const compute = () => {
410
+ const next = {
411
+ mobile: resolveMobile(config().mode, width.matches),
412
+ touch: touch.matches
413
+ };
414
+ if (next.mobile === state.mobile && next.touch === state.touch) return;
415
+ state = next;
416
+ for (const listener of [...listeners]) listener();
417
+ };
418
+ const onChange = () => compute();
419
+ touch.addEventListener("change", onChange);
420
+ width.addEventListener("change", onChange);
421
+ compute();
422
+ return {
423
+ getSnapshot: () => state,
424
+ subscribe(listener) {
425
+ listeners.add(listener);
426
+ return () => {
427
+ listeners.delete(listener);
428
+ };
429
+ },
430
+ refresh() {
431
+ const next = config().breakpoint;
432
+ if (next !== breakpoint) {
433
+ width.removeEventListener("change", onChange);
434
+ breakpoint = next;
435
+ width = matchMedia(widthQuery(breakpoint));
436
+ width.addEventListener("change", onChange);
437
+ }
438
+ compute();
439
+ },
440
+ dispose() {
441
+ touch.removeEventListener("change", onChange);
442
+ width.removeEventListener("change", onChange);
443
+ listeners.clear();
444
+ }
445
+ };
446
+ }
447
+
448
+ // src/client/SettingsPage.tsx
449
+ var import_react3 = require("react");
450
+ var import_jsx_runtime3 = require("react/jsx-runtime");
451
+ var FIELDS = Object.keys(DEFAULT_CONFIG);
452
+ var MODES2 = ["auto", "always", "off"];
453
+ var ENTER_KEYS2 = ["newline", "send"];
454
+ function parseBreakpoint(text) {
455
+ const trimmed = text.trim();
456
+ if (!/^\d+$/.test(trimmed)) return void 0;
457
+ const value = Number(trimmed);
458
+ return value >= MIN_BREAKPOINT && value <= MAX_BREAKPOINT ? value : void 0;
459
+ }
460
+ var sectionStyle = {
461
+ maxWidth: 680,
462
+ padding: "4px 0 32px",
463
+ color: "var(--dsw-alias-label-primary)"
464
+ };
465
+ var headingStyle = { margin: 0, fontSize: 18, fontWeight: 600, lineHeight: 1.4 };
466
+ var introStyle = {
467
+ margin: "8px 0 0",
468
+ color: "var(--dsw-alias-label-secondary)",
469
+ fontSize: 13,
470
+ lineHeight: 1.6
471
+ };
472
+ var statusStyle = {
473
+ margin: "12px 0 0",
474
+ padding: "8px 10px",
475
+ borderRadius: 8,
476
+ background: "var(--dsw-alias-interactive-bg-hover)",
477
+ color: "var(--dsw-alias-label-secondary)",
478
+ fontSize: 12,
479
+ lineHeight: 1.5
480
+ };
481
+ var fieldStyle = {
482
+ display: "flex",
483
+ flexDirection: "column",
484
+ gap: 8,
485
+ marginTop: 22
486
+ };
487
+ var rowStyle = {
488
+ display: "flex",
489
+ alignItems: "center",
490
+ justifyContent: "space-between",
491
+ gap: 16,
492
+ marginTop: 20
493
+ };
494
+ var labelStyle = { fontSize: 13, fontWeight: 600 };
495
+ var hintStyle = {
496
+ margin: 0,
497
+ color: "var(--dsw-alias-label-tertiary)",
498
+ fontSize: 12,
499
+ lineHeight: 1.5,
500
+ fontWeight: 400
501
+ };
502
+ var errorStyle = {
503
+ margin: 0,
504
+ color: "var(--dsw-alias-state-error-primary)",
505
+ fontSize: 12,
506
+ lineHeight: 1.5
507
+ };
508
+ var groupStyle = {
509
+ margin: "26px 0 0",
510
+ paddingTop: 4,
511
+ borderTop: "1px solid var(--dsw-alias-border-l2)"
512
+ };
513
+ var inputStyle = {
514
+ width: 96,
515
+ boxSizing: "border-box",
516
+ padding: "6px 10px",
517
+ borderRadius: 8,
518
+ border: "1px solid var(--dsw-alias-border-l2)",
519
+ background: "var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-1))",
520
+ color: "inherit",
521
+ font: "inherit",
522
+ outline: "none"
523
+ };
524
+ var buttonStyle = {
525
+ marginTop: 26,
526
+ padding: "8px 12px",
527
+ borderRadius: 8,
528
+ border: "1px solid var(--dsw-alias-border-l2)",
529
+ background: "var(--dsw-alias-bg-layer-1)",
530
+ color: "inherit",
531
+ font: "inherit",
532
+ cursor: "pointer"
533
+ };
534
+ var mutedStyle = {
535
+ color: "var(--dsw-alias-label-tertiary)",
536
+ fontSize: 13,
537
+ lineHeight: 1.6
538
+ };
539
+ function useObservable2(observable) {
540
+ const subscribe = (0, import_react3.useCallback)(
541
+ (listener) => observable.subscribe(listener),
542
+ [observable]
543
+ );
544
+ const getSnapshot = (0, import_react3.useCallback)(() => observable.getSnapshot(), [observable]);
545
+ return (0, import_react3.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
546
+ }
547
+ function subscribeResize(listener) {
548
+ window.addEventListener("resize", listener);
549
+ return () => window.removeEventListener("resize", listener);
550
+ }
551
+ function useViewportWidth() {
552
+ return (0, import_react3.useSyncExternalStore)(
553
+ subscribeResize,
554
+ () => Math.round(window.innerWidth),
555
+ () => 0
556
+ );
557
+ }
558
+ function SettingsPage({ form, mode, t }) {
559
+ const subscribe = (0, import_react3.useCallback)((listener) => form.subscribe(listener), [form]);
560
+ const getSnapshot = (0, import_react3.useCallback)(() => form.getSnapshot(), [form]);
561
+ const snapshot = (0, import_react3.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
562
+ const config = normalizeConfig(snapshot.value);
563
+ const state = useObservable2(mode);
564
+ const width = useViewportWidth();
565
+ const [draft, setDraft] = (0, import_react3.useState)(String(config.breakpoint));
566
+ const [writeError, setWriteError] = (0, import_react3.useState)(false);
567
+ (0, import_react3.useEffect)(() => {
568
+ setDraft(String(config.breakpoint));
569
+ }, [config.breakpoint]);
570
+ const write = (field, value) => {
571
+ void form.set(field, value).then((accepted) => setWriteError(!accepted)).catch(() => setWriteError(true));
572
+ };
573
+ const parsed = parseBreakpoint(draft);
574
+ const commitBreakpoint = () => {
575
+ if (parsed === void 0) return;
576
+ if (parsed !== config.breakpoint) write("breakpoint", parsed);
577
+ };
578
+ const onBreakpointKeyDown = (event) => {
579
+ if (event.key !== "Enter" || event.nativeEvent.isComposing) return;
580
+ event.preventDefault();
581
+ event.currentTarget.blur();
582
+ };
583
+ const reset = () => {
584
+ void Promise.all(FIELDS.map((field) => form.unset(field))).then((results) => setWriteError(results.some((accepted) => !accepted))).catch(() => setWriteError(true));
585
+ };
586
+ if (snapshot.status === "unavailable") {
587
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: mutedStyle, children: t("settings.unavailable") });
588
+ }
589
+ const disabled = snapshot.writable === false;
590
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: sectionStyle, className: "dsh-mobile-settings", children: [
591
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { style: headingStyle, children: t("settings.title") }),
592
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: introStyle, children: t("settings.description") }),
593
+ width > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: statusStyle, role: "status", children: state.mobile ? t("settings.statusMobile", { width }) : t("settings.statusDesktop", { width }) }) : null,
594
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: fieldStyle, children: [
595
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: labelStyle, children: t("settings.mode") }),
596
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
597
+ Segmented,
598
+ {
599
+ label: t("settings.mode"),
600
+ options: MODES2.map((value) => ({ value, label: t(`settings.mode.${value}`) })),
601
+ value: config.mode,
602
+ disabled,
603
+ onChange: (next) => write("mode", next)
604
+ }
605
+ ),
606
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: hintStyle, children: t("settings.modeHint") })
607
+ ] }),
608
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: fieldStyle, children: [
609
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: labelStyle, htmlFor: "dsh-mobile-breakpoint", children: t("settings.breakpoint") }),
610
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
611
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
612
+ "input",
613
+ {
614
+ id: "dsh-mobile-breakpoint",
615
+ type: "text",
616
+ inputMode: "numeric",
617
+ autoComplete: "off",
618
+ spellCheck: false,
619
+ value: draft,
620
+ disabled,
621
+ "aria-invalid": parsed === void 0,
622
+ style: inputStyle,
623
+ onChange: (event) => setDraft(event.target.value),
624
+ onBlur: commitBreakpoint,
625
+ onKeyDown: onBreakpointKeyDown
626
+ }
627
+ ),
628
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: mutedStyle, children: "px" })
629
+ ] }),
630
+ parsed === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: errorStyle, children: t("settings.breakpointInvalid") }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: hintStyle, children: t("settings.breakpointHint") })
631
+ ] }),
632
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: groupStyle, children: [
633
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: fieldStyle, children: [
634
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: labelStyle, children: t("settings.enterKey") }),
635
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
636
+ Segmented,
637
+ {
638
+ label: t("settings.enterKey"),
639
+ options: ENTER_KEYS2.map((value) => ({
640
+ value,
641
+ label: t(`settings.enterKey.${value}`)
642
+ })),
643
+ value: config.enterKey,
644
+ disabled,
645
+ onChange: (next) => write("enterKey", next)
646
+ }
647
+ ),
648
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: hintStyle, children: t("settings.enterKeyHint") })
649
+ ] }),
650
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
651
+ Toggle,
652
+ {
653
+ label: t("settings.autoFocusComposer"),
654
+ hint: t("settings.autoFocusComposerHint"),
655
+ checked: config.autoFocusComposer,
656
+ disabled,
657
+ onChange: (next) => write("autoFocusComposer", next)
658
+ }
659
+ ),
660
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
661
+ Toggle,
662
+ {
663
+ label: t("settings.swipe"),
664
+ hint: t("settings.swipeHint"),
665
+ checked: config.swipe,
666
+ disabled,
667
+ onChange: (next) => write("swipe", next)
668
+ }
669
+ ),
670
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
671
+ Toggle,
672
+ {
673
+ label: t("settings.fitKeyboard"),
674
+ hint: t("settings.fitKeyboardHint"),
675
+ checked: config.fitKeyboard,
676
+ disabled,
677
+ onChange: (next) => write("fitKeyboard", next)
678
+ }
679
+ ),
680
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
681
+ Toggle,
682
+ {
683
+ label: t("settings.blockPullToRefresh"),
684
+ hint: t("settings.blockPullToRefreshHint"),
685
+ checked: config.blockPullToRefresh,
686
+ disabled,
687
+ onChange: (next) => write("blockPullToRefresh", next)
688
+ }
689
+ )
690
+ ] }),
691
+ disabled ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: { ...hintStyle, marginTop: 20 }, children: t("settings.readOnly") }) : null,
692
+ writeError ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: { ...errorStyle, marginTop: 20 }, children: t("settings.writeError") }) : null,
693
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", style: buttonStyle, disabled, onClick: reset, children: t("settings.reset") })
694
+ ] });
695
+ }
696
+ function Segmented({
697
+ label,
698
+ options,
699
+ value,
700
+ disabled,
701
+ onChange
702
+ }) {
703
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("fieldset", { className: "dsh-mobile-segmented", "aria-label": label, children: options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
704
+ "button",
705
+ {
706
+ type: "button",
707
+ "aria-pressed": option.value === value,
708
+ className: "dsh-mobile-segment",
709
+ disabled,
710
+ onClick: () => {
711
+ if (option.value !== value) onChange(option.value);
712
+ },
713
+ children: option.label
714
+ },
715
+ option.value
716
+ )) });
717
+ }
718
+ function Toggle({ label, hint, checked, disabled, onChange }) {
719
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: rowStyle, children: [
720
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { display: "flex", flexDirection: "column", gap: 4, minWidth: 0 }, children: [
721
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: labelStyle, children: label }),
722
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: hintStyle, children: hint })
723
+ ] }),
724
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
725
+ "button",
726
+ {
727
+ type: "button",
728
+ role: "switch",
729
+ "aria-checked": checked,
730
+ "aria-label": label,
731
+ className: "dsh-mobile-switch",
732
+ disabled,
733
+ onClick: (event) => {
734
+ event.stopPropagation();
735
+ onChange(!checked);
736
+ },
737
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dsh-mobile-thumb" })
738
+ }
739
+ )
740
+ ] });
741
+ }
742
+
743
+ // src/client/styles.ts
744
+ var MOBILE_ATTRIBUTE = "data-dsh-mobile";
745
+ var TOUCH_ATTRIBUTE = "data-dsh-touch";
746
+ var NO_PULL_ATTRIBUTE = "data-dsh-no-pull";
747
+ var M = `html[${MOBILE_ATTRIBUTE}]`;
748
+ var T = `html[${TOUCH_ATTRIBUTE}]`;
749
+ var SIDEBAR = `${M} ${FRAME} > :has(> [data-slot="sidebar"])`;
750
+ var SETTINGS = `${M} [data-shortcut-modal="settings"][role="dialog"]`;
751
+ var MOBILE_CSS = `
752
+ .dsh-mobile-menu {
753
+ box-sizing: border-box;
754
+ flex: none;
755
+ width: 34px;
756
+ height: 34px;
757
+ margin: -2px 2px -2px -4px;
758
+ padding: 0;
759
+ border: none;
760
+ border-radius: var(--dsw-radius-md);
761
+ background: transparent;
762
+ color: var(--dsw-alias-label-primary);
763
+ font-family: inherit;
764
+ display: inline-flex;
765
+ align-items: center;
766
+ justify-content: center;
767
+ cursor: pointer;
768
+ touch-action: manipulation;
769
+ -webkit-tap-highlight-color: transparent;
770
+ }
771
+ .dsh-mobile-menu[hidden] {
772
+ display: none;
773
+ }
774
+ .dsh-mobile-menu:active {
775
+ background: var(--dsw-alias-interactive-bg-hover);
776
+ }
777
+ @media (hover: hover) {
778
+ .dsh-mobile-menu:hover {
779
+ background: var(--dsw-alias-interactive-bg-hover);
780
+ }
781
+ }
782
+ .dsh-mobile-menu:focus-visible {
783
+ outline: var(--dsw-focus-ring-width) solid
784
+ var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
785
+ outline-offset: -2px;
786
+ }
787
+ .dsh-mobile-floating {
788
+ position: absolute;
789
+ top: 8px;
790
+ left: 8px;
791
+ width: 40px;
792
+ height: 40px;
793
+ margin: 0;
794
+ border: 0.5px solid var(--dsw-alias-border-l3);
795
+ border-radius: 999px;
796
+ background: var(--dsw-alias-button-elevated-fill, var(--dsw-alias-bg-layer-1));
797
+ box-shadow: var(--dsw-elevation-soft, 0 2px 8px rgb(0 0 0 / 12%));
798
+ }
799
+ .dsh-mobile-scrim {
800
+ position: absolute;
801
+ inset: 0;
802
+ background: var(--dsw-alias-bg-mask-1, rgb(0 0 0 / 40%));
803
+ opacity: 0;
804
+ pointer-events: none;
805
+ transition: opacity 0.2s ease;
806
+ -webkit-tap-highlight-color: transparent;
807
+ }
808
+ .dsh-mobile-scrim[data-open] {
809
+ opacity: 1;
810
+ pointer-events: auto;
811
+ }
812
+ .dsh-mobile-segmented {
813
+ box-sizing: border-box;
814
+ min-width: 0;
815
+ margin: 0;
816
+ border: 0;
817
+ display: inline-flex;
818
+ align-self: flex-start;
819
+ max-width: 100%;
820
+ gap: 2px;
821
+ padding: 2px;
822
+ border-radius: 8px;
823
+ background: var(--dsw-alias-interactive-bg-hover);
824
+ }
825
+ .dsh-mobile-segment {
826
+ box-sizing: border-box;
827
+ min-height: 30px;
828
+ padding: 0 14px;
829
+ border: 0;
830
+ border-radius: 6px;
831
+ background: transparent;
832
+ color: var(--dsw-alias-label-secondary);
833
+ font: inherit;
834
+ font-size: 13px;
835
+ white-space: nowrap;
836
+ cursor: pointer;
837
+ touch-action: manipulation;
838
+ }
839
+ .dsh-mobile-segment[aria-pressed="true"] {
840
+ background: var(--dsw-alias-bg-layer-1);
841
+ color: var(--dsw-alias-label-primary);
842
+ font-weight: 600;
843
+ box-shadow: 0 0 0 0.5px var(--dsw-alias-border-l2);
844
+ }
845
+ .dsh-mobile-segment:disabled {
846
+ cursor: default;
847
+ opacity: 0.5;
848
+ }
849
+ .dsh-mobile-segment:focus-visible,
850
+ .dsh-mobile-switch:focus-visible {
851
+ outline: var(--dsw-focus-ring-width) solid
852
+ var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
853
+ outline-offset: 2px;
854
+ }
855
+ .dsh-mobile-switch {
856
+ box-sizing: border-box;
857
+ position: relative;
858
+ flex: 0 0 auto;
859
+ width: 36px;
860
+ height: 20px;
861
+ padding: 2px;
862
+ border: 0;
863
+ border-radius: 999px;
864
+ background: var(--dsw-alias-border-l3);
865
+ cursor: pointer;
866
+ touch-action: manipulation;
867
+ }
868
+ .dsh-mobile-switch[aria-checked="true"] {
869
+ background: var(--dsw-alias-brand-primary);
870
+ }
871
+ .dsh-mobile-switch:disabled {
872
+ cursor: default;
873
+ opacity: 0.5;
874
+ }
875
+ .dsh-mobile-thumb {
876
+ display: block;
877
+ width: 16px;
878
+ height: 16px;
879
+ border-radius: 50%;
880
+ background: var(--dsw-alias-label-primary-foreground);
881
+ transition: transform 0.12s ease;
882
+ }
883
+ .dsh-mobile-switch[aria-checked="false"] .dsh-mobile-thumb {
884
+ background: var(--dsw-alias-switch-thumb);
885
+ }
886
+ .dsh-mobile-switch[aria-checked="true"] .dsh-mobile-thumb {
887
+ transform: translate(16px);
888
+ }
889
+
890
+ ${M} {
891
+ --dsh-frame-top-clearance: 44px;
892
+ }
893
+ ${M} body {
894
+ height: var(--dsh-mobile-vvh, 100dvh);
895
+ overflow: hidden;
896
+ }
897
+ ${M} #root {
898
+ box-sizing: border-box;
899
+ height: var(--dsh-mobile-vvh, 100dvh);
900
+ padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left);
901
+ }
902
+ html[${NO_PULL_ATTRIBUTE}],
903
+ html[${NO_PULL_ATTRIBUTE}] body {
904
+ overscroll-behavior: none;
905
+ }
906
+
907
+ ${M} ${FRAME} {
908
+ grid-template-columns: 0px minmax(0, 1fr) 0px !important;
909
+ }
910
+ ${M} ${FRAME} > :has(> [data-slot="main"]) {
911
+ grid-column: 2;
912
+ grid-row: 1;
913
+ }
914
+ ${M} ${FRAME} > [data-rightbar-col] {
915
+ grid-column: 3;
916
+ grid-row: 1;
917
+ }
918
+ ${M} ${FRAME} > [data-side] {
919
+ display: none;
920
+ }
921
+ ${SIDEBAR} {
922
+ position: absolute;
923
+ z-index: 25;
924
+ top: 0;
925
+ bottom: 0;
926
+ left: 0;
927
+ box-sizing: border-box;
928
+ width: min(86vw, 340px);
929
+ padding-bottom: env(safe-area-inset-bottom);
930
+ border-right: none;
931
+ box-shadow: var(--dsw-elevation-prominent, 0 8px 32px rgb(0 0 0 / 20%));
932
+ overscroll-behavior: contain;
933
+ transition: transform 0.24s cubic-bezier(0.2, 0.8, 0.2, 1);
934
+ }
935
+ ${M} ${FRAME}[data-sidebar-collapsed] > :has(> [data-slot="sidebar"]) {
936
+ transform: translateX(calc(-100% - 32px));
937
+ visibility: hidden;
938
+ box-shadow: none;
939
+ transition:
940
+ transform 0.2s cubic-bezier(0.4, 0, 1, 1),
941
+ visibility 0s linear 0.2s,
942
+ box-shadow 0s linear 0.2s;
943
+ }
944
+
945
+ ${M} header:has(> [data-conversation-header-leading]) {
946
+ min-height: 0;
947
+ padding-top: 6px;
948
+ padding-left: 8px;
949
+ padding-right: 12px;
950
+ }
951
+ ${M} [data-conversation-header-corner] {
952
+ margin-right: 0;
953
+ }
954
+ ${M} [data-conversation-tabs] {
955
+ gap: 20px;
956
+ }
957
+ ${M} [data-conversation-content] {
958
+ --dsh-composer-side-clearance: 8px;
959
+ }
960
+ ${M} [data-width-handle] {
961
+ display: none;
962
+ }
963
+ ${M} [data-composer-seat] {
964
+ --dsh-composer-text-max-height: min(336px, calc(var(--dsh-mobile-vvh, 100dvh) * 0.3));
965
+ padding-bottom: env(safe-area-inset-bottom);
966
+ }
967
+ html[${MOBILE_ATTRIBUTE}][data-dsh-keyboard] [data-composer-seat] {
968
+ padding-bottom: 0;
969
+ }
970
+
971
+ ${SETTINGS} {
972
+ box-sizing: border-box;
973
+ flex-direction: column;
974
+ width: 100vw;
975
+ max-width: 100vw;
976
+ height: var(--dsh-mobile-vvh, 100dvh);
977
+ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom)
978
+ env(safe-area-inset-left);
979
+ border-radius: 0;
980
+ }
981
+ ${SETTINGS} > nav {
982
+ width: auto;
983
+ gap: 10px;
984
+ padding: 14px 12px 4px;
985
+ }
986
+ ${SETTINGS} > nav > :first-child {
987
+ padding-right: 48px;
988
+ }
989
+ ${SETTINGS} > nav > :last-child {
990
+ flex-direction: row;
991
+ gap: 4px;
992
+ overflow-x: auto;
993
+ scrollbar-width: none;
994
+ }
995
+ ${SETTINGS} > nav > :last-child > button {
996
+ flex: none;
997
+ height: 36px;
998
+ }
999
+ ${SETTINGS} > :last-child {
1000
+ min-height: 0;
1001
+ }
1002
+ ${SETTINGS} > :last-child > :first-child {
1003
+ position: absolute;
1004
+ top: calc(env(safe-area-inset-top) + 10px);
1005
+ right: calc(env(safe-area-inset-right) + 10px);
1006
+ height: auto;
1007
+ padding: 0;
1008
+ }
1009
+ ${SETTINGS} > :last-child > :last-child {
1010
+ padding: 8px 16px 16px;
1011
+ }
1012
+
1013
+ ${T} {
1014
+ -webkit-tap-highlight-color: transparent;
1015
+ }
1016
+ ${T} :is(button, a, [role="button"], [role="tab"], [role="treeitem"], [role="menuitem"], [role="option"]) {
1017
+ touch-action: manipulation;
1018
+ }
1019
+ ${T} :is(
1020
+ input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="button"], [type="submit"]),
1021
+ textarea,
1022
+ select,
1023
+ [data-composer-input],
1024
+ [data-composer-placeholder]
1025
+ ) {
1026
+ font-size: max(16px, 1em);
1027
+ }
1028
+ ${T} [role="tooltip"] {
1029
+ display: none !important;
1030
+ }
1031
+ ${T} [data-row-key^="session:"][aria-selected="true"] > [class*="_rowActions"] {
1032
+ display: inline-flex;
1033
+ }
1034
+ ${T} [data-row-key^="session:"][aria-selected="true"] > :is([class*="_time"], [class*="_pinIndicator"]) {
1035
+ display: none;
1036
+ }
1037
+ ${T} [data-row-key] > [class*="_rowActions"] {
1038
+ gap: 4px;
1039
+ }
1040
+ ${T} [data-row-key^="session:"]:not([aria-selected="true"]):hover > [class*="_rowActions"],
1041
+ ${T} [data-row-key^="workspace:"]:hover > [class*="_chevron"],
1042
+ ${T} [class*="_searchResultRow"]:hover [class*="_rowActions"] {
1043
+ display: none;
1044
+ }
1045
+ ${T} [data-row-key^="session:"]:not([aria-selected="true"]):hover > [class*="_time"] {
1046
+ display: inline;
1047
+ }
1048
+ ${T} [data-row-key^="session:"]:not([aria-selected="true"]):hover > [class*="_pinIndicator"],
1049
+ ${T} [data-row-key^="workspace:"]:hover > [class*="_folder"],
1050
+ ${T} [data-row-key^="workspace:"] > [class*="_rowActions"] {
1051
+ display: inline-flex;
1052
+ }
1053
+ ${T} [data-row-key] > [class*="_rowActions"] button {
1054
+ width: 28px;
1055
+ height: 28px;
1056
+ }
1057
+
1058
+ @media (prefers-reduced-motion: reduce) {
1059
+ .dsh-mobile-scrim,
1060
+ ${SIDEBAR} {
1061
+ transition: none !important;
1062
+ }
1063
+ }
1064
+ `;
1065
+
1066
+ // src/client/swipe.ts
1067
+ var DEFAULT_SWIPE = { edge: 24, distance: 56, slop: 10 };
1068
+ function createSwipeRecognizer(options = DEFAULT_SWIPE) {
1069
+ let gesture;
1070
+ const lock = (x, y) => {
1071
+ if (gesture === void 0 || gesture.axis !== void 0) return;
1072
+ const dx = Math.abs(x - gesture.x);
1073
+ const dy = Math.abs(y - gesture.y);
1074
+ if (dx < options.slop && dy < options.slop) return;
1075
+ gesture.axis = dx > dy * 1.2 ? "x" : "y";
1076
+ };
1077
+ return {
1078
+ start(x, y, allowed) {
1079
+ const open = allowed.open && x <= options.edge;
1080
+ gesture = open || allowed.close ? { x, y, open, close: allowed.close, axis: void 0 } : void 0;
1081
+ },
1082
+ move(x, y) {
1083
+ lock(x, y);
1084
+ },
1085
+ end(x, y) {
1086
+ lock(x, y);
1087
+ const current = gesture;
1088
+ gesture = void 0;
1089
+ if (current === void 0 || current.axis !== "x") return null;
1090
+ const dx = x - current.x;
1091
+ if (Math.abs(y - current.y) > Math.abs(dx)) return null;
1092
+ if (current.open && dx >= options.distance) return "open";
1093
+ if (current.close && dx <= -options.distance) return "close";
1094
+ return null;
1095
+ },
1096
+ cancel() {
1097
+ gesture = void 0;
1098
+ }
1099
+ };
1100
+ }
1101
+ function installSwipe(target, host, options = DEFAULT_SWIPE) {
1102
+ const recognizer = createSwipeRecognizer(options);
1103
+ const passive = { passive: true, capture: true };
1104
+ const onStart = (event) => {
1105
+ const touch = event.touches[0];
1106
+ if (!host.enabled() || event.touches.length !== 1 || touch === void 0) {
1107
+ recognizer.cancel();
1108
+ return;
1109
+ }
1110
+ if (host.blocked(event.target)) {
1111
+ recognizer.cancel();
1112
+ return;
1113
+ }
1114
+ const expanded = host.expanded();
1115
+ recognizer.start(touch.clientX, touch.clientY, { open: !expanded, close: expanded });
1116
+ };
1117
+ const onMove = (event) => {
1118
+ const touch = event.touches[0];
1119
+ if (event.touches.length !== 1 || touch === void 0) {
1120
+ recognizer.cancel();
1121
+ return;
1122
+ }
1123
+ recognizer.move(touch.clientX, touch.clientY);
1124
+ };
1125
+ const onEnd = (event) => {
1126
+ const touch = event.changedTouches[0];
1127
+ if (touch === void 0) {
1128
+ recognizer.cancel();
1129
+ return;
1130
+ }
1131
+ const action = recognizer.end(touch.clientX, touch.clientY);
1132
+ if (action === null || !host.enabled()) return;
1133
+ const expanded = host.expanded();
1134
+ if (action === "open" && !expanded || action === "close" && expanded) host.toggle();
1135
+ };
1136
+ const onCancel = () => recognizer.cancel();
1137
+ target.addEventListener("touchstart", onStart, passive);
1138
+ target.addEventListener("touchmove", onMove, passive);
1139
+ target.addEventListener("touchend", onEnd, passive);
1140
+ target.addEventListener("touchcancel", onCancel, passive);
1141
+ return () => {
1142
+ target.removeEventListener("touchstart", onStart, passive);
1143
+ target.removeEventListener("touchmove", onMove, passive);
1144
+ target.removeEventListener("touchend", onEnd, passive);
1145
+ target.removeEventListener("touchcancel", onCancel, passive);
1146
+ };
1147
+ }
1148
+
1149
+ // src/client/viewport.ts
1150
+ var VIEWPORT_ADDITIONS = {
1151
+ "viewport-fit": "cover",
1152
+ "interactive-widget": "resizes-content"
1153
+ };
1154
+ var APP_META = [
1155
+ ["apple-mobile-web-app-capable", "yes"],
1156
+ ["mobile-web-app-capable", "yes"],
1157
+ ["apple-mobile-web-app-title", "DSH"]
1158
+ ];
1159
+ function mergeViewportContent(content, additions) {
1160
+ const entries = [];
1161
+ for (const part of content.split(",")) {
1162
+ const [rawKey, ...rest] = part.split("=");
1163
+ const key = rawKey?.trim().toLowerCase() ?? "";
1164
+ if (key.length === 0) continue;
1165
+ entries.push([key, rest.join("=").trim()]);
1166
+ }
1167
+ for (const [key, value] of Object.entries(additions)) {
1168
+ const found = entries.find((entry) => entry[0] === key);
1169
+ if (found === void 0) entries.push([key, value]);
1170
+ else found[1] = value;
1171
+ }
1172
+ return entries.map(([key, value]) => value.length > 0 ? `${key}=${value}` : key).join(", ");
1173
+ }
1174
+ var OWNER = "@1outres/dsh-plugin-mobile";
1175
+ function installMeta(doc) {
1176
+ const added = [];
1177
+ let viewport = doc.querySelector('meta[name="viewport"]');
1178
+ let original;
1179
+ if (viewport === null) {
1180
+ viewport = doc.createElement("meta");
1181
+ viewport.name = "viewport";
1182
+ viewport.content = mergeViewportContent(
1183
+ "width=device-width, initial-scale=1",
1184
+ VIEWPORT_ADDITIONS
1185
+ );
1186
+ viewport.dataset.plugin = OWNER;
1187
+ doc.head.append(viewport);
1188
+ added.push(viewport);
1189
+ } else {
1190
+ original = viewport.content;
1191
+ viewport.content = mergeViewportContent(original, VIEWPORT_ADDITIONS);
1192
+ }
1193
+ for (const [name, content] of APP_META) {
1194
+ if (doc.querySelector(`meta[name="${name}"]`) !== null) continue;
1195
+ const tag = doc.createElement("meta");
1196
+ tag.name = name;
1197
+ tag.content = content;
1198
+ tag.dataset.plugin = OWNER;
1199
+ doc.head.append(tag);
1200
+ added.push(tag);
1201
+ }
1202
+ const patched = viewport;
1203
+ return () => {
1204
+ if (original !== void 0) patched.content = original;
1205
+ for (const tag of added) tag.remove();
1206
+ };
1207
+ }
1208
+ var KEYBOARD_THRESHOLD = 120;
1209
+ function measureViewport(viewport, baseline, innerHeight) {
1210
+ if (Math.abs(viewport.scale - 1) > 0.01) {
1211
+ return {
1212
+ height: innerHeight,
1213
+ keyboard: false,
1214
+ baseline: baseline ?? { width: viewport.width, height: innerHeight }
1215
+ };
1216
+ }
1217
+ const next = baseline === void 0 || Math.abs(baseline.width - viewport.width) > 1 ? { width: viewport.width, height: Math.max(viewport.height, innerHeight) } : { width: baseline.width, height: Math.max(baseline.height, viewport.height) };
1218
+ return {
1219
+ height: viewport.height,
1220
+ keyboard: next.height - viewport.height > KEYBOARD_THRESHOLD,
1221
+ baseline: next
1222
+ };
1223
+ }
1224
+ var HEIGHT_VARIABLE = "--dsh-mobile-vvh";
1225
+ var KEYBOARD_ATTRIBUTE = "data-dsh-keyboard";
1226
+ function trackViewport(win, root) {
1227
+ const viewport = win.visualViewport;
1228
+ if (viewport === void 0 || viewport === null) return () => {
1229
+ };
1230
+ let baseline;
1231
+ let frame;
1232
+ const update = () => {
1233
+ frame = void 0;
1234
+ const measure = measureViewport(viewport, baseline, win.innerHeight);
1235
+ baseline = measure.baseline;
1236
+ root.style.setProperty(HEIGHT_VARIABLE, `${Math.round(measure.height)}px`);
1237
+ root.toggleAttribute(KEYBOARD_ATTRIBUTE, measure.keyboard);
1238
+ if (measure.keyboard && win.scrollY !== 0) win.scrollTo(0, 0);
1239
+ };
1240
+ const schedule = () => {
1241
+ frame ?? (frame = win.requestAnimationFrame(update));
1242
+ };
1243
+ viewport.addEventListener("resize", schedule);
1244
+ viewport.addEventListener("scroll", schedule);
1245
+ update();
1246
+ return () => {
1247
+ viewport.removeEventListener("resize", schedule);
1248
+ viewport.removeEventListener("scroll", schedule);
1249
+ if (frame !== void 0) win.cancelAnimationFrame(frame);
1250
+ root.style.removeProperty(HEIGHT_VARIABLE);
1251
+ root.removeAttribute(KEYBOARD_ATTRIBUTE);
1252
+ };
1253
+ }
1254
+
1255
+ // src/client/index.tsx
1256
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1257
+ var inject = ["slots", "locale", "configForms", "layout"];
1258
+ var PLUGIN_ID = "@1outres/dsh-plugin-mobile";
1259
+ var HEADER_SLOT = "conversation.header.leading";
1260
+ var OVERLAY_SLOT = "shell.overlay";
1261
+ var SETTINGS_SLOT = "settings.section";
1262
+ function apply(ctx) {
1263
+ ctx.effect(() => ctx.locale.register(NAMESPACE, { en, zh, ja }), "mobile: dictionaries");
1264
+ const t = ctx.locale.bind(NAMESPACE);
1265
+ ctx.effect(() => {
1266
+ const tag = document.createElement("style");
1267
+ tag.dataset.plugin = PLUGIN_ID;
1268
+ tag.dataset.pluginCss = `${PLUGIN_ID}/mobile.css`;
1269
+ tag.textContent = MOBILE_CSS;
1270
+ document.head.append(tag);
1271
+ return () => {
1272
+ tag.remove();
1273
+ };
1274
+ }, "mobile: stylesheet");
1275
+ ctx.effect(() => installMeta(document), "mobile: viewport metadata");
1276
+ const form = ctx.configForms.get(NAMESPACE);
1277
+ let config = normalizeConfig(form.getSnapshot().value);
1278
+ const mode = createModeStore(
1279
+ (query) => window.matchMedia(query),
1280
+ () => config
1281
+ );
1282
+ ctx.effect(() => () => mode.dispose(), "mobile: layout mode");
1283
+ ctx.effect(() => {
1284
+ const root = document.documentElement;
1285
+ let stopViewport;
1286
+ const sync = () => {
1287
+ const state = mode.getSnapshot();
1288
+ root.toggleAttribute(MOBILE_ATTRIBUTE, state.mobile);
1289
+ root.toggleAttribute(TOUCH_ATTRIBUTE, state.touch && config.mode !== "off");
1290
+ root.toggleAttribute(NO_PULL_ATTRIBUTE, state.mobile && config.blockPullToRefresh);
1291
+ const fit = state.mobile && config.fitKeyboard;
1292
+ if (fit && stopViewport === void 0) {
1293
+ stopViewport = trackViewport(window, root);
1294
+ } else if (!fit && stopViewport !== void 0) {
1295
+ stopViewport();
1296
+ stopViewport = void 0;
1297
+ }
1298
+ };
1299
+ const offMode = mode.subscribe(sync);
1300
+ const offForm = form.subscribe(() => {
1301
+ config = normalizeConfig(form.getSnapshot().value);
1302
+ mode.refresh();
1303
+ sync();
1304
+ });
1305
+ sync();
1306
+ return () => {
1307
+ offMode();
1308
+ offForm();
1309
+ stopViewport?.();
1310
+ root.removeAttribute(MOBILE_ATTRIBUTE);
1311
+ root.removeAttribute(TOUCH_ATTRIBUTE);
1312
+ root.removeAttribute(NO_PULL_ATTRIBUTE);
1313
+ };
1314
+ }, "mobile: page layout");
1315
+ const toggle = () => {
1316
+ try {
1317
+ ctx.layout.toggleSidebar();
1318
+ } catch (error) {
1319
+ console.warn("mobile: could not toggle the sidebar", error);
1320
+ }
1321
+ };
1322
+ const face = { mode, toggle };
1323
+ ctx.effect(
1324
+ () => installEnterNewline(window, () => {
1325
+ const state = mode.getSnapshot();
1326
+ return state.mobile && state.touch && config.enterKey === "newline";
1327
+ }),
1328
+ "mobile: Enter adds a line"
1329
+ );
1330
+ ctx.effect(
1331
+ () => installFocusGuard(document, () => {
1332
+ const state = mode.getSnapshot();
1333
+ return state.mobile && state.touch && !config.autoFocusComposer;
1334
+ }),
1335
+ "mobile: keyboard stays closed until the composer is tapped"
1336
+ );
1337
+ ctx.effect(
1338
+ () => installSwipe(document, {
1339
+ enabled: () => mode.getSnapshot().mobile && config.swipe,
1340
+ expanded: () => isExpanded(findFrame(document)),
1341
+ blocked: (target) => swipeBlocked(
1342
+ target,
1343
+ document,
1344
+ (element) => getComputedStyle(element).overflowX
1345
+ ),
1346
+ toggle
1347
+ }),
1348
+ "mobile: drawer swipe"
1349
+ );
1350
+ ctx.effect(
1351
+ () => ctx.slots.inject(
1352
+ HEADER_SLOT,
1353
+ () => ctx.slots.register(
1354
+ { name: HEADER_SLOT, locale: NAMESPACE, inject: () => face },
1355
+ HeaderMenuButton
1356
+ )
1357
+ ),
1358
+ "mobile: header menu button"
1359
+ );
1360
+ ctx.effect(
1361
+ () => ctx.slots.inject(
1362
+ OVERLAY_SLOT,
1363
+ () => ctx.slots.register(
1364
+ { name: OVERLAY_SLOT, id: "mobile-drawer", locale: NAMESPACE, inject: () => face },
1365
+ DrawerOverlay
1366
+ )
1367
+ ),
1368
+ "mobile: drawer scrim"
1369
+ );
1370
+ const Page = () => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SettingsPage, { form, mode, t });
1371
+ ctx.effect(
1372
+ () => ctx.configForms.whileServed(
1373
+ [NAMESPACE],
1374
+ () => ctx.slots.inject(
1375
+ SETTINGS_SLOT,
1376
+ () => ctx.slots.register(
1377
+ {
1378
+ name: SETTINGS_SLOT,
1379
+ id: "mobile",
1380
+ order: 62,
1381
+ label: () => t("nav"),
1382
+ locale: NAMESPACE
1383
+ },
1384
+ Page
1385
+ )
1386
+ )
1387
+ ),
1388
+ "mobile: settings section"
1389
+ );
1390
+ }
1391
+
1392
+ return module.exports;
1393
+ }
1394
+ });