Anchored Settings Shell
錨點設定頁框架:左側 scroll-spy 導覽、右側兩欄設定區塊,指示器平滑跟隨捲動,未儲存變更時儲存列以彈簧滑入。
載入預覽⋯
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<AnchoredSettingsShell />
安裝
npx shadcn@latest add https://webberui.com/r/anchored-settings-shell.json或在 components.json 設定 registries 後,改用 @webberui/anchored-settings-shell 安裝。
安裝依賴後,從 registry JSON(/r/anchored-settings-shell.json 的 files[0].content)複製 anchored-settings-shell.tsx 原始碼到你的 components/ui/ 目錄:
npm install motion clsx tailwind-merge lucide-react使用
import {
AnchoredSettingsShell,
SettingsSection,
SettingsRow,
} from "@/components/ui/anchored-settings-shell";
import { Bell, User } from "lucide-react";
export function Example() {
const [dirty, setDirty] = React.useState(false);
return (
<AnchoredSettingsShell
dirty={dirty}
onSave={() => setDirty(false)}
onDiscard={() => setDirty(false)}
>
<SettingsSection id="profile" label="個人資料" icon={User} description="公開身分與聯絡方式">
<SettingsRow label="顯示名稱" htmlFor="name">
<input id="name" onChange={() => setDirty(true)} />
</SettingsRow>
</SettingsSection>
<SettingsSection id="notifications" label="通知" icon={Bell}>
<SettingsRow label="產品更新">{/* ...控制項 */}</SettingsRow>
</SettingsSection>
</AnchoredSettingsShell>
);
}導覽項目由 SettingsSection 子元件的 id、label、icon 自動推導,順序與內容一致。
非受控模式
省略 dirty prop 時改用內部狀態,於表單欄位的 onChange 呼叫 useSettingsDirty() 回傳的函式即可標記未儲存變更;onSave/onDiscard 後會自動重置。
import { useSettingsDirty } from "@/components/ui/anchored-settings-shell";
function NameField() {
const markDirty = useSettingsDirty();
return <input onChange={markDirty} />;
}捲動容器
若設定頁位於巢狀 overflow-y-auto 容器內,將該容器的 ref 傳入 container,scroll-spy 便會以該容器為捲動根。省略時以視窗為捲動容器。
Props
AnchoredSettingsShell
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
children | ReactNode | — | 放入 SettingsSection 子元件 |
dirty | boolean | — | 受控:是否有未儲存變更;省略則用內部非受控狀態 |
onSave | () => void | Promise<void> | — | 按下儲存時觸發,Promise pending 期間顯示載入狀態 |
onDiscard | () => void | — | 按下捨棄時觸發 |
container | RefObject<HTMLElement | null> | — | 巢狀捲動容器的 ref,作為 scroll-spy 根 |
dirtyMessage | string | "你有尚未儲存的變更" | 儲存列提示文字 |
saveLabel | string | "儲存變更" | 儲存按鈕文字 |
discardLabel | string | "捨棄" | 捨棄按鈕文字 |
navLabel | string | "設定區塊" | 錨點導覽的無障礙標籤 |
SettingsSection
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
id | string | — | 區塊錨點 id,導覽項連結至此 |
label | ReactNode | — | 區塊標題,同時作為導覽項文字 |
icon | LucideIcon | — | 導覽項圖示 |
description | ReactNode | — | 標題下方說明文字 |
children | ReactNode | — | 區塊內容(表單欄位等) |
SettingsRow
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
label | ReactNode | — | 欄位標籤 |
hint | ReactNode | — | 標籤下方輔助說明 |
htmlFor | string | — | 綁定控制項 id 的 label for |
children | ReactNode | — | 右側控制項 |
細節
- 導覽指示器由
useSpring驅動top/height,切換區塊時在導覽項之間連續滑動。 - scroll-spy 以
IntersectionObserver取樣各區塊可見比例,選可見度最高者為目前焦點,同時支援視窗與巢狀容器兩種捲動根。 - 未儲存變更時底部儲存列以彈簧滑入,
sticky bottom定位使其始終貼齊捲動視區底緣。
可及性
- 導覽為
<nav>+ 錨點連結,目前區塊標記aria-current="location";點擊平滑捲動至對應區塊。 - 有未儲存變更時支援
⌘/Ctrl+S快捷鍵觸發儲存。 - 儲存中會停用按鈕並顯示轉圈載入圖示,避免重複送出。
- 開啟「減少動態效果」時,指示器與儲存列改為即時定位、捲動改為
auto,不影響版面與功能。 - 每個區塊以
aria-labelledby對應其標題,切換開關使用role="switch"與aria-checked。