@linxin666/dsh-remote-web-ui 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +346 -0
  3. package/cordis.patch.yml +10 -0
  4. package/lib/client.js +2817 -0
  5. package/lib/index.js +1455 -0
  6. package/lib/invariant.js +21 -0
  7. package/lib/types/client/PairFailedNotice.d.ts +12 -0
  8. package/lib/types/client/PairFailedNotice.d.ts.map +1 -0
  9. package/lib/types/client/PhoneIcon.d.ts +19 -0
  10. package/lib/types/client/PhoneIcon.d.ts.map +1 -0
  11. package/lib/types/client/PluginSettingsCard.d.ts +78 -0
  12. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
  13. package/lib/types/client/RemoteEntry.d.ts +10 -0
  14. package/lib/types/client/RemoteEntry.d.ts.map +1 -0
  15. package/lib/types/client/RemotePanel.d.ts +50 -0
  16. package/lib/types/client/RemotePanel.d.ts.map +1 -0
  17. package/lib/types/client/RemoteSettingsCard.d.ts +75 -0
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -0
  19. package/lib/types/client/deep-link.d.ts +40 -0
  20. package/lib/types/client/deep-link.d.ts.map +1 -0
  21. package/lib/types/client/index.d.ts +54 -0
  22. package/lib/types/client/index.d.ts.map +1 -0
  23. package/lib/types/client/locales.d.ts +144 -0
  24. package/lib/types/client/locales.d.ts.map +1 -0
  25. package/lib/types/client/pair-api.d.ts +100 -0
  26. package/lib/types/client/pair-api.d.ts.map +1 -0
  27. package/lib/types/client/settings-form.d.ts +119 -0
  28. package/lib/types/client/settings-form.d.ts.map +1 -0
  29. package/lib/types/gate.d.ts +58 -0
  30. package/lib/types/gate.d.ts.map +1 -0
  31. package/lib/types/index.d.ts +78 -0
  32. package/lib/types/index.d.ts.map +1 -0
  33. package/lib/types/invariant.d.ts +16 -0
  34. package/lib/types/invariant.d.ts.map +1 -0
  35. package/lib/types/lan.d.ts +15 -0
  36. package/lib/types/lan.d.ts.map +1 -0
  37. package/lib/types/mobile/api.d.ts +64 -0
  38. package/lib/types/mobile/api.d.ts.map +1 -0
  39. package/lib/types/mobile/index.d.ts +10 -0
  40. package/lib/types/mobile/index.d.ts.map +1 -0
  41. package/lib/types/mobile/messages.d.ts +103 -0
  42. package/lib/types/mobile/messages.d.ts.map +1 -0
  43. package/lib/types/mobile/messages.test.d.ts +2 -0
  44. package/lib/types/mobile/messages.test.d.ts.map +1 -0
  45. package/lib/types/mobile/mobile-styles.d.ts +6 -0
  46. package/lib/types/mobile/mobile-styles.d.ts.map +1 -0
  47. package/lib/types/mobile/mobile-theme.d.ts +22 -0
  48. package/lib/types/mobile/mobile-theme.d.ts.map +1 -0
  49. package/lib/types/mobile/mobile-theme.test.d.ts +2 -0
  50. package/lib/types/mobile/mobile-theme.test.d.ts.map +1 -0
  51. package/lib/types/mobile/mux.d.ts +52 -0
  52. package/lib/types/mobile/mux.d.ts.map +1 -0
  53. package/lib/types/mobile/rpc.d.ts +38 -0
  54. package/lib/types/mobile/rpc.d.ts.map +1 -0
  55. package/lib/types/mobile/theme-toggle.d.ts +12 -0
  56. package/lib/types/mobile/theme-toggle.d.ts.map +1 -0
  57. package/lib/types/mobile/views/App.d.ts +59 -0
  58. package/lib/types/mobile/views/App.d.ts.map +1 -0
  59. package/lib/types/mobile/views/ChatView.d.ts +39 -0
  60. package/lib/types/mobile/views/ChatView.d.ts.map +1 -0
  61. package/lib/types/mobile/views/ChatView.test.d.ts +2 -0
  62. package/lib/types/mobile/views/ChatView.test.d.ts.map +1 -0
  63. package/lib/types/mobile/views/SessionListView.d.ts +27 -0
  64. package/lib/types/mobile/views/SessionListView.d.ts.map +1 -0
  65. package/lib/types/mobile/views/SessionListView.test.d.ts +2 -0
  66. package/lib/types/mobile/views/SessionListView.test.d.ts.map +1 -0
  67. package/lib/types/mobile/views/WorkspaceView.d.ts +19 -0
  68. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -0
  69. package/lib/types/mobile-api.d.ts +41 -0
  70. package/lib/types/mobile-api.d.ts.map +1 -0
  71. package/lib/types/mobile-routes.d.ts +16 -0
  72. package/lib/types/mobile-routes.d.ts.map +1 -0
  73. package/lib/types/pairing.d.ts +215 -0
  74. package/lib/types/pairing.d.ts.map +1 -0
  75. package/lib/types/routes.d.ts +58 -0
  76. package/lib/types/routes.d.ts.map +1 -0
  77. package/lib/types/tunnel.d.ts +99 -0
  78. package/lib/types/tunnel.d.ts.map +1 -0
  79. package/package.json +104 -0
  80. package/src/client/PairFailedNotice.tsx +33 -0
  81. package/src/client/PhoneIcon.tsx +31 -0
  82. package/src/client/PluginSettingsCard.tsx +207 -0
  83. package/src/client/RemoteEntry.tsx +202 -0
  84. package/src/client/RemotePanel.tsx +186 -0
  85. package/src/client/RemoteSettingsCard.tsx +220 -0
  86. package/src/client/deep-link.ts +137 -0
  87. package/src/client/index.ts +167 -0
  88. package/src/client/locales.ts +149 -0
  89. package/src/client/pair-api.ts +176 -0
  90. package/src/client/remote.module.css +422 -0
  91. package/src/client/settings-card.module.css +296 -0
  92. package/src/client/settings-form.ts +294 -0
  93. package/src/css-modules.d.ts +6 -0
  94. package/src/gate.ts +121 -0
  95. package/src/index.ts +299 -0
  96. package/src/invariant.ts +32 -0
  97. package/src/lan.ts +21 -0
  98. package/src/mobile/api.ts +106 -0
  99. package/src/mobile/index.tsx +27 -0
  100. package/src/mobile/messages.test.ts +222 -0
  101. package/src/mobile/messages.ts +555 -0
  102. package/src/mobile/mobile-styles.ts +853 -0
  103. package/src/mobile/mobile-theme.test.ts +84 -0
  104. package/src/mobile/mobile-theme.ts +76 -0
  105. package/src/mobile/mux.ts +126 -0
  106. package/src/mobile/rpc.ts +90 -0
  107. package/src/mobile/theme-toggle.tsx +47 -0
  108. package/src/mobile/views/App.tsx +152 -0
  109. package/src/mobile/views/ChatView.test.tsx +243 -0
  110. package/src/mobile/views/ChatView.tsx +650 -0
  111. package/src/mobile/views/SessionListView.test.tsx +139 -0
  112. package/src/mobile/views/SessionListView.tsx +186 -0
  113. package/src/mobile/views/WorkspaceView.tsx +108 -0
  114. package/src/mobile-api.ts +271 -0
  115. package/src/mobile-routes.ts +77 -0
  116. package/src/pairing.ts +390 -0
  117. package/src/routes.ts +346 -0
  118. package/src/tunnel.ts +264 -0
