From 8978f2d167dd2d6174f5a86fe3805da9e2ba508d Mon Sep 17 00:00:00 2001 From: Huarch Date: Wed, 15 Jul 2026 11:41:51 +0800 Subject: [PATCH] refactor(ui): refine glass surfaces --- DESIGN.md | 66 ++++--- app/globals.css | 100 +++++----- .../agent/components/agent-collapsed-rail.tsx | 76 ++------ .../agent/components/agent-command-panel.tsx | 184 ++++++++++++------ .../map/core/components/map-control-styles.ts | 3 + features/map/core/components/scale-line.tsx | 4 +- .../workbench/components/feature-popover.tsx | 27 +-- .../components/scheduled-condition-feed.tsx | 6 +- .../workbench/hooks/use-workbench-agent.ts | 2 +- features/workbench/map-workbench-page.tsx | 7 +- shared/ai-elements/persona.tsx | 4 +- 11 files changed, 271 insertions(+), 208 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index ed8dfa6..d6f58cc 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -91,13 +91,13 @@ Recommended canvas colors: | `canvas-map` | `#eaf1f8` | Fallback map background | | `canvas-map-grid` | `rgba(37, 99, 235, 0.06)` | Subtle engineering grid | | `canvas-page` | `#f8fafc` | Non-map support pages | -| `canvas-panel` | `rgba(255, 255, 255, 0.94)` | Default information-bearing floating panel | +| `canvas-panel` | Named glass material | Use the material level for the component category | | `canvas-panel-strong` | `rgba(255, 255, 255, 0.98)` | Dense text or form surface | | `canvas-panel-soft` | `rgba(255, 255, 255, 0.84)` | Low-density spatial overlay without critical text | -| `surface-overlay` | `rgba(255, 255, 255, 0.90-0.94)` | Floating menu or compact panel shell | -| `surface-readable` | `rgba(255, 255, 255, 0.94)` | Readable rows, menu headers, property panels and content blocks | -| `surface-map-control` | `rgba(255, 255, 255, 0.92)` | Always-visible and persistent map instruments: toolbar, zoom, scaleline, Agent shell, scheduled-condition shell | -| `surface-tool-panel` | `rgba(255, 255, 255, 0.90)` | Toolbar function panel outer shell | +| `surface-overlay` | `glass-menu` | Floating menu or compact choice shell | +| `surface-readable` | `glass-readable` | Right-side details, compact results and summaries | +| `surface-map-control` | `glass-control` | Always-visible toolbar, zoom and compact task shells | +| `surface-tool-panel` | `glass-panel` | Toolbar panels, expanded conditions and Agent shell | ### Color System @@ -223,13 +223,13 @@ Transparency is a material system, not a collection of arbitrary opacity utiliti | Token | Background | Blur | Shadow | Intended use | |---|---|---:|---|---| -| `glass-air` | `rgba(255, 255, 255, 0.84)` | `16px` | `0 10px 30px rgba(15, 23, 42, 0.10)` | Sparse map labels, scale line and low-density overlays without critical text | -| `glass-navigation` | `rgba(255, 255, 255, 0.90)` | `24px` | `0 8px 28px rgba(15, 23, 42, 0.10)` | Top bar and other broad, low-density navigation surfaces | -| `glass-panel` | `rgba(255, 255, 255, 0.90)` | `20px` | `0 24px 70px rgba(30, 64, 115, 0.18)` | Layer, base-map, measure, drawing and other temporary tool panels | -| `glass-readable` | `rgba(255, 255, 255, 0.94)` | `20px` | `0 24px 70px rgba(30, 64, 115, 0.20)` | Feature side panels, SCADA details, menus, compact results and information surfaces | -| `glass-control` | `rgba(255, 255, 255, 0.92)` | `16px` | `0 14px 34px rgba(15, 23, 42, 0.18)` | Persistent map instruments, Agent shell and scheduled-condition shell | -| `glass-menu` | `rgba(255, 255, 255, 0.94)` | `20px` | `0 18px 48px rgba(15, 23, 42, 0.16)` | Dropdown menus, notices, command palettes and transient choices | -| `glass-focus` | `rgba(255, 255, 255, 0.94)` | `24px` | `0 30px 90px rgba(15, 23, 42, 0.26)` | Centered feature popovers, SCADA popovers and focused confirmations | +| `glass-air` | `rgba(255, 255, 255, 0.68)` | `16px` | `0 10px 30px rgba(15, 23, 42, 0.10)` | Scale line, coordinates and lightweight map hints | +| `glass-navigation` | `rgba(255, 255, 255, 0.80)` | `24px` | `0 8px 28px rgba(15, 23, 42, 0.10)` | Top bar and broad low-density navigation surfaces | +| `glass-panel` | `rgba(255, 255, 255, 0.84)` | `20px` | `0 24px 70px rgba(30, 64, 115, 0.18)` | Tool panels, expanded conditions and Agent shell | +| `glass-control` | `rgba(255, 255, 255, 0.90)` | `16px` | `0 14px 34px rgba(15, 23, 42, 0.18)` | Zoom, persistent toolbars and compact task shells | +| `glass-readable` | `rgba(255, 255, 255, 0.90)` | `20px` | `0 24px 70px rgba(30, 64, 115, 0.20)` | Right-side details, compact results and summaries | +| `glass-focus` | `rgba(255, 255, 255, 0.94)` | `24px` | Tailwind `shadow-2xl shadow-blue-950/20` | Normal and SCADA property popovers only | +| `glass-menu` | `rgba(255, 255, 255, 0.92)` | `20px` | `0 18px 48px rgba(15, 23, 42, 0.16)` | Dropdown menus and notices | | `surface-solid` | `rgba(255, 255, 255, 0.98)` | `0` | `0 20px 60px rgba(15, 23, 42, 0.18)` | Forms, reports, long text, destructive confirmations and accessibility fallback | | `scrim-soft` | `rgba(15, 23, 42, 0.20)` | `0` | none | Non-destructive drawers and contextual focus | | `scrim-focus` | `rgba(15, 23, 42, 0.48)` | `0` | none | Modal decisions, destructive or irreversible actions | @@ -258,15 +258,38 @@ Colored glass communicates operational meaning. It must not be used as decoratio Text on colored glass must use a darker shade of the same hue. Do not place neutral gray text on a colored surface. +#### Canonical Focus Glass + +Normal and SCADA property popovers are the readability benchmark. Their outer shell must use this exact Tailwind combination: + +```text +bg-white/[0.94] backdrop-blur-xl +shadow-2xl shadow-blue-950/20 +ring-1 ring-white/80 +``` + +Do not add `saturate()`, gradients, acrylic effects, heavier custom focus shadows, or full-width white surfaces beneath the header and body. Attribute rows use low-opacity dividers. Device-type headers use only a small `bg-*-50/70` tint. + +#### Layering And Performance + +- Use at most one `backdrop-filter` in the same visual region. Inner cards never blur again. +- Do not place full-height `white/90+` child panels inside a glass shell. +- Large surfaces gain hierarchy by lowering white coverage, not by increasing blur. +- Reports, forms, long text, multi-column tables and dangerous confirmations use `surface-solid`. +- Browsers without backdrop-filter fall back to 96% white. +- `prefers-reduced-transparency` disables blur and uses `surface-solid`. +- Coarse pointers retain transparency. Use a solid surface only behind an explicit performance-degradation condition. + #### Component Material Mapping | Component category | Material token | Notes | |---|---|---| | Workbench top bar | `glass-navigation` | Preserve map context across the full width; active items use a semantic colored glass token | -| Zoom, scale line and compact toolbar | `glass-control` | Persistent controls need stable definition over every basemap and use `glass-edge` rather than an opaque border | +| Scale line, coordinates and lightweight hints | `glass-air` | Sparse overlays should preserve the strongest perception of the scattered map | +| Zoom and compact toolbar | `glass-control` | Persistent controls need stable definition over every basemap and use `glass-edge` rather than an opaque border | | Layer, base-map, measure and drawing panels | `glass-panel` | Use one translucent outer shell; repeated controls inside should be flat or lightly tinted | -| Agent command shell | `glass-control` | Conversation content may use `surface-solid` only for long generated text or form controls | -| Scheduled-condition task shell | `glass-control` | Remove stacked white layers; use dividers and state tint to create hierarchy | +| Agent command shell | `glass-panel` | Conversation content may use `surface-solid` only for long generated text or form controls | +| Scheduled-condition task shell | `glass-control` collapsed, `glass-panel` expanded | Remove stacked full-height white layers; use dividers and state tint to create hierarchy | | Feature and SCADA property popovers | `glass-focus` | Reuse the proven high-readability shell and distinguish focus through blur and elevation rather than a whiter background | | Feature insight side panel | `glass-readable` | Dense values remain on the shared glass surface; avoid card-per-field nesting | | Dropdown menus and notices | `glass-menu` | More opaque because the surface is temporary and action-dense | @@ -276,15 +299,14 @@ Text on colored glass must use a darker shade of the same hue. Do not place neut #### Composition Rules -- Use at most two translucent layers over the map at the same location. Three stacked white-alpha surfaces become visually opaque and waste GPU work. +- Use at most one backdrop-filter layer over the map at the same location. Inner translucent tints must not apply blur. - A glass shell should usually contain flat sections separated by `glass-divider`. Do not place an opaque white card around every row. - Long text, editable fields, tables with many columns and destructive confirmations use `surface-solid` even when their parent shell is translucent. -- Readable content must never be less opaque than its surrounding shell. -- When map detail beneath the panel is too noisy, move one step toward `surface-solid`. Do not keep increasing blur. +- When map detail beneath the panel is too noisy, move the outer shell one material level toward `surface-solid`. Do not keep increasing blur or add an opaque full-height child. - Use colored glass only for status, selection, device type or Agent inference. Neutral structure remains white glass. - Semantic colored glass must sit inside a `glass-readable`, `glass-control`, `glass-menu`, or `glass-focus` neutral base. Do not place a 70-82% colored surface directly over the map. -- High-readability glass is the global default for information-bearing UI. Any surface containing operational text must be at least 90% opaque; properties, results, menus and multi-line content use 94%. -- Acrylic-style saturation and edge highlights are limited to broad navigation and transient outer shells. Do not add texture, tint or refractive treatment to property tables, forms, reports or long text. +- Opacity follows component area and information density. Operational text may live on an 84% panel shell when local long-form content uses `surface-solid`; only property focus popovers use 94% glass. +- Do not apply acrylic-style saturation, texture or refractive treatment to any material level. - Prefer layered `box-shadow` or an inset ring for glass edges. A solid high-contrast border breaks the depth illusion. - Hover feedback may change highlight and edge opacity. Do not animate `backdrop-filter` or background blur. @@ -296,8 +318,8 @@ Text on colored glass must use a darker shade of the same hue. Do not place neut - Under `prefers-reduced-transparency: reduce`, replace every glass material with `surface-solid` and remove `backdrop-filter`. - When `backdrop-filter` is unsupported, use `rgba(255, 255, 255, 0.96)` with the same shadow and radius. -- On low-power or touch-first devices, prefer `glass-readable` or `surface-solid` over a larger blur radius. -- On coarse-pointer devices, remove backdrop blur and use a 0.96-0.98 white surface while preserving semantic state colors. +- Low-power fallback must be activated by an explicit performance-degradation condition, not inferred from touch or coarse pointer input. +- Coarse-pointer devices keep the same transparency hierarchy by default. - Never apply backdrop blur separately to every list row. Blur belongs to the shared outer surface. - Maintain WCAG AA contrast for all body text and controls against the composited result, not only against the declared RGBA value. diff --git a/app/globals.css b/app/globals.css index e8558e1..bdc05c2 100644 --- a/app/globals.css +++ b/app/globals.css @@ -19,13 +19,13 @@ --border-strong: rgba(148, 163, 184, 0.34); --shadow-control: 0 14px 34px rgba(15, 23, 42, 0.18); --shadow-panel: 0 24px 70px rgba(30, 64, 115, 0.2); - --shadow-focus: 0 30px 90px rgba(15, 23, 42, 0.26); - --glass-air: rgba(255, 255, 255, 0.84); - --glass-navigation: rgba(255, 255, 255, 0.9); - --glass-panel: rgba(255, 255, 255, 0.9); - --glass-readable: rgba(255, 255, 255, 0.94); - --glass-control: rgba(255, 255, 255, 0.92); - --glass-menu: rgba(255, 255, 255, 0.94); + --shadow-focus: 0 25px 50px -12px rgba(23, 37, 84, 0.2); + --glass-air: rgba(255, 255, 255, 0.68); + --glass-navigation: rgba(255, 255, 255, 0.8); + --glass-panel: rgba(255, 255, 255, 0.84); + --glass-readable: rgba(255, 255, 255, 0.9); + --glass-control: rgba(255, 255, 255, 0.9); + --glass-menu: rgba(255, 255, 255, 0.92); --glass-focus: rgba(255, 255, 255, 0.94); --surface-solid: rgba(255, 255, 255, 0.98); --glass-edge: rgba(255, 255, 255, 0.58); @@ -112,10 +112,11 @@ input:focus-visible { box-shadow: inset 0 1px 0 var(--glass-edge), var(--shadow-panel); - backdrop-filter: blur(var(--blur-panel)) saturate(1.08); - -webkit-backdrop-filter: blur(var(--blur-panel)) saturate(1.08); + backdrop-filter: blur(var(--blur-panel)); + -webkit-backdrop-filter: blur(var(--blur-panel)); } +.glass-air, .glass-navigation, .glass-panel, .glass-readable, @@ -131,13 +132,22 @@ input:focus-visible { background-clip: padding-box; } +.glass-air { + background-color: var(--glass-air); + box-shadow: + inset 0 1px 0 var(--glass-edge), + 0 10px 30px rgba(15, 23, 42, 0.1); + backdrop-filter: blur(var(--blur-air)); + -webkit-backdrop-filter: blur(var(--blur-air)); +} + .glass-navigation { background-color: var(--glass-navigation); box-shadow: inset 0 1px 0 var(--glass-edge), 0 8px 28px rgba(15, 23, 42, 0.1); - backdrop-filter: blur(var(--blur-navigation)) saturate(1.08); - -webkit-backdrop-filter: blur(var(--blur-navigation)) saturate(1.08); + backdrop-filter: blur(var(--blur-navigation)); + -webkit-backdrop-filter: blur(var(--blur-navigation)); } .glass-panel { @@ -145,8 +155,8 @@ input:focus-visible { box-shadow: inset 0 1px 0 var(--glass-edge), 0 24px 70px rgba(30, 64, 115, 0.18); - backdrop-filter: blur(var(--blur-panel)) saturate(1.08); - -webkit-backdrop-filter: blur(var(--blur-panel)) saturate(1.08); + backdrop-filter: blur(var(--blur-panel)); + -webkit-backdrop-filter: blur(var(--blur-panel)); } .glass-readable { @@ -154,8 +164,8 @@ input:focus-visible { box-shadow: inset 0 1px 0 var(--glass-edge-strong), 0 24px 70px rgba(30, 64, 115, 0.2); - backdrop-filter: blur(var(--blur-panel)) saturate(1.06); - -webkit-backdrop-filter: blur(var(--blur-panel)) saturate(1.06); + backdrop-filter: blur(var(--blur-panel)); + -webkit-backdrop-filter: blur(var(--blur-panel)); } .glass-control { @@ -163,8 +173,8 @@ input:focus-visible { box-shadow: inset 0 1px 0 var(--glass-edge-strong), var(--shadow-control); - backdrop-filter: blur(var(--blur-control)) saturate(1.08); - -webkit-backdrop-filter: blur(var(--blur-control)) saturate(1.08); + backdrop-filter: blur(var(--blur-control)); + -webkit-backdrop-filter: blur(var(--blur-control)); } .glass-menu { @@ -172,8 +182,8 @@ input:focus-visible { box-shadow: inset 0 1px 0 var(--glass-edge-strong), 0 18px 48px rgba(15, 23, 42, 0.16); - backdrop-filter: blur(var(--blur-panel)) saturate(1.04); - -webkit-backdrop-filter: blur(var(--blur-panel)) saturate(1.04); + backdrop-filter: blur(var(--blur-panel)); + -webkit-backdrop-filter: blur(var(--blur-panel)); } .glass-focus { @@ -181,8 +191,8 @@ input:focus-visible { box-shadow: inset 0 1px 0 var(--glass-edge-strong), var(--shadow-focus); - backdrop-filter: blur(var(--blur-focus)) saturate(1.06); - -webkit-backdrop-filter: blur(var(--blur-focus)) saturate(1.06); + backdrop-filter: blur(var(--blur-focus)); + -webkit-backdrop-filter: blur(var(--blur-focus)); } .glass-info { @@ -211,6 +221,7 @@ input:focus-visible { } @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { + .glass-air, .glass-navigation, .glass-panel, .glass-readable, @@ -243,6 +254,7 @@ input:focus-visible { } @media (prefers-reduced-transparency: reduce) { + .glass-air, .glass-navigation, .glass-panel, .glass-readable, @@ -276,37 +288,27 @@ input:focus-visible { } } -@media (pointer: coarse) { - .glass-navigation, - .glass-panel, - .glass-readable, - .glass-control, - .glass-menu, - .glass-focus, - .command-panel { - background-color: rgba(255, 255, 255, 0.96); - backdrop-filter: none; - -webkit-backdrop-filter: none; - } - - .glass-control, - .glass-menu, - .glass-focus { - background-color: var(--surface-solid); - } +.agent-panel-shell { + border-color: rgba(148, 163, 184, 0.34); + background: #f8fafc; + box-shadow: + 0 24px 64px rgba(15, 23, 42, 0.18), + 0 2px 8px rgba(15, 23, 42, 0.08); + backdrop-filter: none; + -webkit-backdrop-filter: none; } .agent-panel-band { - background: var(--glass-menu); + background: #ffffff; } .agent-panel-conversation { - background: rgba(255, 255, 255, 0.34); + background: #f8fafc; } .agent-panel-message { - border: 1px solid rgba(226, 232, 240, 0.72); - background: rgba(255, 255, 255, 0.94); + border: 1px solid #e2e8f0; + background: #ffffff; } .agent-streaming-response { @@ -331,18 +333,18 @@ input:focus-visible { } .agent-panel-nested { - border: 1px solid rgba(203, 213, 225, 0.72); - background: rgba(248, 250, 252, 0.9); + border: 1px solid #e2e8f0; + background: #f8fafc; } .agent-panel-control { - border: 1px solid rgba(255, 255, 255, 0.8); - background: rgba(255, 255, 255, 0.96); + border: 1px solid #e2e8f0; + background: #ffffff; } .agent-panel-icon-button { - border: 1px solid rgba(203, 213, 225, 0.72); - background: rgba(255, 255, 255, 0.92); + border: 1px solid #e2e8f0; + background: #f8fafc; box-shadow: none; } diff --git a/features/agent/components/agent-collapsed-rail.tsx b/features/agent/components/agent-collapsed-rail.tsx index 2d77737..c8096ed 100644 --- a/features/agent/components/agent-collapsed-rail.tsx +++ b/features/agent/components/agent-collapsed-rail.tsx @@ -4,7 +4,6 @@ import { ChevronRight } from "lucide-react"; import type { PersonaState } from "@/shared/ai-elements/persona"; import { cn } from "@/lib/utils"; import { - MAP_CONTROL_SURFACE_CLASS_NAME, MAP_MAJOR_PANEL_RADIUS_CLASS_NAME } from "@/features/map/core/components/map-control-styles"; import { AgentPersona } from "./agent-persona"; @@ -16,64 +15,25 @@ type AgentCollapsedRailProps = { }; export function AgentCollapsedRail({ personaState, statusLabel = "Agent", onExpand }: AgentCollapsedRailProps) { - const state = getCollapsedAgentState(statusLabel, personaState); - return ( -