dsh-provider-proxy 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -39,13 +39,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
39
39
  var require_client = __commonJS({
40
40
  "src/client.css"(exports, module2) {
41
41
  "use strict";
42
- var __dshMap = { "root": "1c0df77c_root", "lead": "5d248416_lead", "empty": "1710aef8_empty", "card": "b4a56347_card", "cardActive": "5d17f1d6_cardActive", "cardRejected": "0b12be1b_cardRejected", "defaultCard": "4dc8a19a_defaultCard", "defaultLead": "593693ac_defaultLead", "cardHead": "0c3d7976_cardHead", "cardTitles": "ba5490f7_cardTitles", "name": "fc223200_name", "id": "b54fcb50_id", "pillOn": "2d0e393c_pillOn", "pillLive": "ed98388d_pillLive", "pillOff": "07959fb6_pillOff", "pillRejected": "23916369_pillRejected", "toggle": "44052e5b_toggle", "field": "7826b254_field", "label": "fd3de42a_label", "hint": "9c1dd04c_hint", "input": "0cb12e78_input", "passwordToggle": "e7ed8e99_passwordToggle", "testButton": "3ea31502_testButton", "diag": "8093bbdb_diag", "diagSummary": "bb056060_diagSummary", "diagList": "643d5158_diagList", "chipRow": "cf7604fc_chipRow", "chip": "69584a20_chip", "chipActive": "9e9e3b3e_chipActive", "formGrid": "07283bc3_formGrid", "formGridFull": "31635992_formGridFull", "compactRow": "10f37453_compactRow", "compactRowActive": "b8d0ab91_compactRowActive", "compactRowRejected": "518f0c81_compactRowRejected", "copyButton": "4b86d627_copyButton" };
42
+ var __dshMap = { "root": "53783ebc_root", "lead": "87ded16f_lead", "empty": "f139472e_empty", "card": "99d44a8a_card", "cardActive": "6bfd1d28_cardActive", "cardRejected": "ea4afe84_cardRejected", "defaultCard": "6c49773c_defaultCard", "defaultLead": "8e32da71_defaultLead", "cardHead": "d2cf61e5_cardHead", "cardTitles": "c6ef43cd_cardTitles", "name": "6dad9d13_name", "id": "dd160a22_id", "pillOn": "4b27b813_pillOn", "pillLive": "6e7a5d11_pillLive", "pillOff": "7951c4c2_pillOff", "pillRejected": "4756f162_pillRejected", "toggle": "7c4f39f6_toggle", "field": "ebe8e1a3_field", "label": "d6a07995_label", "hint": "5b4fb148_hint", "input": "30a51506_input", "passwordToggle": "fcfbab02_passwordToggle", "testButton": "b3231562_testButton", "diag": "184b2d06_diag", "diagSummary": "49005df6_diagSummary", "diagList": "7f1b6335_diagList", "chipRow": "406f7239_chipRow", "chip": "446f9f30_chip", "chipActive": "152509e7_chipActive", "formGrid": "6fb245a3_formGrid", "formGridFull": "c89ac7ba_formGridFull", "controlRow": "763cfc36_controlRow", "toolbarRow": "97a483ad_toolbarRow", "compactRow": "342803fe_compactRow", "compactRowActive": "b8cadbfb_compactRowActive", "compactRowRejected": "9ccd93bd_compactRowRejected", "copyButton": "dcf70ff1_copyButton", "profileCard": "9889cd73_profileCard", "panel": "64d062a3_panel" };
43
43
  var __dshStyles = globalThis.__dshPluginStyles || (globalThis.__dshPluginStyles = /* @__PURE__ */ new Set());
44
44
  if (!__dshStyles.has("dsh-provider-proxy/client.css")) {
45
45
  __dshStyles.add("dsh-provider-proxy/client.css");
46
46
  __dshStyle = document.createElement("style");
47
47
  __dshStyle.setAttribute("data-plugin-css", "dsh-provider-proxy/client.css");
48
- __dshStyle.textContent = '/* dsh-provider-proxy \u2014 provider proxy settings page.\r\n Tokens only: no literal colours, matching the shared --dsw-* system. */\r\n\r\n.1c0df77c_root {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 12px;\r\n max-width: 640px;\r\n}\r\n\r\n.5d248416_lead {\r\n margin: 0 0 4px;\r\n font-size: 12px;\r\n line-height: 1.6;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.1710aef8_empty {\r\n margin: 0;\r\n padding: 16px;\r\n border: 1px dashed var(--dsw-alias-border-l1);\r\n border-radius: 8px;\r\n font-size: 12px;\r\n text-align: center;\r\n color: var(--dsw-alias-state-idle-primary);\r\n}\r\n\r\n/* ---- provider card ---- */\r\n\r\n.b4a56347_card,\r\n.5d17f1d6_cardActive,\r\n.0b12be1b_cardRejected {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 10px;\r\n padding: 12px 14px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 10px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n}\r\n\r\n.5d17f1d6_cardActive {\r\n border-color: var(--dsw-alias-state-success-primary);\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n/* An enabled route the Host refused: never dress it as active. */\r\n.0b12be1b_cardRejected {\r\n border-color: var(--dsw-alias-state-error-primary, #f85149);\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n/* ---- shared default endpoint ---- */\r\n\r\n/* A distinct outer layer, so it reads as "one setting for the page" rather than\r\n as one more provider card. */\r\n.4dc8a19a_defaultCard {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 10px;\r\n padding: 14px 16px;\r\n border: 1px solid var(--dsw-alias-border-l2);\r\n border-radius: 10px;\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n.593693ac_defaultLead {\r\n margin: -2px 0 0;\r\n font-size: 11px;\r\n line-height: 1.5;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.0c3d7976_cardHead {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 12px;\r\n}\r\n\r\n.ba5490f7_cardTitles {\r\n display: flex;\r\n align-items: baseline;\r\n gap: 8px;\r\n min-width: 0;\r\n}\r\n\r\n.fc223200_name {\r\n overflow: hidden;\r\n font-size: 13px;\r\n font-weight: 600;\r\n white-space: nowrap;\r\n text-overflow: ellipsis;\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.b54fcb50_id {\r\n flex: none;\r\n padding: 1px 6px;\r\n border-radius: 4px;\r\n background: var(--dsw-alias-bg-layer-2);\r\n font-family: ui-monospace, monospace;\r\n font-size: 11px;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n/* ---- status pill ---- */\r\n\r\n.2d0e393c_pillOn,\r\n.ed98388d_pillLive,\r\n.07959fb6_pillOff,\r\n.23916369_pillRejected {\r\n flex: none;\r\n padding: 2px 8px;\r\n border-radius: 999px;\r\n font-size: 11px;\r\n font-weight: 500;\r\n white-space: nowrap;\r\n}\r\n\r\n.2d0e393c_pillOn {\r\n background: var(--dsw-alias-state-success-primary);\r\n color: var(--dsw-alias-bg-base);\r\n}\r\n\r\n.ed98388d_pillLive {\r\n border: 1px solid var(--dsw-alias-border-l2);\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.07959fb6_pillOff {\r\n color: var(--dsw-alias-state-idle-primary);\r\n}\r\n\r\n.23916369_pillRejected {\r\n border: 1px solid var(--dsw-alias-state-error-primary, #f85149);\r\n color: var(--dsw-alias-state-error-primary, #f85149);\r\n}\r\n\r\n/* ---- toggle ---- */\r\n\r\n.44052e5b_toggle {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding-top: 2px;\r\n font-size: 12px;\r\n color: var(--dsw-alias-label-primary);\r\n cursor: pointer;\r\n}\r\n\r\n.44052e5b_toggle input {\r\n accent-color: var(--dsw-alias-brand-primary);\r\n cursor: pointer;\r\n}\r\n\r\n/* ---- fields ---- */\r\n\r\n.7826b254_field {\r\n display: grid;\r\n grid-template-columns: 76px 1fr;\r\n align-items: center;\r\n gap: 8px;\r\n font-size: 12px;\r\n min-width: 0;\r\n}\r\n\r\n.fd3de42a_label {\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.9c1dd04c_hint {\r\n grid-column: 1 / -1;\r\n margin-top: 2px;\r\n font-size: 11px;\r\n color: var(--dsw-alias-state-warn-primary, #d29922);\r\n}\r\n\r\n.0cb12e78_input {\r\n box-sizing: border-box;\r\n width: 100%;\r\n min-width: 0;\r\n padding: 5px 8px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 6px;\r\n outline: none;\r\n background: var(--dsw-alias-bg-layer-1);\r\n color: var(--dsw-alias-label-primary);\r\n font: inherit;\r\n font-size: 12px;\r\n transition: border-color 120ms ease, background 120ms ease;\r\n}\r\n\r\n/* The base look of every input is INLINE too, so these three state rules only\r\n ever take effect as author-important declarations \u2014 same reason as the\r\n self-check button above. `cursor` has no inline base and needs none, and the\r\n hover border uses `l3` because `l1`\u2192`l2` is a 4%\u219210% black step nobody can\r\n see. */\r\n\r\n.0cb12e78_input:hover:not(:disabled) {\r\n border-color: var(--dsw-alias-border-l3) !important;\r\n}\r\n\r\n.0cb12e78_input:focus {\r\n border-color: var(--dsw-alias-brand-primary) !important;\r\n}\r\n\r\n.0cb12e78_input:disabled {\r\n color: var(--dsw-alias-state-idle-primary) !important;\r\n background: var(--dsw-alias-bg-layer-2) !important;\r\n cursor: not-allowed;\r\n}\r\n\r\n/* The icon sits inside the input, so its hover/focus feedback must be local\r\n and must outrank the transparent inline base. */\r\n.e7ed8e99_passwordToggle {\r\n transition: background 120ms ease, color 120ms ease;\r\n}\r\n\r\n.e7ed8e99_passwordToggle:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover) !important;\r\n color: var(--dsw-alias-label-primary) !important;\r\n}\r\n\r\n.e7ed8e99_passwordToggle:focus-visible {\r\n outline: 2px solid var(--dsw-alias-brand-primary);\r\n outline-offset: 1px;\r\n}\r\n\r\n/* ---- self-check ---- */\r\n\r\n/* The hover state is the one thing an inline style cannot express, which is why\r\n this element carries a class at all. Layout stays inline.\r\n\r\n Two traps, both hit before this comment existed:\r\n - `!important` is load-bearing: the base border and background are INLINE, and\r\n the cascade sorts a style attribute after every author rule, so a plain\r\n declaration here would ship, match, and still lose.\r\n - the TOKENS must be visible ones. `bg-layer-1/2/3` all resolve to\r\n `neutral-bluish-00` in the light theme, so a layer swap is a no-op there, and\r\n `border-l1`\u2192`l2` is 4%\u219210% black on a 1px line. The app\'s own hovers use\r\n `interactive-bg-hover`; using it here is what makes the feedback exist. */\r\n.3ea31502_testButton {\r\n transition: border-color 120ms ease, background 120ms ease;\r\n}\r\n\r\n.3ea31502_testButton:hover:not(:disabled) {\r\n border-color: var(--dsw-alias-border-l3) !important;\r\n background: var(--dsw-alias-interactive-bg-hover) !important;\r\n}\r\n\r\n/* ---- diagnostics ---- */\r\n\r\n.8093bbdb_diag {\r\n margin-top: 2px;\r\n padding: 8px 10px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 8px;\r\n font-size: 11px;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.bb056060_diagSummary {\r\n cursor: pointer;\r\n color: var(--dsw-alias-state-warn-primary);\r\n}\r\n\r\n.643d5158_diagList {\r\n margin: 6px 0 0;\r\n padding-left: 18px;\r\n font-family: ui-monospace, monospace;\r\n word-break: break-all;\r\n}\r\n\r\n/* ---- v0.8.0 N9 additions: status filter chips ---- */\r\n\r\n.cf7604fc_chipRow {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n.69584a20_chip {\r\n transition: border-color 120ms ease, background 120ms ease, color 120ms ease;\r\n}\r\n\r\n.69584a20_chip:hover:not(:disabled) {\r\n border-color: var(--dsw-alias-border-l3) !important;\r\n background: var(--dsw-alias-interactive-bg-hover) !important;\r\n color: var(--dsw-alias-label-primary) !important;\r\n}\r\n\r\n.9e9e3b3e_chipActive {\r\n border-color: var(--dsw-alias-brand-primary) !important;\r\n background: var(--dsw-alias-interactive-bg-hover) !important;\r\n color: var(--dsw-alias-brand-primary) !important;\r\n}\r\n\r\n/* ---- v0.8.0 N11 additions: form grid ---- */\r\n\r\n.07283bc3_formGrid {\r\n display: grid;\r\n grid-template-columns: repeat(2, minmax(0, 1fr));\r\n align-items: start;\r\n gap: 10px 14px;\r\n}\r\n\r\n.31635992_formGridFull {\r\n grid-column: 1 / -1;\r\n}\r\n\r\n@media (max-width: 520px) {\r\n .07283bc3_formGrid {\r\n grid-template-columns: 1fr !important;\r\n }\r\n .31635992_formGridFull {\r\n grid-column: auto !important;\r\n }\r\n}\r\n\r\n/* ---- v0.8.0 N10 additions: compact view ---- */\r\n\r\n.10f37453_compactRow,\r\n.b8d0ab91_compactRowActive,\r\n.518f0c81_compactRowRejected {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 10px;\r\n padding: 10px 14px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 8px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n margin-bottom: 8px;\r\n}\r\n\r\n.b8d0ab91_compactRowActive {\r\n border-color: var(--dsw-alias-state-success-primary);\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n.518f0c81_compactRowRejected {\r\n border-color: var(--dsw-alias-state-error-primary, #f85149);\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n/* ---- v0.8.0 N13 additions: quick copy ---- */\r\n.4b86d627_copyButton {\r\n transition: background 120ms ease, color 120ms ease;\r\n}\r\n\r\n.4b86d627_copyButton:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover) !important;\r\n color: var(--dsw-alias-label-primary) !important;\r\n}\r\n';
48
+ __dshStyle.textContent = "/* dsh-provider-proxy \u2014 provider proxy settings page.\r\n Tokens only: no literal colours, matching the shared --dsw-* system. */\r\n\r\n.53783ebc_root {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 14px;\r\n max-width: 760px;\r\n}\r\n\r\n.87ded16f_lead {\r\n margin: 0 0 4px;\r\n font-size: 12px;\r\n line-height: 1.6;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.f139472e_empty {\r\n margin: 0;\r\n padding: 16px;\r\n border: 1px dashed var(--dsw-alias-border-l1);\r\n border-radius: 8px;\r\n font-size: 12px;\r\n text-align: center;\r\n color: var(--dsw-alias-state-idle-primary);\r\n}\r\n\r\n/* ---- provider card ---- */\r\n\r\n.99d44a8a_card,\r\n.6bfd1d28_cardActive,\r\n.ea4afe84_cardRejected {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 12px;\r\n padding: 14px 16px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 10px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n}\r\n\r\n.6bfd1d28_cardActive {\r\n border-color: var(--dsw-alias-state-success-primary);\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n/* An enabled route the Host refused: never dress it as active. */\r\n.ea4afe84_cardRejected {\r\n border-color: var(--dsw-alias-state-error-primary, #f85149);\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n/* ---- shared default endpoint ---- */\r\n\r\n/* A distinct outer layer, so it reads as \"one setting for the page\" rather than\r\n as one more provider card. */\r\n.6c49773c_defaultCard {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 12px;\r\n padding: 14px 16px;\r\n border: 1px solid var(--dsw-alias-border-l2);\r\n border-radius: 10px;\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n.8e32da71_defaultLead {\r\n margin: -2px 0 0;\r\n font-size: 11px;\r\n line-height: 1.5;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.d2cf61e5_cardHead {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 12px;\r\n}\r\n\r\n.c6ef43cd_cardTitles {\r\n display: flex;\r\n align-items: baseline;\r\n gap: 8px;\r\n min-width: 0;\r\n}\r\n\r\n.6dad9d13_name {\r\n overflow: hidden;\r\n font-size: 13px;\r\n font-weight: 600;\r\n white-space: nowrap;\r\n text-overflow: ellipsis;\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.dd160a22_id {\r\n flex: none;\r\n padding: 1px 6px;\r\n border-radius: 4px;\r\n background: var(--dsw-alias-bg-layer-2);\r\n font-family: ui-monospace, monospace;\r\n font-size: 11px;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n/* ---- status pill ---- */\r\n\r\n.4b27b813_pillOn,\r\n.6e7a5d11_pillLive,\r\n.7951c4c2_pillOff,\r\n.4756f162_pillRejected {\r\n flex: none;\r\n padding: 2px 8px;\r\n border-radius: 999px;\r\n font-size: 11px;\r\n font-weight: 500;\r\n white-space: nowrap;\r\n}\r\n\r\n.4b27b813_pillOn {\r\n background: var(--dsw-alias-state-success-primary);\r\n color: var(--dsw-alias-bg-base);\r\n}\r\n\r\n.6e7a5d11_pillLive {\r\n border: 1px solid var(--dsw-alias-border-l2);\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.7951c4c2_pillOff {\r\n color: var(--dsw-alias-state-idle-primary);\r\n}\r\n\r\n.4756f162_pillRejected {\r\n border: 1px solid var(--dsw-alias-state-error-primary, #f85149);\r\n color: var(--dsw-alias-state-error-primary, #f85149);\r\n}\r\n\r\n/* ---- toggle ---- */\r\n\r\n.7c4f39f6_toggle {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding-top: 2px;\r\n font-size: 12px;\r\n color: var(--dsw-alias-label-primary);\r\n cursor: pointer;\r\n}\r\n\r\n.7c4f39f6_toggle input {\r\n accent-color: var(--dsw-alias-brand-primary);\r\n cursor: pointer;\r\n}\r\n\r\n/* ---- fields ---- */\r\n\r\n.ebe8e1a3_field {\r\n display: grid;\r\n grid-template-columns: 76px 1fr;\r\n align-items: center;\r\n gap: 8px;\r\n font-size: 12px;\r\n min-width: 0;\r\n}\r\n\r\n.d6a07995_label {\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.5b4fb148_hint {\r\n grid-column: 1 / -1;\r\n margin-top: 2px;\r\n font-size: 11px;\r\n color: var(--dsw-alias-state-warn-primary, #d29922);\r\n}\r\n\r\n.30a51506_input {\r\n box-sizing: border-box;\r\n width: 100%;\r\n min-width: 0;\r\n padding: 5px 8px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 6px;\r\n outline: none;\r\n background: var(--dsw-alias-bg-layer-1);\r\n color: var(--dsw-alias-label-primary);\r\n font: inherit;\r\n font-size: 12px;\r\n transition: border-color 120ms ease, background 120ms ease;\r\n}\r\n\r\n/* The base look of every input is INLINE too, so these three state rules only\r\n ever take effect as author-important declarations \u2014 same reason as the\r\n self-check button above. `cursor` has no inline base and needs none, and the\r\n hover border uses `l3` because `l1`\u2192`l2` is a 4%\u219210% black step nobody can\r\n see. */\r\n\r\n.30a51506_input:hover:not(:disabled) {\r\n border-color: var(--dsw-alias-border-l3) !important;\r\n}\r\n\r\n.30a51506_input:focus {\r\n border-color: var(--dsw-alias-brand-primary) !important;\r\n}\r\n\r\n.30a51506_input:disabled {\r\n color: var(--dsw-alias-state-idle-primary) !important;\r\n background: var(--dsw-alias-bg-layer-2) !important;\r\n cursor: not-allowed;\r\n}\r\n\r\n/* The icon sits inside the input, so its hover/focus feedback must be local\r\n and must outrank the transparent inline base. */\r\n.fcfbab02_passwordToggle {\r\n transition: background 120ms ease, color 120ms ease;\r\n}\r\n\r\n.fcfbab02_passwordToggle:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover) !important;\r\n color: var(--dsw-alias-label-primary) !important;\r\n}\r\n\r\n.fcfbab02_passwordToggle:focus-visible {\r\n outline: 2px solid var(--dsw-alias-brand-primary);\r\n outline-offset: 1px;\r\n}\r\n\r\n/* ---- self-check ---- */\r\n\r\n/* The hover state is the one thing an inline style cannot express, which is why\r\n this element carries a class at all. Layout stays inline.\r\n\r\n Two traps, both hit before this comment existed:\r\n - `!important` is load-bearing: the base border and background are INLINE, and\r\n the cascade sorts a style attribute after every author rule, so a plain\r\n declaration here would ship, match, and still lose.\r\n - the TOKENS must be visible ones. `bg-layer-1/2/3` all resolve to\r\n `neutral-bluish-00` in the light theme, so a layer swap is a no-op there, and\r\n `border-l1`\u2192`l2` is 4%\u219210% black on a 1px line. The app's own hovers use\r\n `interactive-bg-hover`; using it here is what makes the feedback exist. */\r\n.b3231562_testButton {\r\n transition: border-color 120ms ease, background 120ms ease;\r\n}\r\n\r\n.b3231562_testButton:hover:not(:disabled) {\r\n border-color: var(--dsw-alias-border-l3) !important;\r\n background: var(--dsw-alias-interactive-bg-hover) !important;\r\n}\r\n\r\n/* ---- diagnostics ---- */\r\n\r\n.184b2d06_diag {\r\n padding: 10px 14px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 10px;\r\n font-size: 11px;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.49005df6_diagSummary {\r\n cursor: pointer;\r\n color: var(--dsw-alias-state-warn-primary);\r\n}\r\n\r\n.7f1b6335_diagList {\r\n margin: 6px 0 0;\r\n padding-left: 18px;\r\n font-family: ui-monospace, monospace;\r\n word-break: break-all;\r\n}\r\n\r\n/* ---- v0.8.0 N9 additions: status filter chips ---- */\r\n\r\n.406f7239_chipRow {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n flex-wrap: wrap;\r\n}\r\n\r\n.446f9f30_chip {\r\n transition: border-color 120ms ease, background 120ms ease, color 120ms ease;\r\n}\r\n\r\n/* Hover is a HINT, never a selection.\r\n\r\n Two earlier versions of this rule were wrong in the same direction:\r\n it painted the active background (any hovered chip looked selected), and\r\n then it painted an outline (a deselected chip still looked framed). An\r\n unselected chip is bare text; hovering gives it a faint surface, nothing\r\n more. Selection is the only state allowed a border and a solid fill. */\r\n.446f9f30_chip:hover:not(:disabled) {\r\n border-color: transparent !important;\r\n background: var(--dsw-alias-interactive-bg-hover) !important;\r\n color: var(--dsw-alias-label-primary) !important;\r\n}\r\n\r\n/* The selected chip, driven by STATE rather than by class precedence, so it\r\n cannot be outranked by `:hover` on a sibling. */\r\n.152509e7_chipActive,\r\n.446f9f30_chip[aria-pressed='true'] {\r\n border-color: var(--dsw-alias-brand-primary) !important;\r\n background: var(--dsw-alias-brand-primary) !important;\r\n color: var(--dsw-alias-bg-base) !important;\r\n font-weight: 600;\r\n}\r\n\r\n/* A selected chip keeps its own surface while hovered: the pointer is on it,\r\n which must not read as \"about to become something else\". */\r\n.152509e7_chipActive:hover:not(:disabled),\r\n.446f9f30_chip[aria-pressed='true']:hover:not(:disabled) {\r\n border-color: var(--dsw-alias-brand-primary) !important;\r\n background: var(--dsw-alias-brand-primary) !important;\r\n color: var(--dsw-alias-bg-base) !important;\r\n}\r\n\r\n/* ---- v0.8.0 N11 additions: form grid ---- */\r\n\r\n.6fb245a3_formGrid {\r\n display: grid;\r\n grid-template-columns: repeat(2, minmax(0, 1fr));\r\n align-items: start;\r\n gap: 10px 14px;\r\n}\r\n\r\n.c89ac7ba_formGridFull {\r\n grid-column: 1 / -1;\r\n}\r\n\r\n@media (max-width: 520px) {\r\n .6fb245a3_formGrid {\r\n grid-template-columns: 1fr !important;\r\n }\r\n .c89ac7ba_formGridFull {\r\n grid-column: auto !important;\r\n }\r\n /* The endpoint-source picker and the enable switch stack too, instead of\r\n shrinking the picker until its option text is unreadable. */\r\n .763cfc36_controlRow {\r\n grid-template-columns: 1fr !important;\r\n }\r\n /* The list toolbar's buttons drop below the search box at this width. */\r\n .97a483ad_toolbarRow > * {\r\n flex: 1 1 100% !important;\r\n }\r\n}\r\n\r\n/* ---- v0.8.0 N10 additions: compact view ---- */\r\n\r\n.342803fe_compactRow,\r\n.b8cadbfb_compactRowActive,\r\n.9ccd93bd_compactRowRejected {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: stretch;\r\n gap: 8px;\r\n padding: 12px 16px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 10px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n}\r\n\r\n.b8cadbfb_compactRowActive {\r\n border-color: var(--dsw-alias-state-success-primary);\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n.9ccd93bd_compactRowRejected {\r\n border-color: var(--dsw-alias-state-error-primary, #f85149);\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n/* ---- v0.8.0 N13 additions: quick copy ---- */\r\n.dcf70ff1_copyButton {\r\n transition: background 120ms ease, color 120ms ease;\r\n}\r\n\r\n.dcf70ff1_copyButton:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover) !important;\r\n color: var(--dsw-alias-label-primary) !important;\r\n}\r\n\r\n/* ---- v0.9.0 N14 additions: named proxy presets ----\r\n The manager reuses the diagnostics/details shell, so only the per-preset row\r\n needs its own rules: an inset card whose border lights up while it is being\r\n edited, which is the only way to tell several identical rows apart. */\r\n\r\n.9889cd73_profileCard {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n padding: 12px 14px;\r\n border: 1px solid var(--dsw-alias-border-l1);\r\n border-radius: 10px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n transition: border-color 120ms ease;\r\n}\r\n\r\n.9889cd73_profileCard:focus-within {\r\n border-color: var(--dsw-alias-brand-primary) !important;\r\n}\r\n\r\n/* ---- v0.9.0 N19 additions: one layout scale ----\r\n These classes exist only so the narrow-screen rules above can reach the\r\n containers. Their visible properties stay inline, for the same reason every\r\n other box on this page does: the stylesheet has silently failed to bind here\r\n before, and layout must survive that. */\r\n\r\n.763cfc36_controlRow,\r\n.97a483ad_toolbarRow,\r\n.64d062a3_panel {\r\n min-width: 0;\r\n}\r\n";
49
49
  document.head.appendChild(__dshStyle);
50
50
  }
51
51
  var __dshStyle;
@@ -71,6 +71,9 @@ function splitHostRules(raw) {
71
71
  function normalizeHostRule(raw) {
72
72
  return raw.trim().replace(/^\*?\./, "").toLowerCase();
73
73
  }
74
+ function parseHostList(raw) {
75
+ return splitHostRules(raw).map(normalizeHostRule).filter((entry) => entry.length > 0);
76
+ }
74
77
  function isBracketedIpv6(token) {
75
78
  return /^\[[0-9a-f:.]+\]$/i.test(token);
76
79
  }
@@ -88,6 +91,30 @@ function hostRuleProblem(raw) {
88
91
  function deadRuleEntries(raw) {
89
92
  return splitHostRules(raw).filter((entry) => hostRuleProblem(entry) !== void 0);
90
93
  }
94
+ var PROVIDER_HOST_TEMPLATES = {
95
+ openai: { label: "OpenAI", hosts: "api.openai.com, *.openai.com" },
96
+ anthropic: { label: "Anthropic", hosts: "api.anthropic.com" },
97
+ google: { label: "Google", hosts: "generativelanguage.googleapis.com" },
98
+ deepseek: { label: "DeepSeek", hosts: "api.deepseek.com" }
99
+ };
100
+ function providerHostTemplate(providerId) {
101
+ const key = providerId.trim().toLowerCase().split(/[-_/:.]/)[0] ?? "";
102
+ const template = PROVIDER_HOST_TEMPLATES[key];
103
+ return template === void 0 ? void 0 : { id: key, ...template };
104
+ }
105
+ function appendHostRules(raw, hosts) {
106
+ const existing = new Set(parseHostList(raw));
107
+ const additions = [];
108
+ for (const token of splitHostRules(hosts)) {
109
+ const normalized = normalizeHostRule(token);
110
+ if (normalized.length === 0 || existing.has(normalized)) continue;
111
+ existing.add(normalized);
112
+ additions.push(token);
113
+ }
114
+ if (additions.length === 0) return raw.trim();
115
+ const prefix = raw.trim();
116
+ return prefix.length === 0 ? additions.join(", ") : `${prefix}, ${additions.join(", ")}`;
117
+ }
91
118
 
92
119
  // src/proxy-address.ts
93
120
  var PROXY_HOST_INVALID = "\u4EE3\u7406\u5730\u5740\u53EA\u80FD\u586B IPv4\u3001IPv6 \u6216\u57DF\u540D";
@@ -125,6 +152,233 @@ function defaultProxyProblem(endpoint) {
125
152
  return void 0;
126
153
  }
127
154
 
155
+ // src/presets.ts
156
+ var DEFAULT_PROFILE_LABEL = "\u9ED8\u8BA4\u9884\u8BBE (Default)";
157
+ var DEFAULT_PROFILE_BINDING = "__default__";
158
+ function routeBindingOf(route) {
159
+ const profileId = (route?.profileId ?? "").trim();
160
+ if (profileId.length > 0) return { kind: "profile", profileId };
161
+ if (route?.useDefault === true) return { kind: "default" };
162
+ return { kind: "own" };
163
+ }
164
+ function bindingFields(binding) {
165
+ switch (binding.kind) {
166
+ case "profile":
167
+ return { profileId: binding.profileId, useDefault: false };
168
+ case "default":
169
+ return { profileId: "", useDefault: true };
170
+ case "own":
171
+ return { profileId: "", useDefault: false };
172
+ }
173
+ }
174
+ function profileNameProblem(raw) {
175
+ const name = raw.trim();
176
+ if (name.length === 0) return "\u9884\u8BBE\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A";
177
+ if (name.length > 40) return "\u9884\u8BBE\u540D\u79F0\u4E0D\u80FD\u8D85\u8FC7 40 \u4E2A\u5B57\u7B26";
178
+ if (/[\u0000-\u001f\u007f]/.test(name)) return "\u9884\u8BBE\u540D\u79F0\u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26";
179
+ if (name === DEFAULT_PROFILE_LABEL) return "\u8BE5\u540D\u79F0\u7531\u5185\u7F6E\u9ED8\u8BA4\u9884\u8BBE\u4FDD\u7559\uFF0C\u8BF7\u6362\u4E00\u4E2A\u540D\u79F0";
180
+ if (name === DEFAULT_PROFILE_BINDING) return "\u8BE5\u540D\u79F0\u7531\u63D2\u4EF6\u4FDD\u7559\uFF0C\u8BF7\u6362\u4E00\u4E2A\u540D\u79F0";
181
+ return void 0;
182
+ }
183
+ function profileNames(profiles) {
184
+ return Object.keys(profiles ?? {}).sort((a, b) => a.localeCompare(b));
185
+ }
186
+ function routesBoundTo(routes, profileId) {
187
+ const out = [];
188
+ for (const [id, route] of Object.entries(routes ?? {})) {
189
+ if ((route?.profileId ?? "").trim() === profileId) out.push(id);
190
+ }
191
+ return out.sort();
192
+ }
193
+ function endpointSummary(endpoint) {
194
+ const protocol = endpoint?.protocol ?? "http";
195
+ const host = (endpoint?.host ?? "").trim() || "\u2014";
196
+ const port = endpoint?.port ?? 0;
197
+ return `${protocol}://${host}:${port === 0 ? "\u2014" : port}`;
198
+ }
199
+ function endpointAddress(endpoint) {
200
+ const host = (endpoint?.host ?? "").trim();
201
+ const port = endpoint?.port ?? 0;
202
+ if (host.length === 0 || port === 0) return "\u672A\u914D\u7F6E";
203
+ return `${host}:${port}`;
204
+ }
205
+
206
+ // src/proxy-clients.ts
207
+ var PROXY_CLIENT_PRESETS = [
208
+ { id: "clash", label: "Clash / Mihomo (127.0.0.1:7890)", protocol: "http", host: "127.0.0.1", port: 7890 },
209
+ { id: "clash-verge", label: "Clash Verge / Meta (127.0.0.1:7897)", protocol: "http", host: "127.0.0.1", port: 7897 },
210
+ { id: "v2ray-http", label: "V2Ray / Xray HTTP (127.0.0.1:10809)", protocol: "http", host: "127.0.0.1", port: 10809 },
211
+ { id: "v2ray-socks", label: "V2Ray / Xray SOCKS5 (127.0.0.1:10808)", protocol: "socks5", host: "127.0.0.1", port: 10808 }
212
+ ];
213
+ function proxyClientPreset(id) {
214
+ return PROXY_CLIENT_PRESETS.find((preset) => preset.id === id);
215
+ }
216
+
217
+ // src/backup.ts
218
+ var PROTOCOLS = ["http", "https", "socks5"];
219
+ function normalizeEndpoint(raw) {
220
+ const value = raw ?? {};
221
+ return {
222
+ protocol: PROTOCOLS.includes(value.protocol) ? value.protocol : "http",
223
+ host: typeof value.host === "string" ? value.host.trim() : "",
224
+ port: typeof value.port === "number" && Number.isFinite(value.port) ? value.port : 0,
225
+ username: typeof value.username === "string" ? value.username.trim() : "",
226
+ password: typeof value.password === "string" ? value.password : ""
227
+ };
228
+ }
229
+ var FALLBACK_TEST_URL = "https://api.ipify.org?format=json";
230
+ function fillRoute(raw) {
231
+ const value = raw ?? {};
232
+ return {
233
+ enabled: value.enabled === true,
234
+ profileId: typeof value.profileId === "string" ? value.profileId.trim() : "",
235
+ useDefault: value.useDefault === true,
236
+ protocol: PROTOCOLS.includes(value.protocol) ? value.protocol : "http",
237
+ host: typeof value.host === "string" ? value.host.trim() : "",
238
+ port: typeof value.port === "number" && Number.isFinite(value.port) ? value.port : 0,
239
+ username: typeof value.username === "string" ? value.username.trim() : "",
240
+ password: typeof value.password === "string" ? value.password : "",
241
+ onlyHosts: typeof value.onlyHosts === "string" ? value.onlyHosts : "",
242
+ bypassHosts: typeof value.bypassHosts === "string" ? value.bypassHosts : "",
243
+ testTarget: typeof value.testTarget === "string" ? value.testTarget : ""
244
+ };
245
+ }
246
+ function buildBackup(source, options = { includePasswords: false }) {
247
+ const include = options.includePasswords === true;
248
+ const hide = (password) => include ? password : "";
249
+ const providers = {};
250
+ for (const [id, raw] of Object.entries(source.providers ?? {})) {
251
+ const filled = fillRoute(raw);
252
+ providers[id] = { ...filled, password: hide(filled.password) };
253
+ }
254
+ const proxyProfiles = {};
255
+ for (const [name, raw] of Object.entries(source.proxyProfiles ?? {})) {
256
+ const filled = normalizeEndpoint(raw);
257
+ proxyProfiles[name] = { ...filled, password: hide(filled.password) };
258
+ }
259
+ const defaultProxy = normalizeEndpoint(source.defaultProxy);
260
+ return {
261
+ version: 1,
262
+ exportedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
263
+ providers,
264
+ proxyProfiles,
265
+ defaultProxy: { ...defaultProxy, password: hide(defaultProxy.password) },
266
+ localDirect: source.localDirect !== false,
267
+ testUrl: typeof source.testUrl === "string" && source.testUrl.trim().length > 0 ? source.testUrl : FALLBACK_TEST_URL
268
+ };
269
+ }
270
+ function backupFileName(now = /* @__PURE__ */ new Date()) {
271
+ const pad = (n) => String(n).padStart(2, "0");
272
+ return `dsh-proxy-backup-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}.json`;
273
+ }
274
+ function isPlainObject(value) {
275
+ return value !== null && typeof value === "object" && !Array.isArray(value);
276
+ }
277
+ function parseBackup(text) {
278
+ let parsed;
279
+ try {
280
+ parsed = JSON.parse(text);
281
+ } catch (error) {
282
+ return {
283
+ ok: false,
284
+ error: `\u4E0D\u662F\u5408\u6CD5\u7684 JSON\uFF1A${error instanceof Error ? error.message : String(error)}`
285
+ };
286
+ }
287
+ if (!isPlainObject(parsed)) return { ok: false, error: "\u5907\u4EFD\u6587\u4EF6\u7684\u9876\u5C42\u5FC5\u987B\u662F\u4E00\u4E2A\u5BF9\u8C61" };
288
+ const root = parsed;
289
+ if (root.providers === void 0 && root.defaultProxy === void 0 && root.proxyProfiles === void 0) {
290
+ return {
291
+ ok: false,
292
+ error: "\u5907\u4EFD\u6587\u4EF6\u7F3A\u5C11 providers / proxyProfiles / defaultProxy\uFF0C\u65E0\u6CD5\u8BC6\u522B\u4E3A\u4EE3\u7406\u914D\u7F6E"
293
+ };
294
+ }
295
+ if (root.providers !== void 0 && !isPlainObject(root.providers)) {
296
+ return { ok: false, error: "providers \u5FC5\u987B\u662F\u4E00\u4E2A\u5BF9\u8C61" };
297
+ }
298
+ if (root.proxyProfiles !== void 0 && !isPlainObject(root.proxyProfiles)) {
299
+ return { ok: false, error: "proxyProfiles \u5FC5\u987B\u662F\u4E00\u4E2A\u5BF9\u8C61" };
300
+ }
301
+ if (root.defaultProxy !== void 0 && !isPlainObject(root.defaultProxy)) {
302
+ return { ok: false, error: "defaultProxy \u5FC5\u987B\u662F\u4E00\u4E2A\u5BF9\u8C61" };
303
+ }
304
+ if (root.localDirect !== void 0 && typeof root.localDirect !== "boolean") {
305
+ return { ok: false, error: "localDirect \u5FC5\u987B\u662F\u5E03\u5C14\u503C" };
306
+ }
307
+ if (root.testUrl !== void 0 && typeof root.testUrl !== "string") {
308
+ return { ok: false, error: "testUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32" };
309
+ }
310
+ const endpoint = (value, label) => {
311
+ if (value.protocol !== void 0 && !PROTOCOLS.includes(value.protocol)) {
312
+ return `${label}.protocol \u5FC5\u987B\u662F ${PROTOCOLS.join(" / ")}`;
313
+ }
314
+ for (const key of ["host", "username", "password"]) {
315
+ if (value[key] !== void 0 && typeof value[key] !== "string") {
316
+ return `${label}.${key} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`;
317
+ }
318
+ }
319
+ if (value.port !== void 0 && (typeof value.port !== "number" || !Number.isFinite(value.port))) {
320
+ return `${label}.port \u5FC5\u987B\u662F\u6570\u5B57`;
321
+ }
322
+ return void 0;
323
+ };
324
+ const providers = {};
325
+ for (const [id, value] of Object.entries(root.providers ?? {})) {
326
+ if (!isPlainObject(value)) return { ok: false, error: `providers.${id} \u5FC5\u987B\u662F\u4E00\u4E2A\u5BF9\u8C61` };
327
+ const why = endpoint(value, `providers.${id}`);
328
+ if (why !== void 0) return { ok: false, error: why };
329
+ for (const key of ["enabled", "useDefault"]) {
330
+ if (value[key] !== void 0 && typeof value[key] !== "boolean") {
331
+ return { ok: false, error: `providers.${id}.${key} \u5FC5\u987B\u662F\u5E03\u5C14\u503C` };
332
+ }
333
+ }
334
+ for (const key of ["profileId", "onlyHosts", "bypassHosts", "testTarget"]) {
335
+ if (value[key] !== void 0 && typeof value[key] !== "string") {
336
+ return { ok: false, error: `providers.${id}.${key} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32` };
337
+ }
338
+ }
339
+ providers[id] = fillRoute(value);
340
+ }
341
+ const proxyProfiles = {};
342
+ for (const [name, value] of Object.entries(
343
+ root.proxyProfiles ?? {}
344
+ )) {
345
+ if (!isPlainObject(value)) return { ok: false, error: `proxyProfiles.${name} \u5FC5\u987B\u662F\u4E00\u4E2A\u5BF9\u8C61` };
346
+ const why = endpoint(value, `proxyProfiles.${name}`);
347
+ if (why !== void 0) return { ok: false, error: why };
348
+ proxyProfiles[name] = normalizeEndpoint(value);
349
+ }
350
+ if (root.defaultProxy !== void 0) {
351
+ const why = endpoint(root.defaultProxy, "defaultProxy");
352
+ if (why !== void 0) return { ok: false, error: why };
353
+ }
354
+ const document2 = {
355
+ version: 1,
356
+ exportedAt: typeof root.exportedAt === "string" ? root.exportedAt : "",
357
+ providers,
358
+ proxyProfiles,
359
+ defaultProxy: normalizeEndpoint(root.defaultProxy ?? {}),
360
+ localDirect: root.localDirect !== false,
361
+ testUrl: typeof root.testUrl === "string" && root.testUrl.trim().length > 0 ? root.testUrl : FALLBACK_TEST_URL
362
+ };
363
+ return { ok: true, document: document2 };
364
+ }
365
+ function backupOps(document2) {
366
+ return [
367
+ { op: "set", path: ["providers"], value: document2.providers },
368
+ { op: "set", path: ["proxyProfiles"], value: document2.proxyProfiles },
369
+ { op: "set", path: ["defaultProxy"], value: document2.defaultProxy },
370
+ { op: "set", path: ["localDirect"], value: document2.localDirect },
371
+ { op: "set", path: ["testUrl"], value: document2.testUrl }
372
+ ];
373
+ }
374
+ function backupCounts(document2) {
375
+ return {
376
+ providers: Object.keys(document2.providers).length,
377
+ profiles: Object.keys(document2.proxyProfiles).length,
378
+ enabled: Object.values(document2.providers).filter((route) => route.enabled).length
379
+ };
380
+ }
381
+
128
382
  // src/client.ts
129
383
  var ReactCache;
130
384
  function react() {
@@ -139,7 +393,7 @@ function react() {
139
393
  }
140
394
  return ReactCache;
141
395
  }
142
- var PROTOCOLS = ["http", "https", "socks5"];
396
+ var PROTOCOLS2 = ["http", "https", "socks5"];
143
397
  var NAMESPACE = "provider-proxy";
144
398
  var PLUGINS = ["slots"];
145
399
  var inject = PLUGINS;
@@ -357,7 +611,24 @@ async function fetchCatalog() {
357
611
  };
358
612
  }
359
613
  }
360
- return { rows, publishedRows: rows, hits, problems, lastUse };
614
+ const environmentProxy = (() => {
615
+ const raw = body?.environment?.proxy;
616
+ if (raw === null || typeof raw !== "object") return null;
617
+ const candidate = raw;
618
+ const protocol = candidate.protocol;
619
+ if (protocol !== "http" && protocol !== "https" && protocol !== "socks5") return null;
620
+ if (typeof candidate.host !== "string" || candidate.host.length === 0) return null;
621
+ const port = candidate.port;
622
+ if (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535) return null;
623
+ return {
624
+ source: typeof candidate.source === "string" ? candidate.source : "environment",
625
+ protocol,
626
+ host: candidate.host,
627
+ port,
628
+ hadCredentials: candidate.hadCredentials === true
629
+ };
630
+ })();
631
+ return { rows, publishedRows: rows, hits, problems, lastUse, environmentProxy };
361
632
  }
362
633
  var CATALOG_REFRESH_MS = 1e4;
363
634
  var catalogPoll;
@@ -378,8 +649,9 @@ var byName = (a, b) => a.name.localeCompare(b.name);
378
649
  function view(route) {
379
650
  return {
380
651
  enabled: route?.enabled === true,
652
+ profileId: typeof route?.profileId === "string" ? route.profileId : "",
381
653
  useDefault: route?.useDefault === true,
382
- protocol: PROTOCOLS.includes(route?.protocol) ? route.protocol : "http",
654
+ protocol: PROTOCOLS2.includes(route?.protocol) ? route.protocol : "http",
383
655
  host: route?.host ?? "",
384
656
  port: route?.port ?? 0,
385
657
  username: route?.username ?? "",
@@ -389,6 +661,15 @@ function view(route) {
389
661
  testTarget: route?.testTarget ?? ""
390
662
  };
391
663
  }
664
+ function profileView(profile) {
665
+ return {
666
+ protocol: PROTOCOLS2.includes(profile?.protocol) ? profile.protocol : "http",
667
+ host: typeof profile?.host === "string" ? profile.host : "",
668
+ port: typeof profile?.port === "number" ? profile.port : 0,
669
+ username: typeof profile?.username === "string" ? profile.username : "",
670
+ password: typeof profile?.password === "string" ? profile.password : ""
671
+ };
672
+ }
392
673
  function unbracket(hostname) {
393
674
  return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
394
675
  }
@@ -411,7 +692,7 @@ function splitProxyUrl(text) {
411
692
  }
412
693
  const scheme = url.protocol.slice(0, -1).toLowerCase();
413
694
  const protocol = scheme === "socks" ? "socks5" : scheme;
414
- if (!PROTOCOLS.includes(protocol)) return void 0;
695
+ if (!PROTOCOLS2.includes(protocol)) return void 0;
415
696
  if (url.hostname.length === 0) return void 0;
416
697
  const decode = (value) => {
417
698
  try {
@@ -431,6 +712,30 @@ function splitProxyUrl(text) {
431
712
  }
432
713
  var WRITE_DELAY_MS = 300;
433
714
  var pendingWrites = /* @__PURE__ */ new Map();
715
+ var VIEW_PREFS_KEY = "dsh-provider-proxy:view";
716
+ function readViewPrefs() {
717
+ try {
718
+ const raw = globalThis.localStorage?.getItem(VIEW_PREFS_KEY);
719
+ if (typeof raw !== "string" || raw.length === 0) return {};
720
+ const parsed = JSON.parse(raw);
721
+ if (parsed === null || typeof parsed !== "object") return {};
722
+ const out = {};
723
+ if (parsed.viewMode === "card" || parsed.viewMode === "compact") out.viewMode = parsed.viewMode;
724
+ if (parsed.statusFilter === "all" || parsed.statusFilter === "proxied" || parsed.statusFilter === "direct" || parsed.statusFilter === "rejected") {
725
+ out.statusFilter = parsed.statusFilter;
726
+ }
727
+ return out;
728
+ } catch {
729
+ return {};
730
+ }
731
+ }
732
+ function writeViewPrefs(patch) {
733
+ try {
734
+ const next = { ...readViewPrefs(), ...patch };
735
+ globalThis.localStorage?.setItem(VIEW_PREFS_KEY, JSON.stringify(next));
736
+ } catch {
737
+ }
738
+ }
434
739
  var writeQueue = Promise.resolve();
435
740
  function enqueueWrite(run) {
436
741
  const next = writeQueue.then(run, run);
@@ -472,16 +777,43 @@ var S = {
472
777
  root: {
473
778
  display: "flex",
474
779
  flexDirection: "column",
475
- gap: "12px",
476
- maxWidth: "720px"
780
+ gap: "14px",
781
+ maxWidth: "760px"
782
+ },
783
+ /**
784
+ * One vertical group of page-level blocks.
785
+ *
786
+ * Page settings (default endpoint, presets, backup, advanced) are one visual
787
+ * unit; the provider list is another. Without the grouping every block was a
788
+ * sibling at the same level, so the list controls ended up four blocks below
789
+ * the list they operate on.
790
+ */
791
+ group: {
792
+ display: "flex",
793
+ flexDirection: "column",
794
+ gap: "10px",
795
+ minWidth: "0"
796
+ },
797
+ /** A small heading that separates the page's groups. */
798
+ groupTitle: {
799
+ margin: "6px 0 -2px",
800
+ fontSize: "11px",
801
+ fontWeight: "600",
802
+ letterSpacing: "0.04em",
803
+ textTransform: "uppercase",
804
+ color: "var(--dsw-alias-label-secondary)"
477
805
  },
478
806
  card: {
479
807
  display: "flex",
480
808
  flexDirection: "column",
481
809
  gap: "12px",
482
810
  padding: "14px 16px",
483
- marginBottom: "12px",
484
- border: "1px solid var(--dsw-alias-border-l1)",
811
+ // Longhands, for the same reason as `chip`: `cardActive`/`cardRejected`
812
+ // override ONLY `borderColor`, and a shorthand here would lose its colour
813
+ // the moment a card went back to the resting state.
814
+ borderWidth: "1px",
815
+ borderStyle: "solid",
816
+ borderColor: "var(--dsw-alias-border-l1)",
485
817
  borderRadius: "10px",
486
818
  background: "var(--dsw-alias-bg-layer-1)"
487
819
  },
@@ -631,7 +963,7 @@ var S = {
631
963
  defaultCard: {
632
964
  display: "flex",
633
965
  flexDirection: "column",
634
- gap: "10px",
966
+ gap: "12px",
635
967
  padding: "14px 16px",
636
968
  border: "1px solid var(--dsw-alias-border-l2)",
637
969
  borderRadius: "10px",
@@ -672,6 +1004,21 @@ var S = {
672
1004
  fontSize: "12px",
673
1005
  minWidth: "0"
674
1006
  },
1007
+ /** A row of peer controls, each with its own label above it. */
1008
+ controlRow: {
1009
+ display: "grid",
1010
+ gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
1011
+ alignItems: "end",
1012
+ gap: "10px 14px"
1013
+ },
1014
+ /** One cell of `controlRow`: a small label over its control. */
1015
+ controlCell: {
1016
+ display: "flex",
1017
+ flexDirection: "column",
1018
+ gap: "4px",
1019
+ fontSize: "12px",
1020
+ minWidth: "0"
1021
+ },
675
1022
  label: { color: "var(--dsw-alias-label-secondary)" },
676
1023
  input: {
677
1024
  boxSizing: "border-box",
@@ -687,10 +1034,9 @@ var S = {
687
1034
  fontSize: "12px"
688
1035
  },
689
1036
  diag: {
690
- marginTop: "4px",
691
- padding: "8px 10px",
1037
+ padding: "10px 14px",
692
1038
  border: "1px solid var(--dsw-alias-border-l1)",
693
- borderRadius: "8px",
1039
+ borderRadius: "10px",
694
1040
  fontSize: "11px",
695
1041
  color: "var(--dsw-alias-label-secondary)"
696
1042
  },
@@ -699,6 +1045,42 @@ var S = {
699
1045
  fontSize: "11px",
700
1046
  color: "var(--dsw-alias-state-warn-primary, #d29922)"
701
1047
  },
1048
+ /** The shared shell of every collapsible panel on the page. */
1049
+ panelBody: {
1050
+ display: "flex",
1051
+ flexDirection: "column",
1052
+ gap: "10px",
1053
+ marginTop: "8px"
1054
+ },
1055
+ /** The bordered block of list controls, directly above the list. */
1056
+ toolbar: {
1057
+ display: "flex",
1058
+ flexDirection: "column",
1059
+ gap: "8px",
1060
+ padding: "10px 12px",
1061
+ border: "1px solid var(--dsw-alias-border-l1)",
1062
+ borderRadius: "10px",
1063
+ background: "var(--dsw-alias-bg-layer-1)"
1064
+ },
1065
+ toolbarRow: {
1066
+ display: "flex",
1067
+ alignItems: "center",
1068
+ flexWrap: "wrap",
1069
+ gap: "8px",
1070
+ minWidth: "0"
1071
+ },
1072
+ /** The backup bar: explanation on the left, its two actions on the right. */
1073
+ backupBar: {
1074
+ display: "flex",
1075
+ alignItems: "center",
1076
+ justifyContent: "space-between",
1077
+ flexWrap: "wrap",
1078
+ gap: "10px",
1079
+ padding: "10px 12px",
1080
+ border: "1px solid var(--dsw-alias-border-l1)",
1081
+ borderRadius: "10px",
1082
+ background: "var(--dsw-alias-bg-layer-1)"
1083
+ },
702
1084
  chipRow: {
703
1085
  display: "flex",
704
1086
  alignItems: "center",
@@ -711,30 +1093,84 @@ var S = {
711
1093
  alignItems: "center",
712
1094
  padding: "3px 10px",
713
1095
  borderRadius: "999px",
714
- border: "1px solid var(--dsw-alias-border-l1)",
715
- background: "var(--dsw-alias-bg-layer-1)",
1096
+ // An UNSELECTED chip is bare text: no outline, no fill.
1097
+ //
1098
+ // It used to carry a faint 1px border, which is the same shape as the
1099
+ // selected chip's outline — so after switching filters the old chip still
1100
+ // read as "selected, just with dimmer text". The border is kept in the
1101
+ // declaration as `transparent` rather than removed, so selecting a chip
1102
+ // cannot shift the row by a pixel.
1103
+ //
1104
+ // WRITTEN AS THREE LONGHANDS, NOT AS THE `border` SHORTHAND. This is not
1105
+ // style: React diffs inline styles key by key, and `chipActive` only
1106
+ // overrides `borderColor`. Deselecting a chip therefore DELETES the
1107
+ // `borderColor` key, React writes `style.borderColor = ''`, and the
1108
+ // shorthand's transparent value is NOT restored — the border falls back to
1109
+ // the CSS initial value `currentColor`, leaving a text-coloured outline on
1110
+ // a chip that is supposed to have none. Keeping all three longhands present
1111
+ // in every state means only the VALUE changes, never the key.
1112
+ borderWidth: "1px",
1113
+ borderStyle: "solid",
1114
+ borderColor: "transparent",
1115
+ background: "transparent",
716
1116
  color: "var(--dsw-alias-label-secondary)",
717
1117
  fontSize: "11px",
718
1118
  fontWeight: "500",
719
1119
  cursor: "pointer",
720
1120
  userSelect: "none"
721
1121
  },
1122
+ /** The one selected chip: a solid fill, unmistakable at a glance. */
722
1123
  chipActive: {
723
1124
  borderColor: "var(--dsw-alias-brand-primary)",
724
- background: "var(--dsw-alias-interactive-bg-hover)",
725
- color: "var(--dsw-alias-brand-primary)",
1125
+ background: "var(--dsw-alias-brand-primary)",
1126
+ color: "var(--dsw-alias-bg-base)",
726
1127
  fontWeight: "600"
727
1128
  },
728
1129
  compactRow: {
1130
+ display: "flex",
1131
+ flexDirection: "column",
1132
+ gap: "8px",
1133
+ padding: "12px 16px",
1134
+ borderWidth: "1px",
1135
+ borderStyle: "solid",
1136
+ borderColor: "var(--dsw-alias-border-l1)",
1137
+ borderRadius: "10px",
1138
+ background: "var(--dsw-alias-bg-layer-1)"
1139
+ },
1140
+ /** Row 1 of a compact entry: identity and the proxy it uses. */
1141
+ compactLine: {
729
1142
  display: "flex",
730
1143
  alignItems: "center",
731
- justifyContent: "space-between",
732
- gap: "10px",
733
- padding: "10px 14px",
734
- border: "1px solid var(--dsw-alias-border-l1)",
735
- borderRadius: "8px",
736
- background: "var(--dsw-alias-bg-layer-1)",
737
- marginBottom: "8px"
1144
+ flexWrap: "wrap",
1145
+ gap: "8px",
1146
+ minWidth: "0"
1147
+ },
1148
+ /** Row 2 of a compact entry: every control, right-aligned. */
1149
+ compactActions: {
1150
+ display: "flex",
1151
+ alignItems: "center",
1152
+ flexWrap: "wrap",
1153
+ gap: "8px",
1154
+ minWidth: "0"
1155
+ },
1156
+ /**
1157
+ * Row 3 of a compact entry: the self-check outcome.
1158
+ *
1159
+ * Its own line, so a running or failing check never shifts the picker or the
1160
+ * buttons, and a long failure message gets the full width instead of being
1161
+ * squeezed between two controls.
1162
+ */
1163
+ compactResultLine: {
1164
+ display: "flex",
1165
+ alignItems: "baseline",
1166
+ flexWrap: "wrap",
1167
+ gap: "6px",
1168
+ paddingTop: "6px",
1169
+ borderTop: "1px solid var(--dsw-alias-border-l1)",
1170
+ fontSize: "11px",
1171
+ color: "var(--dsw-alias-label-secondary)",
1172
+ minWidth: "0",
1173
+ wordBreak: "break-all"
738
1174
  },
739
1175
  compactRowActive: {
740
1176
  borderColor: "var(--dsw-alias-state-success-primary)",
@@ -808,6 +1244,7 @@ function renderPage(ctx) {
808
1244
  hits: {},
809
1245
  problems: {},
810
1246
  lastUse: {},
1247
+ environmentProxy: null,
811
1248
  error: void 0,
812
1249
  pending: true
813
1250
  };
@@ -818,6 +1255,7 @@ function renderPage(ctx) {
818
1255
  hits: catalog.hits,
819
1256
  problems: catalog.problems,
820
1257
  lastUse: catalog.lastUse,
1258
+ environmentProxy: catalog.environmentProxy ?? null,
821
1259
  error: catalog.error,
822
1260
  pending: false
823
1261
  });
@@ -854,6 +1292,24 @@ function renderPage(ctx) {
854
1292
  const [defaultDraft, setDefaultDraft] = (React.useState ?? ((v) => [v, () => {
855
1293
  }]))({});
856
1294
  const editDefault = (field2, value) => setDefaultDraft((current) => ({ ...current, [field2]: value }));
1295
+ const [profileDraft, setProfileDraft] = (React.useState ?? ((v) => [v, () => {
1296
+ }]))({});
1297
+ const editProfile = (name, field2, value) => setProfileDraft((current) => ({
1298
+ ...current,
1299
+ [name]: { ...current[name], [field2]: value }
1300
+ }));
1301
+ const [newProfileName, setNewProfileName] = (React.useState ?? ((v) => [v, () => {
1302
+ }]))("");
1303
+ const [renamingProfile, setRenamingProfile] = (React.useState ?? ((v) => [v, () => {
1304
+ }]))(
1305
+ void 0
1306
+ );
1307
+ const [renameDraft, setRenameDraft] = (React.useState ?? ((v) => [v, () => {
1308
+ }]))("");
1309
+ const [profileManagerOpen, setProfileManagerOpen] = (React.useState ?? ((v) => [v, () => {
1310
+ }]))(
1311
+ false
1312
+ );
857
1313
  const [passwordVisible, setPasswordVisible] = (React.useState ?? ((v) => [v, () => {
858
1314
  }]))({});
859
1315
  const [defaultPasswordVisible, setDefaultPasswordVisible] = (React.useState ?? ((v) => [v, () => {
@@ -921,9 +1377,13 @@ function renderPage(ctx) {
921
1377
  const [filter, setFilter] = (React.useState ?? ((v) => [v, () => {
922
1378
  }]))("");
923
1379
  const [statusFilter, setStatusFilter] = (React.useState ?? ((v) => [v, () => {
924
- }]))("all");
1380
+ }]))(
1381
+ () => readViewPrefs().statusFilter ?? "all"
1382
+ );
925
1383
  const [viewMode, setViewMode] = (React.useState ?? ((v) => [v, () => {
926
- }]))("card");
1384
+ }]))(
1385
+ () => readViewPrefs().viewMode ?? "card"
1386
+ );
927
1387
  const [expandedCards, setExpandedCards] = (React.useState ?? ((v) => [v, () => {
928
1388
  }]))({});
929
1389
  const [batchProgress, setBatchProgress] = (React.useState ?? ((v) => [v, () => {
@@ -1211,6 +1671,50 @@ function renderPage(ctx) {
1211
1671
  event.preventDefault?.();
1212
1672
  apply2(parts);
1213
1673
  };
1674
+ const quickImportSelect = (key, apply2) => {
1675
+ const detected = state.environmentProxy ?? null;
1676
+ const select = h(
1677
+ "select",
1678
+ {
1679
+ className: import_client.default.input,
1680
+ style: { ...S.input, flex: "none", width: "auto" },
1681
+ key: `${key}:select`,
1682
+ value: "",
1683
+ "aria-label": t.quickImport,
1684
+ onChange: (event) => {
1685
+ const value = String(event.target.value ?? "");
1686
+ if (value.length === 0) return;
1687
+ if (value === "environment") {
1688
+ if (detected === null) return;
1689
+ apply2({
1690
+ protocol: detected.protocol,
1691
+ host: detected.host,
1692
+ port: detected.port
1693
+ });
1694
+ return;
1695
+ }
1696
+ const preset = proxyClientPreset(value);
1697
+ if (preset === void 0) return;
1698
+ apply2({ protocol: preset.protocol, host: preset.host, port: preset.port });
1699
+ }
1700
+ },
1701
+ h("option", { key: "none", value: "" }, t.quickImport),
1702
+ ...PROXY_CLIENT_PRESETS.map(
1703
+ (preset) => h("option", { key: preset.id, value: preset.id }, preset.label)
1704
+ ),
1705
+ h(
1706
+ "option",
1707
+ { key: "environment", value: "environment", disabled: detected === null },
1708
+ detected === null ? t.quickImportEnvNone : `${t.quickImportEnv}${detected.source}`
1709
+ )
1710
+ );
1711
+ return h(
1712
+ "div",
1713
+ { key, style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: "0" } },
1714
+ select,
1715
+ detected !== null && detected.hadCredentials ? h("span", { style: { ...S.hint, gridColumn: "auto" } }, t.quickImportEnvCredentials) : null
1716
+ );
1717
+ };
1214
1718
  const field = (label, control, hint, hintError, fullWidth) => h(
1215
1719
  "label",
1216
1720
  {
@@ -1238,12 +1742,206 @@ function renderPage(ctx) {
1238
1742
  hint
1239
1743
  )
1240
1744
  );
1745
+ const panel = (options) => h(
1746
+ "details",
1747
+ {
1748
+ className: `${import_client.default.diag} ${import_client.default.panel}`,
1749
+ style: S.diag,
1750
+ key: options.key,
1751
+ ...options.open === void 0 ? {} : { open: options.open },
1752
+ ...options.onToggle === void 0 ? {} : {
1753
+ onToggle: (event) => {
1754
+ const open = event?.currentTarget?.open;
1755
+ if (typeof open === "boolean") options.onToggle?.(open);
1756
+ }
1757
+ }
1758
+ },
1759
+ h("summary", { className: import_client.default.diagSummary }, options.summary),
1760
+ h(
1761
+ "div",
1762
+ { style: S.panelBody },
1763
+ options.lead === void 0 ? null : h("p", { className: import_client.default.defaultLead, style: S.defaultLead }, options.lead),
1764
+ ...options.children
1765
+ )
1766
+ );
1241
1767
  const storedDefault = snapshot.value?.defaultProxy ?? {};
1242
1768
  const defaultView = view({ ...storedDefault, ...defaultDraft });
1243
1769
  const defaultWhy = defaultProxyProblem(defaultView);
1244
1770
  const defaultHostWhy = defaultView.host.trim().length === 0 ? void 0 : proxyHostProblem(defaultView.host);
1245
1771
  const defaultComplete = defaultWhy === void 0;
1246
1772
  const defaultOrigin = `${defaultView.protocol}://${defaultView.host}:${defaultView.port}`;
1773
+ const cardEndpoint = (route) => {
1774
+ const binding = routeBindingOf(route);
1775
+ if (binding.kind === "profile") {
1776
+ const endpoint = profileValueOf(binding.profileId);
1777
+ if (endpoint === void 0) return { binding, missingProfile: binding.profileId, complete: false };
1778
+ const why = defaultProxyProblem(endpoint);
1779
+ return { binding, endpoint, why, complete: why === void 0 };
1780
+ }
1781
+ if (binding.kind === "default") {
1782
+ return { binding, endpoint: defaultView, why: defaultWhy, complete: defaultComplete };
1783
+ }
1784
+ const complete = route.host.trim().length > 0 && Number.isInteger(route.port) && route.port >= 1 && route.port <= 65535;
1785
+ return { binding, why: defaultProxyProblem(route), complete };
1786
+ };
1787
+ const cardProblemText = (state2) => {
1788
+ if (state2.missingProfile !== void 0) {
1789
+ return `${t.profileMissing}${state2.missingProfile}${t.profileMissingSuffix}`;
1790
+ }
1791
+ if (state2.why === void 0) return void 0;
1792
+ if (state2.binding.kind === "profile") {
1793
+ return `${t.profileUnusable}${state2.binding.profileId}${t.profileUnusableSuffix}${state2.why}`;
1794
+ }
1795
+ if (state2.binding.kind === "default") return `${t.defaultMissing}${state2.why}`;
1796
+ return state2.why;
1797
+ };
1798
+ const storedProfiles = snapshot.value?.proxyProfiles ?? {};
1799
+ const profileEntries = profileNames({ ...storedProfiles, ...profileDraft }).map((name) => ({
1800
+ name,
1801
+ value: profileView({ ...storedProfiles[name], ...profileDraft[name] })
1802
+ }));
1803
+ const profileValueOf = (name) => {
1804
+ if (storedProfiles[name] === void 0 && profileDraft[name] === void 0) return void 0;
1805
+ return profileView({ ...storedProfiles[name], ...profileDraft[name] });
1806
+ };
1807
+ const writeProfiles = (key, next, label, onRefused) => {
1808
+ const full = {};
1809
+ for (const [name, value] of Object.entries(next)) full[name] = profileView(value);
1810
+ saveMany(key, [{ op: "set", path: ["proxyProfiles"], value: full }], label, onRefused);
1811
+ };
1812
+ const addProfile = () => {
1813
+ const name = String(newProfileName ?? "").trim();
1814
+ const problem = profileNameProblem(name);
1815
+ if (problem !== void 0) {
1816
+ setSavedNote(void 0);
1817
+ setSaveError({ label: t.profileNameLabel, message: problem });
1818
+ return;
1819
+ }
1820
+ if (storedProfiles[name] !== void 0 || profileDraft[name] !== void 0) {
1821
+ setSavedNote(void 0);
1822
+ setSaveError({ label: t.profileNameLabel, message: t.profileNameTaken });
1823
+ return;
1824
+ }
1825
+ const next = { ...storedProfiles, ...profileDraft };
1826
+ next[name] = profileView({});
1827
+ setNewProfileName("");
1828
+ setProfileDraft((current) => ({ ...current, [name]: profileView({}) }));
1829
+ setProfileManagerOpen(true);
1830
+ writeProfiles(`\0profile\0add`, next, `${t.profileNameLabel} ${name}`, () => {
1831
+ setProfileDraft((current) => {
1832
+ if (current[name] === void 0) return current;
1833
+ const nextDraft = { ...current };
1834
+ delete nextDraft[name];
1835
+ return nextDraft;
1836
+ });
1837
+ });
1838
+ };
1839
+ const renameProfile = (from) => {
1840
+ const to = String(renameDraft ?? "").trim();
1841
+ setRenamingProfile(void 0);
1842
+ setRenameDraft("");
1843
+ if (to === from) return;
1844
+ const problem = profileNameProblem(to);
1845
+ if (problem !== void 0) {
1846
+ setSavedNote(void 0);
1847
+ setSaveError({ label: t.profileNameLabel, message: problem });
1848
+ return;
1849
+ }
1850
+ if (storedProfiles[to] !== void 0 || profileDraft[to] !== void 0) {
1851
+ setSavedNote(void 0);
1852
+ setSaveError({ label: t.profileNameLabel, message: t.profileNameTaken });
1853
+ return;
1854
+ }
1855
+ const source = profileValueOf(from) ?? profileView({});
1856
+ const next = { ...storedProfiles, ...profileDraft };
1857
+ delete next[from];
1858
+ next[to] = source;
1859
+ const bound = routesBoundTo({ ...configured, ...draft }, from);
1860
+ setProfileDraft((current) => {
1861
+ const nextDraft = { ...current };
1862
+ delete nextDraft[from];
1863
+ nextDraft[to] = source;
1864
+ return nextDraft;
1865
+ });
1866
+ if (bound.length > 0) {
1867
+ setDraft((current) => {
1868
+ const nextDraft = { ...current };
1869
+ for (const id of bound) nextDraft[id] = { ...nextDraft[id], profileId: to };
1870
+ return nextDraft;
1871
+ });
1872
+ }
1873
+ invalidateTests(bound);
1874
+ saveMany(
1875
+ "\0profile\0rename",
1876
+ [
1877
+ { op: "set", path: ["proxyProfiles"], value: next },
1878
+ ...bound.map((id) => ({ op: "set", path: ["providers", id, "profileId"], value: to }))
1879
+ ],
1880
+ `${t.profileNameLabel} ${from} \u2192 ${to}`,
1881
+ () => void 0
1882
+ );
1883
+ };
1884
+ const deleteProfile = (name) => {
1885
+ const bound = routesBoundTo({ ...configured, ...draft }, name);
1886
+ if (globalThis.window?.confirm?.(
1887
+ bound.length === 0 ? t.profileDeleteConfirm : `${t.profileDeleteInUse}${bound.join("\u3001")}
1888
+
1889
+ ${t.profileDeleteConfirm}`
1890
+ ) !== true) {
1891
+ return;
1892
+ }
1893
+ const next = { ...storedProfiles, ...profileDraft };
1894
+ delete next[name];
1895
+ setProfileDraft((current) => {
1896
+ const nextDraft = { ...current };
1897
+ delete nextDraft[name];
1898
+ return nextDraft;
1899
+ });
1900
+ invalidateTests(bound);
1901
+ writeProfiles(`\0profile\0delete`, next, `${t.profileNameLabel} ${name}`, () => void 0);
1902
+ };
1903
+ const bindProfile = (name, field2) => (value) => {
1904
+ editProfile(name, field2, value);
1905
+ invalidateTests(routesBoundTo({ ...configured, ...draft }, name));
1906
+ save(
1907
+ `\0profile\0${name}\0${field2}`,
1908
+ ["proxyProfiles", name, field2],
1909
+ `${name}.${field2}`,
1910
+ value,
1911
+ () => {
1912
+ setProfileDraft((current) => {
1913
+ const entry = current[name];
1914
+ if (entry === void 0 || entry[field2] === void 0) return current;
1915
+ const nextEntry = { ...entry };
1916
+ delete nextEntry[field2];
1917
+ const nextDraft = { ...current };
1918
+ if (Object.keys(nextEntry).length === 0) delete nextDraft[name];
1919
+ else nextDraft[name] = nextEntry;
1920
+ return nextDraft;
1921
+ });
1922
+ }
1923
+ );
1924
+ };
1925
+ const bindRouteProfile = (id) => (value) => {
1926
+ const fields = bindingFields(
1927
+ value === DEFAULT_PROFILE_BINDING ? { kind: "default" } : value.length === 0 ? { kind: "own" } : { kind: "profile", profileId: value }
1928
+ );
1929
+ edit(id, "profileId", fields.profileId);
1930
+ edit(id, "useDefault", fields.useDefault);
1931
+ invalidateTests([id]);
1932
+ saveMany(
1933
+ `${id}\0*profile`,
1934
+ [
1935
+ { op: "set", path: ["providers", id, "profileId"], value: fields.profileId },
1936
+ { op: "set", path: ["providers", id, "useDefault"], value: fields.useDefault }
1937
+ ],
1938
+ `${id}.profileId`,
1939
+ () => {
1940
+ rollbackRouteField(id, "profileId");
1941
+ rollbackRouteField(id, "useDefault");
1942
+ }
1943
+ );
1944
+ };
1247
1945
  const defaultBlock = h(
1248
1946
  "div",
1249
1947
  { className: import_client.default.defaultCard, style: S.defaultCard, key: "default-proxy" },
@@ -1265,6 +1963,11 @@ function renderPage(ctx) {
1265
1963
  )
1266
1964
  ),
1267
1965
  h("p", { className: import_client.default.defaultLead, style: S.defaultLead }, t.defaultLead),
1966
+ h(
1967
+ "div",
1968
+ { style: S.testRow, key: "default:quick-import" },
1969
+ quickImportSelect("default:quick-import", applyDefaultUrl)
1970
+ ),
1268
1971
  h(
1269
1972
  "div",
1270
1973
  { className: import_client.default.formGrid, style: S.formGrid, key: "default:form-grid" },
@@ -1278,7 +1981,7 @@ function renderPage(ctx) {
1278
1981
  value: defaultView.protocol,
1279
1982
  onChange: (event) => bindDefault("protocol")(event.target.value)
1280
1983
  },
1281
- ...PROTOCOLS.map(
1984
+ ...PROTOCOLS2.map(
1282
1985
  (protocol) => h("option", { key: protocol, value: protocol }, protocol.toUpperCase())
1283
1986
  )
1284
1987
  )
@@ -1363,79 +2066,446 @@ function renderPage(ctx) {
1363
2066
  )
1364
2067
  );
1365
2068
  const storedTestUrl = typeof snapshot.value?.testUrl === "string" ? snapshot.value.testUrl : DEFAULT_TEST_URL;
2069
+ const profileManager = panel({
2070
+ key: "profile-manager",
2071
+ summary: `${t.profileManagerTitle} (${profileEntries.length})`,
2072
+ lead: t.profileManagerLead,
2073
+ open: profileManagerOpen === true,
2074
+ onToggle: (open) => setProfileManagerOpen(open),
2075
+ children: [
2076
+ h(
2077
+ "div",
2078
+ { style: { ...S.testRow, alignItems: "stretch" }, key: "profile:add" },
2079
+ h("input", {
2080
+ className: import_client.default.input,
2081
+ style: { ...S.input, flex: "1 1 160px", minWidth: "0" },
2082
+ placeholder: t.profileNamePlaceholder,
2083
+ value: newProfileName,
2084
+ "aria-label": t.profileNameLabel,
2085
+ onChange: (event) => setNewProfileName(event.target.value),
2086
+ // Enter is the obvious gesture in a one-field form; without it the user
2087
+ // has to reach for the button after typing, which is the same dead end
2088
+ // the native prompt left them in.
2089
+ onKeyDown: (event) => {
2090
+ if (event?.key !== "Enter") return;
2091
+ event.preventDefault?.();
2092
+ addProfile();
2093
+ }
2094
+ }),
2095
+ h(
2096
+ "button",
2097
+ {
2098
+ className: import_client.default.testButton,
2099
+ style: { ...S.testButton, flex: "none" },
2100
+ type: "button",
2101
+ onClick: addProfile
2102
+ },
2103
+ t.profileAdd
2104
+ )
2105
+ ),
2106
+ // The built-in preset is EDITED in the default block above (one source of
2107
+ // truth); here it is only listed, so the picker's options and this list
2108
+ // agree.
2109
+ h(
2110
+ "div",
2111
+ { key: "profile:builtin", style: S.inheritNote },
2112
+ `${DEFAULT_PROFILE_LABEL} \u2014 ${endpointSummary(defaultView)}`
2113
+ ),
2114
+ ...profileEntries.map((entry) => {
2115
+ const bound = routesBoundTo({ ...configured, ...draft }, entry.name);
2116
+ const why = defaultProxyProblem(entry.value);
2117
+ return h(
2118
+ "div",
2119
+ {
2120
+ key: `profile:${entry.name}`,
2121
+ className: import_client.default.profileCard,
2122
+ style: {
2123
+ display: "flex",
2124
+ flexDirection: "column",
2125
+ gap: "8px",
2126
+ padding: "12px 14px",
2127
+ border: "1px solid var(--dsw-alias-border-l1)",
2128
+ borderRadius: "10px",
2129
+ background: "var(--dsw-alias-bg-layer-1)"
2130
+ }
2131
+ },
2132
+ h(
2133
+ "div",
2134
+ { style: S.head },
2135
+ h(
2136
+ "div",
2137
+ { style: S.titles },
2138
+ h("span", { style: S.name }, entry.name),
2139
+ h(
2140
+ "span",
2141
+ {
2142
+ className: why === void 0 ? import_client.default.pillOn : import_client.default.pillOff,
2143
+ style: { ...S.pill, ...why === void 0 ? S.pillOn : S.pillOff }
2144
+ },
2145
+ why === void 0 ? t.complete : t.notConfigured
2146
+ )
2147
+ ),
2148
+ h(
2149
+ "div",
2150
+ { style: { display: "flex", gap: "6px", flex: "none" } },
2151
+ h(
2152
+ "button",
2153
+ {
2154
+ className: import_client.default.testButton,
2155
+ style: { ...S.testButton, padding: "3px 8px", fontSize: "11px" },
2156
+ type: "button",
2157
+ onClick: () => {
2158
+ setRenamingProfile(entry.name);
2159
+ setRenameDraft(entry.name);
2160
+ }
2161
+ },
2162
+ t.profileRename
2163
+ ),
2164
+ h(
2165
+ "button",
2166
+ {
2167
+ className: import_client.default.testButton,
2168
+ style: { ...S.testButton, padding: "3px 8px", fontSize: "11px" },
2169
+ type: "button",
2170
+ onClick: () => deleteProfile(entry.name)
2171
+ },
2172
+ t.profileDelete
2173
+ )
2174
+ )
2175
+ ),
2176
+ // The rename field, shown in place of the row title's edit gesture.
2177
+ // Inline rather than a modal, for the same reason the add field is
2178
+ // inline: the Desktop build has no `window.prompt`.
2179
+ renamingProfile !== entry.name ? null : h(
2180
+ "div",
2181
+ { style: { ...S.testRow, alignItems: "stretch" }, key: `profile:${entry.name}:rename` },
2182
+ h("input", {
2183
+ className: import_client.default.input,
2184
+ style: { ...S.input, flex: "1 1 160px", minWidth: "0" },
2185
+ value: renameDraft,
2186
+ placeholder: t.profileRenamePrompt,
2187
+ "aria-label": t.profileRenamePrompt,
2188
+ autoFocus: true,
2189
+ onChange: (event) => setRenameDraft(event.target.value),
2190
+ onKeyDown: (event) => {
2191
+ if (event?.key === "Escape") {
2192
+ setRenamingProfile(void 0);
2193
+ setRenameDraft("");
2194
+ return;
2195
+ }
2196
+ if (event?.key !== "Enter") return;
2197
+ event.preventDefault?.();
2198
+ renameProfile(entry.name);
2199
+ }
2200
+ }),
2201
+ h(
2202
+ "button",
2203
+ {
2204
+ className: import_client.default.testButton,
2205
+ style: { ...S.testButton, flex: "none" },
2206
+ type: "button",
2207
+ onClick: () => renameProfile(entry.name)
2208
+ },
2209
+ t.profileRenameConfirm
2210
+ ),
2211
+ h(
2212
+ "button",
2213
+ {
2214
+ className: import_client.default.testButton,
2215
+ style: { ...S.testButton, flex: "none" },
2216
+ type: "button",
2217
+ onClick: () => {
2218
+ setRenamingProfile(void 0);
2219
+ setRenameDraft("");
2220
+ }
2221
+ },
2222
+ t.cancel
2223
+ )
2224
+ ),
2225
+ bound.length === 0 ? null : h("p", { style: S.inheritNote }, `${t.profileUsedBy}${bound.join("\u3001")}`),
2226
+ h(
2227
+ "div",
2228
+ { style: S.testRow },
2229
+ quickImportSelect(`profile:${entry.name}:quick-import`, (parts) => {
2230
+ setProfileDraft((current) => ({
2231
+ ...current,
2232
+ [entry.name]: { ...current[entry.name], ...parts }
2233
+ }));
2234
+ invalidateTests(bound);
2235
+ saveMany(
2236
+ `\0profile\0${entry.name}\0*quick`,
2237
+ Object.keys(parts).map((field2) => ({
2238
+ op: "set",
2239
+ path: ["proxyProfiles", entry.name, field2],
2240
+ value: parts[field2]
2241
+ })),
2242
+ `${entry.name}\uFF08\u5FEB\u6377\u5BFC\u5165\uFF09`,
2243
+ () => {
2244
+ setProfileDraft((current) => {
2245
+ const value = current[entry.name];
2246
+ if (value === void 0) return current;
2247
+ const nextValue = { ...value };
2248
+ for (const field2 of Object.keys(parts)) delete nextValue[field2];
2249
+ const next = { ...current };
2250
+ if (Object.keys(nextValue).length === 0) delete next[entry.name];
2251
+ else next[entry.name] = nextValue;
2252
+ return next;
2253
+ });
2254
+ }
2255
+ );
2256
+ })
2257
+ ),
2258
+ h(
2259
+ "div",
2260
+ { className: import_client.default.formGrid, style: S.formGrid },
2261
+ field(
2262
+ t.protocol,
2263
+ h(
2264
+ "select",
2265
+ {
2266
+ className: import_client.default.input,
2267
+ style: S.input,
2268
+ value: entry.value.protocol,
2269
+ onChange: (event) => bindProfile(entry.name, "protocol")(event.target.value)
2270
+ },
2271
+ ...PROTOCOLS2.map(
2272
+ (protocol) => h("option", { key: protocol, value: protocol }, protocol.toUpperCase())
2273
+ )
2274
+ ),
2275
+ void 0
2276
+ ),
2277
+ field(
2278
+ t.host,
2279
+ h("input", {
2280
+ className: import_client.default.input,
2281
+ style: S.input,
2282
+ placeholder: "127.0.0.1",
2283
+ value: entry.value.host,
2284
+ onChange: (event) => bindProfile(entry.name, "host")(event.target.value)
2285
+ }),
2286
+ entry.value.host.trim().length === 0 ? void 0 : proxyHostProblem(entry.value.host),
2287
+ entry.value.host.trim().length > 0 && proxyHostProblem(entry.value.host) !== void 0
2288
+ ),
2289
+ field(
2290
+ t.port,
2291
+ h("input", {
2292
+ className: import_client.default.input,
2293
+ style: S.input,
2294
+ type: "number",
2295
+ min: 1,
2296
+ max: 65535,
2297
+ placeholder: "7891",
2298
+ value: entry.value.port === 0 ? "" : String(entry.value.port),
2299
+ onChange: (event) => {
2300
+ const parsed = Number.parseInt(event.target.value, 10);
2301
+ bindProfile(entry.name, "port")(Number.isNaN(parsed) ? 0 : parsed);
2302
+ }
2303
+ }),
2304
+ void 0
2305
+ ),
2306
+ field(
2307
+ t.username,
2308
+ h("input", {
2309
+ className: import_client.default.input,
2310
+ style: S.input,
2311
+ autoComplete: "off",
2312
+ value: entry.value.username,
2313
+ onChange: (event) => bindProfile(entry.name, "username")(event.target.value)
2314
+ }),
2315
+ void 0
2316
+ ),
2317
+ field(
2318
+ t.password,
2319
+ h(
2320
+ "div",
2321
+ { style: S.passwordControl },
2322
+ h("input", {
2323
+ className: import_client.default.input,
2324
+ style: { ...S.input, paddingRight: "36px" },
2325
+ type: "password",
2326
+ autoComplete: "off",
2327
+ value: entry.value.password,
2328
+ onChange: (event) => bindProfile(entry.name, "password")(event.target.value)
2329
+ })
2330
+ ),
2331
+ t.passwordHintProxy,
2332
+ false,
2333
+ true
2334
+ )
2335
+ )
2336
+ );
2337
+ })
2338
+ ]
2339
+ });
1366
2340
  const storedLocalDirect = snapshot.value?.localDirect !== false;
1367
2341
  const globalTestUrl = typeof globalDraft.testUrl === "string" ? globalDraft.testUrl : storedTestUrl;
1368
2342
  const globalLocalDirect = typeof globalDraft.localDirect === "boolean" ? globalDraft.localDirect : storedLocalDirect;
1369
2343
  const globalTestUrlHint = testUrlHint(globalTestUrl, t);
1370
- const globalBlock = h(
1371
- "details",
1372
- {
1373
- className: import_client.default.diag,
1374
- style: {
1375
- ...S.diag,
1376
- marginTop: "0"
2344
+ const exportBackup = () => {
2345
+ const includePasswords = globalThis.window?.confirm?.(t.backupIncludePasswords) === true;
2346
+ const document2 = buildBackup(
2347
+ {
2348
+ providers: { ...configured, ...draft },
2349
+ proxyProfiles: { ...storedProfiles, ...profileDraft },
2350
+ defaultProxy: { ...storedDefault, ...defaultDraft },
2351
+ localDirect: globalLocalDirect,
2352
+ testUrl: globalTestUrl
1377
2353
  },
1378
- key: "global-settings"
1379
- },
1380
- h("summary", { className: import_client.default.diagSummary }, t.globalTitle),
1381
- h("p", { className: import_client.default.defaultLead, style: S.defaultLead }, t.globalLead),
2354
+ { includePasswords }
2355
+ );
2356
+ try {
2357
+ const text = JSON.stringify(document2, null, 2);
2358
+ const blob = new globalThis.Blob([text], { type: "application/json" });
2359
+ const url = globalThis.URL.createObjectURL(blob);
2360
+ const anchor = globalThis.document.createElement("a");
2361
+ anchor.href = url;
2362
+ anchor.download = backupFileName();
2363
+ anchor.click();
2364
+ globalThis.setTimeout(() => globalThis.URL.revokeObjectURL(url), 0);
2365
+ setSavedNote(`${t.backupExported}${includePasswords ? t.backupWithPasswords : t.backupWithoutPasswords}`);
2366
+ } catch (error) {
2367
+ setSavedNote(void 0);
2368
+ setSaveError({ label: t.backupExport, message: String(error?.message ?? error) });
2369
+ }
2370
+ };
2371
+ const importBackupFile = async (file) => {
2372
+ let text;
2373
+ try {
2374
+ text = await file.text();
2375
+ } catch (error) {
2376
+ setSaveError({ label: t.backupImport, message: String(error?.message ?? error) });
2377
+ return;
2378
+ }
2379
+ const parsed = parseBackup(text);
2380
+ if (!parsed.ok) {
2381
+ setSavedNote(void 0);
2382
+ setSaveError({ label: t.backupImport, message: parsed.error });
2383
+ return;
2384
+ }
2385
+ const counts = backupCounts(parsed.document);
2386
+ const confirmed = globalThis.window?.confirm?.(
2387
+ `${t.backupConfirm}${counts.providers}${t.backupConfirmProviders}${counts.profiles}${t.backupConfirmProfiles}`
2388
+ );
2389
+ if (confirmed !== true) return;
2390
+ try {
2391
+ await enqueueWrite(() => writeConfig(ctx, backupOps(parsed.document)));
2392
+ setSaveError(void 0);
2393
+ setSavedNote(`${t.backupRestored}${counts.providers}`);
2394
+ setDraft({});
2395
+ setDefaultDraft({});
2396
+ setProfileDraft({});
2397
+ setGlobalDraft({});
2398
+ } catch (error) {
2399
+ setSavedNote(void 0);
2400
+ setSaveError({ label: t.backupImport, message: String(error?.message ?? error) });
2401
+ }
2402
+ };
2403
+ const backupBlock = h(
2404
+ "div",
2405
+ { style: S.backupBar, key: "backup" },
1382
2406
  h(
1383
- "label",
1384
- { className: import_client.default.field, style: S.field },
1385
- h("span", { className: import_client.default.label, style: S.label }, t.testUrl),
1386
- h("input", {
1387
- className: import_client.default.input,
1388
- style: S.input,
1389
- value: globalTestUrl,
1390
- onChange: (event) => bindGlobal("testUrl")(event.target.value)
1391
- }),
1392
- globalTestUrlHint === void 0 ? h("span", { className: import_client.default.hint, style: S.hint }, t.testUrlHint) : h(
1393
- "span",
1394
- {
1395
- className: import_client.default.hint,
1396
- style: {
1397
- ...S.hint,
1398
- color: "var(--dsw-alias-state-error-primary, #f85149)"
1399
- }
1400
- },
1401
- globalTestUrlHint
1402
- )
2407
+ "div",
2408
+ { style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: "0" } },
2409
+ h("span", { style: { ...S.name, fontSize: "12px" } }, t.backupTitle),
2410
+ h("span", { style: S.defaultLead }, t.backupLead)
1403
2411
  ),
1404
2412
  h(
1405
2413
  "div",
1406
- { style: S.testRow },
2414
+ { style: { ...S.testRow, flex: "none" } },
1407
2415
  h(
1408
2416
  "button",
1409
2417
  {
1410
2418
  className: import_client.default.testButton,
1411
2419
  style: S.testButton,
1412
2420
  type: "button",
1413
- onClick: () => bindGlobal("testUrl")(DEFAULT_TEST_URL)
2421
+ onClick: exportBackup
2422
+ },
2423
+ t.backupExport
2424
+ ),
2425
+ h(
2426
+ "label",
2427
+ {
2428
+ className: import_client.default.testButton,
2429
+ style: { ...S.testButton, display: "inline-flex", alignItems: "center" },
2430
+ key: "backup:import"
1414
2431
  },
1415
- t.restoreDefaultTestUrl
2432
+ t.backupImport,
2433
+ h("input", {
2434
+ type: "file",
2435
+ accept: "application/json,.json",
2436
+ style: { display: "none" },
2437
+ onChange: (event) => {
2438
+ const file = event?.target?.files?.[0];
2439
+ if (file !== void 0) void importBackupFile(file);
2440
+ if (event?.target !== void 0) event.target.value = "";
2441
+ }
2442
+ })
1416
2443
  )
1417
- ),
1418
- h(
1419
- "label",
1420
- { className: import_client.default.toggle, style: S.toggle },
1421
- h("input", {
1422
- type: "checkbox",
1423
- checked: globalLocalDirect,
1424
- onChange: (event) => bindGlobal("localDirect")(event.target.checked === true)
1425
- }),
1426
- h("span", null, t.localDirect)
1427
- ),
1428
- h(
1429
- "p",
1430
- { className: import_client.default.hint, style: { ...S.hint, margin: "2px 0 0" } },
1431
- globalLocalDirect ? t.localDirectOnHint : t.localDirectOffHint
1432
2444
  )
1433
2445
  );
2446
+ const globalBlock = panel({
2447
+ key: "global-settings",
2448
+ summary: t.globalTitle,
2449
+ lead: t.globalLead,
2450
+ children: [
2451
+ h(
2452
+ "label",
2453
+ { className: import_client.default.field, style: S.field },
2454
+ h("span", { className: import_client.default.label, style: S.label }, t.testUrl),
2455
+ h("input", {
2456
+ className: import_client.default.input,
2457
+ style: S.input,
2458
+ value: globalTestUrl,
2459
+ onChange: (event) => bindGlobal("testUrl")(event.target.value)
2460
+ }),
2461
+ globalTestUrlHint === void 0 ? h("span", { className: import_client.default.hint, style: S.hint }, t.testUrlHint) : h(
2462
+ "span",
2463
+ {
2464
+ className: import_client.default.hint,
2465
+ style: {
2466
+ ...S.hint,
2467
+ color: "var(--dsw-alias-state-error-primary, #f85149)"
2468
+ }
2469
+ },
2470
+ globalTestUrlHint
2471
+ )
2472
+ ),
2473
+ h(
2474
+ "div",
2475
+ { style: S.testRow },
2476
+ h(
2477
+ "button",
2478
+ {
2479
+ className: import_client.default.testButton,
2480
+ style: S.testButton,
2481
+ type: "button",
2482
+ onClick: () => bindGlobal("testUrl")(DEFAULT_TEST_URL)
2483
+ },
2484
+ t.restoreDefaultTestUrl
2485
+ )
2486
+ ),
2487
+ h(
2488
+ "label",
2489
+ { className: import_client.default.toggle, style: S.toggle },
2490
+ h("input", {
2491
+ type: "checkbox",
2492
+ checked: globalLocalDirect,
2493
+ onChange: (event) => bindGlobal("localDirect")(event.target.checked === true)
2494
+ }),
2495
+ h("span", null, t.localDirect)
2496
+ ),
2497
+ h(
2498
+ "p",
2499
+ { className: import_client.default.hint, style: { ...S.hint, margin: "2px 0 0" } },
2500
+ globalLocalDirect ? t.localDirectOnHint : t.localDirectOffHint
2501
+ )
2502
+ ]
2503
+ });
1434
2504
  const routeStateOf = (row) => {
1435
2505
  const route = view({ ...configured[row.id], ...draft[row.id] });
1436
2506
  const recorded = problems[row.id];
1437
- const useDefault = route.useDefault === true;
1438
- const problemText = recorded !== void 0 ? recorded : useDefault ? defaultComplete ? void 0 : `${t.defaultMissing}${defaultWhy}` : route.enabled ? defaultProxyProblem(route) : void 0;
2507
+ const state2 = cardEndpoint(route);
2508
+ const problemText = recorded !== void 0 ? recorded : route.enabled ? cardProblemText(state2) : void 0;
1439
2509
  if (route.enabled && problemText !== void 0) return "rejected";
1440
2510
  if (route.enabled) return "proxied";
1441
2511
  return "direct";
@@ -1449,40 +2519,50 @@ function renderPage(ctx) {
1449
2519
  if (statusFilter === "all") return true;
1450
2520
  return routeStateOf(row) === statusFilter;
1451
2521
  });
1452
- const chipRow = rows.length === 0 ? null : h(
2522
+ const listToolbar = rows.length === 0 ? null : h(
1453
2523
  "div",
1454
- { className: import_client.default.chipRow, style: S.chipRow, key: "status-chips" },
1455
- [
1456
- { id: "all", label: `${t.filterStatusAll} (${matchedKeywordRows.length})` },
1457
- { id: "proxied", label: `${t.filterStatusProxied} (${countActive})` },
1458
- { id: "direct", label: `${t.filterStatusDirect} (${countDirect})` },
1459
- { id: "rejected", label: `${t.filterStatusRejected} (${countRejected})` }
1460
- ].map(
1461
- ({ id, label }) => h(
1462
- "button",
1463
- {
1464
- key: id,
1465
- type: "button",
1466
- className: statusFilter === id ? import_client.default.chipActive : import_client.default.chip,
1467
- style: {
1468
- ...S.chip,
1469
- ...statusFilter === id ? S.chipActive : {}
2524
+ { style: S.toolbar, key: "list-toolbar" },
2525
+ h(
2526
+ "div",
2527
+ { className: import_client.default.chipRow, style: S.chipRow, key: "status-chips" },
2528
+ [
2529
+ { id: "all", label: `${t.filterStatusAll} (${matchedKeywordRows.length})` },
2530
+ { id: "proxied", label: `${t.filterStatusProxied} (${countActive})` },
2531
+ { id: "direct", label: `${t.filterStatusDirect} (${countDirect})` },
2532
+ { id: "rejected", label: `${t.filterStatusRejected} (${countRejected})` }
2533
+ ].map(
2534
+ ({ id, label }) => h(
2535
+ "button",
2536
+ {
2537
+ key: id,
2538
+ type: "button",
2539
+ className: statusFilter === id ? import_client.default.chipActive : import_client.default.chip,
2540
+ style: {
2541
+ ...S.chip,
2542
+ ...statusFilter === id ? S.chipActive : {}
2543
+ },
2544
+ // Selection is a STATE, not a decoration: `aria-pressed`
2545
+ // makes exactly one chip self-describing to assistive tech,
2546
+ // and it is what the regression test reads instead of
2547
+ // trusting a colour.
2548
+ "aria-pressed": statusFilter === id,
2549
+ onClick: () => {
2550
+ setStatusFilter(id);
2551
+ writeViewPrefs({ statusFilter: id });
2552
+ }
1470
2553
  },
1471
- onClick: () => setStatusFilter(id)
1472
- },
1473
- label
2554
+ label
2555
+ )
1474
2556
  )
1475
- )
1476
- );
1477
- const filterRow = rows.length === 0 ? null : field(
1478
- t.filterLabel,
2557
+ ),
1479
2558
  h(
1480
2559
  "div",
1481
- { style: { display: "flex", alignItems: "center", gap: "8px" } },
2560
+ { className: import_client.default.toolbarRow, style: S.toolbarRow, key: "list-actions" },
1482
2561
  h("input", {
1483
2562
  className: import_client.default.input,
1484
- style: S.input,
2563
+ style: { ...S.input, flex: "1 1 180px", minWidth: "0" },
1485
2564
  placeholder: t.filterPlaceholder,
2565
+ "aria-label": t.filterLabel,
1486
2566
  value: filter,
1487
2567
  onChange: (event) => setFilter(event.target.value)
1488
2568
  }),
@@ -1503,7 +2583,11 @@ function renderPage(ctx) {
1503
2583
  className: import_client.default.testButton,
1504
2584
  style: { ...S.testButton, flex: "none", whiteSpace: "nowrap" },
1505
2585
  type: "button",
1506
- onClick: () => setViewMode((m) => m === "card" ? "compact" : "card")
2586
+ onClick: () => setViewMode((m) => {
2587
+ const next = m === "card" ? "compact" : "card";
2588
+ writeViewPrefs({ viewMode: next });
2589
+ return next;
2590
+ })
1507
2591
  },
1508
2592
  viewMode === "card" ? t.viewCompact : t.viewCard
1509
2593
  )
@@ -1540,14 +2624,16 @@ function renderPage(ctx) {
1540
2624
  const result = tests[row.id];
1541
2625
  const onlyDead = deadRuleEntries(route.onlyHosts);
1542
2626
  const bypassDead = deadRuleEntries(route.bypassHosts);
2627
+ const hostTemplate = providerHostTemplate(row.id);
1543
2628
  const useDefault = route.useDefault === true;
1544
- const endpointStored = route.useDefault ? defaultComplete : route.host.trim().length > 0 && Number.isInteger(route.port) && route.port >= 1 && route.port <= 65535;
2629
+ const boundProfile = route.profileId.trim();
2630
+ const endpointState = cardEndpoint(route);
2631
+ const endpointStored = endpointState.complete;
1545
2632
  const showTest = route.enabled || endpointStored;
1546
2633
  const problemText = (() => {
1547
2634
  const recorded = problems[row.id];
1548
2635
  if (recorded !== void 0) return recorded;
1549
- if (useDefault) return defaultComplete ? void 0 : `${t.defaultMissing}${defaultWhy}`;
1550
- return route.enabled ? defaultProxyProblem(route) : void 0;
2636
+ return route.enabled ? cardProblemText(endpointState) : void 0;
1551
2637
  })();
1552
2638
  const rejected = route.enabled && problemText !== void 0;
1553
2639
  const endpointFields = [
@@ -1561,7 +2647,7 @@ function renderPage(ctx) {
1561
2647
  value: route.protocol,
1562
2648
  onChange: (event) => bind(row.id, "protocol")(event.target.value)
1563
2649
  },
1564
- ...PROTOCOLS.map(
2650
+ ...PROTOCOLS2.map(
1565
2651
  (protocol) => h("option", { key: protocol, value: protocol }, protocol.toUpperCase())
1566
2652
  )
1567
2653
  ),
@@ -1669,36 +2755,94 @@ function renderPage(ctx) {
1669
2755
  ),
1670
2756
  h("span", { className: pill.className, style: pill.style }, pill.text)
1671
2757
  ),
2758
+ // The two route-level controls, on ONE row and in ONE shape: each is
2759
+ // a label over its control. They were a bare `label` (checkbox) next to
2760
+ // a two-column `field` (picker), so the card's own row looked like two
2761
+ // unrelated widgets stacked.
1672
2762
  h(
1673
- "label",
1674
- { className: import_client.default.toggle, style: S.toggle },
1675
- h("input", {
1676
- type: "checkbox",
1677
- checked: route.enabled,
1678
- onChange: (event) => bind(row.id, "enabled")(event.target.checked === true)
1679
- }),
1680
- h("span", null, t.useProxy)
1681
- ),
1682
- // Opting into the shared endpoint. The card then shows the address it
1683
- // will actually use — or, when the default is not configured, why the
1684
- // route is refused — instead of a set of empty inputs the user never
1685
- // filled and cannot interpret.
1686
- h(
1687
- "label",
1688
- { className: import_client.default.toggle, style: S.toggle, key: `${row.id}:usedefault` },
1689
- h("input", {
1690
- type: "checkbox",
1691
- checked: useDefault,
1692
- onChange: (event) => bind(row.id, "useDefault")(event.target.checked === true)
1693
- }),
1694
- h("span", null, t.useDefault)
2763
+ "div",
2764
+ { className: import_client.default.controlRow, style: S.controlRow, key: `${row.id}:controls` },
2765
+ h(
2766
+ "label",
2767
+ { style: S.controlCell },
2768
+ h("span", { className: import_client.default.label, style: S.label }, t.enableLabel),
2769
+ h(
2770
+ "span",
2771
+ { className: import_client.default.toggle, style: { ...S.toggle, paddingTop: "0" } },
2772
+ h("input", {
2773
+ type: "checkbox",
2774
+ checked: route.enabled,
2775
+ onChange: (event) => bind(row.id, "enabled")(event.target.checked === true)
2776
+ }),
2777
+ h("span", { style: { fontSize: "12px" } }, t.useProxy)
2778
+ )
2779
+ ),
2780
+ // Which endpoint this card takes.
2781
+ //
2782
+ // Always a picker, never a checkbox: the decision has three possible
2783
+ // shapes (own fields / the built-in default / a named preset), and a
2784
+ // checkbox can only express the first two. Every option carries the
2785
+ // address it will use, because "预设: 香港专线" alone does not tell a
2786
+ // user which port it points at.
2787
+ h(
2788
+ "label",
2789
+ { style: S.controlCell, key: `${row.id}:profile` },
2790
+ h("span", { className: import_client.default.label, style: S.label }, t.profileBinding),
2791
+ h(
2792
+ "select",
2793
+ {
2794
+ className: import_client.default.input,
2795
+ style: S.input,
2796
+ value: boundProfile.length > 0 ? boundProfile : useDefault ? DEFAULT_PROFILE_BINDING : "",
2797
+ onChange: (event) => {
2798
+ const value = String(event.target.value);
2799
+ bindRouteProfile(row.id)(value);
2800
+ }
2801
+ },
2802
+ h("option", { key: "own", value: "" }, t.profileBindingOwn),
2803
+ h(
2804
+ "option",
2805
+ { key: "default", value: DEFAULT_PROFILE_BINDING },
2806
+ `${t.profileBindingPrefix}${DEFAULT_PROFILE_LABEL.replace(
2807
+ " (Default)",
2808
+ ""
2809
+ )} (${endpointAddress(defaultView)})`
2810
+ ),
2811
+ ...profileEntries.map(
2812
+ (entry) => h(
2813
+ "option",
2814
+ { key: entry.name, value: entry.name },
2815
+ `${t.profileBindingPrefix}${entry.name} (${endpointAddress(entry.value)})`
2816
+ )
2817
+ ),
2818
+ // A route may still point at a preset that was deleted while this
2819
+ // page was open. Without an option for it the select would render
2820
+ // blank, which reads as "no choice made".
2821
+ boundProfile.length > 0 && !profileEntries.some((entry) => entry.name === boundProfile) ? h(
2822
+ "option",
2823
+ { key: `missing:${boundProfile}`, value: boundProfile },
2824
+ `${t.profileBindingPrefix}${boundProfile}${t.profileMissingOption}`
2825
+ ) : null
2826
+ )
2827
+ )
1695
2828
  ),
1696
- problemText !== void 0 ? h("p", { key: `${row.id}:problem`, style: S.inheritWarning }, problemText) : useDefault ? h(
2829
+ problemText !== void 0 ? h("p", { key: `${row.id}:problem`, style: S.inheritWarning }, problemText) : boundProfile.length > 0 ? h(
2830
+ "p",
2831
+ { key: `${row.id}:profile-note`, style: S.inheritNote },
2832
+ `${t.inheritingProfile}${boundProfile}\uFF1A${endpointSummary(endpointState.endpoint ?? {})}`
2833
+ ) : useDefault ? h(
1697
2834
  "p",
1698
2835
  { key: `${row.id}:inherit`, style: S.inheritNote },
1699
2836
  `${t.inheritingDefault}${defaultOrigin}`
1700
2837
  ) : null,
1701
- ...useDefault ? [] : [h("div", { className: import_client.default.formGrid, style: S.formGrid, key: `${row.id}:form-grid` }, ...endpointFields)],
2838
+ ...useDefault || boundProfile.length > 0 ? [] : [
2839
+ h(
2840
+ "div",
2841
+ { style: S.testRow, key: `${row.id}:quick-import` },
2842
+ quickImportSelect(`${row.id}:quick-import`, (parts) => applyCardUrl(row.id, parts))
2843
+ ),
2844
+ h("div", { className: import_client.default.formGrid, style: S.formGrid, key: `${row.id}:form-grid` }, ...endpointFields)
2845
+ ],
1702
2846
  // The self-check is offered whenever there is something to check: an
1703
2847
  // enabled route (the Host's table carries it, and a rejected one still
1704
2848
  // reports WHY), or a disabled one whose endpoint is already complete —
@@ -1794,7 +2938,7 @@ function renderPage(ctx) {
1794
2938
  // Promote this card's endpoint to the shared default. Offered only
1795
2939
  // when the card carries its OWN endpoint: an inheriting card would
1796
2940
  // just be copying the default onto itself.
1797
- endpointStored && !useDefault ? h(
2941
+ endpointStored && !useDefault && boundProfile.length === 0 ? h(
1798
2942
  "button",
1799
2943
  {
1800
2944
  className: import_client.default.testButton,
@@ -1822,6 +2966,26 @@ function renderPage(ctx) {
1822
2966
  onlyDead.length === 0 ? t.onlyHostsHint : `${t.ruleDeadHint}${onlyDead.join("\u3001")}`,
1823
2967
  onlyDead.length > 0
1824
2968
  ),
2969
+ // The official-upstream pill. Offered only for a provider whose id
2970
+ // maps to a known template: a button that guessed a hostname for an
2971
+ // arbitrary relay would be worse than no button at all.
2972
+ hostTemplate === void 0 ? null : h(
2973
+ "div",
2974
+ { style: S.chipRow, key: `${row.id}:host-template` },
2975
+ h(
2976
+ "button",
2977
+ {
2978
+ className: import_client.default.chip,
2979
+ style: S.chip,
2980
+ type: "button",
2981
+ title: hostTemplate.hosts,
2982
+ onClick: () => bind(row.id, "onlyHosts")(
2983
+ appendHostRules(route.onlyHosts, hostTemplate.hosts)
2984
+ )
2985
+ },
2986
+ `${t.loadHostTemplate}${hostTemplate.label}: ${hostTemplate.hosts}`
2987
+ )
2988
+ ),
1825
2989
  field(
1826
2990
  t.bypassHosts,
1827
2991
  h("input", {
@@ -1851,6 +3015,7 @@ function renderPage(ctx) {
1851
3015
  )
1852
3016
  );
1853
3017
  const isExpanded = expandedCards[row.id] === true;
3018
+ const endpointText = endpointState.missingProfile !== void 0 ? `${t.profileBindingPrefix}${endpointState.missingProfile}${t.profileMissingOption}` : boundProfile.length > 0 ? `${t.profileBindingPrefix}${boundProfile}: ${endpointSummary(endpointState.endpoint ?? {})}` : useDefault ? `${t.useDefault}: ${defaultOrigin}` : `${route.protocol}://${route.host || "\u2014"}:${route.port || "\u2014"}`;
1854
3019
  const compactRowElement = h(
1855
3020
  "section",
1856
3021
  {
@@ -1858,22 +3023,83 @@ function renderPage(ctx) {
1858
3023
  style: rejected ? { ...S.compactRow, ...S.compactRowRejected } : route.enabled ? { ...S.compactRow, ...S.compactRowActive } : S.compactRow,
1859
3024
  key: `compact:${row.id}`
1860
3025
  },
3026
+ // Row 1 — who this is and which proxy actually carries it. The
3027
+ // endpoint shown is the RESOLVED one, so a preset-bound row never
3028
+ // claims an address it will not use.
1861
3029
  h(
1862
3030
  "div",
1863
- { style: { display: "flex", alignItems: "center", gap: "8px", minWidth: "0" } },
1864
- h("input", {
1865
- type: "checkbox",
1866
- checked: route.enabled,
1867
- onChange: (event) => bind(row.id, "enabled")(event.target.checked === true)
1868
- }),
1869
- h("span", { className: import_client.default.name, style: { ...S.name, maxWidth: "140px" } }, row.name),
3031
+ { style: S.compactLine, key: `compact:${row.id}:info` },
3032
+ h("span", { className: import_client.default.name, style: { ...S.name, maxWidth: "160px" } }, row.name),
1870
3033
  h("span", { className: import_client.default.id, style: S.id }, row.id),
1871
- useDefault ? h("span", { style: S.compactEndpoint }, `${t.useDefault}: ${defaultOrigin}`) : h("span", { style: S.compactEndpoint }, `${route.protocol.toUpperCase()}://${route.host || "\u2014"}:${route.port || "\u2014"}`)
3034
+ h(
3035
+ "span",
3036
+ {
3037
+ style: {
3038
+ ...S.compactEndpoint,
3039
+ ...endpointState.missingProfile !== void 0 || endpointState.why !== void 0 ? { color: "var(--dsw-alias-state-error-primary, #f85149)" } : {}
3040
+ },
3041
+ title: endpointText
3042
+ },
3043
+ endpointText
3044
+ ),
3045
+ lastText === void 0 ? null : h(
3046
+ "span",
3047
+ {
3048
+ style: {
3049
+ ...S.compactEndpoint,
3050
+ ...lastUse[row.id]?.ok === false ? S.inheritWarning : {}
3051
+ }
3052
+ },
3053
+ lastText
3054
+ )
1872
3055
  ),
3056
+ // Row 2 — everything actionable, including the endpoint source. The
3057
+ // source picker lives here rather than in row 1 because it is a
3058
+ // CONTROL: putting it among the read-only facts made the row look
3059
+ // uniform while one element was silently editable.
1873
3060
  h(
1874
3061
  "div",
1875
- { style: { display: "flex", alignItems: "center", gap: "8px", flex: "none" } },
1876
- result?.pending === true ? h("span", { style: { fontSize: "11px", color: "var(--dsw-alias-label-secondary)" } }, t.testRunning) : result?.ok === true ? h("span", { style: { fontSize: "11px", color: latencyColor(result.ms), fontWeight: "500" } }, `\u2713 ${result.ms ?? "\u2014"} ms`) : result?.ok === false ? h("span", { style: { fontSize: "11px", color: "var(--dsw-alias-state-error-primary, #f85149)", fontWeight: "500" } }, `\u2717 ${result.error}`) : lastText !== void 0 ? h("span", { style: { fontSize: "11px", color: "var(--dsw-alias-label-secondary)" } }, lastText) : null,
3062
+ { style: S.compactActions, key: `compact:${row.id}:actions` },
3063
+ h(
3064
+ "label",
3065
+ {
3066
+ style: { display: "inline-flex", alignItems: "center", gap: "6px", flex: "none" }
3067
+ },
3068
+ h("input", {
3069
+ type: "checkbox",
3070
+ checked: route.enabled,
3071
+ onChange: (event) => bind(row.id, "enabled")(event.target.checked === true)
3072
+ }),
3073
+ h("span", { style: { fontSize: "11px", color: "var(--dsw-alias-label-secondary)" } }, t.useProxy)
3074
+ ),
3075
+ h(
3076
+ "select",
3077
+ {
3078
+ className: import_client.default.input,
3079
+ style: { ...S.input, flex: "0 1 190px", minWidth: "120px", padding: "3px 6px" },
3080
+ "aria-label": t.profileBinding,
3081
+ value: boundProfile.length > 0 ? boundProfile : useDefault ? DEFAULT_PROFILE_BINDING : "",
3082
+ onChange: (event) => bindRouteProfile(row.id)(String(event.target.value))
3083
+ },
3084
+ h("option", { key: "own", value: "" }, t.profileBindingOwn),
3085
+ h(
3086
+ "option",
3087
+ { key: "default", value: DEFAULT_PROFILE_BINDING },
3088
+ `${t.profileBindingPrefix}${DEFAULT_PROFILE_LABEL.replace(" (Default)", "")} (${endpointAddress(defaultView)})`
3089
+ ),
3090
+ ...profileEntries.map(
3091
+ (entry) => h(
3092
+ "option",
3093
+ { key: entry.name, value: entry.name },
3094
+ `${t.profileBindingPrefix}${entry.name} (${endpointAddress(entry.value)})`
3095
+ )
3096
+ ),
3097
+ boundProfile.length > 0 && !profileEntries.some((entry) => entry.name === boundProfile) ? h(
3098
+ "option",
3099
+ { key: `missing:${boundProfile}`, value: boundProfile },
3100
+ `${t.profileBindingPrefix}${boundProfile}${t.profileMissingOption}`
3101
+ ) : null
3102
+ ),
1877
3103
  h("span", { className: pill.className, style: pill.style }, pill.text),
1878
3104
  showTest ? h(
1879
3105
  "button",
@@ -1896,6 +3122,44 @@ function renderPage(ctx) {
1896
3122
  },
1897
3123
  isExpanded ? t.collapseDetails : t.expandDetails
1898
3124
  )
3125
+ ),
3126
+ // Row 3 — the self-check outcome, on its OWN line.
3127
+ //
3128
+ // Kept out of the control line so a running or failing check can never
3129
+ // shift the picker or the buttons under the cursor, and so a long
3130
+ // failure message has the full width instead of being squeezed
3131
+ // between two controls. It appears only when there is something to
3132
+ // report: an always-present empty row is just extra height.
3133
+ result === void 0 || result.ok === void 0 ? null : h(
3134
+ "div",
3135
+ { style: S.compactResultLine, key: `compact:${row.id}:result` },
3136
+ result.pending === true ? h("span", { style: { color: "var(--dsw-alias-label-secondary)" } }, t.testRunning) : result.ok === true ? [
3137
+ h(
3138
+ "span",
3139
+ { key: "lat", style: { color: latencyColor(result.ms), fontWeight: "500" } },
3140
+ `\u2713 ${result.ms ?? "\u2014"} ms`
3141
+ ),
3142
+ typeof result.status === "number" ? h("span", { key: "status" }, `HTTP ${result.status}`) : null,
3143
+ result.ip === void 0 ? null : h(
3144
+ "span",
3145
+ { key: "ip", style: { display: "inline-flex", alignItems: "center", gap: "3px" } },
3146
+ `${t.exitIpLabel}${result.ip}`,
3147
+ copyButton(`${row.id}:compact:ip`, String(result.ip), t.copy)
3148
+ ),
3149
+ result.direct === true ? h("span", { key: "direct" }, t.directLabel) : null,
3150
+ result.cached === true ? h("span", { key: "cached" }, t.cachedLabel) : null
3151
+ ] : [
3152
+ h(
3153
+ "span",
3154
+ {
3155
+ key: "err",
3156
+ style: { ...S.testFailure, display: "inline-flex", alignItems: "center", gap: "3px" }
3157
+ },
3158
+ `\u2717 ${result.error}`,
3159
+ copyButton(`${row.id}:compact:err`, String(result.error), t.copy)
3160
+ ),
3161
+ typeof result.status === "number" ? h("span", { key: "status" }, `HTTP ${result.status}`) : null
3162
+ ]
1899
3163
  )
1900
3164
  );
1901
3165
  if (viewMode === "compact") {
@@ -1939,45 +3203,42 @@ function renderPage(ctx) {
1939
3203
  t.close
1940
3204
  )
1941
3205
  ),
1942
- defaultBlock,
1943
- globalBlock,
1944
- catalogueNotice,
1945
- chipRow,
1946
- filterRow,
1947
- ...body,
3206
+ // Page-level settings, as one group: they apply to the whole page, not to
3207
+ // one provider.
1948
3208
  h(
1949
- "details",
1950
- {
1951
- className: import_client.default.diag,
1952
- key: "diag",
1953
- style: {
1954
- marginTop: "4px",
1955
- padding: "8px 10px",
1956
- border: "1px solid var(--dsw-alias-border-l1)",
1957
- borderRadius: "8px",
1958
- fontSize: "11px",
1959
- color: "var(--dsw-alias-label-secondary)"
1960
- }
1961
- },
1962
- // The count is the only visible signal while the panel is collapsed: a
1963
- // rejected route is not a page that refuses to render, it is one that
1964
- // quietly does nothing.
1965
- h(
1966
- "summary",
1967
- { className: import_client.default.diagSummary },
1968
- problemCount === 0 ? t.diagnostics : `${t.diagnostics} (${problemCount})`
1969
- ),
1970
- h(
1971
- "ul",
1972
- { className: import_client.default.diagList, style: { margin: "6px 0 0", paddingLeft: "18px" } },
1973
- ...problemCount === 0 && diagnostics.length === 0 && savedNote === void 0 ? [h("li", { key: "none" }, t.diagPending)] : [
1974
- ...problemLines,
1975
- ...catalogueMismatchLine === void 0 ? [] : [catalogueMismatchLine],
1976
- ...savedNote === void 0 ? [] : [h("li", { key: "saved" }, savedNote)],
1977
- ...diagnostics.map((line, index) => h("li", { key: `d${index}` }, line))
1978
- ]
1979
- )
1980
- )
3209
+ "div",
3210
+ { style: S.group, key: "page-settings" },
3211
+ h("h3", { style: S.groupTitle }, t.sectionSettings),
3212
+ defaultBlock,
3213
+ profileManager,
3214
+ globalBlock,
3215
+ backupBlock
3216
+ ),
3217
+ // The provider list, as its own group: heading, toolbar, then the cards.
3218
+ h(
3219
+ "div",
3220
+ { style: S.group, key: "provider-list" },
3221
+ h("h3", { style: S.groupTitle }, t.sectionProviders),
3222
+ catalogueNotice,
3223
+ listToolbar,
3224
+ ...body
3225
+ ),
3226
+ panel({
3227
+ key: "diag",
3228
+ summary: problemCount === 0 ? t.diagnostics : `${t.diagnostics} (${problemCount})`,
3229
+ children: [
3230
+ h(
3231
+ "ul",
3232
+ { className: import_client.default.diagList, style: { margin: "6px 0 0", paddingLeft: "18px" } },
3233
+ ...problemCount === 0 && diagnostics.length === 0 && savedNote === void 0 ? [h("li", { key: "none" }, t.diagPending)] : [
3234
+ ...problemLines,
3235
+ ...catalogueMismatchLine === void 0 ? [] : [catalogueMismatchLine],
3236
+ ...savedNote === void 0 ? [] : [h("li", { key: "saved" }, savedNote)],
3237
+ ...diagnostics.map((line, index) => h("li", { key: `d${index}` }, line))
3238
+ ]
3239
+ )
3240
+ ]
3241
+ })
1981
3242
  );
1982
3243
  }
1983
3244
  var EN = {
@@ -2071,7 +3332,50 @@ var EN = {
2071
3332
  saveFailedPrefix: "Save failed: ",
2072
3333
  close: "Close",
2073
3334
  copy: "Copy",
2074
- copied: "Copied"
3335
+ copied: "Copied",
3336
+ profileManagerTitle: "Proxy presets",
3337
+ profileManagerLead: 'Named endpoints you can reuse across providers. The built-in "Default preset" is the default proxy block above; edit it there.',
3338
+ profileAdd: "Add preset",
3339
+ profileRename: "Rename",
3340
+ profileDelete: "Delete",
3341
+ profileNameLabel: "Preset name",
3342
+ profileNamePlaceholder: "New preset name, e.g. HK-Proxy",
3343
+ profileNamePrompt: "Name for the new proxy preset:",
3344
+ profileRenamePrompt: "New name for this proxy preset:",
3345
+ profileRenameConfirm: "Save name",
3346
+ cancel: "Cancel",
3347
+ profileNameTaken: "A preset with that name already exists.",
3348
+ profileDeleteConfirm: "Delete this proxy preset?",
3349
+ profileDeleteInUse: "These providers are bound to this preset and will stop being proxied until they are rebound:\n",
3350
+ profileUsedBy: "Used by: ",
3351
+ profileBinding: "Endpoint",
3352
+ profileBindingOwn: "Custom (this card)",
3353
+ profileBindingPrefix: "Preset: ",
3354
+ inheritingProfile: "Using preset ",
3355
+ profileMissing: 'The bound preset "',
3356
+ profileMissingSuffix: '" no longer exists, so this provider is not proxied.',
3357
+ profileMissingOption: " (missing)",
3358
+ profileUnusable: 'The bound preset "',
3359
+ profileUnusableSuffix: '" is not usable, so this provider is not proxied: ',
3360
+ quickImport: "Quick import\u2026",
3361
+ quickImportEnv: "From environment: ",
3362
+ quickImportEnvNone: "From environment (none detected)",
3363
+ quickImportEnvCredentials: "The environment variable also carried credentials; enter them below.",
3364
+ loadHostTemplate: "Load official upstream hosts: ",
3365
+ backupExport: "Back up configuration",
3366
+ backupImport: "Restore configuration",
3367
+ backupIncludePasswords: "Include proxy passwords in the backup?\n\nOK = include, Cancel = leave every password empty.",
3368
+ backupWithPasswords: "Exported backup (passwords included \u2014 store it safely)",
3369
+ backupWithoutPasswords: "Exported backup (passwords omitted)",
3370
+ backupConfirm: "Restore this backup? The current configuration will be replaced.\n\nProviders: ",
3371
+ backupConfirmProviders: ", presets: ",
3372
+ backupConfirmProfiles: " entry/entries.",
3373
+ backupRestored: "Restored from backup, providers: ",
3374
+ backupTitle: "Backup and restore",
3375
+ backupLead: "Save the whole proxy configuration to a JSON file, or load one back.",
3376
+ sectionSettings: "Page settings",
3377
+ sectionProviders: "Providers",
3378
+ enableLabel: "Enabled"
2075
3379
  };
2076
3380
  var ZH = {
2077
3381
  lead: "\u8BA9\u6307\u5B9A\u7684 provider \u8D70\u4F60\u81EA\u5DF1\u7684\u6B63\u5411\u4EE3\u7406\uFF0C\u5176\u5B83 provider \u4FDD\u6301\u76F4\u8FDE\u3002\u6307\u5411\u672C\u673A\u6216\u5185\u7F51\u5730\u5740\uFF08localhost\u3001127.x\u3001\u79C1\u6709\u7F51\u6BB5\uFF09\u7684\u8BF7\u6C42\u9ED8\u8BA4\u76F4\u8FDE\uFF0C\u9876\u5C42 localDirect \u8BBE\u4E3A false \u65F6\u6062\u590D\u8D70\u4EE3\u7406\u2014\u2014\u6B63\u5411\u4EE3\u7406\u901A\u5E38\u591F\u4E0D\u5230\u4F60\u81EA\u5DF1\u7684\u673A\u5668\u3002",
@@ -2164,7 +3468,50 @@ var ZH = {
2164
3468
  saveFailedPrefix: "\u4FDD\u5B58\u5931\u8D25\uFF1A",
2165
3469
  close: "\u5173\u95ED",
2166
3470
  copy: "\u590D\u5236",
2167
- copied: "\u5DF2\u590D\u5236"
3471
+ copied: "\u5DF2\u590D\u5236",
3472
+ profileManagerTitle: "\u4EE3\u7406\u9884\u8BBE",
3473
+ profileManagerLead: "\u53EF\u88AB\u591A\u4E2A provider \u590D\u7528\u7684\u547D\u540D\u7AEF\u70B9\u3002\u5185\u7F6E\u7684\u300C\u9ED8\u8BA4\u9884\u8BBE\u300D\u5C31\u662F\u4E0A\u65B9\u300C\u9ED8\u8BA4\u4EE3\u7406\u300D\u533A\u5757\uFF0C\u76F4\u63A5\u5728\u90A3\u91CC\u7F16\u8F91\u3002",
3474
+ profileAdd: "\u6DFB\u52A0\u9884\u8BBE",
3475
+ profileRename: "\u91CD\u547D\u540D",
3476
+ profileDelete: "\u5220\u9664",
3477
+ profileNameLabel: "\u9884\u8BBE\u540D\u79F0",
3478
+ profileNamePlaceholder: "\u65B0\u9884\u8BBE\u540D\u79F0\uFF0C\u4F8B\u5982 HK-Proxy",
3479
+ profileNamePrompt: "\u65B0\u4EE3\u7406\u9884\u8BBE\u7684\u540D\u79F0\uFF1A",
3480
+ profileRenamePrompt: "\u8BE5\u4EE3\u7406\u9884\u8BBE\u7684\u65B0\u540D\u79F0\uFF1A",
3481
+ profileRenameConfirm: "\u4FDD\u5B58\u540D\u79F0",
3482
+ cancel: "\u53D6\u6D88",
3483
+ profileNameTaken: "\u5DF2\u5B58\u5728\u540C\u540D\u9884\u8BBE\u3002",
3484
+ profileDeleteConfirm: "\u786E\u8BA4\u5220\u9664\u8BE5\u4EE3\u7406\u9884\u8BBE\uFF1F",
3485
+ profileDeleteInUse: "\u4EE5\u4E0B provider \u7ED1\u5B9A\u5728\u8BE5\u9884\u8BBE\u4E0A\uFF0C\u5220\u9664\u540E\u5C06\u4E0D\u518D\u8D70\u4EE3\u7406\uFF0C\u76F4\u5230\u91CD\u65B0\u7ED1\u5B9A\uFF1A\n",
3486
+ profileUsedBy: "\u88AB\u4EE5\u4E0B provider \u4F7F\u7528\uFF1A",
3487
+ profileBinding: "\u7AEF\u70B9\u6765\u6E90",
3488
+ profileBindingOwn: "\u72EC\u7ACB\u81EA\u5B9A\u4E49\u914D\u7F6E",
3489
+ profileBindingPrefix: "\u9884\u8BBE\uFF1A",
3490
+ inheritingProfile: "\u4F7F\u7528\u9884\u8BBE ",
3491
+ profileMissing: "\u7ED1\u5B9A\u7684\u9884\u8BBE\u300C",
3492
+ profileMissingSuffix: "\u300D\u5DF2\u4E0D\u5B58\u5728\uFF0C\u8BE5 provider \u4E0D\u4F1A\u8D70\u4EE3\u7406\u3002",
3493
+ profileMissingOption: "\uFF08\u5DF2\u7F3A\u5931\uFF09",
3494
+ profileUnusable: "\u7ED1\u5B9A\u7684\u9884\u8BBE\u300C",
3495
+ profileUnusableSuffix: "\u300D\u4E0D\u53EF\u7528\uFF0C\u8BE5 provider \u4E0D\u4F1A\u8D70\u4EE3\u7406\uFF1A",
3496
+ quickImport: "\u5E38\u7528\u5BA2\u6237\u7AEF\u2026",
3497
+ quickImportEnv: "\u4ECE\u73AF\u5883\u53D8\u91CF\uFF1A",
3498
+ quickImportEnvNone: "\u4ECE\u73AF\u5883\u53D8\u91CF\uFF08\u672A\u68C0\u6D4B\u5230\uFF09",
3499
+ quickImportEnvCredentials: "\u8BE5\u73AF\u5883\u53D8\u91CF\u8FD8\u5E26\u4E86\u51ED\u636E\uFF0C\u8BF7\u5728\u4E0B\u65B9\u8865\u586B\u3002",
3500
+ loadHostTemplate: "\u8F7D\u5165\u5B98\u65B9\u4E0A\u6E38\u57DF\u540D\uFF1A",
3501
+ backupExport: "\u5907\u4EFD\u914D\u7F6E",
3502
+ backupImport: "\u6062\u590D\u914D\u7F6E",
3503
+ backupIncludePasswords: "\u5907\u4EFD\u4E2D\u662F\u5426\u5305\u542B\u4EE3\u7406\u5BC6\u7801\uFF1F\n\n\u786E\u5B9A = \u5305\u542B\uFF1B\u53D6\u6D88 = \u6240\u6709\u5BC6\u7801\u5B57\u6BB5\u7559\u7A7A\u3002",
3504
+ backupWithPasswords: "\u5DF2\u5BFC\u51FA\u5907\u4EFD\uFF08\u5305\u542B\u5BC6\u7801\uFF0C\u8BF7\u59A5\u5584\u4FDD\u7BA1\uFF09",
3505
+ backupWithoutPasswords: "\u5DF2\u5BFC\u51FA\u5907\u4EFD\uFF08\u672A\u5305\u542B\u5BC6\u7801\uFF09",
3506
+ backupConfirm: "\u786E\u8BA4\u7528\u8BE5\u5907\u4EFD\u8986\u76D6\u5F53\u524D\u914D\u7F6E\uFF1F\n\nprovider\uFF1A",
3507
+ backupConfirmProviders: "\uFF0C\u9884\u8BBE\uFF1A",
3508
+ backupConfirmProfiles: " \u4E2A\u3002",
3509
+ backupRestored: "\u5DF2\u4ECE\u5907\u4EFD\u6062\u590D\uFF0Cprovider\uFF1A",
3510
+ backupTitle: "\u5907\u4EFD\u4E0E\u6062\u590D",
3511
+ backupLead: "\u628A\u6574\u4EFD\u4EE3\u7406\u914D\u7F6E\u5B58\u6210 JSON \u6587\u4EF6\uFF0C\u6216\u4ECE\u6587\u4EF6\u6062\u590D\u3002",
3512
+ sectionSettings: "\u9875\u9762\u8BBE\u7F6E",
3513
+ sectionProviders: "Provider \u5217\u8868",
3514
+ enableLabel: "\u542F\u7528"
2168
3515
  };
2169
3516
  function stringsFor(ctx) {
2170
3517
  const language = localeOf(ctx);