@fonlan/dsh-model-router 0.3.5 → 0.3.6

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
@@ -11,9 +11,7 @@ window.__ModuleLoader__.load({
11
11
  const LOCALE_NS = "model-router";
12
12
  const zh = {
13
13
  settingsTitle: "模型路由",
14
- settingsCardDescription: "模型聚合、提供商切换与优先级顺序",
15
- expand: "展开",
16
- collapse: "收起",
14
+ sectionSub: "模型聚合、提供商切换与优先级顺序",
17
15
  readOnly: "本部署的设置为只读。",
18
16
  loading: "加载中…",
19
17
  loadFailed: "加载失败:{message}",
@@ -64,9 +62,7 @@ window.__ModuleLoader__.load({
64
62
  };
65
63
  const en = {
66
64
  settingsTitle: "Model Router",
67
- settingsCardDescription: "Model aggregation, provider switching and preference order",
68
- expand: "Expand",
69
- collapse: "Collapse",
65
+ sectionSub: "Model aggregation, provider switching and preference order",
70
66
  readOnly: "This deployment stores settings read-only.",
71
67
  loading: "Loading…",
72
68
  loadFailed: "Load failed: {message}",
@@ -156,9 +152,9 @@ window.__ModuleLoader__.load({
156
152
  setModelOrder: (order) => call("set-model-order", { order })
157
153
  };
158
154
  //#endregion
159
- //#region \0dsh-css:/home/runner/work/dsh-model-router/dsh-model-router/src/client/settings-card.css.mjs
160
- const css$1 = "/* Model Router settings card — chrome aligned with the built-in plugin cards\n (设置 → 插件 → 插件配置), theme-consistent via DSW alias variables. */\n.mr-settings-card {\n border: 1px solid var(--dsw-alias-border-l2, #2a3346);\n background: var(--dsw-alias-bg-layer-3, #0d1017);\n border-radius: 12px;\n list-style: none;\n transition: border-color .16s, background .16s;\n}\n\n.mr-settings-card:hover {\n border-color: var(--dsw-alias-label-dimmed, #4a5468);\n}\n\n.mr-settings-card[data-open] {\n background: var(--dsw-alias-bg-layer-2, #141a26);\n border-color: var(--dsw-alias-label-dimmed, #4a5468);\n}\n\n.mr-settings-head {\n appearance: none;\n width: 100%;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n background: transparent;\n border: 0;\n border-radius: 12px;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n display: flex;\n}\n\n.mr-settings-head:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary, #4f8cff);\n outline-offset: -2px;\n}\n\n.mr-settings-head-text {\n flex-direction: column;\n flex: 1;\n gap: 4px;\n min-width: 0;\n display: flex;\n}\n\n.mr-settings-title {\n color: var(--dsw-alias-label-primary, #e6e6e6);\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n.mr-settings-sub {\n color: var(--dsw-alias-label-tertiary, #8b93a7);\n font-size: 13px;\n line-height: 1.5;\n}\n\n.mr-settings-chevron {\n color: var(--dsw-alias-label-tertiary, #8b93a7);\n flex: none;\n transition: transform .16s;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.mr-settings-chevron[data-open] {\n transform: rotate(180deg);\n}\n\n.mr-settings-body {\n border-top: 1px solid var(--dsw-alias-border-l2, #2a3346);\n margin: 0 16px;\n padding: 12px 0 8px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.mr-settings-readonly {\n color: var(--dsw-alias-label-tertiary, #8b93a7);\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n}\n\n/* ── card body: the router controls ─────────────────────────────────────── */\n\n.mr-root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 0 0 4px;\n}\n\n.mr-quick-switch {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module-platform);\n cursor: pointer;\n}\n\n.mr-quick-switch input[type=\"checkbox\"] {\n margin-top: 2px;\n accent-color: var(--dsw-alias-accent);\n}\n\n.mr-quick-switch-copy {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.mr-quick-switch-label {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-quick-switch-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-toolbar {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.mr-count {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-button {\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n background: var(--dsw-alias-interactive-bg-hover);\n border: 1px solid var(--dsw-alias-border-l3);\n border-radius: 8px;\n font-size: 12px;\n line-height: 18px;\n padding: 3px 10px;\n}\n\n.mr-button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n filter: brightness(1.08);\n}\n\n.mr-button:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n.mr-error {\n background: var(--dsw-alias-interactive-bg-hover-danger);\n color: var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n padding: 7px 8px;\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-empty {\n color: var(--dsw-alias-label-tertiary);\n padding: 16px 0;\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-models {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.mr-card {\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 12px;\n padding: 10px 12px;\n background: var(--dsw-alias-bg-module-platform);\n transition: opacity 0.12s;\n}\n\n.mr-card-busy {\n opacity: 0.6;\n pointer-events: none;\n}\n\n.mr-card-header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.mr-model-name {\n color: var(--dsw-alias-label-primary);\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n\n.mr-model-id {\n color: var(--dsw-alias-label-tertiary);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12px;\n line-height: 18px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n min-width: 0;\n flex: 1;\n}\n\n.mr-provider-count {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n flex: none;\n}\n\n.mr-drag-hint {\n margin: 2px 0 6px;\n color: var(--dsw-alias-label-caption);\n font-size: 11px;\n line-height: 16px;\n}\n\n.mr-providers {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.mr-provider {\n display: flex;\n align-items: center;\n gap: 6px;\n border: 1px solid transparent;\n border-radius: 10px;\n padding: 2px 8px 2px 4px;\n}\n\n.mr-provider-active {\n background: var(--dsw-alias-interactive-bg-hover);\n border-color: var(--dsw-alias-border-l3);\n}\n\n.mr-provider-nocred {\n opacity: 0.55;\n}\n\n.mr-provider-drop {\n border-color: var(--dsw-alias-border-brand);\n}\n\n.mr-grip {\n color: var(--dsw-alias-label-caption);\n cursor: grab;\n font-size: 12px;\n line-height: 20px;\n flex: none;\n user-select: none;\n}\n\n.mr-grip:active {\n cursor: grabbing;\n}\n\n.mr-provider-button {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 6px;\n color: var(--dsw-alias-label-primary);\n background: none;\n border: none;\n text-align: left;\n cursor: pointer;\n font: inherit;\n padding: 4px 0;\n}\n\n.mr-provider-button:disabled {\n cursor: default;\n}\n\n.mr-provider-name {\n font-size: 13px;\n line-height: 20px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.mr-provider-nocred-label {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n flex: none;\n}\n\n.mr-active-badge {\n flex: none;\n color: var(--dsw-alias-label-inverted, #fff);\n background: var(--dsw-alias-border-brand, #4d6bfe);\n border-radius: 999px;\n font-size: 11px;\n line-height: 16px;\n padding: 0 8px;\n}\n\n/* ── model display order ────────────────────────────────────────────────── */\n\n.mr-sort {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module-platform);\n}\n\n.mr-sort-head {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.mr-sort-title {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-sort-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-sort-modes {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.mr-sort-mode {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 6px;\n cursor: pointer;\n}\n\n.mr-sort-mode:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.mr-sort-mode input[type=\"radio\"] {\n margin-top: 2px;\n accent-color: var(--dsw-alias-accent);\n}\n\n.mr-sort-mode-copy {\n display: flex;\n flex-direction: column;\n gap: 1px;\n min-width: 0;\n}\n\n.mr-sort-mode-label {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-sort-mode-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-sort-custom {\n display: flex;\n flex-direction: column;\n gap: 6px;\n border-top: 1px solid var(--dsw-alias-border-l2, #2a3346);\n padding-top: 8px;\n}\n\n.mr-sort-hint {\n color: var(--dsw-alias-label-caption);\n margin: 0;\n font-size: 11px;\n line-height: 16px;\n}\n\n.mr-sort-list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n max-height: 280px;\n overflow-y: auto;\n}\n\n.mr-sort-item {\n display: flex;\n align-items: center;\n gap: 6px;\n border: 1px solid var(--dsw-alias-border-l2, #2a3346);\n border-radius: 8px;\n padding: 4px 8px 4px 4px;\n background: var(--dsw-alias-bg-layer-1, #0d1017);\n}\n\n.mr-sort-item-drop {\n border-color: var(--dsw-alias-border-brand);\n}\n\n.mr-sort-item-busy {\n opacity: 0.6;\n pointer-events: none;\n}\n\n.mr-sort-model-name {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n min-width: 0;\n}\n\n.mr-sort-model-id {\n color: var(--dsw-alias-label-tertiary);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px;\n line-height: 16px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n flex: 1;\n text-align: right;\n}\n";
161
- const tagId$1 = "@fonlan/dsh-model-router/settings-card.css";
155
+ //#region \0dsh-css:/home/runner/work/dsh-model-router/dsh-model-router/src/client/settings-section.css.mjs
156
+ const css$1 = "/* Model Router settings page — the page owns one entry in the settings\n sidebar and renders its content in the panel's content column. Styles are\n theme-consistent via DSW alias variables; the page draws its own static\n header (title plus a one-line summary) over the always-visible controls. */\n\n/* page layout */\n\n.mr-page {\n display: flex;\n flex-direction: column;\n gap: 16px;\n max-width: 720px;\n}\n\n.mr-page-head {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.mr-page-title {\n margin: 0;\n color: var(--dsw-alias-label-primary, #e6e6e6);\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n.mr-page-sub {\n margin: 0;\n color: var(--dsw-alias-label-tertiary, #8b93a7);\n font-size: 13px;\n line-height: 1.5;\n}\n\n/* body layout */\n\n.mr-body {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.mr-settings-readonly {\n color: var(--dsw-alias-label-tertiary, #8b93a7);\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n}\n\n/* ── page body: the router controls ─────────────────────────────────────── */\n\n.mr-root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 0 0 4px;\n}\n\n.mr-quick-switch {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module-platform);\n cursor: pointer;\n}\n\n.mr-quick-switch input[type=\"checkbox\"] {\n margin-top: 2px;\n accent-color: var(--dsw-alias-accent);\n}\n\n.mr-quick-switch-copy {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.mr-quick-switch-label {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-quick-switch-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-toolbar {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.mr-count {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-button {\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n background: var(--dsw-alias-interactive-bg-hover);\n border: 1px solid var(--dsw-alias-border-l3);\n border-radius: 8px;\n font-size: 12px;\n line-height: 18px;\n padding: 3px 10px;\n}\n\n.mr-button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n filter: brightness(1.08);\n}\n\n.mr-button:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n.mr-error {\n background: var(--dsw-alias-interactive-bg-hover-danger);\n color: var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n padding: 7px 8px;\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-empty {\n color: var(--dsw-alias-label-tertiary);\n padding: 16px 0;\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-models {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.mr-card {\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 12px;\n padding: 10px 12px;\n background: var(--dsw-alias-bg-module-platform);\n transition: opacity 0.12s;\n}\n\n.mr-card-busy {\n opacity: 0.6;\n pointer-events: none;\n}\n\n.mr-card-header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.mr-model-name {\n color: var(--dsw-alias-label-primary);\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n\n.mr-model-id {\n color: var(--dsw-alias-label-tertiary);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12px;\n line-height: 18px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n min-width: 0;\n flex: 1;\n}\n\n.mr-provider-count {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n flex: none;\n}\n\n.mr-drag-hint {\n margin: 2px 0 6px;\n color: var(--dsw-alias-label-caption);\n font-size: 11px;\n line-height: 16px;\n}\n\n.mr-providers {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.mr-provider {\n display: flex;\n align-items: center;\n gap: 6px;\n border: 1px solid transparent;\n border-radius: 10px;\n padding: 2px 8px 2px 4px;\n}\n\n.mr-provider-active {\n background: var(--dsw-alias-interactive-bg-hover);\n border-color: var(--dsw-alias-border-l3);\n}\n\n.mr-provider-nocred {\n opacity: 0.55;\n}\n\n.mr-provider-drop {\n border-color: var(--dsw-alias-border-brand);\n}\n\n.mr-grip {\n color: var(--dsw-alias-label-caption);\n cursor: grab;\n font-size: 12px;\n line-height: 20px;\n flex: none;\n user-select: none;\n}\n\n.mr-grip:active {\n cursor: grabbing;\n}\n\n.mr-provider-button {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 6px;\n color: var(--dsw-alias-label-primary);\n background: none;\n border: none;\n text-align: left;\n cursor: pointer;\n font: inherit;\n padding: 4px 0;\n}\n\n.mr-provider-button:disabled {\n cursor: default;\n}\n\n.mr-provider-name {\n font-size: 13px;\n line-height: 20px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.mr-provider-nocred-label {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n flex: none;\n}\n\n.mr-active-badge {\n flex: none;\n color: var(--dsw-alias-label-inverted, #fff);\n background: var(--dsw-alias-border-brand, #4d6bfe);\n border-radius: 999px;\n font-size: 11px;\n line-height: 16px;\n padding: 0 8px;\n}\n\n/* ── model display order ────────────────────────────────────────────────── */\n\n.mr-sort {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module-platform);\n}\n\n.mr-sort-head {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.mr-sort-title {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-sort-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-sort-modes {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.mr-sort-mode {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 6px;\n cursor: pointer;\n}\n\n.mr-sort-mode:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.mr-sort-mode input[type=\"radio\"] {\n margin-top: 2px;\n accent-color: var(--dsw-alias-accent);\n}\n\n.mr-sort-mode-copy {\n display: flex;\n flex-direction: column;\n gap: 1px;\n min-width: 0;\n}\n\n.mr-sort-mode-label {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-sort-mode-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-sort-custom {\n display: flex;\n flex-direction: column;\n gap: 6px;\n border-top: 1px solid var(--dsw-alias-border-l2, #2a3346);\n padding-top: 8px;\n}\n\n.mr-sort-hint {\n color: var(--dsw-alias-label-caption);\n margin: 0;\n font-size: 11px;\n line-height: 16px;\n}\n\n.mr-sort-list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n max-height: 280px;\n overflow-y: auto;\n}\n\n.mr-sort-item {\n display: flex;\n align-items: center;\n gap: 6px;\n border: 1px solid var(--dsw-alias-border-l2, #2a3346);\n border-radius: 8px;\n padding: 4px 8px 4px 4px;\n background: var(--dsw-alias-bg-layer-1, #0d1017);\n}\n\n.mr-sort-item-drop {\n border-color: var(--dsw-alias-border-brand);\n}\n\n.mr-sort-item-busy {\n opacity: 0.6;\n pointer-events: none;\n}\n\n.mr-sort-model-name {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n min-width: 0;\n}\n\n.mr-sort-model-id {\n color: var(--dsw-alias-label-tertiary);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px;\n line-height: 16px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n flex: 1;\n text-align: right;\n}\n";
157
+ const tagId$1 = "@fonlan/dsh-model-router/settings-section.css";
162
158
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
163
159
  const tag = document.createElement("style");
164
160
  tag.dataset.plugin = "@fonlan/dsh-model-router";
@@ -167,23 +163,23 @@ window.__ModuleLoader__.load({
167
163
  document.head.appendChild(tag);
168
164
  }
169
165
  //#endregion
170
- //#region src/client/settings-card.tsx
166
+ //#region src/client/settings-section.tsx
171
167
  /**
172
- * The Model Router Settings Card (设置 → 插件 → 插件配置 → 模型路由).
168
+ * The Model Router settings page (设置 → 侧栏「模型路由」).
173
169
  *
174
- * Renders its own expandable chrome aligned with the built-in plugin cards
175
- * (external plugins cannot import PluginCard): a header naming the plugin and
176
- * what its settings govern, disclosing the router controls in place. All
177
- * reads/mutations go through the plugin's fenced API; the server persists
178
- * through the `model-router` settings namespace, so a switch here is live for
179
- * the next request globally.
170
+ * Registers into the settings.section slot, so the page owns one entry in the
171
+ * settings sidebar and renders its content in the panel's content column. The
172
+ * page draws its own static header (title plus a one-line summary) over the
173
+ * router controls: every routed model with its providers, their order and the
174
+ * provider the model currently routes to. All reads/mutations go through the
175
+ * plugin's fenced API; the server persists through the `model-router` settings
176
+ * namespace, so a switch here is live for the next request globally.
180
177
  *
181
178
  * The bound settings scope (the same `model-router` namespace the host half
182
- * registers) supplies the card's dispatch state: while the namespace is merely
183
- * loading the card stays mounted, and when it is unavailable (deployment
184
- * without the host half) nothing renders — matching the built-in cards. A
185
- * read-only deployment shows the built-in card's banner and disables the
186
- * mutation controls.
179
+ * registers) supplies the page's dispatch state: while the namespace is merely
180
+ * loading the page stays mounted, and when it is unavailable (deployment
181
+ * without the host half) nothing renders. A read-only deployment shows the
182
+ * banner and disables the mutation controls.
187
183
  */
188
184
  function arrayMove(list, from, to) {
189
185
  const next = [...list];
@@ -208,7 +204,7 @@ window.__ModuleLoader__.load({
208
204
  }, [ctx]);
209
205
  return (0, react.useSyncExternalStore)(subscribe, getSnapshot);
210
206
  }
211
- function makeSettingsCard(ctx) {
207
+ function makeSettingsSection(ctx) {
212
208
  const t = (() => {
213
209
  try {
214
210
  return ctx.locale.bind(LOCALE_NS);
@@ -216,11 +212,10 @@ window.__ModuleLoader__.load({
216
212
  return (key) => key;
217
213
  }
218
214
  })();
219
- return function ModelRouterSettingsCard(props) {
215
+ return function ModelRouterSettingsSection(props) {
220
216
  const { scope } = props;
221
217
  const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
222
218
  useLocaleRevision(ctx);
223
- const [open, setOpen] = (0, react.useState)(false);
224
219
  const [state, setState] = (0, react.useState)(null);
225
220
  const [error, setError] = (0, react.useState)(null);
226
221
  const [loading, setLoading] = (0, react.useState)(true);
@@ -370,44 +365,19 @@ window.__ModuleLoader__.load({
370
365
  if (snapshot.status === "unavailable") return null;
371
366
  const writable = snapshot.writable;
372
367
  const models = (0, react.useMemo)(() => state?.models ?? [], [state]);
373
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
374
- className: "mr-settings-card",
375
- "data-open": open ? "" : void 0,
376
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
377
- type: "button",
378
- className: "mr-settings-head",
379
- "aria-expanded": open,
380
- "aria-label": (open ? t("collapse") : t("expand")) + ":" + t("settingsTitle"),
381
- onClick: () => setOpen(!open),
382
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
383
- className: "mr-settings-head-text",
384
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
385
- className: "mr-settings-title",
386
- children: t("settingsTitle")
387
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
388
- className: "mr-settings-sub",
389
- children: t("settingsCardDescription")
390
- })]
391
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
392
- className: "mr-settings-chevron",
393
- "data-open": open ? "" : void 0,
394
- "aria-hidden": "true",
395
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
396
- width: "14",
397
- height: "14",
398
- viewBox: "0 0 14 14",
399
- fill: "none",
400
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
401
- d: "M3.5 5.25L7 8.75L10.5 5.25",
402
- stroke: "currentColor",
403
- strokeWidth: "1.5",
404
- strokeLinecap: "round",
405
- strokeLinejoin: "round"
406
- })
407
- })
368
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
369
+ className: "mr-page",
370
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
371
+ className: "mr-page-head",
372
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
373
+ className: "mr-page-title",
374
+ children: t("settingsTitle")
375
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
376
+ className: "mr-page-sub",
377
+ children: t("sectionSub")
408
378
  })]
409
- }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
410
- className: "mr-settings-body",
379
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
380
+ className: "mr-body",
411
381
  children: [!writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
412
382
  className: "mr-settings-readonly",
413
383
  role: "status",
@@ -1309,8 +1279,10 @@ window.__ModuleLoader__.load({
1309
1279
  }
1310
1280
  //#endregion
1311
1281
  //#region src/client/index.tsx
1312
- /** The settings namespace this card edits (must match the host half). */
1282
+ /** The settings namespace this page edits (must match the host half). */
1313
1283
  const ROUTER_NS = "model-router";
1284
+ /** The settings sidebar entry this page owns (must stay stable). */
1285
+ const SECTION_ID = "model-router";
1314
1286
  /** Services required before mounting (provided by the client runtime).
1315
1287
  * `remote.session` is included like the host ui-model-selection declares it:
1316
1288
  * the seat's `directoryFor` depends on the namespace, which mounts
@@ -1334,14 +1306,18 @@ window.__ModuleLoader__.load({
1334
1306
  });
1335
1307
  return () => off();
1336
1308
  }, "model-router: dictionaries");
1337
- const SettingsCard = makeSettingsCard(ctx);
1309
+ const t = ctx.locale.bind(LOCALE_NS);
1310
+ const SettingsSection = makeSettingsSection(ctx);
1338
1311
  const services = ctx;
1339
1312
  const scope = services.settingsScope.bind({ namespace: ROUTER_NS });
1340
- services.slots.inject("settings.plugin.item", () => services.slots.register({
1341
- name: "settings.plugin.item",
1342
- key: ROUTER_NS,
1313
+ services.slots.inject("settings.section", () => services.slots.register({
1314
+ name: "settings.section",
1315
+ id: SECTION_ID,
1316
+ order: 300,
1317
+ label: () => t("settingsTitle"),
1318
+ locale: LOCALE_NS,
1343
1319
  inject: () => ({ scope })
1344
- }, SettingsCard));
1320
+ }, SettingsSection));
1345
1321
  registerModelRouterSeat(ctx);
1346
1322
  }
1347
1323
  //#endregion