From 20908859639f238331e678d75796af18f1f71ebe Mon Sep 17 00:00:00 2001 From: changwei Date: Sun, 27 Sep 2026 02:29:10 +0800 Subject: [PATCH 1/2] fix: enhance font fallback support for multiple languages in CSS --- app/globals.css | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/app/globals.css b/app/globals.css index 45f39cef..2d086c7a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -43,12 +43,22 @@ --radius-2xl: calc(var(--radius) + 8px); --radius-3xl: calc(var(--radius) + 12px); --radius-4xl: calc(var(--radius) + 16px); - --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; - --font-heading: ui-monospace, "SFMono-Regular", "SF Mono", Menlo, Consolas, monospace; + --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", var(--font-fallback), sans-serif; + --font-heading: ui-monospace, "SFMono-Regular", "SF Mono", Menlo, Consolas, var(--font-fallback), monospace; + --font-mono: + ui-monospace, "SFMono-Regular", "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", + var(--font-fallback), monospace; } :root { color-scheme: light; + --font-latin: "Noto Sans", "DejaVu Sans", "Liberation Sans"; + --font-han: + "PingFang SC", "Microsoft YaHei", "Noto Sans SC", "Noto Sans CJK SC", "Source Han Sans SC", "WenQuanYi Micro Hei"; + --font-kana: "Hiragino Sans", "Yu Gothic", "Meiryo", "Noto Sans JP", "Noto Sans CJK JP"; + --font-hangul: "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans KR", "Noto Sans CJK KR"; + --font-arabic: "Noto Sans Arabic", "Noto Naskh Arabic", "Geeza Pro", "Tahoma"; + --font-fallback: var(--font-latin), var(--font-han), var(--font-kana), var(--font-hangul), var(--font-arabic); --card: oklch(1 0 0); --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); @@ -83,6 +93,14 @@ --foreground: oklch(0.145 0 0); } +:root:lang(ja) { + --font-fallback: var(--font-latin), var(--font-kana), var(--font-han), var(--font-hangul), var(--font-arabic); +} + +:root:lang(ko) { + --font-fallback: var(--font-latin), var(--font-hangul), var(--font-kana), var(--font-han), var(--font-arabic); +} + .dark { color-scheme: dark; --background: oklch(0.145 0 0); From c6fc96f9d22a1eca6bb30fb15c1ef55ddf2d672d Mon Sep 17 00:00:00 2001 From: changwei Date: Sun, 27 Sep 2026 04:30:58 +0800 Subject: [PATCH 2/2] feat: implement DANGER_BUTTON_CLASS for consistent destructive button styling --- app/(dashboard)/sse/page.tsx | 9 ++-- app/globals.css | 4 ++ components/danger-button.ts | 14 +++++ components/dialog-host.tsx | 5 +- components/object/list.tsx | 5 +- docs/ui-review/register.md | 72 +++++++++++++------------- tests/lib/danger-button.test.ts | 66 +++++++++++++++++++++++ tests/lib/font-fallback-source.test.ts | 63 ++++++++++++++++++++++ tests/lib/sse-safety.test.js | 2 +- 9 files changed, 195 insertions(+), 45 deletions(-) create mode 100644 components/danger-button.ts create mode 100644 tests/lib/danger-button.test.ts create mode 100644 tests/lib/font-fallback-source.test.ts diff --git a/app/(dashboard)/sse/page.tsx b/app/(dashboard)/sse/page.tsx index 0297a0df..bfa5ded4 100644 --- a/app/(dashboard)/sse/page.tsx +++ b/app/(dashboard)/sse/page.tsx @@ -49,6 +49,7 @@ import { type ConfigFormState, } from "@/lib/sse/config" import { RekeyCard } from "@/components/sse/rekey-card" +import { DANGER_BUTTON_CLASS } from "@/components/danger-button" import type { KmsBackendCapabilities, KmsConfigPayload, @@ -2263,7 +2264,7 @@ export default function SSEPage() { {startingKMS || stoppingKMS ? : null} {pendingServiceAction === "stop" ? t("Stop KMS") : t("Confirm")} @@ -2290,7 +2291,7 @@ export default function SSEPage() { {t("Discard")} @@ -2329,9 +2330,7 @@ export default function SSEPage() { Boolean(statusError) || Boolean(isPendingDefaultKey && pendingKeyAction?.type !== "cancelDeletion") } - className={ - pendingKeyAction?.type === "forceDelete" ? "bg-destructive text-destructive-foreground" : undefined - } + className={pendingKeyAction?.type === "forceDelete" ? DANGER_BUTTON_CLASS : undefined} > {processingKeyAction ? : null} {pendingKeyAction?.type === "cancelDeletion" diff --git a/app/globals.css b/app/globals.css index 2d086c7a..2483c516 100644 --- a/app/globals.css +++ b/app/globals.css @@ -24,6 +24,7 @@ --color-input: var(--input); --color-border: var(--border); --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); --color-accent-foreground: var(--accent-foreground); --color-accent: var(--accent); --color-muted-foreground: var(--muted-foreground); @@ -72,6 +73,7 @@ --accent: oklch(0.97 0 0); --accent-foreground: oklch(0.205 0 0); --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.985 0 0); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); --ring: oklch(0.708 0 0); @@ -118,6 +120,8 @@ --accent: oklch(0.269 0 0); --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.704 0.191 22.216); + /* Dark-theme destructive is lightened for tinted text, so its solid fills need a dark label. */ + --destructive-foreground: oklch(0.145 0 0); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.556 0 0); diff --git a/components/danger-button.ts b/components/danger-button.ts new file mode 100644 index 00000000..26c025b4 --- /dev/null +++ b/components/danger-button.ts @@ -0,0 +1,14 @@ +/** + * Solid danger styling for buttons that commit an irreversible action. + * + * The `destructive` Button variant only tints the background (`bg-destructive/10` + * with a `text-destructive` label), so it cannot carry a filled label. Buttons + * that confirm a destructive action layer this class on top of the base button + * classes instead, and read the label from `--destructive-foreground`. + * + * That token is a near-white on the light-theme red (4.6:1) and a near-black on + * the lightened dark-theme red (6.9:1), so the label clears WCAG AA in both + * themes. Override the fill or the label, never the two independently. + */ +export const DANGER_BUTTON_CLASS = + "bg-destructive text-destructive-foreground hover:bg-destructive/85 focus-visible:border-destructive/40 focus-visible:ring-destructive/30" diff --git a/components/dialog-host.tsx b/components/dialog-host.tsx index ed7e2cde..cc16070e 100644 --- a/components/dialog-host.tsx +++ b/components/dialog-host.tsx @@ -17,9 +17,10 @@ import { useDialogController } from "@/lib/feedback/dialog" import type { DialogInstance } from "@/lib/feedback/dialog" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" +import { DANGER_BUTTON_CLASS } from "@/components/danger-button" function positiveButtonVariant(dialog: DialogInstance) { - return dialog.tone === "destructive" ? "destructive" : dialog.tone === "warning" ? "secondary" : "default" + return dialog.tone === "warning" ? "secondary" : "default" } export function DialogHost() { @@ -79,7 +80,7 @@ export function DialogHost() { className={cn( buttonVariants({ variant: positiveButtonVariant(dialog) }), "w-full sm:w-auto", - positiveButtonVariant(dialog) === "destructive" && "text-white", + dialog.tone === "destructive" && DANGER_BUTTON_CLASS, )} onClick={(e) => { e.preventDefault() diff --git a/components/object/list.tsx b/components/object/list.tsx index 9a22315c..fc0ccab9 100644 --- a/components/object/list.tsx +++ b/components/object/list.tsx @@ -49,6 +49,7 @@ import { import { SearchInput } from "@/components/search-input" import { PageHeader } from "@/components/page-header" import { DataTable } from "@/components/data-table/data-table" +import { DANGER_BUTTON_CLASS } from "@/components/danger-button" import { Spinner } from "@/components/ui/spinner" import { useDataTable } from "@/hooks/use-data-table" import { useObject } from "@/hooks/use-object" @@ -61,6 +62,7 @@ import { exportFile } from "@/lib/export-file" import { getAttachmentContentDisposition } from "@/lib/content-disposition" import { getContentType } from "@/lib/mime-types" import { formatBytes, formatDateTime } from "@/lib/functions" +import { cn } from "@/lib/utils" import { normalizeDateToIso } from "@/lib/safe-date" import { buildBucketPath } from "@/lib/bucket-path" import { @@ -1086,8 +1088,7 @@ export function ObjectList({ diff --git a/docs/ui-review/register.md b/docs/ui-review/register.md index e2701035..5f618cdd 100644 --- a/docs/ui-review/register.md +++ b/docs/ui-review/register.md @@ -14,41 +14,42 @@ Use the resolved rows for the PR description: ## Audit record -| ID | Surface | Finding | Impact | Resolution | Evidence key | Status | -| ------ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------- | -------- | -| UI-001 | Running status | Loading, partial, and failed server data were visually conflated. | Operators could act on stale or incomplete health information. | Separated status states and added explicit feedback/retry treatment. | `running-status-10` | Resolved | -| UI-002 | Pool operations | Long-running rebalance/decommission flows did not consistently expose state, errors, and mobile-safe actions. | Destructive operations were difficult to verify and recover from. | Added state-aware action handling, error surfaces, and responsive layouts. | `pool-operations-9` | Resolved | -| UI-003 | Bucket settings | Configuration tabs had inconsistent hierarchy and several flows could reset state, mask errors, or apply unsafe replication semantics. | Misconfiguration risk and poor recovery in lifecycle, events, replication, and encryption flows. | Normalized settings layout; preserved form state; surfaced errors; corrected replication role semantics. | `bucket-settings-11` | Resolved | -| UI-004 | SSE status | A failed status request could be displayed as “not configured”, while stale backend/metrics remained visible; long default key IDs could overflow mobile cards. | Operators could configure or mutate KMS from an untrusted state, and mobile users could lose key context. | Keep the failure distinct from unconfigured; hide all stale details; block stateful actions while status syncs; wrap identifiers. | `sse-16-overview` | Resolved | -| UI-005 | SSE configuration | Long configuration form had weak grouping, incomplete required semantics, and no field-targeted client-side error recovery. | High cognitive load and inaccessible error correction. | Use fieldsets/legends, client validation with focus, and native/ARIA required state. | `sse-14-progressive` | Resolved | -| UI-006 | SSE mutations | KMS create/reconfigure/key actions could overlap or proceed from stale status; uncertain responses left the UI unverified. | Conflicting writes and incorrect default-key deletion decisions. | Serialize mutations, keep create locked while key inventory is loading or failed, reconcile uncertain outcomes from the server, and recheck default-key status before delete. | `sse-16-recaptured-states` | Resolved | -| UI-007 | SSE key actions | Mobile cards dropped schedule/cancel deletion actions; immediate deletion was over-prominent. | Mobile users did not have desktop-equivalent, safe key management. | Provide action parity and present staged deletion before immediate deletion. | `sse-18-key-card-flow` | Resolved | -| UI-008 | SSE confirmation | Key dialogs did not name the target/default-key risk; service restart/stop bypassed confirmation. | Operators could apply destructive/disruptive actions without enough context. | Target-aware destructive confirmation, service-state confirmation, and dismissal lock while processing. | `sse-17-service-stop` | Resolved | -| UI-009 | SSE secrets | Diagnostic request logging could expose credentials. | Vault token leakage through client diagnostics. | Redact secret-bearing request fields before logging. | `sse-13-security` | Resolved | -| UI-010 | SSE Local backend | Local KMS was offered without the backend contract needed to preserve/validate an opaque master key; a reconfiguration could make existing keys unreadable. | Production Local KMS configuration and default-key updates could weaken secret handling or make data unrecoverable. | Make Local configuration read-only in Console until the backend offers preservation plus verified rotation; keep Local default-key changes server-managed. | `sse-16-local-safety` | Resolved | -| UI-011 | SSE unsaved form | Sidebar navigation, browser history, and page exit could silently discard a long KMS configuration form. | Operators could lose backend settings or newly-entered credentials. | Keep a server baseline, guard unload/link/history navigation, and prohibit discarding while a mutation is in progress. | `sse-16-dirty-form` | Resolved | -| UI-012 | IAM user/group selectors | Multi-select controls did not expose their selected state, search purpose, or listbox semantics; edit mode could also leave keyboard focus behind. | Keyboard and assistive-technology users could not reliably understand or resume the current edit flow. | Added named search fields, put multiselect semantics on the actual CommandList, synchronized selected state around cmdk's active-option attribute, and restored focus only on real edit transitions. | `iam-13-a11y` | Resolved | -| UI-013 | SSE configuration | Reliability and cache controls made the mobile KMS form unnecessarily tall and pushed the primary actions below the fold. | First-time operators had to scan a dense expert-only section before reaching the save/reset actions. | Grouped retry/cache controls under a native Advanced Settings disclosure; validation state reopens the section when an affected field needs attention. | `sse-14-progressive` | Resolved | -| UI-014 | IAM group tables | Members and policy tables had no explicit accessible name in the reusable table primitive. | Screen-reader users could lose context when several tables or dialogs were present. | Added an optional visually-hidden table caption and supplied localized captions for the Members and Policies tables without changing the visual layout. | `iam-14-captions` | Resolved | -| UI-015 | OIDC configuration | Claim and role mapping controls occupied the same long mobile flow as the connection credentials and redirect settings. | Operators had to scan a dense expert-only section before reaching the next page section. | Kept provider identity, credentials, scopes, and redirect controls visible; moved claim/role mapping into a native Advanced Settings disclosure. | `oidc-15-progressive` | Resolved | -| UI-016 | Module switch settings | A failed module-switch read left an empty bordered panel after the toast disappeared, with no local retry and no visible indication that values were unavailable. | Operators could mistake an unavailable settings read for an empty configuration and could not recover in context. | Added a persistent destructive alert with the error detail and Sync retry; retained stale values as non-editable while a refresh is unresolved. | `settings-20-error` | Resolved | -| UI-017 | Access-key dialogs | Long credential forms could push actions out of the viewport and did not consistently explain expiry or field errors. | Operators could lose the only chance to copy credentials or submit a corrected form. | Fixed the dialog header/body/footer layout, kept actions reachable, and added expiry guidance plus field-specific validation. | `remaining-21` | Resolved | -| UI-018 | User/group workflows | Assignment and edit dialogs could grow with badges, conflate loading/error/empty states, and retry already-succeeded requests. | Bulk IAM changes were hard to recover and could leave users unsure which groups were updated. | Bounded dialog bodies, added explicit state/retry treatment, compact selection summaries, and retry only failed group operations. | `remaining-21` | Resolved | -| UI-019 | Shared tables/navigation | Pagination and table semantics were difficult to use on narrow screens and lacked context for assistive technology. | Keyboard and mobile users could lose table context or fail to reach page actions. | Added captions/sort semantics, mobile pagination layout, and a single predictable overflow surface. | `remaining-21` | Resolved | -| UI-020 | Utility surfaces | Long filenames, object rows, and license text could overflow or become unreadable on narrow screens. | Import/export and inspection tasks required horizontal scrolling or clipped content. | Added bounded readable surfaces, safe wrapping, and mobile-first action alignment. | `remaining-21` | Resolved | -| UI-021 | Authentication surfaces | Login/configuration hero layouts and actions did not consistently use dynamic viewport sizing or touch-safe targets. | Mobile sign-in could be clipped or require imprecise taps. | Switched to dynamic viewport-safe layout and full-width 44px mobile actions. | `remaining-21` | Resolved | -| UI-022 | Rule/task dialogs | Event, audit, lifecycle, replication, tier, and site-replication forms could hide their footer actions behind long content. | Operators could not reliably save or cancel long configuration flows. | Standardized one scrolling body with fixed header/footer and disabled dismissal while submitting. | `remaining-21` | Resolved | -| UI-023 | Dialog/form hierarchy | Full rectangular borders were reused for outer surfaces, semantic groups, descriptions, metadata, and progress states. | Repeated nested frames weakened hierarchy, increased cognitive load, and made passive content look interactive. | Initial remediation mechanically replaced boxes with recursive separators, while its fixture misrepresented the real hierarchy. Rework and valid recapture are pending. | `border-depth-22` invalid | Open | -| UI-024 | Section divider hierarchy | SSE configuration repeats dividers across sibling fieldsets, Advanced Settings, and nested cache content; key details and site-replication content also stack dividers across semantic levels. | Repeated lines imply false parent-child relationships and make content appear detached from its heading. | Use one divider system only between peer sections; use headings and spacing inside a section, and remove duplicate dividers after already-divided metadata. | Pending live capture | Open | -| UI-025 | Passive metadata surfaces | SSE status metrics, selected-pool details, and site-replication peer metadata are rendered as complete framed panels inside an existing Card or dialog surface. | Passive values look interactive and create card-inside-card depth. | Replace nested panels with definition lists, open grids, muted backgrounds, or a single section separator while retaining warning and selection affordances. | Pending live capture | Open | -| UI-026 | Compound editor rows | Lifecycle/replication tag editors, access-key policy switches, and IAM edit panels add complete frames around already-bordered controls inside dialogs. | Repeated outlines increase density and make ordinary fields look like independent subcards. | Keep input/control outlines, but express the containing editor row with spacing, a muted background, or one sibling divider unless it is itself a selectable object. | Pending live capture | Open | -| UI-027 | Account menu | The menu opened on an avatar with no name and no role, and password management was hidden from administrators because the backend rejected the `add-user` call it made. | Operators could not tell which identity they were signed in as, and administrators had no way to change their own password at all. | Named the identity and its authority on both the trigger and the menu; added Profile and Security entries; replaced the `add-user` call with `POST /account/password` so the control works for every identity that may use it. | `account-2fa-29` | Resolved | -| UI-028 | Recovery code disclosure | A newly generated recovery-code set is the only copy that will ever exist; a dialog that could be dismissed freely, or closed by its parent, would silently destroy it. | An operator could lose their only fallback into a two-factor-protected account without ever seeing a warning. | Kept setup in one dialog with internal steps instead of a nested chain, disabled the acknowledge action until the codes are copied or downloaded, and removed the close affordance while they are on screen. | `account-2fa-29` | Resolved | -| UI-027 | Login form | The desktop form pane used 7/12 of the split panel and allowed a short credential form to expand to `max-w-md`. | Credential fields looked visually loose and the form outweighed the supporting hero. | Balanced the desktop split at 1/2 and constrained the credential form to `max-w-sm` without changing mobile behavior. | `layout-followup-27` | Resolved | -| UI-028 | Edit Policy dialog | The complete form body owned vertical scrolling and the policy editor had a 24rem minimum height with manual resizing. | Name/context and editor moved together, and the editor could force an unnecessarily tall dialog. | Kept the form body fixed, made the Policy field consume remaining space, and confined scrolling to a non-resizable textarea with a 12rem minimum. | `layout-followup-27` | Resolved | -| UI-029 | Running status controls | Mobile `min-h-11` rules were also applied at desktop widths to Refresh, server filters, and the Sort by trigger. | Compact status toolbars appeared stretched and misaligned with surrounding desktop controls. | Preserved 44px controls on small screens and restored the shared compact control height from `sm` upward. | `layout-followup-27` | Resolved | -| UI-030 | Bucket rule detail pages | Events, Replication, and Lifecycle rendered Add/Refresh in a second toolbar below the page header, separate from Back navigation. | Related page-level actions were split across two rows and displaced the table downward. | Added a tab header render seam and composed Back, Add, and Refresh together in the page header for all three scoped bucket pages. | `layout-followup-27` | Resolved | -| UI-031 | Top navigation account | The account artwork and its button trigger were both 32px high, so the circular avatar consumed the trigger's complete vertical space. | The avatar visually touched the trigger boundary and crowded adjacent top-navigation controls. | Reduced the artwork to 24px while retaining the 32px button target, leaving a visible 4px inset around the avatar. | `avatar-inset-28` | Resolved | +| ID | Surface | Finding | Impact | Resolution | Evidence key | Status | +| ------ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | -------- | +| UI-001 | Running status | Loading, partial, and failed server data were visually conflated. | Operators could act on stale or incomplete health information. | Separated status states and added explicit feedback/retry treatment. | `running-status-10` | Resolved | +| UI-002 | Pool operations | Long-running rebalance/decommission flows did not consistently expose state, errors, and mobile-safe actions. | Destructive operations were difficult to verify and recover from. | Added state-aware action handling, error surfaces, and responsive layouts. | `pool-operations-9` | Resolved | +| UI-003 | Bucket settings | Configuration tabs had inconsistent hierarchy and several flows could reset state, mask errors, or apply unsafe replication semantics. | Misconfiguration risk and poor recovery in lifecycle, events, replication, and encryption flows. | Normalized settings layout; preserved form state; surfaced errors; corrected replication role semantics. | `bucket-settings-11` | Resolved | +| UI-004 | SSE status | A failed status request could be displayed as “not configured”, while stale backend/metrics remained visible; long default key IDs could overflow mobile cards. | Operators could configure or mutate KMS from an untrusted state, and mobile users could lose key context. | Keep the failure distinct from unconfigured; hide all stale details; block stateful actions while status syncs; wrap identifiers. | `sse-16-overview` | Resolved | +| UI-005 | SSE configuration | Long configuration form had weak grouping, incomplete required semantics, and no field-targeted client-side error recovery. | High cognitive load and inaccessible error correction. | Use fieldsets/legends, client validation with focus, and native/ARIA required state. | `sse-14-progressive` | Resolved | +| UI-006 | SSE mutations | KMS create/reconfigure/key actions could overlap or proceed from stale status; uncertain responses left the UI unverified. | Conflicting writes and incorrect default-key deletion decisions. | Serialize mutations, keep create locked while key inventory is loading or failed, reconcile uncertain outcomes from the server, and recheck default-key status before delete. | `sse-16-recaptured-states` | Resolved | +| UI-007 | SSE key actions | Mobile cards dropped schedule/cancel deletion actions; immediate deletion was over-prominent. | Mobile users did not have desktop-equivalent, safe key management. | Provide action parity and present staged deletion before immediate deletion. | `sse-18-key-card-flow` | Resolved | +| UI-008 | SSE confirmation | Key dialogs did not name the target/default-key risk; service restart/stop bypassed confirmation. | Operators could apply destructive/disruptive actions without enough context. | Target-aware destructive confirmation, service-state confirmation, and dismissal lock while processing. | `sse-17-service-stop` | Resolved | +| UI-009 | SSE secrets | Diagnostic request logging could expose credentials. | Vault token leakage through client diagnostics. | Redact secret-bearing request fields before logging. | `sse-13-security` | Resolved | +| UI-010 | SSE Local backend | Local KMS was offered without the backend contract needed to preserve/validate an opaque master key; a reconfiguration could make existing keys unreadable. | Production Local KMS configuration and default-key updates could weaken secret handling or make data unrecoverable. | Make Local configuration read-only in Console until the backend offers preservation plus verified rotation; keep Local default-key changes server-managed. | `sse-16-local-safety` | Resolved | +| UI-011 | SSE unsaved form | Sidebar navigation, browser history, and page exit could silently discard a long KMS configuration form. | Operators could lose backend settings or newly-entered credentials. | Keep a server baseline, guard unload/link/history navigation, and prohibit discarding while a mutation is in progress. | `sse-16-dirty-form` | Resolved | +| UI-012 | IAM user/group selectors | Multi-select controls did not expose their selected state, search purpose, or listbox semantics; edit mode could also leave keyboard focus behind. | Keyboard and assistive-technology users could not reliably understand or resume the current edit flow. | Added named search fields, put multiselect semantics on the actual CommandList, synchronized selected state around cmdk's active-option attribute, and restored focus only on real edit transitions. | `iam-13-a11y` | Resolved | +| UI-013 | SSE configuration | Reliability and cache controls made the mobile KMS form unnecessarily tall and pushed the primary actions below the fold. | First-time operators had to scan a dense expert-only section before reaching the save/reset actions. | Grouped retry/cache controls under a native Advanced Settings disclosure; validation state reopens the section when an affected field needs attention. | `sse-14-progressive` | Resolved | +| UI-014 | IAM group tables | Members and policy tables had no explicit accessible name in the reusable table primitive. | Screen-reader users could lose context when several tables or dialogs were present. | Added an optional visually-hidden table caption and supplied localized captions for the Members and Policies tables without changing the visual layout. | `iam-14-captions` | Resolved | +| UI-015 | OIDC configuration | Claim and role mapping controls occupied the same long mobile flow as the connection credentials and redirect settings. | Operators had to scan a dense expert-only section before reaching the next page section. | Kept provider identity, credentials, scopes, and redirect controls visible; moved claim/role mapping into a native Advanced Settings disclosure. | `oidc-15-progressive` | Resolved | +| UI-016 | Module switch settings | A failed module-switch read left an empty bordered panel after the toast disappeared, with no local retry and no visible indication that values were unavailable. | Operators could mistake an unavailable settings read for an empty configuration and could not recover in context. | Added a persistent destructive alert with the error detail and Sync retry; retained stale values as non-editable while a refresh is unresolved. | `settings-20-error` | Resolved | +| UI-017 | Access-key dialogs | Long credential forms could push actions out of the viewport and did not consistently explain expiry or field errors. | Operators could lose the only chance to copy credentials or submit a corrected form. | Fixed the dialog header/body/footer layout, kept actions reachable, and added expiry guidance plus field-specific validation. | `remaining-21` | Resolved | +| UI-018 | User/group workflows | Assignment and edit dialogs could grow with badges, conflate loading/error/empty states, and retry already-succeeded requests. | Bulk IAM changes were hard to recover and could leave users unsure which groups were updated. | Bounded dialog bodies, added explicit state/retry treatment, compact selection summaries, and retry only failed group operations. | `remaining-21` | Resolved | +| UI-019 | Shared tables/navigation | Pagination and table semantics were difficult to use on narrow screens and lacked context for assistive technology. | Keyboard and mobile users could lose table context or fail to reach page actions. | Added captions/sort semantics, mobile pagination layout, and a single predictable overflow surface. | `remaining-21` | Resolved | +| UI-020 | Utility surfaces | Long filenames, object rows, and license text could overflow or become unreadable on narrow screens. | Import/export and inspection tasks required horizontal scrolling or clipped content. | Added bounded readable surfaces, safe wrapping, and mobile-first action alignment. | `remaining-21` | Resolved | +| UI-021 | Authentication surfaces | Login/configuration hero layouts and actions did not consistently use dynamic viewport sizing or touch-safe targets. | Mobile sign-in could be clipped or require imprecise taps. | Switched to dynamic viewport-safe layout and full-width 44px mobile actions. | `remaining-21` | Resolved | +| UI-022 | Rule/task dialogs | Event, audit, lifecycle, replication, tier, and site-replication forms could hide their footer actions behind long content. | Operators could not reliably save or cancel long configuration flows. | Standardized one scrolling body with fixed header/footer and disabled dismissal while submitting. | `remaining-21` | Resolved | +| UI-023 | Dialog/form hierarchy | Full rectangular borders were reused for outer surfaces, semantic groups, descriptions, metadata, and progress states. | Repeated nested frames weakened hierarchy, increased cognitive load, and made passive content look interactive. | Initial remediation mechanically replaced boxes with recursive separators, while its fixture misrepresented the real hierarchy. Rework and valid recapture are pending. | `border-depth-22` invalid | Open | +| UI-024 | Section divider hierarchy | SSE configuration repeats dividers across sibling fieldsets, Advanced Settings, and nested cache content; key details and site-replication content also stack dividers across semantic levels. | Repeated lines imply false parent-child relationships and make content appear detached from its heading. | Use one divider system only between peer sections; use headings and spacing inside a section, and remove duplicate dividers after already-divided metadata. | Pending live capture | Open | +| UI-025 | Passive metadata surfaces | SSE status metrics, selected-pool details, and site-replication peer metadata are rendered as complete framed panels inside an existing Card or dialog surface. | Passive values look interactive and create card-inside-card depth. | Replace nested panels with definition lists, open grids, muted backgrounds, or a single section separator while retaining warning and selection affordances. | Pending live capture | Open | +| UI-026 | Compound editor rows | Lifecycle/replication tag editors, access-key policy switches, and IAM edit panels add complete frames around already-bordered controls inside dialogs. | Repeated outlines increase density and make ordinary fields look like independent subcards. | Keep input/control outlines, but express the containing editor row with spacing, a muted background, or one sibling divider unless it is itself a selectable object. | Pending live capture | Open | +| UI-027 | Account menu | The menu opened on an avatar with no name and no role, and password management was hidden from administrators because the backend rejected the `add-user` call it made. | Operators could not tell which identity they were signed in as, and administrators had no way to change their own password at all. | Named the identity and its authority on both the trigger and the menu; added Profile and Security entries; replaced the `add-user` call with `POST /account/password` so the control works for every identity that may use it. | `account-2fa-29` | Resolved | +| UI-028 | Recovery code disclosure | A newly generated recovery-code set is the only copy that will ever exist; a dialog that could be dismissed freely, or closed by its parent, would silently destroy it. | An operator could lose their only fallback into a two-factor-protected account without ever seeing a warning. | Kept setup in one dialog with internal steps instead of a nested chain, disabled the acknowledge action until the codes are copied or downloaded, and removed the close affordance while they are on screen. | `account-2fa-29` | Resolved | +| UI-027 | Login form | The desktop form pane used 7/12 of the split panel and allowed a short credential form to expand to `max-w-md`. | Credential fields looked visually loose and the form outweighed the supporting hero. | Balanced the desktop split at 1/2 and constrained the credential form to `max-w-sm` without changing mobile behavior. | `layout-followup-27` | Resolved | +| UI-028 | Edit Policy dialog | The complete form body owned vertical scrolling and the policy editor had a 24rem minimum height with manual resizing. | Name/context and editor moved together, and the editor could force an unnecessarily tall dialog. | Kept the form body fixed, made the Policy field consume remaining space, and confined scrolling to a non-resizable textarea with a 12rem minimum. | `layout-followup-27` | Resolved | +| UI-029 | Running status controls | Mobile `min-h-11` rules were also applied at desktop widths to Refresh, server filters, and the Sort by trigger. | Compact status toolbars appeared stretched and misaligned with surrounding desktop controls. | Preserved 44px controls on small screens and restored the shared compact control height from `sm` upward. | `layout-followup-27` | Resolved | +| UI-030 | Bucket rule detail pages | Events, Replication, and Lifecycle rendered Add/Refresh in a second toolbar below the page header, separate from Back navigation. | Related page-level actions were split across two rows and displaced the table downward. | Added a tab header render seam and composed Back, Add, and Refresh together in the page header for all three scoped bucket pages. | `layout-followup-27` | Resolved | +| UI-031 | Top navigation account | The account artwork and its button trigger were both 32px high, so the circular avatar consumed the trigger's complete vertical space. | The avatar visually touched the trigger boundary and crowded adjacent top-navigation controls. | Reduced the artwork to 24px while retaining the 32px button target, leaving a visible 4px inset around the avatar. | `avatar-inset-28` | Resolved | +| UI-032 | Danger confirm buttons | `text-destructive-foreground` was never registered in `@theme inline`, so the solid `bg-destructive` confirm buttons in the SSE discard, stop-KMS, and force-delete dialogs emitted no CSS and inherited `text-popover-foreground` (black on red). Two other sites pushed `text-white` onto the tinted `destructive` Button variant, which removed its red label. | Destructive confirmations were unreadable in both themes: 4.2:1 in light mode and 2.8:1 in dark mode for the SSE dialogs, 1.2:1 for the shared `dialog.error` confirms and the object delete confirm. | Registered `--destructive-foreground` as a light label on the light-theme red and a dark label on the lightened dark-theme red (4.6:1 / 6.9:1), and routed every filled danger button through one shared `DANGER_BUTTON_CLASS` so the fill and its label can no longer drift apart. | `danger-button-32` pending | Resolved | ### Reconciled border rule @@ -85,6 +86,7 @@ The 2026-07-11 follow-up reviewed 44 candidate files containing cards, dialogs, - `border-depth-22` is invalid because the fixture invented a nested SSE hierarchy that does not match the component tree. Do not cite it as improvement evidence. - `layout-followup-27` is a source-faithful static fixture for the four changed layout relationships; source tests and component code remain the implementation proof until authenticated live captures replace it. - `avatar-inset-28` is a source-faithful static fixture for the exact 32px trigger and 32px-to-24px artwork change; source tests and component code remain authoritative. +- `danger-button-32` has no before/after capture yet. The implementation proof is `tests/lib/danger-button.test.ts` plus the compiled CSS, which emits no `.text-destructive-foreground` rule before the change and `color: var(--destructive-foreground)` after it. Capture light and dark dialogs for the SSE discard, stop-KMS, force-delete, object delete, and a `dialog.error` confirm before citing this key. - `account-2fa-29` is a set of live runtime captures, not fixtures: a debug RustFS server with `RUSTFS_IAM_MASTER_KEY` set, this branch on `pnpm dev`, root identity, viewport 1585x1202 CSS px (the setup-dialog frame is 628 CSS px). It covers 2FA off/on, the setup QR, the recovery-code dismissal guard, the profile page, and the login second-factor step. It does **not** include a mobile viewport or a `main`-branch before-capture of the account menu; the responsive rules and the removed administrator gate are covered by source tests, which remain authoritative for those two. - The full on-disk mapping is maintained in the screenshot manifest next to the image archive. diff --git a/tests/lib/danger-button.test.ts b/tests/lib/danger-button.test.ts new file mode 100644 index 00000000..edc5e46a --- /dev/null +++ b/tests/lib/danger-button.test.ts @@ -0,0 +1,66 @@ +import test from "node:test" +import assert from "node:assert/strict" +import fs from "node:fs" + +// The working tree is CRLF on Windows, so normalize before matching line-anchored patterns. +const read = (file: string) => fs.readFileSync(file, "utf8").replace(/\r\n/g, "\n") + +const css = read("app/globals.css") +const dangerButton = read("components/danger-button.ts") +const dialogHost = read("components/dialog-host.tsx") +const objectList = read("components/object/list.tsx") + +const DANGER_USAGES = [ + ["components/dialog-host.tsx", dialogHost], + ["components/object/list.tsx", objectList], + ["app/(dashboard)/sse/page.tsx", read("app/(dashboard)/sse/page.tsx")], +] + +test("destructive label color is a registered theme token", () => { + assert.match(css, /--color-destructive-foreground: var\(--destructive-foreground\)/) + assert.match(css, /:root \{[^}]*--destructive-foreground:/s) + assert.match(css, /\.dark \{[^}]*--destructive-foreground:/s) +}) + +const DANGER_FILL_PAIR = + /--destructive:\s*([^;]+);\s*\n\s*(?:\/\*[^*]*\*\/\s*\n\s*)?--destructive-foreground:\s*([^;]+);/ + +const oklchLightness = (value: string) => Number(/oklch\(([\d.]+)/.exec(value)?.[1]) + +test("destructive label is a light neutral on the light-theme fill and a dark one on the dark-theme fill", () => { + const light = DANGER_FILL_PAIR.exec(css.slice(0, css.indexOf(".dark {"))) + assert.ok(light, "light theme must pair --destructive with --destructive-foreground") + + const dark = DANGER_FILL_PAIR.exec(css.slice(css.indexOf(".dark {"))) + assert.ok(dark, "dark theme must pair --destructive with --destructive-foreground") + + assert.ok( + oklchLightness(light[2]) > 0.9, + "light theme destructive fill needs a near-white label so the filled button stays readable", + ) + assert.ok( + oklchLightness(dark[1]) > oklchLightness(dark[2]), + "the dark theme lightens --destructive for tinted text, so its filled button needs a dark label", + ) +}) + +test("every filled danger button reads its label from the destructive-foreground token", () => { + assert.match(dangerButton, /bg-destructive text-destructive-foreground/) + assert.doesNotMatch(dangerButton, /\btext-white\b/) + + for (const [file, source] of DANGER_USAGES) { + assert.match(source, /DANGER_BUTTON_CLASS/, `${file} must use the shared danger button class`) + } +}) + +test("no filled danger button pairs a solid fill with a hardcoded label color", () => { + const solidFillLabel = /(?:bg-destructive\b[^"'\s]*)[^\n]{0,80}?\btext-(?!destructive\b)[a-z]/ + for (const [file, source] of DANGER_USAGES) { + assert.doesNotMatch(source, solidFillLabel, `${file} must not set a label color beside a solid danger fill`) + } +}) + +test("the soft destructive button variant is never given a filled label color", () => { + assert.doesNotMatch(dialogHost, /variant: "destructive"/) + assert.doesNotMatch(objectList, /variant="destructive"[\s\S]{0,120}?text-white/) +}) diff --git a/tests/lib/font-fallback-source.test.ts b/tests/lib/font-fallback-source.test.ts new file mode 100644 index 00000000..c64b367b --- /dev/null +++ b/tests/lib/font-fallback-source.test.ts @@ -0,0 +1,63 @@ +import test from "node:test" +import assert from "node:assert/strict" +import { readFile } from "node:fs/promises" + +const globalsCssUrl = new URL("../../app/globals.css", import.meta.url) + +const SCRIPT_FALLBACKS = ["latin", "han", "kana", "hangul", "arabic"] as const + +function readStack(source: string, token: string): string { + const stack = source.match(new RegExp(`${token}:([^;]+);`))?.[1] + assert.ok(stack, `missing ${token} declaration`) + return stack +} + +test("font stacks name script faces before the generic family", async () => { + const source = await readFile(globalsCssUrl, "utf8") + + for (const token of ["--font-sans", "--font-heading", "--font-mono"]) { + const stack = readStack(source, token) + const fallbackIndex = stack.indexOf("var(--font-fallback)") + const genericIndex = stack.search(/(sans-serif|monospace)\s*$/) + + assert.ok(fallbackIndex >= 0, `${token} must list var(--font-fallback)`) + assert.ok(genericIndex > fallbackIndex, `${token} must place script faces before its generic family`) + } +}) + +test("every supported script has a concrete fallback face", async () => { + const source = await readFile(globalsCssUrl, "utf8") + + for (const script of SCRIPT_FALLBACKS) { + const faces = readStack(source, `--font-${script}`) + assert.match(faces, /"[^"]+"/, `--font-${script} must name at least one concrete family`) + } + + const fallback = readStack(source, "--font-fallback") + for (const script of SCRIPT_FALLBACKS) { + assert.ok(fallback.includes(`var(--font-${script})`), `--font-fallback must include --font-${script}`) + } +}) + +test("Japanese and Korean prefer their own faces over Chinese ones", async () => { + const source = await readFile(globalsCssUrl, "utf8") + + const kanaFirst = readStack(source, "--font-fallback") + assert.ok(source.includes(":root:lang(ja)"), "missing Japanese language override") + + const jaStack = source.match(/:root:lang\(ja\)\s*\{[\s\S]*?\}/)?.[0] + assert.ok(jaStack) + assert.ok(jaStack.indexOf("var(--font-kana)") < jaStack.indexOf("var(--font-han)")) + + const koStack = source.match(/:root:lang\(ko\)\s*\{[\s\S]*?\}/)?.[0] + assert.ok(koStack) + assert.ok(koStack.indexOf("var(--font-hangul)") < koStack.indexOf("var(--font-han)")) + + assert.ok(kanaFirst.includes("var(--font-han)")) + assert.ok(kanaFirst.includes("var(--font-hangul)")) +}) + +test("the document root applies the sans stack so the fallback is always active", async () => { + const source = await readFile(globalsCssUrl, "utf8") + assert.match(source, /html\s*\{\s*@apply font-sans;/) +}) diff --git a/tests/lib/sse-safety.test.js b/tests/lib/sse-safety.test.js index 68afd0f3..36f1fa98 100644 --- a/tests/lib/sse-safety.test.js +++ b/tests/lib/sse-safety.test.js @@ -133,7 +133,7 @@ test("SSE confirmations name the risk and cannot close while an action is proces assert.match(source, /if \(!open && creatingKey\) return/) assert.match(source, /if \(!open && !processingKeyAction\) setPendingKeyAction\(null\)/) assert.match(source, /pendingKeyAction\?\.type === "forceDelete"\n\s+\? t\("Delete Key Immediately"\)/) - assert.match(source, /bg-destructive text-destructive-foreground/) + assert.match(source, /DANGER_BUTTON_CLASS/) assert.match(source, /KMS will be stopped\. SSE and key management will be unavailable until you start KMS again\./) assert.match(source, /pendingServiceAction === "stop" \? t\("Stop KMS"\) : t\("Confirm"\)/) })