From 5a5eab96e22fae37f72056c202d2daace6256047 Mon Sep 17 00:00:00 2001
From: lzt404 <2596933790@qq.com>
Date: Fri, 2 Oct 2026 14:19:31 +0800
Subject: [PATCH] fix(ui): improve responsive layouts and refine API access
page
- Fix overflow, clipped controls, and toolbar overlap in narrow windows
- Improve responsive layouts across usage, OAuth, agents, settings, and dialogs
- Unify API category navigation and compact provider rows and actions
- Preserve inherited provider priorities instead of displaying zero
- Add responsive layout regression tests and dense provider fixtures
- Bump proxy core version from 8.0.6 to 8.0.10
---
core-version.txt | 2 +-
package.json | 2 +
src/pages/AgentsPage.tsx | 2 +-
src/pages/ApiAccessPage.tsx | 33 +-
src/pages/UsageRecordsPage.tsx | 40 ++-
src/styles.css | 87 +++--
src/styles/feature-pages.css | 31 --
src/styles/index.css | 4 +
src/styles/responsive-agents.css | 244 +++++++++++++
src/styles/responsive-providers.css | 435 ++++++++++++++++++++++++
src/styles/responsive-settings.css | 342 +++++++++++++++++++
src/styles/responsive-shell-usage.css | 174 ++++++++++
src/styles/ui-refinements.css | 55 ++-
tests/agent-configuration-layout-ui.cjs | 47 ++-
tests/api-access-layout-ui.cjs | 138 ++++++++
tests/fixtures/provider-groups.tsx | 59 +++-
tests/responsive-layout-ui.cjs | 256 ++++++++++++++
17 files changed, 1798 insertions(+), 153 deletions(-)
create mode 100644 src/styles/responsive-agents.css
create mode 100644 src/styles/responsive-providers.css
create mode 100644 src/styles/responsive-settings.css
create mode 100644 src/styles/responsive-shell-usage.css
create mode 100644 tests/api-access-layout-ui.cjs
create mode 100644 tests/responsive-layout-ui.cjs
diff --git a/core-version.txt b/core-version.txt
index 68d92dd..cfaf10a 100644
--- a/core-version.txt
+++ b/core-version.txt
@@ -1 +1 @@
-8.0.6
+8.0.10
diff --git a/package.json b/package.json
index 2dc051b..0555ef8 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,8 @@
"preview": "vite preview",
"test:ui:accessibility": "node tests/app-shell-accessibility-ui.cjs",
"test:ui:api-provider-dialog": "node tests/api-provider-dialog-layout-ui.cjs",
+ "test:ui:api-access-layout": "node tests/api-access-layout-ui.cjs",
+ "test:ui:responsive": "node tests/responsive-layout-ui.cjs",
"screenshots": "node scripts/capture-readme-screenshots.cjs",
"tauri": "tauri"
},
diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx
index 04bbce9..584e442 100644
--- a/src/pages/AgentsPage.tsx
+++ b/src/pages/AgentsPage.tsx
@@ -2058,7 +2058,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
disabled={busy}
>
- {row.apiKeys.length > 1
- ? t('apiAccess.keys.summary', { key: maskSecret(row.apiKey), count: row.apiKeys.length })
- : maskSecret(row.apiKey)}
-
{row.baseUrl || t('apiAccess.defaultUrl')}
- {row.models.length > 0 ? {t('apiAccess.models.summary', { count: row.models.length })} : null}
-
+ {row.apiKeys.length > 1
+ ? t('apiAccess.keys.summary', { key: maskSecret(row.apiKey), count: row.apiKeys.length })
+ : maskSecret(row.apiKey)}
+
+ {row.models.length > 0 ? {t('apiAccess.models.summary', { count: row.models.length })} : null}
+ {row.priority === null ? null : {t('apiAccess.priorityValue', { priority: row.priority })}}
| - {row.model} + {row.model} | {compactNumber(row.requests)} | {compactNumber(row.totalTokens)} | diff --git a/src/styles.css b/src/styles.css index d2caa55..a249ba0 100644 --- a/src/styles.css +++ b/src/styles.css @@ -9787,16 +9787,13 @@ body.table-col-resizing * { overflow: auto; } -.usage-pricing-panel { - min-height: 0; - overflow: auto; -} - /* Pricing Tab */ .usage-pricing-panel { - display: grid; + display: flex; min-width: 0; - align-content: start; + min-height: 0; + flex-direction: column; + overflow: hidden; gap: 14px; padding: 16px; border: 1px solid var(--theme-e3e1db); @@ -9936,6 +9933,7 @@ body.table-col-resizing * { .usage-pricing-toolbar { display: flex; + flex: none; align-items: center; justify-content: space-between; gap: 14px; @@ -9944,6 +9942,7 @@ body.table-col-resizing * { .usage-pricing-summary { display: grid; + flex: 1 1 240px; gap: 2px; } @@ -9961,13 +9960,26 @@ body.table-col-resizing * { .usage-pricing-actions { display: flex; + min-width: 0; + flex: 1 1 560px; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.usage-pricing-action-group { + display: flex; + min-width: 0; + flex: 1 1 250px; align-items: center; gap: 8px; } .usage-pricing-actions input, .usage-pricing-actions select { - min-width: 180px; + width: 100%; + min-width: 0; + flex: 1 1 0; height: 34px; padding: 0 10px; border: 1px solid var(--theme-d5d2cb); @@ -9979,7 +9991,10 @@ body.table-col-resizing * { .usage-pricing-actions button, .usage-price-editor-actions button { + flex: none; + min-height: 34px; height: 34px; + white-space: nowrap; } .usage-price-sync-dialog { @@ -10035,6 +10050,10 @@ body.table-col-resizing * { .usage-price-editor { display: grid; + min-height: 0; + flex: 0 1 auto; + max-height: 50%; + overflow: auto; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; padding: 14px; @@ -10073,13 +10092,26 @@ body.table-col-resizing * { } .usage-pricing-table-wrap { - min-height: 380px; + min-height: 120px; +} + +.usage-pricing-table-wrap::-webkit-scrollbar, +.usage-pricing-table-wrap::-webkit-scrollbar:horizontal { + display: block; + width: 8px; + height: 8px; +} + +.usage-pricing-table-wrap::-webkit-scrollbar-thumb { + border-radius: 4px; + background: var(--border-primary); } .usage-pricing-table { width: 100%; min-width: 980px; - border-collapse: collapse; + border-collapse: separate; + border-spacing: 0; font-size: var(--font-size-meta); } @@ -10105,7 +10137,22 @@ body.table-col-resizing * { .usage-pricing-table th:first-child, .usage-pricing-table td:first-child { - text-align: center; + position: sticky; + left: 0; + min-width: 180px; + max-width: 240px; + overflow: hidden; + text-overflow: ellipsis; + box-shadow: 1px 0 0 var(--border-color); +} + +.usage-pricing-table td:first-child { + z-index: 1; + background: var(--bg-primary); +} + +.usage-pricing-table th:first-child { + z-index: 2; } .usage-price-row-actions { @@ -11271,24 +11318,6 @@ body.table-col-resizing * { grid-template-columns: 1fr; } - .usage-pricing-toolbar { - align-items: stretch; - flex-direction: column; - } - - .usage-pricing-actions { - display: grid; - grid-template-columns: 1fr; - } - - .usage-pricing-actions input { - width: 100%; - } - - .usage-price-editor { - grid-template-columns: 1fr; - } - .thinking-alias-workbench { grid-template-columns: 1fr; } diff --git a/src/styles/feature-pages.css b/src/styles/feature-pages.css index 3cea88d..1b4f4b0 100644 --- a/src/styles/feature-pages.css +++ b/src/styles/feature-pages.css @@ -2976,11 +2976,6 @@ grid-template-columns: 1fr; } - .usage-topbar { - align-items: stretch; - flex-direction: column; - } - .usage-tabs { display: flex; width: 100%; @@ -2996,42 +2991,16 @@ padding-inline: 12px; } - .usage-topbar-actions { - width: 100%; - } - .usage-events-summary-left, .usage-events-summary-right { width: 100%; justify-content: space-between; } - .usage-collector-state { - flex: 1; - } - .usage-analysis-grid { grid-template-columns: 1fr; } - .usage-pricing-toolbar { - align-items: stretch; - flex-direction: column; - } - - .usage-pricing-actions { - display: grid; - grid-template-columns: 1fr; - } - - .usage-pricing-actions input { - width: 100%; - } - - .usage-price-editor { - grid-template-columns: 1fr; - } - .auth-files-toolbar { grid-template-columns: 18px minmax(0, 1fr); } diff --git a/src/styles/index.css b/src/styles/index.css index 66ada7b..d6a3448 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,3 +1,7 @@ @import '../styles.css'; @import './feature-pages.css'; @import './ui-refinements.css'; +@import './responsive-shell-usage.css'; +@import './responsive-providers.css'; +@import './responsive-agents.css'; +@import './responsive-settings.css'; diff --git a/src/styles/responsive-agents.css b/src/styles/responsive-agents.css new file mode 100644 index 0000000..05f5c89 --- /dev/null +++ b/src/styles/responsive-agents.css @@ -0,0 +1,244 @@ +/* Use the space left for the workbench, including the app sidebar and padding. */ +.agents-page:not(.agents-page-embedded) { + container: agent-workspace / inline-size; +} + +@container agent-workspace (max-width: 760px) { + .app-shell:has(.agents-page:not(.agents-page-embedded)) .agent-workbench { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr); + gap: 12px; + } + + .app-shell:has(.agents-page:not(.agents-page-embedded)) .agent-client-list { + height: auto; + max-height: none; + gap: 6px; + padding: 10px 12px; + } + + .agents-page:not(.agents-page-embedded) .agent-list-items { + display: grid; + flex: none; + grid-auto-flow: column; + grid-auto-columns: 192px; + grid-template-columns: none; + grid-template-rows: auto; + overflow-x: auto; + overflow-y: hidden; + padding: 0 0 4px; + scrollbar-gutter: auto; + scroll-snap-type: x proximity; + } + + .agents-page:not(.agents-page-embedded) .agent-list-items button { + min-height: 58px; + scroll-snap-align: start; + } + + .agents-page:not(.agents-page-embedded) .agent-list-items strong, + .agents-page:not(.agents-page-embedded) .agent-list-items small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +.agent-save-bar, +.agent-codex-save-actions, +.agent-signin-label, +.agent-section-heading-actions { + flex-wrap: wrap; +} + +.agent-save-bar .agent-save-feedback { + flex-basis: 180px; +} + +.agent-save-actions button, +.agent-management-actions button { + max-width: 100%; +} + +.agent-section-heading > div:first-child, +.agent-claude-code-disable-compact > span:first-child { + overflow-wrap: anywhere; +} + +@container agent-panel (max-width: 640px) { + .agent-desktop-models > .agent-section-heading { + align-items: flex-start; + flex-direction: column; + } + + .agent-desktop-models > .agent-section-heading > .agent-section-heading-actions { + width: 100%; + justify-content: flex-start; + } + + .agent-desktop-model-fields { + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; + } + + .agent-desktop-model-field:first-child { + grid-column: 1 / -1; + } + + .agent-desktop-model-actions { + height: 44px; + justify-self: end; + } + + .agent-status-grid, + .agent-status-grid.dual-install-status-grid, + .agent-status-grid.pi-status-grid { + grid-template-columns: repeat(auto-fit, minmax(min(100%, 140px), 1fr)); + } + + .codex-session-overview-head, + .codex-session-list-head { + flex-wrap: wrap; + align-items: flex-start; + } + + .codex-session-selection-actions { + justify-content: flex-start; + } + + .codex-session-pagination { + flex-wrap: wrap; + } +} + +@container agent-panel (max-width: 420px) { + .agent-subpage-tabs { + width: fit-content; + overflow-x: auto; + } + + .agent-subpage-tabs button { + flex: none; + width: auto; + padding-inline: 10px; + } + + .agent-signin-explanation dl > div { + grid-template-columns: minmax(0, 1fr); + gap: 4px; + } + + .codex-session-meta, + .codex-session-row:has(.codex-session-checkbox) .codex-session-meta { + grid-template-columns: auto minmax(0, 1fr); + gap: 6px; + } + + .codex-session-meta time { + grid-column: 1 / -1; + justify-self: start; + } +} + +/* Catalog forms must respond to the editor width, not the whole dialog. */ +.codex-catalog-editor { + container: catalog-editor / inline-size; +} + +.codex-catalog-sidebar, +.codex-catalog-form > label, +.codex-catalog-models > button { + min-width: 0; +} + +.codex-catalog-model-heading, +.codex-catalog-footer, +.codex-catalog-footer > div:last-child, +.codex-catalog-confirm > div > div { + flex-wrap: wrap; +} + +.codex-catalog-model-heading > button { + flex: none; + max-width: 100%; +} + +.codex-catalog-footer > div:last-child { + margin-left: auto; +} + +.codex-catalog-hint, +.codex-catalog-heading p { + overflow-wrap: anywhere; +} + +@container catalog-editor (max-width: 500px) { + .codex-catalog-form, + .harness-reasoning-levels { + grid-template-columns: minmax(0, 1fr); + } +} + +/* Keep the model list beside the editor on small desktop windows so it does + not consume most of the dialog's available height. */ +@media (min-width: 600px) and (max-width: 900px) { + .codex-catalog-body, + .harness-catalog-dialog .codex-catalog-body { + grid-template-columns: 180px minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr); + } + + .codex-catalog-sidebar { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr) auto; + gap: 8px; + padding: 10px; + border-right: 1px solid var(--theme-d5d2cb); + border-bottom: 0; + } + + .harness-catalog-dialog .codex-catalog-sidebar { + grid-template-rows: auto auto minmax(0, 1fr) auto; + } + + .codex-catalog-sidebar > *, + .harness-catalog-dialog .codex-catalog-sidebar > * { + grid-column: 1; + grid-row: auto; + } + + .codex-catalog-footer { + flex-direction: row; + gap: 8px; + } + + .codex-catalog-footer > div:last-child { + display: flex; + } +} + +.agent-backup-modal { + container: agent-backup / inline-size; +} + +.agent-backup-modal footer { + flex-wrap: wrap; +} + +.agent-backup-columns nav { + min-width: 0; +} + +.agent-backup-columns nav button { + overflow-wrap: anywhere; +} + +@container agent-backup (max-width: 680px) { + .agent-backup-columns { + grid-template-columns: minmax(0, 1fr); + } + + .agent-backup-columns nav { + max-height: 160px; + } +} diff --git a/src/styles/responsive-providers.css b/src/styles/responsive-providers.css new file mode 100644 index 0000000..6f4bc0c --- /dev/null +++ b/src/styles/responsive-providers.css @@ -0,0 +1,435 @@ +/* Keep API navigation above the list at every width. */ +.api-access-page .real-provider-workbench { + grid-template-columns: minmax(0, 1fr); + gap: 16px; +} + +.api-access-page .provider-category-panel { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 4px; + height: auto; + padding: 8px; +} + +.api-access-page .provider-category-panel button { + grid-template-columns: 24px minmax(0, auto) auto; + justify-content: center; + gap: 8px; + height: 50px; + min-height: 50px; + padding: 8px; + border-radius: 8px; +} + +.api-access-page .provider-category-panel button.active { + background: var(--ui-accent-soft); + border-color: color-mix(in srgb, var(--ui-accent) 20%, transparent); + color: var(--ui-accent); +} + +.api-access-page .provider-category-panel .provider-logo { + width: 24px; + height: 24px; +} + +.api-access-page .provider-category-panel span { + white-space: normal; + overflow-wrap: normal; + overflow: hidden; + text-overflow: clip; +} + +.api-access-page .provider-category-panel strong { + justify-self: center; + min-width: 22px; + padding: 2px 5px; + border-radius: 6px; + background: var(--bg-secondary); + color: var(--text-quaternary); + font-size: 12px; + line-height: 18px; + text-align: center; +} + +.api-access-page .provider-category-panel .active strong { + background: color-mix(in srgb, var(--ui-accent) 10%, var(--bg-primary)); + color: var(--ui-accent); +} + +.api-access-page .provider-resource-panel { + container: provider-resources / inline-size; + min-width: 0; + min-height: 0; +} + +.api-access-page .management-panel-heading { + display: grid; + grid-template-columns: auto minmax(180px, 1fr) auto; + grid-template-areas: 'title search actions'; + gap: 14px 24px; + align-items: center; +} + +.api-access-page .management-panel-heading > div:first-child { + grid-area: title; + text-align: left; +} + +.api-access-page .api-access-search { + grid-area: search; + width: 100%; + max-width: 440px; + justify-self: end; +} + +.api-access-page .api-access-heading-tools { + grid-area: actions; + display: flex; + width: auto; + flex: none; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 8px; +} + +.api-access-page .api-access-heading-tools > button { + flex: 0 0 auto; + width: auto; +} + +.api-access-page .real-provider-list { + margin-top: 18px; +} + +.api-access-page .real-provider-row { + grid-template-columns: 24px minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + min-height: 104px; + padding: 16px 0; +} + +.api-access-page .real-provider-row:last-child { + border-bottom: 0; + padding-bottom: 0; +} + +.api-access-page .provider-drag-handle { + grid-column: 1; + grid-row: 1; + width: 24px; + min-width: 24px; + height: 32px; + padding: 0; + margin: 0; + align-self: start; + border-color: transparent; + background: transparent; + box-shadow: none; +} + +.api-access-page .provider-row-main { + grid-column: 2; + grid-row: 1; + gap: 7px; +} + +.api-access-page .provider-row-title { + align-items: baseline; + flex-wrap: wrap; + gap: 2px 10px; +} + +.api-access-page .provider-row-title strong { + max-width: 100%; + font-size: 15px; + line-height: 22px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.api-access-page .provider-row-title > span:not(.state-pill) { + min-width: 0; + max-width: 100%; + overflow: hidden; + color: var(--text-quaternary); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.api-access-page .provider-row-title .state-pill { + min-height: 22px; + padding: 1px 7px; + font-size: 11px; + line-height: 18px; +} + +.api-access-page .provider-row-main > .provider-row-url { + overflow: hidden; + font-size: 13px; + line-height: 20px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.api-access-page .provider-row-meta { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + gap: 6px 12px; + padding: 0; + line-height: 20px; +} + +.api-access-page .provider-row-meta code { + min-width: 0; + max-width: 100%; + overflow-wrap: anywhere; + color: var(--text-quaternary); + font-size: 12px; +} + +.api-access-page .provider-row-meta span { + flex: none; + color: var(--text-quaternary); + font-size: 12px; +} + +.api-access-page .provider-row-actions { + grid-column: 3; + grid-row: 1; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; + max-width: 340px; +} + +.api-access-page .provider-enabled-control { + min-width: 0; +} + +.api-access-page .provider-row-actions .icon-button { + width: 32px; + height: 32px; + min-width: 32px; +} + +/* Compact tabs keep every category visible instead of wrapping a lone tab. */ +@container workspace (max-width: 820px) { + .api-access-page .provider-category-panel button { + grid-template-columns: 24px auto; + grid-template-rows: 24px auto; + gap: 4px 8px; + height: auto; + min-height: 68px; + } + + .api-access-page .provider-category-panel span { + grid-column: 1 / -1; + grid-row: 2; + font-size: 12px; + text-align: center; + white-space: normal; + overflow-wrap: normal; + } + + .api-access-page .provider-category-panel strong { + grid-column: 2; + grid-row: 1; + } +} + +@container provider-resources (max-width: 720px) { + .api-access-page .management-panel-heading { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-areas: 'title actions' 'search search'; + column-gap: 12px; + } + + .api-access-page .api-access-search { + max-width: none; + } +} + +@container provider-resources (max-width: 580px) { + .api-access-page .real-provider-row { + grid-template-columns: 24px minmax(0, 1fr); + gap: 10px; + } + + .api-access-page .provider-row-actions { + grid-column: 2; + grid-row: 2; + max-width: none; + justify-content: flex-start; + } +} + +/* Keep controls compact while allowing complete groups to move to the next line. */ +.auth-files-page .management-heading-actions { + min-width: 0; + max-width: 100%; + flex-wrap: wrap; +} + +.auth-files-page .management-heading-actions button { + flex: 0 0 auto; + width: auto; +} + +.auth-files-page .real-auth-files-panel { + container: credential-files / inline-size; + min-width: 0; +} + +.auth-files-page .auth-files-toolbar select { + min-width: 0; +} + +@container credential-files (max-width: 680px) { + .auth-files-page .auth-files-toolbar { + grid-template-columns: 18px minmax(0, 1fr) minmax(0, 1fr); + gap: 0 9px; + } + + .auth-files-page .auth-files-toolbar input { + grid-column: 2 / -1; + } + + .auth-files-page .auth-files-toolbar select:first-of-type { + grid-column: 1 / 3; + } + + .auth-files-page .auth-files-toolbar select:last-of-type { + grid-column: 3; + } + + .auth-files-page .auth-files-toolbar select { + border-top: 1px solid var(--theme-e3e1db); + border-left: 0; + } +} + +.oauth-login-page > .oauth-grid { + grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr)); +} + +.oauth-browser-picker { + min-width: 0; + max-width: 100%; + flex-wrap: wrap; +} + +.oauth-browser-picker select { + min-width: 0; + max-width: 100%; + flex: 0 1 240px; +} + +.oauth-card .oauth-auth-url-actions, +.oauth-card .management-card-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.oauth-card .management-card-actions > button { + flex: 1 1 auto; +} + +.oauth-card .oauth-callback-row { + grid-template-columns: minmax(0, 1fr) auto; +} + +.oauth-card .oauth-callback-row > button { + width: auto; + white-space: nowrap; +} + +.quota-group-heading { + flex-wrap: wrap; +} + +.quota-group-heading .quota-group-actions { + flex: 1 1 280px; +} + +.quota-group-actions .muted-summary { + width: auto; +} + +.real-quota-row > div:first-child { + flex-wrap: wrap; +} + +.real-quota-row small, +.quota-reset-credit-summary span { + min-width: 0; + white-space: normal; + overflow-wrap: anywhere; +} + +/* Optional key selectors must not shift fixed dialog grid tracks onto the model list. */ +.model-discovery-dialog.provider-health-dialog { + display: flex; + flex-direction: column; + height: min(660px, calc(100dvh - 48px)); + min-height: 0; + overflow: auto; +} + +.provider-health-dialog > :not(.provider-health-model-list) { + flex-shrink: 0; +} + +.provider-health-dialog .provider-health-model-list { + flex: 1 1 180px; + min-height: 100px; + height: auto; +} + +.provider-health-dialog .model-discovery-header > div { + flex-wrap: wrap; +} + +.provider-health-dialog .model-discovery-header span { + white-space: normal; + overflow-wrap: anywhere; +} + +.provider-health-dialog .provider-health-key-selector { + min-width: 0; + padding: 0; +} + +.model-discovery-dialog .model-discovery-toolbar, +.model-discovery-dialog .model-discovery-actions, +.model-discovery-dialog .model-transfer-summary { + flex-wrap: wrap; +} + +.model-discovery-dialog .model-discovery-toolbar > span { + white-space: normal; +} + +.api-provider-dialog .provider-group-heading, +.api-provider-dialog .model-config-heading, +.api-provider-dialog .thinking-level-heading { + flex-wrap: wrap; +} + +@container workspace (max-width: 760px) { + .oauth-browser-picker { + justify-content: flex-start; + } + + .auth-files-page .management-heading-actions, + .quota-group-heading .quota-group-actions { + justify-content: flex-start; + } +} diff --git a/src/styles/responsive-settings.css b/src/styles/responsive-settings.css new file mode 100644 index 0000000..416b59d --- /dev/null +++ b/src/styles/responsive-settings.css @@ -0,0 +1,342 @@ +/* Keep each section responsive to its available content width, including the + sidebar and the narrower cards in the two-column home layout. */ +.home-quick-actions { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +@container workspace (max-width: 1080px) { + .home-quick-actions { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@container workspace (max-width: 480px) { + .home-quick-actions { + grid-template-columns: minmax(0, 1fr); + } +} + +.client-api-panel { + container: home-api / inline-size; +} + +.client-api-panel .client-api-heading { + flex-wrap: wrap; + gap: 10px; +} + +.client-api-panel .client-api-heading > div { + flex: 1 1 260px; +} + +.client-api-panel .client-api-key-actions { + max-width: 100%; + flex-shrink: 1; +} + +.client-api-key-code { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +@container home-api (max-width: 480px) { + .client-api-card { + height: auto; + flex-direction: column; + align-items: stretch; + gap: 10px; + } + + .client-api-card-heading { + flex: none; + width: auto; + min-width: 0; + } +} + +/* The download source is one control group: move the group together, then let + the select shrink while the action retains a readable label. */ +.version-management-page > .version-list { + padding: 0; +} + +.version-source-row { + display: flex; + flex-wrap: wrap; + gap: 16px 24px; +} + +.version-source-copy { + flex: 1 1 240px; +} + +.version-source-control { + grid-template-columns: minmax(0, 1fr) auto; + width: min(360px, 100%); + min-width: 0; +} + +.version-source-control label, +.version-source-control select { + min-width: 0; + width: 100%; +} + +.version-source-add-button { + grid-column: auto; + white-space: nowrap; +} + +@container workspace (max-width: 620px) { + .app-release-notes-heading { + flex-wrap: wrap; + padding: 16px; + } + + .app-release-notes-body { + padding: 0 16px 16px; + } + + .app-release-notes-markdown { + gap: 14px 12px; + } + + .app-release-notes-markdown > .release-notes-category { + max-width: 80px; + } +} + +/* Settings have variable-length descriptions; fixed row heights and controls + sized from the window instead of the workspace can make adjacent cards overlap. */ +.config-page .config-panel-heading, +.config-page .config-heading-actions, +.config-page .config-network-section-heading, +.config-page .config-proxy-heading { + flex-wrap: wrap; +} + +.config-page .config-panel-heading { + gap: 10px 14px; +} + +.config-page .config-heading-actions { + margin-left: auto; +} + +.config-keys-panel { + grid-template-rows: auto minmax(0, 1fr); +} + +.config-network-grid { + grid-auto-rows: minmax(154px, auto); +} + +.config-network-field small, +.config-diagnostics-setting-copy, +.config-software-setting-copy { + overflow-wrap: anywhere; +} + +.config-diagnostics-actions, +.config-management-actions { + flex-wrap: wrap; +} + +@container workspace (max-width: 900px) { + .config-network-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .config-diagnostics-toggle-grid { + grid-template-columns: minmax(0, 1fr); + } +} + +@container workspace (max-width: 700px) { + .config-diagnostics-fields { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .config-software-setting-row { + gap: 16px; + padding: 15px 16px; + } + + .config-software-close-row { + align-items: stretch; + flex-direction: column; + } + + .config-software-select { + width: 100%; + min-width: 0; + padding-left: 54px; + } + + .config-diagnostics-footer, + .config-management-form-footer { + grid-template-columns: minmax(0, 1fr); + gap: 8px; + } + + .config-diagnostics-actions { + justify-content: flex-end; + } +} + +@container workspace (max-width: 560px) { + .config-network-grid, + .config-diagnostics-fields { + grid-template-columns: minmax(0, 1fr); + } + + .config-network-grid { + grid-auto-rows: auto; + } + + .config-software-select { + padding-left: 0; + } + + .config-heading-title h2 { + white-space: normal; + } +} + +/* Alias routes should keep their names readable before metadata and actions + wrap onto a second line. The editor uses its own width inside a modal. */ +.thinking-alias-list-panel { + container: alias-list / inline-size; +} + +.thinking-alias-dialog { + container: alias-editor / inline-size; +} + +.thinking-alias-field { + min-width: 0; +} + +@container alias-list (max-width: 560px) { + .thinking-alias-row { + grid-template-columns: minmax(0, 1fr) 34px; + } + + .thinking-alias-route { + grid-column: 1 / -1; + } + + .thinking-alias-badges { + flex-wrap: wrap; + justify-content: flex-start; + } +} + +@container alias-editor (max-width: 580px) { + .thinking-effort-options { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} + +/* Keep novice-mode actions compact at intermediate widths. Existing phone + rules made every action full-width even when two controls easily fit. */ +.simple-mode-topbar, +.simple-mode-topbar-right, +.simple-mode-brand-title, +.simple-mode-choice-title, +.simple-mode-api-model-heading, +.simple-mode-api-model-selection-heading { + flex-wrap: wrap; +} + +.simple-mode-topbar-left, +.simple-mode-topbar-right, +.simple-mode-brand, +.simple-mode-brand-text { + min-width: 0; +} + +.simple-mode-topbar-left { + flex: 1 1 340px; +} + +.simple-mode-topbar-right { + flex: 0 1 auto; + justify-content: flex-end; + margin-left: auto; +} + +.simple-mode-topbar-right > *, +.simple-mode-guide-toggle { + flex-shrink: 0; +} + +.simple-mode-choice-title strong, +.simple-mode-brand-title strong { + white-space: normal; + overflow-wrap: anywhere; +} + +.simple-mode-provider-card-foot { + flex-wrap: wrap; +} + +.simple-mode-field .text-input, +.simple-mode-field .select-input { + min-width: 0; +} + +@container workspace (max-width: 720px) { + .simple-mode-topbar-left, + .simple-mode-topbar-right { + width: 100%; + } + + .simple-mode-topbar-right { + justify-content: flex-start; + } + + .simple-mode-choice-grid, + .simple-mode-api-fields { + grid-template-columns: minmax(0, 1fr); + } + + .simple-mode-api-actions, + .simple-mode-task-footer { + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: wrap; + } + + .simple-mode-api-actions button, + .simple-mode-task-footer > .primary-button, + .simple-mode-task-footer > .secondary-button { + width: auto; + } + + .simple-mode-task-footer > .primary-button { + margin-left: auto; + } +} + +@container workspace (max-width: 520px) { + .simple-mode-api-model-options { + grid-template-columns: minmax(0, 1fr); + } + + .simple-mode-step-status-track { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + } + + .simple-mode-step-status-connector { + display: none; + } + + .simple-mode-step-status-copy strong, + .simple-mode-step-status-copy small { + white-space: normal; + overflow-wrap: anywhere; + } +} diff --git a/src/styles/responsive-shell-usage.css b/src/styles/responsive-shell-usage.css new file mode 100644 index 0000000..ddc9eeb --- /dev/null +++ b/src/styles/responsive-shell-usage.css @@ -0,0 +1,174 @@ +/* Keep footer controls reachable when desktop navigation exceeds the height. */ +@media (min-width: 901px) { + .sidebar-brand, + .sidebar-bottom { + flex-shrink: 0; + } + + .sidebar > .nav-section { + min-height: 0; + margin-bottom: 18px; + overflow-x: hidden; + overflow-y: auto; + scrollbar-width: thin; + } +} + +/* Let toolbar rows keep their height instead of overlapping the table. */ +.usage-events-summary { + flex: none; + flex-wrap: wrap; +} + +.usage-events-summary-left, +.usage-events-summary-right { + width: auto; + max-width: 100%; + justify-content: flex-start; +} + +.usage-events-summary-right { + margin-left: auto; +} + +.usage-pagination-controls { + min-width: 0; + max-width: 100%; + flex-wrap: wrap; +} + +.usage-topbar, +.usage-filter-panel { + flex-shrink: 0; +} + +.usage-page-navigation, +.usage-topbar-actions { + max-width: 100%; +} + +.usage-tabs, +.config-subpage-tabs, +.oauth-subpage-tabs { + min-width: 0; + max-width: 100%; + overflow-x: auto; +} + +.usage-tabs button, +.config-subpage-tabs button, +.oauth-subpage-tabs button { + flex-shrink: 0; + white-space: nowrap; +} + +.usage-topbar-actions { + min-width: 0; + flex-wrap: wrap; +} + +.usage-collector-state { + max-width: 100%; +} + +.usage-collector-state > span:last-child { + overflow: hidden; + text-overflow: ellipsis; +} + +.usage-section-heading { + gap: 10px; + flex-wrap: wrap; +} + +.usage-section-heading > div { + min-width: 0; +} + +.usage-section-heading strong { + white-space: normal; + overflow-wrap: anywhere; +} + +.usage-custom-range input { + min-width: 0; + max-width: 100%; +} + +.usage-pricing-table-wrap:focus-visible { + outline: 2px solid var(--ui-accent); + outline-offset: -2px; +} + +.usage-pricing-panel { + overflow: auto; +} + +.usage-price-editor { + min-height: 160px; + max-height: min(320px, 40vh); + flex-shrink: 0; +} + +.usage-storage-limit-editor { + min-width: 0; + max-width: 100%; +} + +.usage-storage-limit-editor label { + min-width: 0; + flex: 1; +} + +.usage-storage-limit-editor input { + min-width: 0; + width: 100%; + max-width: 168px; +} + +.usage-storage-limit-editor button { + flex: none; + white-space: nowrap; +} + +@container workspace (max-width: 760px) { + .usage-overview-layout { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: max-content minmax(280px, 1fr) auto; + } + + .usage-health-panel { + height: auto; + } + + .usage-data-management-action { + flex-wrap: wrap; + gap: 12px; + } + + .usage-data-management-action > div:first-child { + min-width: 0; + flex: 1 1 260px; + } +} + +@container workspace (max-width: 540px) { + .usage-events-summary-right { + margin-left: 0; + } + + .usage-storage-limit-editor { + flex-direction: row; + } + + .usage-storage-limit-editor .primary-button { + width: auto; + } +} + +@media (min-width: 761px) { + .usage-events-panel > .usage-table-wrap, + .usage-events-panel > .usage-empty { + min-height: 0; + } +} diff --git a/src/styles/ui-refinements.css b/src/styles/ui-refinements.css index 7d3fe54..357f065 100644 --- a/src/styles/ui-refinements.css +++ b/src/styles/ui-refinements.css @@ -387,40 +387,6 @@ body, html, #root { font-weight: var(--font-weight-semibold); } -/* API access toolbar */ -.api-access-page .management-panel-heading { - align-items: center; - flex-wrap: wrap; -} - -.api-access-heading-tools { - display: flex; - min-width: 0; - flex: 1 1 420px; - align-items: center; - justify-content: flex-end; - gap: 8px; -} - -.api-access-heading-tools .compact-toolbar { - width: min(280px, 100%); - flex: 1 1 180px; -} - - -@container workspace (max-width: 760px) { - .api-access-heading-tools { - width: 100%; - flex-basis: 100%; - flex-wrap: wrap; - justify-content: flex-start; - } - - .api-access-heading-tools .compact-toolbar { - flex-basis: 100%; - } -} - /* 7. OAuth Management Page Polish */ .oauth-management-page .management-header h1 { display: none; @@ -618,13 +584,18 @@ body, html, #root { gap: 8px; } +.usage-records-page .usage-filter-group { + display: grid; + min-width: 0; + grid-template-columns: repeat(6, minmax(0, 1fr)); +} + .usage-records-page .usage-filter-item { flex: 1 1 150px; flex-direction: row; align-items: center; gap: 0; - min-width: 132px; - max-width: 210px; + min-width: 0; height: 36px; padding: 0 8px; border: 1px solid var(--border-color); @@ -688,6 +659,18 @@ body, html, #root { color: var(--text-primary); } +@container workspace (max-width: 880px) { + .usage-records-page .usage-filter-group { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@container workspace (max-width: 520px) { + .usage-records-page .usage-filter-group { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + .usage-records-page .usage-section-heading strong { color: #1d1d1f; font-weight: 650; diff --git a/tests/agent-configuration-layout-ui.cjs b/tests/agent-configuration-layout-ui.cjs index f8869bb..97bdaea 100644 --- a/tests/agent-configuration-layout-ui.cjs +++ b/tests/agent-configuration-layout-ui.cjs @@ -128,27 +128,42 @@ const desktopViewports = [ const assertClientListScroll = async (label, { requireOverflow = true } = {}) => { const metrics = await page.locator('.agent-list-items').evaluate(list => { const style = getComputedStyle(list); - const last = list.lastElementChild; - list.scrollTop = list.scrollHeight; - const scrollTop = list.scrollTop; + const overflowX = list.scrollWidth > list.clientWidth + 1; + const overflowY = list.scrollHeight > list.clientHeight + 1; + const original = { + left: list.scrollLeft, + top: list.scrollTop, + snap: list.style.scrollSnapType, + behavior: list.style.scrollBehavior, + }; + list.style.scrollSnapType = 'none'; + list.style.scrollBehavior = 'auto'; const listRect = list.getBoundingClientRect(); - const lastRect = last?.getBoundingClientRect(); - const lastReachable = !!lastRect && lastRect.top >= listRect.top - 1 && lastRect.bottom <= listRect.bottom + 1; - list.scrollTop = 0; + const unreachable = Array.from(list.querySelectorAll('button')).filter(button => { + const before = button.getBoundingClientRect(); + list.scrollLeft += before.left - listRect.left; + list.scrollTop += before.top - listRect.top; + const after = button.getBoundingClientRect(); + return after.left < listRect.left - 1 || after.right > listRect.right + 1 + || after.top < listRect.top - 1 || after.bottom > listRect.bottom + 1; + }).map(button => button.textContent.trim()); + const scrollable = (!overflowX || ['auto', 'scroll'].includes(style.overflowX)) + && (!overflowY || ['auto', 'scroll'].includes(style.overflowY)); + list.scrollLeft = original.left; + list.scrollTop = original.top; + list.style.scrollSnapType = original.snap; + list.style.scrollBehavior = original.behavior; return { - clientHeight: list.clientHeight, - scrollHeight: list.scrollHeight, - overflowY: style.overflowY, - scrollTop, - lastReachable, + overflows: overflowX || overflowY, + scrollable, + unreachable, }; }); - assert.equal(metrics.overflowY, 'auto', `${label}: the client list must own vertical scrolling`); + assert.equal(metrics.scrollable, true, `${label}: overflowing client options must remain scrollable`); if (requireOverflow) { - assert.ok(metrics.scrollHeight > metrics.clientHeight + 1, `${label}: the complete client list must overflow internally`); - assert.ok(metrics.scrollTop > 0, `${label}: the client list must accept scrolling`); + assert.equal(metrics.overflows, true, `${label}: the fixture must exercise scrolling through the client list`); } - assert.equal(metrics.lastReachable, true, `${label}: scrolling must reveal the final client`); + assert.deepEqual(metrics.unreachable, [], `${label}: scrolling must reveal every client option`); }; const assertDesktopLayout = async (label, { requireClientOverflow = true } = {}) => { @@ -421,6 +436,8 @@ const desktopViewports = [ `${shellNarrowLabel}: panels must stack at the same width after the workspace breakpoint`); assert.ok(shellNarrow.client.bottom <= shellNarrow.configuration.y + 1, `${shellNarrowLabel}: the client list must remain above the configuration panel`); + assert.ok(shellNarrow.configuration.height >= shellNarrow.workbench.height / 2, + `${shellNarrowLabel}: stacked client options must leave most of the workbench available for configuration`); assert.ok(Math.abs(shellNarrow.bottomGap - shellNarrow.paddingBottom) <= 1, `${shellNarrowLabel}: the stacked workbench must preserve the fixed window-edge gap`); assert.ok(shellNarrow.documentScrollHeight <= shellNarrow.documentClientHeight + 1, diff --git a/tests/api-access-layout-ui.cjs b/tests/api-access-layout-ui.cjs new file mode 100644 index 0000000..318e488 --- /dev/null +++ b/tests/api-access-layout-ui.cjs @@ -0,0 +1,138 @@ +const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright'); +const assert = require('node:assert/strict'); +const path = require('node:path'); + +const widths = [1440, 980, 854, 640]; +const locales = ['zh-CN', 'en']; + +(async () => { + const { createServer } = await import('vite'); + const react = (await import('@vitejs/plugin-react')).default; + const server = await createServer({ + configFile: false, + root: path.resolve(__dirname, '..'), + plugins: [react()], + optimizeDeps: { entries: ['tests/fixtures/provider-groups.html'] }, + logLevel: 'error', + server: { host: '127.0.0.1', port: 0, watch: null }, + }); + let browser; + try { + await server.listen(); + const base = `http://127.0.0.1:${server.httpServer.address().port}`; + const channel = process.env.PLAYWRIGHT_CHANNEL || (process.platform === 'win32' ? 'msedge' : undefined); + browser = await chromium.launch({ channel, headless: true, args: ['--no-proxy-server'] }); + const errors = []; + + for (const locale of locales) for (const width of widths) { + const label = `${locale} ${width}x600`; + const page = await browser.newPage({ viewport: { width, height: 600 } }); + page.setDefaultTimeout(10000); + page.on('pageerror', error => errors.push(`${label}: ${error}`)); + await page.route('**/*', route => route.request().url().startsWith(`${base}/`) ? route.continue() : route.abort()); + await page.goto(`${base}/tests/fixtures/provider-groups.html?layout=dense&locale=${locale}`, { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => document.querySelectorAll('.real-provider-row').length === 4 + && document.querySelectorAll('.provider-row-title > span:not(.state-pill)').length === 4); + await page.evaluate(async () => { + await document.fonts.ready; + await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); + }); + + const navigation = await page.locator('.provider-category-panel').evaluate(panel => { + const rect = panel.getBoundingClientRect(); + const buttons = [...panel.querySelectorAll('button')]; + return { + count: buttons.length, + rows: new Set(buttons.map(button => Math.round(button.getBoundingClientRect().top))).size, + allVisible: buttons.every(button => { + const item = button.getBoundingClientRect(); + return item.width > 0 && item.height > 0 && item.left >= rect.left - 1 && item.right <= rect.right + 1 + && item.top >= rect.top - 1 && item.bottom <= rect.bottom + 1 && item.right <= innerWidth + 1; + }), + labelsFit: buttons.every(button => { + const label = button.querySelector('span'); + const range = document.createRange(); + range.selectNodeContents(label); + const text = range.getBoundingClientRect(); + const bounds = label.getBoundingClientRect(); + return text.left >= bounds.left - 1 && text.right <= bounds.right + 1 + && text.top >= bounds.top - 1 && text.bottom <= bounds.bottom + 1; + }), + bottom: rect.bottom, + listTop: document.querySelector('.provider-resource-panel').getBoundingClientRect().top, + }; + }); + assert.equal(navigation.count, 5, `${label}: every provider category remains available`); + assert.equal(navigation.rows, 1, `${label}: categories retain a single top row`); + assert.equal(navigation.allVisible, true, `${label}: all category controls fit without hidden columns`); + assert.equal(navigation.labelsFit, true, `${label}: full category names stay readable`); + assert.ok(navigation.bottom <= navigation.listTop + 1, `${label}: category navigation stays above the resource list`); + + const overflow = await page.evaluate(() => [document.documentElement, document.body, + document.querySelector('.content'), document.querySelector('.api-access-page')] + .filter(element => element.scrollWidth > element.clientWidth + 1) + .map(element => element.className || element.tagName)); + assert.deepEqual(overflow, [], `${label}: dense API content must not overflow the page horizontally`); + + const groups = await page.evaluate(() => window.groupFixture.groups); + const rows = page.locator('.real-provider-row'); + for (const group of groups) { + const row = rows.filter({ has: page.locator('.provider-row-title strong', { hasText: group.name }) }); + const meta = row.locator('.provider-row-meta'); + assert.equal(await row.count(), 1, `${label}: each group has exactly one row`); + assert.equal(await meta.isVisible(), true, `${label} ${group.name}: metadata stays visible`); + const metaText = await meta.innerText(); + const expectedModels = locale === 'en' ? `${group.models.length} models` : `模型 ${group.models.length} 个`; + if (group.models.length) assert.ok(metaText.includes(expectedModels), `${label} ${group.name}: model count is preserved`); + const priorityLabel = locale === 'en' ? 'Priority' : '优先级'; + if (group.priority === null) assert.ok(!metaText.includes(priorityLabel), `${label}: inherited priority is not displayed as zero`); + else assert.ok(metaText.includes(`${priorityLabel} ${group.priority}`), `${label} ${group.name}: priority, including zero, is preserved`); + if (group.keys.length > 1) { + const keyCount = locale === 'en' ? `${group.keys.length} keys total` : `共 ${group.keys.length} 个密钥`; + assert.ok(metaText.includes(keyCount), `${label} ${group.name}: multiple keys stay grouped and counted`); + } + assert.equal(await row.locator('.provider-row-url').getAttribute('title'), group['base-url'], `${label}: full URL remains available`); + const remark = row.locator('.provider-row-title > span:not(.state-pill)'); + assert.equal(await remark.getAttribute('title'), await remark.textContent(), `${label}: the full remark remains available when shortened`); + + const layout = await row.evaluate(element => { + const bounds = element.getBoundingClientRect(); + const main = element.querySelector('.provider-row-main').getBoundingClientRect(); + const actions = element.querySelector('.provider-row-actions').getBoundingClientRect(); + const controls = [...element.querySelectorAll('.provider-row-actions > button, .provider-enabled-control')]; + const rects = controls.map(control => control.getBoundingClientRect()); + const overlap = (a, b) => Math.min(a.right, b.right) - Math.max(a.left, b.left) > 1 + && Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top) > 1; + return { + mainOverlapsActions: overlap(main, actions), + controlsContained: rects.every(rect => rect.width > 0 && rect.height > 0 && rect.left >= bounds.left - 1 + && rect.right <= bounds.right + 1 && rect.top >= bounds.top - 1 && rect.bottom <= bounds.bottom + 1), + controlsOverlap: rects.some((rect, index) => rects.slice(index + 1).some(next => overlap(rect, next))), + }; + }); + assert.equal(layout.mainOverlapsActions, false, `${label} ${group.name}: details and actions do not overlap`); + assert.equal(layout.controlsContained, true, `${label} ${group.name}: every action fits its own row`); + assert.equal(layout.controlsOverlap, false, `${label} ${group.name}: action targets remain separate`); + for (const control of await row.locator('.provider-row-actions > button, .provider-enabled-control, .provider-drag-handle').all()) { + await control.scrollIntoViewIfNeeded(); + assert.equal(await control.evaluate(element => { + const rect = element.getBoundingClientRect(); + const hit = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2); + return rect.top >= -1 && rect.bottom <= innerHeight + 1 && !!hit && element.contains(hit); + }), true, `${label} ${group.name}: actions and drag handle remain reachable by scrolling`); + } + } + + const partial = rows.filter({ hasText: 'Mock Borealis partial' }); + assert.equal(await partial.locator('.state-pill').textContent(), locale === 'en' ? 'Some keys disabled' : '部分密钥已停用'); + assert.equal(await rows.filter({ hasText: 'Mock Dusk disabled' }).getByRole('checkbox').isChecked(), false); + assert.deepEqual(await page.evaluate(() => window.groupFixture.writes), [], 'Layout checks must not change provider records'); + await page.close(); + } + assert.deepEqual(errors, [], 'Dense API layouts must not produce runtime errors'); + console.log('PASS: dense API groups retain navigation, metadata and reachable actions at four widths in Chinese and English.'); + } finally { + if (browser) await browser.close(); + await server.close(); + } +})().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/tests/fixtures/provider-groups.tsx b/tests/fixtures/provider-groups.tsx index 4f18746..1ab476f 100644 --- a/tests/fixtures/provider-groups.tsx +++ b/tests/fixtures/provider-groups.tsx @@ -2,22 +2,71 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; import { mockIPC } from '@tauri-apps/api/mocks'; import { I18nProvider } from '../../src/i18n'; -import { ApiAccessPage } from '../../src/pages/ApiAccessPage'; +import { + ApiAccessPage, + apiAccessRemarkLocatorFromRecord, + providerRemarkIdentity, + type ApiAccessRemarkLocator, + type ProviderSection, +} from '../../src/pages/ApiAccessPage'; import '../../src/styles/index.css'; -localStorage.setItem('easy-cli-proxy-api.locale', new URLSearchParams(location.search).get('locale') ?? 'en'); +const query = new URLSearchParams(location.search); +const denseLayout = query.get('layout') === 'dense'; +localStorage.setItem('easy-cli-proxy-api.locale', query.get('locale') ?? 'en'); const fixture = window as typeof window & { groupFixture: { groups: Record