fix(web): settings page layout pass (FE-7)
- Section header (h2 + description) and section blocks as h3; unified block padding and title size - Global settings forms use label-left / control-right rows, stacked below md; control width fixed by type (number short, text/select medium, wide/textarea full) - Per-block SaveBar pinned at the block end with dirty hint; naming template saves moved from the header to the bar - Console shell uses overflow-x-clip so the sticky header and sticky section nav actually stick; anchor jumps re-align while async content loads and land below the header - Section content uses a single minmax(0,1fr) column so wide tables scroll inside their card instead of widening the page - useSettings fills missing keys from DEFAULT_SETTINGS so an older backend no longer crashes the metadata section Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
04cffcd1f1
commit
639fdeacae
13 files changed
+254
-111
No files matched your search
@@ -5,7 +5,13 @@ import { cn } from "~/lib/utils.ts";
|
||||
/** 分区内锚点 id:`<分区>-<节>`,URL hash 写作 `#<分区>/<节>` */
|
||||
export const anchorId = (section: string, anchor: string) => `settings-${section}-${anchor}`;
|
||||
|
||||
/** 设置分区里的一节:标题 + 一句说明 + 未保存提示 + 内容;带锚点 */
|
||||
/** 节卡片内边距;SaveBar 用同一组负边距贴满卡片底边 */
|
||||
const SLAB_PAD = "p-5 sm:p-6";
|
||||
|
||||
/**
|
||||
* 设置分区里的一节:标题(h3)+ 一句说明 + 未保存提示 + 内容;带锚点。
|
||||
* 层级:页面 h1「设置」→ 分区 h2 → 节 h3。节内的保存放在节尾的 SaveBar,不放标题行。
|
||||
*/
|
||||
export function SettingsBlock({
|
||||
section,
|
||||
anchor,
|
||||
@@ -21,6 +27,7 @@ export function SettingsBlock({
|
||||
title: string;
|
||||
description?: ReactNode;
|
||||
dirty?: boolean;
|
||||
/** 标题行右侧:只放"添加 / 打开"这类入口,保存用 SaveBar */
|
||||
actions?: ReactNode;
|
||||
/** 不包卡片(内容自带 DataTable 卡片时用) */
|
||||
plain?: boolean;
|
||||
@@ -28,18 +35,33 @@ export function SettingsBlock({
|
||||
}) {
|
||||
const id = anchorId(section, anchor);
|
||||
return (
|
||||
<section id={id} aria-labelledby={`${id}-title`} className="scroll-mt-24">
|
||||
<div className="mb-3 flex flex-wrap items-end justify-between gap-3">
|
||||
<section id={id} aria-labelledby={`${id}-title`} className="scroll-mt-20">
|
||||
<div className="mb-3 flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4">
|
||||
<div className="min-w-0">
|
||||
<h2 id={`${id}-title`} className="flex items-center gap-2 text-body font-semibold text-foreground">
|
||||
<h3 id={`${id}-title`} className="flex items-center gap-2 text-lead font-semibold text-foreground">
|
||||
{title}
|
||||
{dirty ? <StatusBadge tone="warning">未保存</StatusBadge> : null}
|
||||
</h2>
|
||||
{description ? <p className="mt-0.5 max-w-2xl text-caption text-muted-foreground">{description}</p> : null}
|
||||
</h3>
|
||||
{description ? <p className="mt-1 max-w-2xl text-caption text-muted-foreground">{description}</p> : null}
|
||||
</div>
|
||||
{actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null}
|
||||
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div> : null}
|
||||
</div>
|
||||
<div className={cn(!plain && "pg-slab min-w-0 rounded-xl p-5")}>{children}</div>
|
||||
<div className={cn(!plain && `pg-slab min-w-0 rounded-xl ${SLAB_PAD}`)}>{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/** 节尾保存条:贴满卡片底边,左侧脏状态提示,右侧操作(主操作放最右) */
|
||||
export function SaveBar({ dirty, note, children }: { dirty?: boolean; note?: ReactNode; children: ReactNode }) {
|
||||
return (
|
||||
<div className="-mx-5 mt-5 -mb-5 flex flex-wrap items-center justify-end gap-2 rounded-b-xl border-t border-border/60 px-5 py-3 sm:-mx-6 sm:-mb-6 sm:px-6">
|
||||
<p
|
||||
className={cn("mr-auto min-w-0 text-meta", dirty ? "text-warning" : "text-muted-foreground")}
|
||||
aria-live="polite"
|
||||
>
|
||||
{dirty ? "有未保存的修改" : (note ?? "")}
|
||||
</p>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user