@linxin666/dsh-remote-web-ui 0.3.23 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +13 -10
- package/README.zh.md +13 -10
- package/icon.svg +3 -0
- package/lib/client.js +396 -132
- package/lib/client.js.map +1 -1
- package/lib/index.js +604 -262
- package/lib/types/client/FenceNotice.d.ts.map +1 -1
- package/lib/types/client/FenceNotice.js +0 -3
- package/lib/types/client/PluginSettingsCard.d.ts +4 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -2
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +14 -9
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +13 -4
- package/lib/types/client/RemoteSettingsCard.d.ts +6 -6
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +14 -8
- package/lib/types/client/UpdateEntry.js +2 -2
- package/lib/types/client/UpdatePanel.js +2 -2
- package/lib/types/client/deep-link.js +2 -2
- package/lib/types/client/index.d.ts +16 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +56 -32
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +6 -8
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +143 -28
- package/lib/types/client/pair-api.d.ts +32 -3
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +44 -6
- package/lib/types/client/plugin-card-seat.d.ts +104 -0
- package/lib/types/client/plugin-card-seat.d.ts.map +1 -0
- package/lib/types/client/plugin-card-seat.js +143 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +18 -5
- package/lib/types/client/settings-form.d.ts +17 -17
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +30 -27
- package/lib/types/console-output.d.ts +41 -0
- package/lib/types/console-output.d.ts.map +1 -0
- package/lib/types/console-output.js +89 -0
- package/lib/types/http.d.ts +18 -1
- package/lib/types/http.d.ts.map +1 -1
- package/lib/types/http.js +22 -1
- package/lib/types/index.d.ts +19 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +91 -76
- package/lib/types/inner-auth.d.ts +3 -2
- package/lib/types/inner-auth.d.ts.map +1 -1
- package/lib/types/inner-auth.js +3 -2
- package/lib/types/pair-grant.d.ts +66 -0
- package/lib/types/pair-grant.d.ts.map +1 -0
- package/lib/types/pair-grant.js +95 -0
- package/lib/types/pairing.d.ts +44 -12
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +69 -20
- package/lib/types/pairing.persist.test.js +26 -0
- package/lib/types/posture.d.ts +11 -3
- package/lib/types/posture.d.ts.map +1 -1
- package/lib/types/posture.js +8 -3
- package/lib/types/remote-api.d.ts +10 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +34 -34
- package/lib/types/remote-channel-boot.d.ts +6 -1
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +16 -8
- package/lib/types/remote-channel-rules.d.ts +14 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +8 -0
- package/lib/types/remote-methods.d.ts +1 -1
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +1 -0
- package/lib/types/routes.d.ts +47 -5
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +160 -58
- package/lib/types/tunnel.d.ts +21 -6
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +58 -11
- package/lib/types/update.d.ts +4 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +74 -33
- package/package.json +23 -20
- package/src/client/FenceNotice.tsx +0 -2
- package/src/client/PluginSettingsCard.tsx +7 -2
- package/src/client/RemoteEntry.tsx +14 -9
- package/src/client/RemotePanel.tsx +17 -7
- package/src/client/RemoteSettingsCard.tsx +16 -9
- package/src/client/UpdateEntry.tsx +2 -2
- package/src/client/UpdatePanel.tsx +3 -3
- package/src/client/deep-link.ts +2 -2
- package/src/client/index.ts +70 -40
- package/src/client/locales.ts +6 -8
- package/src/client/mobile-adapt.ts +136 -25
- package/src/client/pair-api.ts +47 -7
- package/src/client/plugin-card-seat.ts +192 -0
- package/src/client/remote-channel.ts +19 -4
- package/src/client/remote.module.css +38 -0
- package/src/client/settings-form.ts +34 -31
- package/src/console-output.ts +97 -0
- package/src/http.ts +23 -1
- package/src/index.ts +102 -79
- package/src/inner-auth.ts +3 -2
- package/src/pair-grant.ts +131 -0
- package/src/pairing.persist.test.ts +36 -0
- package/src/pairing.ts +75 -24
- package/src/posture.ts +12 -4
- package/src/remote-api.ts +34 -41
- package/src/remote-channel-boot.ts +16 -8
- package/src/remote-channel-rules.ts +17 -0
- package/src/remote-methods.ts +1 -0
- package/src/routes.ts +165 -57
- package/src/tunnel.ts +56 -11
- package/src/update.ts +74 -29
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,mEAAmE;AACnE,eAAO,MAAM,EAAE
|
|
1
|
+
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoKmB,CAAA;AAElC,sCAAsC;AACtC,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA;AAEvC,wDAAwD;AACxD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAA;AAEvC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoKsB,CAAA"}
|
|
@@ -30,9 +30,9 @@ export const zh = {
|
|
|
30
30
|
'pair.linkLabel': '配对链接',
|
|
31
31
|
'pair.tokenLabel': '配对令牌',
|
|
32
32
|
'pair.dockerHint': 'Docker 或反向代理环境下,可直接复制此令牌并在目标设备配对页面输入。',
|
|
33
|
-
'pair.oneTimeHint': '
|
|
33
|
+
'pair.oneTimeHint': '链接含限时令牌;在令牌过期或刷新二维码之前,同一链接可为多台设备完成配对(每次配对各自生成独立设备会话)。',
|
|
34
34
|
'pair.failed.title': '配对失败',
|
|
35
|
-
'pair.failed.detail': '
|
|
35
|
+
'pair.failed.detail': '链接无效或已过期,请回到电脑端刷新二维码后重新扫码。',
|
|
36
36
|
'fence.unpaired.title': '此设备未配对,无法访问工作区数据',
|
|
37
37
|
'fence.unpaired.eyebrow': '需要设备配对',
|
|
38
38
|
'fence.unpaired.hint': '为保护工作区、会话与插件数据,远程电脑必须先通过主电脑授权。',
|
|
@@ -44,7 +44,6 @@ export const zh = {
|
|
|
44
44
|
'fence.unpaired.pairAction': '立即配对',
|
|
45
45
|
'fence.unpaired.pairing': '配对中…',
|
|
46
46
|
'fence.unpaired.tokenInvalid': '配对链接或 Token 无效已过期',
|
|
47
|
-
'fence.unpaired.tokenUsed': '该配对链接已被使用',
|
|
48
47
|
'fence.unpaired.tokenFailed': '配对失败,请检查网络或重新获取链接',
|
|
49
48
|
'fence.unpaired.footnote': '请勿使用他人提供的配对链接;管理员可随时取消此设备的授权。',
|
|
50
49
|
'posture.exposed': '/api 通道对未配对设备敞开',
|
|
@@ -71,7 +70,7 @@ export const zh = {
|
|
|
71
70
|
'relay.registering': '正在把临时隧道同步到固定域名(<id>.dsh-market.com)…',
|
|
72
71
|
'relay.failed': '固定域名同步失败:{error}。正在自动重试,期间二维码可能使用临时地址。',
|
|
73
72
|
'close.label': '关闭远程访问面板',
|
|
74
|
-
//
|
|
73
|
+
// 插件设置卡片(家族 list 席位,或官方 plugins.bundle.config 席位)。
|
|
75
74
|
'settings.title': '远程访问设置',
|
|
76
75
|
'settings.description': '配对安全与设备限额。',
|
|
77
76
|
'settings.enabled': '启用远程访问',
|
|
@@ -197,9 +196,9 @@ export const en = {
|
|
|
197
196
|
'pair.linkLabel': 'Pairing link',
|
|
198
197
|
'pair.tokenLabel': 'Pairing token',
|
|
199
198
|
'pair.dockerHint': 'In Docker or reverse proxy environments, copy this token to pair directly on the target device.',
|
|
200
|
-
'pair.oneTimeHint': 'The link carries
|
|
199
|
+
'pair.oneTimeHint': 'The link carries a time-limited token: until it expires or the QR is refreshed, the same link can pair several devices, each with its own device session.',
|
|
201
200
|
'pair.failed.title': 'Pairing failed',
|
|
202
|
-
'pair.failed.detail': 'The link is invalid or
|
|
201
|
+
'pair.failed.detail': 'The link is invalid or has expired. Refresh the QR code on your computer and scan again.',
|
|
203
202
|
'fence.unpaired.title': 'This device is not paired and cannot reach workspace data',
|
|
204
203
|
'fence.unpaired.eyebrow': 'Device pairing required',
|
|
205
204
|
'fence.unpaired.hint': 'To protect workspace, session, and plugin data, a remote computer must first be authorized by the primary computer.',
|
|
@@ -211,7 +210,6 @@ export const en = {
|
|
|
211
210
|
'fence.unpaired.pairAction': 'Pair Now',
|
|
212
211
|
'fence.unpaired.pairing': 'Pairing…',
|
|
213
212
|
'fence.unpaired.tokenInvalid': 'Pairing link or token is invalid or expired',
|
|
214
|
-
'fence.unpaired.tokenUsed': 'This pairing link has already been used',
|
|
215
213
|
'fence.unpaired.tokenFailed': 'Pairing failed, please check network or issue a new link',
|
|
216
214
|
'fence.unpaired.footnote': 'Do not use pairing links from people you do not trust. An administrator can revoke this device at any time.',
|
|
217
215
|
'posture.exposed': 'The /api channel is open to unpaired devices',
|
|
@@ -238,7 +236,7 @@ export const en = {
|
|
|
238
236
|
'relay.registering': 'Syncing the ephemeral tunnel to the stable hostname (<id>.dsh-market.com)…',
|
|
239
237
|
'relay.failed': 'Stable-hostname sync failed: {error}. Retrying automatically; the QR may use the ephemeral address meanwhile.',
|
|
240
238
|
'close.label': 'Close remote access panel',
|
|
241
|
-
// Plugin settings card (the
|
|
239
|
+
// Plugin settings card (the family list seat, or the official bundle-config seat).
|
|
242
240
|
'settings.title': 'Remote access settings',
|
|
243
241
|
'settings.description': 'Pairing security and device limits.',
|
|
244
242
|
'settings.enabled': 'Enable remote access',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-adapt.d.ts","sourceRoot":"","sources":["../../../src/client/mobile-adapt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,yDAAyD;IACzD,aAAa,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAClC,yDAAyD;IACzD,YAAY,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IACjC;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,SAAS,KAAK,MAAM,CAAC,GAAG,IAAI,CAAA;IAC9C,wEAAwE;IACxE,UAAU,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC7B,4GAA4G;IAC5G,iBAAiB,IAAI,IAAI,CAAA;CAC1B;
|
|
1
|
+
{"version":3,"file":"mobile-adapt.d.ts","sourceRoot":"","sources":["../../../src/client/mobile-adapt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,yDAAyD;IACzD,aAAa,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAClC,yDAAyD;IACzD,YAAY,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IACjC;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,SAAS,KAAK,MAAM,CAAC,GAAG,IAAI,CAAA;IAC9C,wEAAwE;IACxE,UAAU,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC7B,4GAA4G;IAC5G,iBAAiB,IAAI,IAAI,CAAA;CAC1B;AAmRD;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAq5BvC"}
|
|
@@ -40,6 +40,12 @@ const EFFORT_BTN_ID = 'dshRemoteEffortPick';
|
|
|
40
40
|
const COMPACT_CLASS = 'dsh-remote-compact-picker';
|
|
41
41
|
/** Body class while the header actions are seated in the tabs row. */
|
|
42
42
|
const HEADER_SEATED_CLASS = 'dsh-remote-header-seated';
|
|
43
|
+
/**
|
|
44
|
+
* Width the seated header actions paint over the tabs row, as a CSS variable
|
|
45
|
+
* on <html>. The row caps its own width with it (see the v80 rules) instead of
|
|
46
|
+
* reserving the space with padding.
|
|
47
|
+
*/
|
|
48
|
+
const HEADER_RESERVE_VAR = '--dsh-remote-header-actions-reserve';
|
|
43
49
|
/** Locale-dependent fast path for the official picker cells (zh/en). */
|
|
44
50
|
const PICKER_CELL_PATTERN = {
|
|
45
51
|
model: /模型|Model/, // i18n-allow: matches official picker cell text, not plugin copy
|
|
@@ -228,6 +234,20 @@ const ADAPT_CSS = [
|
|
|
228
234
|
// [class$="_tab"] misses the active tab (its class ends in "_tabActive")
|
|
229
235
|
// — containment so BOTH tabs match.
|
|
230
236
|
'[class$="_header"] [class$="_tabs"] [class*="_tab"]{font-size:12px;white-space:nowrap}',
|
|
237
|
+
// v80: the tabs row must give the painted actions their width up on its own.
|
|
238
|
+
// The v67 padding reservation could not do it: the row is a nowrap flex
|
|
239
|
+
// container, and once the tab labels plus their gaps overflow the reduced
|
|
240
|
+
// content box the flex items spill over the padding box, so the trailing
|
|
241
|
+
// tabs render underneath the actions (reported on a phone: a long agent
|
|
242
|
+
// preset label covered the last tabs). Cap the row against the reserve and
|
|
243
|
+
// let the labels scroll instead. The value rides a CSS variable on <html>
|
|
244
|
+
// because the update lands before React re-creates the row node, and an
|
|
245
|
+
// inline reservation on that node is dropped until the next sync tick.
|
|
246
|
+
// The 2px bottom padding keeps the active-tab underline inside the clipping
|
|
247
|
+
// box; the matching negative margin keeps the header height official.
|
|
248
|
+
`body.${HEADER_SEATED_CLASS} [class$="_header"] [class$="_tabs"]{max-width:max(0px,calc(100% - var(${HEADER_RESERVE_VAR},0px)));overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-webkit-overflow-scrolling:touch;padding-bottom:2px;margin-bottom:-2px}`,
|
|
249
|
+
`body.${HEADER_SEATED_CLASS} [class$="_header"] [class$="_tabs"]::-webkit-scrollbar{display:none}`,
|
|
250
|
+
`body.${HEADER_SEATED_CLASS} [class$="_header"] [class$="_tabs"] > [class*="_tab"]{flex:0 0 auto}`,
|
|
231
251
|
// Mobile scope: hide the plugin surfaces that do not fit a phone — the
|
|
232
252
|
// right-hand details column and every desktop-oriented tool surface. The
|
|
233
253
|
// list keys on the L2 semantic roots (data-dsh-plugin, ownership stays
|
|
@@ -320,7 +340,7 @@ export function startMobileAdapt() {
|
|
|
320
340
|
* rail compaction) while the body class stays.
|
|
321
341
|
*/
|
|
322
342
|
function ensureAdaptStyle() {
|
|
323
|
-
if (
|
|
343
|
+
if (nodeOf(`style[data-plugin-css="${ADAPT_CSS_ID}"]`) !== null)
|
|
324
344
|
return;
|
|
325
345
|
const tag = document.createElement('style');
|
|
326
346
|
tag.dataset.plugin = 'remote-web-ui';
|
|
@@ -376,6 +396,9 @@ export function startMobileAdapt() {
|
|
|
376
396
|
}
|
|
377
397
|
if (whaleEl !== null)
|
|
378
398
|
whaleEl.style.display = 'none';
|
|
399
|
+
// The next apply() is a hidden-to-shown transition again: without this the
|
|
400
|
+
// whale would keep its pre-revert position state and skip the clamp.
|
|
401
|
+
whaleShown = false;
|
|
379
402
|
setWhaleTimer(false);
|
|
380
403
|
if (whaleObserver !== null) {
|
|
381
404
|
whaleObserver.disconnect();
|
|
@@ -447,7 +470,7 @@ export function startMobileAdapt() {
|
|
|
447
470
|
function syncCompactPicker() {
|
|
448
471
|
if (!active)
|
|
449
472
|
return;
|
|
450
|
-
const tools =
|
|
473
|
+
const tools = nodeOf('[class$="_composerSeat"] [class$="_tools"]');
|
|
451
474
|
const trigger = tools?.parentElement?.querySelector('[class$="_triggerEffort"]')?.parentElement;
|
|
452
475
|
if (tools === null || trigger === null) {
|
|
453
476
|
removeCompactPicker();
|
|
@@ -626,22 +649,28 @@ export function startMobileAdapt() {
|
|
|
626
649
|
whaleEl = whale;
|
|
627
650
|
document.body.appendChild(whale);
|
|
628
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* Write one row's recorded official draggable state back onto it.
|
|
654
|
+
* @param row - the tracked row element.
|
|
655
|
+
* @param original - the recorded attribute value, or null when it was absent.
|
|
656
|
+
*/
|
|
657
|
+
function restoreRowDragState(row, original) {
|
|
658
|
+
if (original === null)
|
|
659
|
+
row.removeAttribute('draggable');
|
|
660
|
+
else
|
|
661
|
+
row.setAttribute('draggable', original);
|
|
662
|
+
}
|
|
629
663
|
/**
|
|
630
664
|
* Restore the official draggable state this layer overrode while active.
|
|
631
665
|
* Rows are React-owned and may have been re-created meanwhile, so only the
|
|
632
|
-
* tracked elements are touched; a detached element is
|
|
666
|
+
* tracked elements are touched; a detached element is written back too
|
|
667
|
+
* (harmless, and it keeps the entry prunable at every tick).
|
|
633
668
|
*/
|
|
634
669
|
function restoreRowDrag() {
|
|
635
670
|
if (dragOverridden.size === 0)
|
|
636
671
|
return;
|
|
637
|
-
for (const [row, original] of dragOverridden)
|
|
638
|
-
|
|
639
|
-
continue;
|
|
640
|
-
if (original === null)
|
|
641
|
-
row.removeAttribute('draggable');
|
|
642
|
-
else
|
|
643
|
-
row.setAttribute('draggable', original);
|
|
644
|
-
}
|
|
672
|
+
for (const [row, original] of dragOverridden)
|
|
673
|
+
restoreRowDragState(row, original);
|
|
645
674
|
dragOverridden.clear();
|
|
646
675
|
}
|
|
647
676
|
/** Restore a dragged position when the whale becomes visible again. */
|
|
@@ -680,6 +709,44 @@ export function startMobileAdapt() {
|
|
|
680
709
|
row.setAttribute('draggable', 'false');
|
|
681
710
|
}
|
|
682
711
|
}
|
|
712
|
+
// React replaces rows as the session list re-renders; a detached row can
|
|
713
|
+
// never be restored by a later revert, so keeping it here would retain the
|
|
714
|
+
// whole detached subtree (and its children) for the page lifetime. Its
|
|
715
|
+
// official state goes back on the node itself and the entry is dropped:
|
|
716
|
+
// a node React re-attaches is recorded again on the next tick.
|
|
717
|
+
if (dragOverridden.size === 0)
|
|
718
|
+
return;
|
|
719
|
+
for (const [row, original] of dragOverridden) {
|
|
720
|
+
if (row.isConnected)
|
|
721
|
+
continue;
|
|
722
|
+
restoreRowDragState(row, original);
|
|
723
|
+
dragOverridden.delete(row);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Official nodes resolved through the document and cached while they stay
|
|
728
|
+
* connected. The sync tick runs every 600ms for the page lifetime, and these
|
|
729
|
+
* selectors (suffix class matches, :has()) cannot use Blink's fast paths, so
|
|
730
|
+
* every miss walks the whole mounted DOM — the official chat keeps the whole
|
|
731
|
+
* conversation mounted, i.e. tens of thousands of elements. React replaces a
|
|
732
|
+
* node on a major re-render, which the isConnected guard detects.
|
|
733
|
+
*/
|
|
734
|
+
const nodeCache = new Map();
|
|
735
|
+
function nodeOf(selector) {
|
|
736
|
+
const cached = nodeCache.get(selector);
|
|
737
|
+
if (cached !== undefined && cached.isConnected)
|
|
738
|
+
return cached;
|
|
739
|
+
const found = document.querySelector(selector);
|
|
740
|
+
if (found === null)
|
|
741
|
+
nodeCache.delete(selector);
|
|
742
|
+
else
|
|
743
|
+
nodeCache.set(selector, found);
|
|
744
|
+
return found;
|
|
745
|
+
}
|
|
746
|
+
/** The official application frame, through the same cached lookup. */
|
|
747
|
+
function frameEl() {
|
|
748
|
+
const frame = nodeOf(APP_FRAME_SELECTOR);
|
|
749
|
+
return frame instanceof HTMLElement ? frame : null;
|
|
683
750
|
}
|
|
684
751
|
function syncWhale() {
|
|
685
752
|
// Keep the stylesheet present while the layer is active (the tick runs
|
|
@@ -692,8 +759,8 @@ export function startMobileAdapt() {
|
|
|
692
759
|
whaleEl.style.display = 'none';
|
|
693
760
|
return;
|
|
694
761
|
}
|
|
695
|
-
const collapsed =
|
|
696
|
-
const overlayUp =
|
|
762
|
+
const collapsed = frameEl()?.hasAttribute('data-sidebar-collapsed') === true;
|
|
763
|
+
const overlayUp = nodeOf('[class$="_overlay"]') !== null;
|
|
697
764
|
const show = collapsed && !overlayUp;
|
|
698
765
|
// Restore on every hidden-to-shown transition — including the very
|
|
699
766
|
// first show on a fresh page load, where the inline display is still
|
|
@@ -723,7 +790,7 @@ export function startMobileAdapt() {
|
|
|
723
790
|
function seatHeaderActions() {
|
|
724
791
|
if (!active)
|
|
725
792
|
return;
|
|
726
|
-
const header =
|
|
793
|
+
const header = nodeOf('[class$="_header"]');
|
|
727
794
|
const tabs = header !== null ? header.querySelector('[class$="_tabs"]') : null;
|
|
728
795
|
const actions = header !== null ? header.querySelector('[class$="_titleCluster"] [class$="_headerActions"]') : null;
|
|
729
796
|
// The actions node is NOT moved: it is a React-owned host node whose badge
|
|
@@ -745,7 +812,7 @@ export function startMobileAdapt() {
|
|
|
745
812
|
function alignActionsText() {
|
|
746
813
|
if (!active)
|
|
747
814
|
return;
|
|
748
|
-
const header =
|
|
815
|
+
const header = nodeOf('[class$="_header"]');
|
|
749
816
|
if (header === null)
|
|
750
817
|
return;
|
|
751
818
|
const tabs = header.querySelector('[class$="_tabs"]');
|
|
@@ -775,9 +842,14 @@ export function startMobileAdapt() {
|
|
|
775
842
|
let dx = translate !== null ? parseFloat(translate[1] ?? '0') : 0;
|
|
776
843
|
let dy = translate !== null ? parseFloat(translate[2] ?? '0') : 0;
|
|
777
844
|
const actionsRect = actions.getBoundingClientRect();
|
|
778
|
-
|
|
779
|
-
//
|
|
780
|
-
|
|
845
|
+
// Horizontal: the actions' right edge lands on the header's content edge,
|
|
846
|
+
// NOT on the tabs row's right edge. The row caps its own width against the
|
|
847
|
+
// reserve below (v80), so anchoring to the row would let the pair walk
|
|
848
|
+
// leftwards on every measurement.
|
|
849
|
+
const headerRect = header.getBoundingClientRect();
|
|
850
|
+
const headerStyle = getComputedStyle(header);
|
|
851
|
+
const contentRight = headerRect.right - (parseFloat(headerStyle.paddingRight) || 0) - (parseFloat(headerStyle.borderRightWidth) || 0);
|
|
852
|
+
const rightDiff = contentRight - actionsRect.right;
|
|
781
853
|
if (Math.abs(rightDiff) >= 0.5)
|
|
782
854
|
dx = Math.round((dx + rightDiff) * 10) / 10;
|
|
783
855
|
// Vertical: the actions' text bottom lands on the tab text bottom.
|
|
@@ -798,18 +870,27 @@ export function startMobileAdapt() {
|
|
|
798
870
|
const next = `translate(${dx}px, ${dy}px)`;
|
|
799
871
|
if (actions.style.transform !== next)
|
|
800
872
|
actions.style.transform = next;
|
|
801
|
-
// Reserve the painted width so the tab labels never
|
|
873
|
+
// Reserve the painted width on the row itself, so the tab labels never
|
|
874
|
+
// slide under it. The value rides a CSS variable on <html>: React
|
|
875
|
+
// recreates the row node on re-render, which would drop an inline
|
|
876
|
+
// reservation until the next sync tick, and until then the labels render
|
|
877
|
+
// underneath the painted actions.
|
|
802
878
|
const reserve = `${Math.ceil(actionsRect.width) + 8}px`;
|
|
803
|
-
|
|
804
|
-
|
|
879
|
+
const rootStyle = document.documentElement.style;
|
|
880
|
+
if (rootStyle.getPropertyValue(HEADER_RESERVE_VAR) !== reserve)
|
|
881
|
+
rootStyle.setProperty(HEADER_RESERVE_VAR, reserve);
|
|
805
882
|
}
|
|
806
883
|
function unseatHeaderActions() {
|
|
807
884
|
document.body.classList.remove(HEADER_SEATED_CLASS);
|
|
885
|
+
document.documentElement.style.removeProperty(HEADER_RESERVE_VAR);
|
|
808
886
|
const header = document.querySelector('[class$="_header"]');
|
|
809
887
|
const tabs = header !== null ? header.querySelector('[class$="_tabs"]') : null;
|
|
810
888
|
const actions = header !== null ? header.querySelector('[class$="_titleCluster"] [class$="_headerActions"]') : null;
|
|
811
889
|
if (actions instanceof HTMLElement)
|
|
812
890
|
actions.style.transform = '';
|
|
891
|
+
// The v67 padding reservation is gone with the v80 cap; clearing it keeps a
|
|
892
|
+
// profile that hot-updated from an older build from keeping a stale inline
|
|
893
|
+
// width on the row.
|
|
813
894
|
if (tabs instanceof HTMLElement)
|
|
814
895
|
tabs.style.paddingRight = '';
|
|
815
896
|
if (headerObserver !== null) {
|
|
@@ -856,6 +937,12 @@ export function startMobileAdapt() {
|
|
|
856
937
|
if (isMobilePortrait()) {
|
|
857
938
|
apply();
|
|
858
939
|
ensureWhaleObserver();
|
|
940
|
+
// A viewport shrink (split-screen, foldable, rotation) can leave the
|
|
941
|
+
// whale — the only portrait sidebar entry — parked beyond the new right
|
|
942
|
+
// edge while it stays visible; re-clamp it here. Skipped mid-drag so a
|
|
943
|
+
// resize cannot yank the button away from the finger.
|
|
944
|
+
if (active && drag === null)
|
|
945
|
+
applyWhalePos();
|
|
859
946
|
}
|
|
860
947
|
else {
|
|
861
948
|
revert();
|
|
@@ -967,14 +1054,18 @@ export function startMobileAdapt() {
|
|
|
967
1054
|
}
|
|
968
1055
|
const t = e.target;
|
|
969
1056
|
const editable = t instanceof HTMLElement && t.isContentEditable;
|
|
970
|
-
if (t instanceof Element && (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT' || editable || t.closest(`#${WHALE_ID}`) !== null ||
|
|
1057
|
+
if (t instanceof Element && (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT' || editable || t.closest(`#${WHALE_ID}`) !== null || t.closest('table, [class$="_table"], [class$="_tablePane"]') !== null)) {
|
|
971
1058
|
swipeTouch = null;
|
|
972
1059
|
return;
|
|
973
1060
|
}
|
|
974
1061
|
const ct = e.changedTouches[0];
|
|
975
1062
|
if (ct === undefined)
|
|
976
1063
|
return;
|
|
977
|
-
|
|
1064
|
+
// The horizontally-scrollable veto is judged at release (see touchend):
|
|
1065
|
+
// evaluating it here forced a style recalc plus a synchronous layout on
|
|
1066
|
+
// every tap and scroll-start while the page was dirty, for a gesture that
|
|
1067
|
+
// mostly never becomes a swipe.
|
|
1068
|
+
swipeTouch = { x: ct.clientX, y: ct.clientY, id: ct.identifier, el: t instanceof Element ? t : null };
|
|
978
1069
|
}, { capture: true, passive: true });
|
|
979
1070
|
document.addEventListener('touchend', (e) => {
|
|
980
1071
|
if (swipeTouch === null)
|
|
@@ -987,15 +1078,21 @@ export function startMobileAdapt() {
|
|
|
987
1078
|
return;
|
|
988
1079
|
const dx = ct.clientX - swipeTouch.x;
|
|
989
1080
|
const dy = ct.clientY - swipeTouch.y;
|
|
1081
|
+
const start = swipeTouch;
|
|
990
1082
|
swipeTouch = null;
|
|
991
1083
|
if (!active)
|
|
992
1084
|
return;
|
|
993
1085
|
if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy) * 1.5)
|
|
994
1086
|
return;
|
|
995
|
-
|
|
1087
|
+
// A gesture that started inside a horizontally scrollable container belongs
|
|
1088
|
+
// to that container, not to the sidebar. Judged now, only for a gesture
|
|
1089
|
+
// that already qualified as a swipe.
|
|
1090
|
+
if (start.el !== null && insideHScrollable(start.el))
|
|
1091
|
+
return;
|
|
1092
|
+
const frame = frameEl();
|
|
996
1093
|
if (frame === null)
|
|
997
1094
|
return;
|
|
998
|
-
if (
|
|
1095
|
+
if (nodeOf('[class$="_overlay"], [class$="_dialog"], [class$="_menu"], [class*="_portal"]') !== null)
|
|
999
1096
|
return;
|
|
1000
1097
|
const collapsed = frame.hasAttribute('data-sidebar-collapsed');
|
|
1001
1098
|
if (dx < 0 && !collapsed)
|
|
@@ -1156,11 +1253,25 @@ export function startMobileAdapt() {
|
|
|
1156
1253
|
return isComposerField(el) ? el : null;
|
|
1157
1254
|
};
|
|
1158
1255
|
const lanOrigFocus = HTMLElement.prototype.focus;
|
|
1159
|
-
|
|
1256
|
+
const patchedFocus = function (options) {
|
|
1160
1257
|
if (active && isComposerField(this) && Date.now() - lastComposerTap >= 800)
|
|
1161
1258
|
return;
|
|
1162
1259
|
lanOrigFocus.call(this, options);
|
|
1163
1260
|
};
|
|
1261
|
+
/** (Re-)install the composer-focus guard (see setEnabled). */
|
|
1262
|
+
const installFocusPatch = () => {
|
|
1263
|
+
HTMLElement.prototype.focus = patchedFocus;
|
|
1264
|
+
};
|
|
1265
|
+
/**
|
|
1266
|
+
* Remove the guard when the layer is disabled. A patch another plugin
|
|
1267
|
+
* installed after ours is left alone (identity check), so disabling this
|
|
1268
|
+
* layer never removes someone else's behavior.
|
|
1269
|
+
*/
|
|
1270
|
+
const restoreFocusPatch = () => {
|
|
1271
|
+
if (HTMLElement.prototype.focus === patchedFocus)
|
|
1272
|
+
HTMLElement.prototype.focus = lanOrigFocus;
|
|
1273
|
+
};
|
|
1274
|
+
installFocusPatch();
|
|
1164
1275
|
document.addEventListener('pointerdown', (e) => {
|
|
1165
1276
|
if (!active)
|
|
1166
1277
|
return;
|
|
@@ -1177,10 +1288,14 @@ export function startMobileAdapt() {
|
|
|
1177
1288
|
translate: null,
|
|
1178
1289
|
setEnabled(on) {
|
|
1179
1290
|
adaptEnabled = on;
|
|
1180
|
-
if (on)
|
|
1291
|
+
if (on) {
|
|
1292
|
+
installFocusPatch();
|
|
1181
1293
|
evaluate();
|
|
1182
|
-
|
|
1294
|
+
}
|
|
1295
|
+
else {
|
|
1183
1296
|
revert();
|
|
1297
|
+
restoreFocusPatch();
|
|
1298
|
+
}
|
|
1184
1299
|
},
|
|
1185
1300
|
flushCloseDetails() {
|
|
1186
1301
|
// The first apply() ran before any wiring existed, so its
|
|
@@ -39,7 +39,7 @@ export declare function readPairGatePolicy(): Promise<PairGatePolicy>;
|
|
|
39
39
|
/** accept() refusal codes. */
|
|
40
40
|
export type AcceptFailure = {
|
|
41
41
|
ok: false;
|
|
42
|
-
code: 'invalid' | '
|
|
42
|
+
code: 'invalid' | 'forbidden';
|
|
43
43
|
};
|
|
44
44
|
/** One auto-tunnel status frame (absent while the feature is off). */
|
|
45
45
|
export interface TunnelStatusFrame {
|
|
@@ -116,8 +116,20 @@ export declare function stopPair(): Promise<void>;
|
|
|
116
116
|
* @param deviceId - the session id of the row to drop.
|
|
117
117
|
*/
|
|
118
118
|
export declare function revokePair(deviceId: string): Promise<void>;
|
|
119
|
-
/**
|
|
120
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Presence heartbeat from a paired phone.
|
|
121
|
+
* @returns the response status, so the caller can stop polling once the server
|
|
122
|
+
* proves this page is not paired (see {@link shouldStopHeartbeat}).
|
|
123
|
+
*/
|
|
124
|
+
export declare function sendHeartbeat(): Promise<number>;
|
|
125
|
+
/**
|
|
126
|
+
* Whether a heartbeat answer means "this page can never be accepted again" and
|
|
127
|
+
* the 10 s wake source should stop: 401 (unpaired, or the device was revoked)
|
|
128
|
+
* and 403 (the fence refused it) are permanent for this page, while a network
|
|
129
|
+
* error or a 5xx is transient and keeps the cadence.
|
|
130
|
+
* @param status - the heartbeat response status.
|
|
131
|
+
*/
|
|
132
|
+
export declare function shouldStopHeartbeat(status: number): boolean;
|
|
121
133
|
/** Whether the current page URL carries a pairing token. */
|
|
122
134
|
export declare function readPairParams(search: string): {
|
|
123
135
|
pair?: string;
|
|
@@ -148,8 +160,25 @@ export interface LanBindFrame {
|
|
|
148
160
|
/** true when the running bind has not caught up with the toggle yet. */
|
|
149
161
|
pendingRestart?: boolean;
|
|
150
162
|
}
|
|
163
|
+
/** A failed LAN-bind read, carrying the HTTP status when the server answered. */
|
|
164
|
+
export declare class LanBindStatusError extends Error {
|
|
165
|
+
readonly status: number | undefined;
|
|
166
|
+
/**
|
|
167
|
+
* @param status - the response status, or undefined when the request failed
|
|
168
|
+
* before a response (network error).
|
|
169
|
+
* @param message - the diagnostic message.
|
|
170
|
+
*/
|
|
171
|
+
constructor(status: number | undefined, message: string);
|
|
172
|
+
}
|
|
151
173
|
/** Read the LAN-bind facts (loopback-only endpoint). */
|
|
152
174
|
export declare function readLanBindStatus(): Promise<LanBindFrame>;
|
|
175
|
+
/**
|
|
176
|
+
* Whether a LAN-bind status failure means this origin can never read the
|
|
177
|
+
* endpoint (401/403 from the loopback-only fence), so a poll should stop
|
|
178
|
+
* instead of retrying a known refusal; a transient error keeps the cadence.
|
|
179
|
+
* @param status - the response status, or undefined for a network failure.
|
|
180
|
+
*/
|
|
181
|
+
export declare function shouldStopLanBindPoll(status: number | undefined): boolean;
|
|
153
182
|
/** Human-readable expiry clock, e.g. "10:35". */
|
|
154
183
|
export declare function formatClock(epochMs: number): string;
|
|
155
184
|
/** Calendar + clock for last-seen timestamps, e.g. "2026-08-19 10:35". */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pair-api.d.ts","sourceRoot":"","sources":["../../../src/client/pair-api.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wBAAwB;AACxB,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,IAAI,CAAA;IACR,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,cAAc,CAAA;CACrB;AAED,uEAAuE;AACvE,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,iBAAiB,CAAA;CACxB;AAED,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,WAAW,CAAA;CAClB;AAED,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,gBAAgB,GAAG,mBAAmB,GAAG,qBAAqB,CAAA;AAExG,oEAAoE;AACpE,MAAM,WAAW,cAAc;IAC7B,oBAAoB,EAAE,OAAO,CAAA;CAC9B;AAED,0DAA0D;AAC1D,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,cAAc,CAAC,CAQlE;AAED,8BAA8B;AAC9B,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,SAAS,GAAG,
|
|
1
|
+
{"version":3,"file":"pair-api.d.ts","sourceRoot":"","sources":["../../../src/client/pair-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -57,10 +57,10 @@ export async function acceptPair(token) {
|
|
|
57
57
|
});
|
|
58
58
|
if (response.ok)
|
|
59
59
|
return { ok: true };
|
|
60
|
-
|
|
60
|
+
// 409 is the legacy "already used" answer a pre-bearer-token host could send;
|
|
61
|
+
// it means the same thing to the caller as a 404.
|
|
62
|
+
if (response.status === 404 || response.status === 409)
|
|
61
63
|
return { ok: false, code: 'invalid' };
|
|
62
|
-
if (response.status === 409)
|
|
63
|
-
return { ok: false, code: 'used' };
|
|
64
64
|
return { ok: false, code: 'forbidden' };
|
|
65
65
|
}
|
|
66
66
|
/** Revoke mobile access (paired devices + the current token). */
|
|
@@ -84,9 +84,24 @@ export async function revokePair(deviceId) {
|
|
|
84
84
|
if (!response.ok)
|
|
85
85
|
throw new Error(`remote-web-ui: revoke failed with ${String(response.status)}`);
|
|
86
86
|
}
|
|
87
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Presence heartbeat from a paired phone.
|
|
89
|
+
* @returns the response status, so the caller can stop polling once the server
|
|
90
|
+
* proves this page is not paired (see {@link shouldStopHeartbeat}).
|
|
91
|
+
*/
|
|
88
92
|
export async function sendHeartbeat() {
|
|
89
|
-
await fetch('/api/pair/heartbeat', { method: 'POST' });
|
|
93
|
+
const response = await fetch('/api/pair/heartbeat', { method: 'POST' });
|
|
94
|
+
return response.status;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Whether a heartbeat answer means "this page can never be accepted again" and
|
|
98
|
+
* the 10 s wake source should stop: 401 (unpaired, or the device was revoked)
|
|
99
|
+
* and 403 (the fence refused it) are permanent for this page, while a network
|
|
100
|
+
* error or a 5xx is transient and keeps the cadence.
|
|
101
|
+
* @param status - the heartbeat response status.
|
|
102
|
+
*/
|
|
103
|
+
export function shouldStopHeartbeat(status) {
|
|
104
|
+
return status === 401 || status === 403;
|
|
90
105
|
}
|
|
91
106
|
/** Whether the current page URL carries a pairing token. */
|
|
92
107
|
export function readPairParams(search) {
|
|
@@ -104,13 +119,36 @@ export function stripParam(name) {
|
|
|
104
119
|
url.searchParams.delete(name);
|
|
105
120
|
return url.search;
|
|
106
121
|
}
|
|
122
|
+
/** A failed LAN-bind read, carrying the HTTP status when the server answered. */
|
|
123
|
+
export class LanBindStatusError extends Error {
|
|
124
|
+
status;
|
|
125
|
+
/**
|
|
126
|
+
* @param status - the response status, or undefined when the request failed
|
|
127
|
+
* before a response (network error).
|
|
128
|
+
* @param message - the diagnostic message.
|
|
129
|
+
*/
|
|
130
|
+
constructor(status, message) {
|
|
131
|
+
super(message);
|
|
132
|
+
this.status = status;
|
|
133
|
+
this.name = 'LanBindStatusError';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
107
136
|
/** Read the LAN-bind facts (loopback-only endpoint). */
|
|
108
137
|
export async function readLanBindStatus() {
|
|
109
138
|
const response = await fetch('/api/pair/lan-bind');
|
|
110
139
|
if (!response.ok)
|
|
111
|
-
throw new
|
|
140
|
+
throw new LanBindStatusError(response.status, `remote-web-ui: lan-bind status failed with ${String(response.status)}`);
|
|
112
141
|
return await response.json();
|
|
113
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Whether a LAN-bind status failure means this origin can never read the
|
|
145
|
+
* endpoint (401/403 from the loopback-only fence), so a poll should stop
|
|
146
|
+
* instead of retrying a known refusal; a transient error keeps the cadence.
|
|
147
|
+
* @param status - the response status, or undefined for a network failure.
|
|
148
|
+
*/
|
|
149
|
+
export function shouldStopLanBindPoll(status) {
|
|
150
|
+
return status === 401 || status === 403;
|
|
151
|
+
}
|
|
114
152
|
/** Human-readable expiry clock, e.g. "10:35". */
|
|
115
153
|
export function formatClock(epochMs) {
|
|
116
154
|
const date = new Date(epochMs);
|