dsh-provider-proxy 0.7.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": "b4bf55bf_root", "lead": "03b72018_lead", "empty": "9a9a9c27_empty", "card": "6d6907d5_card", "cardActive": "a287416b_cardActive", "cardRejected": "aabaa9b3_cardRejected", "defaultCard": "ebf26189_defaultCard", "defaultLead": "5b9e9eb5_defaultLead", "cardHead": "a3319282_cardHead", "cardTitles": "6f64c28d_cardTitles", "name": "847f7e47_name", "id": "7864474d_id", "pillOn": "7ed5399b_pillOn", "pillLive": "0ccb6da5_pillLive", "pillOff": "3accadc4_pillOff", "pillRejected": "86897991_pillRejected", "toggle": "81025292_toggle", "field": "5e2577de_field", "label": "d9087e9b_label", "hint": "aae2a8a2_hint", "input": "feb8c902_input", "passwordToggle": "5645283d_passwordToggle", "testButton": "de8ca09a_testButton", "diag": "4ad17b9d_diag", "diagSummary": "b2f0759b_diagSummary", "diagList": "f49699bb_diagList" };
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.\n Tokens only: no literal colours, matching the shared --dsw-* system. */\n\n.b4bf55bf_root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n max-width: 640px;\n}\n\n.03b72018_lead {\n margin: 0 0 4px;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\n}\n\n.9a9a9c27_empty {\n margin: 0;\n padding: 16px;\n border: 1px dashed var(--dsw-alias-border-l1);\n border-radius: 8px;\n font-size: 12px;\n text-align: center;\n color: var(--dsw-alias-state-idle-primary);\n}\n\n/* ---- provider card ---- */\n\n.6d6907d5_card,\n.a287416b_cardActive,\n.aabaa9b3_cardRejected {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.a287416b_cardActive {\n border-color: var(--dsw-alias-state-success-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* An enabled route the Host refused: never dress it as active. */\n.aabaa9b3_cardRejected {\n border-color: var(--dsw-alias-state-error-primary, #f85149);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* ---- shared default endpoint ---- */\n\n/* A distinct outer layer, so it reads as "one setting for the page" rather than\n as one more provider card. */\n.ebf26189_defaultCard {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 14px 16px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.5b9e9eb5_defaultLead {\n margin: -2px 0 0;\n font-size: 11px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n}\n\n.a3319282_cardHead {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.6f64c28d_cardTitles {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.847f7e47_name {\n overflow: hidden;\n font-size: 13px;\n font-weight: 600;\n white-space: nowrap;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.7864474d_id {\n flex: none;\n padding: 1px 6px;\n border-radius: 4px;\n background: var(--dsw-alias-bg-layer-2);\n font-family: ui-monospace, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* ---- status pill ---- */\n\n.7ed5399b_pillOn,\n.0ccb6da5_pillLive,\n.3accadc4_pillOff,\n.86897991_pillRejected {\n flex: none;\n padding: 2px 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 500;\n white-space: nowrap;\n}\n\n.7ed5399b_pillOn {\n background: var(--dsw-alias-state-success-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.0ccb6da5_pillLive {\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-secondary);\n}\n\n.3accadc4_pillOff {\n color: var(--dsw-alias-state-idle-primary);\n}\n\n.86897991_pillRejected {\n border: 1px solid var(--dsw-alias-state-error-primary, #f85149);\n color: var(--dsw-alias-state-error-primary, #f85149);\n}\n\n/* ---- toggle ---- */\n\n.81025292_toggle {\n display: flex;\n align-items: center;\n gap: 8px;\n padding-top: 2px;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n}\n\n.81025292_toggle input {\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n/* ---- fields ---- */\n\n.5e2577de_field {\n display: grid;\n grid-template-columns: 96px 1fr;\n align-items: center;\n gap: 10px;\n font-size: 12px;\n}\n\n.d9087e9b_label {\n color: var(--dsw-alias-label-secondary);\n}\n\n.aae2a8a2_hint {\n grid-column: 2;\n margin-top: -2px;\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.feb8c902_input {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n padding: 5px 8px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n outline: none;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n transition: border-color 120ms ease, background 120ms ease;\n}\n\n/* The base look of every input is INLINE too, so these three state rules only\n ever take effect as author-important declarations \u2014 same reason as the\n self-check button above. `cursor` has no inline base and needs none, and the\n hover border uses `l3` because `l1`\u2192`l2` is a 4%\u219210% black step nobody can\n see. */\n\n.feb8c902_input:hover:not(:disabled) {\n border-color: var(--dsw-alias-border-l3) !important;\n}\n\n.feb8c902_input:focus {\n border-color: var(--dsw-alias-brand-primary) !important;\n}\n\n.feb8c902_input:disabled {\n color: var(--dsw-alias-state-idle-primary) !important;\n background: var(--dsw-alias-bg-layer-2) !important;\n cursor: not-allowed;\n}\n\n/* The icon sits inside the input, so its hover/focus feedback must be local\n and must outrank the transparent inline base. */\n.5645283d_passwordToggle {\n transition: background 120ms ease, color 120ms ease;\n}\n\n.5645283d_passwordToggle:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover) !important;\n color: var(--dsw-alias-label-primary) !important;\n}\n\n.5645283d_passwordToggle:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* ---- self-check ---- */\n\n/* The hover state is the one thing an inline style cannot express, which is why\n this element carries a class at all. Layout stays inline.\n\n Two traps, both hit before this comment existed:\n - `!important` is load-bearing: the base border and background are INLINE, and\n the cascade sorts a style attribute after every author rule, so a plain\n declaration here would ship, match, and still lose.\n - the TOKENS must be visible ones. `bg-layer-1/2/3` all resolve to\n `neutral-bluish-00` in the light theme, so a layer swap is a no-op there, and\n `border-l1`\u2192`l2` is 4%\u219210% black on a 1px line. The app\'s own hovers use\n `interactive-bg-hover`; using it here is what makes the feedback exist. */\n.de8ca09a_testButton {\n transition: border-color 120ms ease, background 120ms ease;\n}\n\n.de8ca09a_testButton:hover:not(:disabled) {\n border-color: var(--dsw-alias-border-l3) !important;\n background: var(--dsw-alias-interactive-bg-hover) !important;\n}\n\n/* ---- diagnostics ---- */\n\n.4ad17b9d_diag {\n margin-top: 2px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.b2f0759b_diagSummary {\n cursor: pointer;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.f49699bb_diagList {\n margin: 6px 0 0;\n padding-left: 18px;\n font-family: ui-monospace, monospace;\n word-break: break-all;\n}\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",
@@ -666,10 +998,26 @@ var S = {
666
998
  },
667
999
  field: {
668
1000
  display: "grid",
669
- gridTemplateColumns: "88px 1fr",
1001
+ gridTemplateColumns: "76px 1fr",
670
1002
  alignItems: "center",
671
- gap: "10px",
672
- fontSize: "12px"
1003
+ gap: "8px",
1004
+ fontSize: "12px",
1005
+ minWidth: "0"
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"
673
1021
  },
674
1022
  label: { color: "var(--dsw-alias-label-secondary)" },
675
1023
  input: {
@@ -686,17 +1034,180 @@ var S = {
686
1034
  fontSize: "12px"
687
1035
  },
688
1036
  diag: {
689
- marginTop: "4px",
690
- padding: "8px 10px",
1037
+ padding: "10px 14px",
691
1038
  border: "1px solid var(--dsw-alias-border-l1)",
692
- borderRadius: "8px",
1039
+ borderRadius: "10px",
693
1040
  fontSize: "11px",
694
1041
  color: "var(--dsw-alias-label-secondary)"
695
1042
  },
696
1043
  hint: {
697
- gridColumn: "2",
1044
+ gridColumn: "1 / -1",
698
1045
  fontSize: "11px",
699
- color: "var(--dsw-alias-label-secondary)"
1046
+ color: "var(--dsw-alias-state-warn-primary, #d29922)"
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
+ },
1084
+ chipRow: {
1085
+ display: "flex",
1086
+ alignItems: "center",
1087
+ gap: "6px",
1088
+ flexWrap: "wrap",
1089
+ marginBottom: "2px"
1090
+ },
1091
+ chip: {
1092
+ display: "inline-flex",
1093
+ alignItems: "center",
1094
+ padding: "3px 10px",
1095
+ borderRadius: "999px",
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",
1116
+ color: "var(--dsw-alias-label-secondary)",
1117
+ fontSize: "11px",
1118
+ fontWeight: "500",
1119
+ cursor: "pointer",
1120
+ userSelect: "none"
1121
+ },
1122
+ /** The one selected chip: a solid fill, unmistakable at a glance. */
1123
+ chipActive: {
1124
+ borderColor: "var(--dsw-alias-brand-primary)",
1125
+ background: "var(--dsw-alias-brand-primary)",
1126
+ color: "var(--dsw-alias-bg-base)",
1127
+ fontWeight: "600"
1128
+ },
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: {
1142
+ display: "flex",
1143
+ alignItems: "center",
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"
1174
+ },
1175
+ compactRowActive: {
1176
+ borderColor: "var(--dsw-alias-state-success-primary)",
1177
+ background: "var(--dsw-alias-bg-layer-2)"
1178
+ },
1179
+ compactRowRejected: {
1180
+ borderColor: "var(--dsw-alias-state-error-primary, #f85149)",
1181
+ background: "var(--dsw-alias-bg-layer-2)"
1182
+ },
1183
+ compactEndpoint: {
1184
+ fontSize: "11px",
1185
+ color: "var(--dsw-alias-label-secondary)",
1186
+ overflow: "hidden",
1187
+ textOverflow: "ellipsis",
1188
+ whiteSpace: "nowrap",
1189
+ maxWidth: "180px"
1190
+ },
1191
+ formGrid: {
1192
+ display: "grid",
1193
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
1194
+ alignItems: "start",
1195
+ gap: "10px 14px"
1196
+ },
1197
+ formGridFull: {
1198
+ gridColumn: "1 / -1"
1199
+ },
1200
+ copyButton: {
1201
+ display: "inline-flex",
1202
+ alignItems: "center",
1203
+ padding: "2px 4px",
1204
+ border: "none",
1205
+ borderRadius: "4px",
1206
+ background: "transparent",
1207
+ color: "var(--dsw-alias-label-secondary)",
1208
+ cursor: "pointer",
1209
+ fontSize: "11px",
1210
+ lineHeight: "1"
700
1211
  }
701
1212
  };
702
1213
  function renderPage(ctx) {
@@ -733,6 +1244,7 @@ function renderPage(ctx) {
733
1244
  hits: {},
734
1245
  problems: {},
735
1246
  lastUse: {},
1247
+ environmentProxy: null,
736
1248
  error: void 0,
737
1249
  pending: true
738
1250
  };
@@ -743,6 +1255,7 @@ function renderPage(ctx) {
743
1255
  hits: catalog.hits,
744
1256
  problems: catalog.problems,
745
1257
  lastUse: catalog.lastUse,
1258
+ environmentProxy: catalog.environmentProxy ?? null,
746
1259
  error: catalog.error,
747
1260
  pending: false
748
1261
  });
@@ -779,6 +1292,24 @@ function renderPage(ctx) {
779
1292
  const [defaultDraft, setDefaultDraft] = (React.useState ?? ((v) => [v, () => {
780
1293
  }]))({});
781
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
+ );
782
1313
  const [passwordVisible, setPasswordVisible] = (React.useState ?? ((v) => [v, () => {
783
1314
  }]))({});
784
1315
  const [defaultPasswordVisible, setDefaultPasswordVisible] = (React.useState ?? ((v) => [v, () => {
@@ -845,11 +1376,81 @@ function renderPage(ctx) {
845
1376
  };
846
1377
  const [filter, setFilter] = (React.useState ?? ((v) => [v, () => {
847
1378
  }]))("");
1379
+ const [statusFilter, setStatusFilter] = (React.useState ?? ((v) => [v, () => {
1380
+ }]))(
1381
+ () => readViewPrefs().statusFilter ?? "all"
1382
+ );
1383
+ const [viewMode, setViewMode] = (React.useState ?? ((v) => [v, () => {
1384
+ }]))(
1385
+ () => readViewPrefs().viewMode ?? "card"
1386
+ );
1387
+ const [expandedCards, setExpandedCards] = (React.useState ?? ((v) => [v, () => {
1388
+ }]))({});
1389
+ const [batchProgress, setBatchProgress] = (React.useState ?? ((v) => [v, () => {
1390
+ }]))(null);
1391
+ const [copiedKey, setCopiedKey] = (React.useState ?? ((v) => [v, () => {
1392
+ }]))(void 0);
1393
+ const copyToClipboard = (key, text) => {
1394
+ try {
1395
+ if (typeof globalThis.navigator?.clipboard?.writeText === "function") {
1396
+ void globalThis.navigator.clipboard.writeText(text).then(() => {
1397
+ setCopiedKey(key);
1398
+ setTimeout(() => setCopiedKey((prev) => prev === key ? void 0 : prev), 1500);
1399
+ }).catch(() => void 0);
1400
+ }
1401
+ } catch {
1402
+ }
1403
+ };
1404
+ const copyButton = (key, text, ariaLabel) => h(
1405
+ "button",
1406
+ {
1407
+ className: import_client.default.copyButton,
1408
+ style: S.copyButton,
1409
+ type: "button",
1410
+ "aria-label": ariaLabel,
1411
+ title: copiedKey === key ? t.copied : ariaLabel,
1412
+ onClick: (e) => {
1413
+ e.stopPropagation?.();
1414
+ copyToClipboard(key, text);
1415
+ }
1416
+ },
1417
+ copiedKey === key ? h(
1418
+ "svg",
1419
+ {
1420
+ width: 12,
1421
+ height: 12,
1422
+ viewBox: "0 0 24 24",
1423
+ fill: "none",
1424
+ stroke: "var(--dsw-alias-state-success-primary, #2ea043)",
1425
+ strokeWidth: 2,
1426
+ strokeLinecap: "round",
1427
+ strokeLinejoin: "round",
1428
+ "aria-hidden": "true"
1429
+ },
1430
+ h("path", { d: "M20 6 9 17l-5-5" })
1431
+ ) : h(
1432
+ "svg",
1433
+ {
1434
+ width: 12,
1435
+ height: 12,
1436
+ viewBox: "0 0 24 24",
1437
+ fill: "none",
1438
+ stroke: "currentColor",
1439
+ strokeWidth: 2,
1440
+ strokeLinecap: "round",
1441
+ strokeLinejoin: "round",
1442
+ "aria-hidden": "true"
1443
+ },
1444
+ h("rect", { width: 14, height: 14, x: 8, y: 8, rx: 2, ry: 2 }),
1445
+ h("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })
1446
+ ),
1447
+ copiedKey === key ? h("span", { style: { marginLeft: "3px", fontSize: "10px", color: "var(--dsw-alias-state-success-primary, #2ea043)" } }, t.copied) : null
1448
+ );
848
1449
  const runTest = (id) => {
849
1450
  const generation = (testGeneration.get(id) ?? 0) + 1;
850
1451
  testGeneration.set(id, generation);
851
1452
  setTests((current) => ({ ...current, [id]: { pending: true } }));
852
- void (async () => {
1453
+ return (async () => {
853
1454
  try {
854
1455
  for (const [key, entry] of [...pendingWrites]) {
855
1456
  if (key.startsWith(`${id}\0`) || key.startsWith("\0default\0") || key.startsWith("\0global\0")) {
@@ -875,6 +1476,28 @@ function renderPage(ctx) {
875
1476
  }
876
1477
  })();
877
1478
  };
1479
+ const runBatchTest = () => {
1480
+ const activeIds = rows.filter((row) => view({ ...configured[row.id], ...draft[row.id] }).enabled).map((row) => row.id);
1481
+ if (activeIds.length === 0 || batchProgress !== null) return;
1482
+ setBatchProgress({ done: 0, total: activeIds.length });
1483
+ void (async () => {
1484
+ let index = 0;
1485
+ let completed = 0;
1486
+ const concurrency = Math.min(3, activeIds.length);
1487
+ const worker = async () => {
1488
+ while (index < activeIds.length) {
1489
+ const id = activeIds[index++];
1490
+ await runTest(id);
1491
+ completed++;
1492
+ setBatchProgress({ done: completed, total: activeIds.length });
1493
+ }
1494
+ };
1495
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
1496
+ setTimeout(() => {
1497
+ setBatchProgress(null);
1498
+ }, 2e3);
1499
+ })();
1500
+ };
878
1501
  const rows = knownProviders({ ...configured, ...draft }, published);
879
1502
  const inheritingIds = () => rows.filter((row) => view({ ...configured[row.id], ...draft[row.id] }).useDefault === true).map((row) => row.id);
880
1503
  const save = (key, path, label, value, onRefused) => {
@@ -1048,32 +1671,277 @@ function renderPage(ctx) {
1048
1671
  event.preventDefault?.();
1049
1672
  apply2(parts);
1050
1673
  };
1051
- const field = (label, control, hint, hintError) => h(
1052
- "label",
1053
- { className: import_client.default.field, style: S.field, key: label },
1054
- h("span", { className: import_client.default.label, style: S.label }, label),
1055
- control,
1056
- hint === void 0 ? null : h(
1057
- "span",
1674
+ const quickImportSelect = (key, apply2) => {
1675
+ const detected = state.environmentProxy ?? null;
1676
+ const select = h(
1677
+ "select",
1058
1678
  {
1059
- className: import_client.default.hint,
1060
- style: {
1061
- gridColumn: "2",
1062
- fontSize: "11px",
1063
- // A warning that a value can never work is an answer, not
1064
- // decoration — same rule as the failed self-check text.
1065
- ...hintError === true ? { color: "var(--dsw-alias-state-error-primary, #f85149)" } : {}
1066
- }
1067
- },
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
+ };
1718
+ const field = (label, control, hint, hintError, fullWidth) => h(
1719
+ "label",
1720
+ {
1721
+ className: `${import_client.default.field}${fullWidth ? ` ${import_client.default.formGridFull}` : ""}`,
1722
+ style: {
1723
+ ...S.field,
1724
+ ...fullWidth ? S.formGridFull : {}
1725
+ },
1726
+ key: label
1727
+ },
1728
+ h("span", { className: import_client.default.label, style: S.label }, label),
1729
+ control,
1730
+ hint === void 0 ? null : h(
1731
+ "span",
1732
+ {
1733
+ className: import_client.default.hint,
1734
+ style: {
1735
+ ...S.hint,
1736
+ marginTop: "2px",
1737
+ // A warning that a value can never work is an answer, not
1738
+ // decoration — same rule as the failed self-check text.
1739
+ ...hintError === true ? { color: "var(--dsw-alias-state-error-primary, #f85149)" } : {}
1740
+ }
1741
+ },
1068
1742
  hint
1069
1743
  )
1070
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
+ );
1071
1767
  const storedDefault = snapshot.value?.defaultProxy ?? {};
1072
1768
  const defaultView = view({ ...storedDefault, ...defaultDraft });
1073
1769
  const defaultWhy = defaultProxyProblem(defaultView);
1074
1770
  const defaultHostWhy = defaultView.host.trim().length === 0 ? void 0 : proxyHostProblem(defaultView.host);
1075
1771
  const defaultComplete = defaultWhy === void 0;
1076
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
+ };
1077
1945
  const defaultBlock = h(
1078
1946
  "div",
1079
1947
  { className: import_client.default.defaultCard, style: S.defaultCard, key: "default-proxy" },
@@ -1095,77 +1963,88 @@ function renderPage(ctx) {
1095
1963
  )
1096
1964
  ),
1097
1965
  h("p", { className: import_client.default.defaultLead, style: S.defaultLead }, t.defaultLead),
1098
- field(
1099
- t.protocol,
1100
- h(
1101
- "select",
1102
- {
1966
+ h(
1967
+ "div",
1968
+ { style: S.testRow, key: "default:quick-import" },
1969
+ quickImportSelect("default:quick-import", applyDefaultUrl)
1970
+ ),
1971
+ h(
1972
+ "div",
1973
+ { className: import_client.default.formGrid, style: S.formGrid, key: "default:form-grid" },
1974
+ field(
1975
+ t.protocol,
1976
+ h(
1977
+ "select",
1978
+ {
1979
+ className: import_client.default.input,
1980
+ style: S.input,
1981
+ value: defaultView.protocol,
1982
+ onChange: (event) => bindDefault("protocol")(event.target.value)
1983
+ },
1984
+ ...PROTOCOLS2.map(
1985
+ (protocol) => h("option", { key: protocol, value: protocol }, protocol.toUpperCase())
1986
+ )
1987
+ )
1988
+ ),
1989
+ field(
1990
+ t.host,
1991
+ h("input", {
1103
1992
  className: import_client.default.input,
1104
1993
  style: S.input,
1105
- value: defaultView.protocol,
1106
- onChange: (event) => bindDefault("protocol")(event.target.value)
1107
- },
1108
- ...PROTOCOLS.map(
1109
- (protocol) => h("option", { key: protocol, value: protocol }, protocol.toUpperCase())
1110
- )
1111
- )
1112
- ),
1113
- field(
1114
- t.host,
1115
- h("input", {
1116
- className: import_client.default.input,
1117
- style: S.input,
1118
- placeholder: "127.0.0.1",
1119
- value: defaultView.host,
1120
- onChange: (event) => bindDefault("host")(event.target.value),
1121
- // Same convenience as the cards: a full URL fills the whole block.
1122
- onPaste: onProxyUrlPaste(applyDefaultUrl)
1123
- }),
1124
- defaultHostWhy ?? t.hostHint,
1125
- defaultHostWhy !== void 0
1126
- ),
1127
- field(
1128
- t.port,
1129
- h("input", {
1130
- className: import_client.default.input,
1131
- style: S.input,
1132
- type: "number",
1133
- min: 1,
1134
- max: 65535,
1135
- placeholder: "7890",
1136
- value: defaultView.port === 0 ? "" : String(defaultView.port),
1137
- onChange: (event) => {
1138
- const parsed = Number.parseInt(event.target.value, 10);
1139
- bindDefault("port")(Number.isNaN(parsed) ? 0 : parsed);
1140
- }
1141
- })
1142
- ),
1143
- field(
1144
- t.username,
1145
- h("input", {
1146
- className: import_client.default.input,
1147
- style: S.input,
1148
- autoComplete: "off",
1149
- value: defaultView.username,
1150
- onChange: (event) => bindDefault("username")(event.target.value)
1151
- })
1152
- ),
1153
- field(
1154
- t.password,
1155
- h(
1156
- "div",
1157
- { style: S.passwordControl },
1994
+ placeholder: "127.0.0.1",
1995
+ value: defaultView.host,
1996
+ onChange: (event) => bindDefault("host")(event.target.value),
1997
+ // Same convenience as the cards: a full URL fills the whole block.
1998
+ onPaste: onProxyUrlPaste(applyDefaultUrl)
1999
+ }),
2000
+ defaultHostWhy ?? t.hostHint,
2001
+ defaultHostWhy !== void 0
2002
+ ),
2003
+ field(
2004
+ t.port,
2005
+ h("input", {
2006
+ className: import_client.default.input,
2007
+ style: S.input,
2008
+ type: "number",
2009
+ min: 1,
2010
+ max: 65535,
2011
+ placeholder: "7890",
2012
+ value: defaultView.port === 0 ? "" : String(defaultView.port),
2013
+ onChange: (event) => {
2014
+ const parsed = Number.parseInt(event.target.value, 10);
2015
+ bindDefault("port")(Number.isNaN(parsed) ? 0 : parsed);
2016
+ }
2017
+ })
2018
+ ),
2019
+ field(
2020
+ t.username,
1158
2021
  h("input", {
1159
2022
  className: import_client.default.input,
1160
- style: { ...S.input, paddingRight: "36px" },
1161
- type: defaultPasswordVisible === true ? "text" : "password",
2023
+ style: S.input,
1162
2024
  autoComplete: "off",
1163
- value: defaultView.password,
1164
- onChange: (event) => bindDefault("password")(event.target.value)
1165
- }),
1166
- passwordToggle(defaultPasswordVisible === true, toggleDefaultPassword)
2025
+ value: defaultView.username,
2026
+ onChange: (event) => bindDefault("username")(event.target.value)
2027
+ })
1167
2028
  ),
1168
- t.passwordHintProxy
2029
+ field(
2030
+ t.password,
2031
+ h(
2032
+ "div",
2033
+ { style: S.passwordControl },
2034
+ h("input", {
2035
+ className: import_client.default.input,
2036
+ style: { ...S.input, paddingRight: "36px" },
2037
+ type: defaultPasswordVisible === true ? "text" : "password",
2038
+ autoComplete: "off",
2039
+ value: defaultView.password,
2040
+ onChange: (event) => bindDefault("password")(event.target.value)
2041
+ }),
2042
+ passwordToggle(defaultPasswordVisible === true, toggleDefaultPassword)
2043
+ ),
2044
+ t.passwordHintProxy,
2045
+ false,
2046
+ true
2047
+ )
1169
2048
  ),
1170
2049
  // Bulk opt-in, in ONE write: a per-card walk would rebuild the table N times.
1171
2050
  // Disabled while the default configuration is incomplete, because pointing every card at an
@@ -1187,86 +2066,533 @@ function renderPage(ctx) {
1187
2066
  )
1188
2067
  );
1189
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
+ });
1190
2340
  const storedLocalDirect = snapshot.value?.localDirect !== false;
1191
2341
  const globalTestUrl = typeof globalDraft.testUrl === "string" ? globalDraft.testUrl : storedTestUrl;
1192
2342
  const globalLocalDirect = typeof globalDraft.localDirect === "boolean" ? globalDraft.localDirect : storedLocalDirect;
1193
2343
  const globalTestUrlHint = testUrlHint(globalTestUrl, t);
1194
- const globalBlock = h(
1195
- "details",
1196
- {
1197
- className: import_client.default.diag,
1198
- style: {
1199
- ...S.diag,
1200
- 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
1201
2353
  },
1202
- key: "global-settings"
1203
- },
1204
- h("summary", { className: import_client.default.diagSummary }, t.globalTitle),
1205
- 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" },
1206
2406
  h(
1207
- "label",
1208
- { className: import_client.default.field, style: S.field },
1209
- h("span", { className: import_client.default.label, style: S.label }, t.testUrl),
1210
- h("input", {
1211
- className: import_client.default.input,
1212
- style: S.input,
1213
- value: globalTestUrl,
1214
- onChange: (event) => bindGlobal("testUrl")(event.target.value)
1215
- }),
1216
- globalTestUrlHint === void 0 ? h("span", { style: S.hint }, t.testUrlHint) : h(
1217
- "span",
1218
- {
1219
- className: import_client.default.hint,
1220
- style: {
1221
- ...S.hint,
1222
- color: "var(--dsw-alias-state-error-primary, #f85149)"
1223
- }
1224
- },
1225
- globalTestUrlHint
1226
- )
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)
1227
2411
  ),
1228
2412
  h(
1229
2413
  "div",
1230
- { style: S.testRow },
2414
+ { style: { ...S.testRow, flex: "none" } },
1231
2415
  h(
1232
2416
  "button",
1233
2417
  {
1234
2418
  className: import_client.default.testButton,
1235
2419
  style: S.testButton,
1236
2420
  type: "button",
1237
- onClick: () => bindGlobal("testUrl")(DEFAULT_TEST_URL)
2421
+ onClick: exportBackup
1238
2422
  },
1239
- t.restoreDefaultTestUrl
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"
2431
+ },
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
+ })
2443
+ )
2444
+ )
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
+ });
2504
+ const routeStateOf = (row) => {
2505
+ const route = view({ ...configured[row.id], ...draft[row.id] });
2506
+ const recorded = problems[row.id];
2507
+ const state2 = cardEndpoint(route);
2508
+ const problemText = recorded !== void 0 ? recorded : route.enabled ? cardProblemText(state2) : void 0;
2509
+ if (route.enabled && problemText !== void 0) return "rejected";
2510
+ if (route.enabled) return "proxied";
2511
+ return "direct";
2512
+ };
2513
+ const query = filter.trim().toLowerCase();
2514
+ const matchedKeywordRows = query.length === 0 ? rows : rows.filter((row) => `${row.id} ${row.name}`.toLowerCase().includes(query));
2515
+ const countActive = matchedKeywordRows.filter((r) => routeStateOf(r) === "proxied").length;
2516
+ const countDirect = matchedKeywordRows.filter((r) => routeStateOf(r) === "direct").length;
2517
+ const countRejected = matchedKeywordRows.filter((r) => routeStateOf(r) === "rejected").length;
2518
+ const shown = matchedKeywordRows.filter((row) => {
2519
+ if (statusFilter === "all") return true;
2520
+ return routeStateOf(row) === statusFilter;
2521
+ });
2522
+ const listToolbar = rows.length === 0 ? null : h(
2523
+ "div",
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
+ }
2553
+ },
2554
+ label
2555
+ )
1240
2556
  )
1241
2557
  ),
1242
2558
  h(
1243
- "label",
1244
- { className: import_client.default.toggle, style: S.toggle },
2559
+ "div",
2560
+ { className: import_client.default.toolbarRow, style: S.toolbarRow, key: "list-actions" },
1245
2561
  h("input", {
1246
- type: "checkbox",
1247
- checked: globalLocalDirect,
1248
- onChange: (event) => bindGlobal("localDirect")(event.target.checked === true)
2562
+ className: import_client.default.input,
2563
+ style: { ...S.input, flex: "1 1 180px", minWidth: "0" },
2564
+ placeholder: t.filterPlaceholder,
2565
+ "aria-label": t.filterLabel,
2566
+ value: filter,
2567
+ onChange: (event) => setFilter(event.target.value)
1249
2568
  }),
1250
- h("span", null, t.localDirect)
1251
- ),
1252
- h(
1253
- "p",
1254
- { style: S.hint },
1255
- globalLocalDirect ? t.localDirectOnHint : t.localDirectOffHint
2569
+ h(
2570
+ "button",
2571
+ {
2572
+ className: import_client.default.testButton,
2573
+ style: { ...S.testButton, flex: "none", whiteSpace: "nowrap" },
2574
+ type: "button",
2575
+ disabled: countActive === 0 || batchProgress !== null,
2576
+ onClick: runBatchTest
2577
+ },
2578
+ batchProgress !== null ? `${t.batchTesting} (${batchProgress.done}/${batchProgress.total})` : `${t.batchTest} (${countActive})`
2579
+ ),
2580
+ h(
2581
+ "button",
2582
+ {
2583
+ className: import_client.default.testButton,
2584
+ style: { ...S.testButton, flex: "none", whiteSpace: "nowrap" },
2585
+ type: "button",
2586
+ onClick: () => setViewMode((m) => {
2587
+ const next = m === "card" ? "compact" : "card";
2588
+ writeViewPrefs({ viewMode: next });
2589
+ return next;
2590
+ })
2591
+ },
2592
+ viewMode === "card" ? t.viewCompact : t.viewCard
2593
+ )
1256
2594
  )
1257
2595
  );
1258
- const query = filter.trim().toLowerCase();
1259
- const shown = query.length === 0 ? rows : rows.filter((row) => `${row.id} ${row.name}`.toLowerCase().includes(query));
1260
- const filterRow = rows.length === 0 ? null : field(
1261
- t.filterLabel,
1262
- h("input", {
1263
- className: import_client.default.input,
1264
- style: S.input,
1265
- placeholder: t.filterPlaceholder,
1266
- value: filter,
1267
- onChange: (event) => setFilter(event.target.value)
1268
- })
1269
- );
1270
2596
  const catalogueStatus = rows.length === 0 ? state.pending ? { text: t.catalogueLoading, error: false } : state.error !== void 0 ? { text: `${t.catalogueError}${state.error}`, error: true } : void 0 : void 0;
1271
2597
  const catalogueNotice = state.error !== void 0 && rows.length > 0 ? h(
1272
2598
  "p",
@@ -1298,14 +2624,16 @@ function renderPage(ctx) {
1298
2624
  const result = tests[row.id];
1299
2625
  const onlyDead = deadRuleEntries(route.onlyHosts);
1300
2626
  const bypassDead = deadRuleEntries(route.bypassHosts);
2627
+ const hostTemplate = providerHostTemplate(row.id);
1301
2628
  const useDefault = route.useDefault === true;
1302
- 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;
1303
2632
  const showTest = route.enabled || endpointStored;
1304
2633
  const problemText = (() => {
1305
2634
  const recorded = problems[row.id];
1306
2635
  if (recorded !== void 0) return recorded;
1307
- if (useDefault) return defaultComplete ? void 0 : `${t.defaultMissing}${defaultWhy}`;
1308
- return route.enabled ? defaultProxyProblem(route) : void 0;
2636
+ return route.enabled ? cardProblemText(endpointState) : void 0;
1309
2637
  })();
1310
2638
  const rejected = route.enabled && problemText !== void 0;
1311
2639
  const endpointFields = [
@@ -1319,7 +2647,7 @@ function renderPage(ctx) {
1319
2647
  value: route.protocol,
1320
2648
  onChange: (event) => bind(row.id, "protocol")(event.target.value)
1321
2649
  },
1322
- ...PROTOCOLS.map(
2650
+ ...PROTOCOLS2.map(
1323
2651
  (protocol) => h("option", { key: protocol, value: protocol }, protocol.toUpperCase())
1324
2652
  )
1325
2653
  ),
@@ -1384,11 +2712,13 @@ function renderPage(ctx) {
1384
2712
  }),
1385
2713
  passwordToggle(passwordVisible[row.id] === true, () => togglePassword(row.id))
1386
2714
  ),
1387
- route.protocol === "socks5" ? t.passwordHint : t.passwordHintProxy
2715
+ route.protocol === "socks5" ? t.passwordHint : t.passwordHintProxy,
2716
+ false,
2717
+ true
1388
2718
  )
1389
2719
  ];
1390
2720
  const pill = rejected ? { className: import_client.default.pillRejected, style: { ...S.pill, ...S.pillRejected }, text: t.rejected } : route.enabled ? { className: import_client.default.pillOn, style: { ...S.pill, ...S.pillOn }, text: t.on } : { className: import_client.default.pillLive, style: { ...S.pill, ...S.pillLive }, text: t.off };
1391
- return h(
2721
+ const cardSection = h(
1392
2722
  "section",
1393
2723
  {
1394
2724
  className: rejected ? import_client.default.cardRejected : route.enabled ? import_client.default.cardActive : import_client.default.card,
@@ -1425,36 +2755,94 @@ function renderPage(ctx) {
1425
2755
  ),
1426
2756
  h("span", { className: pill.className, style: pill.style }, pill.text)
1427
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.
1428
2762
  h(
1429
- "label",
1430
- { className: import_client.default.toggle, style: S.toggle },
1431
- h("input", {
1432
- type: "checkbox",
1433
- checked: route.enabled,
1434
- onChange: (event) => bind(row.id, "enabled")(event.target.checked === true)
1435
- }),
1436
- h("span", null, t.useProxy)
1437
- ),
1438
- // Opting into the shared endpoint. The card then shows the address it
1439
- // will actually use — or, when the default is not configured, why the
1440
- // route is refused — instead of a set of empty inputs the user never
1441
- // filled and cannot interpret.
1442
- h(
1443
- "label",
1444
- { className: import_client.default.toggle, style: S.toggle, key: `${row.id}:usedefault` },
1445
- h("input", {
1446
- type: "checkbox",
1447
- checked: useDefault,
1448
- onChange: (event) => bind(row.id, "useDefault")(event.target.checked === true)
1449
- }),
1450
- 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
+ )
1451
2828
  ),
1452
- 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(
1453
2834
  "p",
1454
2835
  { key: `${row.id}:inherit`, style: S.inheritNote },
1455
2836
  `${t.inheritingDefault}${defaultOrigin}`
1456
2837
  ) : null,
1457
- ...useDefault ? [] : 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
+ ],
1458
2846
  // The self-check is offered whenever there is something to check: an
1459
2847
  // enabled route (the Host's table carries it, and a rejected one still
1460
2848
  // reports WHY), or a disabled one whose endpoint is already complete —
@@ -1495,7 +2883,12 @@ function renderPage(ctx) {
1495
2883
  { key: "status" },
1496
2884
  `HTTP ${result.status}`
1497
2885
  ) : null,
1498
- result.ip === void 0 ? null : h("span", { key: "ip" }, `${t.exitIpLabel}${result.ip}`),
2886
+ result.ip === void 0 ? null : h(
2887
+ "span",
2888
+ { key: "ip", style: { display: "inline-flex", alignItems: "center", gap: "3px" } },
2889
+ `${t.exitIpLabel}${result.ip}`,
2890
+ copyButton(`${row.id}:ip`, result.ip, t.copy)
2891
+ ),
1499
2892
  result.direct === true ? h("span", { key: "direct" }, t.directLabel) : null,
1500
2893
  result.cached === true ? h("span", { key: "cached" }, t.cachedLabel) : null,
1501
2894
  result.target === void 0 ? null : h(
@@ -1506,13 +2899,22 @@ function renderPage(ctx) {
1506
2899
  flexBasis: "100%",
1507
2900
  fontSize: "11px",
1508
2901
  color: "var(--dsw-alias-label-secondary)",
1509
- wordBreak: "break-all"
2902
+ wordBreak: "break-all",
2903
+ display: "inline-flex",
2904
+ alignItems: "center",
2905
+ gap: "3px"
1510
2906
  }
1511
2907
  },
1512
- `${t.testTargetLabel}${result.target}${targetSourceSuffix(result, t)}`
2908
+ `${t.testTargetLabel}${result.target}${targetSourceSuffix(result, t)}`,
2909
+ copyButton(`${row.id}:target`, result.target, t.copy)
1513
2910
  )
1514
2911
  ] : [
1515
- h("span", { key: "err", style: S.testFailure }, `\u2717 ${result.error}`),
2912
+ h(
2913
+ "span",
2914
+ { key: "err", style: { ...S.testFailure, display: "inline-flex", alignItems: "center", gap: "3px" } },
2915
+ `\u2717 ${result.error}`,
2916
+ copyButton(`${row.id}:err`, String(result.error), t.copy)
2917
+ ),
1516
2918
  typeof result.status === "number" ? h("span", { key: "status" }, `HTTP ${result.status}`) : null,
1517
2919
  result.target === void 0 ? null : h(
1518
2920
  "span",
@@ -1522,17 +2924,21 @@ function renderPage(ctx) {
1522
2924
  flexBasis: "100%",
1523
2925
  fontSize: "11px",
1524
2926
  color: "var(--dsw-alias-label-secondary)",
1525
- wordBreak: "break-all"
2927
+ wordBreak: "break-all",
2928
+ display: "inline-flex",
2929
+ alignItems: "center",
2930
+ gap: "3px"
1526
2931
  }
1527
2932
  },
1528
- `${t.testTargetLabel}${result.target}${targetSourceSuffix(result, t)}`
2933
+ `${t.testTargetLabel}${result.target}${targetSourceSuffix(result, t)}`,
2934
+ copyButton(`${row.id}:target`, result.target, t.copy)
1529
2935
  )
1530
2936
  ]
1531
2937
  ) : null,
1532
2938
  // Promote this card's endpoint to the shared default. Offered only
1533
2939
  // when the card carries its OWN endpoint: an inheriting card would
1534
2940
  // just be copying the default onto itself.
1535
- endpointStored && !useDefault ? h(
2941
+ endpointStored && !useDefault && boundProfile.length === 0 ? h(
1536
2942
  "button",
1537
2943
  {
1538
2944
  className: import_client.default.testButton,
@@ -1560,6 +2966,26 @@ function renderPage(ctx) {
1560
2966
  onlyDead.length === 0 ? t.onlyHostsHint : `${t.ruleDeadHint}${onlyDead.join("\u3001")}`,
1561
2967
  onlyDead.length > 0
1562
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
+ ),
1563
2989
  field(
1564
2990
  t.bypassHosts,
1565
2991
  h("input", {
@@ -1588,6 +3014,158 @@ function renderPage(ctx) {
1588
3014
  )
1589
3015
  )
1590
3016
  );
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"}`;
3019
+ const compactRowElement = h(
3020
+ "section",
3021
+ {
3022
+ className: rejected ? import_client.default.compactRowRejected : route.enabled ? import_client.default.compactRowActive : import_client.default.compactRow,
3023
+ style: rejected ? { ...S.compactRow, ...S.compactRowRejected } : route.enabled ? { ...S.compactRow, ...S.compactRowActive } : S.compactRow,
3024
+ key: `compact:${row.id}`
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.
3029
+ h(
3030
+ "div",
3031
+ { style: S.compactLine, key: `compact:${row.id}:info` },
3032
+ h("span", { className: import_client.default.name, style: { ...S.name, maxWidth: "160px" } }, row.name),
3033
+ h("span", { className: import_client.default.id, style: S.id }, row.id),
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
+ )
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.
3060
+ h(
3061
+ "div",
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
+ ),
3103
+ h("span", { className: pill.className, style: pill.style }, pill.text),
3104
+ showTest ? h(
3105
+ "button",
3106
+ {
3107
+ className: import_client.default.testButton,
3108
+ style: { ...S.testButton, padding: "3px 8px", fontSize: "11px" },
3109
+ type: "button",
3110
+ disabled: result?.pending === true,
3111
+ onClick: () => runTest(row.id)
3112
+ },
3113
+ result?.pending === true ? t.testRunning : t.testButton
3114
+ ) : null,
3115
+ h(
3116
+ "button",
3117
+ {
3118
+ className: import_client.default.testButton,
3119
+ style: { ...S.testButton, padding: "3px 8px", fontSize: "11px" },
3120
+ type: "button",
3121
+ onClick: () => setExpandedCards((curr) => ({ ...curr, [row.id]: !curr[row.id] }))
3122
+ },
3123
+ isExpanded ? t.collapseDetails : t.expandDetails
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
+ ]
3163
+ )
3164
+ );
3165
+ if (viewMode === "compact") {
3166
+ return isExpanded ? h("div", { key: `compact-wrap:${row.id}` }, compactRowElement, cardSection) : compactRowElement;
3167
+ }
3168
+ return cardSection;
1591
3169
  });
1592
3170
  return h(
1593
3171
  "div",
@@ -1625,44 +3203,42 @@ function renderPage(ctx) {
1625
3203
  t.close
1626
3204
  )
1627
3205
  ),
1628
- defaultBlock,
1629
- globalBlock,
1630
- catalogueNotice,
1631
- filterRow,
1632
- ...body,
3206
+ // Page-level settings, as one group: they apply to the whole page, not to
3207
+ // one provider.
1633
3208
  h(
1634
- "details",
1635
- {
1636
- className: import_client.default.diag,
1637
- key: "diag",
1638
- style: {
1639
- marginTop: "4px",
1640
- padding: "8px 10px",
1641
- border: "1px solid var(--dsw-alias-border-l1)",
1642
- borderRadius: "8px",
1643
- fontSize: "11px",
1644
- color: "var(--dsw-alias-label-secondary)"
1645
- }
1646
- },
1647
- // The count is the only visible signal while the panel is collapsed: a
1648
- // rejected route is not a page that refuses to render, it is one that
1649
- // quietly does nothing.
1650
- h(
1651
- "summary",
1652
- { className: import_client.default.diagSummary },
1653
- problemCount === 0 ? t.diagnostics : `${t.diagnostics} (${problemCount})`
1654
- ),
1655
- h(
1656
- "ul",
1657
- { className: import_client.default.diagList, style: { margin: "6px 0 0", paddingLeft: "18px" } },
1658
- ...problemCount === 0 && diagnostics.length === 0 && savedNote === void 0 ? [h("li", { key: "none" }, t.diagPending)] : [
1659
- ...problemLines,
1660
- ...catalogueMismatchLine === void 0 ? [] : [catalogueMismatchLine],
1661
- ...savedNote === void 0 ? [] : [h("li", { key: "saved" }, savedNote)],
1662
- ...diagnostics.map((line, index) => h("li", { key: `d${index}` }, line))
1663
- ]
1664
- )
1665
- )
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
+ })
1666
3242
  );
