# 设置页

> 用方条控件拼成的完整设置页：分区导航、实时预览、已修改计数、恢复默认与异步保存。

A shadcn/ui block from the [@yiui](https://yiui.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/yiui/settings-01).

- Page, with a live preview: https://shadcn.exchange/r/yiui/settings-01
- Screenshot: https://shadcn.exchange/shot/yiui/settings-01/light.png
- Registry item: https://yiui.dev/r/settings-01.json
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/yiui/settings-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`, `motion`
- Registry items: `button`, `https://yiui.dev/r/value-slider.json`, `https://yiui.dev/r/segmented-control.json`, `https://yiui.dev/r/toggle-switch.json`, `https://yiui.dev/r/tone-picker.json`, `https://yiui.dev/r/control-panel.json`, `https://yiui.dev/r/flow-tone.json`, `item`, `popover`, `sheet`

## Files

### `src/registry/blocks/settings-01/page.tsx`

```tsx
"use client"

import { Settings01 } from "@/components/settings-01/settings-01"

/**
 * 设置页入口：用 onSave 把设置存到你自己的后端。
 * 回调返回 Promise；失败时 throw new Error("中文原因")，原因会显示在底部保存栏上。
 * 想把改动实时应用到页面（例如切换主题），在 onValueChange 里处理。
 */
export default function SettingsPage() {
  return (
    <Settings01
      onSave={async (values) => {
        // TODO: 调用你的接口，例如 await fetch("/api/settings", { method: "PUT", body: JSON.stringify(values) })
        console.info("save settings", values)
        await new Promise((resolve) => setTimeout(resolve, 600))
      }}
    />
  )
}
```

### `src/registry/blocks/settings-01/settings-01.tsx`

```tsx
"use client"

import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import { Check, LoaderCircle, Settings } from "lucide-react"
import { useMemo, useState, type ReactNode } from "react"

import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { useControllableState } from "@/hooks/use-controllable-state"
import { flowToneStyle } from "@/lib/flow-tone"

import { Settings01Changes } from "./changes"
import { AppearanceSection } from "./appearance-section"
import {
  DEFAULT_VALUES,
  SECTION_IDS,
  SECTION_KEYS,
  resolveLabels,
  type Settings01Labels,
} from "./config"
import { EditorSection } from "./editor-section"
import { NotificationsSection } from "./notifications-section"
import { PrivacySection } from "./privacy-section"
import type { SectionProps, SetValue } from "./section-layout"
import { SectionNav, SectionTabs } from "./section-nav"
import { SoundSection } from "./sound-section"
import type { Settings01SectionId, Settings01Values } from "./types"
import { useSaveAction } from "./use-save-action"
import { isSameValue } from "./utils"

const SECTIONS: Record<
  Settings01SectionId,
  (props: SectionProps) => ReactNode
> = {
  appearance: AppearanceSection,
  notifications: NotificationsSection,
  sound: SoundSection,
  editor: EditorSection,
  privacy: PrivacySection,
}

const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
// 未传回调时的示例实现：只在控制台打印，并模拟 0.8 秒的网络请求
async function demoSave(values: Settings01Values) {
  console.info("[settings-01] onSave", values)
  await wait(800)
}

function pickKeys(
  values: Settings01Values,
  keys: readonly (keyof Settings01Values)[],
) {
  return Object.fromEntries(keys.map((key) => [key, values[key]]))
}

/**
 * 设置页：左侧分区导航（窄屏变成顶部分段选择）+ 右侧当前分区的控制面板 + 底部保存栏。
 * 值同时支持受控与非受控；保存回调返回 Promise，组件管理 loading，回调 throw 的 Error.message 会显示在保存栏上。
 */
function Settings01({
  value: valueProp,
  defaultValue,
  defaults,
  onValueChange,
  section: sectionProp,
  defaultSection = "appearance",
  onSectionChange,
  onSave = demoSave,
  labels,
  className,
}: {
  /** 受控的全部设置值 */
  value?: Settings01Values
  /** 非受控时的初始值，没写的项取 `defaults` */
  defaultValue?: Partial<Settings01Values>
  /** 出厂默认值，逐项覆盖 config.ts 的 DEFAULT_VALUES；「已修改」「恢复默认」都以它为准 */
  defaults?: Partial<Settings01Values>
  /** 任何一项变化时回调，参数是变化后的全部设置 */
  onValueChange?: (value: Settings01Values) => void
  /** 受控的当前分区 */
  section?: Settings01SectionId
  defaultSection?: Settings01SectionId
  onSectionChange?: (section: Settings01SectionId) => void
  /** 点击「保存」时调用；失败时 throw new Error("原因")，原因会显示在保存栏 */
  onSave?: (values: Settings01Values) => Promise<void>
  /** 覆盖默认文案，见 config.ts 的 DEFAULT_LABELS */
  labels?: Partial<Settings01Labels>
  className?: string
}) {
  const text = resolveLabels(labels)
  const reducedMotion = useReducedMotion()
  const resolvedDefaults = useMemo<Settings01Values>(
    () => ({ ...DEFAULT_VALUES, ...defaults }),
    [defaults],
  )
  const [values, setValues] = useControllableState<Settings01Values>({
    prop: valueProp,
    defaultProp: { ...resolvedDefaults, ...defaultValue },
    onChange: onValueChange,
  })
  const [section, setSection] = useControllableState<Settings01SectionId>({
    prop: sectionProp,
    defaultProp: defaultSection,
    onChange: onSectionChange,
  })
  // 上一次保存（或首次载入）时的值：和默认值比出「已修改」，和它比出「有未保存的更改」
  const [saved, setSaved] = useState(values)
  const action = useSaveAction(text.saveFailed)

  const set: SetValue = (key, next) => {
    setValues((previous) => ({ ...previous, [key]: next }))
    action.reset()
  }
  const modified = (key: keyof Settings01Values) =>
    !isSameValue(values[key], resolvedDefaults[key])
  const counts = Object.fromEntries(
    SECTION_IDS.map((id) => [id, SECTION_KEYS[id].filter(modified).length]),
  ) as Record<Settings01SectionId, number>
  const changes = SECTION_IDS.flatMap((section) => SECTION_KEYS[section].filter(modified).map((key) => ({ key, section })))
  const total = changes.length
  const dirty = (Object.keys(values) as (keyof Settings01Values)[]).some(
    (key) => !isSameValue(values[key], saved[key]),
  )
  const pending = action.status === "pending"

  const resetSection = (id: Settings01SectionId) => {
    setValues((previous) => ({
      ...previous,
      ...pickKeys(resolvedDefaults, SECTION_KEYS[id]),
    }))
    action.reset()
  }
  const resetAll = () => {
    setValues({ ...resolvedDefaults })
    action.reset()
  }
  const save = async () => {
    const snapshot = values
    if (await action.run(() => onSave(snapshot))) setSaved(snapshot)
  }

  const Section = SECTIONS[section]
  const sectionProps: SectionProps = {
    values,
    labels: text,
    set,
    modified,
    count: counts[section],
    onReset: () => resetSection(section),
    getCode: () =>
      JSON.stringify(pickKeys(values, SECTION_KEYS[section]), null, 2),
  }

  return (
    <div
      data-slot="settings-01"
      className={cn(
        "relative flex min-h-svh flex-col bg-background text-foreground",
        className,
      )}
      // 强调色同时作为 --flow-tone，导航的选中标记等跟着它走
      style={flowToneStyle(values.accent)}
    >
      <header className="border-b border-edge">
        <div className="mx-auto flex w-full max-w-6xl items-center gap-3 px-4 py-4 sm:px-6 lg:border-x lg:border-edge">
          <span
            aria-hidden="true"
            className="flex size-8 shrink-0 items-center justify-center rounded-md border border-edge bg-muted/50"
          >
            <Settings className="size-4" />
          </span>
          <div className="min-w-0">
            <h1 className="text-base leading-tight font-semibold tracking-tight">
              {text.title}
            </h1>
            <p className="truncate text-xs text-muted-foreground">
              {text.description}
            </p>
          </div>
        </div>
      </header>

      <div className="mx-auto flex w-full max-w-6xl flex-1 flex-col md:flex-row lg:border-x lg:border-edge">
        <SectionNav
          section={section}
          onSectionChange={setSection}
          counts={counts}
          labels={text}
        />
        <SectionTabs
          section={section}
          onSectionChange={setSection}
          labels={text}
          tone={values.accent}
        />
        <main className="min-w-0 flex-1 p-4 sm:p-6">
          <AnimatePresence mode="wait" initial={false}>
            <motion.div
              key={section}
              initial={{ opacity: 0, y: reducedMotion ? 0 : 8 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0, y: reducedMotion ? 0 : -8 }}
              transition={{ duration: 0.18 }}
            >
              <Section {...sectionProps} />
            </motion.div>
          </AnimatePresence>
        </main>
      </div>

      <footer
        aria-label={text.footerLabel}
        data-slot="settings-01-footer"
        className="sticky bottom-0 z-20 border-t border-edge bg-background/90 backdrop-blur"
      >
        <div className="mx-auto flex w-full max-w-6xl items-center justify-between gap-3 px-4 py-3 sm:px-6 lg:border-x lg:border-edge">
          <div className="min-w-0 text-sm">
            <Settings01Changes changes={changes} values={values} defaults={resolvedDefaults} labels={text} pending={pending} dirty={dirty} onUndo={(key) => set(key, resolvedDefaults[key])} onReset={resetAll} onSave={save} />
            {/* 状态行固定占一行高度，避免保存时底栏跳动；出错时同一位置换成红色的错误原因 */}
            {action.status === "error" ? (
              <p
                role="alert"
                data-slot="settings-01-error"
                className="min-h-4 truncate text-xs text-destructive"
              >
                {action.error}
              </p>
            ) : (
              <p
                role="status"
                data-slot="settings-01-status"
                className="flex min-h-4 items-center gap-1 truncate text-xs text-muted-foreground"
              >
                {pending ? (
                  text.saving
                ) : action.status === "saved" ? (
                  <>
                    <Check className="size-3.5 shrink-0" />
                    {text.saved}
                  </>
                ) : dirty ? (
                  text.unsaved
                ) : (
                  text.upToDate
                )}
              </p>
            )}
          </div>
          <div className="flex shrink-0 items-center gap-2">
            <Button
              variant="outline"
              disabled={total === 0 || pending}
              onClick={resetAll}
            >
              {text.resetAll}
            </Button>
            <Button disabled={!dirty || pending} onClick={save}>
              {pending ? (
                <LoaderCircle className="animate-spin" data-icon="inline-start" />
              ) : null}
              {pending ? text.saving : text.save}
            </Button>
          </div>
        </div>
      </footer>
    </div>
  )
}

export { Settings01 }
export type { Settings01Labels, Settings01SectionId, Settings01Values }
```

### `src/registry/blocks/settings-01/config.ts`

```ts
import type { Settings01SectionId, Settings01Values } from "./types"

/**
 * 设置页所有用户可见的文案。
 * 含 {count}、{value}、{section} 的文案在渲染时由 formatLabel 填入实际值。
 */
const DEFAULT_LABELS = {
  // 页面
  title: "设置",
  description: "调整外观、通知、声音与编辑器的偏好",
  navLabel: "设置分区",
  navSectionModified: "已修改 {count} 项",
  // 分区名（导航与面板）
  sectionAppearance: "外观",
  sectionNotifications: "通知",
  sectionSound: "声音与触感",
  sectionEditor: "编辑器",
  sectionPrivacy: "隐私",
  // 窄屏顶部分段选择里的短名，宽度有限所以单独一份
  navAppearance: "外观",
  navNotifications: "通知",
  navSound: "声音",
  navEditor: "编辑器",
  navPrivacy: "隐私",
  // 分区说明
  descriptionAppearance: "主题、强调色与界面尺寸，右侧预览会实时变化",
  descriptionNotifications: "决定哪些事件会打扰你，以及什么时候不打扰",
  descriptionSound: "音量、按键音效与触感反馈",
  descriptionEditor: "缩进、行高与空白字符的显示方式",
  descriptionPrivacy: "谁能看到你，以及我们收集哪些数据",
  // 面板头部
  panelModified: "已修改 {count} 项",
  panelDefault: "全部为默认值",
  copySection: "复制本分区设置",
  copiedSection: "已复制",
  resetSection: "恢复本分区默认值",
  // 外观
  groupTheme: "主题",
  groupShape: "尺寸与形状",
  groupMotion: "动态",
  theme: "主题模式",
  themeLight: "浅色",
  themeDark: "深色",
  themeSystem: "跟随系统",
  accent: "强调色",
  fontSize: "界面字号",
  radius: "圆角",
  density: "信息密度",
  densityCompact: "紧凑",
  densityComfortable: "舒适",
  densitySpacious: "宽松",
  reduceMotion: "减少动态效果",
  reduceMotionHint: "关闭动画",
  readoutPx: "{value}px",
  // 预览卡片
  previewTitle: "实时预览",
  previewHeading: "本周回顾",
  previewBody: "已完成 12 项任务，比上周多 3 项。",
  previewAction: "查看详情",
  previewProgress: "进度",
  previewLive: "实时",
  previewModeLight: "浅色",
  previewModeDark: "深色",
  previewModeSystem: "跟随系统",
  // 通知
  groupChannels: "通知渠道",
  groupQuiet: "免打扰",
  notifications: "允许通知",
  notificationsHint: "总开关",
  notifyMentions: "提及我",
  notifyMentionsHint: "有人 @ 我时",
  notifyReplies: "评论回复",
  notifyRepliesHint: "有人回复时",
  notifyEmail: "邮件摘要",
  notifyEmailHint: "每天一封",
  notifyUpdates: "产品动态",
  notifyUpdatesHint: "新功能上线",
  quietHours: "免打扰时段",
  // 声音与触感
  groupVolume: "音量",
  groupFeedback: "反馈",
  volume: "主音量",
  keySound: "按键音效",
  keySoundHint: "输入时发声",
  soundTheme: "音色",
  soundSoft: "柔和",
  soundCrisp: "清脆",
  soundMechanical: "机械",
  haptics: "触感反馈",
  hapticsHint: "设备震动",
  // 编辑器
  groupIndent: "缩进与行距",
  groupWhitespace: "空白字符",
  indent: "缩进宽度",
  indentTwo: "2 空格",
  indentFour: "4 空格",
  indentTab: "制表符",
  lineHeight: "行高",
  showTabs: "显示制表符",
  showTabsHint: "标出缩进",
  editorPreviewTitle: "代码预览",
  // 隐私
  groupProfile: "个人资料",
  groupData: "数据",
  visibility: "资料可见范围",
  visibilityPublic: "公开",
  visibilityTeam: "团队",
  visibilityPrivate: "仅自己",
  analytics: "使用统计",
  analyticsHint: "匿名数据",
  crashReports: "崩溃报告",
  crashReportsHint: "自动上报",
  retention: "历史保留",
  readoutDays: "{value} 天",
  changesTitle: "改动对比",
  undoChange: "撤销 {field} 的修改",
  resetChanges: "全部恢复",
  valueOn: "开",
  valueOff: "关",
  // 底部操作栏
  footerLabel: "保存设置",
  modifiedCount: "已修改 {count} 项",
  unmodified: "全部为默认值",
  unsaved: "有未保存的更改",
  upToDate: "没有未保存的更改",
  saving: "保存中…",
  saved: "已保存",
  save: "保存",
  resetAll: "恢复默认",
  saveFailed: "保存失败，请稍后重试",
} as const

type Settings01Labels = { [Key in keyof typeof DEFAULT_LABELS]: string }

/** 出厂默认值。组件的「已修改」判断、恢复默认和未受控时的初始值都以它为准。 */
const DEFAULT_VALUES: Settings01Values = {
  theme: "system",
  accent: "primary",
  fontSize: 14,
  radius: 10,
  density: "comfortable",
  reduceMotion: false,
  notifications: true,
  notifyMentions: true,
  notifyReplies: true,
  notifyEmail: false,
  notifyUpdates: false,
  quietHours: [22, 31],
  volume: 60,
  keySound: false,
  soundTheme: "soft",
  haptics: true,
  indent: "2",
  lineHeight: 1.6,
  showTabs: false,
  visibility: "team",
  analytics: true,
  crashReports: true,
  retention: 90,
}

/** 分区的显示顺序 */
const SECTION_IDS: readonly Settings01SectionId[] = [
  "appearance",
  "notifications",
  "sound",
  "editor",
  "privacy",
]

/** 每个分区管哪些设置项：分区计数、分区复制与分区恢复默认都按它来。 */
const SECTION_KEYS: Record<Settings01SectionId, readonly (keyof Settings01Values)[]> =
  {
    appearance: [
      "theme",
      "accent",
      "fontSize",
      "radius",
      "density",
      "reduceMotion",
    ],
    notifications: [
      "notifications",
      "notifyMentions",
      "notifyReplies",
      "notifyEmail",
      "notifyUpdates",
      "quietHours",
    ],
    sound: ["volume", "keySound", "soundTheme", "haptics"],
    editor: ["indent", "lineHeight", "showTabs"],
    privacy: ["visibility", "analytics", "crashReports", "retention"],
  }

/** 每个分区用到的文案键：完整名称、说明与窄屏短名。 */
const SECTION_LABEL_KEYS: Record<
  Settings01SectionId,
  {
    title: keyof Settings01Labels
    description: keyof Settings01Labels
    nav: keyof Settings01Labels
  }
> = {
  appearance: {
    title: "sectionAppearance",
    description: "descriptionAppearance",
    nav: "navAppearance",
  },
  notifications: {
    title: "sectionNotifications",
    description: "descriptionNotifications",
    nav: "navNotifications",
  },
  sound: {
    title: "sectionSound",
    description: "descriptionSound",
    nav: "navSound",
  },
  editor: {
    title: "sectionEditor",
    description: "descriptionEditor",
    nav: "navEditor",
  },
  privacy: {
    title: "sectionPrivacy",
    description: "descriptionPrivacy",
    nav: "navPrivacy",
  },
}

/** 合并用户传入的文案与默认文案。 */
function resolveLabels(labels?: Partial<Settings01Labels>): Settings01Labels {
  return { ...DEFAULT_LABELS, ...labels }
}

/** 把文案里的 {key} 占位符替换成实际值，例如「已修改 {count} 项」。 */
function formatLabel(
  template: string,
  values: Record<string, string | number>,
): string {
  return Object.entries(values).reduce(
    (text, [key, value]) => text.replaceAll(`{${key}}`, String(value)),
    template,
  )
}

export {
  DEFAULT_LABELS,
  DEFAULT_VALUES,
  SECTION_IDS,
  SECTION_KEYS,
  SECTION_LABEL_KEYS,
  formatLabel,
  resolveLabels,
}
export type { Settings01Labels }
```

### `src/registry/blocks/settings-01/types.ts`

```ts
type ThemeMode = "light" | "dark" | "system"
type Density = "compact" | "comfortable" | "spacious"
type SoundTheme = "soft" | "crisp" | "mechanical"
type IndentWidth = "2" | "4" | "tab"
type ProfileVisibility = "public" | "team" | "private"

/** 设置页的全部取值。键按分区归类，见 config.ts 的 SECTION_KEYS。 */
type Settings01Values = {
  // 外观
  theme: ThemeMode
  /** 强调色，取值是 tone-picker 的色调名（primary、blue……）或任意 CSS 颜色 */
  accent: string
  /** 界面字号（px） */
  fontSize: number
  /** 圆角（px） */
  radius: number
  density: Density
  reduceMotion: boolean
  // 通知
  notifications: boolean
  notifyMentions: boolean
  notifyReplies: boolean
  notifyEmail: boolean
  notifyUpdates: boolean
  /** 免打扰时段 [起点, 终点]，单位小时，范围 12–36（36 即次日 12:00），读数按 24 小时取模 */
  quietHours: [number, number]
  // 声音与触感
  volume: number
  keySound: boolean
  soundTheme: SoundTheme
  haptics: boolean
  // 编辑器
  indent: IndentWidth
  lineHeight: number
  showTabs: boolean
  // 隐私
  visibility: ProfileVisibility
  analytics: boolean
  crashReports: boolean
  /** 历史记录保留天数 */
  retention: number
}

type Settings01SectionId =
  | "appearance"
  | "notifications"
  | "sound"
  | "editor"
  | "privacy"

export type {
  Density,
  IndentWidth,
  ProfileVisibility,
  Settings01SectionId,
  Settings01Values,
  SoundTheme,
  ThemeMode,
}
```

### `src/registry/blocks/settings-01/utils.ts`

```ts
import { DEFAULT_LABELS } from "./config"

/** 两个设置值是否相同；区间（数组）逐项比较。 */
function isSameValue(a: unknown, b: unknown): boolean {
  if (Array.isArray(a) && Array.isArray(b))
    return a.length === b.length && a.every((item, index) => item === b[index])
  return Object.is(a, b)
}

/**
 * 把小时数格式化成「22:00」。
 * 免打扰滑块的范围是 12–36（跨午夜），所以先对 24 取模；用 Intl 并固定 UTC，保证服务端与客户端一致。
 */
function formatHour(hour: number, locale: string = "zh-CN"): string {
  const whole = Math.floor(hour)
  const minutes = Math.round((hour - whole) * 60)
  return new Intl.DateTimeFormat(locale, {
    hour: "2-digit",
    minute: "2-digit",
    hourCycle: "h23",
    timeZone: "UTC",
  }).format(new Date(Date.UTC(1970, 0, 1, whole % 24, minutes)))
}

/** 把未知错误转成可展示的文案；没有 message 时用兜底文案。 */
function errorMessage(
  error: unknown,
  fallback: string = DEFAULT_LABELS.saveFailed,
) {
  return error instanceof Error && error.message ? error.message : fallback
}

export { errorMessage, formatHour, isSameValue }
```

### `src/registry/blocks/settings-01/section-layout.tsx`

```tsx
import type * as React from "react"
import { useId } from "react"

import { cn } from "@/lib/utils"
import {
  ControlPanel,
  ControlPanelActions,
  ControlPanelCopy,
  ControlPanelHeader,
  ControlPanelReset,
  ControlPanelTitle,
} from "@/components/ui/control-panel/control-panel"

import {
  SECTION_LABEL_KEYS,
  formatLabel,
  type Settings01Labels,
} from "./config"
import type { Settings01SectionId, Settings01Values } from "./types"

type SetValue = <Key extends keyof Settings01Values>(
  key: Key,
  value: Settings01Values[Key],
) => void

/** 每个分区组件收到的同一组属性 */
type SectionProps = {
  values: Settings01Values
  labels: Settings01Labels
  set: SetValue
  /** 某一项是否与默认值不同，传给控件的 `modified` */
  modified: (key: keyof Settings01Values) => boolean
  /** 本分区有几项不是默认值 */
  count: number
  /** 把本分区恢复为默认值 */
  onReset: () => void
  /** 本分区当前取值的 JSON，复制按钮用 */
  getCode: () => string
}

/**
 * 分区外壳：标题与说明 + 控制面板（头部显示本分区已修改项数，带复制与恢复）。
 * 有 `aside` 时在宽屏放到右侧，窄屏排在面板上方。
 */
function SectionLayout({
  id,
  labels,
  count,
  onReset,
  getCode,
  aside,
  children,
}: Pick<SectionProps, "labels" | "count" | "onReset" | "getCode"> & {
  id: Settings01SectionId
  aside?: React.ReactNode
  children: React.ReactNode
}) {
  const headingId = useId()
  const keys = SECTION_LABEL_KEYS[id]
  return (
    <section
      id={`settings-01-${id}`}
      aria-labelledby={headingId}
      data-slot="settings-01-section"
      data-section={id}
      className="flex flex-col gap-4"
    >
      <header>
        <h2
          id={headingId}
          className="text-base font-semibold tracking-tight"
        >
          {labels[keys.title]}
        </h2>
        <p className="mt-1 text-sm leading-6 text-muted-foreground">
          {labels[keys.description]}
        </p>
      </header>
      <div
        className={cn(
          "grid items-start gap-6",
          aside && "lg:grid-cols-[minmax(0,1fr)_16rem]",
        )}
      >
        <ControlPanel className={cn("max-w-2xl", aside && "lg:max-w-none")}>
          <ControlPanelHeader>
            <ControlPanelTitle>
              {count > 0
                ? formatLabel(labels.panelModified, { count })
                : labels.panelDefault}
            </ControlPanelTitle>
            <ControlPanelActions>
              <ControlPanelCopy
                code={getCode}
                label={labels.copySection}
                copiedLabel={labels.copiedSection}
              />
              <ControlPanelReset
                label={labels.resetSection}
                disabled={count === 0}
                onClick={onReset}
              />
            </ControlPanelActions>
          </ControlPanelHeader>
          {children}
        </ControlPanel>
        {aside ? (
          <div
            data-slot="settings-01-aside"
            className="order-first lg:sticky lg:top-6 lg:order-none"
          >
            {aside}
          </div>
        ) : null}
      </div>
    </section>
  )
}

export { SectionLayout }
export type { SectionProps, SetValue }
```

### `src/registry/blocks/settings-01/section-nav.tsx`

```tsx
import type * as React from "react"
import { Bell, Code, Palette, ShieldCheck, Volume2 } from "lucide-react"

import { SegmentedControl } from "@/components/ui/segmented-control/segmented-control"

import {
  SECTION_IDS,
  SECTION_LABEL_KEYS,
  formatLabel,
  type Settings01Labels,
} from "./config"
import type { Settings01SectionId } from "./types"

const SECTION_ICONS: Record<
  Settings01SectionId,
  React.ComponentType<{ className?: string }>
> = {
  appearance: Palette,
  notifications: Bell,
  sound: Volume2,
  editor: Code,
  privacy: ShieldCheck,
}

/** 宽屏左侧的分区导航：一行一个分区，右侧小圆标显示该分区已修改的项数。 */
function SectionNav({
  section,
  onSectionChange,
  counts,
  labels,
}: {
  section: Settings01SectionId
  onSectionChange: (section: Settings01SectionId) => void
  counts: Record<Settings01SectionId, number>
  labels: Settings01Labels
}) {
  return (
    <nav
      aria-label={labels.navLabel}
      data-slot="settings-01-nav"
      className="hidden w-52 shrink-0 flex-col border-r border-edge md:flex"
    >
      {SECTION_IDS.map((id) => {
        const Icon = SECTION_ICONS[id]
        const active = id === section
        const count = counts[id]
        return (
          <button
            key={id}
            type="button"
            aria-current={active ? "page" : undefined}
            data-slot="settings-01-nav-item"
            onClick={() => onSectionChange(id)}
            className="relative flex h-10 w-full shrink-0 cursor-pointer items-center gap-2.5 border-b border-edge px-4 text-start text-sm text-muted-foreground outline-none transition-colors before:absolute before:inset-y-0 before:left-0 before:w-0.5 hover:bg-muted/50 hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset aria-[current=page]:bg-muted/60 aria-[current=page]:font-medium aria-[current=page]:text-foreground aria-[current=page]:before:bg-(--flow-tone)"
          >
            <Icon className="size-4 shrink-0" />
            <span className="truncate">{labels[SECTION_LABEL_KEYS[id].title]}</span>
            {count > 0 ? (
              <span
                data-slot="settings-01-nav-count"
                title={formatLabel(labels.navSectionModified, { count })}
                className="ms-auto min-w-5 rounded-full bg-foreground/10 px-1.5 text-center text-[11px] leading-5 text-foreground tabular-nums"
              >
                {count}
                <span className="sr-only">
                  {formatLabel(labels.navSectionModified, { count })}
                </span>
              </span>
            ) : null}
          </button>
        )
      })}
    </nav>
  )
}

/** 窄屏顶部的分段选择：用短名，五项刚好排进 375px。 */
function SectionTabs({
  section,
  onSectionChange,
  labels,
  tone,
}: {
  section: Settings01SectionId
  onSectionChange: (section: Settings01SectionId) => void
  labels: Settings01Labels
  tone: string
}) {
  return (
    <div
      data-slot="settings-01-tabs"
      className="border-b border-edge p-4 sm:px-6 md:hidden"
    >
      <SegmentedControl
        aria-label={labels.navLabel}
        tone={tone}
        value={section}
        onValueChange={(next) => onSectionChange(next as Settings01SectionId)}
        options={SECTION_IDS.map((id) => ({
          value: id,
          label: labels[SECTION_LABEL_KEYS[id].nav],
        }))}
      />
    </div>
  )
}

export { SectionNav, SectionTabs }
```

### `src/registry/blocks/settings-01/previews.tsx`

```tsx
import type * as React from "react"
import { Monitor, Moon, Sparkles, Sun } from "lucide-react"

import { cn } from "@/lib/utils"
import { flowToneStyle } from "@/lib/flow-tone"

import type { Settings01Labels } from "./config"
import type { Density, Settings01Values, ThemeMode } from "./types"

const DENSITY_CLASSES: Record<Density, string> = {
  compact: "gap-2 p-3",
  comfortable: "gap-3 p-4",
  spacious: "gap-4 p-5",
}

const MODE_ICONS: Record<ThemeMode, React.ComponentType<{ className?: string }>> =
  { light: Sun, dark: Moon, system: Monitor }

/** 预览卡片的外框：标题栏 + 内容区。纯展示，对读屏隐藏。 */
function PreviewFrame({
  title,
  badge,
  dark = false,
  className,
  style,
  children,
}: {
  title: string
  badge?: React.ReactNode
  /** 在卡片内套用深色主题变量 */
  dark?: boolean
  className?: string
  style?: React.CSSProperties
  children: React.ReactNode
}) {
  return (
    <div
      aria-hidden="true"
      data-slot="settings-01-preview"
      className={cn(
        "overflow-hidden rounded-lg border border-edge bg-muted/40 text-foreground",
        dark && "dark bg-background",
        className,
      )}
      style={style}
    >
      <div className="flex h-10 items-center justify-between gap-2 border-b border-edge px-3 text-xs text-muted-foreground">
        <span className="truncate">{title}</span>
        {badge ? (
          <span className="flex shrink-0 items-center gap-1 [&_svg]:size-3.5">
            {badge}
          </span>
        ) : null}
      </div>
      {children}
    </div>
  )
}

/**
 * 外观预览：一张小卡片，随主题模式、强调色、字号、圆角、信息密度和动态效果实时变化。
 * 深色用 `.dark` 变量在卡片内套用；站点本身已是深色时，浅色无法在卡片里反向还原，所以标题栏另外写出当前模式。
 */
function AppearancePreview({
  values,
  labels,
}: {
  values: Settings01Values
  labels: Settings01Labels
}) {
  const ModeIcon = MODE_ICONS[values.theme]
  const modeLabel = {
    light: labels.previewModeLight,
    dark: labels.previewModeDark,
    system: labels.previewModeSystem,
  }[values.theme]
  return (
    <PreviewFrame
      title={labels.previewTitle}
      dark={values.theme === "dark"}
      badge={
        <>
          <ModeIcon />
          {modeLabel}
        </>
      }
      style={
        {
          ...flowToneStyle(values.accent),
          "--preview-radius": `${values.radius}px`,
        } as React.CSSProperties
      }
    >
      <div className="p-3">
        <div
          className={cn(
            "flex flex-col rounded-(--preview-radius) border border-edge bg-background shadow-xs transition-[gap,padding] duration-200 motion-reduce:transition-none",
            DENSITY_CLASSES[values.density],
          )}
          style={{ fontSize: values.fontSize }}
        >
          <div className="flex items-center gap-2.5">
            <span className="flex size-8 shrink-0 items-center justify-center rounded-(--preview-radius) bg-(--flow-tone)/15 text-(--flow-tone)">
              <Sparkles className="size-4" />
            </span>
            <div className="min-w-0 flex-1">
              <p className="truncate text-[1.1em] leading-tight font-medium">
                {labels.previewHeading}
              </p>
              <p className="flex items-center gap-1.5 text-[0.8em] text-muted-foreground">
                <span
                  className={cn(
                    "size-1.5 rounded-full bg-(--flow-tone)",
                    !values.reduceMotion && "animate-pulse motion-reduce:animate-none",
                  )}
                />
                {labels.previewLive}
              </p>
            </div>
          </div>
          <p className="text-[1em] leading-snug text-muted-foreground">
            {labels.previewBody}
          </p>
          <div className="flex flex-col gap-1.5">
            <div className="flex items-center justify-between text-[0.8em] text-muted-foreground">
              <span>{labels.previewProgress}</span>
              <span className="tabular-nums">72%</span>
            </div>
            <div className="h-1.5 overflow-hidden rounded-full bg-muted">
              <div className="h-full w-[72%] rounded-full bg-(--flow-tone)" />
            </div>
          </div>
          {/* 模拟按钮：高度取默认档 32px，圆角跟随预览圆角 */}
          <span className="inline-flex h-8 items-center justify-center self-start rounded-(--preview-radius) border border-(--flow-tone) bg-(--flow-tone)/10 px-3 text-[0.9em] font-medium">
            {labels.previewAction}
          </span>
        </div>
      </div>
    </PreviewFrame>
  )
}

/** 示例代码：level 是嵌套层级 */
const SAMPLE_CODE = [
  { level: 0, text: "function greet(name) {" },
  { level: 1, text: "if (name) {" },
  { level: 2, text: "return `Hi, ${name}`" },
  { level: 1, text: "}" },
  { level: 1, text: 'return "Hi"' },
  { level: 0, text: "}" },
] as const

const INDENT_WIDTH = { "2": 2, "4": 4, tab: 4 } as const

/** 编辑器预览：缩进宽度、行高与空白字符的显示会实时变化。 */
function EditorPreview({
  values,
  labels,
}: {
  values: Settings01Values
  labels: Settings01Labels
}) {
  const width = INDENT_WIDTH[values.indent]
  const marker = values.indent === "tab" ? "→" : "·".repeat(width)
  return (
    <PreviewFrame title={labels.editorPreviewTitle}>
      <pre
        className="overflow-hidden bg-background p-3 font-mono text-xs"
        style={{ lineHeight: values.lineHeight }}
      >
        {SAMPLE_CODE.map((line, index) => (
          <div key={index} className="flex">
            <span className="w-5 shrink-0 pe-2 text-end text-muted-foreground/60 tabular-nums select-none">
              {index + 1}
            </span>
            {Array.from({ length: line.level }, (_, unit) => (
              <span
                key={unit}
                className="inline-block shrink-0 overflow-hidden text-muted-foreground/50"
                style={{ width: `${width}ch` }}
              >
                {values.showTabs ? marker : ""}
              </span>
            ))}
            <span className="whitespace-pre">{line.text}</span>
          </div>
        ))}
      </pre>
    </PreviewFrame>
  )
}

export { AppearancePreview, EditorPreview }
```

### `src/registry/blocks/settings-01/appearance-section.tsx`

```tsx
import { Monitor, Moon, Sun } from "lucide-react"

import { ControlPanelGroup } from "@/components/ui/control-panel/control-panel"
import { SegmentedControl } from "@/components/ui/segmented-control/segmented-control"
import { ToggleSwitch } from "@/components/ui/toggle-switch/toggle-switch"
import { TonePicker } from "@/components/ui/tone-picker/tone-picker"
import { ValueSlider } from "@/components/ui/value-slider/value-slider"

import { formatLabel } from "./config"
import { AppearancePreview } from "./previews"
import { SectionLayout, type SectionProps } from "./section-layout"
import type { Density, ThemeMode } from "./types"

/** 外观：主题、强调色、字号、圆角、信息密度、动态效果，右侧是实时预览。 */
function AppearanceSection(props: SectionProps) {
  const { values, labels, set, modified } = props
  const tone = values.accent
  return (
    <SectionLayout
      id="appearance"
      {...props}
      aside={<AppearancePreview values={values} labels={labels} />}
    >
      <ControlPanelGroup title={labels.groupTheme}>
        <SegmentedControl
          label={labels.theme}
          tone={tone}
          value={values.theme}
          onValueChange={(next) => set("theme", next as ThemeMode)}
          modified={modified("theme")}
          options={[
            { value: "light", label: labels.themeLight, icon: <Sun /> },
            { value: "dark", label: labels.themeDark, icon: <Moon /> },
            { value: "system", label: labels.themeSystem, icon: <Monitor /> },
          ]}
        />
        <TonePicker
          label={labels.accent}
          value={values.accent}
          onValueChange={(next) => set("accent", next)}
          modified={modified("accent")}
        />
      </ControlPanelGroup>
      <ControlPanelGroup title={labels.groupShape} columns={2}>
        <ValueSlider
          label={labels.fontSize}
          tone={tone}
          min={12}
          max={20}
          step={1}
          editable
          formatValue={(value) =>
            formatLabel(labels.readoutPx, { value })
          }
          value={values.fontSize}
          onValueChange={(next) => set("fontSize", next)}
          modified={modified("fontSize")}
        />
        <ValueSlider
          label={labels.radius}
          tone={tone}
          min={0}
          max={20}
          step={1}
          editable
          formatValue={(value) =>
            formatLabel(labels.readoutPx, { value })
          }
          value={values.radius}
          onValueChange={(next) => set("radius", next)}
          modified={modified("radius")}
        />
        <SegmentedControl
          className="sm:col-span-2"
          label={labels.density}
          tone={tone}
          value={values.density}
          onValueChange={(next) => set("density", next as Density)}
          modified={modified("density")}
          options={[
            { value: "compact", label: labels.densityCompact },
            { value: "comfortable", label: labels.densityComfortable },
            { value: "spacious", label: labels.densitySpacious },
          ]}
        />
      </ControlPanelGroup>
      <ControlPanelGroup title={labels.groupMotion}>
        <ToggleSwitch
          label={labels.reduceMotion}
          description={labels.reduceMotionHint}
          tone={tone}
          checked={values.reduceMotion}
          onCheckedChange={(next) => set("reduceMotion", next)}
          modified={modified("reduceMotion")}
        />
      </ControlPanelGroup>
    </SectionLayout>
  )
}

export { AppearanceSection }
```

### `src/registry/blocks/settings-01/notifications-section.tsx`

```tsx
import { ControlPanelGroup } from "@/components/ui/control-panel/control-panel"
import { ToggleSwitch } from "@/components/ui/toggle-switch/toggle-switch"
import { ValueSlider } from "@/components/ui/value-slider/value-slider"

import { SectionLayout, type SectionProps } from "./section-layout"
import { formatHour } from "./utils"

/** 一天里从中午到次日中午，免打扰通常跨午夜，所以滑块范围取 12–36 */
const QUIET_MIN = 12
const QUIET_MAX = 36

/** 通知：总开关控制其余开关的禁用态，免打扰时段是区间滑块。 */
function NotificationsSection(props: SectionProps) {
  const { values, labels, set, modified } = props
  const tone = values.accent
  const off = !values.notifications
  return (
    <SectionLayout id="notifications" {...props}>
      <ControlPanelGroup title={labels.groupChannels} columns={2}>
        <ToggleSwitch
          className="sm:col-span-2"
          label={labels.notifications}
          description={labels.notificationsHint}
          tone={tone}
          checked={values.notifications}
          onCheckedChange={(next) => set("notifications", next)}
          modified={modified("notifications")}
        />
        <ToggleSwitch
          label={labels.notifyMentions}
          description={labels.notifyMentionsHint}
          tone={tone}
          disabled={off}
          checked={values.notifyMentions}
          onCheckedChange={(next) => set("notifyMentions", next)}
          modified={modified("notifyMentions")}
        />
        <ToggleSwitch
          label={labels.notifyReplies}
          description={labels.notifyRepliesHint}
          tone={tone}
          disabled={off}
          checked={values.notifyReplies}
          onCheckedChange={(next) => set("notifyReplies", next)}
          modified={modified("notifyReplies")}
        />
        <ToggleSwitch
          label={labels.notifyEmail}
          description={labels.notifyEmailHint}
          tone={tone}
          disabled={off}
          checked={values.notifyEmail}
          onCheckedChange={(next) => set("notifyEmail", next)}
          modified={modified("notifyEmail")}
        />
        <ToggleSwitch
          label={labels.notifyUpdates}
          description={labels.notifyUpdatesHint}
          tone={tone}
          disabled={off}
          checked={values.notifyUpdates}
          onCheckedChange={(next) => set("notifyUpdates", next)}
          modified={modified("notifyUpdates")}
        />
      </ControlPanelGroup>
      <ControlPanelGroup title={labels.groupQuiet}>
        {/* 区间读数较长，用矮轨让标签与读数排在轨道上方，窄屏也不会被手柄压住 */}
        <ValueSlider
          label={labels.quietHours}
          variant="slim"
          tone={tone}
          min={QUIET_MIN}
          max={QUIET_MAX}
          step={0.5}
          largeStep={3}
          minStepsBetweenValues={1}
          ticks={[18, 24, 30]}
          disabled={off}
          formatValue={formatHour}
          value={values.quietHours}
          onValueChange={(next) => set("quietHours", [next[0], next[1]])}
          modified={modified("quietHours")}
        />
      </ControlPanelGroup>
    </SectionLayout>
  )
}

export { NotificationsSection }
```

### `src/registry/blocks/settings-01/sound-section.tsx`

```tsx
import { Volume2, VolumeX } from "lucide-react"

import { ControlPanelGroup } from "@/components/ui/control-panel/control-panel"
import { SegmentedControl } from "@/components/ui/segmented-control/segmented-control"
import { ToggleSwitch } from "@/components/ui/toggle-switch/toggle-switch"
import { ValueSlider } from "@/components/ui/value-slider/value-slider"

import { SectionLayout, type SectionProps } from "./section-layout"
import type { SoundTheme } from "./types"

/** 声音与触感：主音量带拖动音效，音色在按键音效关闭时禁用。 */
function SoundSection(props: SectionProps) {
  const { values, labels, set, modified } = props
  const tone = values.accent
  return (
    <SectionLayout id="sound" {...props}>
      <ControlPanelGroup title={labels.groupVolume}>
        <ValueSlider
          label={labels.volume}
          tone={tone}
          sound
          editable
          min={0}
          max={100}
          step={1}
          startIcon={<VolumeX />}
          endIcon={<Volume2 />}
          value={values.volume}
          onValueChange={(next) => set("volume", next)}
          modified={modified("volume")}
        />
      </ControlPanelGroup>
      <ControlPanelGroup title={labels.groupFeedback} columns={2}>
        <ToggleSwitch
          label={labels.keySound}
          description={labels.keySoundHint}
          tone={tone}
          checked={values.keySound}
          onCheckedChange={(next) => set("keySound", next)}
          modified={modified("keySound")}
        />
        <ToggleSwitch
          label={labels.haptics}
          description={labels.hapticsHint}
          tone={tone}
          checked={values.haptics}
          onCheckedChange={(next) => set("haptics", next)}
          modified={modified("haptics")}
        />
        <SegmentedControl
          className="sm:col-span-2"
          label={labels.soundTheme}
          tone={tone}
          disabled={!values.keySound}
          value={values.soundTheme}
          onValueChange={(next) => set("soundTheme", next as SoundTheme)}
          modified={modified("soundTheme")}
          options={[
            { value: "soft", label: labels.soundSoft },
            { value: "crisp", label: labels.soundCrisp },
            { value: "mechanical", label: labels.soundMechanical },
          ]}
        />
      </ControlPanelGroup>
    </SectionLayout>
  )
}

export { SoundSection }
```

### `src/registry/blocks/settings-01/editor-section.tsx`

```tsx
import { ControlPanelGroup } from "@/components/ui/control-panel/control-panel"
import { SegmentedControl } from "@/components/ui/segmented-control/segmented-control"
import { ToggleSwitch } from "@/components/ui/toggle-switch/toggle-switch"
import { ValueSlider } from "@/components/ui/value-slider/value-slider"

import { EditorPreview } from "./previews"
import { SectionLayout, type SectionProps } from "./section-layout"
import type { IndentWidth } from "./types"

/** 编辑器：缩进、行高与制表符显示，右侧是代码预览。 */
function EditorSection(props: SectionProps) {
  const { values, labels, set, modified } = props
  const tone = values.accent
  return (
    <SectionLayout
      id="editor"
      {...props}
      aside={<EditorPreview values={values} labels={labels} />}
    >
      <ControlPanelGroup title={labels.groupIndent}>
        <SegmentedControl
          label={labels.indent}
          tone={tone}
          value={values.indent}
          onValueChange={(next) => set("indent", next as IndentWidth)}
          modified={modified("indent")}
          options={[
            { value: "2", label: labels.indentTwo },
            { value: "4", label: labels.indentFour },
            { value: "tab", label: labels.indentTab },
          ]}
        />
        <ValueSlider
          label={labels.lineHeight}
          tone={tone}
          editable
          min={1.2}
          max={2}
          step={0.1}
          value={values.lineHeight}
          onValueChange={(next) => set("lineHeight", next)}
          modified={modified("lineHeight")}
        />
      </ControlPanelGroup>
      <ControlPanelGroup title={labels.groupWhitespace}>
        <ToggleSwitch
          label={labels.showTabs}
          description={labels.showTabsHint}
          tone={tone}
          checked={values.showTabs}
          onCheckedChange={(next) => set("showTabs", next)}
          modified={modified("showTabs")}
        />
      </ControlPanelGroup>
    </SectionLayout>
  )
}

export { EditorSection }
```

### `src/registry/blocks/settings-01/privacy-section.tsx`

```tsx
import { ControlPanelGroup } from "@/components/ui/control-panel/control-panel"
import { SegmentedControl } from "@/components/ui/segmented-control/segmented-control"
import { ToggleSwitch } from "@/components/ui/toggle-switch/toggle-switch"
import { ValueSlider } from "@/components/ui/value-slider/value-slider"

import { formatLabel } from "./config"
import { SectionLayout, type SectionProps } from "./section-layout"
import type { ProfileVisibility } from "./types"

/** 隐私：资料可见范围、数据收集开关与历史保留天数。 */
function PrivacySection(props: SectionProps) {
  const { values, labels, set, modified } = props
  const tone = values.accent
  return (
    <SectionLayout id="privacy" {...props}>
      <ControlPanelGroup title={labels.groupProfile}>
        <SegmentedControl
          label={labels.visibility}
          tone={tone}
          value={values.visibility}
          onValueChange={(next) => set("visibility", next as ProfileVisibility)}
          modified={modified("visibility")}
          options={[
            { value: "public", label: labels.visibilityPublic },
            { value: "team", label: labels.visibilityTeam },
            { value: "private", label: labels.visibilityPrivate },
          ]}
        />
      </ControlPanelGroup>
      <ControlPanelGroup title={labels.groupData} columns={2}>
        <ToggleSwitch
          label={labels.analytics}
          description={labels.analyticsHint}
          tone={tone}
          checked={values.analytics}
          onCheckedChange={(next) => set("analytics", next)}
          modified={modified("analytics")}
        />
        <ToggleSwitch
          label={labels.crashReports}
          description={labels.crashReportsHint}
          tone={tone}
          checked={values.crashReports}
          onCheckedChange={(next) => set("crashReports", next)}
          modified={modified("crashReports")}
        />
        <ValueSlider
          className="sm:col-span-2"
          label={labels.retention}
          tone={tone}
          editable
          min={7}
          max={365}
          step={1}
          largeStep={30}
          ticks={[30, 90, 180]}
          snapToTicks
          formatValue={(value) => formatLabel(labels.readoutDays, { value })}
          value={values.retention}
          onValueChange={(next) => set("retention", next)}
          modified={modified("retention")}
        />
      </ControlPanelGroup>
    </SectionLayout>
  )
}

export { PrivacySection }
```

### `src/registry/blocks/settings-01/use-save-action.ts`

```ts
"use client"

import { useCallback, useState } from "react"

import { DEFAULT_LABELS } from "./config"
import { errorMessage } from "./utils"

type SaveStatus = "idle" | "pending" | "saved" | "error"

/** 包装保存动作：管理 loading，成功后记为 saved，抛出的错误转成可展示的文案。 */
function useSaveAction(fallbackError: string = DEFAULT_LABELS.saveFailed) {
  const [status, setStatus] = useState<SaveStatus>("idle")
  const [error, setError] = useState<string | null>(null)
  const run = useCallback(
    async (action: () => Promise<void>) => {
      setStatus("pending")
      setError(null)
      try {
        await action()
        setStatus("saved")
        return true
      } catch (caught) {
        setError(errorMessage(caught, fallbackError))
        setStatus("error")
        return false
      }
    },
    [fallbackError],
  )
  /** 回到空闲：设置被再次修改后，「已保存」「出错」的提示就该消失 */
  const reset = useCallback(() => {
    setStatus("idle")
    setError(null)
  }, [])
  return { status, error, run, reset }
}

export { useSaveAction }
export type { SaveStatus }
```

### `src/registry/blocks/settings-01/changes.tsx`

```tsx
"use client"

import { useSyncExternalStore } from "react"
import { ArrowRight, Undo2 } from "lucide-react"

import { Button } from "@/components/ui/button"
import { Item, ItemContent, ItemTitle, ItemDescription, ItemGroup } from "@/components/ui/item"
import { Popover, PopoverTrigger, PopoverContent, PopoverTitle } from "@/components/ui/popover"
import { Sheet, SheetTrigger, SheetContent, SheetTitle } from "@/components/ui/sheet"
import { flowToneStyle } from "@/lib/flow-tone"

import { SECTION_LABEL_KEYS, formatLabel, type Settings01Labels } from "./config"
import type { Settings01SectionId, Settings01Values } from "./types"
import { formatHour } from "./utils"

function subscribe(callback: () => void) {
  const media = window.matchMedia("(max-width: 767px)")
  media.addEventListener("change", callback)
  return () => media.removeEventListener("change", callback)
}

/** 所有字段的对比读数在这里统一格式化。 */
function formatSetting(key: keyof Settings01Values, value: Settings01Values[keyof Settings01Values], labels: Settings01Labels): string {
  if (typeof value === "boolean") return value ? labels.valueOn : labels.valueOff
  if (Array.isArray(value)) return value.map((hour) => formatHour(hour)).join(" – ")
  if (key === "fontSize" || key === "radius") return formatLabel(labels.readoutPx, { value: String(value) })
  if (key === "retention") return formatLabel(labels.readoutDays, { value: String(value) })
  if (key === "volume") return `${value}%`
  const options: Partial<Record<keyof Settings01Values, Record<string, string>>> = {
    theme: { light: labels.themeLight, dark: labels.themeDark, system: labels.themeSystem },
    density: { compact: labels.densityCompact, comfortable: labels.densityComfortable, spacious: labels.densitySpacious },
    soundTheme: { soft: labels.soundSoft, crisp: labels.soundCrisp, mechanical: labels.soundMechanical },
    indent: { "2": labels.indentTwo, "4": labels.indentFour, tab: labels.indentTab },
    visibility: { public: labels.visibilityPublic, team: labels.visibilityTeam, private: labels.visibilityPrivate },
  }
  return options[key]?.[String(value)] ?? String(value)
}

function Settings01Changes({ changes, values, defaults, labels, pending, dirty, onUndo, onReset, onSave }: {
  changes: { key: keyof Settings01Values; section: Settings01SectionId }[]
  values: Settings01Values
  defaults: Settings01Values
  labels: Settings01Labels
  pending: boolean
  dirty: boolean
  onUndo: (key: keyof Settings01Values) => void
  onReset: () => void
  onSave: () => void
}) {
  const mobile = useSyncExternalStore(subscribe, () => window.matchMedia("(max-width: 767px)").matches, () => false)
  const trigger = <Button variant="ghost" disabled={!changes.length || pending} className="max-w-full justify-start px-0 tabular-nums">{changes.length ? formatLabel(labels.modifiedCount, { count: changes.length }) : labels.unmodified}</Button>
  const content = (
    <div data-slot="settings-01-changes" className="min-w-0">
      <ItemGroup className="max-h-[55svh] gap-0 overflow-y-auto">
        {changes.map(({ key, section }) => (
          <Item key={key} role="listitem" size="xs" className="flex-nowrap gap-2 px-0">
            <ItemContent className="min-w-0">
              <ItemTitle>{labels[key]}</ItemTitle>
              <ItemDescription className="text-xs">{labels[SECTION_LABEL_KEYS[section].title]}</ItemDescription>
            </ItemContent>
            <div className="flex min-w-0 flex-wrap items-center justify-end gap-1 text-xs">
              <span className="inline-flex items-center gap-1 text-muted-foreground line-through">
                {key === "accent" && <span aria-hidden className="size-2.5 rounded-sm bg-(--flow-tone)" style={flowToneStyle(String(defaults[key]))} />}
                {formatSetting(key, defaults[key], labels)}
              </span>
              <ArrowRight aria-hidden className="size-3 shrink-0 text-muted-foreground" />
              <span className="inline-flex items-center gap-1 font-medium text-foreground">
                {key === "accent" && <span aria-hidden className="size-2.5 rounded-sm bg-(--flow-tone)" style={flowToneStyle(String(values[key]))} />}
                {formatSetting(key, values[key], labels)}
              </span>
            </div>
            {/* 行内次要操作使用 icon-sm；底部主操作统一 default。 */}
            <Button variant="ghost" size="icon-sm" disabled={pending} aria-label={formatLabel(labels.undoChange, { field: labels[key] })} onClick={() => onUndo(key)}><Undo2 aria-hidden /></Button>
          </Item>
        ))}
      </ItemGroup>
      <div className="mt-3 flex justify-end gap-2 border-t border-edge pt-3">
        <Button variant="outline" disabled={!changes.length || pending} onClick={onReset}>{labels.resetChanges}</Button>
        <Button disabled={!dirty || pending} onClick={onSave}>{pending ? labels.saving : labels.save}</Button>
      </div>
    </div>
  )
  return mobile ? (
    <Sheet>
      <SheetTrigger render={trigger} />
      <SheetContent side="bottom" className="p-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
        <SheetTitle>{labels.changesTitle}</SheetTitle>
        {content}
      </SheetContent>
    </Sheet>
  ) : (
    <Popover>
      <PopoverTrigger render={trigger} />
      <PopoverContent side="top" align="start" className="w-88 max-w-[calc(100vw-2rem)] p-4">
        <PopoverTitle>{labels.changesTitle}</PopoverTitle>
        {content}
      </PopoverContent>
    </Popover>
  )
}

export { Settings01Changes }
```
