dsh-mobile 0.6.2 → 0.6.3

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 (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/CONTRIBUTORS.md +11 -4
  3. package/README.en.md +11 -7
  4. package/README.md +11 -7
  5. package/SECURITY.md +3 -2
  6. package/apps/mobile/README.md +14 -2
  7. package/apps/mobile/release.json +3 -3
  8. package/cordis.patch.yml +2 -1
  9. package/docs/ATTACH_EXISTING_FRPS.md +1 -1
  10. package/docs/CADDY_MANAGED.en.md +15 -1
  11. package/docs/CADDY_MANAGED.md +15 -1
  12. package/docs/CLOUDFLARE_TUNNEL.en.md +2 -2
  13. package/docs/CLOUDFLARE_TUNNEL.md +3 -3
  14. package/docs/EXTENSION_WORKERS.en.md +49 -0
  15. package/docs/EXTENSION_WORKERS.md +49 -0
  16. package/docs/MOBILE_COMPOSER_TOOLBAR.en.md +19 -0
  17. package/docs/MOBILE_COMPOSER_TOOLBAR.md +19 -0
  18. package/docs/README.md +2 -0
  19. package/docs/SELF_HOSTED_FRP.en.md +1 -1
  20. package/docs/SELF_HOSTED_FRP.md +1 -1
  21. package/docs/TRUSTED_NETWORKS.en.md +1 -1
  22. package/docs/TRUSTED_NETWORKS.md +1 -1
  23. package/lib/cli.js +1 -0
  24. package/lib/cli.js.map +1 -1
  25. package/lib/client.js +864 -97
  26. package/lib/client.js.map +1 -1
  27. package/lib/extension-worker-runtime.mjs +1646 -0
  28. package/lib/extension-worker-runtime.mjs.map +1 -0
  29. package/lib/index.d.mts +199 -7
  30. package/lib/index.mjs +1594 -533
  31. package/lib/index.mjs.map +1 -1
  32. package/lib/mobile-layout.js.map +1 -1
  33. package/package.json +15 -6
  34. package/packages/question-fixes/LICENSE +201 -0
  35. package/packages/question-fixes/package.json +9 -2
  36. package/node_modules/dsh-mobile-question-fixes/lib/client.js +0 -78
  37. package/node_modules/dsh-mobile-question-fixes/lib/client.js.map +0 -1
  38. package/node_modules/dsh-mobile-question-fixes/lib/index.mjs +0 -14
  39. package/node_modules/dsh-mobile-question-fixes/lib/index.mjs.map +0 -1
  40. package/node_modules/dsh-mobile-question-fixes/package.json +0 -25
package/lib/client.js CHANGED
@@ -6,6 +6,59 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  //#region src/client-messages.ts
9
+ const storageMessages = {
10
+ en: {
11
+ funnelStateInvalid: "The private Tailscale state folder cannot be accessed safely. Check its permissions or directory links; reinstalling the component is not required.",
12
+ componentPathUnsafe: "The component folder contains an unsafe link or cannot be accessed. Check the directory and permissions, then retry."
13
+ },
14
+ zh: {
15
+ funnelStateInvalid: "Tailscale 私有登录目录无法安全访问。请检查目录权限或链接,不必重新安装组件。",
16
+ componentPathUnsafe: "组件目录包含不安全的链接或无法访问。请检查目录和权限后重试。"
17
+ },
18
+ it: {
19
+ funnelStateInvalid: "La cartella privata Tailscale non è accessibile in sicurezza. Controlla permessi o collegamenti; non occorre reinstallare il componente.",
20
+ componentPathUnsafe: "La cartella del componente contiene un collegamento non sicuro o non è accessibile. Controlla directory e permessi, poi riprova."
21
+ }
22
+ };
23
+ /** Desktop-only stopped-worker recovery, independent of remote provider state. */
24
+ const EXTENSION_RECOVERY_MESSAGES = {
25
+ en: {
26
+ extensionRecoveryTitle: "An extension needs a restart",
27
+ extensionRecoveryHelp: "Only the stopped extension restarts. Unfinished actions are not retried.",
28
+ extensionRecoveryRestart: "Restart",
29
+ extensionRecoveryRestarting: "Restarting…",
30
+ extensionRecoveryRestartAria: "Restart extension {name}",
31
+ extensionRecoveryConfirm: "Restart extension “{name}”? Unfinished actions will not be retried. Previously completed changes will not be undone.",
32
+ extensionRecoveryRefresh: "Check again",
33
+ extensionRecoveryLoadFailed: "Could not check the extension status. Check again before restarting.",
34
+ extensionRecoveryFailed: "The extension did not restart. Check its configuration, then retry.",
35
+ extensionRecoveryChanged: "The extension files changed. Review the changes and reload the extension before trying again."
36
+ },
37
+ it: {
38
+ extensionRecoveryTitle: "Un’estensione richiede un riavvio",
39
+ extensionRecoveryHelp: "Si riavvia solo l’estensione arrestata. Le azioni incompiute non vengono ripetute.",
40
+ extensionRecoveryRestart: "Riavvia",
41
+ extensionRecoveryRestarting: "Riavvio…",
42
+ extensionRecoveryRestartAria: "Riavvia l’estensione {name}",
43
+ extensionRecoveryConfirm: "Riavviare l’estensione “{name}”? Le azioni incompiute non verranno ripetute. Le modifiche già completate non saranno annullate.",
44
+ extensionRecoveryRefresh: "Controlla di nuovo",
45
+ extensionRecoveryLoadFailed: "Impossibile verificare lo stato. Controlla di nuovo prima di riavviare.",
46
+ extensionRecoveryFailed: "L’estensione non si è riavviata. Controlla la configurazione e riprova.",
47
+ extensionRecoveryChanged: "I file dell’estensione sono cambiati. Verifica le modifiche e ricarica l’estensione prima di riprovare."
48
+ },
49
+ zh: {
50
+ extensionRecoveryTitle: "扩展需要重启",
51
+ extensionRecoveryHelp: "仅重启已停止的扩展,不会重新执行未完成的操作。",
52
+ extensionRecoveryRestart: "重启扩展",
53
+ extensionRecoveryRestarting: "正在重启…",
54
+ extensionRecoveryRestartAria: "重启扩展 {name}",
55
+ extensionRecoveryConfirm: "要重启扩展“{name}”吗?未完成的操作不会重新执行,已经完成的修改也不会撤销。",
56
+ extensionRecoveryRefresh: "重新检测",
57
+ extensionRecoveryLoadFailed: "暂时无法检查扩展状态,请重新检测后再操作。",
58
+ extensionRecoveryFailed: "扩展没有重启成功,请检查配置后重试。",
59
+ extensionRecoveryChanged: "扩展文件已经变化,请核对修改并重新加载扩展后再操作。"
60
+ }
61
+ };
9
62
  const MOBILE_CONTROL_MESSAGES = {
10
63
  en: {
11
64
  mobileAccess: "Mobile access",
@@ -345,6 +398,16 @@ window.__ModuleLoader__.load({
345
398
  },
346
399
  zh: {}
347
400
  };
401
+ for (const locale of [
402
+ "en",
403
+ "it",
404
+ "zh"
405
+ ]) Object.assign(MOBILE_CONTROL_MESSAGES[locale], EXTENSION_RECOVERY_MESSAGES[locale]);
406
+ for (const locale of [
407
+ "en",
408
+ "it",
409
+ "zh"
410
+ ]) Object.assign(MOBILE_CONTROL_MESSAGES[locale], storageMessages[locale]);
348
411
  Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
349
412
  mobileAccess: "移动访问",
350
413
  collapseMobileAccess: "收起移动访问",
@@ -1136,7 +1199,9 @@ window.__ModuleLoader__.load({
1136
1199
  Object.assign(MOBILE_CONTROL_MESSAGES.en, {
1137
1200
  selfHostedDescription: "Use your own VPS or HTTPS reverse proxy without a public tunnel service.",
1138
1201
  originName: "Own reverse proxy",
1139
- originDescription: "You manage HTTPS. DSH Mobile provides a private, authenticated HTTP backend; no tunnel component is installed.",
1202
+ caddyName: "Managed Caddy",
1203
+ caddyModePreviewNote: "HTTPS form preview differs from the current connection.",
1204
+ originDescription: "Use managed Caddy or your existing HTTPS reverse proxy with DSH Mobile’s private, authenticated backend.",
1140
1205
  prepareOrigin: "Connect your HTTPS reverse proxy",
1141
1206
  originPublicOrigin: "Public HTTPS address",
1142
1207
  originListenHost: "Private listen IPv4",
@@ -1193,7 +1258,9 @@ window.__ModuleLoader__.load({
1193
1258
  Object.assign(MOBILE_CONTROL_MESSAGES.it, {
1194
1259
  selfHostedDescription: "Usa il tuo VPS o reverse proxy HTTPS senza un servizio di tunnel pubblico.",
1195
1260
  originName: "Reverse proxy proprio",
1196
- originDescription: "Gestisci tu HTTPS. DSH Mobile offre un backend HTTP privato con autenticazione; non installa componenti tunnel.",
1261
+ caddyName: "Caddy gestito",
1262
+ caddyModePreviewNote: "L’anteprima del modulo HTTPS differisce dalla connessione attuale.",
1263
+ originDescription: "Usa Caddy gestito o il tuo reverse proxy HTTPS con il backend privato autenticato di DSH Mobile.",
1197
1264
  prepareOrigin: "Collega il tuo reverse proxy HTTPS",
1198
1265
  originPublicOrigin: "Indirizzo HTTPS pubblico",
1199
1266
  originListenHost: "IPv4 privato di ascolto",
@@ -1250,7 +1317,9 @@ window.__ModuleLoader__.load({
1250
1317
  Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
1251
1318
  selfHostedDescription: "使用自己的 VPS 或 HTTPS 反向代理,不依赖公共隧道服务。",
1252
1319
  originName: "自有反向代理",
1253
- originDescription: "HTTPS 由你管理,插件提供带认证的私有 HTTP 后端,无需安装隧道组件。",
1320
+ caddyName: "托管 Caddy",
1321
+ caddyModePreviewNote: "HTTPS 表单预览与当前连接不同。",
1322
+ originDescription: "使用托管 Caddy 或已有 HTTPS 反向代理,接入 DSH Mobile 带认证的私有后端。",
1254
1323
  prepareOrigin: "接入自有 HTTPS 反向代理",
1255
1324
  originPublicOrigin: "公网 HTTPS 地址",
1256
1325
  originListenHost: "私有监听 IPv4",
@@ -2707,6 +2776,38 @@ window.__ModuleLoader__.load({
2707
2776
  if (anchorTimer !== 0) window.clearTimeout(anchorTimer);
2708
2777
  };
2709
2778
  }
2779
+ //#endregion
2780
+ //#region src/mobile-composer-toolbar.ts
2781
+ /**
2782
+ * Compact the stock InputBar without changing its React ownership, handlers,
2783
+ * activity tree or the two groups observed by DSH's model-width controller.
2784
+ * Extra controls wrap inside their own group; menus remain overflow-visible.
2785
+ */
2786
+ const MOBILE_COMPOSER_TOOLBAR_STYLES = `
2787
+ /* These markers are attached only to the stock composer on the mobile page.
2788
+ Body portals and inline dialogs are not part of the compact toolbar. */
2789
+ [data-dsh-mobile-composer-row] { min-width:0 !important; max-width:100% !important; }
2790
+ [data-dsh-mobile-composer-tools]:not([hidden]),[data-dsh-mobile-composer-controls]:not([hidden]) { flex-wrap:wrap !important; min-width:0 !important; max-width:100% !important; }
2791
+ [data-dsh-mobile-composer-tools] > :not(:where(dialog,[popover],[role="dialog"],[role="menu"],[role="listbox"],[data-trigger-menu])),
2792
+ [data-dsh-mobile-composer-tools] > [data-slot] > :not(:where(dialog,[popover],[role="dialog"],[role="menu"],[role="listbox"],[data-trigger-menu])),
2793
+ [data-dsh-mobile-composer-controls] > :not(:where(dialog,[popover],[role="dialog"],[role="menu"],[role="listbox"],[data-trigger-menu])),
2794
+ [data-dsh-mobile-composer-controls] > [data-slot] > :not(:where(dialog,[popover],[role="dialog"],[role="menu"],[role="listbox"],[data-trigger-menu])) { min-width:0 !important; max-width:100% !important; }
2795
+ [data-dsh-mobile-composer-tools] button:not(:where(dialog *,[popover] *,[role="dialog"] *,[role="menu"] *,[role="listbox"] *,[data-trigger-menu] *)),
2796
+ [data-dsh-mobile-composer-controls] button:not(:where(dialog *,[popover] *,[role="dialog"] *,[role="menu"] *,[role="listbox"] *,[data-trigger-menu] *)) { white-space:normal !important; overflow-wrap:anywhere !important; }
2797
+ @media (max-width:720px) {
2798
+ [data-dsh-mobile-composer-row] button:not([role="switch"]):not(:where(dialog *,[popover] *,[role="dialog"] *,[role="menu"] *,[role="listbox"] *,[data-trigger-menu] *)) { min-width:44px !important; min-height:44px !important; touch-action:manipulation; }
2799
+ [data-dsh-mobile-composer-trailing] > button[class*="_primary"] { min-width:44px !important; flex-shrink:0 !important; }
2800
+ [data-dsh-mobile-composer-row] { display:flex !important; flex-wrap:wrap !important; align-items:center !important; gap:4px 8px !important; min-width:0 !important; max-width:100% !important; }
2801
+ [data-dsh-mobile-composer-tools]:not([hidden]) { display:flex !important; flex:0 1 auto !important; flex-wrap:wrap !important; width:auto !important; min-width:0 !important; max-width:100% !important; gap:6px !important; }
2802
+ /* The activity seat can be wider than a single icon. Intrinsic demand, not
2803
+ a fixed 100px reserve, decides whether this intact group needs to wrap. */
2804
+ [data-dsh-mobile-composer-trailing] { display:flex !important; flex:1 0 auto !important; flex-wrap:nowrap !important; width:auto !important; min-width:0 !important; max-width:100% !important; gap:6px !important; margin-left:0 !important; justify-content:flex-end !important; }
2805
+ [data-dsh-mobile-composer-controls]:not([hidden]) { display:flex !important; flex:1 1 0 !important; flex-wrap:wrap !important; justify-content:flex-end !important; min-width:44px !important; max-width:100% !important; gap:6px !important; }
2806
+ [data-dsh-mobile-composer-model] { flex:0 1 auto !important; width:auto !important; min-width:44px !important; max-width:100% !important; }
2807
+ [data-dsh-mobile-composer-model-trigger] { box-sizing:border-box !important; width:100% !important; max-width:100% !important; min-width:0 !important; padding-left:6px !important; padding-right:4px !important; }
2808
+ [data-dsh-mobile-composer-model-label] { flex:1 1 auto !important; max-width:none !important; min-width:0 !important; overflow:hidden !important; text-overflow:ellipsis !important; white-space:nowrap !important; }
2809
+ }
2810
+ `;
2710
2811
  //#endregion
2711
2812
  //#region src/native-mobile.ts
2712
2813
  /**
@@ -2747,13 +2848,15 @@ window.__ModuleLoader__.load({
2747
2848
  .dsh-mobile-settings_fontControls { display:flex; align-items:center; gap:8px; flex-shrink:0; }
2748
2849
  .dsh-mobile-settings_fontControls button { min-width:48px; padding:0 12px; }
2749
2850
  .dsh-mobile-settings_fontControls output { min-width:42px; text-align:center; font-variant-numeric:tabular-nums; }
2851
+ .dsh-mobile-settings_fontDetails { min-width:0; padding:0 0 16px; border-bottom:0.5px solid var(--dsw-alias-border-l2); }
2852
+ .dsh-mobile-settings_fontDetails > summary { display:list-item; min-height:48px; padding:14px 0; box-sizing:border-box; color:var(--dsw-alias-label-primary); font-size:14px; cursor:pointer; }
2853
+ .dsh-mobile-settings_fontDetails .dsh-mobile-settings_rowText { padding-right:8px; }
2854
+ .dsh-mobile-settings_fontFamily { display:flex; flex-wrap:wrap; align-items:center; gap:8px; min-width:0; padding:12px 0; }
2855
+ .dsh-mobile-settings_fontFamily > span { flex:1 1 100px; }
2856
+ .dsh-mobile-settings_fontFamily input { box-sizing:border-box; flex:1 1 180px; width:100%; min-width:0; min-height:48px; padding:8px 12px; border:0.5px solid var(--dsw-alias-border-l2); border-radius:10px; background:var(--dsw-alias-bg-module-platform); color:var(--dsw-alias-label-primary); font:inherit; }
2857
+ .dsh-mobile-settings_fontFamily input:focus-visible,.dsh-mobile-settings_fontDetails > summary:focus-visible { outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb); outline-offset:2px; }
2750
2858
  /* The mobile row replaces the Host-backed font control, not the theme selector. */
2751
2859
  html.dsh-native-mobile-active [data-slot="settings.general.item"]:has([data-mobile-font-setting]) > :not([data-mobile-font-setting])[class*="_row"]:has([class*="_control"] > [class*="_stepper"] > [class*="_arrows"]) { display:none !important; }
2752
- /* Landscape and wide App windows also constrain standard extension controls. */
2753
- [data-dsh-mobile-composer-row] { min-width:0 !important; max-width:100% !important; }
2754
- [data-dsh-mobile-composer-tools]:not([hidden]),[data-dsh-mobile-composer-controls]:not([hidden]) { flex-wrap:wrap !important; min-width:0 !important; max-width:100% !important; }
2755
- [data-dsh-mobile-composer-tools] > *,[data-dsh-mobile-composer-tools] > [data-slot] > *,[data-dsh-mobile-composer-controls] > *,[data-dsh-mobile-composer-controls] > [data-slot] > * { min-width:0 !important; max-width:100% !important; }
2756
- [data-dsh-mobile-composer-tools] button,[data-dsh-mobile-composer-controls] button { white-space:normal !important; overflow-wrap:anywhere !important; }
2757
2860
  @media ${NATIVE_MOBILE_OVERLAY_QUERY} {
2758
2861
  html.dsh-native-mobile-active,html.dsh-native-mobile-active body { width:100%; height:100%; overflow:hidden; }
2759
2862
  html.dsh-native-mobile-active { --dsh-mobile-motion-duration:200ms; --dsh-mobile-motion-ease:cubic-bezier(.22,1,.36,1); }
@@ -2770,6 +2873,9 @@ window.__ModuleLoader__.load({
2770
2873
  the ellipsis action visible and give it a reliable hit target. */
2771
2874
  html.dsh-native-mobile-active [data-dsh-mobile-sidebar] [class*="_rowActions"] { display:inline-flex !important; align-items:center !important; gap:8px !important; }
2772
2875
  html.dsh-native-mobile-active [data-dsh-mobile-sidebar] [class*="_sessionRow"] [class*="_time"] { display:none !important; }
2876
+ /* The always-visible pin action already conveys pinned state. Desktop only
2877
+ hides this passive marker on hover; touch rows must hide it throughout. */
2878
+ html.dsh-native-mobile-active [data-dsh-mobile-sidebar] [class*="_sessionRow"] [class*="_pinIndicator"] { display:none !important; }
2773
2879
  html.dsh-native-mobile-active [data-dsh-mobile-sidebar] [class*="_rowActions"] button { box-sizing:border-box !important; width:32px !important; min-width:32px !important; height:32px !important; min-height:32px !important; }
2774
2880
  [data-dsh-mobile-frame] { grid-template-columns:0 minmax(0,1fr) 0 !important; width:100% !important; height:100dvh !important; overflow:hidden !important; }
2775
2881
  [data-dsh-mobile-center] { grid-column:2 !important; width:100vw !important; min-width:0 !important; }
@@ -2827,6 +2933,11 @@ window.__ModuleLoader__.load({
2827
2933
  [data-dsh-mobile-settings-options] [data-slot="settings.general.item"] [class*="_selector"] { box-sizing:border-box !important; align-self:flex-start !important; justify-content:space-between !important; min-width:0 !important; min-height:48px !important; max-width:100% !important; }
2828
2934
  [data-dsh-mobile-settings-options] :is(input,select,textarea,button) { max-width:100%; }
2829
2935
  [data-dsh-mobile-settings-options] :is(input,select,textarea) { box-sizing:border-box; width:100%; min-width:0; }
2936
+ /* Local steppers and native page-scale controls keep their own touch targets. */
2937
+ [data-dsh-mobile-settings-options] [data-slot="settings.general.item"] .dsh-mobile-settings_fontControls button { flex:0 0 auto !important; width:auto !important; min-width:48px !important; align-self:center !important; justify-content:center !important; }
2938
+ [data-dsh-mobile-settings-options] [data-slot="settings.general.item"] .dsh-mobile-settings_fontControls select { flex:0 0 auto !important; width:auto !important; min-width:76px !important; align-self:center !important; }
2939
+ [data-dsh-mobile-settings-options] [data-mobile-font-setting] .dsh-mobile-settings_row { flex-wrap:wrap !important; }
2940
+ [data-dsh-mobile-settings-options] [data-mobile-font-setting] .dsh-mobile-settings_rowText { flex:1 1 140px !important; width:auto !important; padding-right:8px !important; }
2830
2941
  [data-dsh-mobile-settings-options] [class*="_head"] { min-width:0; flex-wrap:wrap; }
2831
2942
  /* Provider names may shrink, but their edit/delete actions remain horizontal
2832
2943
  and retain a full touch target on narrow screens. */
@@ -2931,23 +3042,6 @@ window.__ModuleLoader__.load({
2931
3042
  /* Collapse the scroll viewport only; the editor, draft and attachments stay
2932
3043
  owned by DSH. Toolbar focus keeps the card expanded during menu taps. */
2933
3044
  [data-dsh-mobile-center] [data-composer-card]:not(:focus-within) > [data-input-scroll] { max-height:72px !important; overflow-y:auto !important; }
2934
- /* Stock Send/Queue/Steer and Stop keep their handlers and disabled state. */
2935
- [data-dsh-mobile-composer-row] button { min-width:44px !important; min-height:44px !important; touch-action:manipulation; }
2936
- [data-dsh-mobile-composer-row] button[class*="_primary"] { min-width:44px !important; flex-shrink:0 !important; }
2937
- /* Keep ordinary controls on one row. The trailing group reserves room for
2938
- its model and primary buttons; extra tools wrap only when space is short. */
2939
- [data-dsh-mobile-composer-row] { display:flex !important; flex-wrap:wrap !important; align-items:center !important; gap:4px 8px !important; min-width:0 !important; max-width:100% !important; }
2940
- [data-dsh-mobile-composer-tools]:not([hidden]) { display:flex !important; flex:0 1 auto !important; flex-wrap:wrap !important; width:auto !important; min-width:0 !important; max-width:100% !important; gap:6px !important; }
2941
- [data-dsh-mobile-composer-tools] > *,[data-dsh-mobile-composer-tools] > [data-slot] > * { min-width:0 !important; max-width:100% !important; }
2942
- [data-dsh-mobile-composer-tools] button { white-space:normal !important; overflow-wrap:anywhere !important; }
2943
- [data-dsh-mobile-composer-trailing] { display:flex !important; flex:1 0 100px !important; flex-wrap:nowrap !important; width:auto !important; min-width:min(100%,100px) !important; max-width:100% !important; gap:6px !important; margin-left:0 !important; justify-content:flex-end !important; }
2944
- [data-dsh-mobile-composer-trailing]:has(> button[class*="_primary"] ~ button[class*="_primary"]) { flex-basis:144px !important; min-width:min(100%,144px) !important; }
2945
- [data-dsh-mobile-composer-controls]:not([hidden]) { display:flex !important; flex:1 1 0 !important; flex-wrap:wrap !important; justify-content:flex-end !important; min-width:0 !important; max-width:100% !important; gap:6px !important; }
2946
- [data-dsh-mobile-composer-controls] > *,[data-dsh-mobile-composer-controls] > [data-slot] > * { min-width:0 !important; max-width:100% !important; }
2947
- [data-dsh-mobile-composer-controls] button { white-space:normal !important; overflow-wrap:anywhere !important; }
2948
- [data-dsh-mobile-composer-model] { flex:0 1 auto !important; width:auto !important; min-width:44px !important; max-width:100% !important; }
2949
- [data-dsh-mobile-composer-model-trigger] { box-sizing:border-box !important; width:100% !important; max-width:100% !important; min-width:0 !important; padding-left:6px !important; padding-right:4px !important; }
2950
- [data-dsh-mobile-composer-model-label] { flex:1 1 auto !important; max-width:none !important; min-width:0 !important; overflow:hidden !important; text-overflow:ellipsis !important; white-space:nowrap !important; }
2951
3045
  [data-dsh-mobile-center] [class*="_root"]:has(> [class*="_card"] textarea) { box-sizing:border-box !important; width:100% !important; padding:0 0 8px !important; }
2952
3046
  [data-dsh-mobile-center] [class*="_root"]:has(> [class*="_card"] textarea) > [class=""]:last-child { display:none !important; }
2953
3047
  }
@@ -2972,7 +3066,7 @@ window.__ModuleLoader__.load({
2972
3066
  [data-dsh-mobile-settings-content][data-dsh-mobile-view-transition="true"],
2973
3067
  [data-dsh-mobile-view][data-dsh-mobile-view-transition="true"] { animation:none !important; }
2974
3068
  }
2975
- `;
3069
+ ` + MOBILE_COMPOSER_TOOLBAR_STYLES;
2976
3070
  function classToken(element, suffix) {
2977
3071
  return Array.from(element.classList).some((value) => value.endsWith(suffix));
2978
3072
  }
@@ -4306,6 +4400,92 @@ window.__ModuleLoader__.load({
4306
4400
  //#region src/mobile-font.ts
4307
4401
  /** Device-local typography for authenticated mobile pages, without Host settings writes. */
4308
4402
  const MOBILE_FONT_STORAGE_KEY = "dsh-mobile.content-font-size.v1";
4403
+ const MOBILE_TYPOGRAPHY_STORAGE_KEY = "dsh-mobile.typography.v1";
4404
+ const MOBILE_FONT_ROLES = ["text", "code"];
4405
+ const roleSpecs = {
4406
+ text: {
4407
+ default: 16,
4408
+ min: 12,
4409
+ variable: "--dsh-content-font-size"
4410
+ },
4411
+ code: {
4412
+ default: 11,
4413
+ min: 10,
4414
+ variable: "--dsh-code-font-size"
4415
+ }
4416
+ };
4417
+ const genericFamilies = /* @__PURE__ */ new Set([
4418
+ "serif",
4419
+ "sans-serif",
4420
+ "monospace",
4421
+ "cursive",
4422
+ "fantasy",
4423
+ "system-ui",
4424
+ "ui-serif",
4425
+ "ui-sans-serif",
4426
+ "ui-monospace",
4427
+ "ui-rounded",
4428
+ "emoji",
4429
+ "math",
4430
+ "fangsong",
4431
+ "-apple-system",
4432
+ "blinkmacsystemfont"
4433
+ ]);
4434
+ const MAX_FONT_FAMILY_LENGTH = 1024;
4435
+ /** Normalize a comma-separated local font list without loading or discovering fonts. */
4436
+ function normalizeMobileFontFamily(value) {
4437
+ let result = "";
4438
+ for (const part of value.slice(0, MAX_FONT_FAMILY_LENGTH * 2).split(",")) {
4439
+ const name = part.replace(/["'\\<>\u0000-\u001f\u007f]/gu, "").replace(/\s+/gu, " ").trim();
4440
+ if (name === "") continue;
4441
+ const generic = genericFamilies.has(name.toLowerCase());
4442
+ const separator = result === "" ? "" : ", ";
4443
+ const remaining = MAX_FONT_FAMILY_LENGTH - result.length - separator.length - (generic ? 0 : 2);
4444
+ if (remaining < 1 || generic && name.length > remaining) break;
4445
+ result += separator + (generic ? name : `"${name.slice(0, remaining)}"`);
4446
+ }
4447
+ return result;
4448
+ }
4449
+ function defaultTypography(textSize = 16) {
4450
+ return Object.freeze({
4451
+ sizes: Object.freeze({
4452
+ text: textSize,
4453
+ code: roleSpecs.code.default
4454
+ }),
4455
+ families: Object.freeze({
4456
+ text: "",
4457
+ code: ""
4458
+ })
4459
+ });
4460
+ }
4461
+ /** Durable local JSON is validated independently for each typography role. */
4462
+ function readTypography(value, legacySize) {
4463
+ const fallback = defaultTypography(legacySize);
4464
+ if (value === null) return fallback;
4465
+ let parsed;
4466
+ try {
4467
+ parsed = JSON.parse(value);
4468
+ } catch (error) {
4469
+ return fallback;
4470
+ }
4471
+ if (typeof parsed !== "object" || parsed === null || !("sizes" in parsed) || !("families" in parsed)) return fallback;
4472
+ const sizes = { ...fallback.sizes };
4473
+ const families = { ...fallback.families };
4474
+ for (const role of MOBILE_FONT_ROLES) {
4475
+ if (typeof parsed.sizes === "object" && parsed.sizes !== null && role in parsed.sizes) {
4476
+ const size = Reflect.get(parsed.sizes, role);
4477
+ if (typeof size === "number" && Number.isInteger(size) && size >= roleSpecs[role].min && size <= 32) sizes[role] = size;
4478
+ }
4479
+ if (typeof parsed.families === "object" && parsed.families !== null && role in parsed.families) {
4480
+ const family = Reflect.get(parsed.families, role);
4481
+ if (typeof family === "string") families[role] = normalizeMobileFontFamily(family);
4482
+ }
4483
+ }
4484
+ return Object.freeze({
4485
+ sizes: Object.freeze(sizes),
4486
+ families: Object.freeze(families)
4487
+ });
4488
+ }
4309
4489
  /** Read one local storage value; malformed or unavailable preferences use the mobile default. */
4310
4490
  function parseMobileFontSize(value) {
4311
4491
  if (value === null || !/^\d+$/.test(value)) return 16;
@@ -4315,15 +4495,21 @@ window.__ModuleLoader__.load({
4315
4495
  /** Local preference storage; unavailable browser storage still permits in-page adjustment. */
4316
4496
  var MobileFontPreference = class {
4317
4497
  storage;
4318
- size = 16;
4498
+ typography = defaultTypography();
4319
4499
  listeners = /* @__PURE__ */ new Set();
4320
4500
  constructor(storage) {
4321
4501
  this.storage = storage;
4502
+ let legacySize = 16;
4503
+ try {
4504
+ legacySize = parseMobileFontSize(storage?.getItem("dsh-mobile.content-font-size.v1") ?? null);
4505
+ } catch (error) {}
4506
+ this.typography = defaultTypography(legacySize);
4322
4507
  try {
4323
- this.size = parseMobileFontSize(storage?.getItem("dsh-mobile.content-font-size.v1") ?? null);
4508
+ this.typography = readTypography(storage?.getItem("dsh-mobile.typography.v1") ?? null, legacySize);
4324
4509
  } catch (error) {}
4325
4510
  }
4326
- getSnapshot = () => this.size;
4511
+ getSnapshot = () => this.typography.sizes.text;
4512
+ getTypographySnapshot = () => this.typography;
4327
4513
  subscribe = (listener) => {
4328
4514
  this.listeners.add(listener);
4329
4515
  return () => {
@@ -4332,11 +4518,45 @@ window.__ModuleLoader__.load({
4332
4518
  };
4333
4519
  /** Apply a user-selected size locally; never send it to DSH's settings RPC. */
4334
4520
  set(size) {
4335
- if (!Number.isInteger(size) || size < 12 || size > 32) throw new RangeError("Mobile font size must be an integer from 12 to 32");
4336
- if (this.size === size) return;
4337
- this.size = size;
4521
+ this.setRoleSize("text", size);
4522
+ }
4523
+ /** Change one mobile font role independently; all values remain address-local. */
4524
+ setRoleSize(role, size) {
4525
+ if (!Number.isInteger(size) || size < roleSpecs[role].min || size > 32) throw new RangeError(`Mobile ${role} font size must be an integer from ${roleSpecs[role].min} to 32`);
4526
+ if (this.typography.sizes[role] === size) return;
4527
+ this.persist({
4528
+ sizes: Object.freeze({
4529
+ ...this.typography.sizes,
4530
+ [role]: size
4531
+ }),
4532
+ families: this.typography.families
4533
+ });
4534
+ }
4535
+ /** Select existing local fonts; an empty list uses DSH's built-in font stack. */
4536
+ setRoleFamily(role, family) {
4537
+ const normalized = normalizeMobileFontFamily(family);
4538
+ if (this.typography.families[role] === normalized) return;
4539
+ this.persist({
4540
+ sizes: this.typography.sizes,
4541
+ families: Object.freeze({
4542
+ ...this.typography.families,
4543
+ [role]: normalized
4544
+ })
4545
+ });
4546
+ }
4547
+ /** Restore mobile sizes and built-in font stacks without modifying Host settings. */
4548
+ reset() {
4549
+ const next = defaultTypography();
4550
+ if (MOBILE_FONT_ROLES.every((role) => this.typography.sizes[role] === next.sizes[role] && this.typography.families[role] === "")) return;
4551
+ this.persist(next);
4552
+ }
4553
+ persist(typography) {
4554
+ this.typography = Object.freeze(typography);
4555
+ try {
4556
+ this.storage?.setItem(MOBILE_TYPOGRAPHY_STORAGE_KEY, JSON.stringify(typography));
4557
+ } catch (error) {}
4338
4558
  try {
4339
- this.storage?.setItem(MOBILE_FONT_STORAGE_KEY, String(size));
4559
+ this.storage?.setItem(MOBILE_FONT_STORAGE_KEY, String(typography.sizes.text));
4340
4560
  } catch (error) {}
4341
4561
  for (const listener of this.listeners) listener();
4342
4562
  }
@@ -4348,19 +4568,31 @@ window.__ModuleLoader__.load({
4348
4568
  storage = window.localStorage;
4349
4569
  } catch (error) {}
4350
4570
  const preference = new MobileFontPreference(storage);
4351
- const property = "--dsh-content-font-size";
4352
- let previous = document.body.style.getPropertyValue(property);
4353
- let previousPriority = document.body.style.getPropertyPriority(property);
4354
- let ownedValue;
4571
+ const style = document.body.style;
4572
+ const properties = MOBILE_FONT_ROLES.flatMap((role) => [roleSpecs[role].variable, `--dsh-font-family-${role}`]);
4573
+ const previous = new Map(properties.map((property) => [property, {
4574
+ value: style.getPropertyValue(property),
4575
+ priority: style.getPropertyPriority(property)
4576
+ }]));
4577
+ const owned = /* @__PURE__ */ new Map();
4355
4578
  const apply = () => {
4356
- const current = document.body.style.getPropertyValue(property);
4357
- if (ownedValue !== void 0 && current !== ownedValue) {
4358
- previous = current;
4359
- previousPriority = document.body.style.getPropertyPriority(property);
4579
+ const typography = preference.getTypographySnapshot();
4580
+ for (const role of MOBILE_FONT_ROLES) for (const [property, value] of [[roleSpecs[role].variable, `${typography.sizes[role]}px`], [`--dsh-font-family-${role}`, typography.families[role]]]) {
4581
+ const current = style.getPropertyValue(property);
4582
+ const priority = style.getPropertyPriority(property);
4583
+ const last = owned.get(property);
4584
+ if (last !== void 0 && (current !== last.value || priority !== last.priority)) previous.set(property, {
4585
+ value: current,
4586
+ priority
4587
+ });
4588
+ owned.set(property, {
4589
+ value,
4590
+ priority: ""
4591
+ });
4592
+ if (current === value && priority === "") continue;
4593
+ if (value === "") style.removeProperty(property);
4594
+ else style.setProperty(property, value);
4360
4595
  }
4361
- const value = `${preference.getSnapshot()}px`;
4362
- ownedValue = value;
4363
- if (current !== value) document.body.style.setProperty(property, value);
4364
4596
  };
4365
4597
  apply();
4366
4598
  const unsubscribe = preference.subscribe(apply);
@@ -4374,59 +4606,343 @@ window.__ModuleLoader__.load({
4374
4606
  dispose: () => {
4375
4607
  observer.disconnect();
4376
4608
  unsubscribe();
4377
- if (previous === "") document.body.style.removeProperty(property);
4378
- else document.body.style.setProperty(property, previous, previousPriority);
4609
+ for (const [property, value] of previous) {
4610
+ const last = owned.get(property);
4611
+ if (last === void 0 || style.getPropertyValue(property) !== last.value || style.getPropertyPriority(property) !== last.priority) continue;
4612
+ if (value.value === "") style.removeProperty(property);
4613
+ else style.setProperty(property, value.value, value.priority);
4614
+ }
4379
4615
  }
4380
4616
  };
4381
4617
  }
4382
- const messages = {
4618
+ const messages$1 = {
4383
4619
  zh: {
4384
4620
  title: "移动端字号",
4385
4621
  description: "仅保存在当前访问地址的本地数据中,不影响电脑或其他设备。",
4386
4622
  decrease: "减小移动端字号",
4387
- increase: "增大移动端字号"
4623
+ increase: "增大移动端字号",
4624
+ more: "更多字体设置",
4625
+ reset: "恢复默认",
4626
+ defaultFamily: "默认字体",
4627
+ familyHint: "填写设备已有的字体,多个名称用逗号分隔;留空使用默认字体。",
4628
+ codeSize: "代码字号",
4629
+ textFamily: "正文字体",
4630
+ codeFamily: "代码字体"
4388
4631
  },
4389
4632
  en: {
4390
4633
  title: "Mobile font size",
4391
4634
  description: "Saved locally for this address, without changing your computer or other devices.",
4392
4635
  decrease: "Decrease mobile font size",
4393
- increase: "Increase mobile font size"
4636
+ increase: "Increase mobile font size",
4637
+ more: "More font settings",
4638
+ reset: "Restore defaults",
4639
+ defaultFamily: "Default font",
4640
+ familyHint: "Use fonts already on your device, separated by commas. Leave empty for the default.",
4641
+ codeSize: "Code font size",
4642
+ textFamily: "Text font",
4643
+ codeFamily: "Code font"
4394
4644
  },
4395
4645
  it: {
4396
4646
  title: "Dimensione testo mobile",
4397
4647
  description: "Salvata localmente per questo indirizzo, senza modificare il computer o altri dispositivi.",
4398
4648
  decrease: "Riduci il testo mobile",
4399
- increase: "Aumenta il testo mobile"
4649
+ increase: "Aumenta il testo mobile",
4650
+ more: "Altre impostazioni dei caratteri",
4651
+ reset: "Ripristina predefiniti",
4652
+ defaultFamily: "Carattere predefinito",
4653
+ familyHint: "Usa caratteri presenti sul dispositivo, separati da virgole. Lascia vuoto per il predefinito.",
4654
+ codeSize: "Dimensione codice",
4655
+ textFamily: "Carattere testo",
4656
+ codeFamily: "Carattere codice"
4400
4657
  }
4401
4658
  };
4659
+ function FontFamilyInput({ preference, role, value, label, placeholder }) {
4660
+ const [draft, setDraft] = (0, react.useState)(value);
4661
+ (0, react.useEffect)(() => {
4662
+ setDraft(value);
4663
+ }, [value]);
4664
+ return (0, react.createElement)("label", { className: "dsh-mobile-settings_fontFamily" }, (0, react.createElement)("span", { className: "dsh-mobile-settings_title" }, label), (0, react.createElement)("input", {
4665
+ type: "text",
4666
+ value: draft,
4667
+ maxLength: MAX_FONT_FAMILY_LENGTH,
4668
+ placeholder,
4669
+ autoComplete: "off",
4670
+ spellCheck: false,
4671
+ "data-mobile-font-family": role,
4672
+ onChange: (event) => {
4673
+ setDraft(event.currentTarget.value);
4674
+ },
4675
+ onBlur: () => {
4676
+ preference.setRoleFamily(role, draft);
4677
+ setDraft(preference.getTypographySnapshot().families[role]);
4678
+ }
4679
+ }));
4680
+ }
4402
4681
  /** General settings row matching the existing Mobile controls and DSH theme tokens. */
4403
4682
  function MobileFontSizeRow({ preference, locale }) {
4404
- const size = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
4405
- const copy = messages[locale];
4683
+ const typography = (0, react.useSyncExternalStore)(preference.subscribe, preference.getTypographySnapshot);
4684
+ const copy = messages$1[locale];
4685
+ const controls = (role, title) => {
4686
+ const size = typography.sizes[role];
4687
+ return (0, react.createElement)("div", {
4688
+ className: "dsh-mobile-settings_fontControls",
4689
+ "data-mobile-font-role": role
4690
+ }, (0, react.createElement)("button", {
4691
+ type: "button",
4692
+ className: "dsh-mobile-settings_selector",
4693
+ "aria-label": `${copy.decrease}: ${title}`,
4694
+ disabled: size === roleSpecs[role].min,
4695
+ onClick: () => {
4696
+ preference.setRoleSize(role, size - 1);
4697
+ }
4698
+ }, "−"), (0, react.createElement)("output", {
4699
+ "aria-live": "polite",
4700
+ "aria-label": title
4701
+ }, `${size}px`), (0, react.createElement)("button", {
4702
+ type: "button",
4703
+ className: "dsh-mobile-settings_selector",
4704
+ "aria-label": `${copy.increase}: ${title}`,
4705
+ disabled: size === 32,
4706
+ onClick: () => {
4707
+ preference.setRoleSize(role, size + 1);
4708
+ }
4709
+ }, "+"));
4710
+ };
4406
4711
  return (0, react.createElement)("div", {
4407
- className: "dsh-mobile-settings_row",
4408
4712
  lang: locale,
4409
4713
  "data-mobile-font-setting": true
4410
- }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", { className: "dsh-mobile-settings_desc" }, copy.description)), (0, react.createElement)("div", { className: "dsh-mobile-settings_fontControls" }, (0, react.createElement)("button", {
4714
+ }, (0, react.createElement)("div", { className: "dsh-mobile-settings_row" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", { className: "dsh-mobile-settings_desc" }, copy.description)), controls("text", copy.title)), (0, react.createElement)("details", { className: "dsh-mobile-settings_fontDetails" }, (0, react.createElement)("summary", null, copy.more), (0, react.createElement)("p", { className: "dsh-mobile-settings_desc" }, copy.familyHint), (0, react.createElement)("div", { className: "dsh-mobile-settings_row" }, (0, react.createElement)("span", { className: "dsh-mobile-settings_rowText dsh-mobile-settings_title" }, copy.codeSize), controls("code", copy.codeSize)), ...MOBILE_FONT_ROLES.map((role) => (0, react.createElement)(FontFamilyInput, {
4715
+ key: role,
4716
+ preference,
4717
+ role,
4718
+ value: typography.families[role],
4719
+ label: role === "text" ? copy.textFamily : copy.codeFamily,
4720
+ placeholder: copy.defaultFamily
4721
+ })), (0, react.createElement)("button", {
4411
4722
  type: "button",
4412
4723
  className: "dsh-mobile-settings_selector",
4413
- "aria-label": copy.decrease,
4414
- disabled: size === 12,
4415
4724
  onClick: () => {
4416
- preference.set(size - 1);
4725
+ preference.reset();
4726
+ }
4727
+ }, copy.reset)));
4728
+ }
4729
+ //#endregion
4730
+ //#region src/mobile-display-scale.ts
4731
+ /** Native App page scaling in DSH General settings, independent from local typography. */
4732
+ /** Validate the native reply before displaying or reusing its values in a request. */
4733
+ function parseMobileDisplayScale(value) {
4734
+ if (typeof value !== "object" || value === null || !("percent" in value) || !("minPercent" in value) || !("maxPercent" in value)) throw new TypeError("Native display scale reply is invalid");
4735
+ const { percent, minPercent, maxPercent } = value;
4736
+ if (typeof percent !== "number" || !Number.isInteger(percent) || typeof minPercent !== "number" || !Number.isInteger(minPercent) || typeof maxPercent !== "number" || !Number.isInteger(maxPercent) || minPercent < 80 || maxPercent > 125 || minPercent > 100 || maxPercent < 100 || percent < minPercent || percent > maxPercent) throw new TypeError("Native display scale reply is invalid");
4737
+ return Object.freeze({
4738
+ percent,
4739
+ minPercent,
4740
+ maxPercent
4741
+ });
4742
+ }
4743
+ /** Serial native requests; a replaced bridge or disposed row cannot publish stale results. */
4744
+ var MobileDisplayScaleController = class {
4745
+ getBridge;
4746
+ snapshot = {
4747
+ state: "hidden",
4748
+ value: null
4749
+ };
4750
+ listeners = /* @__PURE__ */ new Set();
4751
+ bridge;
4752
+ generation = 0;
4753
+ active = false;
4754
+ constructor(getBridge) {
4755
+ this.getBridge = getBridge;
4756
+ }
4757
+ getSnapshot = () => this.snapshot;
4758
+ subscribe = (listener) => {
4759
+ this.listeners.add(listener);
4760
+ return () => {
4761
+ this.listeners.delete(listener);
4762
+ };
4763
+ };
4764
+ start() {
4765
+ this.active = true;
4766
+ this.refresh();
4767
+ }
4768
+ stop() {
4769
+ this.active = false;
4770
+ this.generation += 1;
4771
+ this.bridge = void 0;
4772
+ }
4773
+ /** Discover both capabilities before showing a control or querying its current native value. */
4774
+ refresh = () => {
4775
+ if (!this.active) return;
4776
+ const generation = ++this.generation;
4777
+ const bridge = this.getBridge();
4778
+ this.bridge = bridge;
4779
+ this.publish({
4780
+ state: "hidden",
4781
+ value: null
4782
+ });
4783
+ if (bridge === void 0) return;
4784
+ Promise.resolve().then(() => bridge.capabilities()).then(async (capabilities) => {
4785
+ if (!this.current(bridge, generation)) return;
4786
+ if (!capabilities.includes("mobile.display-scale.get") || !capabilities.includes("mobile.display-scale.set")) return;
4787
+ this.publish({
4788
+ state: "loading",
4789
+ value: null
4790
+ });
4791
+ try {
4792
+ const value = parseMobileDisplayScale(await bridge.invoke("mobile.display-scale.get", {}));
4793
+ if (this.current(bridge, generation)) this.publish({
4794
+ state: "ready",
4795
+ value
4796
+ });
4797
+ } catch (error) {
4798
+ if (this.current(bridge, generation)) this.publish({
4799
+ state: "error",
4800
+ value: null
4801
+ });
4802
+ }
4803
+ }, () => {});
4804
+ };
4805
+ /** Apply a device-local value once, preserving the last confirmed value when the native request fails. */
4806
+ setPercent(percent) {
4807
+ const value = this.snapshot.value;
4808
+ const bridge = this.bridge;
4809
+ if (!this.active || bridge === void 0 || bridge !== this.getBridge() || value === null || this.snapshot.state === "busy" || this.snapshot.state === "loading") return;
4810
+ if (!Number.isInteger(percent) || percent < value.minPercent || percent > value.maxPercent) return;
4811
+ if (percent === value.percent && this.snapshot.state !== "error") return;
4812
+ const generation = this.generation;
4813
+ this.publish({
4814
+ state: "busy",
4815
+ value
4816
+ });
4817
+ Promise.resolve().then(() => this.current(bridge, generation) ? bridge.invoke("mobile.display-scale.set", { percent }) : void 0).then((reply) => {
4818
+ if (!this.current(bridge, generation)) return;
4819
+ try {
4820
+ this.publish({
4821
+ state: "ready",
4822
+ value: parseMobileDisplayScale(reply)
4823
+ });
4824
+ } catch (error) {
4825
+ this.publish({
4826
+ state: "error",
4827
+ value
4828
+ });
4829
+ }
4830
+ }, () => {
4831
+ if (this.current(bridge, generation)) this.publish({
4832
+ state: "error",
4833
+ value
4834
+ });
4835
+ });
4836
+ }
4837
+ current(bridge, generation) {
4838
+ return this.active && generation === this.generation && this.bridge === bridge && this.getBridge() === bridge;
4839
+ }
4840
+ publish(snapshot) {
4841
+ this.snapshot = Object.freeze(snapshot);
4842
+ for (const listener of this.listeners) listener();
4843
+ }
4844
+ };
4845
+ const messages = {
4846
+ zh: {
4847
+ title: "页面缩放",
4848
+ description: "调整整个页面的显示大小,仅保存在当前 App 中,与字号设置独立。",
4849
+ reset: "恢复 100%",
4850
+ loading: "读取中…",
4851
+ busy: "正在调整…",
4852
+ error: "无法调整页面缩放,请重试。",
4853
+ retry: "重试"
4854
+ },
4855
+ en: {
4856
+ title: "Page zoom",
4857
+ description: "Resize the whole page. Saved in this App, separately from font size.",
4858
+ reset: "Reset to 100%",
4859
+ loading: "Loading…",
4860
+ busy: "Applying…",
4861
+ error: "Could not change page zoom. Try again.",
4862
+ retry: "Retry"
4863
+ },
4864
+ it: {
4865
+ title: "Zoom pagina",
4866
+ description: "Ridimensiona tutta la pagina. Salvato in questa app, separatamente dalla dimensione del testo.",
4867
+ reset: "Ripristina 100%",
4868
+ loading: "Caricamento…",
4869
+ busy: "Applicazione…",
4870
+ error: "Impossibile modificare lo zoom. Riprova.",
4871
+ retry: "Riprova"
4872
+ }
4873
+ };
4874
+ /** Native-only General settings row using the existing DSH Mobile controls and locale. */
4875
+ function MobileDisplayScaleRow({ locale }) {
4876
+ const [controller] = (0, react.useState)(() => new MobileDisplayScaleController(() => window.__DSH_MOBILE_NATIVE__));
4877
+ const snapshot = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
4878
+ (0, react.useEffect)(() => {
4879
+ controller.start();
4880
+ window.addEventListener("dsh-mobile-native-ready", controller.refresh);
4881
+ return () => {
4882
+ window.removeEventListener("dsh-mobile-native-ready", controller.refresh);
4883
+ controller.stop();
4884
+ };
4885
+ }, [controller]);
4886
+ if (snapshot.state === "hidden") return null;
4887
+ const copy = messages[locale];
4888
+ const value = snapshot.value;
4889
+ const busy = snapshot.state === "busy" || snapshot.state === "loading";
4890
+ const description = snapshot.state === "error" ? copy.error : snapshot.state === "loading" ? copy.loading : snapshot.state === "busy" ? copy.busy : copy.description;
4891
+ const options = value === null ? [] : [.../* @__PURE__ */ new Set([
4892
+ value.minPercent,
4893
+ value.maxPercent,
4894
+ value.percent,
4895
+ ...[
4896
+ 80,
4897
+ 85,
4898
+ 90,
4899
+ 95,
4900
+ 100,
4901
+ 105,
4902
+ 110,
4903
+ 115,
4904
+ 120,
4905
+ 125
4906
+ ].filter((percent) => percent >= value.minPercent && percent <= value.maxPercent)
4907
+ ])].sort((a, b) => a - b);
4908
+ return (0, react.createElement)("div", {
4909
+ className: "dsh-mobile-settings_row",
4910
+ style: { flexWrap: "wrap" },
4911
+ lang: locale,
4912
+ "data-mobile-display-scale-setting": true,
4913
+ "aria-busy": busy
4914
+ }, (0, react.createElement)("div", {
4915
+ className: "dsh-mobile-settings_rowText",
4916
+ style: {
4917
+ flexBasis: 160,
4918
+ paddingRight: 8
4919
+ }
4920
+ }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
4921
+ className: "dsh-mobile-settings_desc",
4922
+ "aria-live": "polite"
4923
+ }, description)), (0, react.createElement)("div", { className: "dsh-mobile-settings_fontControls" }, value === null ? null : (0, react.createElement)("select", {
4924
+ className: "dsh-mobile-settings_selector",
4925
+ "aria-label": copy.title,
4926
+ disabled: busy,
4927
+ value: value.percent,
4928
+ onChange: (event) => {
4929
+ controller.setPercent(Number(event.currentTarget.value));
4417
4930
  }
4418
- }, "−"), (0, react.createElement)("output", {
4419
- "aria-live": "polite",
4420
- "aria-label": copy.title
4421
- }, `${size}px`), (0, react.createElement)("button", {
4931
+ }, ...options.map((percent) => (0, react.createElement)("option", {
4932
+ key: percent,
4933
+ value: percent
4934
+ }, `${percent}%`))), snapshot.state === "error" ? (0, react.createElement)("button", {
4935
+ type: "button",
4936
+ className: "dsh-mobile-settings_selector",
4937
+ onClick: controller.refresh
4938
+ }, copy.retry) : (0, react.createElement)("button", {
4422
4939
  type: "button",
4423
4940
  className: "dsh-mobile-settings_selector",
4424
- "aria-label": copy.increase,
4425
- disabled: size === 32,
4941
+ disabled: busy || value === null || value.percent === 100,
4426
4942
  onClick: () => {
4427
- preference.set(size + 1);
4943
+ controller.setPercent(100);
4428
4944
  }
4429
- }, "+")));
4945
+ }, copy.reset)));
4430
4946
  }
4431
4947
  //#endregion
4432
4948
  //#region src/local-admin-host.ts
@@ -4778,6 +5294,244 @@ window.__ModuleLoader__.load({
4778
5294
  }, copy.open));
4779
5295
  }
4780
5296
  //#endregion
5297
+ //#region src/extension-recovery-ui.ts
5298
+ /** Validate the Host response and select only stopped workers, never in-process hosts. */
5299
+ function parseUnavailableExtensionHosts(payload) {
5300
+ if (typeof payload !== "object" || payload === null || !("hosts" in payload) || !Array.isArray(payload.hosts)) return void 0;
5301
+ const ids = /* @__PURE__ */ new Set();
5302
+ const hosts = [];
5303
+ for (const value of payload.hosts) {
5304
+ if (typeof value !== "object" || value === null) return void 0;
5305
+ const entry = value;
5306
+ if (typeof entry.id !== "string" || !/^[a-z][a-z0-9-]{0,63}$/u.test(entry.id) || ids.has(entry.id) || typeof entry.name !== "string" || entry.name.trim() === "" || entry.mode !== "worker" && entry.mode !== "in-process" || entry.state !== "ready" && entry.state !== "unavailable" || typeof entry.generation !== "string" || !/^[a-f\d]{64}$/u.test(entry.generation)) return void 0;
5307
+ ids.add(entry.id);
5308
+ if (entry.mode === "worker" && entry.state === "unavailable") hosts.push({
5309
+ id: entry.id,
5310
+ name: entry.name,
5311
+ generation: entry.generation
5312
+ });
5313
+ }
5314
+ return hosts;
5315
+ }
5316
+ /** One section shared across desktop access views; recovery is never automatic. */
5317
+ function installExtensionRecoveryControl(options) {
5318
+ const { document, signal, request, confirm, translate: t } = options;
5319
+ const section = document.createElement("section");
5320
+ section.className = "dsh-mobile-control__extension-recovery";
5321
+ section.hidden = true;
5322
+ section.setAttribute("aria-label", t("extensionRecoveryTitle"));
5323
+ const title = document.createElement("h3");
5324
+ title.textContent = t("extensionRecoveryTitle");
5325
+ const help = document.createElement("p");
5326
+ help.textContent = t("extensionRecoveryHelp");
5327
+ const list = document.createElement("ul");
5328
+ const feedback = document.createElement("p");
5329
+ feedback.className = "dsh-mobile-control__extension-recovery-error";
5330
+ feedback.hidden = true;
5331
+ feedback.setAttribute("role", "status");
5332
+ feedback.setAttribute("aria-live", "polite");
5333
+ const retry = document.createElement("button");
5334
+ retry.type = "button";
5335
+ retry.className = "dsh-mobile-control__secondary";
5336
+ retry.textContent = t("extensionRecoveryRefresh");
5337
+ retry.hidden = true;
5338
+ section.append(title, help, list, feedback, retry);
5339
+ const rows = /* @__PURE__ */ new Map();
5340
+ let hosts = [];
5341
+ let busyId;
5342
+ let errorText = "";
5343
+ let errorSource;
5344
+ let revision = 0;
5345
+ let reading = false;
5346
+ let refreshPending = false;
5347
+ let disposed = false;
5348
+ const live = () => !disposed && !signal.aborted;
5349
+ const render = () => {
5350
+ if (!live()) return;
5351
+ section.hidden = hosts.length === 0;
5352
+ section.setAttribute("aria-busy", String(busyId !== void 0));
5353
+ for (const [id, entry] of rows) {
5354
+ if (hosts.some((host) => host.id === id)) continue;
5355
+ entry.row.remove();
5356
+ rows.delete(id);
5357
+ }
5358
+ for (const host of hosts) {
5359
+ let entry = rows.get(host.id);
5360
+ if (entry === void 0) {
5361
+ const row = document.createElement("li");
5362
+ const text = document.createElement("div");
5363
+ const name = document.createElement("strong");
5364
+ const id = document.createElement("code");
5365
+ const button = document.createElement("button");
5366
+ button.type = "button";
5367
+ button.className = "dsh-mobile-control__secondary";
5368
+ button.dataset.extensionRecoveryId = host.id;
5369
+ button.addEventListener("click", () => {
5370
+ recover(host.id);
5371
+ });
5372
+ text.append(name, id);
5373
+ row.append(text, button);
5374
+ list.append(row);
5375
+ entry = {
5376
+ row,
5377
+ name,
5378
+ id,
5379
+ button
5380
+ };
5381
+ rows.set(host.id, entry);
5382
+ }
5383
+ entry.name.textContent = host.name;
5384
+ entry.id.textContent = host.id;
5385
+ entry.button.disabled = busyId !== void 0;
5386
+ entry.button.textContent = t(busyId === host.id ? "extensionRecoveryRestarting" : "extensionRecoveryRestart");
5387
+ entry.button.setAttribute("aria-label", t("extensionRecoveryRestartAria", { name: host.name }));
5388
+ }
5389
+ feedback.hidden = errorText === "";
5390
+ feedback.textContent = errorText;
5391
+ retry.hidden = errorText === "";
5392
+ retry.disabled = reading || busyId !== void 0;
5393
+ };
5394
+ const accept = (payload, recovered = false) => {
5395
+ const parsed = parseUnavailableExtensionHosts(payload);
5396
+ if (parsed === void 0) throw new Error("extension_recovery_invalid_response");
5397
+ hosts = parsed;
5398
+ if (recovered || hosts.length === 0 || errorSource === "load") {
5399
+ errorText = "";
5400
+ errorSource = void 0;
5401
+ }
5402
+ render();
5403
+ };
5404
+ const refresh = async () => {
5405
+ if (!live()) return;
5406
+ if (reading || busyId !== void 0) {
5407
+ refreshPending = true;
5408
+ return;
5409
+ }
5410
+ reading = true;
5411
+ const current = ++revision;
5412
+ render();
5413
+ try {
5414
+ const payload = await request("/api/mobile-access/extensions/hosts");
5415
+ if (live() && current === revision) accept(payload);
5416
+ } catch (_error) {
5417
+ if (live() && current === revision && hosts.length > 0 && errorSource !== "recover") {
5418
+ errorText = t("extensionRecoveryLoadFailed");
5419
+ errorSource = "load";
5420
+ }
5421
+ } finally {
5422
+ reading = false;
5423
+ if (live()) {
5424
+ render();
5425
+ if (refreshPending && busyId === void 0) {
5426
+ refreshPending = false;
5427
+ await refresh();
5428
+ }
5429
+ }
5430
+ }
5431
+ };
5432
+ const recover = async (id) => {
5433
+ const host = hosts.find((entry) => entry.id === id);
5434
+ if (!live() || busyId !== void 0 || host === void 0 || !confirm(t("extensionRecoveryConfirm", { name: host.name }))) return;
5435
+ if (!live()) return;
5436
+ busyId = id;
5437
+ errorText = "";
5438
+ errorSource = void 0;
5439
+ const current = ++revision;
5440
+ render();
5441
+ try {
5442
+ const payload = await request("/api/mobile-access/extensions/recover", {
5443
+ method: "POST",
5444
+ body: JSON.stringify({
5445
+ id,
5446
+ confirm: true
5447
+ })
5448
+ });
5449
+ if (live() && current === revision) accept(payload, true);
5450
+ } catch (error) {
5451
+ if (live() && current === revision) {
5452
+ errorText = t(error instanceof Error && error.message === "extension_changed_during_activation" ? "extensionRecoveryChanged" : "extensionRecoveryFailed");
5453
+ errorSource = "recover";
5454
+ }
5455
+ } finally {
5456
+ busyId = void 0;
5457
+ if (live()) {
5458
+ render();
5459
+ if (refreshPending && !reading) {
5460
+ refreshPending = false;
5461
+ await refresh();
5462
+ }
5463
+ }
5464
+ }
5465
+ };
5466
+ retry.addEventListener("click", () => {
5467
+ errorText = "";
5468
+ errorSource = void 0;
5469
+ refresh();
5470
+ });
5471
+ return {
5472
+ section,
5473
+ refresh,
5474
+ dispose: () => {
5475
+ disposed = true;
5476
+ revision++;
5477
+ section.remove();
5478
+ rows.clear();
5479
+ }
5480
+ };
5481
+ }
5482
+ /** Theme-owned compact recovery rows; normal connection views keep their layout. */
5483
+ const EXTENSION_RECOVERY_STYLES = `
5484
+ .dsh-mobile-control__extension-recovery{margin:0 0 14px;padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary)}
5485
+ .dsh-mobile-control__extension-recovery[hidden]{display:none}
5486
+ .dsh-mobile-control__extension-recovery h3{margin:0 0 6px;font-size:13px;line-height:1.5;font-weight:600}
5487
+ .dsh-mobile-control__extension-recovery p{margin:0 0 8px;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary)}
5488
+ .dsh-mobile-control__extension-recovery ul{list-style:none;margin:0;padding:0}
5489
+ .dsh-mobile-control__extension-recovery li{display:flex;align-items:center;gap:12px;padding:6px 0}
5490
+ .dsh-mobile-control__extension-recovery li>div{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;overflow-wrap:anywhere}
5491
+ .dsh-mobile-control__extension-recovery strong{font-size:12px;line-height:1.5}
5492
+ .dsh-mobile-control__extension-recovery code{font-size:11px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}
5493
+ .dsh-mobile-control__extension-recovery button{flex:none;box-sizing:border-box;min-height:48px;min-width:48px;white-space:normal}
5494
+ .dsh-mobile-control__extension-recovery button:disabled{cursor:wait;opacity:.55}
5495
+ .dsh-mobile-control__extension-recovery button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
5496
+ .dsh-mobile-control__extension-recovery button:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover))}
5497
+ .dsh-mobile-control__extension-recovery .dsh-mobile-control__extension-recovery-error{color:var(--dsw-alias-label-primary)}
5498
+ `;
5499
+ //#endregion
5500
+ //#region src/native-capability.ts
5501
+ /** Cancel queued calls before native effects start; started effects are not rolled back. */
5502
+ function invokeBoundNativeCapability(bridge, currentBridge, action, input, signal) {
5503
+ const abortReason = () => signal.reason ?? new DOMException("mobile extension disposed", "AbortError");
5504
+ if (signal.aborted) return Promise.reject(abortReason());
5505
+ return new Promise((resolve, reject) => {
5506
+ let settled = false;
5507
+ const finish = (callback) => {
5508
+ if (settled) return;
5509
+ settled = true;
5510
+ signal.removeEventListener("abort", onAbort);
5511
+ callback();
5512
+ };
5513
+ const onAbort = () => {
5514
+ finish(() => {
5515
+ reject(abortReason());
5516
+ });
5517
+ };
5518
+ signal.addEventListener("abort", onAbort, { once: true });
5519
+ Promise.resolve().then(() => {
5520
+ if (settled || signal.aborted) throw abortReason();
5521
+ if (currentBridge() !== bridge) throw new DOMException("native bridge changed", "AbortError");
5522
+ return bridge.invoke(action, input);
5523
+ }).then((value) => {
5524
+ finish(() => {
5525
+ resolve(value);
5526
+ });
5527
+ }, (error) => {
5528
+ finish(() => {
5529
+ reject(error);
5530
+ });
5531
+ });
5532
+ });
5533
+ }
5534
+ //#endregion
4781
5535
  //#region src/client.ts
4782
5536
  /** Map a host-reported provider onto the UI union, defaulting to the built-in one. */
4783
5537
  function parseMobileRemoteProvider(value) {
@@ -5291,6 +6045,14 @@ window.__ModuleLoader__.load({
5291
6045
  funnel_https_required: "funnelHttps",
5292
6046
  funnel_start_failed: "funnelStart",
5293
6047
  funnel_start_timeout: "funnelTimeout",
6048
+ funnel_state_invalid: "funnelStateInvalid",
6049
+ cpolar_path_invalid: "componentPathUnsafe",
6050
+ cloudflared_path_invalid: "componentPathUnsafe",
6051
+ cloudflared_tunnel_path_invalid: "componentPathUnsafe",
6052
+ frp_path_invalid: "componentPathUnsafe",
6053
+ frp_config_path_invalid: "componentPathUnsafe",
6054
+ frp_ingress_path_invalid: "componentPathUnsafe",
6055
+ caddy_path_invalid: "componentPathUnsafe",
5294
6056
  tailscale_dns_missing: "tailscaleDnsMissing",
5295
6057
  gateway_start_failed: "gatewayStartFailed",
5296
6058
  control_channel_failed: "controlChannelFailed",
@@ -5388,6 +6150,7 @@ window.__ModuleLoader__.load({
5388
6150
  "cpolar_component_invalid",
5389
6151
  "cpolar_config_missing",
5390
6152
  "cpolar_config_invalid",
6153
+ "funnel_state_invalid",
5391
6154
  "cpolar_start_timeout",
5392
6155
  "cpolar_stopped",
5393
6156
  "cpolar_exited",
@@ -5462,6 +6225,13 @@ window.__ModuleLoader__.load({
5462
6225
  ...init,
5463
6226
  signal: lifecycle.signal
5464
6227
  }, timeoutMs);
6228
+ const extensionRecovery = installExtensionRecoveryControl({
6229
+ document,
6230
+ signal: lifecycle.signal,
6231
+ request: controlRequestJson,
6232
+ confirm: (message) => window.confirm(message),
6233
+ translate: t
6234
+ });
5465
6235
  const root = element("div", "dsh-mobile-control");
5466
6236
  root.lang = locale;
5467
6237
  const panel = element("section", "dsh-mobile-control__panel");
@@ -6274,6 +7044,9 @@ window.__ModuleLoader__.load({
6274
7044
  caddyModeManagedLabel.append(caddyModeManaged, caddyManagedText);
6275
7045
  caddyModeExternalLabel.append(caddyModeExternal, caddyExternalText);
6276
7046
  caddyModeRow.append(caddyModeLegend, caddyModeExternalLabel, caddyModeManagedLabel);
7047
+ const caddyModePreviewNote = element("p", "dsh-mobile-control__component-note");
7048
+ caddyModePreviewNote.textContent = t("caddyModePreviewNote");
7049
+ caddyModePreviewNote.hidden = true;
6277
7050
  const caddyForm = element("div", "dsh-mobile-control__caddy-form");
6278
7051
  caddyForm.hidden = true;
6279
7052
  const caddyFields = element("div", "dsh-mobile-control__origin-fields");
@@ -6341,7 +7114,7 @@ window.__ModuleLoader__.load({
6341
7114
  const caddyActions = element("div", "dsh-mobile-control__actions dsh-mobile-control__caddy-actions");
6342
7115
  caddyActions.append(caddyInstall, caddySave, caddyPurgeButton);
6343
7116
  caddyForm.append(caddyFields, caddyInstallNote, caddyStatus, caddyActions, caddyFeedback);
6344
- originSetup.append(originSetupTitle, caddyModeRow, originHelp, originWarning, originFields, originFeedback, originFormActions, caddyForm, originBackend);
7117
+ originSetup.append(originSetupTitle, caddyModeRow, caddyModePreviewNote, originHelp, originWarning, originFields, originFeedback, originFormActions, caddyForm, originBackend);
6345
7118
  const tailscaleInfo = element("details", "dsh-mobile-control__details");
6346
7119
  const tailscaleInfoSummary = element("summary");
6347
7120
  tailscaleInfoSummary.textContent = t("tailscaleHelp");
@@ -6670,7 +7443,7 @@ window.__ModuleLoader__.load({
6670
7443
  lanView.append(lanSetup, access, qrBox, status, extensionStatus, actions, manageRow, devicePanel, trustedNetworks);
6671
7444
  remoteView.append(remoteIntro, httpFrameWarning, providerSection, remoteWorkspace);
6672
7445
  diagnosticsView.append(diagnosticsIntro, diagnosticsSummary, diagnosticsToolbar, diagnosticsFeedback, diagnosticsChecks, wsPathsSection, diagnosticsDetails);
6673
- panel.append(header, releaseNotice, updateCard, appDownload, switcher, lanView, remoteView, diagnosticsView);
7446
+ panel.append(header, releaseNotice, updateCard, appDownload, extensionRecovery.section, switcher, lanView, remoteView, diagnosticsView);
6674
7447
  root.append(panel);
6675
7448
  document.body.append(root);
6676
7449
  let running = false;
@@ -6790,6 +7563,7 @@ window.__ModuleLoader__.load({
6790
7563
  providerInfoButton.setAttribute("aria-expanded", "false");
6791
7564
  });
6792
7565
  const selectView = (view) => {
7566
+ if (!panel.hidden) extensionRecovery.refresh();
6793
7567
  if (view !== "diagnostics") previousAccessView = view;
6794
7568
  lanView.hidden = view !== "lan";
6795
7569
  remoteView.hidden = view !== "remote";
@@ -6820,6 +7594,7 @@ window.__ModuleLoader__.load({
6820
7594
  });
6821
7595
  const setOpen = (open) => {
6822
7596
  panel.hidden = !open;
7597
+ if (open) extensionRecovery.refresh();
6823
7598
  for (const trigger of document.querySelectorAll(".dsh-mobile-control__trigger")) trigger.setAttribute("aria-expanded", String(open));
6824
7599
  };
6825
7600
  const render = (data) => {
@@ -7042,6 +7817,7 @@ window.__ModuleLoader__.load({
7042
7817
  const cloudflared = remoteProvider === "cloudflared";
7043
7818
  const frp = remoteProvider === "frp";
7044
7819
  const origin = remoteProvider === "origin";
7820
+ const managedOrigin = origin && data.originMode === "managed";
7045
7821
  const tailscale = remoteProvider === "tailscale";
7046
7822
  tailscaleChoice.classList.toggle("is-selected", tailscale);
7047
7823
  cpolarChoice.classList.toggle("is-selected", cpolar);
@@ -7056,7 +7832,7 @@ window.__ModuleLoader__.load({
7056
7832
  cloudflaredChoice.tabIndex = cloudflared ? 0 : -1;
7057
7833
  frpChoice.setAttribute("aria-pressed", String(frp));
7058
7834
  originChoice.setAttribute("aria-pressed", String(origin));
7059
- providerSetupName.textContent = cpolar ? "cpolar" : cloudflared ? "cloudflared" : frp ? t("frpName") : origin ? t("originName") : "Tailscale Funnel";
7835
+ providerSetupName.textContent = cpolar ? "cpolar" : cloudflared ? "cloudflared" : frp ? t("frpName") : origin ? t(managedOrigin ? "caddyName" : "originName") : "Tailscale Funnel";
7060
7836
  tailscaleChoice.disabled = remoteProviderBusy;
7061
7837
  cpolarChoice.disabled = remoteProviderBusy;
7062
7838
  cloudflaredChoice.disabled = remoteProviderBusy;
@@ -7251,6 +8027,7 @@ window.__ModuleLoader__.load({
7251
8027
  const caddySupported = caddyComponentInfo !== void 0 && caddyComponentInfo.supported === true;
7252
8028
  if (!caddyModePreviewDirty) caddyModeManagedSelected = data.originMode === "managed";
7253
8029
  const showManaged = origin && caddyModeManagedSelected;
8030
+ caddyModePreviewNote.hidden = !origin || !caddyModePreviewDirty || caddyModeManagedSelected === managedOrigin;
7254
8031
  const caddyConfiguration = typeof data.caddyConfiguration === "object" && data.caddyConfiguration !== null ? data.caddyConfiguration : {};
7255
8032
  const caddyState = typeof data.caddyState === "object" && data.caddyState !== null ? data.caddyState : {};
7256
8033
  caddyModeRow.hidden = !origin;
@@ -7284,7 +8061,6 @@ window.__ModuleLoader__.load({
7284
8061
  caddySecretKey,
7285
8062
  caddyPort
7286
8063
  ]) input.disabled = remoteProviderBusy || !caddySupported;
7287
- selfHostedBadge.textContent = origin ? originListening ? t("originBackendReady") : t("advanced") : frpConfigured && frpInstalled ? t("ready") : t("advanced");
7288
8064
  const state = typeof data.state === "string" ? data.state : "error";
7289
8065
  const errorCode = typeof data.errorCode === "string" ? data.errorCode : "";
7290
8066
  const remoteOrigin = typeof data.origin === "string" ? data.origin : "";
@@ -7297,10 +8073,11 @@ window.__ModuleLoader__.load({
7297
8073
  }[errorCode] ?? "";
7298
8074
  const needsFunnelSetup = state === "error" && remoteSetupUrl !== "";
7299
8075
  remoteReady = remoteRunning && state === "ready" && remoteOrigin !== "";
8076
+ selfHostedBadge.textContent = origin ? managedOrigin ? t(remoteReady ? "ready" : "advanced") : originListening ? t("originBackendReady") : t("advanced") : frpConfigured && frpInstalled ? t("ready") : t("advanced");
7300
8077
  remoteStateBadge.classList.toggle("is-ready", remoteReady);
7301
8078
  remoteStateBadge.classList.toggle("is-busy", state === "starting" || state === "connecting" || state === "needs-login");
7302
8079
  remoteStateBadge.classList.toggle("is-attention", state === "error" || state === "unavailable");
7303
- remoteStateBadge.textContent = remoteReady ? origin ? t("originBackendReady") : t("ready") : state === "starting" || state === "connecting" || state === "needs-login" ? t("remoteStateConnecting") : state === "error" || state === "unavailable" ? t("remoteStateAttention") : t("remoteStateOff");
8080
+ remoteStateBadge.textContent = remoteReady ? origin && !managedOrigin ? t("originBackendReady") : t("ready") : state === "starting" || state === "connecting" || state === "needs-login" ? t("remoteStateConnecting") : state === "error" || state === "unavailable" ? t("remoteStateAttention") : t("remoteStateOff");
7304
8081
  remoteAccess.hidden = !remoteReady;
7305
8082
  remoteAccessLink.href = remoteOrigin;
7306
8083
  remoteAccessLink.textContent = remoteOrigin;
@@ -7312,7 +8089,7 @@ window.__ModuleLoader__.load({
7312
8089
  starting: cpolar ? t("remoteStartingCpolar") : cloudflared ? t("remoteStartingCloudflared") : frp ? t("remoteStartingFrp") : origin ? t("originStarting") : t("remoteStartingTailscale"),
7313
8090
  "needs-login": t("remoteNeedsLogin"),
7314
8091
  connecting: cpolar ? t("remoteConnectingCpolar") : cloudflared ? t("remoteConnectingCloudflared") : frp ? t("remoteConnectingFrp") : t("remoteConnectingTailscale"),
7315
- ready: origin ? t("originReady") : t("remoteReady"),
8092
+ ready: origin ? t(managedOrigin ? "caddyVerified" : "originReady") : t("remoteReady"),
7316
8093
  error: t("remoteError")
7317
8094
  };
7318
8095
  const errorLabels = translatedRemoteErrors();
@@ -8711,6 +9488,9 @@ window.__ModuleLoader__.load({
8711
9488
  loadDiagnostics();
8712
9489
  });
8713
9490
  diagnosticsRun.addEventListener("click", loadDiagnostics);
9491
+ diagnosticsRun.addEventListener("click", () => {
9492
+ extensionRecovery.refresh();
9493
+ });
8714
9494
  const closeUpdateCard = () => {
8715
9495
  updateCard.hidden = true;
8716
9496
  if (pluginUpdateAvailable && pluginLatestVersion !== "") updatePlugin.hidden = !diagnosticsView.hidden;
@@ -8802,10 +9582,14 @@ window.__ModuleLoader__.load({
8802
9582
  if (!diagnosticsView.hidden) renderWsBlocked(wsPathsBlocked);
8803
9583
  }, () => {});
8804
9584
  };
8805
- const wsBlockedPoll = window.setInterval(pollWsBlocked, 2e4);
9585
+ const wsBlockedPoll = window.setInterval(() => {
9586
+ pollWsBlocked();
9587
+ if (!panel.hidden) extensionRecovery.refresh();
9588
+ }, 2e4);
8806
9589
  return {
8807
9590
  remove: () => {
8808
9591
  lifecycle.abort();
9592
+ extensionRecovery.dispose();
8809
9593
  window.clearInterval(remotePoll);
8810
9594
  window.clearInterval(wsBlockedPoll);
8811
9595
  window.removeEventListener("focus", retryAfterSetup);
@@ -9412,30 +10196,7 @@ window.__ModuleLoader__.load({
9412
10196
  const abortReason = () => signal.reason ?? new DOMException("mobile extension disposed", "AbortError");
9413
10197
  if (signal.aborted) throw abortReason();
9414
10198
  const bridge = window.__DSH_MOBILE_NATIVE__;
9415
- if (bridge !== void 0) return new Promise((resolve, reject) => {
9416
- let settled = false;
9417
- const finish = (callback) => {
9418
- if (settled) return;
9419
- settled = true;
9420
- signal.removeEventListener("abort", onAbort);
9421
- callback();
9422
- };
9423
- const onAbort = () => {
9424
- finish(() => {
9425
- reject(abortReason());
9426
- });
9427
- };
9428
- signal.addEventListener("abort", onAbort, { once: true });
9429
- Promise.resolve().then(() => bridge.invoke(action, input)).then((value) => {
9430
- finish(() => {
9431
- resolve(materializeNativeFile(value));
9432
- });
9433
- }, (error) => {
9434
- finish(() => {
9435
- reject(error);
9436
- });
9437
- });
9438
- });
10199
+ if (bridge !== void 0) return invokeBoundNativeCapability(bridge, () => window.__DSH_MOBILE_NATIVE__, action, input, signal).then(materializeNativeFile);
9439
10200
  if (action === "share" && typeof navigator.share === "function") {
9440
10201
  await navigator.share(input ?? {});
9441
10202
  return { ok: true };
@@ -10051,7 +10812,7 @@ window.__ModuleLoader__.load({
10051
10812
  .dsh-mobile-control__origin-modes{min-width:0;margin:0 0 14px;padding:0;border:0}.dsh-mobile-control__origin-modes legend{margin:0 0 8px;padding:0;color:var(--dsw-alias-label-secondary,#606873);font:600 12px/1.4 system-ui}.dsh-mobile-control__origin-mode{display:flex;box-sizing:border-box;min-height:44px;align-items:center;gap:10px;padding:10px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:10px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);font:12px/1.5 system-ui;cursor:pointer}.dsh-mobile-control__origin-mode+.dsh-mobile-control__origin-mode{margin-top:8px}.dsh-mobile-control__origin-mode:has(input:checked){border-color:var(--dsw-alias-label-primary-bluish,#2563eb)}.dsh-mobile-control__origin-mode:focus-within{outline:2px solid var(--dsw-alias-state-business-primary,#2563eb);outline-offset:2px}.dsh-mobile-control__origin-mode:has(input:disabled){cursor:wait;opacity:.55}.dsh-mobile-control__origin-mode input[type=radio]{flex:0 0 18px;box-sizing:border-box;width:18px;height:18px;min-height:18px;margin:0;padding:0;accent-color:var(--dsw-alias-label-primary-bluish,#2563eb)}.dsh-mobile-control__origin-mode span{min-width:0;overflow-wrap:anywhere}
10052
10813
  /* Base the columns on the card width, not the desktop viewport behind the popup. */
10053
10814
  .dsh-mobile-control__origin-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,140px),1fr));gap:12px}.dsh-mobile-control__origin-fields>.dsh-mobile-control__field{min-width:0;grid-column:auto;font-size:12px}.dsh-mobile-control__origin-fields>.dsh-mobile-control__field--full{grid-column:1/-1}.dsh-mobile-control__origin-fields select{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__origin-fields select:disabled{cursor:not-allowed;opacity:.55}.dsh-mobile-control__caddy-form{display:grid;gap:12px}.dsh-mobile-control__caddy-form>.dsh-mobile-control__component-note,.dsh-mobile-control__caddy-form>.dsh-mobile-control__component-status,.dsh-mobile-control__caddy-form>.dsh-mobile-control__origin-feedback{margin:0;overflow-wrap:anywhere}.dsh-mobile-control__origin-setup .dsh-mobile-control__actions.dsh-mobile-control__caddy-actions{grid-template-columns:minmax(0,1fr);margin:0}.dsh-mobile-control__caddy-actions button{box-sizing:border-box;min-width:0;min-height:44px;padding:10px 12px;border-radius:10px;font:600 12px/1.4 system-ui;cursor:pointer}.dsh-mobile-control__origin-warning{margin:12px 0;padding:10px 12px;border-left:3px solid var(--dsw-alias-state-warn-primary,#a56810);border-radius:4px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-primary,#384152);font-size:12px;line-height:1.6}.dsh-mobile-control__origin-feedback{font-size:12px;line-height:1.6;overflow-wrap:anywhere}.dsh-mobile-control__origin-feedback.is-error{color:var(--dsw-alias-state-error-primary,#bc3030)}.dsh-mobile-control__origin-fields input[aria-invalid=true]{border-color:var(--dsw-alias-state-error-primary,#bc3030)}.dsh-mobile-control__origin-backend{display:flex;min-width:0;flex-direction:column;align-items:stretch;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2,#dbe1e8)}.dsh-mobile-control__origin-backend button{align-self:flex-start;min-height:44px}.dsh-mobile-control__self-hosted-body .is-origin{margin-top:8px}
10054
- `;
10815
+ ` + EXTENSION_RECOVERY_STYLES;
10055
10816
  /** Mount the desktop control or mobile feature enhancements. */
10056
10817
  function apply(ctx) {
10057
10818
  ctx.effect(() => {
@@ -10086,6 +10847,11 @@ window.__ModuleLoader__.load({
10086
10847
  preference: localFont.preference,
10087
10848
  locale: selectedMobileControlLocale()
10088
10849
  }));
10850
+ const removeDisplayScale = ctx.slots.register({
10851
+ name: "settings.general.item",
10852
+ id: "dsh-mobile-display-scale",
10853
+ order: 12
10854
+ }, () => (0, react.createElement)(MobileDisplayScaleRow, { locale: selectedMobileControlLocale() }));
10089
10855
  const removeSwitchComputer = ctx.slots.register({
10090
10856
  name: "settings.general.item",
10091
10857
  id: "dsh-mobile-switch-computer",
@@ -10099,6 +10865,7 @@ window.__ModuleLoader__.load({
10099
10865
  return () => {
10100
10866
  removeTaskNotifications();
10101
10867
  removeSwitchComputer();
10868
+ removeDisplayScale();
10102
10869
  removeFontSize();
10103
10870
  };
10104
10871
  });