feat: 统一界面视觉规范并优化导出交互

This commit is contained in:
Wxw-Gu
2026-08-20 20:13:23 +08:00
parent 65830d70fe
commit 740bec6d04
28 changed files with 777 additions and 1468 deletions
@@ -177,7 +177,7 @@ export function ExportConfigurationPanel({
aria-pressed={active} aria-pressed={active}
className={`${ className={`${
active ? 'active border-2 border-primary bg-primary/10 hover:bg-primary/15' : '' active ? 'active border-2 border-primary bg-primary/10 hover:bg-primary/15' : ''
} h-[78px] min-w-0 flex-col gap-1.5 whitespace-normal px-2 text-foreground`} } !h-[78px] min-w-0 flex-col gap-1.5 whitespace-normal px-2 text-foreground`}
disabled={!exportAll && exportContactCount > 1 && value !== 'html'} disabled={!exportAll && exportContactCount > 1 && value !== 'html'}
onClick={() => onFormatChange(value)} onClick={() => onFormatChange(value)}
> >
@@ -1,6 +1,8 @@
import React from 'react' import React, { useRef } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
import type { ExportContactType } from '../../../../shared/export' import type { ExportContactType } from '../../../../shared/export'
import { Button, Checkbox, Input, Tabs, TabsList, TabsTrigger } from '../ui' import { Button, Checkbox, Input, Tabs, TabsList, TabsTrigger } from '../ui'
import { SearchIcon } from '../chat/icons'
import type { Contact, SelfInfo } from './exportTypes' import type { Contact, SelfInfo } from './exportTypes'
import { displayName } from './exportUtils' import { displayName } from './exportUtils'
@@ -27,6 +29,71 @@ interface ExportContactPanelProps {
onOpenSettings: () => void onOpenSettings: () => void
} }
interface ExportContactRowProps {
contact: Contact
active: boolean
pressed: boolean
selected: boolean
showSelection: boolean
disabled: boolean
style?: React.CSSProperties
onSelect: (contact: Contact) => void
}
function ExportContactRow({
contact,
active,
pressed,
selected,
showSelection,
disabled,
style,
onSelect
}: ExportContactRowProps): React.ReactElement {
const name = displayName(contact)
return (
<button
type="button"
className={`${style ? 'absolute left-0 top-0 ' : ''}flex w-full items-center gap-2.5 border-0 border-l-[3px] px-4 py-[11px] text-left text-foreground transition-colors hover:bg-surface/60 disabled:cursor-not-allowed disabled:opacity-50 ${
active ? 'border-l-primary bg-primary/10' : 'border-l-transparent bg-transparent'
}`}
style={style}
onClick={() => onSelect(contact)}
disabled={disabled}
aria-pressed={pressed}
>
<span className="grid h-[38px] w-[38px] shrink-0 place-items-center overflow-hidden rounded-lg bg-primary/10 font-bold text-primary">
{contact.avatar ? (
<img className="h-full w-full object-cover" src={contact.avatar} alt="" />
) : (
name.slice(0, 1)
)}
</span>
<span className="grid min-w-0 flex-1 gap-0.5">
<strong className="overflow-hidden text-ellipsis whitespace-nowrap text-[13px]">
{name}
</strong>
<small className="text-[11px] text-muted-foreground">
{contact.type === 'group' ? '群聊' : '联系人'}
</small>
</span>
{showSelection && (
<span
className={`grid h-[18px] w-[18px] shrink-0 place-items-center rounded border text-xs text-primary-foreground ${
selected ? 'border-primary bg-primary' : 'border-border-strong'
}`}
aria-hidden
>
{selected && (
<span className="h-2 w-1 -translate-y-px rotate-45 border-b-2 border-r-2 border-current" />
)}
</span>
)}
</button>
)
}
export function ExportContactPanel({ export function ExportContactPanel({
contacts, contacts,
filteredContacts, filteredContacts,
@@ -49,6 +116,17 @@ export function ExportContactPanel({
onToggleAllContactType, onToggleAllContactType,
onOpenSettings onOpenSettings
}: ExportContactPanelProps): React.ReactElement { }: ExportContactPanelProps): React.ReactElement {
const virtualizeContacts = filteredContacts.length >= 100
const contactListRef = useRef<HTMLDivElement>(null)
// TanStack Virtual owns mutable measurements, so React Compiler must skip this hook.
// eslint-disable-next-line react-hooks/incompatible-library
const contactVirtualizer = useVirtualizer({
count: virtualizeContacts ? filteredContacts.length : 0,
getScrollElement: () => contactListRef.current,
estimateSize: () => 60,
getItemKey: (index) => filteredContacts[index]?.md5 || index,
overscan: 10
})
const groupCount = contacts.filter((contact) => contact.type === 'group').length const groupCount = contacts.filter((contact) => contact.type === 'group').length
const userCount = contacts.length - groupCount const userCount = contacts.length - groupCount
const selectedAllCount = const selectedAllCount =
@@ -69,7 +147,7 @@ export function ExportContactPanel({
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
aria-hidden aria-hidden
> >
<SearchIcon className="h-3.5 w-3.5 fill-none stroke-current stroke-2" />
</span> </span>
<Input <Input
className="h-[38px] pl-9 text-[13px]" className="h-[38px] pl-9 text-[13px]"
@@ -104,7 +182,7 @@ export function ExportContactPanel({
</Tabs> </Tabs>
<Button <Button
variant="outline" variant="outline"
className={`mt-2.5 h-auto w-full justify-between gap-3 px-2.5 py-2 text-left ${ className={`mt-2.5 !h-auto w-full justify-between gap-3 px-2.5 py-2 text-left ${
exportAll ? 'border-primary bg-primary/10 text-primary hover:bg-primary/15' : '' exportAll ? 'border-primary bg-primary/10 text-primary hover:bg-primary/15' : ''
}`} }`}
aria-pressed={exportAll} aria-pressed={exportAll}
@@ -170,61 +248,71 @@ export function ExportContactPanel({
</div> </div>
) : null} ) : null}
<div className="export-contact-list min-h-0 flex-1 overflow-auto py-2"> <div ref={contactListRef} className="export-contact-list min-h-0 flex-1 overflow-auto py-2">
{filteredContacts.map((contact) => { {virtualizeContacts ? (
const name = displayName(contact) <div
className="relative w-full"
style={{ height: `${contactVirtualizer.getTotalSize()}px` }}
>
{contactVirtualizer.getVirtualItems().map((virtualItem) => {
const contact = filteredContacts[virtualItem.index]
if (!contact) return null
const selected = selectedContactIds.includes(contact.md5) const selected = selectedContactIds.includes(contact.md5)
const selectedByAll = exportAll && allContactTypes.includes(contact.type) const selectedByAll = exportAll && allContactTypes.includes(contact.type)
const visuallySelected = exportAll ? selectedByAll : selected const visuallySelected = exportAll ? selectedByAll : selected
const atLimit = const atLimit =
!exportAll && selectionMode && !selected && selectedContactIds.length >= selectionLimit !exportAll &&
selectionMode &&
!selected &&
selectedContactIds.length >= selectionLimit
return ( return (
<button <ExportContactRow
key={contact.md5} key={virtualItem.key}
type="button" contact={contact}
className={`flex w-full items-center gap-2.5 border-0 border-l-[3px] px-4 py-[11px] text-left text-foreground transition-colors hover:bg-surface/60 disabled:cursor-not-allowed disabled:opacity-50 ${ active={!exportAll && activeContact?.md5 === contact.md5}
!exportAll && activeContact?.md5 === contact.md5 pressed={visuallySelected}
? 'border-l-primary bg-primary/10' selected={selected}
: 'border-l-transparent bg-transparent' showSelection={!exportAll && selectionMode}
}`}
onClick={() => onSelectContact(contact)}
disabled={atLimit} disabled={atLimit}
aria-pressed={visuallySelected} style={{
> height: `${virtualItem.size}px`,
<span className="grid h-[38px] w-[38px] shrink-0 place-items-center overflow-hidden rounded-lg bg-primary/10 font-bold text-primary"> transform: `translateY(${virtualItem.start}px)`
{contact.avatar ? ( }}
<img className="h-full w-full object-cover" src={contact.avatar} alt="" /> onSelect={onSelectContact}
) : ( />
name.slice(0, 1)
)}
</span>
<span className="grid min-w-0 flex-1 gap-0.5">
<strong className="overflow-hidden text-ellipsis whitespace-nowrap text-[13px]">
{name}
</strong>
<small className="text-[11px] text-muted-foreground">
{contact.type === 'group' ? '群聊' : '联系人'}
</small>
</span>
{!exportAll && selectionMode && (
<span
className={`grid h-[18px] w-[18px] shrink-0 place-items-center rounded border text-xs text-primary-foreground ${
selected ? 'border-primary bg-primary' : 'border-border-strong'
}`}
aria-hidden
>
{selected ? '✓' : ''}
</span>
)}
</button>
) )
})} })}
</div> </div>
) : (
filteredContacts.map((contact) => {
const selected = selectedContactIds.includes(contact.md5)
const selectedByAll = exportAll && allContactTypes.includes(contact.type)
const visuallySelected = exportAll ? selectedByAll : selected
const atLimit =
!exportAll &&
selectionMode &&
!selected &&
selectedContactIds.length >= selectionLimit
return (
<ExportContactRow
key={contact.md5}
contact={contact}
active={!exportAll && activeContact?.md5 === contact.md5}
pressed={visuallySelected}
selected={selected}
showSelection={!exportAll && selectionMode}
disabled={atLimit}
onSelect={onSelectContact}
/>
)
})
)}
</div>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
className="h-auto justify-start gap-2 border-t border-border px-4 py-3.5 text-left" className="!h-auto justify-start gap-2 border-t border-border px-4 py-3.5 text-left"
onClick={onOpenSettings} onClick={onOpenSettings}
> >
<span className="grid h-[34px] w-[34px] shrink-0 place-items-center overflow-hidden rounded-full bg-primary/10 font-bold text-primary"> <span className="grid h-[34px] w-[34px] shrink-0 place-items-center overflow-hidden rounded-full bg-primary/10 font-bold text-primary">
@@ -141,7 +141,7 @@ export function ExportPreviewPanel({
systemMessage systemMessage
? 'max-w-[92%] bg-muted px-2.5 py-1 text-center text-[11px] text-muted-foreground shadow-none' ? 'max-w-[92%] bg-muted px-2.5 py-1 text-center text-[11px] text-muted-foreground shadow-none'
: message.isSender : message.isSender
? 'bg-[#95ec69]' ? 'bg-accent'
: 'bg-surface' : 'bg-surface'
}`} }`}
> >
+15 -11
View File
@@ -3,23 +3,27 @@ import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '../../lib/cn' import { cn } from '../../lib/cn'
const buttonVariants = cva( const buttonVariants = cva(
'inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md border-0 bg-transparent text-sm font-medium transition-colors duration-fast ease-tm-standard focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', 'inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border-0 bg-transparent text-sm font-medium transition-[color,background-color,border-color,transform] duration-fast ease-tm-standard active:translate-y-px disabled:pointer-events-none disabled:translate-y-0 disabled:!text-disabled-foreground disabled:opacity-100',
{ {
variants: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground shadow-surface hover:bg-primary/90', default:
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/75', 'bg-primary text-primary-foreground hover:bg-primary-hover disabled:bg-disabled-surface disabled:text-disabled-foreground',
ghost: 'text-foreground hover:bg-accent hover:text-accent-foreground', secondary:
outline: 'border border-border bg-surface text-foreground shadow-surface hover:bg-accent', 'bg-secondary text-secondary-foreground hover:bg-secondary/75 active:bg-secondary/60 disabled:bg-disabled-surface disabled:text-disabled-foreground',
ghost:
'text-foreground hover:bg-accent hover:text-accent-foreground active:bg-accent/75 disabled:text-disabled-foreground',
outline:
'border border-border bg-surface text-foreground hover:border-primary/25 hover:bg-accent active:bg-accent/75 disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground',
destructive: destructive:
'bg-destructive text-destructive-foreground shadow-surface hover:bg-destructive/90', 'bg-destructive text-destructive-foreground hover:bg-destructive/90 active:bg-destructive/80 disabled:bg-disabled-surface disabled:text-disabled-foreground',
link: 'text-primary underline-offset-4 hover:underline' link: '!h-auto !p-0 text-primary underline-offset-4 hover:text-primary-hover hover:underline disabled:text-disabled-foreground'
}, },
size: { size: {
sm: 'h-8 rounded-md px-3 text-xs', sm: 'h-control-compact rounded-md px-2.5 text-xs',
default: 'h-9 px-4 py-2', default: 'h-control-standard px-3.5',
lg: 'h-10 rounded-lg px-6', lg: 'h-control-form rounded-lg px-5',
icon: 'h-9 w-9' icon: 'h-control-standard w-8 p-0'
} }
}, },
defaultVariants: { defaultVariants: {
+3 -3
View File
@@ -9,13 +9,13 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-border bg-surface shadow-surface transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground', 'peer h-4 w-4 shrink-0 rounded-sm border border-muted-foreground/60 bg-surface transition-colors duration-fast ease-tm-standard hover:border-primary/70 disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground disabled:data-[state=checked]:border-disabled-border disabled:data-[state=checked]:bg-primary/40',
className className
)} )}
{...props} {...props}
> >
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-[11px]"> <CheckboxPrimitive.Indicator className="flex items-center justify-center">
<span className="h-2 w-1 -translate-y-px rotate-45 border-b-2 border-r-2 border-current" />
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>
)) ))
+1 -1
View File
@@ -10,7 +10,7 @@ export const Input = React.forwardRef<
ref={ref} ref={ref}
type={type} type={type}
className={cn( className={cn(
'flex h-9 w-full rounded-md border border-border bg-surface px-3 py-1 text-sm text-foreground shadow-surface transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', 'flex h-control-form w-full rounded-md border border-border bg-surface px-3 py-1 text-sm text-foreground transition-colors duration-fast ease-tm-standard placeholder:text-muted-foreground hover:border-primary/25 disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 disabled:placeholder:text-disabled-foreground',
className className
)} )}
{...props} {...props}
@@ -17,7 +17,7 @@ const RadioGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item <RadioGroupPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
'aspect-square h-4 w-4 rounded-full border border-border bg-surface text-primary shadow-surface focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', 'aspect-square h-4 w-4 rounded-full border border-muted-foreground/60 bg-surface text-primary transition-colors duration-fast ease-tm-standard hover:border-primary/70 disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 data-[state=checked]:border-primary disabled:data-[state=checked]:border-disabled-border',
className className
)} )}
{...props} {...props}
+9 -6
View File
@@ -13,14 +13,17 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
'flex h-9 w-full items-center justify-between rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground shadow-surface focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', 'flex h-control-form w-full items-center justify-between rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground transition-colors duration-fast ease-tm-standard hover:border-primary/25 disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 [&>span]:line-clamp-1',
className className
)} )}
{...props} {...props}
> >
{children} {children}
<span aria-hidden="true" className="ml-2 text-muted-foreground"> <span
aria-hidden="true"
className="ml-2 flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground"
>
<span className="h-1.5 w-1.5 -translate-y-px rotate-45 border-b border-r border-current" />
</span> </span>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
)) ))
@@ -66,13 +69,13 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', 'relative flex min-h-8 w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors duration-fast ease-tm-standard focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:text-disabled-foreground data-[disabled]:opacity-100',
className className
)} )}
{...props} {...props}
> >
<SelectPrimitive.ItemIndicator className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <SelectPrimitive.ItemIndicator className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center text-primary">
<span className="h-2 w-1 rotate-45 border-b-2 border-r-2 border-current" />
</SelectPrimitive.ItemIndicator> </SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
+2 -2
View File
@@ -9,12 +9,12 @@ const Switch = React.forwardRef<
<SwitchPrimitive.Root <SwitchPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary', 'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-muted transition-colors duration-fast ease-tm-standard hover:bg-muted/80 disabled:cursor-not-allowed disabled:bg-disabled-surface disabled:opacity-100 data-[state=checked]:bg-primary disabled:data-[state=checked]:bg-primary/40',
className className
)} )}
{...props} {...props}
> >
<SwitchPrimitive.Thumb className="pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0" /> <SwitchPrimitive.Thumb className="pointer-events-none block h-4 w-4 rounded-full bg-white shadow-surface ring-0 transition-transform duration-fast ease-tm-standard data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0" />
</SwitchPrimitive.Root> </SwitchPrimitive.Root>
)) ))
Switch.displayName = SwitchPrimitive.Root.displayName Switch.displayName = SwitchPrimitive.Root.displayName
+17 -16
View File
@@ -1,20 +1,21 @@
:root { :root {
--wxex-bg-app: #f4f6f5; // Original TraceMemo brand tokens remain available while the UI Layer consumes --tm-*.
--wxex-bg-main: #fafbfa; --wxex-bg-app: hsl(var(--tm-background));
--wxex-bg-sidebar: #eef2f0; --wxex-bg-main: hsl(var(--tm-canvas));
--wxex-bg-elevated: #ffffff; --wxex-bg-sidebar: hsl(var(--tm-surface-muted));
--wxex-text-primary: #202724; --wxex-bg-elevated: hsl(var(--tm-surface-elevated));
--wxex-text-secondary: #66706b; --wxex-text-primary: hsl(var(--tm-foreground));
--wxex-text-muted: #929a96; --wxex-text-secondary: hsl(var(--tm-muted-foreground));
--wxex-border: #dde3e0; --wxex-text-muted: hsl(var(--tm-subtle-foreground));
--wxex-brand: #247a63; --wxex-border: hsl(var(--tm-border));
--wxex-brand-hover: #1d6754; --wxex-brand: hsl(var(--tm-primary));
--wxex-brand-soft: #ddede7; --wxex-brand-hover: hsl(var(--tm-primary-hover));
--wxex-ai: #686edc; --wxex-brand-soft: hsl(var(--tm-accent));
--wxex-ai-soft: #eeeffd; --wxex-ai: hsl(var(--tm-ai));
--wxex-success: #2e8b68; --wxex-ai-soft: hsl(var(--tm-ai-soft));
--wxex-warning: #c68635; --wxex-success: hsl(var(--tm-success));
--wxex-danger: #c85a5a; --wxex-warning: hsl(var(--tm-warning));
--wxex-danger: hsl(var(--tm-destructive));
--wxex-font: --wxex-font:
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
--wxex-nav-width: 76px; --wxex-nav-width: 76px;
+10 -26
View File
@@ -86,7 +86,6 @@
} }
.api-section-heading h2, .api-section-heading h2,
.api-introduction h2, .api-introduction h2,
.api-integrations h2,
.api-runtime-title h2 { .api-runtime-title h2 {
margin: 0; margin: 0;
color: var(--wxex-text-primary); color: var(--wxex-text-primary);
@@ -150,7 +149,7 @@
} }
.api-skill-status.error { .api-skill-status.error {
color: var(--wxex-danger); color: var(--wxex-danger);
background: #fff1f1; background: color-mix(in srgb, var(--wxex-danger) 10%, var(--wxex-bg-elevated));
} }
.api-version { .api-version {
color: var(--wxex-text-muted); color: var(--wxex-text-muted);
@@ -166,21 +165,6 @@
.api-header-actions { .api-header-actions {
flex: 0 0 auto; flex: 0 0 auto;
} }
.api-primary-button {
border-color: var(--wxex-brand) !important;
background: var(--wxex-brand) !important;
color: #fff !important;
}
.api-primary-button:hover:not(:disabled),
.api-primary-button:focus-visible,
.api-primary-button:active {
background: var(--wxex-brand-hover) !important;
color: #fff !important;
}
.api-primary-button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.api-trust-bar { .api-trust-bar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -190,7 +174,7 @@
padding: 10px 12px; padding: 10px 12px;
border: 1px solid var(--wxex-border); border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md); border-radius: var(--wxex-radius-md);
background: #f5f8f6; background: var(--wxex-bg-sidebar);
color: var(--wxex-text-secondary); color: var(--wxex-text-secondary);
font: 12px/18px var(--wxex-font); font: 12px/18px var(--wxex-font);
} }
@@ -228,7 +212,7 @@
padding: 7px 10px; padding: 7px 10px;
border: 1px solid var(--wxex-border); border: 1px solid var(--wxex-border);
border-radius: 5px; border-radius: 5px;
background: #fff; background: var(--wxex-bg-elevated);
white-space: nowrap; white-space: nowrap;
} }
.api-flow strong { .api-flow strong {
@@ -360,7 +344,7 @@
padding: 11px 14px; padding: 11px 14px;
} }
.api-endpoint-head { .api-endpoint-head {
background: #f1f4f2; background: var(--wxex-bg-sidebar);
color: var(--wxex-text-secondary); color: var(--wxex-text-secondary);
font: 700 12px/18px var(--wxex-font); font: 700 12px/18px var(--wxex-font);
} }
@@ -371,7 +355,7 @@
font: 12px/18px var(--wxex-font); font: 12px/18px var(--wxex-font);
} }
.api-endpoint-row.active { .api-endpoint-row.active {
background: #f7faf8; background: var(--wxex-brand-soft);
} }
.api-endpoint-row code { .api-endpoint-row code {
color: var(--wxex-text-primary); color: var(--wxex-text-primary);
@@ -405,7 +389,7 @@
padding: 10px 12px; padding: 10px 12px;
border: 1px solid var(--wxex-border); border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md); border-radius: var(--wxex-radius-md);
background: #f5f8f6; background: var(--wxex-bg-sidebar);
overflow: hidden; overflow: hidden;
} }
.api-request-meta code { .api-request-meta code {
@@ -446,7 +430,7 @@
padding: 8px 10px; padding: 8px 10px;
border: 1px solid rgba(200, 90, 90, 0.35); border: 1px solid rgba(200, 90, 90, 0.35);
border-radius: var(--wxex-radius-sm); border-radius: var(--wxex-radius-sm);
background: #fff4f4; background: color-mix(in srgb, var(--wxex-danger) 10%, var(--wxex-bg-elevated));
color: var(--wxex-danger); color: var(--wxex-danger);
font: 12px/18px var(--wxex-font); font: 12px/18px var(--wxex-font);
} }
@@ -512,8 +496,8 @@
margin: 12px 16px; margin: 12px 16px;
padding: 9px 10px; padding: 9px 10px;
border-left: 3px solid var(--wxex-warning); border-left: 3px solid var(--wxex-warning);
background: #fff8ec; background: color-mix(in srgb, var(--wxex-warning) 10%, var(--wxex-bg-elevated));
color: #915d1e; color: var(--wxex-warning);
font: 12px/18px var(--wxex-font); font: 12px/18px var(--wxex-font);
} }
.api-token-value { .api-token-value {
@@ -595,7 +579,7 @@
font: 11px/16px var(--wxex-font); font: 11px/16px var(--wxex-font);
} }
.api-privacy { .api-privacy {
background: #f5f8f6; background: var(--wxex-bg-sidebar);
} }
.api-privacy p { .api-privacy p {
margin: 0; margin: 0;
+11 -241
View File
@@ -162,7 +162,7 @@
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
gap: 3px 10px; gap: 3px 10px;
border-radius: var(--wxex-radius-md); border-radius: var(--wxex-radius-md);
background: #f3f6f4; background: var(--wxex-bg-sidebar);
padding: 12px 14px; padding: 12px 14px;
span { span {
@@ -201,8 +201,8 @@
} }
&.blocked { &.blocked {
border-color: rgba(190, 96, 60, 0.24); border-color: color-mix(in srgb, var(--wxex-danger) 28%, transparent);
background: rgba(190, 96, 60, 0.06); background: color-mix(in srgb, var(--wxex-danger) 7%, var(--wxex-bg-elevated));
} }
strong, strong,
@@ -230,7 +230,7 @@
} }
small.error { small.error {
color: #b15338; color: var(--wxex-danger);
word-break: break-all; word-break: break-all;
} }
} }
@@ -287,11 +287,11 @@
.ready & { .ready & {
background: var(--wxex-success); background: var(--wxex-success);
box-shadow: 0 0 0 3px rgba(36, 122, 99, 0.12); box-shadow: 0 0 0 3px color-mix(in srgb, var(--wxex-success) 14%, transparent);
} }
.blocked & { .blocked & {
background: #be603c; background: var(--wxex-danger);
} }
} }
@@ -406,11 +406,11 @@
display: grid; display: grid;
gap: 3px; gap: 3px;
border-radius: var(--wxex-radius-md); border-radius: var(--wxex-radius-md);
background: rgba(190, 96, 60, 0.08); background: color-mix(in srgb, var(--wxex-warning) 10%, var(--wxex-bg-elevated));
padding: 10px 12px; padding: 10px 12px;
&.ready { &.ready {
background: rgba(36, 122, 99, 0.08); background: color-mix(in srgb, var(--wxex-success) 9%, var(--wxex-bg-elevated));
} }
strong { strong {
@@ -559,8 +559,8 @@
} }
&.error { &.error {
background: rgba(190, 96, 60, 0.1); background: color-mix(in srgb, var(--wxex-danger) 10%, var(--wxex-bg-elevated));
color: #a84e32; color: var(--wxex-danger);
} }
} }
@@ -621,7 +621,7 @@
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 16px;
border-bottom: 1px solid var(--wxex-border); border-bottom: 1px solid var(--wxex-border);
background: #f3f6f4; background: var(--wxex-bg-sidebar);
color: var(--wxex-text-secondary); color: var(--wxex-text-secondary);
padding: 0 18px; padding: 0 18px;
font-size: 12px; font-size: 12px;
@@ -769,11 +769,6 @@
word-break: break-word; word-break: break-word;
} }
.wechat-system-message-meta {
color: #a0a7ab;
font-size: 11px;
}
.message-avatar { .message-avatar {
width: 38px; width: 38px;
height: 38px; height: 38px;
@@ -845,10 +840,6 @@
} }
} }
.message-meta {
display: none;
}
.message-hover-time, .message-hover-time,
.message-accessible-sender { .message-accessible-sender {
color: var(--wxex-text-muted); color: var(--wxex-text-muted);
@@ -1202,90 +1193,6 @@
word-break: break-word; word-break: break-word;
} }
.chat-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
th {
text-align: left;
padding: 8px;
border-bottom: 1px solid var(--border-color);
background-color: #f9f9f9;
color: #666;
font-weight: normal;
position: sticky;
top: 0;
z-index: 1;
position: relative;
}
td {
padding: 8px;
border-bottom: 1px solid #eee;
vertical-align: top;
color: #333;
}
tr:hover {
background-color: #f0f0f0;
}
}
.column-resizer {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 5px;
cursor: col-resize;
z-index: 2;
}
.column-resizer:hover {
background-color: #ccc;
}
.col-sender {
width: 80px;
}
.col-type {
width: 80px;
}
.col-time {
width: 150px;
}
.col-content {
}
.chat-toolbar {
padding: 10px;
border-top: 1px solid var(--border-color);
display: flex;
gap: 10px;
background-color: #fff;
flex-wrap: wrap;
.toolbar-btn {
padding: 4px 12px;
border: 1px solid #dcdcdc;
border-radius: 4px;
background-color: #fff;
font-size: 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
&:hover {
background-color: #f5f5f5;
}
}
}
.empty-state { .empty-state {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1970,143 +1877,6 @@
} }
} }
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
min-width: 400px;
max-width: 90vw;
max-height: 90vh;
overflow: auto;
}
.ai-settings-modal {
width: min(460px, 90vw);
}
.ai-settings-modal h3 {
margin: 0 0 18px;
}
.ai-filter-section {
margin-bottom: 16px;
}
.ai-filter-label {
margin-bottom: 7px;
color: #333;
font-size: 14px;
font-weight: 600;
}
.ai-date-options {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
}
.ai-date-options label {
display: flex;
min-width: 0;
align-items: center;
justify-content: center;
gap: 5px;
padding: 8px 6px;
border: 1px solid #d8dcdf;
border-radius: 6px;
background: #fff;
color: #4a5257;
cursor: pointer;
font-size: 13px;
}
.ai-date-options label.selected {
border-color: #07c160;
background: #eefaf3;
color: #078f49;
}
.ai-date-options input,
.ai-type-options input {
margin: 0;
accent-color: #07c160;
}
.ai-type-options {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px 10px;
padding: 11px;
border: 1px solid #e1e4e6;
border-radius: 6px;
background: #f8f9fa;
}
.ai-type-options label {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
color: #3f474c;
cursor: pointer;
font-size: 13px;
white-space: nowrap;
}
.image-preview-modal {
width: min(520px, 92vw);
max-height: 88vh;
background: #f7f8fa;
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.18);
padding: 16px;
overflow: hidden;
}
.report-preview-frame {
border: 1px solid #d8dde3;
border-radius: 10px;
background: #eef1f4;
padding: 12px;
}
.report-preview-scroller {
max-height: calc(88vh - 120px);
overflow-y: auto;
overflow-x: hidden;
padding-right: 4px;
}
.report-preview-image {
display: block;
width: min(430px, 100%);
height: auto;
margin: 0 auto;
border-radius: 8px;
border: 1px solid #ccd3d9;
background: #fff;
}
.report-preview-actions {
margin-top: 12px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.database-account-list { .database-account-list {
display: grid; display: grid;
gap: 10px; gap: 10px;
-201
View File
@@ -109,39 +109,6 @@
stroke-linejoin: round; stroke-linejoin: round;
} }
.conversation-date-range {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 6px;
margin-top: 10px;
-webkit-app-region: no-drag;
}
.conversation-date-range-button {
box-sizing: border-box;
height: 30px;
min-width: 0;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: var(--wxex-bg-elevated);
color: var(--wxex-text-secondary);
cursor: pointer;
font: 12px/16px var(--wxex-font);
white-space: nowrap;
&:hover {
border-color: rgba(36, 122, 99, 0.28);
color: var(--wxex-text-primary);
}
&.active {
border-color: var(--wxex-brand);
background: var(--wxex-brand);
color: #ffffff;
font-weight: 600;
}
}
.conversation-list { .conversation-list {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -320,15 +287,6 @@
white-space: nowrap; white-space: nowrap;
} }
.conversation-item-meta {
overflow: hidden;
color: var(--wxex-text-muted);
font-size: 12px;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.conversation-sidebar-account { .conversation-sidebar-account {
box-sizing: border-box; box-sizing: border-box;
flex: 0 0 auto; flex: 0 0 auto;
@@ -345,153 +303,6 @@
flex: 1; flex: 1;
} }
.sidebar {
background-color: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
flex-shrink: 0;
/* Prevent shrinking */
}
.sidebar-header {
flex: 0 0 auto;
padding: 10px;
border-bottom: 1px solid var(--border-color);
-webkit-app-region: drag;
}
.sidebar-footer {
padding: 10px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
}
.sidebar-btn {
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
color: #666;
&:hover {
color: #333;
}
}
.sidebar-status {
color: #07c160;
font-size: 12px;
}
.search-input {
width: 100%;
padding: 5px;
border-radius: 4px;
border: 1px solid #ccc;
-webkit-app-region: no-drag;
}
.date-range-selector {
display: flex;
gap: 5px;
margin-top: 8px;
justify-content: space-between;
}
.range-btn {
padding: 2px 6px;
font-size: 10px;
border: 1px solid #ccc;
border-radius: 10px;
background: #fff;
cursor: pointer;
color: #666;
flex: 1;
text-align: center;
&.active {
background: #07c160;
color: #fff;
border-color: #07c160;
}
}
.contact-list {
flex: 1;
overflow-y: auto;
}
.section-header {
padding: 8px 10px;
background-color: #f0f0f0;
font-size: 12px;
color: #666;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
user-select: none;
&:hover {
background-color: #e0e0e0;
}
.arrow {
margin-right: 5px;
font-size: 10px;
width: 12px;
}
}
.section-empty {
padding: 10px 14px 12px 27px;
color: #888;
font-size: 12px;
}
.contact-item {
padding: 10px;
display: flex;
align-items: center;
cursor: pointer;
&:hover {
background-color: var(--sidebar-hover);
}
&.active {
background-color: var(--sidebar-active);
}
}
.contact-avatar {
width: 40px;
height: 40px;
border-radius: 4px;
background-color: #ccc;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #fff;
overflow: hidden;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.message-avatar { .message-avatar {
img { img {
width: 100%; width: 100%;
@@ -501,18 +312,6 @@
} }
} }
.contact-info {
flex: 1;
overflow: hidden;
}
.contact-name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.resizer { .resizer {
width: 4px; width: 4px;
cursor: col-resize; cursor: col-resize;
-34
View File
@@ -361,40 +361,6 @@ body {
background: var(--wxex-bg-main); background: var(--wxex-bg-main);
} }
.app-page-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
color: var(--wxex-text-secondary);
text-align: center;
h2 {
margin: 0 0 8px;
color: var(--wxex-text-primary);
font-size: 20px;
font-weight: 600;
line-height: 28px;
}
p {
margin: 0;
font-size: 13px;
line-height: 20px;
}
}
.app-page-placeholder-eyebrow {
margin-bottom: 8px;
color: var(--wxex-brand);
font-size: 11px;
font-weight: 700;
letter-spacing: 0;
}
.app-container { .app-container {
display: flex; display: flex;
width: 100%; width: 100%;
+116 -89
View File
@@ -226,7 +226,7 @@
padding: 8px 10px; padding: 8px 10px;
border: 1px solid rgba(200, 90, 90, 0.35); border: 1px solid rgba(200, 90, 90, 0.35);
border-radius: var(--wxex-radius-md); border-radius: var(--wxex-radius-md);
background: #fff4f4; background: color-mix(in srgb, var(--wxex-danger) 10%, var(--wxex-bg-elevated));
color: var(--wxex-danger); color: var(--wxex-danger);
font: 12px/17px var(--wxex-font); font: 12px/17px var(--wxex-font);
} }
@@ -248,7 +248,6 @@
} }
.report-empty-state, .report-empty-state,
.report-config-section,
.report-privacy-note, .report-privacy-note,
.report-result-panel { .report-result-panel {
margin-bottom: 16px; margin-bottom: 16px;
@@ -258,6 +257,11 @@
background: var(--wxex-bg-elevated); background: var(--wxex-bg-elevated);
} }
.report-config-section {
margin-bottom: 22px;
padding: 0;
}
.report-empty-state h2, .report-empty-state h2,
.report-privacy-note h3, .report-privacy-note h3,
.report-config-section h3, .report-config-section h3,
@@ -317,7 +321,11 @@
.report-density-options { .report-density-options {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px; gap: 2px;
padding: 3px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: var(--wxex-bg-sidebar);
} }
.report-density-options { .report-density-options {
@@ -334,7 +342,11 @@
.report-type-grid { .report-type-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px; gap: 1px;
overflow: hidden;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: var(--wxex-border);
} }
.report-check-row { .report-check-row {
@@ -342,11 +354,17 @@
grid-template-columns: auto minmax(0, 1fr) auto; grid-template-columns: auto minmax(0, 1fr) auto;
align-items: start; align-items: start;
gap: 8px; gap: 8px;
padding: 10px; min-width: 0;
border: 1px solid var(--wxex-border); padding: 11px 12px;
border-radius: var(--wxex-radius-md); border: 0;
background: #fbfcfb; border-radius: 0;
background: var(--wxex-bg-elevated);
color: var(--wxex-text-primary); color: var(--wxex-text-primary);
transition: background-color var(--tm-motion-fast) var(--tm-ease-standard);
}
.report-check-row:hover {
background: var(--wxex-bg-sidebar);
} }
.report-check-row span { .report-check-row span {
@@ -372,7 +390,11 @@
.report-readonly-modules { .report-readonly-modules {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px; gap: 1px;
overflow: hidden;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: var(--wxex-border);
cursor: default; cursor: default;
} }
@@ -381,10 +403,10 @@
min-width: 0; min-width: 0;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 8px 10px; padding: 9px 10px;
border: 1px solid #e9eeeb; border: 0;
border-radius: var(--wxex-radius-md); border-radius: 0;
background: #f7f9f8; background: var(--wxex-bg-elevated);
color: var(--wxex-text-secondary); color: var(--wxex-text-secondary);
font: 12px/17px var(--wxex-font); font: 12px/17px var(--wxex-font);
cursor: default; cursor: default;
@@ -477,11 +499,11 @@
.ai-report-footer { .ai-report-footer {
display: flex; display: flex;
flex: 0 0 auto; flex: 0 0 auto;
min-height: 66px; min-height: 64px;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 16px;
padding: 12px 28px; padding: 10px 28px;
border-top: 1px solid var(--wxex-border); border-top: 1px solid var(--wxex-border);
background: var(--wxex-bg-main); background: var(--wxex-bg-main);
} }
@@ -494,7 +516,10 @@
} }
.report-footer-note { .report-footer-note {
flex: 1 1 auto;
overflow: hidden; overflow: hidden;
color: var(--wxex-text-muted);
font-size: 11px;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
@@ -504,12 +529,14 @@
min-width: 0; min-width: 0;
flex: 0 0 auto; flex: 0 0 auto;
align-items: center; align-items: center;
gap: 12px; gap: 10px;
} }
.report-footer-actions span { .report-footer-actions span {
max-width: 280px; max-width: 240px;
overflow: hidden; overflow: hidden;
color: var(--wxex-text-muted);
font-size: 11px;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
@@ -522,7 +549,7 @@
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
border-left: 1px solid var(--wxex-border); border-left: 1px solid var(--wxex-border);
background: #f7f9f8; background: var(--wxex-bg-sidebar);
} }
.report-task-header { .report-task-header {
@@ -657,7 +684,7 @@
} }
.report-image-insight-list article.failed { .report-image-insight-list article.failed {
background: #fff7f3; background: color-mix(in srgb, var(--wxex-danger) 8%, var(--wxex-bg-main));
} }
.report-image-insight-list article > div { .report-image-insight-list article > div {
@@ -687,7 +714,7 @@
.report-image-decision { .report-image-decision {
border-color: rgba(197, 137, 48, 0.4); border-color: rgba(197, 137, 48, 0.4);
background: #fffaf0; background: color-mix(in srgb, var(--wxex-warning) 10%, var(--wxex-bg-main));
} }
.report-image-decision p { .report-image-decision p {
@@ -796,7 +823,7 @@
.report-task-error { .report-task-error {
border: 1px solid rgba(200, 90, 90, 0.35); border: 1px solid rgba(200, 90, 90, 0.35);
background: #fff4f4; background: color-mix(in srgb, var(--wxex-danger) 10%, var(--wxex-bg-sidebar));
color: var(--wxex-danger); color: var(--wxex-danger);
} }
@@ -846,16 +873,17 @@
height: 100%; height: 100%;
min-width: 0; min-width: 0;
overflow: auto; overflow: auto;
padding: 34px 40px 48px; padding: 26px 32px 36px;
background: #fafbfa; background: var(--wxex-bg-main);
color: var(--wxex-text-primary);
} }
.agent-hub-header { .agent-hub-header {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 24px; gap: 20px;
max-width: 1080px; max-width: 1080px;
margin: 0 auto 28px; margin: 0 auto 20px;
} }
.agent-hub-eyebrow, .agent-hub-eyebrow,
.agent-hub-card-kicker { .agent-hub-card-kicker {
@@ -867,7 +895,7 @@
.agent-hub-header h1 { .agent-hub-header h1 {
margin: 4px 0 6px; margin: 4px 0 6px;
color: var(--wxex-text-primary); color: var(--wxex-text-primary);
font: 750 28px/36px var(--wxex-font); font: 750 24px/32px var(--wxex-font);
} }
.agent-hub-header p, .agent-hub-header p,
.agent-hub-card p { .agent-hub-card p {
@@ -880,22 +908,22 @@
align-items: center; align-items: center;
padding: 6px 10px; padding: 6px 10px;
border-radius: 999px; border-radius: 999px;
background: #f0f2f0; background: var(--wxex-bg-sidebar);
color: var(--wxex-text-muted); color: var(--wxex-text-muted);
font: 700 12px/18px var(--wxex-font); font: 700 12px/18px var(--wxex-font);
} }
.agent-hub-runtime.online { .agent-hub-runtime.online {
background: #e8f6ed; background: color-mix(in srgb, var(--wxex-success) 12%, var(--wxex-bg-elevated));
color: #24864b; color: var(--wxex-success);
} }
.agent-hub-runtime.error { .agent-hub-runtime.error {
background: #fff0ef; background: color-mix(in srgb, var(--wxex-danger) 10%, var(--wxex-bg-elevated));
color: var(--wxex-danger); color: var(--wxex-danger);
} }
.agent-hub-grid { .agent-hub-grid {
display: grid; display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(280px, 0.75fr); grid-template-columns: minmax(0, 1.55fr) minmax(280px, 0.75fr);
gap: 20px; gap: 16px;
max-width: 1080px; max-width: 1080px;
margin: 0 auto; margin: 0 auto;
} }
@@ -903,18 +931,17 @@
box-sizing: border-box; box-sizing: border-box;
min-width: 0; min-width: 0;
border: 1px solid var(--wxex-border); border: 1px solid var(--wxex-border);
border-radius: 18px; border-radius: var(--wxex-radius-lg);
background: var(--wxex-bg-elevated); background: var(--wxex-bg-elevated);
box-shadow: 0 8px 28px rgba(28, 38, 31, 0.045);
} }
.agent-hub-login-card { .agent-hub-login-card {
min-height: 520px; min-height: 420px;
padding: 26px; padding: 22px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.agent-hub-capability-card { .agent-hub-capability-card {
padding: 26px; padding: 22px;
align-self: start; align-self: start;
} }
.agent-hub-card-heading { .agent-hub-card-heading {
@@ -922,7 +949,7 @@
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 16px;
padding-bottom: 20px; padding-bottom: 16px;
border-bottom: 1px solid var(--wxex-border); border-bottom: 1px solid var(--wxex-border);
} }
.agent-hub-card h2 { .agent-hub-card h2 {
@@ -936,7 +963,7 @@
gap: 7px; gap: 7px;
padding: 5px 9px; padding: 5px 9px;
border-radius: 999px; border-radius: 999px;
background: #f2f3f2; background: var(--wxex-bg-sidebar);
color: var(--wxex-text-secondary); color: var(--wxex-text-secondary);
font: 650 12px/18px var(--wxex-font); font: 650 12px/18px var(--wxex-font);
white-space: nowrap; white-space: nowrap;
@@ -945,28 +972,28 @@
width: 7px; width: 7px;
height: 7px; height: 7px;
border-radius: 50%; border-radius: 50%;
background: #9aa39d; background: var(--wxex-text-muted);
} }
.agent-hub-status.online { .agent-hub-status.online {
background: #e8f6ed; background: color-mix(in srgb, var(--wxex-success) 12%, var(--wxex-bg-elevated));
color: #24864b; color: var(--wxex-success);
} }
.agent-hub-status.online i { .agent-hub-status.online i {
background: #35a663; background: var(--wxex-success);
} }
.agent-hub-status.waiting_scan, .agent-hub-status.waiting_scan,
.agent-hub-status.scanned, .agent-hub-status.scanned,
.agent-hub-status.starting { .agent-hub-status.starting {
background: #fff5de; background: color-mix(in srgb, var(--wxex-warning) 12%, var(--wxex-bg-elevated));
color: #9b6a12; color: var(--wxex-warning);
} }
.agent-hub-status.waiting_scan i, .agent-hub-status.waiting_scan i,
.agent-hub-status.scanned i, .agent-hub-status.scanned i,
.agent-hub-status.starting i { .agent-hub-status.starting i {
background: #d89b2b; background: var(--wxex-warning);
} }
.agent-hub-status.error { .agent-hub-status.error {
background: #fff0ef; background: color-mix(in srgb, var(--wxex-danger) 10%, var(--wxex-bg-elevated));
color: var(--wxex-danger); color: var(--wxex-danger);
} }
.agent-hub-status.error i { .agent-hub-status.error i {
@@ -978,13 +1005,13 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 280px; min-height: 210px;
text-align: center; text-align: center;
} }
.agent-hub-empty-login h3, .agent-hub-empty-login h3,
.agent-hub-connected h3, .agent-hub-connected h3,
.agent-hub-qr-copy h3 { .agent-hub-qr-copy h3 {
margin: 18px 0 6px; margin: 14px 0 5px;
color: var(--wxex-text-primary); color: var(--wxex-text-primary);
font: 700 16px/24px var(--wxex-font); font: 700 16px/24px var(--wxex-font);
} }
@@ -992,26 +1019,26 @@
max-width: 360px; max-width: 360px;
} }
.agent-hub-phone { .agent-hub-phone {
width: 64px; width: 56px;
height: 88px; height: 76px;
position: relative; position: relative;
border: 2px solid #9eb3a5; border: 2px solid color-mix(in srgb, var(--wxex-brand) 45%, var(--wxex-border));
border-radius: 13px; border-radius: 11px;
background: linear-gradient(145deg, #f6fbf7, #eaf4ed); background: var(--wxex-brand-soft);
} }
.agent-hub-phone::before { .agent-hub-phone::before {
content: ''; content: '';
position: absolute; position: absolute;
top: 8px; top: 8px;
left: 22px; left: 19px;
width: 18px; width: 16px;
height: 3px; height: 3px;
border-radius: 3px; border-radius: 3px;
background: #b8c8bd; background: color-mix(in srgb, var(--wxex-brand) 30%, var(--wxex-bg-elevated));
} }
.agent-hub-phone span { .agent-hub-phone span {
position: absolute; position: absolute;
left: 23px; left: 19px;
bottom: 8px; bottom: 8px;
width: 16px; width: 16px;
height: 16px; height: 16px;
@@ -1023,7 +1050,7 @@
justify-content: center; justify-content: center;
gap: 10px; gap: 10px;
min-height: 38px; min-height: 38px;
padding-top: 18px; padding-top: 14px;
} }
.agent-hub-qr-panel { .agent-hub-qr-panel {
flex: 1; flex: 1;
@@ -1076,17 +1103,17 @@
display: grid; display: grid;
place-items: center; place-items: center;
border-radius: 50%; border-radius: 50%;
background: #e4f5ea; background: color-mix(in srgb, var(--wxex-success) 14%, var(--wxex-bg-elevated));
color: #23834a; color: var(--wxex-success);
font: 800 22px/1 var(--wxex-font); font: 800 22px/1 var(--wxex-font);
} }
.agent-hub-capability-card > p { .agent-hub-capability-card > p {
margin-top: 8px; margin-top: 8px;
} }
.agent-hub-example { .agent-hub-example {
margin: 24px 0 20px; margin: 18px 0 14px;
padding: 16px; padding: 14px;
border-radius: 12px; border-radius: var(--wxex-radius-md);
background: var(--wxex-brand-soft); background: var(--wxex-brand-soft);
} }
.agent-hub-example span { .agent-hub-example span {
@@ -1109,7 +1136,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 9px; gap: 9px;
padding: 9px 0; padding: 7px 0;
border-bottom: 1px solid var(--wxex-border); border-bottom: 1px solid var(--wxex-border);
color: var(--wxex-text-secondary); color: var(--wxex-text-secondary);
font: 13px/19px var(--wxex-font); font: 13px/19px var(--wxex-font);
@@ -1133,8 +1160,8 @@
} }
.agent-hub-log-card { .agent-hub-log-card {
max-width: 1080px; max-width: 1080px;
margin: 20px auto 0; margin: 16px auto 0;
padding: 22px; padding: 20px 22px;
} }
.agent-hub-log-heading { .agent-hub-log-heading {
display: flex; display: flex;
@@ -1245,13 +1272,13 @@
} }
.ai-vision-test h2 { .ai-vision-test h2 {
color: #202724; color: var(--wxex-text-primary);
font-size: 15px; font-size: 15px;
} }
.ai-vision-test header h3 { .ai-vision-test header h3 {
margin-top: 12px; margin-top: 12px;
color: #35403b; color: var(--wxex-text-primary);
font-size: 13px; font-size: 13px;
} }
@@ -1259,22 +1286,22 @@
.ai-vision-model, .ai-vision-model,
.ai-vision-upload small { .ai-vision-upload small {
margin-top: 4px; margin-top: 4px;
color: #66706b; color: var(--wxex-text-secondary);
font-size: 12px; font-size: 12px;
} }
.ai-vision-capability { .ai-vision-capability {
border-radius: 999px; border-radius: 999px;
padding: 5px 9px; padding: 5px 9px;
background: #f1f3f2; background: var(--wxex-bg-sidebar);
color: #66706b; color: var(--wxex-text-secondary);
white-space: nowrap; white-space: nowrap;
font-size: 12px; font-size: 12px;
} }
.ai-vision-capability.supported { .ai-vision-capability.supported {
background: #eaf5f1; background: var(--wxex-brand-soft);
color: #2e8b68; color: var(--wxex-brand);
} }
.ai-vision-model { .ai-vision-model {
@@ -1288,18 +1315,18 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 14px; gap: 14px;
border: 1px dashed #bfc9c4; border: 1px dashed var(--wxex-border);
border-radius: 10px; border-radius: var(--wxex-radius-md);
padding: 14px; padding: 14px;
background: #f7f9f8; background: var(--wxex-bg-sidebar);
color: #35403b; color: var(--wxex-text-primary);
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
} }
.ai-vision-upload:hover { .ai-vision-upload:hover {
border-color: #247a63; border-color: var(--wxex-brand);
background: #f2f8f5; background: var(--wxex-brand-soft);
} }
.ai-vision-upload input { .ai-vision-upload input {
@@ -1333,26 +1360,26 @@
} }
.ai-vision-privacy { .ai-vision-privacy {
color: #66706b; color: var(--wxex-text-secondary);
font-size: 12px; font-size: 12px;
line-height: 1.6; line-height: 1.6;
} }
.ai-vision-error { .ai-vision-error {
border-left: 3px solid #c85a5a; border-left: 3px solid var(--wxex-danger);
padding: 9px 11px; padding: 9px 11px;
background: #fff2f2; background: color-mix(in srgb, var(--wxex-danger) 12%, transparent);
color: #a84444; color: var(--wxex-danger);
font-size: 12px; font-size: 12px;
} }
.ai-vision-result { .ai-vision-result {
display: grid; display: grid;
gap: 12px; gap: 12px;
border: 1px solid #dce7e2; border: 1px solid var(--wxex-border);
border-radius: 9px; border-radius: var(--wxex-radius-md);
padding: 14px; padding: 14px;
background: #f7faf9; background: var(--wxex-bg-sidebar);
} }
.ai-vision-result dl { .ai-vision-result dl {
@@ -1363,20 +1390,20 @@
} }
.ai-vision-result dt { .ai-vision-result dt {
color: #929a96; color: var(--wxex-text-muted);
font-size: 11px; font-size: 11px;
} }
.ai-vision-result dd { .ai-vision-result dd {
margin: 4px 0 0; margin: 4px 0 0;
color: #35403b; color: var(--wxex-text-primary);
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
} }
.ai-vision-result > p { .ai-vision-result > p {
white-space: pre-wrap; white-space: pre-wrap;
color: #3d4742; color: var(--wxex-text-secondary);
font-size: 13px; font-size: 13px;
line-height: 1.7; line-height: 1.7;
} }
@@ -555,7 +555,7 @@
padding: 12px 14px; padding: 12px 14px;
border: 1.5px solid var(--wxex-border); border: 1.5px solid var(--wxex-border);
border-radius: 12px; border-radius: 12px;
background: #fff; background: var(--wxex-bg-elevated);
transition: transition:
border-color 0.2s, border-color 0.2s,
background 0.2s; background 0.2s;
@@ -563,7 +563,7 @@
.report-template-item.active { .report-template-item.active {
border-color: var(--wxex-primary, #07c160); border-color: var(--wxex-primary, #07c160);
background: #f0fbf3; background: var(--wxex-brand-soft);
} }
.report-template-item.disabled { .report-template-item.disabled {
+5 -441
View File
@@ -20,7 +20,7 @@
min-height: var(--ai-search-header-height); min-height: var(--ai-search-header-height);
padding: 14px 22px 12px; padding: 14px 22px 12px;
border-bottom: 1px solid var(--wxex-border); border-bottom: 1px solid var(--wxex-border);
background: rgba(250, 251, 250, 0.94); background: color-mix(in srgb, var(--wxex-bg-main) 94%, transparent);
h1 { h1 {
margin: 0; margin: 0;
@@ -334,50 +334,6 @@
font-weight: 700; font-weight: 700;
} }
.ai-search-local-badge {
border-radius: var(--wxex-radius-sm);
background: var(--wxex-ai-soft);
color: var(--wxex-ai);
font-size: 10px;
font-weight: 700;
line-height: 16px;
padding: 2px 6px;
white-space: nowrap;
}
.ai-search-scope-toggle,
.ai-search-range-grid {
display: grid;
gap: 4px;
}
.ai-search-scope-toggle {
grid-template-columns: 1fr 1fr;
margin-bottom: 18px;
padding: 3px;
border-radius: var(--wxex-radius-sm);
background: var(--wxex-bg-elevated);
button {
padding: 7px 4px;
font-size: 11px;
&.active {
background: var(--wxex-brand);
color: #fff;
font-weight: 700;
}
}
}
.ai-search-scope-help {
min-height: 30px;
margin: -10px 2px 12px;
color: var(--wxex-text-muted);
font-size: 10px;
line-height: 15px;
}
.ai-search-field-label { .ai-search-field-label {
display: block; display: block;
margin: 0 0 7px; margin: 0 0 7px;
@@ -387,178 +343,6 @@
line-height: 16px; line-height: 16px;
} }
.ai-search-filter-input {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
margin-bottom: 10px;
padding: 7px 9px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-sm);
background: var(--wxex-bg-elevated);
svg {
width: 15px;
height: 15px;
flex: 0 0 auto;
color: var(--wxex-text-muted);
}
input {
width: 100%;
min-width: 0;
border: 0;
outline: 0;
background: transparent;
color: var(--wxex-text-primary);
font: inherit;
font-size: 11px;
}
}
.ai-search-contact-list {
display: flex;
max-height: 168px;
flex-direction: column;
gap: 3px;
margin-bottom: 18px;
overflow-y: auto;
}
.ai-search-contact {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
padding: 7px;
border: 0;
border-radius: var(--wxex-radius-sm);
background: transparent;
color: var(--wxex-text-primary);
cursor: pointer;
font: inherit;
text-align: left;
&:hover,
&.active {
background: var(--wxex-brand-soft);
}
}
.ai-search-contact-avatar {
width: 28px;
height: 28px;
display: grid;
place-items: center;
flex: 0 0 auto;
overflow: hidden;
border-radius: var(--wxex-radius-sm);
background: var(--wxex-bg-elevated);
color: var(--wxex-brand);
font-size: 12px;
font-weight: 700;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.ai-search-contact > span:last-child {
display: flex;
min-width: 0;
flex-direction: column;
gap: 1px;
}
.ai-search-contact strong,
.ai-search-contact small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ai-search-contact strong {
font-size: 11px;
font-weight: 700;
line-height: 16px;
}
.ai-search-contact small {
color: var(--wxex-text-muted);
font-size: 10px;
line-height: 14px;
}
.ai-search-range-grid {
padding: 4px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: var(--wxex-bg-elevated);
grid-template-columns: 1fr 1fr;
margin-bottom: 16px;
button {
border: 0;
border-radius: var(--wxex-radius-sm);
background: transparent;
color: var(--wxex-text-secondary);
cursor: pointer;
font: inherit;
min-height: 34px;
padding: 7px 4px;
border: 1px solid transparent;
font-size: 11px;
&:hover {
border-color: var(--wxex-brand);
color: var(--wxex-brand);
}
&.active {
border-color: var(--wxex-brand);
background: var(--wxex-brand);
color: #fff;
box-shadow: 0 1px 3px rgba(34, 89, 74, 0.2);
}
}
}
.ai-search-field-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.ai-search-field-heading .ai-search-field-label {
margin-bottom: 7px;
}
.ai-search-field-heading > span {
color: var(--wxex-brand);
font-size: 10px;
font-weight: 700;
}
.ai-search-filter-note {
display: flex;
gap: 6px;
padding: 9px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-sm);
background: rgba(255, 255, 255, 0.48);
color: var(--wxex-text-secondary);
font-size: 10px;
line-height: 16px;
span {
color: var(--wxex-success);
}
}
.ai-search-main { .ai-search-main {
display: flex; display: flex;
min-width: 0; min-width: 0;
@@ -640,41 +424,6 @@
} }
} }
.ai-search-spinner {
width: 42px;
height: 42px;
margin-bottom: 20px;
border: 3px solid var(--wxex-brand-soft);
border-top-color: var(--wxex-brand);
border-radius: 50%;
animation: ai-search-spin 0.9s linear infinite;
}
@keyframes ai-search-spin {
to {
transform: rotate(360deg);
}
}
.ai-search-loading-steps {
display: flex;
flex-direction: column;
gap: 9px;
margin-top: 24px;
color: var(--wxex-text-muted);
font-size: 11px;
text-align: left;
.done {
color: var(--wxex-success);
}
.active {
color: var(--wxex-brand);
font-weight: 700;
}
}
.ai-search-loading { .ai-search-loading {
align-items: flex-start; align-items: flex-start;
justify-content: flex-start; justify-content: flex-start;
@@ -745,7 +494,7 @@
} }
&.error { &.error {
background: #fff8ef; background: color-mix(in srgb, var(--wxex-warning) 10%, var(--wxex-bg-elevated));
strong, strong,
p { p {
@@ -782,7 +531,7 @@
.ai-search-pipeline-step.error .ai-search-pipeline-mark { .ai-search-pipeline-step.error .ai-search-pipeline-mark {
border-color: var(--wxex-warning); border-color: var(--wxex-warning);
background: #fff1df; background: color-mix(in srgb, var(--wxex-warning) 12%, var(--wxex-bg-elevated));
color: var(--wxex-warning); color: var(--wxex-warning);
} }
@@ -1062,55 +811,6 @@
} }
} }
.ai-search-knowledge-status {
display: grid;
gap: 4px;
margin: 0 0 16px;
padding: 10px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: var(--wxex-bg-elevated);
}
.ai-search-knowledge-status > div {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.ai-search-knowledge-status > div span,
.ai-search-knowledge-status p,
.ai-search-knowledge-status small {
margin: 0;
color: var(--wxex-text-muted);
font-size: 10px;
line-height: 15px;
}
.ai-search-knowledge-status > div span {
font-weight: 700;
letter-spacing: 0.04em;
}
.ai-search-knowledge-status strong {
color: var(--wxex-text-primary);
font-size: 11px;
}
.ai-search-knowledge-status.building,
.ai-search-knowledge-status.syncing {
border-color: var(--wxex-ai);
}
.ai-search-knowledge-status.ready strong {
color: var(--wxex-success);
}
.ai-search-knowledge-status.error {
border-color: var(--wxex-warning);
}
.ai-search-insufficient-icon { .ai-search-insufficient-icon {
width: 44px; width: 44px;
height: 44px; height: 44px;
@@ -1141,7 +841,6 @@
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.ai-search-spinner,
.ai-search-result, .ai-search-result,
.ai-search-summary-block, .ai-search-summary-block,
.ai-search-answer > *, .ai-search-answer > *,
@@ -1158,12 +857,6 @@
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
} }
.ai-search-muted {
color: var(--wxex-text-muted);
font-size: 10px;
line-height: 16px;
}
@media (max-width: 1120px) { @media (max-width: 1120px) {
.ai-search-workspace { .ai-search-workspace {
--ai-search-sidebar-width: 210px; --ai-search-sidebar-width: 210px;
@@ -1190,10 +883,6 @@
padding-top: 12px; padding-top: 12px;
} }
.ai-search-contact-list {
max-height: 148px;
}
.ai-search-empty, .ai-search-empty,
.ai-search-loading, .ai-search-loading,
.ai-search-insufficient { .ai-search-insufficient {
@@ -1217,7 +906,7 @@
flex: 0 0 auto; flex: 0 0 auto;
padding: 10px 22px 12px; padding: 10px 22px 12px;
border-bottom: 1px solid var(--wxex-border); border-bottom: 1px solid var(--wxex-border);
background: #f7faf8; background: var(--wxex-bg-sidebar);
} }
.ai-search-debug-header { .ai-search-debug-header {
@@ -1263,7 +952,7 @@
overflow: auto; overflow: auto;
border: 1px solid var(--wxex-border); border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-sm); border-radius: var(--wxex-radius-sm);
background: #fff; background: var(--wxex-bg-elevated);
color: var(--wxex-text-secondary); color: var(--wxex-text-secondary);
font: font:
10px/16px ui-monospace, 10px/16px ui-monospace,
@@ -1274,127 +963,6 @@
word-break: break-word; word-break: break-word;
} }
.ai-search-task-button {
padding: 7px 10px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-sm);
background: var(--wxex-bg-elevated);
color: var(--wxex-text-secondary);
cursor: pointer;
font: inherit;
font-size: 11px;
white-space: nowrap;
}
.ai-search-task-button.active,
.ai-search-task-button:hover {
border-color: var(--wxex-brand);
color: var(--wxex-brand);
}
.ai-search-task-center {
flex: 0 0 auto;
max-height: 218px;
overflow-y: auto;
padding: 10px 22px 8px;
border-bottom: 1px solid var(--wxex-border);
background: var(--wxex-bg-elevated);
}
.ai-search-task-center-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding-bottom: 10px;
border-bottom: 1px solid var(--wxex-border);
}
.ai-search-task-center-header div {
display: grid;
gap: 2px;
}
.ai-search-task-center-header strong {
color: var(--wxex-text-primary);
font-size: 12px;
}
.ai-search-task-center-header span,
.ai-search-task-center-header > span {
color: var(--wxex-text-muted);
font-size: 10px;
}
.ai-search-task-row {
display: grid;
grid-template-columns: 8px minmax(0, 1fr) auto;
align-items: center;
gap: 9px;
padding: 10px 0;
border-bottom: 1px solid var(--wxex-border);
}
.ai-search-task-row:last-child {
border-bottom: 0;
}
.ai-search-task-status {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--wxex-text-muted);
}
.ai-search-task-status.running {
background: var(--wxex-brand);
box-shadow: 0 0 0 3px var(--wxex-brand-soft);
}
.ai-search-task-status.completed {
background: var(--wxex-success);
}
.ai-search-task-status.failed {
background: var(--wxex-warning);
}
.ai-search-task-row > div {
display: grid;
min-width: 0;
gap: 2px;
}
.ai-search-task-row strong,
.ai-search-task-row small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ai-search-task-row strong {
color: var(--wxex-text-primary);
font-size: 11px;
}
.ai-search-task-row small {
color: var(--wxex-text-muted);
font-size: 10px;
}
.ai-search-task-result {
color: var(--wxex-text-secondary);
font-size: 10px;
white-space: nowrap;
}
.ai-search-task-empty {
margin: 12px 0 2px;
color: var(--wxex-text-muted);
font-size: 11px;
text-align: center;
}
.ai-search-markdown-spacer { .ai-search-markdown-spacer {
height: 8px; height: 8px;
} }
@@ -1442,8 +1010,4 @@
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
} }
.ai-search-task-row {
grid-template-columns: 8px minmax(0, 1fr) auto;
}
} }
@@ -1,45 +1,3 @@
.settings-primary-button,
.settings-danger-button {
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-sm);
background: var(--wxex-bg-elevated);
color: var(--wxex-text-primary);
cursor: pointer;
font: 12px/18px var(--wxex-font);
padding: 8px 12px;
&:hover:not(:disabled) {
border-color: var(--wxex-brand);
color: var(--wxex-brand);
}
&:disabled {
cursor: not-allowed;
opacity: 0.55;
}
}
.settings-primary-button {
border-color: var(--wxex-brand);
background: var(--wxex-brand);
color: #fff;
font-weight: 600;
&:hover:not(:disabled) {
background: var(--wxex-brand-hover);
color: #fff;
}
}
.settings-danger-button {
color: var(--wxex-danger);
&:hover:not(:disabled) {
border-color: var(--wxex-danger);
color: var(--wxex-danger);
}
}
.settings-card-kicker { .settings-card-kicker {
display: block; display: block;
margin-bottom: 6px; margin-bottom: 6px;
@@ -306,7 +264,7 @@
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: var(--wxex-surface-muted, #f7f9f8); background: var(--wxex-bg-sidebar);
} }
&.selected { &.selected {
@@ -322,7 +280,7 @@
overflow: hidden; overflow: hidden;
border: 1px solid var(--wxex-border); border: 1px solid var(--wxex-border);
border-radius: 50%; border-radius: 50%;
background: var(--wxex-surface-muted, #f7f9f8); background: var(--wxex-bg-sidebar);
color: var(--wxex-text-secondary); color: var(--wxex-text-secondary);
font-size: 12px; font-size: 12px;
} }
@@ -383,7 +341,7 @@
align-content: start; align-content: start;
gap: 16px; gap: 16px;
padding: 14px; padding: 14px;
background: var(--wxex-surface-muted, #f7f9f8); background: var(--wxex-bg-sidebar);
} }
.voice-batch-field { .voice-batch-field {
+4 -101
View File
@@ -18,7 +18,7 @@
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
background: #fafbfa; background: var(--wxex-bg-main);
} }
.settings-page-panel { .settings-page-panel {
@@ -244,9 +244,9 @@
font-size: 16px; font-size: 16px;
} }
.settings-card { .settings-card {
border: 1px solid #dde3e0; border: 1px solid var(--wxex-border);
border-radius: 10px; border-radius: var(--wxex-radius-md);
background: #fff; background: var(--wxex-bg-elevated);
padding: 24px 26px; padding: 24px 26px;
} }
.settings-account-overview { .settings-account-overview {
@@ -505,34 +505,6 @@
.database-key-muted { .database-key-muted {
color: #66706b !important; color: #66706b !important;
} }
.database-key-primary,
.database-key-secondary {
min-height: 36px;
padding: 0 15px;
border: 1px solid #247a63;
border-radius: 8px;
cursor: pointer;
font: 600 13px/1 inherit;
}
.database-key-primary {
background: #247a63;
color: #fff;
}
.database-key-secondary {
background: #fff;
color: #247a63;
}
.database-key-primary:hover:not(:disabled) {
background: #1d6754;
}
.database-key-secondary:hover:not(:disabled) {
background: #f0f7f4;
}
.database-key-primary:disabled,
.database-key-secondary:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.database-key-editor label { .database-key-editor label {
display: block; display: block;
margin-bottom: 9px; margin-bottom: 9px;
@@ -841,52 +813,6 @@
color: #786868; color: #786868;
font-size: 12px; font-size: 12px;
} }
.database-key-confirm-backdrop {
position: fixed;
z-index: 30;
inset: 0;
display: grid;
place-items: center;
padding: 24px;
background: rgba(25, 32, 29, 0.34);
}
.database-key-confirm {
width: min(100%, 430px);
padding: 22px;
border-radius: 12px;
background: #fff;
box-shadow: 0 18px 50px rgba(24, 35, 30, 0.2);
}
.database-key-confirm h2 {
margin: 0;
color: #202724;
font-size: 17px;
}
.database-key-confirm p {
margin: 12px 0 22px;
color: #66706b;
font-size: 13px;
line-height: 1.7;
}
.database-key-confirm > div {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.database-key-confirm button {
min-height: 34px;
border: 1px solid #dde3e0;
border-radius: 8px;
padding: 0 14px;
background: #fff;
color: #46514c;
cursor: pointer;
}
.database-key-confirm button.danger {
border-color: #c85a5a;
background: #c85a5a;
color: #fff;
}
/* SETTINGS-03: image decryption */ /* SETTINGS-03: image decryption */
.image-decryption-content { .image-decryption-content {
padding-bottom: 48px; padding-bottom: 48px;
@@ -1102,29 +1028,6 @@
font-size: 12px; font-size: 12px;
line-height: 1.6; line-height: 1.6;
} }
.image-test-result {
display: flex;
flex-wrap: wrap;
gap: 8px 18px;
margin-top: 14px;
padding: 12px 14px;
border-radius: 8px;
color: #2e765d;
font-size: 12px;
}
.image-test-result.success {
border: 1px solid #bfded3;
background: #eaf5f1;
}
.image-test-result.error {
border: 1px solid #efcaca;
background: #fff2f2;
color: #a84444;
}
.image-test-result p {
width: 100%;
margin: 2px 0 0;
}
.image-step-list { .image-step-list {
list-style: none; list-style: none;
margin: 14px 0 0; margin: 14px 0 0;
@@ -1,71 +1,3 @@
/* Sidebar 自助卡片 + 入口 */
.sidebar-footer {
padding: 10px 12px;
border-top: 1px solid var(--border-color);
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
background-color: #f3f4f5;
transition: background-color 0.15s ease;
user-select: none;
&:hover {
background-color: #e6e9eb;
}
}
.sidebar-self-avatar {
width: 36px;
height: 36px;
border-radius: 6px;
background-color: #07c160;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 500;
overflow: hidden;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.sidebar-self-info {
flex: 1;
min-width: 0;
overflow: hidden;
}
.sidebar-self-nickname {
font-size: 13px;
font-weight: 500;
color: #1f2429;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-self-wxid {
font-size: 11px;
color: #8a9298;
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-self-arrow {
color: #8a9298;
font-size: 18px;
flex-shrink: 0;
}
/* 启动自动连接 Splash */ /* 启动自动连接 Splash */
.boot-splash { .boot-splash {
position: fixed; position: fixed;
+81 -44
View File
@@ -4,26 +4,34 @@
@layer base { @layer base {
:root { :root {
--tm-background: 150 18% 96%; --tm-background: 150 10% 96.0784%;
--tm-foreground: 150 12% 15%; --tm-canvas: 120 11.1111% 98.2353%;
--tm-foreground: 154.2857 9.8592% 13.9216%;
--tm-surface: 0 0% 100%; --tm-surface: 0 0% 100%;
--tm-surface-muted: 150 14% 94%; --tm-surface-muted: 150 13.3333% 94.1176%;
--tm-surface-elevated: 0 0% 100%; --tm-surface-elevated: 0 0% 100%;
--tm-border: 150 13% 87%; --tm-border: 150 9.6774% 87.8431%;
--tm-border-subtle: 150 10% 92%; --tm-border-subtle: 150 10% 92%;
--tm-primary: 161 54% 31%; --tm-primary: 163.9535 54.4304% 30.9804%;
--tm-primary-hover: 164.5946 56.0606% 25.8824%;
--tm-primary-foreground: 0 0% 100%; --tm-primary-foreground: 0 0% 100%;
--tm-secondary: 150 13% 91%; --tm-secondary: 150 13.3333% 94.1176%;
--tm-secondary-foreground: 150 12% 20%; --tm-secondary-foreground: 154.2857 9.8592% 13.9216%;
--tm-muted: 150 11% 92%; --tm-muted: 150 10% 92%;
--tm-muted-foreground: 150 8% 44%; --tm-muted-foreground: 150 4.6729% 41.9608%;
--tm-accent: 157 39% 89%; --tm-subtle-foreground: 150 3.8095% 58.8235%;
--tm-accent-foreground: 161 54% 25%; --tm-accent: 157.5 30.7692% 89.8039%;
--tm-success: 155 51% 37%; --tm-accent-foreground: 163.9535 54.4304% 30.9804%;
--tm-warning: 34 57% 49%; --tm-ai: 236.8966 62.3656% 63.5294%;
--tm-destructive: 0 51% 54%; --tm-ai-soft: 236 78.9474% 96.2745%;
--tm-success: 157.4194 50.2703% 36.2745%;
--tm-warning: 33.5172 57.7689% 49.2157%;
--tm-destructive: 0 50% 56.8627%;
--tm-destructive-foreground: 0 0% 100%; --tm-destructive-foreground: 0 0% 100%;
--tm-focus-ring: 161 54% 38%; --tm-focus-ring: 164 54.4% 31%;
--tm-disabled-surface: 150 13.3333% 94.1176%;
--tm-disabled-foreground: 150 3.8095% 58.8235%;
--tm-disabled-border: 150 10% 92%;
--tm-radius-sm: 0.375rem; --tm-radius-sm: 0.375rem;
--tm-radius-md: 0.5rem; --tm-radius-md: 0.5rem;
--tm-radius-lg: 0.75rem; --tm-radius-lg: 0.75rem;
@@ -37,6 +45,9 @@
--tm-motion-slow: 260ms; --tm-motion-slow: 260ms;
--tm-ease-standard: cubic-bezier(0.2, 0, 0, 1); --tm-ease-standard: cubic-bezier(0.2, 0, 0, 1);
--tm-ease-emphasized: cubic-bezier(0.16, 1, 0.3, 1); --tm-ease-emphasized: cubic-bezier(0.16, 1, 0.3, 1);
--tm-control-height-compact: 1.75rem;
--tm-control-height-standard: 2rem;
--tm-control-height-form: 2.25rem;
--tm-z-base: 0; --tm-z-base: 0;
--tm-z-dropdown: 40; --tm-z-dropdown: 40;
--tm-z-popover: 50; --tm-z-popover: 50;
@@ -45,26 +56,34 @@
} }
[data-theme='dark'] { [data-theme='dark'] {
--tm-background: 150 15% 10%; --tm-background: 165 8% 9.8039%;
--tm-foreground: 145 25% 94%; --tm-canvas: 160 9.0909% 12.9412%;
--tm-foreground: 145.7143 24.1379% 94.3137%;
--tm-surface: 150 13% 15%; --tm-surface: 150 13% 15%;
--tm-surface-muted: 150 12% 18%; --tm-surface-muted: 153.3333 12.3288% 14.3137%;
--tm-surface-elevated: 150 12% 19%; --tm-surface-elevated: 160 10.3448% 17.0588%;
--tm-border: 150 12% 28%; --tm-border: 152.3077 10.2362% 24.902%;
--tm-border-subtle: 150 10% 22%; --tm-border-subtle: 150 10% 22%;
--tm-primary: 157 48% 52%; --tm-primary: 163.9535 54.4304% 30.9804%;
--tm-primary-foreground: 150 15% 10%; --tm-primary-hover: 157.4194 50.2703% 36.2745%;
--tm-secondary: 150 12% 24%; --tm-primary-foreground: 0 0% 100%;
--tm-secondary-foreground: 145 25% 94%; --tm-secondary: 150 10% 22%;
--tm-muted: 150 12% 24%; --tm-secondary-foreground: 145.7143 24.1379% 94.3137%;
--tm-muted-foreground: 150 10% 66%; --tm-muted: 150 10% 22%;
--tm-accent: 157 31% 25%; --tm-muted-foreground: 150 10% 72.549%;
--tm-accent-foreground: 157 48% 82%; --tm-subtle-foreground: 153.75 6.7797% 53.7255%;
--tm-accent: 158.8235 30.9091% 21.5686%;
--tm-accent-foreground: 145.7143 24.1379% 94.3137%;
--tm-ai: 236.8966 62.3656% 63.5294%;
--tm-ai-soft: 236 24% 20%;
--tm-success: 155 51% 55%; --tm-success: 155 51% 55%;
--tm-warning: 34 75% 63%; --tm-warning: 34 75% 63%;
--tm-destructive: 0 65% 66%; --tm-destructive: 0 65% 66%;
--tm-destructive-foreground: 150 15% 10%; --tm-destructive-foreground: 165 8% 9.8039%;
--tm-focus-ring: 157 48% 62%; --tm-focus-ring: 157 48% 62%;
--tm-disabled-surface: 150 10% 18%;
--tm-disabled-foreground: 153.75 6.7797% 53.7255%;
--tm-disabled-border: 150 10% 22%;
--tm-shadow-surface: 0 1px 2px rgb(0 0 0 / 0.2); --tm-shadow-surface: 0 1px 2px rgb(0 0 0 / 0.2);
--tm-shadow-floating: 0 8px 24px rgb(0 0 0 / 0.28); --tm-shadow-floating: 0 8px 24px rgb(0 0 0 / 0.28);
--tm-shadow-popover: 0 12px 32px rgb(0 0 0 / 0.38); --tm-shadow-popover: 0 12px 32px rgb(0 0 0 / 0.38);
@@ -73,30 +92,48 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
[data-theme='system'] { [data-theme='system'] {
--tm-background: 150 15% 10%; --tm-background: 165 8% 9.8039%;
--tm-foreground: 145 25% 94%; --tm-canvas: 160 9.0909% 12.9412%;
--tm-foreground: 145.7143 24.1379% 94.3137%;
--tm-surface: 150 13% 15%; --tm-surface: 150 13% 15%;
--tm-surface-muted: 150 12% 18%; --tm-surface-muted: 153.3333 12.3288% 14.3137%;
--tm-surface-elevated: 150 12% 19%; --tm-surface-elevated: 160 10.3448% 17.0588%;
--tm-border: 150 12% 28%; --tm-border: 152.3077 10.2362% 24.902%;
--tm-border-subtle: 150 10% 22%; --tm-border-subtle: 150 10% 22%;
--tm-primary: 157 48% 52%; --tm-primary: 163.9535 54.4304% 30.9804%;
--tm-primary-foreground: 150 15% 10%; --tm-primary-hover: 157.4194 50.2703% 36.2745%;
--tm-secondary: 150 12% 24%; --tm-primary-foreground: 0 0% 100%;
--tm-secondary-foreground: 145 25% 94%; --tm-secondary: 150 10% 22%;
--tm-muted: 150 12% 24%; --tm-secondary-foreground: 145.7143 24.1379% 94.3137%;
--tm-muted-foreground: 150 10% 66%; --tm-muted: 150 10% 22%;
--tm-accent: 157 31% 25%; --tm-muted-foreground: 150 10% 72.549%;
--tm-accent-foreground: 157 48% 82%; --tm-subtle-foreground: 153.75 6.7797% 53.7255%;
--tm-accent: 158.8235 30.9091% 21.5686%;
--tm-accent-foreground: 145.7143 24.1379% 94.3137%;
--tm-ai: 236.8966 62.3656% 63.5294%;
--tm-ai-soft: 236 24% 20%;
--tm-success: 155 51% 55%; --tm-success: 155 51% 55%;
--tm-warning: 34 75% 63%; --tm-warning: 34 75% 63%;
--tm-destructive: 0 65% 66%; --tm-destructive: 0 65% 66%;
--tm-destructive-foreground: 150 15% 10%; --tm-destructive-foreground: 165 8% 9.8039%;
--tm-focus-ring: 157 48% 62%; --tm-focus-ring: 157 48% 62%;
--tm-disabled-surface: 150 10% 18%;
--tm-disabled-foreground: 153.75 6.7797% 53.7255%;
--tm-disabled-border: 150 10% 22%;
} }
} }
:where(button, input, textarea, select):focus-visible { :where(
button,
input,
textarea,
select,
[role='button'],
[role='checkbox'],
[role='radio'],
[role='switch'],
[role='tab']
):focus-visible {
outline: 2px solid hsl(var(--tm-focus-ring) / 0.75); outline: 2px solid hsl(var(--tm-focus-ring) / 0.75);
outline-offset: 2px; outline-offset: 2px;
} }
+16 -10
View File
@@ -1,13 +1,20 @@
@mixin dark-theme { @mixin dark-theme {
--wxex-bg-app: #171b1a; --wxex-bg-app: hsl(var(--tm-background));
--wxex-bg-main: #1e2422; --wxex-bg-main: hsl(var(--tm-canvas));
--wxex-bg-sidebar: #202925; --wxex-bg-sidebar: hsl(var(--tm-surface-muted));
--wxex-bg-elevated: #27302d; --wxex-bg-elevated: hsl(var(--tm-surface-elevated));
--wxex-text-primary: #edf4f0; --wxex-text-primary: hsl(var(--tm-foreground));
--wxex-text-secondary: #b2c0b9; --wxex-text-secondary: hsl(var(--tm-muted-foreground));
--wxex-text-muted: #81918a; --wxex-text-muted: hsl(var(--tm-subtle-foreground));
--wxex-border: #394640; --wxex-border: hsl(var(--tm-border));
--wxex-brand-soft: #26483c; --wxex-brand: hsl(var(--tm-primary));
--wxex-brand-hover: hsl(var(--tm-primary-hover));
--wxex-brand-soft: hsl(var(--tm-accent));
--wxex-ai: hsl(var(--tm-ai));
--wxex-ai-soft: hsl(var(--tm-ai-soft));
--wxex-success: hsl(var(--tm-success));
--wxex-warning: hsl(var(--tm-warning));
--wxex-danger: hsl(var(--tm-destructive));
.conversation-section-header:hover, .conversation-section-header:hover,
.conversation-item:hover, .conversation-item:hover,
@@ -52,7 +59,6 @@
color: var(--wxex-text-secondary); color: var(--wxex-text-secondary);
} }
.wechat-system-message-meta,
.message-sender-name, .message-sender-name,
.message-hover-time, .message-hover-time,
.message-accessible-sender { .message-accessible-sender {
+11
View File
@@ -11,6 +11,7 @@ module.exports = {
extend: { extend: {
colors: { colors: {
background: 'hsl(var(--tm-background) / <alpha-value>)', background: 'hsl(var(--tm-background) / <alpha-value>)',
canvas: 'hsl(var(--tm-canvas) / <alpha-value>)',
foreground: 'hsl(var(--tm-foreground) / <alpha-value>)', foreground: 'hsl(var(--tm-foreground) / <alpha-value>)',
surface: 'hsl(var(--tm-surface) / <alpha-value>)', surface: 'hsl(var(--tm-surface) / <alpha-value>)',
'surface-muted': 'hsl(var(--tm-surface-muted) / <alpha-value>)', 'surface-muted': 'hsl(var(--tm-surface-muted) / <alpha-value>)',
@@ -18,19 +19,29 @@ module.exports = {
border: 'hsl(var(--tm-border) / <alpha-value>)', border: 'hsl(var(--tm-border) / <alpha-value>)',
'border-subtle': 'hsl(var(--tm-border-subtle) / <alpha-value>)', 'border-subtle': 'hsl(var(--tm-border-subtle) / <alpha-value>)',
primary: 'hsl(var(--tm-primary) / <alpha-value>)', primary: 'hsl(var(--tm-primary) / <alpha-value>)',
'primary-hover': 'hsl(var(--tm-primary-hover) / <alpha-value>)',
'primary-foreground': 'hsl(var(--tm-primary-foreground) / <alpha-value>)', 'primary-foreground': 'hsl(var(--tm-primary-foreground) / <alpha-value>)',
secondary: 'hsl(var(--tm-secondary) / <alpha-value>)', secondary: 'hsl(var(--tm-secondary) / <alpha-value>)',
'secondary-foreground': 'hsl(var(--tm-secondary-foreground) / <alpha-value>)', 'secondary-foreground': 'hsl(var(--tm-secondary-foreground) / <alpha-value>)',
muted: 'hsl(var(--tm-muted) / <alpha-value>)', muted: 'hsl(var(--tm-muted) / <alpha-value>)',
'muted-foreground': 'hsl(var(--tm-muted-foreground) / <alpha-value>)', 'muted-foreground': 'hsl(var(--tm-muted-foreground) / <alpha-value>)',
'subtle-foreground': 'hsl(var(--tm-subtle-foreground) / <alpha-value>)',
accent: 'hsl(var(--tm-accent) / <alpha-value>)', accent: 'hsl(var(--tm-accent) / <alpha-value>)',
'accent-foreground': 'hsl(var(--tm-accent-foreground) / <alpha-value>)', 'accent-foreground': 'hsl(var(--tm-accent-foreground) / <alpha-value>)',
success: 'hsl(var(--tm-success) / <alpha-value>)', success: 'hsl(var(--tm-success) / <alpha-value>)',
warning: 'hsl(var(--tm-warning) / <alpha-value>)', warning: 'hsl(var(--tm-warning) / <alpha-value>)',
destructive: 'hsl(var(--tm-destructive) / <alpha-value>)', destructive: 'hsl(var(--tm-destructive) / <alpha-value>)',
'destructive-foreground': 'hsl(var(--tm-destructive-foreground) / <alpha-value>)', 'destructive-foreground': 'hsl(var(--tm-destructive-foreground) / <alpha-value>)',
'disabled-surface': 'hsl(var(--tm-disabled-surface) / <alpha-value>)',
'disabled-foreground': 'hsl(var(--tm-disabled-foreground) / <alpha-value>)',
'disabled-border': 'hsl(var(--tm-disabled-border) / <alpha-value>)',
ring: 'hsl(var(--tm-focus-ring) / <alpha-value>)' ring: 'hsl(var(--tm-focus-ring) / <alpha-value>)'
}, },
height: {
'control-compact': 'var(--tm-control-height-compact)',
'control-standard': 'var(--tm-control-height-standard)',
'control-form': 'var(--tm-control-height-form)'
},
borderRadius: { borderRadius: {
sm: 'var(--tm-radius-sm)', sm: 'var(--tm-radius-sm)',
md: 'var(--tm-radius-md)', md: 'var(--tm-radius-md)',
@@ -16,6 +16,32 @@ const baseProps = {
} }
describe('export progress panel', () => { describe('export progress panel', () => {
it('uses the semantic accent surface for outgoing preview messages', () => {
render(
<ExportPreviewPanel
{...baseProps}
status="idle"
previewItems={[
{
id: 'outgoing-preview',
from: 'self',
type: '文字',
datetime: '',
content: '已发送的消息',
isSender: true
}
]}
progress={null}
includeVoiceTranscripts={false}
zip={false}
/>
)
expect(screen.getByText('已发送的消息').closest('.export-preview-bubble')).toHaveClass(
'bg-accent'
)
})
it('shows an indeterminate bar while the first message scan is still at zero', () => { it('shows an indeterminate bar while the first message scan is still at zero', () => {
render( render(
<ExportPreviewPanel <ExportPreviewPanel
+112
View File
@@ -0,0 +1,112 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it } from 'vitest'
import {
Button,
Checkbox,
Input,
RadioGroup,
RadioGroupItem,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Switch
} from '../../src/renderer/src/components/ui'
describe('TraceMemo UI control states', () => {
it('uses the compact, standard, and form control height contract', () => {
render(
<>
<Button size="sm"></Button>
<Button></Button>
<Button size="lg"></Button>
<Input aria-label="表单输入" />
</>
)
expect(screen.getByRole('button', { name: '紧凑' })).toHaveClass('h-control-compact')
expect(screen.getByRole('button', { name: '标准' })).toHaveClass('h-control-standard')
expect(screen.getByRole('button', { name: '表单' })).toHaveClass('h-control-form')
expect(screen.getByRole('textbox', { name: '表单输入' })).toHaveClass('h-control-form')
})
it('keeps disabled controls readable without the old opacity and surface shadow', () => {
render(
<>
<Button disabled></Button>
<Input aria-label="不可用输入" value="仍可读取" disabled readOnly />
<Checkbox aria-label="不可用多选" disabled />
<RadioGroup aria-label="不可用单选组">
<RadioGroupItem value="disabled" aria-label="不可用单选" disabled />
</RadioGroup>
<Switch aria-label="不可用开关" disabled />
</>
)
for (const control of [
screen.getByRole('button', { name: '不可用操作' }),
screen.getByRole('textbox', { name: '不可用输入' }),
screen.getByRole('checkbox', { name: '不可用多选' }),
screen.getByRole('radio', { name: '不可用单选' }),
screen.getByRole('switch', { name: '不可用开关' })
]) {
expect(control).toHaveClass('disabled:opacity-100')
expect(control).not.toHaveClass('shadow-surface')
}
expect(screen.getByRole('button', { name: '不可用操作' })).toHaveClass(
'disabled:bg-disabled-surface',
'disabled:text-disabled-foreground',
'disabled:!text-disabled-foreground'
)
expect(screen.getByRole('checkbox', { name: '不可用多选' })).toHaveClass(
'border-muted-foreground/60'
)
expect(screen.getByRole('radio', { name: '不可用单选' })).toHaveClass(
'border-muted-foreground/60'
)
})
it('keeps selection semantics while removing text glyph indicators', async () => {
const user = userEvent.setup()
render(
<>
<Select defaultValue="recent">
<SelectTrigger aria-label="时间范围">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="recent"> 30 </SelectItem>
<SelectItem value="all"></SelectItem>
</SelectContent>
</Select>
<Checkbox aria-label="包含图片" />
<RadioGroup aria-label="密度" defaultValue="compact">
<RadioGroupItem value="compact" aria-label="紧凑密度" />
<RadioGroupItem value="comfortable" aria-label="舒适密度" />
</RadioGroup>
<Switch aria-label="显示头像" />
</>
)
const trigger = screen.getByRole('combobox', { name: '时间范围' })
expect(trigger).not.toHaveTextContent('⌄')
await user.click(trigger)
expect(await screen.findByRole('option', { name: '最近 30 天' })).not.toHaveTextContent('✓')
await user.keyboard('{Escape}')
const checkbox = screen.getByRole('checkbox', { name: '包含图片' })
await user.click(checkbox)
expect(checkbox).toBeChecked()
expect(checkbox).not.toHaveTextContent('✓')
await user.click(screen.getByRole('radio', { name: '舒适密度' }))
expect(screen.getByRole('radio', { name: '舒适密度' })).toBeChecked()
const toggle = screen.getByRole('switch', { name: '显示头像' })
await user.click(toggle)
expect(toggle).toBeChecked()
})
})
+56
View File
@@ -649,6 +649,62 @@ test('EXPORT-01 multi-chat selection stays local to export and forces HTML', asy
} }
}) })
test('EXPORT-02 large contact list stays bounded and searchable', async () => {
const fixture = await launchTestApp({ largeContacts: 1500 })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 1400, height: 772 })
await fixture.page.getByRole('button', { name: '导出' }).click()
await expect(fixture.page.getByText('共 1,503 个')).toBeVisible()
const contactList = fixture.page.locator('.export-contact-list')
expect(await contactList.getByRole('button').count()).toBeLessThan(50)
const compositeButtons = [
fixture.page.getByRole('button', { name: /^全部导出/ }),
fixture.page.getByRole('button', { name: /HTML/ }),
fixture.page.locator('.export-workspace > aside:first-child > button:last-child')
]
for (const button of compositeButtons) {
await expect(button).toBeVisible()
expect(
await button.evaluate(
(element) => element.scrollHeight <= element.clientHeight && element.clientHeight > 32
)
).toBe(true)
}
const previewGeometryIsStable = await fixture.page
.locator('.export-workspace > aside:last-child')
.evaluate((previewPanel) => {
const scrollRegion = previewPanel.children.item(1)
const statistics = previewPanel.children.item(2)
if (!(scrollRegion instanceof HTMLElement) || !(statistics instanceof HTMLElement)) {
return false
}
const panelRect = previewPanel.getBoundingClientRect()
const scrollRect = scrollRegion.getBoundingClientRect()
const statisticsRect = statistics.getBoundingClientRect()
return (
scrollRect.bottom <= statisticsRect.top &&
statisticsRect.bottom <= panelRect.bottom &&
statistics.scrollHeight <= statistics.clientHeight
)
})
expect(previewGeometryIsStable).toBe(true)
await fixture.page.getByRole('textbox', { name: '搜索聊天' }).fill('性能样本 1499')
const target = contactList.getByRole('button', { name: /性能样本 1499/ })
await expect(target).toBeVisible()
await target.click()
await expect(target).toHaveAttribute('aria-pressed', 'true')
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('LAYOUT-01 core workspaces fit a narrow desktop viewport without page errors', async () => { test('LAYOUT-01 core workspaces fit a narrow desktop viewport without page errors', async () => {
const fixture = await launchTestApp() const fixture = await launchTestApp()
const pageErrors: Error[] = [] const pageErrors: Error[] = []
+15 -3
View File
@@ -23,10 +23,16 @@ export async function launchTestApp(
aiFailure?: string aiFailure?: string
now?: number now?: number
appearanceTheme?: 'light' | 'dark' appearanceTheme?: 'light' | 'dark'
stableUserData?: string
} = {} } = {}
): Promise<TestApplication> { ): Promise<TestApplication> {
const ownsDirectory = !options.userData const ownsDirectory = !options.userData || Boolean(options.stableUserData)
const userData = options.userData || mkdtempSync(resolve(tmpdir(), 'wxe-e2e-')) const userData =
options.userData ||
(options.stableUserData
? resolve(options.stableUserData)
: mkdtempSync(resolve(tmpdir(), 'wxe-e2e-')))
if (options.stableUserData) rmSync(userData, { recursive: true, force: true })
const localTestEnv = loadEnv('test', process.cwd(), 'WXE_E2E_') const localTestEnv = loadEnv('test', process.cwd(), 'WXE_E2E_')
const configuredCloseDelay = Number( const configuredCloseDelay = Number(
process.env.WXE_E2E_CLOSE_DELAY_MS ?? localTestEnv.WXE_E2E_CLOSE_DELAY_MS process.env.WXE_E2E_CLOSE_DELAY_MS ?? localTestEnv.WXE_E2E_CLOSE_DELAY_MS
@@ -50,15 +56,21 @@ export async function launchTestApp(
const page = await app.firstWindow() const page = await app.firstWindow()
await page.waitForLoadState('domcontentloaded') await page.waitForLoadState('domcontentloaded')
const setWindowContentSize = async (size: { width: number; height: number }): Promise<void> => { const setWindowContentSize = async (size: { width: number; height: number }): Promise<void> => {
await app.evaluate(({ BrowserWindow }, nextSize) => { await app.evaluate(({ BrowserWindow, screen }, nextSize) => {
const [window] = BrowserWindow.getAllWindows() const [window] = BrowserWindow.getAllWindows()
if (!window) throw new Error('E2E BrowserWindow is unavailable') if (!window) throw new Error('E2E BrowserWindow is unavailable')
const { workArea } = screen.getPrimaryDisplay()
window.setPosition(workArea.x + 40, workArea.y + 40)
window.setContentSize(nextSize.width, nextSize.height) window.setContentSize(nextSize.width, nextSize.height)
}, size) }, size)
await page.waitForFunction( await page.waitForFunction(
(nextSize) => window.innerWidth === nextSize.width && window.innerHeight === nextSize.height, (nextSize) => window.innerWidth === nextSize.width && window.innerHeight === nextSize.height,
size size
) )
await page.evaluate(
() =>
new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
)
} }
return { return {
app, app,
+64 -14
View File
@@ -4,7 +4,7 @@ import { resolve } from 'path'
import { launchTestApp } from './support/electron' import { launchTestApp } from './support/electron'
const baselineDirectory = resolve(`tests/e2e/__screenshots__/${process.platform}/visual.spec.ts`) const baselineDirectory = resolve(`tests/e2e/__screenshots__/${process.platform}/visual.spec.ts`)
const visualViewport = { width: 1000, height: 650 } const visualViewport = { width: 1400, height: 772 }
const visualNow = Date.parse('2026-08-19T14:46:40+08:00') const visualNow = Date.parse('2026-08-19T14:46:40+08:00')
async function clearScreenshotFocus(page: import('@playwright/test').Page): Promise<void> { async function clearScreenshotFocus(page: import('@playwright/test').Page): Promise<void> {
@@ -200,6 +200,30 @@ test('API-00 Reader Skill page visual @visual', async () => {
} }
}) })
test('API-00 Reader Skill page dark visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow, appearanceTheme: 'dark' })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByRole('button', { name: 'API' }).click()
await expect(fixture.page.getByRole('heading', { name: 'TraceMemo Reader' })).toBeVisible()
await expect(fixture.page.getByText('API Token', { exact: true })).toBeVisible()
await expect(fixture.page.locator('html')).toHaveAttribute('data-theme', 'dark')
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('api-center-page-dark.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('API-01 Reader Skill preview visual @visual', async () => { test('API-01 Reader Skill preview visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow }) const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = [] const pageErrors: Error[] = []
@@ -259,8 +283,9 @@ test('API-02 Agent target segmented control visual @visual', async () => {
} }
}) })
test('REPORT-00 report configuration controls visual @visual', async () => { for (const appearanceTheme of ['light', 'dark'] as const) {
const fixture = await launchTestApp({ now: visualNow }) test(`REPORT-00 report configuration controls ${appearanceTheme} visual @visual`, async () => {
const fixture = await launchTestApp({ now: visualNow, appearanceTheme })
const pageErrors: Error[] = [] const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error)) fixture.page.on('pageerror', (error) => pageErrors.push(error))
try { try {
@@ -275,21 +300,30 @@ test('REPORT-00 report configuration controls visual @visual', async () => {
).toBe(true) ).toBe(true)
expect(pageErrors).toEqual([]) expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page) await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('report-config-page.png', { await expect(fixture.page).toHaveScreenshot(
`${appearanceTheme === 'light' ? 'report-config-page' : 'report-config-page-dark'}.png`,
{
animations: 'disabled', animations: 'disabled',
caret: 'hide' caret: 'hide'
}) }
)
const modelHeading = fixture.page.getByRole('heading', { name: '模型配置' }) const modelHeading = fixture.page.getByRole('heading', { name: '模型配置' })
await modelHeading.scrollIntoViewIfNeeded() await modelHeading.scrollIntoViewIfNeeded()
await expect(modelHeading).toBeVisible() await expect(modelHeading).toBeVisible()
await clearScreenshotFocus(fixture.page) await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('report-model-controls.png', { await expect(fixture.page).toHaveScreenshot(
`${appearanceTheme === 'light' ? 'report-model-controls' : 'report-model-controls-dark'}.png`,
{
animations: 'disabled', animations: 'disabled',
caret: 'hide' caret: 'hide'
}) }
)
const templateSection = fixture.page.getByRole('heading', { name: '日报模板' }).locator('..') if (appearanceTheme === 'light') {
const templateSection = fixture.page
.getByRole('heading', { name: '日报模板' })
.locator('..')
await templateSection.getByRole('button', { name: '查看版式' }).first().click() await templateSection.getByRole('button', { name: '查看版式' }).first().click()
const templateDialog = fixture.page.getByRole('dialog', { name: '经典日报' }) const templateDialog = fixture.page.getByRole('dialog', { name: '经典日报' })
await expect(templateDialog).toBeVisible() await expect(templateDialog).toBeVisible()
@@ -298,13 +332,21 @@ test('REPORT-00 report configuration controls visual @visual', async () => {
animations: 'disabled', animations: 'disabled',
caret: 'hide' caret: 'hide'
}) })
}
} finally { } finally {
await fixture.close() await fixture.close()
} }
}) })
}
test('REPORT-01 report retry controls visual @visual', async () => { for (const appearanceTheme of ['light', 'dark'] as const) {
const fixture = await launchTestApp({ now: visualNow, aiFailure: '401' }) test(`REPORT-01 report retry controls ${appearanceTheme} visual @visual`, async () => {
const fixture = await launchTestApp({
now: visualNow,
aiFailure: '401',
appearanceTheme,
stableUserData: '/tmp/tracememo-e2e-report-user-data'
})
const pageErrors: Error[] = [] const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error)) fixture.page.on('pageerror', (error) => pageErrors.push(error))
try { try {
@@ -322,14 +364,18 @@ test('REPORT-01 report retry controls visual @visual', async () => {
).toBe(true) ).toBe(true)
expect(pageErrors).toEqual([]) expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page) await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('report-retry-controls.png', { await expect(fixture.page).toHaveScreenshot(
`${appearanceTheme === 'light' ? 'report-retry-controls' : 'report-retry-controls-dark'}.png`,
{
animations: 'disabled', animations: 'disabled',
caret: 'hide' caret: 'hide'
}) }
)
} finally { } finally {
await fixture.close() await fixture.close()
} }
}) })
}
for (const appearanceTheme of ['light', 'dark'] as const) { for (const appearanceTheme of ['light', 'dark'] as const) {
test(`CHAT-02 personal WeChat send dialog ${appearanceTheme} visual @visual`, async () => { test(`CHAT-02 personal WeChat send dialog ${appearanceTheme} visual @visual`, async () => {
@@ -568,6 +614,10 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
await expect(fixture.page.getByRole('heading', { name: '新增供应商' })).toBeVisible() await expect(fixture.page.getByRole('heading', { name: '新增供应商' })).toBeVisible()
await fixture.page.getByLabel('供应商 ID').fill('fixture-new-provider') await fixture.page.getByLabel('供应商 ID').fill('fixture-new-provider')
await expect(fixture.page.getByRole('combobox', { name: '快速模板' })).toBeVisible() await expect(fixture.page.getByRole('combobox', { name: '快速模板' })).toBeVisible()
// Pin the screenshot after fill() auto-scroll to avoid half-pixel capture drift.
await fixture.page
.locator('.settings-page-scroll')
.evaluate((element) => (element.scrollTop = 396))
expect( expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth) await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true) ).toBe(true)