1667
3243
  }
1668
3244
  var EN = {
@@ -1679,6 +3255,10 @@ var EN = {
1679
3255
  defaultMissing: "The default proxy configuration is incomplete, so this provider is not proxied: ",
1680
3256
  filterLabel: "Filter",
1681
3257
  filterPlaceholder: "Filter by name or id\u2026",
3258
+ filterStatusAll: "All",
3259
+ filterStatusProxied: "Proxied",
3260
+ filterStatusDirect: "Direct",
3261
+ filterStatusRejected: "Rejected",
1682
3262
  noMatch: "No provider matches the filter.",
1683
3263
  setAsDefault: "Set as the default proxy",
1684
3264
  applyDefaultAll: "Use the default proxy for all providers",
@@ -1690,7 +3270,7 @@ var EN = {
1690
3270
  showPassword: "Show",
1691
3271
  hidePassword: "Hide",
1692
3272
  confirmPromoteToDefault: "The default proxy is already in use by one or more providers. Replace it with this card's endpoint?",
1693
- hostHint: "Only an IPv4, IPv6, or domain name is allowed. A full proxy URL (socks5://user:pass@host:1080) can be pasted instead \u2014 it is split into the fields.",
3273
+ hostHint: "IP or domain only; paste full URL supported",
1694
3274
  passwordHint: "Sent to the SOCKS5 proxy for authentication; stored with this setting.",
1695
3275
  passwordHintProxy: "Sent to the proxy as an authentication password; stored with this setting.",
1696
3276
  hitCountPrefix: "",
@@ -1703,6 +3283,12 @@ var EN = {
1703
3283
  minutesAgo: " min ago",
1704
3284
  hoursAgo: " h ago",
1705
3285
  daysAgo: " d ago",
3286
+ viewCard: "Card view",
3287
+ viewCompact: "Compact view",
3288
+ expandDetails: "Edit",
3289
+ collapseDetails: "Collapse",
3290
+ batchTest: "Test active",
3291
+ batchTesting: "Testing",
1706
3292
  testButton: "Test",
1707
3293
  testRunning: "Testing\u2026",
1708
3294
  testDisabledNote: "Not enabled \u2014 test only: ",
@@ -1744,7 +3330,52 @@ var EN = {
1744
3330
  catalogueMismatch: "Saved but not listed by the Host catalogue: ",
1745
3331
  catalogueMismatchSuffix: ". Their proxy settings are kept.",
1746
3332
  saveFailedPrefix: "Save failed: ",
1747
- close: "Close"
3333
+ close: "Close",
3334
+ copy: "Copy",
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"
1748
3379
  };
1749
3380
  var ZH = {
1750
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",
@@ -1760,6 +3391,10 @@ var ZH = {
1760
3391
  defaultMissing: "\u9876\u5C42\u9ED8\u8BA4\u4EE3\u7406\u914D\u7F6E\u4E0D\u5B8C\u6574\uFF0C\u8BE5 provider \u4E0D\u4F1A\u8D70\u4EE3\u7406\uFF1A",
1761
3392
  filterLabel: "\u8FC7\u6EE4",
1762
3393
  filterPlaceholder: "\u6309\u540D\u79F0\u6216 id \u8FC7\u6EE4\u2026",
3394
+ filterStatusAll: "\u5168\u90E8",
3395
+ filterStatusProxied: "\u8D70\u4EE3\u7406",
3396
+ filterStatusDirect: "\u76F4\u8FDE",
3397
+ filterStatusRejected: "\u914D\u7F6E\u88AB\u62D2",
1763
3398
  noMatch: "\u6CA1\u6709\u5339\u914D\u7684 provider\u3002",
1764
3399
  setAsDefault: "\u8BBE\u4E3A\u9ED8\u8BA4\u4EE3\u7406",
1765
3400
  applyDefaultAll: "\u6240\u6709 provider \u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406",
@@ -1771,7 +3406,7 @@ var ZH = {
1771
3406
  showPassword: "\u663E\u793A",
1772
3407
  hidePassword: "\u9690\u85CF",
1773
3408
  confirmPromoteToDefault: "\u5DF2\u6709 provider \u6B63\u5728\u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406\u3002\u8981\u7528\u8FD9\u5F20\u5361\u7247\u7684\u7AEF\u70B9\u8986\u76D6\u5B83\u5417\uFF1F",
1774
- hostHint: "\u53EA\u5141\u8BB8\u586B\u5199 IPv4\u3001IPv6 \u6216\u57DF\u540D\uFF1B\u4E5F\u53EF\u4EE5\u76F4\u63A5\u7C98\u8D34\u5B8C\u6574\u4EE3\u7406 URL\uFF08\u5982 socks5://user:pass@host:1080\uFF09\uFF0C\u4F1A\u81EA\u52A8\u62C6\u5206\u5230\u5404\u5B57\u6BB5\u3002",
3409
+ hostHint: "\u4EC5\u9650 IP \u6216\u57DF\u540D\uFF0C\u652F\u6301\u7C98\u8D34\u5B8C\u6574\u4EE3\u7406 URL",
1775
3410
  passwordHint: "\u4EC5\u4F5C\u4E3A SOCKS5 \u8BA4\u8BC1\u53E3\u4EE4\u53D1\u9001\u7ED9\u4EE3\u7406\uFF1B\u968F\u8BE5\u8BBE\u7F6E\u4E00\u8D77\u4FDD\u5B58\u3002",
1776
3411
  passwordHintProxy: "\u4F5C\u4E3A\u4EE3\u7406\u8BA4\u8BC1\u53E3\u4EE4\u53D1\u9001\u7ED9\u4EE3\u7406\uFF1B\u968F\u8BE5\u8BBE\u7F6E\u4E00\u8D77\u4FDD\u5B58\u3002",
1777
3412
  hitCountPrefix: "\u5DF2\u4EE3\u7406 ",
@@ -1784,6 +3419,12 @@ var ZH = {
1784
3419
  minutesAgo: " \u5206\u949F\u524D",
1785
3420
  hoursAgo: " \u5C0F\u65F6\u524D",
1786
3421
  daysAgo: " \u5929\u524D",
3422
+ viewCard: "\u5361\u7247\u89C6\u56FE",
3423
+ viewCompact: "\u7D27\u51D1\u89C6\u56FE",
3424
+ expandDetails: "\u7F16\u8F91",
3425
+ collapseDetails: "\u6536\u8D77",
3426
+ batchTest: "\u6D4B\u8BD5\u5DF2\u542F\u7528",
3427
+ batchTesting: "\u6B63\u5728\u6D4B\u8BD5",
1787
3428
  testButton: "\u6D4B\u8BD5",
1788
3429
  testRunning: "\u6D4B\u8BD5\u4E2D\u2026",
1789
3430
  testDisabledNote: "\u672A\u542F\u7528\uFF0C\u4EC5\u6D4B\u8BD5\uFF1A",
@@ -1825,7 +3466,52 @@ var ZH = {
1825
3466
  catalogueMismatch: "\u5DF2\u4FDD\u5B58\u4F46\u672A\u51FA\u73B0\u5728 Host \u76EE\u5F55\u4E2D\u7684 provider\uFF1A",
1826
3467
  catalogueMismatchSuffix: "\u3002\u4EE3\u7406\u914D\u7F6E\u4ECD\u4F1A\u4FDD\u7559\u3002",
1827
3468
  saveFailedPrefix: "\u4FDD\u5B58\u5931\u8D25\uFF1A",
1828
- close: "\u5173\u95ED"
3469
+ close: "\u5173\u95ED",
3470
+ copy: "\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"
1829
3515
  };
1830
3516
  function stringsFor(ctx) {
1831
3517
  const language = localeOf(ctx);