package/lib/client.js ADDED
@@ -0,0 +1,2817 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@linxin666/dsh-remote-web-ui",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region \0rolldown/runtime.js
8
+ var __create = Object.create;
9
+ var __defProp$1 = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp$1 = Object.prototype.hasOwnProperty;
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
16
+ key = keys[i];
17
+ if (!__hasOwnProp$1.call(to, key) && key !== except) __defProp$1(to, key, {
18
+ get: ((k) => from[k]).bind(null, key),
19
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
+ });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp$1(target, "default", {
25
+ value: mod,
26
+ enumerable: true
27
+ }) : target, mod));
28
+ //#endregion
29
+ let react = require("react");
30
+ react = __toESM(react, 1);
31
+ let react_dom_client = require("react-dom/client");
32
+ let react_dom = require("react-dom");
33
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
34
+ let react_jsx_runtime = require("react/jsx-runtime");
35
+ let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
36
+ //#region ../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
37
+ function r(e) {
38
+ var t, f, n = "";
39
+ if ("string" == typeof e || "number" == typeof e) n += e;
40
+ else if ("object" == typeof e) if (Array.isArray(e)) {
41
+ var o = e.length;
42
+ for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
43
+ } else for (f in e) e[f] && (n && (n += " "), n += f);
44
+ return n;
45
+ }
46
+ function clsx() {
47
+ for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
48
+ return n;
49
+ }
50
+ //#endregion
51
+ //#region ../../node_modules/.pnpm/qrcode.react@4.2.0_react@18.3.1/node_modules/qrcode.react/lib/esm/index.js
52
+ var __defProp = Object.defineProperty;
53
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
54
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
55
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
56
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
57
+ enumerable: true,
58
+ configurable: true,
59
+ writable: true,
60
+ value
61
+ }) : obj[key] = value;
62
+ var __spreadValues = (a, b) => {
63
+ for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]);
64
+ if (__getOwnPropSymbols) {
65
+ for (var prop of __getOwnPropSymbols(b)) if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]);
66
+ }
67
+ return a;
68
+ };
69
+ var __objRest = (source, exclude) => {
70
+ var target = {};
71
+ for (var prop in source) if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0) target[prop] = source[prop];
72
+ if (source != null && __getOwnPropSymbols) {
73
+ for (var prop of __getOwnPropSymbols(source)) if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop)) target[prop] = source[prop];
74
+ }
75
+ return target;
76
+ };
77
+ /**
78
+ * @license QR Code generator library (TypeScript)
79
+ * Copyright (c) Project Nayuki.
80
+ * SPDX-License-Identifier: MIT
81
+ */
82
+ var qrcodegen;
83
+ ((qrcodegen2) => {
84
+ const _QrCode = class _QrCode {
85
+ constructor(version, errorCorrectionLevel, dataCodewords, msk) {
86
+ this.version = version;
87
+ this.errorCorrectionLevel = errorCorrectionLevel;
88
+ this.modules = [];
89
+ this.isFunction = [];
90
+ if (version < _QrCode.MIN_VERSION || version > _QrCode.MAX_VERSION) throw new RangeError("Version value out of range");
91
+ if (msk < -1 || msk > 7) throw new RangeError("Mask value out of range");
92
+ this.size = version * 4 + 17;
93
+ let row = [];
94
+ for (let i = 0; i < this.size; i++) row.push(false);
95
+ for (let i = 0; i < this.size; i++) {
96
+ this.modules.push(row.slice());
97
+ this.isFunction.push(row.slice());
98
+ }
99
+ this.drawFunctionPatterns();
100
+ const allCodewords = this.addEccAndInterleave(dataCodewords);
101
+ this.drawCodewords(allCodewords);
102
+ if (msk == -1) {
103
+ let minPenalty = 1e9;
104
+ for (let i = 0; i < 8; i++) {
105
+ this.applyMask(i);
106
+ this.drawFormatBits(i);
107
+ const penalty = this.getPenaltyScore();
108
+ if (penalty < minPenalty) {
109
+ msk = i;
110
+ minPenalty = penalty;
111
+ }
112
+ this.applyMask(i);
113
+ }
114
+ }
115
+ assert(0 <= msk && msk <= 7);
116
+ this.mask = msk;
117
+ this.applyMask(msk);
118
+ this.drawFormatBits(msk);
119
+ this.isFunction = [];
120
+ }
121
+ static encodeText(text, ecl) {
122
+ const segs = qrcodegen2.QrSegment.makeSegments(text);
123
+ return _QrCode.encodeSegments(segs, ecl);
124
+ }
125
+ static encodeBinary(data, ecl) {
126
+ const seg = qrcodegen2.QrSegment.makeBytes(data);
127
+ return _QrCode.encodeSegments([seg], ecl);
128
+ }
129
+ static encodeSegments(segs, ecl, minVersion = 1, maxVersion = 40, mask = -1, boostEcl = true) {
130
+ if (!(_QrCode.MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= _QrCode.MAX_VERSION) || mask < -1 || mask > 7) throw new RangeError("Invalid value");
131
+ let version;
132
+ let dataUsedBits;
133
+ for (version = minVersion;; version++) {
134
+ const dataCapacityBits2 = _QrCode.getNumDataCodewords(version, ecl) * 8;
135
+ const usedBits = QrSegment.getTotalBits(segs, version);
136
+ if (usedBits <= dataCapacityBits2) {
137
+ dataUsedBits = usedBits;
138
+ break;
139
+ }
140
+ if (version >= maxVersion) throw new RangeError("Data too long");
141
+ }
142
+ for (const newEcl of [
143
+ _QrCode.Ecc.MEDIUM,
144
+ _QrCode.Ecc.QUARTILE,
145
+ _QrCode.Ecc.HIGH
146
+ ]) if (boostEcl && dataUsedBits <= _QrCode.getNumDataCodewords(version, newEcl) * 8) ecl = newEcl;
147
+ let bb = [];
148
+ for (const seg of segs) {
149
+ appendBits(seg.mode.modeBits, 4, bb);
150
+ appendBits(seg.numChars, seg.mode.numCharCountBits(version), bb);
151
+ for (const b of seg.getData()) bb.push(b);
152
+ }
153
+ assert(bb.length == dataUsedBits);
154
+ const dataCapacityBits = _QrCode.getNumDataCodewords(version, ecl) * 8;
155
+ assert(bb.length <= dataCapacityBits);
156
+ appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb);
157
+ appendBits(0, (8 - bb.length % 8) % 8, bb);
158
+ assert(bb.length % 8 == 0);
159
+ for (let padByte = 236; bb.length < dataCapacityBits; padByte ^= 253) appendBits(padByte, 8, bb);
160
+ let dataCodewords = [];
161
+ while (dataCodewords.length * 8 < bb.length) dataCodewords.push(0);
162
+ bb.forEach((b, i) => dataCodewords[i >>> 3] |= b << 7 - (i & 7));
163
+ return new _QrCode(version, ecl, dataCodewords, mask);
164
+ }
165
+ getModule(x, y) {
166
+ return 0 <= x && x < this.size && 0 <= y && y < this.size && this.modules[y][x];
167
+ }
168
+ getModules() {
169
+ return this.modules;
170
+ }
171
+ drawFunctionPatterns() {
172
+ for (let i = 0; i < this.size; i++) {
173
+ this.setFunctionModule(6, i, i % 2 == 0);
174
+ this.setFunctionModule(i, 6, i % 2 == 0);
175
+ }
176
+ this.drawFinderPattern(3, 3);
177
+ this.drawFinderPattern(this.size - 4, 3);
178
+ this.drawFinderPattern(3, this.size - 4);
179
+ const alignPatPos = this.getAlignmentPatternPositions();
180
+ const numAlign = alignPatPos.length;
181
+ for (let i = 0; i < numAlign; i++) for (let j = 0; j < numAlign; j++) if (!(i == 0 && j == 0 || i == 0 && j == numAlign - 1 || i == numAlign - 1 && j == 0)) this.drawAlignmentPattern(alignPatPos[i], alignPatPos[j]);
182
+ this.drawFormatBits(0);
183
+ this.drawVersion();
184
+ }
185
+ drawFormatBits(mask) {
186
+ const data = this.errorCorrectionLevel.formatBits << 3 | mask;
187
+ let rem = data;
188
+ for (let i = 0; i < 10; i++) rem = rem << 1 ^ (rem >>> 9) * 1335;
189
+ const bits = (data << 10 | rem) ^ 21522;
190
+ assert(bits >>> 15 == 0);
191
+ for (let i = 0; i <= 5; i++) this.setFunctionModule(8, i, getBit(bits, i));
192
+ this.setFunctionModule(8, 7, getBit(bits, 6));
193
+ this.setFunctionModule(8, 8, getBit(bits, 7));
194
+ this.setFunctionModule(7, 8, getBit(bits, 8));
195
+ for (let i = 9; i < 15; i++) this.setFunctionModule(14 - i, 8, getBit(bits, i));
196
+ for (let i = 0; i < 8; i++) this.setFunctionModule(this.size - 1 - i, 8, getBit(bits, i));
197
+ for (let i = 8; i < 15; i++) this.setFunctionModule(8, this.size - 15 + i, getBit(bits, i));
198
+ this.setFunctionModule(8, this.size - 8, true);
199
+ }
200
+ drawVersion() {
201
+ if (this.version < 7) return;
202
+ let rem = this.version;
203
+ for (let i = 0; i < 12; i++) rem = rem << 1 ^ (rem >>> 11) * 7973;
204
+ const bits = this.version << 12 | rem;
205
+ assert(bits >>> 18 == 0);
206
+ for (let i = 0; i < 18; i++) {
207
+ const color = getBit(bits, i);
208
+ const a = this.size - 11 + i % 3;
209
+ const b = Math.floor(i / 3);
210
+ this.setFunctionModule(a, b, color);
211
+ this.setFunctionModule(b, a, color);
212
+ }
213
+ }
214
+ drawFinderPattern(x, y) {
215
+ for (let dy = -4; dy <= 4; dy++) for (let dx = -4; dx <= 4; dx++) {
216
+ const dist = Math.max(Math.abs(dx), Math.abs(dy));
217
+ const xx = x + dx;
218
+ const yy = y + dy;
219
+ if (0 <= xx && xx < this.size && 0 <= yy && yy < this.size) this.setFunctionModule(xx, yy, dist != 2 && dist != 4);
220
+ }
221
+ }
222
+ drawAlignmentPattern(x, y) {
223
+ for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) this.setFunctionModule(x + dx, y + dy, Math.max(Math.abs(dx), Math.abs(dy)) != 1);
224
+ }
225
+ setFunctionModule(x, y, isDark) {
226
+ this.modules[y][x] = isDark;
227
+ this.isFunction[y][x] = true;
228
+ }
229
+ addEccAndInterleave(data) {
230
+ const ver = this.version;
231
+ const ecl = this.errorCorrectionLevel;
232
+ if (data.length != _QrCode.getNumDataCodewords(ver, ecl)) throw new RangeError("Invalid argument");
233
+ const numBlocks = _QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver];
234
+ const blockEccLen = _QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver];
235
+ const rawCodewords = Math.floor(_QrCode.getNumRawDataModules(ver) / 8);
236
+ const numShortBlocks = numBlocks - rawCodewords % numBlocks;
237
+ const shortBlockLen = Math.floor(rawCodewords / numBlocks);
238
+ let blocks = [];
239
+ const rsDiv = _QrCode.reedSolomonComputeDivisor(blockEccLen);
240
+ for (let i = 0, k = 0; i < numBlocks; i++) {
241
+ let dat = data.slice(k, k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1));
242
+ k += dat.length;
243
+ const ecc = _QrCode.reedSolomonComputeRemainder(dat, rsDiv);
244
+ if (i < numShortBlocks) dat.push(0);
245
+ blocks.push(dat.concat(ecc));
246
+ }
247
+ let result = [];
248
+ for (let i = 0; i < blocks[0].length; i++) blocks.forEach((block, j) => {
249
+ if (i != shortBlockLen - blockEccLen || j >= numShortBlocks) result.push(block[i]);
250
+ });
251
+ assert(result.length == rawCodewords);
252
+ return result;
253
+ }
254
+ drawCodewords(data) {
255
+ if (data.length != Math.floor(_QrCode.getNumRawDataModules(this.version) / 8)) throw new RangeError("Invalid argument");
256
+ let i = 0;
257
+ for (let right = this.size - 1; right >= 1; right -= 2) {
258
+ if (right == 6) right = 5;
259
+ for (let vert = 0; vert < this.size; vert++) for (let j = 0; j < 2; j++) {
260
+ const x = right - j;
261
+ const y = (right + 1 & 2) == 0 ? this.size - 1 - vert : vert;
262
+ if (!this.isFunction[y][x] && i < data.length * 8) {
263
+ this.modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));
264
+ i++;
265
+ }
266
+ }
267
+ }
268
+ assert(i == data.length * 8);
269
+ }
270
+ applyMask(mask) {
271
+ if (mask < 0 || mask > 7) throw new RangeError("Mask value out of range");
272
+ for (let y = 0; y < this.size; y++) for (let x = 0; x < this.size; x++) {
273
+ let invert;
274
+ switch (mask) {
275
+ case 0:
276
+ invert = (x + y) % 2 == 0;
277
+ break;
278
+ case 1:
279
+ invert = y % 2 == 0;
280
+ break;
281
+ case 2:
282
+ invert = x % 3 == 0;
283
+ break;
284
+ case 3:
285
+ invert = (x + y) % 3 == 0;
286
+ break;
287
+ case 4:
288
+ invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 == 0;
289
+ break;
290
+ case 5:
291
+ invert = x * y % 2 + x * y % 3 == 0;
292
+ break;
293
+ case 6:
294
+ invert = (x * y % 2 + x * y % 3) % 2 == 0;
295
+ break;
296
+ case 7:
297
+ invert = ((x + y) % 2 + x * y % 3) % 2 == 0;
298
+ break;
299
+ default: throw new Error("Unreachable");
300
+ }
301
+ if (!this.isFunction[y][x] && invert) this.modules[y][x] = !this.modules[y][x];
302
+ }
303
+ }
304
+ getPenaltyScore() {
305
+ let result = 0;
306
+ for (let y = 0; y < this.size; y++) {
307
+ let runColor = false;
308
+ let runX = 0;
309
+ let runHistory = [
310
+ 0,
311
+ 0,
312
+ 0,
313
+ 0,
314
+ 0,
315
+ 0,
316
+ 0
317
+ ];
318
+ for (let x = 0; x < this.size; x++) if (this.modules[y][x] == runColor) {
319
+ runX++;
320
+ if (runX == 5) result += _QrCode.PENALTY_N1;
321
+ else if (runX > 5) result++;
322
+ } else {
323
+ this.finderPenaltyAddHistory(runX, runHistory);
324
+ if (!runColor) result += this.finderPenaltyCountPatterns(runHistory) * _QrCode.PENALTY_N3;
325
+ runColor = this.modules[y][x];
326
+ runX = 1;
327
+ }
328
+ result += this.finderPenaltyTerminateAndCount(runColor, runX, runHistory) * _QrCode.PENALTY_N3;
329
+ }
330
+ for (let x = 0; x < this.size; x++) {
331
+ let runColor = false;
332
+ let runY = 0;
333
+ let runHistory = [
334
+ 0,
335
+ 0,
336
+ 0,
337
+ 0,
338
+ 0,
339
+ 0,
340
+ 0
341
+ ];
342
+ for (let y = 0; y < this.size; y++) if (this.modules[y][x] == runColor) {
343
+ runY++;
344
+ if (runY == 5) result += _QrCode.PENALTY_N1;
345
+ else if (runY > 5) result++;
346
+ } else {
347
+ this.finderPenaltyAddHistory(runY, runHistory);
348
+ if (!runColor) result += this.finderPenaltyCountPatterns(runHistory) * _QrCode.PENALTY_N3;
349
+ runColor = this.modules[y][x];
350
+ runY = 1;
351
+ }
352
+ result += this.finderPenaltyTerminateAndCount(runColor, runY, runHistory) * _QrCode.PENALTY_N3;
353
+ }
354
+ for (let y = 0; y < this.size - 1; y++) for (let x = 0; x < this.size - 1; x++) {
355
+ const color = this.modules[y][x];
356
+ if (color == this.modules[y][x + 1] && color == this.modules[y + 1][x] && color == this.modules[y + 1][x + 1]) result += _QrCode.PENALTY_N2;
357
+ }
358
+ let dark = 0;
359
+ for (const row of this.modules) dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark);
360
+ const total = this.size * this.size;
361
+ const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
362
+ assert(0 <= k && k <= 9);
363
+ result += k * _QrCode.PENALTY_N4;
364
+ assert(0 <= result && result <= 2568888);
365
+ return result;
366
+ }
367
+ getAlignmentPatternPositions() {
368
+ if (this.version == 1) return [];
369
+ else {
370
+ const numAlign = Math.floor(this.version / 7) + 2;
371
+ const step = this.version == 32 ? 26 : Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2;
372
+ let result = [6];
373
+ for (let pos = this.size - 7; result.length < numAlign; pos -= step) result.splice(1, 0, pos);
374
+ return result;
375
+ }
376
+ }
377
+ static getNumRawDataModules(ver) {
378
+ if (ver < _QrCode.MIN_VERSION || ver > _QrCode.MAX_VERSION) throw new RangeError("Version number out of range");
379
+ let result = (16 * ver + 128) * ver + 64;
380
+ if (ver >= 2) {
381
+ const numAlign = Math.floor(ver / 7) + 2;
382
+ result -= (25 * numAlign - 10) * numAlign - 55;
383
+ if (ver >= 7) result -= 36;
384
+ }
385
+ assert(208 <= result && result <= 29648);
386
+ return result;
387
+ }
388
+ static getNumDataCodewords(ver, ecl) {
389
+ return Math.floor(_QrCode.getNumRawDataModules(ver) / 8) - _QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver] * _QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver];
390
+ }
391
+ static reedSolomonComputeDivisor(degree) {
392
+ if (degree < 1 || degree > 255) throw new RangeError("Degree out of range");
393
+ let result = [];
394
+ for (let i = 0; i < degree - 1; i++) result.push(0);
395
+ result.push(1);
396
+ let root = 1;
397
+ for (let i = 0; i < degree; i++) {
398
+ for (let j = 0; j < result.length; j++) {
399
+ result[j] = _QrCode.reedSolomonMultiply(result[j], root);
400
+ if (j + 1 < result.length) result[j] ^= result[j + 1];
401
+ }
402
+ root = _QrCode.reedSolomonMultiply(root, 2);
403
+ }
404
+ return result;
405
+ }
406
+ static reedSolomonComputeRemainder(data, divisor) {
407
+ let result = divisor.map((_) => 0);
408
+ for (const b of data) {
409
+ const factor = b ^ result.shift();
410
+ result.push(0);
411
+ divisor.forEach((coef, i) => result[i] ^= _QrCode.reedSolomonMultiply(coef, factor));
412
+ }
413
+ return result;
414
+ }
415
+ static reedSolomonMultiply(x, y) {
416
+ if (x >>> 8 != 0 || y >>> 8 != 0) throw new RangeError("Byte out of range");
417
+ let z = 0;
418
+ for (let i = 7; i >= 0; i--) {
419
+ z = z << 1 ^ (z >>> 7) * 285;
420
+ z ^= (y >>> i & 1) * x;
421
+ }
422
+ assert(z >>> 8 == 0);
423
+ return z;
424
+ }
425
+ finderPenaltyCountPatterns(runHistory) {
426
+ const n = runHistory[1];
427
+ assert(n <= this.size * 3);
428
+ const core = n > 0 && runHistory[2] == n && runHistory[3] == n * 3 && runHistory[4] == n && runHistory[5] == n;
429
+ return (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0) + (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0);
430
+ }
431
+ finderPenaltyTerminateAndCount(currentRunColor, currentRunLength, runHistory) {
432
+ if (currentRunColor) {
433
+ this.finderPenaltyAddHistory(currentRunLength, runHistory);
434
+ currentRunLength = 0;
435
+ }
436
+ currentRunLength += this.size;
437
+ this.finderPenaltyAddHistory(currentRunLength, runHistory);
438
+ return this.finderPenaltyCountPatterns(runHistory);
439
+ }
440
+ finderPenaltyAddHistory(currentRunLength, runHistory) {
441
+ if (runHistory[0] == 0) currentRunLength += this.size;
442
+ runHistory.pop();
443
+ runHistory.unshift(currentRunLength);
444
+ }
445
+ };
446
+ _QrCode.MIN_VERSION = 1;
447
+ _QrCode.MAX_VERSION = 40;
448
+ _QrCode.PENALTY_N1 = 3;
449
+ _QrCode.PENALTY_N2 = 3;
450
+ _QrCode.PENALTY_N3 = 40;
451
+ _QrCode.PENALTY_N4 = 10;
452
+ _QrCode.ECC_CODEWORDS_PER_BLOCK = [
453
+ [
454
+ -1,
455
+ 7,
456
+ 10,
457
+ 15,
458
+ 20,
459
+ 26,
460
+ 18,
461
+ 20,
462
+ 24,
463
+ 30,
464
+ 18,
465
+ 20,
466
+ 24,
467
+ 26,
468
+ 30,
469
+ 22,
470
+ 24,
471
+ 28,
472
+ 30,
473
+ 28,
474
+ 28,
475
+ 28,
476
+ 28,
477
+ 30,
478
+ 30,
479
+ 26,
480
+ 28,
481
+ 30,
482
+ 30,
483
+ 30,
484
+ 30,
485
+ 30,
486
+ 30,
487
+ 30,
488
+ 30,
489
+ 30,
490
+ 30,
491
+ 30,
492
+ 30,
493
+ 30,
494
+ 30
495
+ ],
496
+ [
497
+ -1,
498
+ 10,
499
+ 16,
500
+ 26,
501
+ 18,
502
+ 24,
503
+ 16,
504
+ 18,
505
+ 22,
506
+ 22,
507
+ 26,
508
+ 30,
509
+ 22,
510
+ 22,
511
+ 24,
512
+ 24,
513
+ 28,
514
+ 28,
515
+ 26,
516
+ 26,
517
+ 26,
518
+ 26,
519
+ 28,
520
+ 28,
521
+ 28,
522
+ 28,
523
+ 28,
524
+ 28,
525
+ 28,
526
+ 28,
527
+ 28,
528
+ 28,
529
+ 28,
530
+ 28,
531
+ 28,
532
+ 28,
533
+ 28,
534
+ 28,
535
+ 28,
536
+ 28,
537
+ 28
538
+ ],
539
+ [
540
+ -1,
541
+ 13,
542
+ 22,
543
+ 18,
544
+ 26,
545
+ 18,
546
+ 24,
547
+ 18,
548
+ 22,
549
+ 20,
550
+ 24,
551
+ 28,
552
+ 26,
553
+ 24,
554
+ 20,
555
+ 30,
556
+ 24,
557
+ 28,
558
+ 28,
559
+ 26,
560
+ 30,
561
+ 28,
562
+ 30,
563
+ 30,
564
+ 30,
565
+ 30,
566
+ 28,
567
+ 30,
568
+ 30,
569
+ 30,
570
+ 30,
571
+ 30,
572
+ 30,
573
+ 30,
574
+ 30,
575
+ 30,
576
+ 30,
577
+ 30,
578
+ 30,
579
+ 30,
580
+ 30
581
+ ],
582
+ [
583
+ -1,
584
+ 17,
585
+ 28,
586
+ 22,
587
+ 16,
588
+ 22,
589
+ 28,
590
+ 26,
591
+ 26,
592
+ 24,
593
+ 28,
594
+ 24,
595
+ 28,
596
+ 22,
597
+ 24,
598
+ 24,
599
+ 30,
600
+ 28,
601
+ 28,
602
+ 26,
603
+ 28,
604
+ 30,
605
+ 24,
606
+ 30,
607
+ 30,
608
+ 30,
609
+ 30,
610
+ 30,
611
+ 30,
612
+ 30,
613
+ 30,
614
+ 30,
615
+ 30,
616
+ 30,
617
+ 30,
618
+ 30,
619
+ 30,
620
+ 30,
621
+ 30,
622
+ 30,
623
+ 30
624
+ ]
625
+ ];
626
+ _QrCode.NUM_ERROR_CORRECTION_BLOCKS = [
627
+ [
628
+ -1,
629
+ 1,
630
+ 1,
631
+ 1,
632
+ 1,
633
+ 1,
634
+ 2,
635
+ 2,
636
+ 2,
637
+ 2,
638
+ 4,
639
+ 4,
640
+ 4,
641
+ 4,
642
+ 4,
643
+ 6,
644
+ 6,
645
+ 6,
646
+ 6,
647
+ 7,
648
+ 8,
649
+ 8,
650
+ 9,
651
+ 9,
652
+ 10,
653
+ 12,
654
+ 12,
655
+ 12,
656
+ 13,
657
+ 14,
658
+ 15,
659
+ 16,
660
+ 17,
661
+ 18,
662
+ 19,
663
+ 19,
664
+ 20,
665
+ 21,
666
+ 22,
667
+ 24,
668
+ 25
669
+ ],
670
+ [
671
+ -1,
672
+ 1,
673
+ 1,
674
+ 1,
675
+ 2,
676
+ 2,
677
+ 4,
678
+ 4,
679
+ 4,
680
+ 5,
681
+ 5,
682
+ 5,
683
+ 8,
684
+ 9,
685
+ 9,
686
+ 10,
687
+ 10,
688
+ 11,
689
+ 13,
690
+ 14,
691
+ 16,
692
+ 17,
693
+ 17,
694
+ 18,
695
+ 20,
696
+ 21,
697
+ 23,
698
+ 25,
699
+ 26,
700
+ 28,
701
+ 29,
702
+ 31,
703
+ 33,
704
+ 35,
705
+ 37,
706
+ 38,
707
+ 40,
708
+ 43,
709
+ 45,
710
+ 47,
711
+ 49
712
+ ],
713
+ [
714
+ -1,
715
+ 1,
716
+ 1,
717
+ 2,
718
+ 2,
719
+ 4,
720
+ 4,
721
+ 6,
722
+ 6,
723
+ 8,
724
+ 8,
725
+ 8,
726
+ 10,
727
+ 12,
728
+ 16,
729
+ 12,
730
+ 17,
731
+ 16,
732
+ 18,
733
+ 21,
734
+ 20,
735
+ 23,
736
+ 23,
737
+ 25,
738
+ 27,
739
+ 29,
740
+ 34,
741
+ 34,
742
+ 35,
743
+ 38,
744
+ 40,
745
+ 43,
746
+ 45,
747
+ 48,
748
+ 51,
749
+ 53,
750
+ 56,
751
+ 59,
752
+ 62,
753
+ 65,
754
+ 68
755
+ ],
756
+ [
757
+ -1,
758
+ 1,
759
+ 1,
760
+ 2,
761
+ 4,
762
+ 4,
763
+ 4,
764
+ 5,
765
+ 6,
766
+ 8,
767
+ 8,
768
+ 11,
769
+ 11,
770
+ 16,
771
+ 16,
772
+ 18,
773
+ 16,
774
+ 19,
775
+ 21,
776
+ 25,
777
+ 25,
778
+ 25,
779
+ 34,
780
+ 30,
781
+ 32,
782
+ 35,
783
+ 37,
784
+ 40,
785
+ 42,
786
+ 45,
787
+ 48,
788
+ 51,
789
+ 54,
790
+ 57,
791
+ 60,
792
+ 63,
793
+ 66,
794
+ 70,
795
+ 74,
796
+ 77,
797
+ 81
798
+ ]
799
+ ];
800
+ qrcodegen2.QrCode = _QrCode;
801
+ function appendBits(val, len, bb) {
802
+ if (len < 0 || len > 31 || val >>> len != 0) throw new RangeError("Value out of range");
803
+ for (let i = len - 1; i >= 0; i--) bb.push(val >>> i & 1);
804
+ }
805
+ function getBit(x, i) {
806
+ return (x >>> i & 1) != 0;
807
+ }
808
+ function assert(cond) {
809
+ if (!cond) throw new Error("Assertion error");
810
+ }
811
+ const _QrSegment = class _QrSegment {
812
+ constructor(mode, numChars, bitData) {
813
+ this.mode = mode;
814
+ this.numChars = numChars;
815
+ this.bitData = bitData;
816
+ if (numChars < 0) throw new RangeError("Invalid argument");
817
+ this.bitData = bitData.slice();
818
+ }
819
+ static makeBytes(data) {
820
+ let bb = [];
821
+ for (const b of data) appendBits(b, 8, bb);
822
+ return new _QrSegment(_QrSegment.Mode.BYTE, data.length, bb);
823
+ }
824
+ static makeNumeric(digits) {
825
+ if (!_QrSegment.isNumeric(digits)) throw new RangeError("String contains non-numeric characters");
826
+ let bb = [];
827
+ for (let i = 0; i < digits.length;) {
828
+ const n = Math.min(digits.length - i, 3);
829
+ appendBits(parseInt(digits.substring(i, i + n), 10), n * 3 + 1, bb);
830
+ i += n;
831
+ }
832
+ return new _QrSegment(_QrSegment.Mode.NUMERIC, digits.length, bb);
833
+ }
834
+ static makeAlphanumeric(text) {
835
+ if (!_QrSegment.isAlphanumeric(text)) throw new RangeError("String contains unencodable characters in alphanumeric mode");
836
+ let bb = [];
837
+ let i;
838
+ for (i = 0; i + 2 <= text.length; i += 2) {
839
+ let temp = _QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)) * 45;
840
+ temp += _QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i + 1));
841
+ appendBits(temp, 11, bb);
842
+ }
843
+ if (i < text.length) appendBits(_QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6, bb);
844
+ return new _QrSegment(_QrSegment.Mode.ALPHANUMERIC, text.length, bb);
845
+ }
846
+ static makeSegments(text) {
847
+ if (text == "") return [];
848
+ else if (_QrSegment.isNumeric(text)) return [_QrSegment.makeNumeric(text)];
849
+ else if (_QrSegment.isAlphanumeric(text)) return [_QrSegment.makeAlphanumeric(text)];
850
+ else return [_QrSegment.makeBytes(_QrSegment.toUtf8ByteArray(text))];
851
+ }
852
+ static makeEci(assignVal) {
853
+ let bb = [];
854
+ if (assignVal < 0) throw new RangeError("ECI assignment value out of range");
855
+ else if (assignVal < 128) appendBits(assignVal, 8, bb);
856
+ else if (assignVal < 16384) {
857
+ appendBits(2, 2, bb);
858
+ appendBits(assignVal, 14, bb);
859
+ } else if (assignVal < 1e6) {
860
+ appendBits(6, 3, bb);
861
+ appendBits(assignVal, 21, bb);
862
+ } else throw new RangeError("ECI assignment value out of range");
863
+ return new _QrSegment(_QrSegment.Mode.ECI, 0, bb);
864
+ }
865
+ static isNumeric(text) {
866
+ return _QrSegment.NUMERIC_REGEX.test(text);
867
+ }
868
+ static isAlphanumeric(text) {
869
+ return _QrSegment.ALPHANUMERIC_REGEX.test(text);
870
+ }
871
+ getData() {
872
+ return this.bitData.slice();
873
+ }
874
+ static getTotalBits(segs, version) {
875
+ let result = 0;
876
+ for (const seg of segs) {
877
+ const ccbits = seg.mode.numCharCountBits(version);
878
+ if (seg.numChars >= 1 << ccbits) return Infinity;
879
+ result += 4 + ccbits + seg.bitData.length;
880
+ }
881
+ return result;
882
+ }
883
+ static toUtf8ByteArray(str) {
884
+ str = encodeURI(str);
885
+ let result = [];
886
+ for (let i = 0; i < str.length; i++) if (str.charAt(i) != "%") result.push(str.charCodeAt(i));
887
+ else {
888
+ result.push(parseInt(str.substring(i + 1, i + 3), 16));
889
+ i += 2;
890
+ }
891
+ return result;
892
+ }
893
+ };
894
+ _QrSegment.NUMERIC_REGEX = /^[0-9]*$/;
895
+ _QrSegment.ALPHANUMERIC_REGEX = /^[A-Z0-9 $%*+.\/:-]*$/;
896
+ _QrSegment.ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
897
+ let QrSegment = _QrSegment;
898
+ qrcodegen2.QrSegment = _QrSegment;
899
+ })(qrcodegen || (qrcodegen = {}));
900
+ ((qrcodegen2) => {
901
+ ((QrCode2) => {
902
+ const _Ecc = class _Ecc {
903
+ constructor(ordinal, formatBits) {
904
+ this.ordinal = ordinal;
905
+ this.formatBits = formatBits;
906
+ }
907
+ };
908
+ _Ecc.LOW = new _Ecc(0, 1);
909
+ _Ecc.MEDIUM = new _Ecc(1, 0);
910
+ _Ecc.QUARTILE = new _Ecc(2, 3);
911
+ _Ecc.HIGH = new _Ecc(3, 2);
912
+ QrCode2.Ecc = _Ecc;
913
+ })(qrcodegen2.QrCode || (qrcodegen2.QrCode = {}));
914
+ })(qrcodegen || (qrcodegen = {}));
915
+ ((qrcodegen2) => {
916
+ ((QrSegment2) => {
917
+ const _Mode = class _Mode {
918
+ constructor(modeBits, numBitsCharCount) {
919
+ this.modeBits = modeBits;
920
+ this.numBitsCharCount = numBitsCharCount;
921
+ }
922
+ numCharCountBits(ver) {
923
+ return this.numBitsCharCount[Math.floor((ver + 7) / 17)];
924
+ }
925
+ };
926
+ _Mode.NUMERIC = new _Mode(1, [
927
+ 10,
928
+ 12,
929
+ 14
930
+ ]);
931
+ _Mode.ALPHANUMERIC = new _Mode(2, [
932
+ 9,
933
+ 11,
934
+ 13
935
+ ]);
936
+ _Mode.BYTE = new _Mode(4, [
937
+ 8,
938
+ 16,
939
+ 16
940
+ ]);
941
+ _Mode.KANJI = new _Mode(8, [
942
+ 8,
943
+ 10,
944
+ 12
945
+ ]);
946
+ _Mode.ECI = new _Mode(7, [
947
+ 0,
948
+ 0,
949
+ 0
950
+ ]);
951
+ QrSegment2.Mode = _Mode;
952
+ })(qrcodegen2.QrSegment || (qrcodegen2.QrSegment = {}));
953
+ })(qrcodegen || (qrcodegen = {}));
954
+ var qrcodegen_default = qrcodegen;
955
+ /**
956
+ * @license qrcode.react
957
+ * Copyright (c) Paul O'Shannessy
958
+ * SPDX-License-Identifier: ISC
959
+ */
960
+ var ERROR_LEVEL_MAP = {
961
+ L: qrcodegen_default.QrCode.Ecc.LOW,
962
+ M: qrcodegen_default.QrCode.Ecc.MEDIUM,
963
+ Q: qrcodegen_default.QrCode.Ecc.QUARTILE,
964
+ H: qrcodegen_default.QrCode.Ecc.HIGH
965
+ };
966
+ var DEFAULT_SIZE = 128;
967
+ var DEFAULT_LEVEL = "L";
968
+ var DEFAULT_BGCOLOR = "#FFFFFF";
969
+ var DEFAULT_FGCOLOR = "#000000";
970
+ var DEFAULT_INCLUDEMARGIN = false;
971
+ var DEFAULT_MINVERSION = 1;
972
+ var SPEC_MARGIN_SIZE = 4;
973
+ var DEFAULT_MARGIN_SIZE = 0;
974
+ var DEFAULT_IMG_SCALE = .1;
975
+ function generatePath(modules, margin = 0) {
976
+ const ops = [];
977
+ modules.forEach(function(row, y) {
978
+ let start = null;
979
+ row.forEach(function(cell, x) {
980
+ if (!cell && start !== null) {
981
+ ops.push(`M${start + margin} ${y + margin}h${x - start}v1H${start + margin}z`);
982
+ start = null;
983
+ return;
984
+ }
985
+ if (x === row.length - 1) {
986
+ if (!cell) return;
987
+ if (start === null) ops.push(`M${x + margin},${y + margin} h1v1H${x + margin}z`);
988
+ else ops.push(`M${start + margin},${y + margin} h${x + 1 - start}v1H${start + margin}z`);
989
+ return;
990
+ }
991
+ if (cell && start === null) start = x;
992
+ });
993
+ });
994
+ return ops.join("");
995
+ }
996
+ function excavateModules(modules, excavation) {
997
+ return modules.slice().map((row, y) => {
998
+ if (y < excavation.y || y >= excavation.y + excavation.h) return row;
999
+ return row.map((cell, x) => {
1000
+ if (x < excavation.x || x >= excavation.x + excavation.w) return cell;
1001
+ return false;
1002
+ });
1003
+ });
1004
+ }
1005
+ function getImageSettings(cells, size, margin, imageSettings) {
1006
+ if (imageSettings == null) return null;
1007
+ const numCells = cells.length + margin * 2;
1008
+ const defaultSize = Math.floor(size * DEFAULT_IMG_SCALE);
1009
+ const scale = numCells / size;
1010
+ const w = (imageSettings.width || defaultSize) * scale;
1011
+ const h = (imageSettings.height || defaultSize) * scale;
1012
+ const x = imageSettings.x == null ? cells.length / 2 - w / 2 : imageSettings.x * scale;
1013
+ const y = imageSettings.y == null ? cells.length / 2 - h / 2 : imageSettings.y * scale;
1014
+ const opacity = imageSettings.opacity == null ? 1 : imageSettings.opacity;
1015
+ let excavation = null;
1016
+ if (imageSettings.excavate) {
1017
+ let floorX = Math.floor(x);
1018
+ let floorY = Math.floor(y);
1019
+ excavation = {
1020
+ x: floorX,
1021
+ y: floorY,
1022
+ w: Math.ceil(w + x - floorX),
1023
+ h: Math.ceil(h + y - floorY)
1024
+ };
1025
+ }
1026
+ const crossOrigin = imageSettings.crossOrigin;
1027
+ return {
1028
+ x,
1029
+ y,
1030
+ h,
1031
+ w,
1032
+ excavation,
1033
+ opacity,
1034
+ crossOrigin
1035
+ };
1036
+ }
1037
+ function getMarginSize(includeMargin, marginSize) {
1038
+ if (marginSize != null) return Math.max(Math.floor(marginSize), 0);
1039
+ return includeMargin ? SPEC_MARGIN_SIZE : DEFAULT_MARGIN_SIZE;
1040
+ }
1041
+ function useQRCode({ value, level, minVersion, includeMargin, marginSize, imageSettings, size, boostLevel }) {
1042
+ let qrcode = react.default.useMemo(() => {
1043
+ const segments = (Array.isArray(value) ? value : [value]).reduce((accum, v) => {
1044
+ accum.push(...qrcodegen_default.QrSegment.makeSegments(v));
1045
+ return accum;
1046
+ }, []);
1047
+ return qrcodegen_default.QrCode.encodeSegments(segments, ERROR_LEVEL_MAP[level], minVersion, void 0, void 0, boostLevel);
1048
+ }, [
1049
+ value,
1050
+ level,
1051
+ minVersion,
1052
+ boostLevel
1053
+ ]);
1054
+ const { cells, margin, numCells, calculatedImageSettings } = react.default.useMemo(() => {
1055
+ let cells2 = qrcode.getModules();
1056
+ const margin2 = getMarginSize(includeMargin, marginSize);
1057
+ return {
1058
+ cells: cells2,
1059
+ margin: margin2,
1060
+ numCells: cells2.length + margin2 * 2,
1061
+ calculatedImageSettings: getImageSettings(cells2, size, margin2, imageSettings)
1062
+ };
1063
+ }, [
1064
+ qrcode,
1065
+ size,
1066
+ imageSettings,
1067
+ includeMargin,
1068
+ marginSize
1069
+ ]);
1070
+ return {
1071
+ qrcode,
1072
+ margin,
1073
+ cells,
1074
+ numCells,
1075
+ calculatedImageSettings
1076
+ };
1077
+ }
1078
+ var SUPPORTS_PATH2D = function() {
1079
+ try {
1080
+ new Path2D().addPath(new Path2D());
1081
+ } catch (e) {
1082
+ return false;
1083
+ }
1084
+ return true;
1085
+ }();
1086
+ var QRCodeCanvas = react.default.forwardRef(function QRCodeCanvas2(props, forwardedRef) {
1087
+ const _a = props, { value, size = DEFAULT_SIZE, level = DEFAULT_LEVEL, bgColor = DEFAULT_BGCOLOR, fgColor = DEFAULT_FGCOLOR, includeMargin = DEFAULT_INCLUDEMARGIN, minVersion = DEFAULT_MINVERSION, boostLevel, marginSize, imageSettings } = _a;
1088
+ const _b = __objRest(_a, [
1089
+ "value",
1090
+ "size",
1091
+ "level",
1092
+ "bgColor",
1093
+ "fgColor",
1094
+ "includeMargin",
1095
+ "minVersion",
1096
+ "boostLevel",
1097
+ "marginSize",
1098
+ "imageSettings"
1099
+ ]), { style } = _b, otherProps = __objRest(_b, ["style"]);
1100
+ const imgSrc = imageSettings == null ? void 0 : imageSettings.src;
1101
+ const _canvas = react.default.useRef(null);
1102
+ const _image = react.default.useRef(null);
1103
+ const setCanvasRef = react.default.useCallback((node) => {
1104
+ _canvas.current = node;
1105
+ if (typeof forwardedRef === "function") forwardedRef(node);
1106
+ else if (forwardedRef) forwardedRef.current = node;
1107
+ }, [forwardedRef]);
1108
+ const [isImgLoaded, setIsImageLoaded] = react.default.useState(false);
1109
+ const { margin, cells, numCells, calculatedImageSettings } = useQRCode({
1110
+ value,
1111
+ level,
1112
+ minVersion,
1113
+ boostLevel,
1114
+ includeMargin,
1115
+ marginSize,
1116
+ imageSettings,
1117
+ size
1118
+ });
1119
+ react.default.useEffect(() => {
1120
+ if (_canvas.current != null) {
1121
+ const canvas = _canvas.current;
1122
+ const ctx = canvas.getContext("2d");
1123
+ if (!ctx) return;
1124
+ let cellsToDraw = cells;
1125
+ const image = _image.current;
1126
+ const haveImageToRender = calculatedImageSettings != null && image !== null && image.complete && image.naturalHeight !== 0 && image.naturalWidth !== 0;
1127
+ if (haveImageToRender) {
1128
+ if (calculatedImageSettings.excavation != null) cellsToDraw = excavateModules(cells, calculatedImageSettings.excavation);
1129
+ }
1130
+ const pixelRatio = window.devicePixelRatio || 1;
1131
+ canvas.height = canvas.width = size * pixelRatio;
1132
+ const scale = size / numCells * pixelRatio;
1133
+ ctx.scale(scale, scale);
1134
+ ctx.fillStyle = bgColor;
1135
+ ctx.fillRect(0, 0, numCells, numCells);
1136
+ ctx.fillStyle = fgColor;
1137
+ if (SUPPORTS_PATH2D) ctx.fill(new Path2D(generatePath(cellsToDraw, margin)));
1138
+ else cells.forEach(function(row, rdx) {
1139
+ row.forEach(function(cell, cdx) {
1140
+ if (cell) ctx.fillRect(cdx + margin, rdx + margin, 1, 1);
1141
+ });
1142
+ });
1143
+ if (calculatedImageSettings) ctx.globalAlpha = calculatedImageSettings.opacity;
1144
+ if (haveImageToRender) ctx.drawImage(image, calculatedImageSettings.x + margin, calculatedImageSettings.y + margin, calculatedImageSettings.w, calculatedImageSettings.h);
1145
+ }
1146
+ });
1147
+ react.default.useEffect(() => {
1148
+ setIsImageLoaded(false);
1149
+ }, [imgSrc]);
1150
+ const canvasStyle = __spreadValues({
1151
+ height: size,
1152
+ width: size
1153
+ }, style);
1154
+ let img = null;
1155
+ if (imgSrc != null) img = /* @__PURE__ */ react.default.createElement("img", {
1156
+ src: imgSrc,
1157
+ key: imgSrc,
1158
+ style: { display: "none" },
1159
+ onLoad: () => {
1160
+ setIsImageLoaded(true);
1161
+ },
1162
+ ref: _image,
1163
+ crossOrigin: calculatedImageSettings == null ? void 0 : calculatedImageSettings.crossOrigin
1164
+ });
1165
+ return /* @__PURE__ */ react.default.createElement(react.default.Fragment, null, /* @__PURE__ */ react.default.createElement("canvas", __spreadValues({
1166
+ style: canvasStyle,
1167
+ height: size,
1168
+ width: size,
1169
+ ref: setCanvasRef,
1170
+ role: "img"
1171
+ }, otherProps)), img);
1172
+ });
1173
+ QRCodeCanvas.displayName = "QRCodeCanvas";
1174
+ var QRCodeSVG = react.default.forwardRef(function QRCodeSVG2(props, forwardedRef) {
1175
+ const _a = props, { value, size = DEFAULT_SIZE, level = DEFAULT_LEVEL, bgColor = DEFAULT_BGCOLOR, fgColor = DEFAULT_FGCOLOR, includeMargin = DEFAULT_INCLUDEMARGIN, minVersion = DEFAULT_MINVERSION, boostLevel, title, marginSize, imageSettings } = _a, otherProps = __objRest(_a, [
1176
+ "value",
1177
+ "size",
1178
+ "level",
1179
+ "bgColor",
1180
+ "fgColor",
1181
+ "includeMargin",
1182
+ "minVersion",
1183
+ "boostLevel",
1184
+ "title",
1185
+ "marginSize",
1186
+ "imageSettings"
1187
+ ]);
1188
+ const { margin, cells, numCells, calculatedImageSettings } = useQRCode({
1189
+ value,
1190
+ level,
1191
+ minVersion,
1192
+ boostLevel,
1193
+ includeMargin,
1194
+ marginSize,
1195
+ imageSettings,
1196
+ size
1197
+ });
1198
+ let cellsToDraw = cells;
1199
+ let image = null;
1200
+ if (imageSettings != null && calculatedImageSettings != null) {
1201
+ if (calculatedImageSettings.excavation != null) cellsToDraw = excavateModules(cells, calculatedImageSettings.excavation);
1202
+ image = /* @__PURE__ */ react.default.createElement("image", {
1203
+ href: imageSettings.src,
1204
+ height: calculatedImageSettings.h,
1205
+ width: calculatedImageSettings.w,
1206
+ x: calculatedImageSettings.x + margin,
1207
+ y: calculatedImageSettings.y + margin,
1208
+ preserveAspectRatio: "none",
1209
+ opacity: calculatedImageSettings.opacity,
1210
+ crossOrigin: calculatedImageSettings.crossOrigin
1211
+ });
1212
+ }
1213
+ const fgPath = generatePath(cellsToDraw, margin);
1214
+ return /* @__PURE__ */ react.default.createElement("svg", __spreadValues({
1215
+ height: size,
1216
+ width: size,
1217
+ viewBox: `0 0 ${numCells} ${numCells}`,
1218
+ ref: forwardedRef,
1219
+ role: "img"
1220
+ }, otherProps), !!title && /* @__PURE__ */ react.default.createElement("title", null, title), /* @__PURE__ */ react.default.createElement("path", {
1221
+ fill: bgColor,
1222
+ d: `M0,0 h${numCells}v${numCells}H0z`,
1223
+ shapeRendering: "crispEdges"
1224
+ }), /* @__PURE__ */ react.default.createElement("path", {
1225
+ fill: fgColor,
1226
+ d: fgPath,
1227
+ shapeRendering: "crispEdges"
1228
+ }), image);
1229
+ });
1230
+ QRCodeSVG.displayName = "QRCodeSVG";
1231
+ //#endregion
1232
+ //#region src/client/pair-api.ts
1233
+ /**
1234
+ * Mint a fresh pairing token (one active token at a time — this invalidates
1235
+ * any previous link).
1236
+ * @param workspaceId - optional current workspace to deep-link the phone into.
1237
+ * @param address - optional LAN IP literal the QR must be built from (the
1238
+ * default is the first interface); unknown literals refuse with
1239
+ * 'unknown-address'.
1240
+ * @returns the issued link, the lan-required refusal (server never bound
1241
+ * 0.0.0.0), or the forbidden refusal (the loopback-only fence rejected this
1242
+ * origin — the panel is a desktop control endpoint).
1243
+ */
1244
+ async function issuePair(workspaceId, address) {
1245
+ const response = await fetch("/api/pair/issue", {
1246
+ method: "POST",
1247
+ headers: { "content-type": "application/json" },
1248
+ body: JSON.stringify({
1249
+ ...workspaceId !== void 0 ? { workspaceId } : {},
1250
+ ...address !== void 0 ? { address } : {}
1251
+ })
1252
+ });
1253
+ if (!response.ok) {
1254
+ if (response.status === 409) return {
1255
+ ok: false,
1256
+ code: "lan-required"
1257
+ };
1258
+ if (response.status === 403) return {
1259
+ ok: false,
1260
+ code: "forbidden"
1261
+ };
1262
+ if (response.status === 400) return {
1263
+ ok: false,
1264
+ code: "unknown-address"
1265
+ };
1266
+ throw new Error(`remote-web-ui: issue failed with ${String(response.status)}`);
1267
+ }
1268
+ return await response.json();
1269
+ }
1270
+ /**
1271
+ * Accept a pairing token (the phone's first open of the QR link). Success
1272
+ * sets the device cookie; the page then reloads to boot with it.
1273
+ * @param token - the token from the URL.
1274
+ * @returns the wire result.
1275
+ */
1276
+ async function acceptPair(token) {
1277
+ const response = await fetch("/api/pair/accept", {
1278
+ method: "POST",
1279
+ headers: { "content-type": "application/json" },
1280
+ body: JSON.stringify({ token })
1281
+ });
1282
+ if (response.ok) return { ok: true };
1283
+ if (response.status === 404) return {
1284
+ ok: false,
1285
+ code: "invalid"
1286
+ };
1287
+ if (response.status === 409) return {
1288
+ ok: false,
1289
+ code: "used"
1290
+ };
1291
+ return {
1292
+ ok: false,
1293
+ code: "forbidden"
1294
+ };
1295
+ }
1296
+ /** Revoke mobile access (paired devices + the current token). */
1297
+ async function stopPair() {
1298
+ const response = await fetch("/api/pair/stop", { method: "POST" });
1299
+ if (!response.ok) throw new Error(`remote-web-ui: stop failed with ${String(response.status)}`);
1300
+ }
1301
+ /** Presence heartbeat from a paired phone (unpaired heartbeats 401 harmlessly). */
1302
+ async function sendHeartbeat() {
1303
+ await fetch("/api/pair/heartbeat", { method: "POST" });
1304
+ }
1305
+ /** Whether the current page URL carries a pairing token / workspace target. */
1306
+ function readPairParams(search) {
1307
+ const params = new URLSearchParams(search);
1308
+ const pair = params.get("pair");
1309
+ const workspace = params.get("workspace");
1310
+ return {
1311
+ ...pair !== null && pair !== "" ? { pair } : {},
1312
+ ...workspace !== null && workspace !== "" ? { workspace } : {}
1313
+ };
1314
+ }
1315
+ function formatClock(epochMs) {
1316
+ const date = new Date(epochMs);
1317
+ return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
1318
+ }
1319
+ /**
1320
+ * Copy text to the clipboard with a fallback for insecure contexts
1321
+ * (plain-HTTP LAN origins lack navigator.clipboard).
1322
+ * @param text - the text to copy.
1323
+ * @returns whether the copy succeeded.
1324
+ */
1325
+ async function copyText(text) {
1326
+ if (typeof navigator !== "undefined" && navigator.clipboard !== void 0) try {
1327
+ await navigator.clipboard.writeText(text);
1328
+ return true;
1329
+ } catch {}
1330
+ try {
1331
+ const area = document.createElement("textarea");
1332
+ area.value = text;
1333
+ area.style.position = "fixed";
1334
+ area.style.opacity = "0";
1335
+ document.body.appendChild(area);
1336
+ area.select();
1337
+ const ok = document.execCommand("copy");
1338
+ area.remove();
1339
+ return ok;
1340
+ } catch {
1341
+ return false;
1342
+ }
1343
+ }
1344
+ //#endregion
1345
+ //#region \0dsh-css:/Users/zcl/code/dsh-web-ui/packages/dsh-remote-web-ui/src/client/remote.module.css.mjs
1346
+ const css$1 = "._1FzWja_overlay{z-index:1000;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}._1FzWja_mask{background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur);position:absolute;inset:0}._1FzWja_trigger{width:36px;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex}._1FzWja_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}._1FzWja_trigger:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}._1FzWja_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}._1FzWja_trigger:disabled{opacity:.5;cursor:default}._1FzWja_panel{z-index:1;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);width:560px;max-width:calc(100vw - 48px);max-height:calc(100vh - 48px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:24px;flex-direction:column;gap:14px;padding:24px;font-size:14px;line-height:22px;display:flex;position:relative;overflow:auto}._1FzWja_header{align-items:flex-start;gap:12px;display:flex}._1FzWja_heading{flex:1;min-width:0}._1FzWja_title{margin:0;font-size:18px;font-weight:600;line-height:26px}._1FzWja_subtitle{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}._1FzWja_close{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex}._1FzWja_close:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._1FzWja_close:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}._1FzWja_close:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}._1FzWja_close:disabled{opacity:.5;cursor:default}._1FzWja_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;align-items:center;gap:12px;padding:16px;display:flex}._1FzWja_cardHeader{justify-content:space-between;align-items:center;gap:12px;width:100%;display:flex}._1FzWja_cardTitle{font-weight:500}._1FzWja_badge{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;min-width:0;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex}._1FzWja_badge:before{content:\"\";background:currentColor;border-radius:50%;width:8px;height:8px}._1FzWja_badge-waiting{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}._1FzWja_badge-connected{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}._1FzWja_badge-disconnected{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-interactive-bg-hover)}._1FzWja_badge-stopped{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}._1FzWja_badgePublic{color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}._1FzWja_badges{flex:none;align-items:center;gap:6px;display:inline-flex}._1FzWja_qrWrap{background:var(--dsw-alias-bg-base);border-radius:12px;justify-content:center;align-items:center;padding:12px;display:flex}._1FzWja_qr{display:block}._1FzWja_expired{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}._1FzWja_expiry{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}._1FzWja_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}._1FzWja_link{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-caption);font-family:var(--dsw-font-mono,ui-monospace, monospace);margin:0;font-size:12px;overflow:hidden}._1FzWja_stoppedHint{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}._1FzWja_tunnelNote{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}._1FzWja_tunnelFailed{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}._1FzWja_actions{gap:8px;display:flex}._1FzWja_action{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;border-radius:10px;justify-content:center;align-items:center;gap:6px;padding:0 14px;font-size:13px;transition:background-color .12s,border-color .12s,box-shadow .12s;display:inline-flex}._1FzWja_action:hover:not(:disabled){background:var(--dsw-alias-button-floating-hover)}._1FzWja_action:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}._1FzWja_action:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}._1FzWja_action:disabled{opacity:.5;cursor:default}._1FzWja_banner{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;padding:16px}._1FzWja_bannerTitle{color:var(--dsw-alias-state-warn-primary);margin:0;font-weight:500}._1FzWja_bannerHint{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}._1FzWja_notice{z-index:2000;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);max-width:min(420px,100vw - 32px);box-shadow:var(--dsw-shadow-lv3);border-radius:12px;padding:12px 16px;position:fixed;top:16px;left:50%;transform:translate(-50%)}._1FzWja_noticeTitle{color:var(--dsw-alias-state-error-primary);margin:0;font-weight:500}._1FzWja_noticeDetail{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}._1FzWja_addresses{border:none;margin:12px 0 0;padding:0}._1FzWja_addresses legend{color:var(--dsw-alias-label-secondary);padding:0;font-size:13px}._1FzWja_address{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px;align-items:center;gap:8px;margin-top:6px;padding:4px 6px;font-size:13px;transition:background-color .12s;display:flex}._1FzWja_address:hover{background:var(--dsw-alias-interactive-bg-hover)}._1FzWja_address input:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);border-radius:50%;outline:none}._1FzWja_addressValue{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-size:12px;overflow:hidden}._1FzWja_addressHint{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;font-size:12px}@media (prefers-reduced-motion:reduce){._1FzWja_trigger,._1FzWja_close,._1FzWja_action,._1FzWja_address{transition:none}}";
1347
+ const tagId$1 = "@linxin666/dsh-remote-web-ui/remote.module.css";
1348
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
1349
+ const tag = document.createElement("style");
1350
+ tag.dataset.plugin = "@linxin666/dsh-remote-web-ui";
1351
+ tag.dataset.pluginCss = tagId$1;
1352
+ tag.textContent = css$1;
1353
+ document.head.appendChild(tag);
1354
+ }
1355
+ var remote_module_css_default = {
1356
+ "action": "_1FzWja_action",
1357
+ "actions": "_1FzWja_actions",
1358
+ "address": "_1FzWja_address",
1359
+ "addressHint": "_1FzWja_addressHint",
1360
+ "addressValue": "_1FzWja_addressValue",
1361
+ "addresses": "_1FzWja_addresses",
1362
+ "badge": "_1FzWja_badge",
1363
+ "badge-connected": "_1FzWja_badge-connected",
1364
+ "badge-disconnected": "_1FzWja_badge-disconnected",
1365
+ "badge-stopped": "_1FzWja_badge-stopped",
1366
+ "badge-waiting": "_1FzWja_badge-waiting",
1367
+ "badgePublic": "_1FzWja_badgePublic",
1368
+ "badges": "_1FzWja_badges",
1369
+ "banner": "_1FzWja_banner",
1370
+ "bannerHint": "_1FzWja_bannerHint",
1371
+ "bannerTitle": "_1FzWja_bannerTitle",
1372
+ "card": "_1FzWja_card",
1373
+ "cardHeader": "_1FzWja_cardHeader",
1374
+ "cardTitle": "_1FzWja_cardTitle",
1375
+ "close": "_1FzWja_close",
1376
+ "expired": "_1FzWja_expired",
1377
+ "expiry": "_1FzWja_expiry",
1378
+ "header": "_1FzWja_header",
1379
+ "heading": "_1FzWja_heading",
1380
+ "hint": "_1FzWja_hint",
1381
+ "link": "_1FzWja_link",
1382
+ "mask": "_1FzWja_mask",
1383
+ "notice": "_1FzWja_notice",
1384
+ "noticeDetail": "_1FzWja_noticeDetail",
1385
+ "noticeTitle": "_1FzWja_noticeTitle",
1386
+ "overlay": "_1FzWja_overlay",
1387
+ "panel": "_1FzWja_panel",
1388
+ "qr": "_1FzWja_qr",
1389
+ "qrWrap": "_1FzWja_qrWrap",
1390
+ "stoppedHint": "_1FzWja_stoppedHint",
1391
+ "subtitle": "_1FzWja_subtitle",
1392
+ "title": "_1FzWja_title",
1393
+ "trigger": "_1FzWja_trigger",
1394
+ "tunnelFailed": "_1FzWja_tunnelFailed",
1395
+ "tunnelNote": "_1FzWja_tunnelNote"
1396
+ };
1397
+ //#endregion
1398
+ //#region src/client/RemotePanel.tsx
1399
+ /**
1400
+ * The mobile remote-control panel body: status card (state text + badge),
1401
+ * the QR code, the open-on-phone hint with the link text, and the three
1402
+ * actions (stop / refresh / copy). Pure presentation — all state and
1403
+ * actions arrive through props from the entry's behavior component.
1404
+ */
1405
+ /** Badge text + tone per phase (ready states only). */
1406
+ function statusOf(t, state) {
1407
+ switch (state.phase) {
1408
+ case "connected": return {
1409
+ text: t("status.connected", { n: state.onlineCount }),
1410
+ tone: "connected"
1411
+ };
1412
+ case "disconnected": return {
1413
+ text: t("status.disconnected"),
1414
+ tone: "disconnected"
1415
+ };
1416
+ case "stopped": return {
1417
+ text: t("status.stopped"),
1418
+ tone: "stopped"
1419
+ };
1420
+ case "lan-required": return {
1421
+ text: t("status.lanRequired"),
1422
+ tone: "stopped"
1423
+ };
1424
+ case "waiting": return {
1425
+ text: t("status.waiting"),
1426
+ tone: "waiting"
1427
+ };
1428
+ }
1429
+ }
1430
+ /**
1431
+ * Render the pairing panel.
1432
+ * @param props - copy, state, and actions.
1433
+ * @returns the panel element tree.
1434
+ */
1435
+ function RemotePanel({ t, state, copied, onClose, onStop, onRefresh, onCopy, onPickAddress, onPickPublic }) {
1436
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1437
+ className: remote_module_css_default.panel,
1438
+ role: "dialog",
1439
+ "aria-modal": "true",
1440
+ "aria-label": t("title"),
1441
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1442
+ className: remote_module_css_default.header,
1443
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1444
+ className: remote_module_css_default.heading,
1445
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1446
+ className: remote_module_css_default.title,
1447
+ children: t("title")
1448
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1449
+ className: remote_module_css_default.subtitle,
1450
+ children: t("subtitle")
1451
+ })]
1452
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1453
+ type: "button",
1454
+ className: remote_module_css_default.close,
1455
+ "aria-label": t("close.label"),
1456
+ onClick: onClose,
1457
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 14 })
1458
+ })]
1459
+ }), state.kind === "lan-required" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1460
+ className: remote_module_css_default.banner,
1461
+ role: "alert",
1462
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1463
+ className: remote_module_css_default.bannerTitle,
1464
+ children: t("status.lanRequired")
1465
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1466
+ className: remote_module_css_default.bannerHint,
1467
+ children: t("status.lanRequiredHint")
1468
+ })]
1469
+ }) : state.kind === "loopback-required" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1470
+ className: remote_module_css_default.banner,
1471
+ role: "alert",
1472
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1473
+ className: remote_module_css_default.bannerTitle,
1474
+ children: t("status.loopbackRequired")
1475
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1476
+ className: remote_module_css_default.bannerHint,
1477
+ children: t("status.loopbackRequiredHint")
1478
+ })]
1479
+ }) : state.kind === "unreachable" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1480
+ className: remote_module_css_default.banner,
1481
+ role: "alert",
1482
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1483
+ className: remote_module_css_default.bannerTitle,
1484
+ children: t("status.unreachable")
1485
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1486
+ className: remote_module_css_default.bannerHint,
1487
+ children: t("status.unreachableHint")
1488
+ })]
1489
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1490
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1491
+ className: remote_module_css_default.card,
1492
+ children: [
1493
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1494
+ className: remote_module_css_default.cardHeader,
1495
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1496
+ className: remote_module_css_default.cardTitle,
1497
+ children: t("card.title")
1498
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1499
+ className: remote_module_css_default.badges,
1500
+ children: [state.public && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1501
+ className: clsx(remote_module_css_default.badge, remote_module_css_default.badgePublic),
1502
+ children: t("public.badge")
1503
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1504
+ className: clsx(remote_module_css_default.badge, remote_module_css_default[`badge-${statusOf(t, state).tone}`]),
1505
+ children: statusOf(t, state).text
1506
+ })]
1507
+ })]
1508
+ }),
1509
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1510
+ className: remote_module_css_default.qrWrap,
1511
+ "data-testid": "remote-qr",
1512
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QRCodeSVG, {
1513
+ value: state.url,
1514
+ size: 184,
1515
+ level: "M",
1516
+ marginSize: 1,
1517
+ className: remote_module_css_default.qr
1518
+ })
1519
+ }),
1520
+ state.expired ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1521
+ className: remote_module_css_default.expired,
1522
+ children: t("pair.expired")
1523
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1524
+ className: remote_module_css_default.expiry,
1525
+ children: t("pair.expires", { time: formatClock(state.expiresAt) })
1526
+ })
1527
+ ]
1528
+ }),
1529
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1530
+ className: remote_module_css_default.hint,
1531
+ children: state.public ? t("pair.publicHint") : t("pair.hint")
1532
+ }),
1533
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1534
+ className: remote_module_css_default.link,
1535
+ title: state.url,
1536
+ children: state.url
1537
+ }),
1538
+ state.phase === "stopped" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1539
+ className: remote_module_css_default.stoppedHint,
1540
+ children: t("stopped.hint")
1541
+ }),
1542
+ state.tunnel !== void 0 && state.tunnel.state !== "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1543
+ className: state.tunnel.state === "failed" ? remote_module_css_default.tunnelFailed : remote_module_css_default.tunnelNote,
1544
+ role: "status",
1545
+ children: state.tunnel.state === "failed" ? t("tunnel.failed", { error: state.tunnel.error ?? t("tunnel.unknownError") }) : t("tunnel.starting")
1546
+ }),
1547
+ (state.publicBaseUrl !== void 0 || state.lanAddresses.length > 1) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1548
+ className: remote_module_css_default.addresses,
1549
+ children: [
1550
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: t("address.label") }),
1551
+ state.publicBaseUrl !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1552
+ className: remote_module_css_default.address,
1553
+ children: [
1554
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1555
+ type: "radio",
1556
+ name: "lan-address",
1557
+ "aria-label": t("address.public"),
1558
+ checked: state.public,
1559
+ onChange: onPickPublic
1560
+ }),
1561
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("address.public") }),
1562
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
1563
+ className: remote_module_css_default.addressValue,
1564
+ children: state.publicBaseUrl
1565
+ })
1566
+ ]
1567
+ }, "public"),
1568
+ state.lanAddresses.map((address) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1569
+ className: remote_module_css_default.address,
1570
+ children: [
1571
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1572
+ type: "radio",
1573
+ name: "lan-address",
1574
+ "aria-label": address,
1575
+ checked: !state.public && address === state.address,
1576
+ onChange: () => onPickAddress(address)
1577
+ }),
1578
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("address.lan") }),
1579
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
1580
+ className: remote_module_css_default.addressValue,
1581
+ children: address
1582
+ })
1583
+ ]
1584
+ }, address)),
1585
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1586
+ className: remote_module_css_default.addressHint,
1587
+ children: t("address.hint")
1588
+ })
1589
+ ]
1590
+ }),
1591
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1592
+ className: remote_module_css_default.actions,
1593
+ children: [
1594
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1595
+ type: "button",
1596
+ className: remote_module_css_default.action,
1597
+ onClick: onStop,
1598
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconStopFill16, { size: 14 }), t("action.stop")]
1599
+ }),
1600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1601
+ type: "button",
1602
+ className: remote_module_css_default.action,
1603
+ onClick: onRefresh,
1604
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, { size: 14 }), t("action.refresh")]
1605
+ }),
1606
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1607
+ type: "button",
1608
+ className: remote_module_css_default.action,
1609
+ onClick: onCopy,
1610
+ children: [copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLinkOutline16, { size: 14 }), copied ? t("action.copied") : t("action.copy")]
1611
+ })
1612
+ ]
1613
+ })
1614
+ ] })]
1615
+ });
1616
+ }
1617
+ //#endregion
1618
+ //#region src/client/PhoneIcon.tsx
1619
+ /**
1620
+ * Render the phone icon.
1621
+ * @param props - size and optional class.
1622
+ * @returns the svg element.
1623
+ */
1624
+ function PhoneIcon({ size = 16, className }) {
1625
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1626
+ width: size,
1627
+ height: size,
1628
+ className,
1629
+ viewBox: "0 0 16 16",
1630
+ fill: "none",
1631
+ "aria-hidden": "true",
1632
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1633
+ d: "M4.2 1.8h2.4l.9 2.4-1.5 1.1a7.4 7.4 0 0 0 4.7 4.7l1.1-1.5 2.4.9v2.4a.9.9 0 0 1-1 .9A11.4 11.4 0 0 1 3.3 2.8a.9.9 0 0 1 .9-1Z",
1634
+ stroke: "currentColor",
1635
+ strokeWidth: "1.3",
1636
+ strokeLinejoin: "round"
1637
+ })
1638
+ });
1639
+ }
1640
+ //#endregion
1641
+ //#region src/client/RemoteEntry.tsx
1642
+ /**
1643
+ * The sidebar remote-control seat: the phone-icon trigger beside the
1644
+ * settings button plus the pairing panel modal. Owns the panel behavior —
1645
+ * token minting on open, the status SSE subscription, stop/refresh/copy —
1646
+ * and renders the pure {@link RemotePanel} body. Component-local state per
1647
+ * the client stack rules: nothing here survives remounts or crosses
1648
+ * entries.
1649
+ */
1650
+ /** Apply one status frame onto the current ready state. */
1651
+ function mergeFrame(state, frame) {
1652
+ if (state.kind !== "ready") return state;
1653
+ return {
1654
+ ...state,
1655
+ phase: frame.phase,
1656
+ deviceCount: frame.deviceCount,
1657
+ onlineCount: frame.onlineCount,
1658
+ ...frame.tunnel !== void 0 ? { tunnel: frame.tunnel } : {}
1659
+ };
1660
+ }
1661
+ /**
1662
+ * Render the remote-control trigger and panel.
1663
+ * @param props - composed slot props (contract in this package).
1664
+ * @returns the entry element tree.
1665
+ */
1666
+ function RemoteEntry({ wide, useWorkspaces, t }) {
1667
+ const [open, setOpen] = (0, react.useState)(false);
1668
+ const [state, setState] = (0, react.useState)({ kind: "lan-required" });
1669
+ const [copied, setCopied] = (0, react.useState)(false);
1670
+ const eventSource = (0, react.useRef)(void 0);
1671
+ const workspaceId = useWorkspaces((s) => s.recentWorkspaceId);
1672
+ const closeEventSource = (0, react.useCallback)(() => {
1673
+ eventSource.current?.close();
1674
+ eventSource.current = void 0;
1675
+ }, []);
1676
+ const mint = (0, react.useCallback)(async (address) => {
1677
+ let result;
1678
+ try {
1679
+ result = await issuePair(workspaceId, address);
1680
+ } catch {
1681
+ return { kind: "unreachable" };
1682
+ }
1683
+ if (!result.ok) {
1684
+ if (result.code === "forbidden") return { kind: "loopback-required" };
1685
+ if (result.code === "unknown-address") return { kind: "unreachable" };
1686
+ return { kind: "lan-required" };
1687
+ }
1688
+ const publicBaseUrl = result.publicBaseUrl;
1689
+ return {
1690
+ kind: "ready",
1691
+ url: result.url,
1692
+ expiresAt: result.expiresAt,
1693
+ expired: Date.now() > result.expiresAt,
1694
+ phase: "waiting",
1695
+ deviceCount: 0,
1696
+ onlineCount: 0,
1697
+ public: publicBaseUrl !== void 0 && result.url.startsWith(publicBaseUrl),
1698
+ ...publicBaseUrl !== void 0 ? { publicBaseUrl } : {},
1699
+ address: address ?? result.lanAddresses[0] ?? "",
1700
+ lanAddresses: result.lanAddresses
1701
+ };
1702
+ }, [workspaceId]);
1703
+ const openPanel = (0, react.useCallback)(async () => {
1704
+ setOpen(true);
1705
+ const next = await mint();
1706
+ setState(next);
1707
+ if (next.kind !== "ready") return;
1708
+ const source = new EventSource("/api/pair/events");
1709
+ eventSource.current = source;
1710
+ source.onmessage = (event) => {
1711
+ try {
1712
+ const frame = JSON.parse(event.data);
1713
+ if (frame.type !== "state") return;
1714
+ setState((previous) => mergeFrame(previous, frame));
1715
+ } catch {}
1716
+ };
1717
+ }, [mint]);
1718
+ const closePanel = (0, react.useCallback)(() => {
1719
+ closeEventSource();
1720
+ setOpen(false);
1721
+ }, [closeEventSource]);
1722
+ (0, react.useEffect)(() => {
1723
+ if (state.kind !== "ready") return;
1724
+ if (state.expired) return;
1725
+ const delay = state.expiresAt - Date.now();
1726
+ if (delay <= 0) {
1727
+ setState((previous) => previous.kind === "ready" ? {
1728
+ ...previous,
1729
+ expired: true
1730
+ } : previous);
1731
+ return;
1732
+ }
1733
+ const timer = window.setTimeout(() => {
1734
+ setState((previous) => previous.kind === "ready" ? {
1735
+ ...previous,
1736
+ expired: true
1737
+ } : previous);
1738
+ }, delay);
1739
+ return () => {
1740
+ window.clearTimeout(timer);
1741
+ };
1742
+ }, [state]);
1743
+ (0, react.useEffect)(() => closeEventSource, [closeEventSource]);
1744
+ const handleStop = (0, react.useCallback)(() => {
1745
+ stopPair().catch(() => {});
1746
+ setState((previous) => previous.kind === "ready" ? {
1747
+ ...previous,
1748
+ phase: "stopped"
1749
+ } : previous);
1750
+ }, []);
1751
+ const handleRefresh = (0, react.useCallback)(() => {
1752
+ mint().then(setState);
1753
+ }, [mint]);
1754
+ /** Re-mint against another LAN literal (multi-homed machines). */
1755
+ const handlePickAddress = (0, react.useCallback)((address) => {
1756
+ mint(address).then(setState);
1757
+ }, [mint]);
1758
+ /** Re-mint against the configured public (tunneled) base. */
1759
+ const handlePickPublic = (0, react.useCallback)(() => {
1760
+ mint().then(setState);
1761
+ }, [mint]);
1762
+ const handleCopy = (0, react.useCallback)(() => {
1763
+ if (state.kind !== "ready") return;
1764
+ copyText(state.url).then((ok) => {
1765
+ if (!ok) return;
1766
+ setCopied(true);
1767
+ window.setTimeout(() => {
1768
+ setCopied(false);
1769
+ }, 1500);
1770
+ });
1771
+ }, [state]);
1772
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipAnchor, {
1773
+ wide,
1774
+ label: t("entry.label"),
1775
+ onClick: openPanel
1776
+ }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1777
+ className: remote_module_css_default.overlay,
1778
+ role: "presentation",
1779
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1780
+ className: remote_module_css_default.mask,
1781
+ "aria-hidden": "true",
1782
+ onClick: closePanel
1783
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RemotePanel, {
1784
+ t,
1785
+ state,
1786
+ copied,
1787
+ onClose: closePanel,
1788
+ onStop: handleStop,
1789
+ onRefresh: handleRefresh,
1790
+ onCopy: handleCopy,
1791
+ onPickAddress: handlePickAddress,
1792
+ onPickPublic: handlePickPublic
1793
+ })]
1794
+ }), document.body)] });
1795
+ }
1796
+ /** The trigger: an icon button matching the settings rail/row geometry. */
1797
+ function TooltipAnchor({ wide, label, onClick }) {
1798
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1799
+ type: "button",
1800
+ className: remote_module_css_default.trigger,
1801
+ "data-wide": wide ? void 0 : "rail",
1802
+ "aria-label": label,
1803
+ title: label,
1804
+ onClick,
1805
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhoneIcon, { size: wide ? 16 : 18 })
1806
+ });
1807
+ }
1808
+ //#endregion
1809
+ //#region src/client/PairFailedNotice.tsx
1810
+ /**
1811
+ * One-time failed-pairing notice: a fixed toast rendered on the phone after
1812
+ * a QR accept failed (invalid/used token or a network error). Mounted by
1813
+ * the client apply with a plain React root — no slot machinery for a
1814
+ * transient diagnostic.
1815
+ */
1816
+ /**
1817
+ * Render the failed-pair toast (auto-dismisses).
1818
+ * @param props - localized copy.
1819
+ * @returns the toast element.
1820
+ */
1821
+ function PairFailedNotice({ t }) {
1822
+ const [visible, setVisible] = (0, react.useState)(true);
1823
+ (0, react.useEffect)(() => {
1824
+ const timer = window.setTimeout(() => {
1825
+ setVisible(false);
1826
+ }, 8e3);
1827
+ return () => {
1828
+ window.clearTimeout(timer);
1829
+ };
1830
+ }, []);
1831
+ if (!visible) return null;
1832
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1833
+ className: remote_module_css_default.notice,
1834
+ role: "alert",
1835
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1836
+ className: remote_module_css_default.noticeTitle,
1837
+ children: t("pair.failed.title")
1838
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1839
+ className: remote_module_css_default.noticeDetail,
1840
+ children: t("pair.failed.detail")
1841
+ })]
1842
+ });
1843
+ }
1844
+ //#endregion
1845
+ //#region \0dsh-css:/Users/zcl/code/dsh-web-ui/packages/dsh-remote-web-ui/src/client/settings-card.module.css.mjs
1846
+ const css = ".jEUd8W_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;list-style:none;transition:border-color .16s,background .16s;overflow:hidden}.jEUd8W_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.jEUd8W_header{cursor:pointer;text-align:left;width:100%;font:inherit;background:0 0;border:0;border-radius:8px;align-items:center;gap:8px;padding:10px 14px;transition:background-color .12s,box-shadow .12s;display:flex}.jEUd8W_header:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.jEUd8W_header:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.jEUd8W_header:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.jEUd8W_header:disabled{opacity:.5;cursor:default}.jEUd8W_headText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.jEUd8W_name{color:var(--dsw-alias-label-primary);font-weight:600}.jEUd8W_description{color:var(--dsw-alias-label-tertiary);font-size:12px}.jEUd8W_pending{color:var(--dsw-alias-state-warn-primary);font-size:12px}.jEUd8W_chevron{color:var(--dsw-alias-label-tertiary);transition:transform .12s}.jEUd8W_chevronOpen{transform:rotate(180deg)}.jEUd8W_body{flex-direction:column;gap:14px;padding:0 14px 14px;display:flex}.jEUd8W_readOnly{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px}.jEUd8W_footer{justify-content:flex-end;align-items:center;gap:8px;display:flex}.jEUd8W_failed{color:var(--dsw-alias-state-error-primary);margin:0 auto 0 0;font-size:12px}.jEUd8W_discard,.jEUd8W_save{font:inherit;cursor:pointer;border-radius:6px;padding:5px 12px;font-size:13px;transition:background-color .12s,border-color .12s,color .12s,box-shadow .12s}.jEUd8W_discard{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.jEUd8W_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.jEUd8W_discard:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.jEUd8W_discard:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.jEUd8W_save{border:1px solid var(--dsw-alias-button-info-fill);background:var(--dsw-alias-button-info-fill);color:var(--dsw-alias-label-primary-foreground)}.jEUd8W_save:hover:not(:disabled){border-color:var(--dsw-alias-button-info-hover);background:var(--dsw-alias-button-info-hover)}.jEUd8W_save:active:not(:disabled){border-color:var(--dsw-alias-button-info-hover);background:var(--dsw-alias-button-info-hover);filter:brightness(.96)}.jEUd8W_save:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.jEUd8W_discard:disabled,.jEUd8W_save:disabled{opacity:.5;cursor:default}.jEUd8W_field{flex-direction:column;gap:4px;display:flex}.jEUd8W_head{align-items:center;gap:8px;display:flex}.jEUd8W_label{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}.jEUd8W_badges{align-items:center;gap:6px;display:flex}.jEUd8W_badge{background:var(--dsw-alias-interactive-bg-hover-accent);min-width:0;color:var(--dsw-alias-state-business-primary);white-space:nowrap;border-radius:999px;flex:none;padding:1px 6px;font-size:11px}.jEUd8W_reset{color:var(--dsw-alias-state-business-primary);cursor:pointer;background:0 0;border:0;border-radius:4px;flex:none;padding:1px 2px;font-size:11px;transition:background-color .12s,color .12s,box-shadow .12s}.jEUd8W_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.jEUd8W_reset:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.jEUd8W_reset:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.jEUd8W_reset:disabled{opacity:.5;cursor:default}.jEUd8W_input,.jEUd8W_select{border:1px solid var(--dsw-alias-border-l2);font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border-radius:6px;padding:6px 8px;font-size:13px;transition:border-color .12s,box-shadow .12s}.jEUd8W_input:focus-visible,.jEUd8W_select:focus-visible{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.jEUd8W_inputInvalid{border:1px solid var(--dsw-alias-state-error-primary);font:inherit;color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 8px;font-size:13px;transition:border-color .12s,box-shadow .12s}.jEUd8W_inputInvalid:focus-visible{border-color:var(--dsw-alias-state-error-primary);box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-state-error-primary);outline:none}.jEUd8W_input:disabled,.jEUd8W_select:disabled{opacity:.6}.jEUd8W_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}.jEUd8W_invalid{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}@media (prefers-reduced-motion:reduce){.jEUd8W_card,.jEUd8W_header,.jEUd8W_chevron,.jEUd8W_discard,.jEUd8W_save,.jEUd8W_reset,.jEUd8W_input,.jEUd8W_select,.jEUd8W_inputInvalid{transition:none}}";
1847
+ const tagId = "@linxin666/dsh-remote-web-ui/settings-card.module.css";
1848
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1849
+ const tag = document.createElement("style");
1850
+ tag.dataset.plugin = "@linxin666/dsh-remote-web-ui";
1851
+ tag.dataset.pluginCss = tagId;
1852
+ tag.textContent = css;
1853
+ document.head.appendChild(tag);
1854
+ }
1855
+ var settings_card_module_css_default = {
1856
+ "badge": "jEUd8W_badge",
1857
+ "badges": "jEUd8W_badges",
1858
+ "body": "jEUd8W_body",
1859
+ "card": "jEUd8W_card",
1860
+ "cardOpen": "jEUd8W_cardOpen",
1861
+ "chevron": "jEUd8W_chevron",
1862
+ "chevronOpen": "jEUd8W_chevronOpen",
1863
+ "description": "jEUd8W_description",
1864
+ "discard": "jEUd8W_discard",
1865
+ "failed": "jEUd8W_failed",
1866
+ "field": "jEUd8W_field",
1867
+ "footer": "jEUd8W_footer",
1868
+ "head": "jEUd8W_head",
1869
+ "headText": "jEUd8W_headText",
1870
+ "header": "jEUd8W_header",
1871
+ "hint": "jEUd8W_hint",
1872
+ "input": "jEUd8W_input",
1873
+ "inputInvalid": "jEUd8W_inputInvalid",
1874
+ "invalid": "jEUd8W_invalid",
1875
+ "label": "jEUd8W_label",
1876
+ "name": "jEUd8W_name",
1877
+ "pending": "jEUd8W_pending",
1878
+ "readOnly": "jEUd8W_readOnly",
1879
+ "reset": "jEUd8W_reset",
1880
+ "save": "jEUd8W_save",
1881
+ "select": "jEUd8W_select"
1882
+ };
1883
+ //#endregion
1884
+ //#region src/client/PluginSettingsCard.tsx
1885
+ /**
1886
+ * Shared chrome for the plugin settings card: a disclosure header naming the
1887
+ * plugin and what its settings govern, the controls inside, and the save that
1888
+ * writes them. Renders nothing while the namespace is unavailable — a
1889
+ * deployment that does not compose the owning plugin should show no trace of
1890
+ * it. Mirrors the official ui-plugin-config PluginCard in a self-contained
1891
+ * slice (this package must not depend on a sibling UI package).
1892
+ */
1893
+ /**
1894
+ * Render one plugin settings card.
1895
+ * @param props - the plugin's copy keys, its form state, and its controls.
1896
+ * @returns the card, or nothing when the namespace is unavailable.
1897
+ */
1898
+ function PluginSettingsCard(props) {
1899
+ const [open, setOpen] = (0, react.useState)(false);
1900
+ const { state } = props;
1901
+ if (!state.available) return null;
1902
+ const title = props.t(props.titleKey);
1903
+ const blocked = !state.dirty || state.invalid || state.saving;
1904
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1905
+ className: settings_card_module_css_default.card,
1906
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1907
+ type: "button",
1908
+ className: settings_card_module_css_default.header,
1909
+ "aria-expanded": open,
1910
+ "aria-label": `${props.t(open ? "settings.collapse" : "settings.expand")}: ${title}`,
1911
+ onClick: () => {
1912
+ setOpen(!open);
1913
+ },
1914
+ children: [
1915
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1916
+ className: settings_card_module_css_default.headText,
1917
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1918
+ className: settings_card_module_css_default.name,
1919
+ children: title
1920
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1921
+ className: settings_card_module_css_default.description,
1922
+ children: props.t(props.descriptionKey)
1923
+ })]
1924
+ }),
1925
+ state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1926
+ className: settings_card_module_css_default.pending,
1927
+ children: props.t("settings.unsaved")
1928
+ }) : null,
1929
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1930
+ className: open ? settings_card_module_css_default.chevronOpen : settings_card_module_css_default.chevron,
1931
+ children: "▾"
1932
+ })
1933
+ ]
1934
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1935
+ className: settings_card_module_css_default.body,
1936
+ children: [
1937
+ !state.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1938
+ className: settings_card_module_css_default.readOnly,
1939
+ role: "status",
1940
+ children: props.t("settings.readOnly")
1941
+ }) : null,
1942
+ props.children,
1943
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1944
+ className: settings_card_module_css_default.footer,
1945
+ children: [
1946
+ state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1947
+ className: settings_card_module_css_default.failed,
1948
+ role: "status",
1949
+ children: props.t("settings.saveFailed")
1950
+ }) : null,
1951
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1952
+ type: "button",
1953
+ className: settings_card_module_css_default.discard,
1954
+ disabled: !state.dirty || state.saving,
1955
+ onClick: props.onDiscard,
1956
+ children: props.t("settings.discard")
1957
+ }),
1958
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1959
+ type: "button",
1960
+ className: settings_card_module_css_default.save,
1961
+ disabled: blocked,
1962
+ onClick: props.onSave,
1963
+ children: props.t(!state.saving ? "settings.save" : "settings.saving")
1964
+ })
1965
+ ]
1966
+ })
1967
+ ]
1968
+ }) : null]
1969
+ });
1970
+ }
1971
+ /** A staged value field. `numeric` only hints the keypad: which drafts a field accepts is decided by its spec. */
1972
+ function ValueField(props) {
1973
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1974
+ className: settings_card_module_css_default.field,
1975
+ children: [
1976
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1977
+ className: settings_card_module_css_default.head,
1978
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1979
+ className: settings_card_module_css_default.label,
1980
+ htmlFor: props.id,
1981
+ children: props.label
1982
+ }), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1983
+ className: settings_card_module_css_default.badges,
1984
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1985
+ className: settings_card_module_css_default.badge,
1986
+ children: props.overriddenLabel
1987
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1988
+ type: "button",
1989
+ className: settings_card_module_css_default.reset,
1990
+ disabled: props.disabled,
1991
+ onClick: props.onReset,
1992
+ children: props.resetLabel
1993
+ })]
1994
+ }) : null]
1995
+ }),
1996
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1997
+ id: props.id,
1998
+ className: props.invalid ? settings_card_module_css_default.inputInvalid : settings_card_module_css_default.input,
1999
+ type: "text",
2000
+ ...props.numeric === true ? { inputMode: "numeric" } : {},
2001
+ ...props.invalid ? { "aria-invalid": true } : {},
2002
+ value: props.text,
2003
+ placeholder: props.placeholder ?? "",
2004
+ disabled: props.disabled,
2005
+ onChange: (event) => {
2006
+ props.onEdit(event.target.value);
2007
+ }
2008
+ }),
2009
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2010
+ className: props.invalid ? settings_card_module_css_default.invalid : settings_card_module_css_default.hint,
2011
+ children: props.invalid ? props.invalidLabel : props.hint
2012
+ })
2013
+ ]
2014
+ });
2015
+ }
2016
+ /** A staged boolean field: 继承 / 开 / 关. */
2017
+ function BooleanField(props) {
2018
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2019
+ className: settings_card_module_css_default.field,
2020
+ children: [
2021
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2022
+ className: settings_card_module_css_default.head,
2023
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2024
+ className: settings_card_module_css_default.label,
2025
+ htmlFor: props.id,
2026
+ children: props.label
2027
+ }), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2028
+ className: settings_card_module_css_default.badges,
2029
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2030
+ className: settings_card_module_css_default.badge,
2031
+ children: props.overriddenLabel
2032
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2033
+ type: "button",
2034
+ className: settings_card_module_css_default.reset,
2035
+ disabled: props.disabled,
2036
+ onClick: props.onReset,
2037
+ children: props.resetLabel
2038
+ })]
2039
+ }) : null]
2040
+ }),
2041
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2042
+ id: props.id,
2043
+ className: settings_card_module_css_default.select,
2044
+ value: props.text,
2045
+ disabled: props.disabled,
2046
+ onChange: (event) => {
2047
+ props.onEdit(event.target.value);
2048
+ },
2049
+ children: [
2050
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2051
+ value: "",
2052
+ children: props.inheritLabel
2053
+ }),
2054
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2055
+ value: "true",
2056
+ children: props.onLabel
2057
+ }),
2058
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2059
+ value: "false",
2060
+ children: props.offLabel
2061
+ })
2062
+ ]
2063
+ }),
2064
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2065
+ className: settings_card_module_css_default.hint,
2066
+ children: props.hint
2067
+ })
2068
+ ]
2069
+ });
2070
+ }
2071
+ //#endregion
2072
+ //#region src/client/settings-form.ts
2073
+ /** A whole-number field. An empty draft clears the field; any other draft that is not a finite number blocks the save. */
2074
+ function numberField(field) {
2075
+ return {
2076
+ field,
2077
+ format: (value) => typeof value === "number" ? String(value) : "",
2078
+ parse: (text) => {
2079
+ const trimmed = text.trim();
2080
+ if (trimmed === "") return { kind: "clear" };
2081
+ const parsed = Number(trimmed);
2082
+ return Number.isFinite(parsed) ? {
2083
+ kind: "set",
2084
+ value: parsed
2085
+ } : void 0;
2086
+ }
2087
+ };
2088
+ }
2089
+ /** A free-text field. An empty draft clears the field. */
2090
+ function textField(field) {
2091
+ return {
2092
+ field,
2093
+ format: (value) => typeof value === "string" ? value : "",
2094
+ parse: (text) => {
2095
+ const trimmed = text.trim();
2096
+ return trimmed === "" ? { kind: "clear" } : {
2097
+ kind: "set",
2098
+ value: trimmed
2099
+ };
2100
+ }
2101
+ };
2102
+ }
2103
+ /** A boolean field, edited through true/false draft text. */
2104
+ function booleanField(field) {
2105
+ return {
2106
+ field,
2107
+ format: (value) => typeof value === "boolean" ? String(value) : "",
2108
+ parse: (text) => {
2109
+ if (text === "true") return {
2110
+ kind: "set",
2111
+ value: true
2112
+ };
2113
+ if (text === "false") return {
2114
+ kind: "set",
2115
+ value: false
2116
+ };
2117
+ }
2118
+ };
2119
+ }
2120
+ /**
2121
+ * Stages one card's edits over one settings namespace and writes them on save.
2122
+ *
2123
+ * The Host is the only authority on whether a value was accepted — its
2124
+ * validators own the constraints no schema can express — so the outcome is
2125
+ * read back from the section rather than predicted here. A save that did not
2126
+ * land keeps its drafts, so the user can correct them instead of retyping.
2127
+ */
2128
+ var CardForm = class {
2129
+ scope;
2130
+ specs;
2131
+ staged = /* @__PURE__ */ new Map();
2132
+ listeners = /* @__PURE__ */ new Set();
2133
+ saving = false;
2134
+ failed = false;
2135
+ /** @param scope - the bound settings scope for this card's namespace. */
2136
+ constructor(scope, specs) {
2137
+ this.scope = scope;
2138
+ this.specs = new Map(specs.map((spec) => [spec.field, spec]));
2139
+ scope.subscribe(() => {
2140
+ this.publish();
2141
+ });
2142
+ }
2143
+ /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
2144
+ bind(project) {
2145
+ const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(project());
2146
+ this.listeners.add(() => {
2147
+ store.set(project());
2148
+ });
2149
+ return store;
2150
+ }
2151
+ /** Read the card-level state: what the Host serves, and what a save would do. */
2152
+ shell() {
2153
+ const snapshot = this.scope.getSnapshot();
2154
+ const plan = this.plan();
2155
+ return {
2156
+ available: snapshot.status === "ready",
2157
+ writable: snapshot.writable,
2158
+ dirty: plan.length > 0,
2159
+ invalid: plan.some((item) => item.run === void 0),
2160
+ saving: this.saving,
2161
+ failed: this.failed
2162
+ };
2163
+ }
2164
+ /** Read one field's state from the effective section and its staged draft. */
2165
+ field(field) {
2166
+ const spec = this.specOf(field);
2167
+ const staged = this.staged.get(field);
2168
+ if (staged === void 0) return {
2169
+ text: spec.format(this.sectionValue(field)),
2170
+ overridden: this.stored(field),
2171
+ invalid: false
2172
+ };
2173
+ const write = staged.clear ? { kind: "clear" } : spec.parse(staged.text);
2174
+ return {
2175
+ text: staged.text,
2176
+ overridden: write?.kind === "set",
2177
+ invalid: write === void 0
2178
+ };
2179
+ }
2180
+ /** The actions the card's slot registration injects. */
2181
+ actions() {
2182
+ return {
2183
+ edit: (field, text) => {
2184
+ this.stage(field, {
2185
+ text,
2186
+ clear: false
2187
+ });
2188
+ },
2189
+ resetField: (field) => {
2190
+ this.stage(field, {
2191
+ text: this.specOf(field).format(this.baseValue(field)),
2192
+ clear: true
2193
+ });
2194
+ },
2195
+ save: () => {
2196
+ this.save();
2197
+ },
2198
+ discard: () => {
2199
+ if (this.staged.size === 0 && !this.failed) return;
2200
+ this.staged.clear();
2201
+ this.failed = false;
2202
+ this.publish();
2203
+ }
2204
+ };
2205
+ }
2206
+ /**
2207
+ * Write every staged edit, then re-seed from what the Host accepted.
2208
+ * @returns settlement after every write and the read-back.
2209
+ */
2210
+ async save() {
2211
+ const plan = this.plan();
2212
+ const writes = plan.flatMap((item) => item.run === void 0 ? [] : [item.run]);
2213
+ if (plan.length === 0 || this.saving || writes.length !== plan.length) return;
2214
+ this.saving = true;
2215
+ this.failed = false;
2216
+ this.publish();
2217
+ let landed = true;
2218
+ for (const write of writes) landed = await write() && landed;
2219
+ if (landed) this.staged.clear();
2220
+ this.saving = false;
2221
+ this.failed = !landed;
2222
+ this.publish();
2223
+ }
2224
+ /**
2225
+ * Every staged edit a save would write. An entry whose draft is not a value
2226
+ * its field accepts carries no write: the form is still dirty, and the save
2227
+ * refuses rather than dropping the edit. A staged edit that matches the
2228
+ * effective section is not a write at all.
2229
+ * @returns the planned writes, in the order the fields were staged.
2230
+ */
2231
+ plan() {
2232
+ const plan = [];
2233
+ for (const [field, staged] of this.staged) {
2234
+ const spec = this.specOf(field);
2235
+ if (staged.clear) {
2236
+ if (this.stored(field)) plan.push({
2237
+ field,
2238
+ run: () => this.clear(field)
2239
+ });
2240
+ continue;
2241
+ }
2242
+ if (staged.text === spec.format(this.sectionValue(field))) continue;
2243
+ const write = spec.parse(staged.text);
2244
+ if (write === void 0) plan.push({
2245
+ field,
2246
+ run: void 0
2247
+ });
2248
+ else if (write.kind === "clear") plan.push({
2249
+ field,
2250
+ run: () => this.clear(field)
2251
+ });
2252
+ else plan.push({
2253
+ field,
2254
+ run: () => this.store(field, write.value)
2255
+ });
2256
+ }
2257
+ return plan;
2258
+ }
2259
+ async clear(field) {
2260
+ await this.scope.unset(field);
2261
+ return !this.stored(field);
2262
+ }
2263
+ async store(field, value) {
2264
+ await this.scope.set(field, value);
2265
+ return this.userLayer()?.[field] === value;
2266
+ }
2267
+ stage(field, edit) {
2268
+ this.staged.set(field, edit);
2269
+ this.failed = false;
2270
+ this.publish();
2271
+ }
2272
+ specOf(field) {
2273
+ const spec = this.specs.get(field);
2274
+ if (spec === void 0) throw new Error(`settings card has no field ${field}`);
2275
+ return spec;
2276
+ }
2277
+ snapshotOf() {
2278
+ return this.scope.getSnapshot();
2279
+ }
2280
+ sectionValue(field) {
2281
+ return this.snapshotOf().value?.[field];
2282
+ }
2283
+ baseValue(field) {
2284
+ return this.snapshotOf().base?.[field];
2285
+ }
2286
+ userLayer() {
2287
+ return this.snapshotOf().user;
2288
+ }
2289
+ stored(field) {
2290
+ const user = this.userLayer();
2291
+ return user !== void 0 && Object.hasOwn(user, field);
2292
+ }
2293
+ publish() {
2294
+ for (const listener of this.listeners) listener();
2295
+ }
2296
+ };
2297
+ //#endregion
2298
+ //#region src/client/RemoteSettingsCard.tsx
2299
+ /** Bridges the `remote-web-ui` scope onto the card's staged form. */
2300
+ var RemoteSettingsCardController = class {
2301
+ form;
2302
+ store;
2303
+ /** @param scope - the bound settings scope for the `remote-web-ui` namespace. */
2304
+ constructor(scope) {
2305
+ this.form = new CardForm(scope, [
2306
+ booleanField("enabled"),
2307
+ numberField("tokenTtlMs"),
2308
+ numberField("offlineAfterMs"),
2309
+ numberField("maxDevices"),
2310
+ textField("cookieName"),
2311
+ booleanField("requirePairingForLan"),
2312
+ textField("publicBaseUrl"),
2313
+ booleanField("autoTunnel")
2314
+ ]);
2315
+ this.store = this.form.bind(() => this.projection());
2316
+ }
2317
+ projection() {
2318
+ return {
2319
+ ...this.form.shell(),
2320
+ enabled: this.form.field("enabled"),
2321
+ tokenTtlMs: this.form.field("tokenTtlMs"),
2322
+ offlineAfterMs: this.form.field("offlineAfterMs"),
2323
+ maxDevices: this.form.field("maxDevices"),
2324
+ cookieName: this.form.field("cookieName"),
2325
+ requirePairingForLan: this.form.field("requirePairingForLan"),
2326
+ publicBaseUrl: this.form.field("publicBaseUrl"),
2327
+ autoTunnel: this.form.field("autoTunnel")
2328
+ };
2329
+ }
2330
+ /**
2331
+ * Build the face the card's slot registration injects.
2332
+ * @returns the card's snapshot and its form actions.
2333
+ */
2334
+ inject() {
2335
+ return {
2336
+ hooks: { remoteSettingsCard: this.store },
2337
+ ...this.form.actions()
2338
+ };
2339
+ }
2340
+ };
2341
+ /**
2342
+ * Render the remote-control card.
2343
+ * @param props - locale copy, the card snapshot, and its form actions.
2344
+ * @returns the card.
2345
+ */
2346
+ function RemoteSettingsCard(props) {
2347
+ const { t } = props;
2348
+ const state = props.useRemoteSettingsCard((snapshot) => snapshot);
2349
+ const disabled = !state.writable;
2350
+ const fieldProps = {
2351
+ overriddenLabel: t("settings.overridden"),
2352
+ resetLabel: t("settings.reset"),
2353
+ invalidLabel: t("settings.invalidNumber"),
2354
+ disabled
2355
+ };
2356
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(PluginSettingsCard, {
2357
+ t,
2358
+ titleKey: "settings.title",
2359
+ descriptionKey: "settings.description",
2360
+ state,
2361
+ onSave: props.save,
2362
+ onDiscard: props.discard,
2363
+ children: [
2364
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
2365
+ id: "settings-remote-enabled",
2366
+ label: t("settings.enabled"),
2367
+ hint: t("settings.enabledHint"),
2368
+ inheritLabel: t("settings.inherit"),
2369
+ onLabel: t("settings.on"),
2370
+ offLabel: t("settings.off"),
2371
+ ...fieldProps,
2372
+ ...state.enabled,
2373
+ onEdit: (text) => {
2374
+ props.edit("enabled", text);
2375
+ },
2376
+ onReset: () => {
2377
+ props.resetField("enabled");
2378
+ }
2379
+ }),
2380
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
2381
+ id: "settings-remote-token-ttl",
2382
+ label: t("settings.tokenTtlMs"),
2383
+ hint: t("settings.tokenTtlMsHint"),
2384
+ numeric: true,
2385
+ ...fieldProps,
2386
+ ...state.tokenTtlMs,
2387
+ onEdit: (text) => {
2388
+ props.edit("tokenTtlMs", text);
2389
+ },
2390
+ onReset: () => {
2391
+ props.resetField("tokenTtlMs");
2392
+ }
2393
+ }),
2394
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
2395
+ id: "settings-remote-offline",
2396
+ label: t("settings.offlineAfterMs"),
2397
+ hint: t("settings.offlineAfterMsHint"),
2398
+ numeric: true,
2399
+ ...fieldProps,
2400
+ ...state.offlineAfterMs,
2401
+ onEdit: (text) => {
2402
+ props.edit("offlineAfterMs", text);
2403
+ },
2404
+ onReset: () => {
2405
+ props.resetField("offlineAfterMs");
2406
+ }
2407
+ }),
2408
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
2409
+ id: "settings-remote-max-devices",
2410
+ label: t("settings.maxDevices"),
2411
+ hint: t("settings.maxDevicesHint"),
2412
+ numeric: true,
2413
+ ...fieldProps,
2414
+ ...state.maxDevices,
2415
+ onEdit: (text) => {
2416
+ props.edit("maxDevices", text);
2417
+ },
2418
+ onReset: () => {
2419
+ props.resetField("maxDevices");
2420
+ }
2421
+ }),
2422
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
2423
+ id: "settings-remote-cookie",
2424
+ label: t("settings.cookieName"),
2425
+ hint: t("settings.cookieNameHint"),
2426
+ ...fieldProps,
2427
+ ...state.cookieName,
2428
+ onEdit: (text) => {
2429
+ props.edit("cookieName", text);
2430
+ },
2431
+ onReset: () => {
2432
+ props.resetField("cookieName");
2433
+ }
2434
+ }),
2435
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
2436
+ id: "settings-remote-fence",
2437
+ label: t("settings.requirePairingForLan"),
2438
+ hint: t("settings.requirePairingForLanHint"),
2439
+ inheritLabel: t("settings.inherit"),
2440
+ onLabel: t("settings.on"),
2441
+ offLabel: t("settings.off"),
2442
+ ...fieldProps,
2443
+ ...state.requirePairingForLan,
2444
+ onEdit: (text) => {
2445
+ props.edit("requirePairingForLan", text);
2446
+ },
2447
+ onReset: () => {
2448
+ props.resetField("requirePairingForLan");
2449
+ }
2450
+ }),
2451
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
2452
+ id: "settings-remote-public-base",
2453
+ label: t("settings.publicBaseUrl"),
2454
+ hint: t("settings.publicBaseUrlHint"),
2455
+ placeholder: "https://example.trycloudflare.com",
2456
+ ...fieldProps,
2457
+ ...state.publicBaseUrl,
2458
+ onEdit: (text) => {
2459
+ props.edit("publicBaseUrl", text);
2460
+ },
2461
+ onReset: () => {
2462
+ props.resetField("publicBaseUrl");
2463
+ }
2464
+ }),
2465
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
2466
+ id: "settings-remote-auto-tunnel",
2467
+ label: t("settings.autoTunnel"),
2468
+ hint: t("settings.autoTunnelHint"),
2469
+ inheritLabel: t("settings.inherit"),
2470
+ onLabel: t("settings.on"),
2471
+ offLabel: t("settings.off"),
2472
+ ...fieldProps,
2473
+ ...state.autoTunnel,
2474
+ onEdit: (text) => {
2475
+ props.edit("autoTunnel", text);
2476
+ },
2477
+ onReset: () => {
2478
+ props.resetField("autoTunnel");
2479
+ }
2480
+ })
2481
+ ]
2482
+ });
2483
+ }
2484
+ //#endregion
2485
+ //#region src/client/locales.ts
2486
+ /** `remote` namespace dictionaries: the mobile remote-control surface copy. */
2487
+ /** Simplified Chinese dictionary (the key-set source of truth). */
2488
+ const zh = {
2489
+ "entry.label": "移动端远程控制",
2490
+ "title": "移动端远程控制",
2491
+ "subtitle": "扫码或在手机上打开链接,即可远程控制当前工作区",
2492
+ "card.title": "手机扫码连接",
2493
+ "public.badge": "公网",
2494
+ "status.waiting": "等待手机连接",
2495
+ "status.connected": "已连接 {n} 台设备",
2496
+ "status.disconnected": "已配对设备离线",
2497
+ "status.stopped": "已停止移动端访问",
2498
+ "status.lanRequired": "此功能需要以 --host 0.0.0.0 启动 dsh web,或配置公网地址才能使用",
2499
+ "status.lanRequiredHint": "当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请用 dsh web --host 0.0.0.0 重新启动,或在设置中填写内网穿透的公网地址。",
2500
+ "status.loopbackRequired": "配对面板仅限本机使用",
2501
+ "status.loopbackRequiredHint": "请通过 http://127.0.0.1 打开此页面后重试;手机请使用配对链接访问。",
2502
+ "status.unreachable": "无法连接配对服务",
2503
+ "status.unreachableHint": "请刷新页面后重试。",
2504
+ "pair.hint": "无法扫码?可以在手机上打开链接",
2505
+ "pair.publicHint": "公网链接:手机无需与电脑在同一网络",
2506
+ "address.label": "选择二维码指向的网络",
2507
+ "address.public": "公网地址",
2508
+ "address.lan": "局域网",
2509
+ "address.hint": "公网地址适合手机不在同一网络时使用;局域网地址仅在手机与电脑同一网络时可用。",
2510
+ "pair.expires": "二维码有效至 {time}",
2511
+ "pair.expired": "二维码已过期,请刷新",
2512
+ "pair.failed.title": "配对失败",
2513
+ "pair.failed.detail": "链接无效或已使用,请回到电脑端刷新二维码后重新扫码。",
2514
+ "action.stop": "停止",
2515
+ "action.refresh": "刷新二维码",
2516
+ "action.copy": "复制链接",
2517
+ "action.copied": "已复制",
2518
+ "stopped.hint": "已停止移动端访问。点击\"刷新二维码\"重新开启。",
2519
+ "tunnel.starting": "公网隧道启动中,二维码将自动变为公网链接…",
2520
+ "tunnel.failed": "公网隧道启动失败:{error}",
2521
+ "tunnel.unknownError": "未知错误",
2522
+ "close.label": "关闭移动端远程控制面板",
2523
+ "settings.title": "移动端远程控制",
2524
+ "settings.description": "配对安全与设备限额。",
2525
+ "settings.enabled": "启用移动端远程控制",
2526
+ "settings.enabledHint": "关闭后移除侧边栏入口并停用配对路由与局域网栅栏。",
2527
+ "settings.tokenTtlMs": "配对令牌有效期(毫秒)",
2528
+ "settings.tokenTtlMsHint": "新生成的二维码链接在此时间后失效。",
2529
+ "settings.offlineAfterMs": "设备离线判定(毫秒)",
2530
+ "settings.offlineAfterMsHint": "配对设备超过该时长未上报心跳即视为离线。",
2531
+ "settings.maxDevices": "已配对设备上限",
2532
+ "settings.maxDevicesHint": "超过上限时淘汰最旧的设备会话。",
2533
+ "settings.cookieName": "设备 Cookie 名",
2534
+ "settings.cookieNameHint": "携带已配对设备标识的 Cookie 名称。",
2535
+ "settings.requirePairingForLan": "局域网访问要求配对",
2536
+ "settings.requirePairingForLanHint": "开启:所有非本机回环的 /api 请求必须携带有效配对 Cookie;关闭:局域网栅栏放开,配对仅用于令牌与状态管理。",
2537
+ "settings.publicBaseUrl": "公网地址(可选)",
2538
+ "settings.publicBaseUrlHint": "内网穿透的公网地址,如 Cloudflare Tunnel:https://xxx.trycloudflare.com。填写后二维码将生成公网链接,手机不在同一网络也能配对;留空则仅局域网可用。",
2539
+ "settings.autoTunnel": "自动公网隧道",
2540
+ "settings.autoTunnelHint": "开启后插件自动启动 Cloudflare quick tunnel(无需安装任何工具),并自动更新公网地址与信任配置,手机随时可用公网配对;关闭时使用上方手动填写的公网地址。",
2541
+ "settings.inherit": "继承",
2542
+ "settings.on": "开",
2543
+ "settings.off": "关",
2544
+ "settings.overridden": "已覆盖",
2545
+ "settings.reset": "恢复默认",
2546
+ "settings.readOnly": "当前部署的设置只读。",
2547
+ "settings.expand": "展开设置",
2548
+ "settings.collapse": "收起设置",
2549
+ "settings.save": "保存",
2550
+ "settings.saving": "保存中…",
2551
+ "settings.discard": "放弃",
2552
+ "settings.unsaved": "未保存",
2553
+ "settings.saveFailed": "部署未接受这些值,已保留供你修改。",
2554
+ "settings.invalidNumber": "请输入数字,留空则使用默认值。"
2555
+ };
2556
+ /** English dictionary, checked complete against the zh key set. */
2557
+ const en = {
2558
+ "entry.label": "Mobile remote control",
2559
+ "title": "Mobile remote control",
2560
+ "subtitle": "Scan the QR code or open the link on your phone to control this workspace remotely",
2561
+ "card.title": "Scan to connect",
2562
+ "public.badge": "Public",
2563
+ "status.waiting": "Waiting for a phone",
2564
+ "status.connected": "{n} device(s) connected",
2565
+ "status.disconnected": "Paired devices offline",
2566
+ "status.stopped": "Mobile access stopped",
2567
+ "status.lanRequired": "This feature needs dsh web started with --host 0.0.0.0, or a configured public address",
2568
+ "status.lanRequiredHint": "The server is bound to 127.0.0.1 and no public address is configured, so a phone cannot reach it. Restart with dsh web --host 0.0.0.0, or set the tunneled public address in settings.",
2569
+ "status.loopbackRequired": "The pairing panel works on this machine only",
2570
+ "status.loopbackRequiredHint": "Open this page at http://127.0.0.1 to mint a QR code; phones use the paired link.",
2571
+ "status.unreachable": "Cannot reach the pairing service",
2572
+ "status.unreachableHint": "Refresh the page and try again.",
2573
+ "pair.hint": "Cannot scan? Open the link on your phone",
2574
+ "pair.publicHint": "Public link: the phone does not need to be on the same network",
2575
+ "address.label": "Network the QR code points to",
2576
+ "address.public": "Public",
2577
+ "address.lan": "LAN",
2578
+ "address.hint": "Use the public address when the phone is not on the same network; LAN addresses only work on the same network.",
2579
+ "pair.expires": "QR code valid until {time}",
2580
+ "pair.expired": "QR code expired — refresh it",
2581
+ "pair.failed.title": "Pairing failed",
2582
+ "pair.failed.detail": "The link is invalid or was already used. Refresh the QR code on your computer and scan again.",
2583
+ "action.stop": "Stop",
2584
+ "action.refresh": "Refresh QR",
2585
+ "action.copy": "Copy link",
2586
+ "action.copied": "Copied",
2587
+ "stopped.hint": "Mobile access is stopped. Click \"Refresh QR\" to re-enable it.",
2588
+ "tunnel.starting": "The public tunnel is starting; the QR code will switch to a public link shortly…",
2589
+ "tunnel.failed": "The public tunnel failed to start: {error}",
2590
+ "tunnel.unknownError": "unknown error",
2591
+ "close.label": "Close mobile remote control panel",
2592
+ "settings.title": "Mobile remote control",
2593
+ "settings.description": "Pairing security and device limits.",
2594
+ "settings.enabled": "Enable mobile remote control",
2595
+ "settings.enabledHint": "When off, the sidebar entry is removed and pairing routes plus the LAN fence stop.",
2596
+ "settings.tokenTtlMs": "Pairing token lifetime (ms)",
2597
+ "settings.tokenTtlMsHint": "How long a minted QR link stays valid before it dies.",
2598
+ "settings.offlineAfterMs": "Device offline threshold (ms)",
2599
+ "settings.offlineAfterMsHint": "A paired device flips to offline when it has not been seen for this long.",
2600
+ "settings.maxDevices": "Paired device cap",
2601
+ "settings.maxDevicesHint": "Hard cap on paired device sessions; the oldest is evicted when full.",
2602
+ "settings.cookieName": "Device cookie name",
2603
+ "settings.cookieNameHint": "Cookie that carries the paired device id.",
2604
+ "settings.requirePairingForLan": "Require pairing for LAN access",
2605
+ "settings.requirePairingForLanHint": "On: every non-loopback /api request must carry a live paired-device cookie. Off: the LAN fence is open and pairing only manages tokens/status.",
2606
+ "settings.publicBaseUrl": "Public address (optional)",
2607
+ "settings.publicBaseUrlHint": "The public URL of a tunnel in front of this server, e.g. a Cloudflare Tunnel: https://xxx.trycloudflare.com. When set, the QR link is built from it so a phone anywhere can pair; leave blank for LAN-only usage.",
2608
+ "settings.autoTunnel": "Auto public tunnel",
2609
+ "settings.autoTunnelHint": "When on, the plugin runs its own Cloudflare quick tunnel (no tool installation needed) and keeps the public address and trust config in sync automatically, so a phone anywhere can pair at any time; when off, the manually entered public address above applies.",
2610
+ "settings.inherit": "Inherit",
2611
+ "settings.on": "On",
2612
+ "settings.off": "Off",
2613
+ "settings.overridden": "Overridden",
2614
+ "settings.reset": "Reset to default",
2615
+ "settings.readOnly": "This deployment stores settings read-only.",
2616
+ "settings.expand": "Show settings",
2617
+ "settings.collapse": "Hide settings",
2618
+ "settings.save": "Save",
2619
+ "settings.saving": "Saving…",
2620
+ "settings.discard": "Discard",
2621
+ "settings.unsaved": "Unsaved",
2622
+ "settings.saveFailed": "The deployment did not accept these values; they were left for you to correct.",
2623
+ "settings.invalidNumber": "Enter a number, or leave blank to use the default."
2624
+ };
2625
+ //#endregion
2626
+ //#region src/client/deep-link.ts
2627
+ /** sessionStorage key for the failed-pair notice. */
2628
+ const PAIR_FAILED_MARKER = "dsh-remote-pair-failed";
2629
+ /** Poll budget for the runtime services (activation order is unconstrained). */
2630
+ const SERVICE_WAIT_MS = 1e4;
2631
+ /** The browser implementation of {@link PageSurface}. */
2632
+ const browserPage = {
2633
+ get href() {
2634
+ return window.location.href;
2635
+ },
2636
+ replaceState(url) {
2637
+ window.history.replaceState(null, "", url);
2638
+ },
2639
+ navigate(url) {
2640
+ window.location.assign(url);
2641
+ },
2642
+ reload() {
2643
+ window.location.reload();
2644
+ }
2645
+ };
2646
+ /** Whether this browser looks like a phone/tablet (the simplified mobile surface). */
2647
+ function isMobileSurface() {
2648
+ if (typeof navigator === "undefined") return false;
2649
+ return /Android|iPhone|iPad|iPod|Mobile|mobile/i.test(navigator.userAgent);
2650
+ }
2651
+ /**
2652
+ * Run the pair/workspace boot flow for this page load.
2653
+ * @param ctx - client root context (workspaces/sessions read at need time).
2654
+ * @param search - the current location.search.
2655
+ * @param page - the page surface (defaults to the browser).
2656
+ */
2657
+ function runPairBootFlow(ctx, search, page = browserPage) {
2658
+ const params = readPairParams(search);
2659
+ if (params.pair !== void 0) {
2660
+ runAccept(params.pair, page);
2661
+ return;
2662
+ }
2663
+ if (params.workspace !== void 0) runDeepLink(ctx, params.workspace, page);
2664
+ }
2665
+ /** Accept the token, then reload (the workspace param survives the reload). */
2666
+ async function runAccept(token, page) {
2667
+ let ok = false;
2668
+ try {
2669
+ ok = (await acceptPair(token)).ok;
2670
+ if (!ok) sessionStorage.setItem(PAIR_FAILED_MARKER, "failed");
2671
+ } catch {
2672
+ sessionStorage.setItem(PAIR_FAILED_MARKER, "failed");
2673
+ }
2674
+ const url = new URL(page.href);
2675
+ url.searchParams.delete("pair");
2676
+ page.replaceState(`${url.pathname}${url.search}${url.hash}`);
2677
+ if (ok) if (isMobileSurface()) page.navigate("/m");
2678
+ else page.reload();
2679
+ }
2680
+ /**
2681
+ * Connect the deep-linked workspace and open its session. Waits for the
2682
+ * runtime services AND for the target workspace to appear in the workspace
2683
+ * list (both are asynchronous after boot), then connects and opens; gives
2684
+ * up silently within the budget — the workspace param is stripped either
2685
+ * way, so a late failure cannot loop.
2686
+ * @param ctx - client root context.
2687
+ * @param workspaceId - the target workspace.
2688
+ * @param page - the page surface.
2689
+ */
2690
+ async function runDeepLink(ctx, workspaceId, page) {
2691
+ const target = workspaceId;
2692
+ const deadline = Date.now() + SERVICE_WAIT_MS;
2693
+ while (Date.now() < deadline) {
2694
+ const workspaces = ctx.get("workspaces");
2695
+ const sessions = ctx.get("sessions");
2696
+ if (workspaces !== void 0 && sessions !== void 0) {
2697
+ if (workspaces.list.getSnapshot().items.some((item) => item.workspaceId === target)) {
2698
+ try {
2699
+ const sessionId = await workspaces.connectWorkspace(target);
2700
+ sessions.open(sessionId);
2701
+ } catch {}
2702
+ break;
2703
+ }
2704
+ }
2705
+ await new Promise((resolve) => setTimeout(resolve, 200));
2706
+ }
2707
+ const url = new URL(page.href);
2708
+ url.searchParams.delete("workspace");
2709
+ page.replaceState(`${url.pathname}${url.search}${url.hash}`);
2710
+ }
2711
+ //#endregion
2712
+ //#region src/client/index.ts
2713
+ /**
2714
+ * Mobile remote control — browser half. Registers the `remote` dictionaries,
2715
+ * the sidebar-foot entry (phone trigger + pairing panel) into the
2716
+ * ui-sidebar-declared `sidebar.remote` seat, and runs the phone-side boot
2717
+ * flow (pair accept + workspace deep-link + presence heartbeats) plus the
2718
+ * one-time failed-pair notice. Export discipline: packages/client/AGENTS.md
2719
+ * — the /client surface carries only what cordis loading needs plus types.
2720
+ */
2721
+ /** Dictionary namespace owned by this plugin. */
2722
+ const NS = "remote";
2723
+ /** Settings namespace the remote-control card edits (the Host plugin registers it). */
2724
+ const REMOTE_WEB_UI_NS = "remote-web-ui";
2725
+ /** Heartbeat cadence from a paired phone (presence + revocation liveness). */
2726
+ const HEARTBEAT_INTERVAL_MS = 1e4;
2727
+ /** Services required by this plugin. */
2728
+ const inject = [
2729
+ "slots",
2730
+ "locale",
2731
+ "connection",
2732
+ "settingsScope",
2733
+ "remote"
2734
+ ];
2735
+ /**
2736
+ * Register the remote-control surface.
2737
+ * @param ctx - client root context.
2738
+ */
2739
+ function apply(ctx) {
2740
+ ctx.effect(() => ctx.locale.register(NS, {
2741
+ zh,
2742
+ en
2743
+ }), "remote-web-ui: dictionaries");
2744
+ const t = ctx.locale.bind(NS);
2745
+ const settingsScope = ctx.settingsScope.bind({ namespace: REMOTE_WEB_UI_NS });
2746
+ const enabled = () => {
2747
+ const snapshot = settingsScope.getSnapshot();
2748
+ return snapshot.status === "ready" ? snapshot.value?.enabled ?? true : snapshot.status === "unavailable";
2749
+ };
2750
+ ctx.slots.inject("sidebar.remote", () => {
2751
+ let disposeEntry;
2752
+ const syncEntry = () => {
2753
+ if (enabled() && disposeEntry === void 0) disposeEntry = ctx.slots.register({
2754
+ name: "sidebar.remote",
2755
+ locale: NS
2756
+ }, RemoteEntry);
2757
+ else if (!enabled() && disposeEntry !== void 0) {
2758
+ disposeEntry();
2759
+ disposeEntry = void 0;
2760
+ }
2761
+ };
2762
+ const unsubscribe = settingsScope.subscribe(syncEntry);
2763
+ syncEntry();
2764
+ return () => {
2765
+ unsubscribe();
2766
+ disposeEntry?.();
2767
+ };
2768
+ });
2769
+ const remoteSettings = new RemoteSettingsCardController(settingsScope);
2770
+ ctx.slots.inject("web-ui.plugin.item", () => ctx.slots.register({
2771
+ name: "web-ui.plugin.item",
2772
+ id: "remote-web-ui",
2773
+ order: 90,
2774
+ locale: NS,
2775
+ inject: () => remoteSettings.inject()
2776
+ }, RemoteSettingsCard));
2777
+ let disposeRuntime;
2778
+ const syncRuntime = () => {
2779
+ if (enabled() && disposeRuntime === void 0) disposeRuntime = ctx.effect(() => {
2780
+ const loopback = ctx.get("connection")?.isLoopback ?? true;
2781
+ runPairBootFlow(ctx, window.location.search);
2782
+ if (loopback) return () => {};
2783
+ const timer = window.setInterval(() => {
2784
+ sendHeartbeat().catch(() => {});
2785
+ }, HEARTBEAT_INTERVAL_MS);
2786
+ return () => {
2787
+ window.clearInterval(timer);
2788
+ };
2789
+ }, "remote-web-ui: pair flow + heartbeats");
2790
+ else if (!enabled() && disposeRuntime !== void 0) {
2791
+ disposeRuntime();
2792
+ disposeRuntime = void 0;
2793
+ }
2794
+ };
2795
+ settingsScope.subscribe(syncRuntime);
2796
+ syncRuntime();
2797
+ ctx.effect(() => {
2798
+ const timer = window.setTimeout(() => {
2799
+ if (sessionStorage.getItem("dsh-remote-pair-failed") === null) return;
2800
+ sessionStorage.removeItem(PAIR_FAILED_MARKER);
2801
+ const mount = document.createElement("div");
2802
+ document.body.appendChild(mount);
2803
+ (0, react_dom_client.createRoot)(mount).render((0, react.createElement)(PairFailedNotice, { t }));
2804
+ }, 1500);
2805
+ return () => {
2806
+ window.clearTimeout(timer);
2807
+ };
2808
+ }, "remote-web-ui: failed-pair notice");
2809
+ }
2810
+ //#endregion
2811
+ exports.apply = apply;
2812
+ exports.inject = inject;
2813
+ return module.exports;
2814
+ }
2815
+ });
2816
+
2817
+ //# sourceMappingURL=client.js.map