mirror of
https://github.com/dataelement/bisheng.git
synced 2026-08-28 17:14:44 +08:00
perf(frontend): drop frosted glass everywhere and write the rule down
A customer on 信保安全浏览器 (Chromium 108, no GPU acceleration) had daily chat stall on scroll and even on mouse movement. Forcing `backdrop-filter: none` from the console fixed it outright, which pinned the cause: every such element gets its own compositing layer and re-snapshots + re-blurs its backdrop each frame, and that machine rasterizes it on the CPU. The cost is per element, not per radius — a 4px blur on a 24px button costs the same order as a full-screen one. What made it fatal was multiplication: the per-message action buttons carry one each, so a long conversation ran dozens of blur layers, each with a `hover:` transition that repaints them on mouse-over. Removes all 40 occurrences (client 33, platform 7). Most were invisible anyway — their backgrounds sit at 70-100% opacity, where the blur contributes nothing. Two spellings were missed by a first pass that grepped only `backdrop-blur`: the arbitrary-property form `[backdrop-filter:blur(8px)]` in the notification dialog, and one inside an `@apply` in platform's applies.css. The rule is recorded where each audience will meet it: the design spec gets the reasoning plus the alternatives to reach for (translucent fill, noise texture, pre-blurred asset for static backdrops), root AGENTS.md gets the hard constraint, and the 总纲 hard-rules list gets a one-line pointer. Not duplicated into packages/ui/AGENTS.md — the rule spans both apps and the library, so it belongs at the root, and the library had no occurrences to begin with. Both apps typecheck clean.
This commit is contained in:
@@ -50,6 +50,7 @@ Two React apps that **must not be mixed**. Per-app rules auto-load from each sub
|
||||
- **Never** introduce new UI or state-management libraries.
|
||||
- All code comments in English.
|
||||
- 403 handled automatically by response interceptors — never add 403 branches in business code.
|
||||
- **No frosted glass by default** — no `backdrop-filter` / `backdrop-blur-*`, including arbitrary values (`backdrop-blur-[4px]`), variant prefixes (`hover:backdrop-blur-sm`) and the arbitrary-property form `[backdrop-filter:blur(…)]`. Every such element gets its own compositing layer and re-snapshots + re-blurs its backdrop each frame; without GPU acceleration — the norm on 信创 machines — that runs on the CPU. A customer on 信保安全浏览器 (Chromium 108) had scrolling *and mouse movement* stall in daily chat; forcing `backdrop-filter: none` fixed it outright. Cost is **per element, not per radius**: a 4px blur on a 24px button costs the same order as a full-screen one, and the per-message action buttons multiplied it by conversation length. Use a translucent background instead (`bg-white/80`, `bg-black/40`) — past ~70% opacity the blur was invisible anyway. **Only exception**, and it must be justified: a full-screen overlay of which at most one exists at a time, verified on a 信创 browser. Never on anything that scales with content (list rows, message bubbles, cards, notification items). Rationale + alternatives: `packages/ui/docs/基础-阴影与圆角规范.mdx` §3.
|
||||
- **i18n**: no hardcoded Chinese in source (lint-enforced; legacy frozen). New keys ship all three languages (zh-Hans/en/ja) in the same PR. Error-code copy lives ONLY in `src/frontend/packages/locales` (`api_errors` domain — platform addresses `api_errors:<code>`, client `api_errors.<code>`); its generated artifacts (`platform/public/locales/*/api_errors.json`, `client/src/locales/*/api_errors.gen.json`) are never edited by hand (CI-checked). CI also runs `pnpm check-i18n` — key parity across languages + backend error-code coverage; legacy drift is frozen in `scripts/i18n-baseline.json` (shrink-only, `--update-baseline` after healing). Legacy hardcoded Chinese is paid down by whoever touches the file: when editing a file with frozen violations, extract its Chinese strings to i18n (`/i18n-localizer`) in the same change. See `packages/locales/README.md`.
|
||||
- **Quality gate (CI-enforced, `frontend-quality.yml`)**: `pnpm lint` + `pnpm typecheck` (run from `src/frontend/`) must pass. Legacy violations are frozen — ESLint in each app's `eslint-suppressions.json`, TS strict via `// @ts-strict-ignore` file headers — and may only shrink: never hand-edit the suppressions file, never add `@ts-strict-ignore` to a new file. After fixing violations in a file, run `pnpm lint:prune` (per app) and delete its `@ts-strict-ignore` header if it now passes strict.
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ export const ConfirmProvider = ({ children }: { children: React.ReactNode }) =>
|
||||
<AlertDialogFooter className="w-full flex-row gap-2 sm:space-x-0">
|
||||
{!options.hideCancel && <AlertDialogCancel
|
||||
onClick={handleCancel}
|
||||
className="mt-0 h-8 flex-1 rounded-md border-[#ebecf0] bg-white/50 px-4 text-sm font-normal text-[#070038] backdrop-blur-[4px] hover:bg-[#f7f8fa] focus:ring-0 focus:ring-offset-0 focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 sm:mt-0 sm:flex-none"
|
||||
className="mt-0 h-8 flex-1 rounded-md border-[#ebecf0] bg-white/50 px-4 text-sm font-normal text-[#070038] hover:bg-[#f7f8fa] focus:ring-0 focus:ring-offset-0 focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 sm:mt-0 sm:flex-none"
|
||||
>
|
||||
{options.cancelText || defaultCancel}
|
||||
</AlertDialogCancel>}
|
||||
|
||||
@@ -462,7 +462,7 @@ export default function AiChatMessages({
|
||||
<button
|
||||
type="button"
|
||||
onClick={scrollToBottom}
|
||||
className="flex items-center h-8 justify-center gap-2 rounded-md border border-[#EBECF0] bg-white/80 backdrop-blur-[4px] px-2.5 text-sm leading-5 text-neutral-800 hover:bg-white/90 transition-colors"
|
||||
className="flex items-center h-8 justify-center gap-2 rounded-md border border-[#EBECF0] bg-white/80 px-2.5 text-sm leading-5 text-neutral-800 hover:bg-white/90 transition-colors"
|
||||
>
|
||||
<ArrowDownIcon size={16} />
|
||||
<span className="text-sm">回到底部</span>
|
||||
|
||||
@@ -193,7 +193,7 @@ function CopyButton({ text }: { text: string }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="flex size-6 items-center justify-center rounded-md backdrop-blur-[4px] transition-colors hover:bg-[#F7F7F7]"
|
||||
className="flex size-6 items-center justify-center rounded-md transition-colors hover:bg-[#F7F7F7]"
|
||||
title={localize('com_ui_copy')}
|
||||
aria-label={localize('com_ui_copy')}
|
||||
>
|
||||
@@ -791,7 +791,7 @@ function AssistantBubble({
|
||||
/>
|
||||
)}
|
||||
<TextToSpeechButton
|
||||
className="flex size-6 items-center justify-center rounded-md backdrop-blur-[4px] transition-colors hover:bg-[#F7F7F7]"
|
||||
className="flex size-6 items-center justify-center rounded-md transition-colors hover:bg-[#F7F7F7]"
|
||||
messageId={message.messageId || ""}
|
||||
text={regularContent}
|
||||
/>
|
||||
|
||||
@@ -58,10 +58,9 @@ export default function DragDropOverlay() {
|
||||
return (
|
||||
<div
|
||||
className="bg-surface-primary/85 fixed inset-0 z-[9999] flex flex-col items-center justify-center
|
||||
gap-2 text-text-primary
|
||||
backdrop-blur-[4px] transition-all duration-200
|
||||
gap-2 text-text-primary transition-all duration-200
|
||||
ease-in-out animate-in fade-in
|
||||
zoom-in-95 hover:backdrop-blur-sm"
|
||||
zoom-in-95"
|
||||
>
|
||||
<DragDropOverlayIcon />
|
||||
<h3>{localize('com_addAnything')}</h3>
|
||||
|
||||
@@ -59,7 +59,7 @@ export function ExportSelectionButton({
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
'flex size-6 items-center justify-center rounded-md backdrop-blur-[4px] transition-colors hover:bg-[#F7F7F7]',
|
||||
'flex size-6 items-center justify-center rounded-md transition-colors hover:bg-[#F7F7F7]',
|
||||
active && 'bg-[#F0F0F0]',
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||
<div className={cn('fixed inset-0 flex w-screen items-center justify-center p-4')}>
|
||||
<DialogPanel
|
||||
className={cn(
|
||||
'min-h-[600px] overflow-hidden rounded-xl rounded-b-lg bg-background pb-6 shadow-2xl backdrop-blur-2xl animate-in sm:rounded-2xl touch-desktop:min-h-[373px] touch-desktop:w-[680px]',
|
||||
'min-h-[600px] overflow-hidden rounded-xl rounded-b-lg bg-background pb-6 shadow-2xl animate-in sm:rounded-2xl touch-desktop:min-h-[373px] touch-desktop:w-[680px]',
|
||||
)}
|
||||
>
|
||||
<DialogTitle
|
||||
|
||||
@@ -118,7 +118,7 @@ const BackupCodesItem: React.FC = () => {
|
||||
});
|
||||
document.dispatchEvent(announcement);
|
||||
}}
|
||||
className={`flex flex-col rounded-xl border p-4 backdrop-blur-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary ${
|
||||
className={`flex flex-col rounded-xl border p-4 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary ${
|
||||
isUsed
|
||||
? 'border-red-200 bg-red-50/80 dark:border-red-800 dark:bg-red-900/20'
|
||||
: 'border-green-200 bg-green-50/80 dark:border-green-800 dark:bg-green-900/20'
|
||||
|
||||
@@ -1080,7 +1080,7 @@ export function NotificationsDialog({
|
||||
onClick={() => setOnlyUnread((v) => !v)}
|
||||
className={
|
||||
onlyUnread
|
||||
? "h-8 rounded-md border border-transparent bg-[rgb(var(--brand-500)/0.2)] px-3 py-0 text-[14px] font-normal leading-none text-blue-500 [backdrop-filter:blur(4px)] hover:bg-[rgb(var(--brand-500)/0.28)] hover:text-blue-600 active:translate-y-0"
|
||||
? "h-8 rounded-md border border-transparent bg-[rgb(var(--brand-500)/0.2)] px-3 py-0 text-[14px] font-normal leading-none text-blue-500 hover:bg-[rgb(var(--brand-500)/0.28)] hover:text-blue-600 active:translate-y-0"
|
||||
: "h-8 rounded-md border border-[#e5e6eb] px-3 py-0 text-[14px] font-normal leading-none text-[#4e5969] hover:bg-[#f7f8fa] active:translate-y-0"
|
||||
}
|
||||
>
|
||||
@@ -1093,7 +1093,7 @@ export function NotificationsDialog({
|
||||
handleMarkAllAsRead();
|
||||
}}
|
||||
variant="outline"
|
||||
className="h-8 rounded-md border-transparent bg-[#F8F8F8] px-3 py-0 text-[14px] font-normal leading-none text-[#4e5969] [backdrop-filter:blur(4px)] hover:bg-[#f0f0f0] active:translate-y-0"
|
||||
className="h-8 rounded-md border-transparent bg-[#F8F8F8] px-3 py-0 text-[14px] font-normal leading-none text-[#4e5969] hover:bg-[#f0f0f0] active:translate-y-0"
|
||||
>
|
||||
{localize("com_notifications_mark_all_read")}
|
||||
</Button>
|
||||
@@ -1122,8 +1122,8 @@ export function NotificationsDialog({
|
||||
onClick={() => setOnlyUnread((v) => !v)}
|
||||
className={
|
||||
onlyUnread
|
||||
? "h-8 shrink-0 rounded-md border border-transparent bg-[rgb(var(--brand-500)/0.2)] px-4 py-[5px] text-[14px] font-normal text-blue-500 [backdrop-filter:blur(8px)]"
|
||||
: "h-8 shrink-0 rounded-md border border-[#EBECF0] bg-white/50 px-4 py-[5px] text-[14px] font-normal text-[#212121] [backdrop-filter:blur(8px)]"
|
||||
? "h-8 shrink-0 rounded-md border border-transparent bg-[rgb(var(--brand-500)/0.2)] px-4 py-[5px] text-[14px] font-normal text-blue-500"
|
||||
: "h-8 shrink-0 rounded-md border border-[#EBECF0] bg-white/50 px-4 py-[5px] text-[14px] font-normal text-[#212121]"
|
||||
}
|
||||
>
|
||||
{localize("com_notifications_unread_only")}
|
||||
@@ -1135,7 +1135,7 @@ export function NotificationsDialog({
|
||||
handleMarkAllAsRead();
|
||||
}}
|
||||
variant="outline"
|
||||
className="h-8 shrink-0 rounded-md border-transparent bg-[#F8F8F8] px-4 py-[5px] text-[14px] font-normal text-[#212121] [backdrop-filter:blur(8px)]"
|
||||
className="h-8 shrink-0 rounded-md border-transparent bg-[#F8F8F8] px-4 py-[5px] text-[14px] font-normal text-[#212121]"
|
||||
>
|
||||
{localize("com_notifications_mark_all_read")}
|
||||
</Button>
|
||||
|
||||
@@ -24,7 +24,7 @@ const AlertDialogOverlay = React.forwardRef<
|
||||
>(({ className = '', ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-[110] bg-gray-500/90 backdrop-blur-md transition-opacity animate-in fade-in dark:bg-gray-800/90',
|
||||
'fixed inset-0 z-[110] bg-gray-500/90 transition-opacity animate-in fade-in dark:bg-gray-800/90',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -39,7 +39,7 @@ const AnimatedSearchInput = ({
|
||||
placeholder={placeholder}
|
||||
className={`
|
||||
peer relative z-20 w-full rounded-lg bg-surface-secondary px-10
|
||||
py-2 outline-none ring-0 backdrop-blur-sm transition-all
|
||||
py-2 outline-none ring-0 transition-all
|
||||
duration-500 ease-in-out placeholder:text-gray-400
|
||||
focus:outline-none focus:ring-0
|
||||
`}
|
||||
|
||||
@@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-[100] bg-black/40 backdrop-blur-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"fixed inset-0 z-[100] bg-black/40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -122,7 +122,7 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
|
||||
<div className="flex h-auto gap-2 max-sm:w-full max-sm:flex-col sm:flex-row">
|
||||
{buttons != null ? buttons : null}
|
||||
{showCancelButton && (
|
||||
<OGDialogClose className="flex h-auto items-center justify-center rounded-md border border-[#ebecf0] bg-white/50 px-4 py-[5px] text-sm font-normal text-[#070038] backdrop-blur-[4px] transition-colors hover:bg-[#f7f8fa] focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 dark:border-gray-600 dark:bg-transparent dark:text-gray-100 dark:hover:bg-gray-700 max-sm:order-last max-sm:w-full sm:order-first">
|
||||
<OGDialogClose className="flex h-auto items-center justify-center rounded-md border border-[#ebecf0] bg-white/50 px-4 py-[5px] text-sm font-normal text-[#070038] transition-colors hover:bg-[#f7f8fa] focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 dark:border-gray-600 dark:bg-transparent dark:text-gray-100 dark:hover:bg-gray-700 max-sm:order-last max-sm:w-full sm:order-first">
|
||||
{Cancel}
|
||||
</OGDialogClose>
|
||||
)}
|
||||
|
||||
@@ -39,7 +39,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-gray-500/90 backdrop-blur-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 dark:bg-gray-800/90',
|
||||
'fixed inset-0 z-50 bg-gray-500/90 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 dark:bg-gray-800/90',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -209,7 +209,7 @@ export default function ArticlePage() {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex size-9 items-center justify-center rounded-xl border border-black/5 bg-white/70 text-[#212121] shadow-[0_2px_8px_rgba(0,0,0,0.06)] backdrop-blur-md transition-colors hover:bg-white/85"
|
||||
className="inline-flex size-9 items-center justify-center rounded-xl border border-black/5 bg-white/70 text-[#212121] shadow-[0_2px_8px_rgba(0,0,0,0.06)] transition-colors hover:bg-white/85"
|
||||
aria-label={localize("com_subscription.channel_settings")}
|
||||
>
|
||||
<Outlined.MoreCircle className="size-5" />
|
||||
@@ -218,7 +218,7 @@ export default function ArticlePage() {
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={6}
|
||||
className="z-[120] w-[160px] space-y-1 border border-black/5 bg-white/85 p-1 backdrop-blur-md"
|
||||
className="z-[120] w-[160px] space-y-1 border border-black/5 bg-white/85 p-1"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="flex w-full cursor-pointer items-center gap-2 px-2 py-[5px] text-sm text-[#212121]"
|
||||
|
||||
@@ -234,7 +234,7 @@ export function FilterConditionEditor({
|
||||
<button
|
||||
type="button"
|
||||
onClick={addCondition}
|
||||
className="inline-flex items-center gap-[4px] rounded-md border border-[#EBECF0] bg-white/50 backdrop-blur-[4px] px-[12px] py-[3px] text-[14px] leading-[22px] text-[#212121] hover:bg-[#F8F8F8]"
|
||||
className="inline-flex items-center gap-[4px] rounded-md border border-[#EBECF0] bg-white/50 px-[12px] py-[3px] text-[14px] leading-[22px] text-[#212121] hover:bg-[#F8F8F8]"
|
||||
title={localize("com_subscription.add_condition")}
|
||||
>
|
||||
<SquarePlus className="size-4 shrink-0 text-[#212121]" strokeWidth={1.5} />
|
||||
@@ -341,7 +341,7 @@ export function FilterConditionEditor({
|
||||
<button
|
||||
type="button"
|
||||
onClick={addCondition}
|
||||
className="inline-flex items-center gap-[4px] rounded-md border border-[#EBECF0] bg-white/50 backdrop-blur-[4px] px-[12px] py-[3px] text-[14px] leading-[22px] text-[#212121] hover:bg-[#F8F8F8]"
|
||||
className="inline-flex items-center gap-[4px] rounded-md border border-[#EBECF0] bg-white/50 px-[12px] py-[3px] text-[14px] leading-[22px] text-[#212121] hover:bg-[#F8F8F8]"
|
||||
title={localize("com_subscription.add_condition")}
|
||||
>
|
||||
<SquarePlus className="size-4 shrink-0 text-[#212121]" strokeWidth={1.5} />
|
||||
|
||||
@@ -89,7 +89,7 @@ export function ConfirmDialogSection() {
|
||||
[
|
||||
'遮罩',
|
||||
<>
|
||||
<code>bg-gray-500/90</code> + <code>backdrop-blur-md</code>
|
||||
<code>bg-gray-500/90</code> + <code></code>
|
||||
</>,
|
||||
'灰底毛玻璃',
|
||||
],
|
||||
|
||||
@@ -78,7 +78,7 @@ export function ModalSection() {
|
||||
rows={[
|
||||
['圆角', <code key="c">rounded-2xl(16px)</code>, '移动端是否保留直角/贴底待定'],
|
||||
['内边距', <code key="c">p-5(20px)· 区块间 gap-4</code>, ''],
|
||||
['遮罩', <><code>bg-gray-500/90</code> + <code>backdrop-blur-md</code></>, '灰底毛玻璃'],
|
||||
['遮罩', <><code>bg-gray-500/90</code> + <code></code></>, '灰底毛玻璃'],
|
||||
['边框 / 阴影', <code key="c">border #ebebeb + 淡投影</code>, ''],
|
||||
['标题', <code key="c">text-base font-medium leading-6</code>, ''],
|
||||
['取消 / 确认按钮', '白底描边 + danger/primary 两档', '与二次确认一致'],
|
||||
|
||||
@@ -113,7 +113,7 @@ export function SearchOverlay({
|
||||
}, [filteredResults, hasMore, isLoadingMore, loadMoreItems, itemsPerLoad])
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 bg-background backdrop-blur-sm z-50">
|
||||
<div className="absolute inset-0 bg-background z-50">
|
||||
<div ref={scrollContainerRef} className="h-full overflow-auto">
|
||||
<div className="container mx-auto px-6 py-6">
|
||||
{loading && displayedResults.length === 0 ? (
|
||||
|
||||
@@ -112,7 +112,7 @@ export default function AppCenter() {
|
||||
const exploreLink = (
|
||||
<Link
|
||||
to="/apps/explore"
|
||||
className="backdrop-blur-[4px] flex shrink-0 items-center justify-center gap-[6px] rounded-lg px-[10px] py-[6px] transition-colors fine-pointer:hover:bg-gray-50"
|
||||
className="flex shrink-0 items-center justify-center gap-[6px] rounded-lg px-[10px] py-[6px] transition-colors fine-pointer:hover:bg-gray-50"
|
||||
>
|
||||
<ChannelBlocksArrowsIcon className="size-4 text-blue-500" />
|
||||
<span className="font-['PingFang_SC'] text-[#212121] text-[12px] leading-[20px] whitespace-nowrap">
|
||||
|
||||
@@ -315,7 +315,7 @@ export default function FilePreviewPage() {
|
||||
type="button"
|
||||
onClick={handleDownloadFile}
|
||||
aria-label={localize("com_knowledge.download_file")}
|
||||
className="fixed right-4 top-[calc(env(safe-area-inset-top,0px)+12px)] z-10 inline-flex size-9 items-center justify-center rounded-xl border border-black/5 bg-white/70 text-[#212121] shadow-[0_2px_8px_rgba(0,0,0,0.06)] backdrop-blur-md transition-colors hover:bg-white/85"
|
||||
className="fixed right-4 top-[calc(env(safe-area-inset-top,0px)+12px)] z-10 inline-flex size-9 items-center justify-center rounded-xl border border-black/5 bg-white/70 text-[#212121] shadow-[0_2px_8px_rgba(0,0,0,0.06)] transition-colors hover:bg-white/85"
|
||||
>
|
||||
<Outlined.Download className="size-5" />
|
||||
</button>
|
||||
|
||||
@@ -381,8 +381,8 @@ export function MediaPlayer({ kind, src, allowDownload = false, onDownload }: Me
|
||||
/* Menus float over the media, so they stay translucent and open upward
|
||||
rather than covering the stage below the bar. */
|
||||
const menuSurfaceClass = onDarkStage
|
||||
? "border-0 bg-black/75 backdrop-blur-md"
|
||||
: "bg-white/85 backdrop-blur-md";
|
||||
? "border-0 bg-black/75"
|
||||
: "bg-white/85";
|
||||
/* The shared menu item forces a dark label while highlighted, which is
|
||||
unreadable on the dark surface — keep it white on hover/focus too. */
|
||||
const menuItemClass = onDarkStage
|
||||
|
||||
@@ -322,7 +322,7 @@ export function EditTagsModal({
|
||||
<DialogFooter className="flex h-14 shrink-0 items-center justify-end gap-3 border-none px-5 py-3 max-md:!mt-auto max-md:!h-auto max-md:!flex-row max-md:!justify-stretch max-md:border-t max-md:border-[#ECECEC] max-md:px-4 max-md:py-3 sm:space-x-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 min-w-[60px] rounded-md border-[#ebecf0] bg-white/50 px-4 font-normal text-[#070038] backdrop-blur-[8px] hover:bg-white/70 max-md:flex-1"
|
||||
className="h-8 min-w-[60px] rounded-md border-[#ebecf0] bg-white/50 px-4 font-normal text-[#070038] hover:bg-white/70 max-md:flex-1"
|
||||
onClick={handleClose}
|
||||
>
|
||||
{localize("com_knowledge.cancel")}</Button>
|
||||
|
||||
@@ -710,7 +710,7 @@ export default function Knowledge() {
|
||||
{/* Drag and Drop Overlay */}
|
||||
{isDragging && (
|
||||
<div
|
||||
className={`absolute inset-0.5 z-[100] rounded-[12px] backdrop-blur-[16px] flex flex-col items-center justify-center pointer-events-none transition-all duration-300 ${dragError ? "border border-dashed border-red-500 bg-[rgba(255,236,232,0.7)]" : "border border-dashed bg-[rgba(255,255,255,0.7)]"}`}
|
||||
className={`absolute inset-0.5 z-[100] rounded-[12px] flex flex-col items-center justify-center pointer-events-none transition-all duration-300 ${dragError ? "border border-dashed border-red-500 bg-[rgba(255,236,232,0.7)]" : "border border-dashed bg-[rgba(255,255,255,0.7)]"}`}
|
||||
>
|
||||
<div className={`flex flex-col items-center justify-center p-8 rounded-2xl ${dragError ? "bg-transparent" : "bg-white/50"}`}>
|
||||
{dragError ? (
|
||||
|
||||
@@ -127,7 +127,7 @@ export default function RouteErrorBoundary() {
|
||||
role="alert"
|
||||
className="flex min-h-screen flex-col items-center justify-center bg-surface-primary bg-gradient-to-br"
|
||||
>
|
||||
<div className="bg-surface-primary/60 mx-4 w-11/12 max-w-4xl rounded-2xl border border-border-light p-8 shadow-2xl backdrop-blur-xl">
|
||||
<div className="bg-surface-primary/60 mx-4 w-11/12 max-w-4xl rounded-2xl border border-border-light p-8 shadow-2xl">
|
||||
<h2 className="mb-6 text-center text-3xl font-medium tracking-tight text-text-primary">
|
||||
Oops! Something Unexpected Occurred
|
||||
</h2>
|
||||
|
||||
@@ -148,6 +148,7 @@ npm run dev # 开发服务器,端口 4001
|
||||
- UI 组件在 `~/components/ui/`;**不引入新 UI 库**。
|
||||
- 品牌色蓝⇄绿主题:**不硬编码品牌 hex**,用 `blue-*` 类(已重指向 `--brand-*`)或 `rgb(var(--brand-NNN))`。
|
||||
- 语义色(成功/危险/警告)不参与换肤。
|
||||
- **默认不用毛玻璃 `backdrop-filter`**:信创环境无硬件加速,每个毛玻璃元素每帧都要重做背景快照与模糊,已导致客户端滚动卡死。改用半透明纯色;例外仅限同时最多一个的全屏遮罩,见 [基础-阴影与圆角规范.mdx](基础-阴影与圆角规范.mdx) §3。
|
||||
- 详见 `src/frontend/client/CLAUDE.md`。
|
||||
|
||||
## 七、图标规则(设计师偏好,优先级高于项目历史习惯)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
| 色彩(品牌/中性/语义/标签/插画) | [基础-色彩规范.md](基础-色彩规范.md) | ✅ v1 定稿,token 已落地 |
|
||||
| 字体(字体栈/九档字号/字重) | [基础-字体规范.md](基础-字体规范.md) | ✅ v1,token 已落地 |
|
||||
| 多端适配(双判定口径 + 4 原则) | [基础-多端适配原则.md](基础-多端适配原则.md) | ✅ v1 |
|
||||
| 阴影与圆角(圆角八档/投影两档) | [基础-阴影与圆角规范.mdx](基础-阴影与圆角规范.mdx) | 🟨 v1 提案(2026-07-23),待设计师验收 → 落 token |
|
||||
| 阴影与圆角与模糊(圆角八档/投影两档/毛玻璃默认禁用) | [基础-阴影与圆角规范.mdx](基础-阴影与圆角规范.mdx) | 🟨 v1 提案(2026-07-23),待设计师验收 → 落 token |
|
||||
| 按钮(双轴类型/尺寸/状态) | [组件-Button按钮.md](组件-Button按钮.md) | ✅ v1 定稿,基准组件已重构 |
|
||||
| 弹窗(遮罩/壳解剖/收敛方向) | [组件-Modal弹窗.md](组件-Modal弹窗.md) | 🟨 进行中,待定夺 |
|
||||
| 间距/层级(本文) | 01-设计规范.md | 🟨 待定 |
|
||||
|
||||
@@ -220,7 +220,44 @@ BISHENG 的投影走「淡」路线:带一点蓝黑色、透明度很低,负
|
||||
| 下拉面板:`shadow-popup`(浮层投影) | `shadow-lg`(Tailwind 纯黑预设) | 一次违反两条规则:投影不在两档内,颜色是纯黑——又重又灰,和蓝黑淡投影混排会打乱层次 |
|
||||
| 静态卡片:只用边框分层 | 页面里每张卡都加投影 | 投影表达的是「浮起的高度」,静态内容浮起来没有意义,还让真正的浮层失去对比 |
|
||||
|
||||
## 3. 使用约定
|
||||
## 3. 模糊与毛玻璃 Blur
|
||||
|
||||
**默认不用毛玻璃。** 想「透出底层」用半透明纯色,想要材质感用半透明加一层很淡的噪点。
|
||||
|
||||
### 3.1 为什么
|
||||
|
||||
毛玻璃(`backdrop-filter`)要求浏览器每一帧都把元素背后的画面截下来、模糊、再合成,而且每个这样的元素都单独占一个合成层。没有硬件加速的机器只能拿 CPU 算 —— 信创环境普遍如此。
|
||||
|
||||
2026-08-04,客户在信保安全浏览器(内核 Chromium 108)上打开日常会话,滚动乃至鼠标移动都卡住;在控制台强制 `backdrop-filter: none` 后立刻恢复。
|
||||
|
||||
代价按**元素个数**算,不按模糊半径算:24px 小按钮上的 4px 模糊,和一整块全屏遮罩是同一量级。当时消息气泡的操作按钮每条回答一组,几十轮会话就是几十个合成层,再叠上 hover 变色 —— 鼠标划过就重绘,重绘就重做模糊。
|
||||
|
||||
### 3.2 用什么替代
|
||||
|
||||
| 想要的效果 | 用这个 | 说明 |
|
||||
|---|---|---|
|
||||
| 透出底层内容 | 半透明纯色 `bg-white/80`、`bg-black/40` | 不透明度到 70% 以上,和真毛玻璃肉眼无差 |
|
||||
| 「玻璃材质」的质感 | 半透明 + 极淡噪点纹理 + 1px 内高光 | 不产生合成层 |
|
||||
| 背景固定的大块区域 | 预先模糊好的图片资源 | 仅限背景不随内容变化时 |
|
||||
|
||||
不要用「复制一份背景再 `filter: blur()`」的老办法 —— 同样要模糊、同样要合成层,还多一份 DOM 要同步,比毛玻璃更贵。
|
||||
|
||||
### 3.3 唯一可申请的例外
|
||||
|
||||
同时最多只存在一个的**全屏遮罩**可以申请保留,但要在信创环境实测通过。
|
||||
|
||||
**数量随内容增长的元素一律禁止** —— 列表项、消息气泡、卡片、通知条里的按钮都不行。上面那次卡死就出在这里。
|
||||
|
||||
### 3.4 正误对照
|
||||
|
||||
✅ 弹窗遮罩 `bg-black/40`,不加模糊
|
||||
✅ 吸顶栏 `bg-white/80`,不加模糊
|
||||
❌ 消息操作按钮 `backdrop-blur-[4px]` —— 数量随会话增长
|
||||
❌ 背景已经 90% 不透明还加 `backdrop-blur-md` —— 看不见,纯烧 CPU
|
||||
|
||||
---
|
||||
|
||||
## 4. 使用约定
|
||||
|
||||
- **圆角与投影一律取表内档位**:出现表外的手写值即视为未走规范;确属例外的,在对应组件文档里写明原因并注明「见阴影与圆角规范」。
|
||||
- **新档位先进规范、再进代码**:要用新的圆角或投影值,先由设计师在本文与 design token 里定档,之后才能出现在代码里,不允许先用后补——这就是本站所说「规范评审」的全部含义:设计师定夺 + 文档与 token 先行。
|
||||
@@ -276,6 +313,7 @@ BISHENG 的投影走「淡」路线:带一点蓝黑色、透明度很低,负
|
||||
5. `design-token.mdx` 增加「阴影 Shadow」节,从 SSOT 渲染(SHADOW 进 cjs 之后做,别手抄值)。
|
||||
6. 待决策(不阻塞 v1):① 两档投影色是否统一为同一蓝黑基色(现浮层 rgba(0,23,66,·) vs 模态 rgba(3,7,117,·),肉眼几乎无差);② `.shadow-stroke`(13 处)/ `shadow-outline`(6 处)两个自定义类的去留;③ 暗色模式是否为投影单独给值(现方案:同值不调);④ 下拉基准(DropdownMenu / Select)是否补 1px 边框(边框已改为非硬性,见 §2.2)。
|
||||
7. 组件-Modal弹窗.md 待决策清单「圆角:弹窗用几 px?」可勾销,标注「2026-07-23 定 16px,见基础-阴影与圆角规范」。
|
||||
8a. §3 毛玻璃的 agent 执行口径:禁用 `backdrop-blur-*`(含 `backdrop-blur-[4px]` 等任意值、`hover:` 等变体前缀)与任意属性写法 `[backdrop-filter:blur(…)]`;CSS 里同样禁 `backdrop-filter` / `-webkit-backdrop-filter`。2026-08-04 已全量清除 client 33 处 + platform 7 处,含两处只在 `@apply` 里出现、grep `backdrop-blur` 抓不到的写法。
|
||||
8. 归并迁移照总纲节奏:设计师逐批点名、每批一笔提交;映射表见附录 A.1,投影迁移优先加 shadow-lg 41 处(下拉/浮层聚集)。
|
||||
|
||||
(sidebar 注册与 01-设计规范 指针已随本次建档完成:rspress.config.ts「设计规范」组新增本页入口,改 config 后需重启 dev 服务。) */}
|
||||
@@ -285,5 +323,6 @@ BISHENG 的投影走「淡」路线:带一点蓝黑色、透明度很低,负
|
||||
| 日期 | 改了什之 | 涉及 | 提交 |
|
||||
|---|---|---|---|
|
||||
| 2026-07-23 | 建档 v1 提案:圆角八档使用规则(控件跟尺寸 4/6/8、容器 12/16/24/32、弹窗定 16px 终结 8vs16、嵌套=外层−内边距、禁任意值);投影两档 popup/modal(取下拉/弹窗基准组件已落地值)+「淡投影+1px 边框」配套规则;全量现状扫描存档(rounded 1429 处 / shadow 260 处,口径见附录 A);01-设计规范 圆角/阴影两节迁出归口本文,总纲看板与 rspress 侧栏同步 | 本文件、01-设计规范.md、00-总纲.md、rspress.config.ts | 待 committer 窗口提交 |
|
||||
| 2026-08-04 | 新增 §3 模糊与毛玻璃:默认禁用 `backdrop-filter`,给出半透明纯色/噪点/预模糊图三条替代,例外仅限同时最多一个的全屏遮罩且需信创实测;原 §3 使用约定顺延为 §4。起因为客户在信保安全浏览器(Chromium 108,无硬件加速)上日常会话滚动与鼠标移动卡死,强制关闭毛玻璃后恢复 | 本文件、根 AGENTS.md、00-总纲.md、01-设计规范.md | 待 committer 窗口提交 |
|
||||
| 2026-07-23 | v1.2 展示层去迁移工程措辞:正文(§1.2/1.3/1.4/1.6/2.1/2.2/3)删「已落地 / 现行基准 / 验收后 / 接线完成前 / 迁移归并 / 存量归并」等迁移状态描述,只留设计规则;迁移事实与归并映射原样保留在隐藏区(附录 A、给实现窗口、本改动记录) | 本文件 | 待 committer 窗口提交 |
|
||||
| 2026-07-23 | v1.1 按设计师 7 条批注修订:① 工作档头补「控件/容器两分法」来源口径;② 新增 §1.4 贴边直角(移动端全屏/贴底容器贴边侧直角,认可审批中心先例,多端适配原则加指针);③ §2.1 说明 token 与 Tailwind 类名同名同物、验收后进 design token 页;④「投影必须搭 1px 边框」降级为非硬性搭配(§2.2 末条),相关 ❌ 例与附录锚点同步改;⑤「只用两档」正文改白话,精确类名清单移隐藏区(给实现窗口 1a);⑥「不用纯黑投影」升级为 §2.2 规则条;⑦ §3「规范评审」写实为「设计师定夺 + 先改规范再写代码」 | 本文件、基础-多端适配原则.md | 待 committer 窗口提交 | */}
|
||||
|
||||
@@ -212,7 +212,7 @@ export function SkillFormDrawer({ open, editing, onOpenChange, onSaved }: SkillF
|
||||
</div>
|
||||
|
||||
{/* footer — actions only */}
|
||||
<div className="shrink-0 flex items-center justify-end gap-3 px-6 py-3.5 border-t bg-background/90 backdrop-blur">
|
||||
<div className="shrink-0 flex items-center justify-end gap-3 px-6 py-3.5 border-t bg-background/90">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>{t('skillManage.form.cancel')}</Button>
|
||||
<Button onClick={handleSave} disabled={saving}>{t('skillManage.form.save')}</Button>
|
||||
</div>
|
||||
|
||||
@@ -58,10 +58,9 @@ export default function DragDropOverlay() {
|
||||
return (
|
||||
<div
|
||||
className="bg-surface-primary/85 fixed inset-0 z-[9999] flex flex-col items-center justify-center
|
||||
gap-2 text-text-primary
|
||||
backdrop-blur-[4px] transition-all duration-200
|
||||
gap-2 text-text-primary transition-all duration-200
|
||||
ease-in-out animate-in fade-in
|
||||
zoom-in-95 hover:backdrop-blur-sm"
|
||||
zoom-in-95"
|
||||
>
|
||||
<DragDropOverlayIcon />
|
||||
<h3>{t('com_addAnything')}</h3>
|
||||
|
||||
@@ -59,7 +59,7 @@ export function ChartContainer({ isPreviewMode, isDark, component }: ChartContai
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 bg-white px-6 py-2 rounded-md backdrop-blur-sm">
|
||||
<div className="relative z-10 bg-white px-6 py-2 rounded-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[#8da9ff] font-medium text-lg tracking-wider break-keep">
|
||||
{t('updatingCharts')}
|
||||
|
||||
@@ -154,7 +154,7 @@ export const ComponentWrapper = memo(({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 bg-background/80 backdrop-blur-sm border border-border shadow-sm hover:bg-accent dark:border-gray-500 dark:text-gray-500"
|
||||
className="h-6 w-6 bg-background/80 border border-border shadow-sm hover:bg-accent dark:border-gray-500 dark:text-gray-500"
|
||||
>
|
||||
<MoreVerticalIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -127,7 +127,7 @@ export default function RouteErrorBoundary() {
|
||||
role="alert"
|
||||
className="flex min-h-screen flex-col items-center justify-center bg-surface-primary bg-gradient-to-br"
|
||||
>
|
||||
<div className="bg-surface-primary/60 mx-4 w-11/12 max-w-4xl rounded-2xl border border-border-light p-8 shadow-2xl backdrop-blur-xl">
|
||||
<div className="bg-surface-primary/60 mx-4 w-11/12 max-w-4xl rounded-2xl border border-border-light p-8 shadow-2xl">
|
||||
<div className='w-20 h-20 mx-auto mb-2 bg-orange-200 rounded-full text-white flex items-center justify-center'>
|
||||
<CircleAlert size={80} />
|
||||
</div>
|
||||
|
||||
@@ -620,7 +620,7 @@
|
||||
}
|
||||
|
||||
.send-message-modal-transition {
|
||||
@apply fixed inset-0 bg-black bg-opacity-80 backdrop-blur-sm transition-opacity
|
||||
@apply fixed inset-0 bg-black bg-opacity-80 transition-opacity
|
||||
}
|
||||
.chat-modal-box {
|
||||
@apply fixed inset-0 z-10 overflow-y-auto
|
||||
|
||||
Reference in New Issue
Block a user