chore: init monorepo snapshot
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type ComponentType } from "react";
|
||||
import { BookOpenCheck, Focus, ListOrdered, ListTree, Maximize2, ShieldCheck, Type } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { DocumentStats, PageOptionsState } from "@/types/page-options";
|
||||
import { DocumentTaskPanel } from "@/components/document-task-panel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type TabId = "page" | "custom" | "global";
|
||||
|
||||
const TABS: Array<{ id: TabId; label: string }> = [
|
||||
{ id: "page", label: "页面选项" },
|
||||
{ id: "custom", label: "自定义页面" },
|
||||
{ id: "global", label: "全局选项" },
|
||||
];
|
||||
|
||||
const OPTION_META: Record<
|
||||
keyof PageOptionsState,
|
||||
{ label: string; description: string; icon: ComponentType<{ className?: string }> }
|
||||
> = {
|
||||
wideLayout: {
|
||||
label: "自适应宽度",
|
||||
description: "让编辑区域根据屏幕自动铺满",
|
||||
icon: Maximize2,
|
||||
},
|
||||
smallText: {
|
||||
label: "小字体",
|
||||
description: "使用更紧凑的字号排版",
|
||||
icon: Type,
|
||||
},
|
||||
showHeadingNumbers: {
|
||||
label: "标题编号",
|
||||
description: "自动为标题添加编号",
|
||||
icon: ListOrdered,
|
||||
},
|
||||
showToc: {
|
||||
label: "显示目录",
|
||||
description: "在右侧展示目录导航",
|
||||
icon: ListTree,
|
||||
},
|
||||
showStructure: {
|
||||
label: "块结构线框",
|
||||
description: "显示块级元素的结构边界",
|
||||
icon: Focus,
|
||||
},
|
||||
protectEditing: {
|
||||
label: "编辑保护",
|
||||
description: "保护内容避免误触修改",
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
showWordCount: {
|
||||
label: "字数提示",
|
||||
description: "实时展示字数和块统计",
|
||||
icon: BookOpenCheck,
|
||||
},
|
||||
};
|
||||
|
||||
const CUSTOM_LAYOUT_OPTIONS: (keyof PageOptionsState)[] = ["wideLayout", "smallText"];
|
||||
const CUSTOM_STRUCTURE_OPTIONS: (keyof PageOptionsState)[] = ["showHeadingNumbers", "showToc"];
|
||||
const GLOBAL_OPTIONS: (keyof PageOptionsState)[] = ["showStructure", "protectEditing", "showWordCount"];
|
||||
|
||||
interface PageOptionsSidebarProps {
|
||||
documentId: string;
|
||||
options: PageOptionsState;
|
||||
stats?: DocumentStats;
|
||||
onToggle: (key: keyof PageOptionsState) => void;
|
||||
onExport: () => void;
|
||||
onOpenHistory: () => void;
|
||||
}
|
||||
|
||||
export function PageOptionsSidebar({
|
||||
documentId,
|
||||
options,
|
||||
stats,
|
||||
onToggle,
|
||||
onExport,
|
||||
onOpenHistory,
|
||||
}: PageOptionsSidebarProps) {
|
||||
const [activeTab, setActiveTab] = useState<TabId>("page");
|
||||
|
||||
return (
|
||||
<aside className="flex h-full w-80 shrink-0 flex-col border-l border-[#f0f0f0] bg-white/95">
|
||||
<div className="flex border-b border-[#f5f5f5]">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex-1 border-b-2 px-4 py-3 text-sm font-medium text-gray-500",
|
||||
activeTab === tab.id ? "border-[#2563eb] text-[#2563eb]" : "border-transparent hover:text-gray-700",
|
||||
)}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{activeTab === "page" && (
|
||||
<div className="space-y-4">
|
||||
{options.showWordCount && stats && (
|
||||
<section className="rounded-2xl border border-[#eef1f6] p-4">
|
||||
<div className="text-sm font-semibold text-gray-800">页面数据</div>
|
||||
<div className="mt-3 grid grid-cols-3 gap-2 text-center text-xs text-gray-500">
|
||||
<StatsCell label="字数" value={stats.wordCount} />
|
||||
<StatsCell label="字符" value={stats.characterCount} />
|
||||
<StatsCell label="块数" value={stats.blockCount} />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<section className="rounded-2xl border border-[#eef1f6] p-4 text-sm text-gray-600">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-semibold text-gray-800">页面操作</span>
|
||||
<div className="flex gap-2">
|
||||
<Button type="button" variant="outline" size="sm" onClick={onExport}>
|
||||
导出
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" onClick={onOpenHistory}>
|
||||
历史
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-400">导出最新快照或打开历史版本面板。</p>
|
||||
</section>
|
||||
<DocumentTaskPanel documentId={documentId} />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "custom" && (
|
||||
<div className="space-y-5">
|
||||
<OptionToggleGroup
|
||||
title="布局与排版"
|
||||
optionKeys={CUSTOM_LAYOUT_OPTIONS}
|
||||
options={options}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
<OptionToggleGroup
|
||||
title="结构与目录"
|
||||
optionKeys={CUSTOM_STRUCTURE_OPTIONS}
|
||||
options={options}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "global" && (
|
||||
<div className="space-y-5">
|
||||
<OptionToggleGroup title="全局偏好" optionKeys={GLOBAL_OPTIONS} options={options} onToggle={onToggle} />
|
||||
<section className="rounded-2xl border border-dashed border-[#e3e3e3] p-4 text-xs text-gray-400">
|
||||
更多全局配置(如 Good Night 模式、导出默认行为等)将在后续版本开放。
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function OptionToggleGroup({
|
||||
title,
|
||||
optionKeys,
|
||||
options,
|
||||
onToggle,
|
||||
}: {
|
||||
title: string;
|
||||
optionKeys: (keyof PageOptionsState)[];
|
||||
options: PageOptionsState;
|
||||
onToggle: (key: keyof PageOptionsState) => void;
|
||||
}) {
|
||||
return (
|
||||
<section>
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-gray-400">{title}</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
{optionKeys.map((key) => (
|
||||
<OptionToggle key={key} optionKey={key} options={options} onToggle={onToggle} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function OptionToggle({
|
||||
optionKey,
|
||||
options,
|
||||
onToggle,
|
||||
}: {
|
||||
optionKey: keyof PageOptionsState;
|
||||
options: PageOptionsState;
|
||||
onToggle: (key: keyof PageOptionsState) => void;
|
||||
}) {
|
||||
const meta = OPTION_META[optionKey];
|
||||
const Icon = meta.icon;
|
||||
const active = options[optionKey];
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between rounded-2xl border border-transparent bg-[#f9fafc] px-3 py-2 text-left shadow-sm transition hover:border-[#dbe7ff]"
|
||||
onClick={() => onToggle(optionKey)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-xl border p-2",
|
||||
active ? "border-[#cddfff] bg-[#eef4ff]" : "border-transparent bg-white",
|
||||
)}
|
||||
>
|
||||
<Icon className={cn("h-4 w-4", active ? "text-[#2563eb]" : "text-gray-500")} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{meta.label}</div>
|
||||
<div className="text-xs text-gray-400">{meta.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className={cn("text-xs font-semibold", active ? "text-[#2563eb]" : "text-gray-400")}>
|
||||
{active ? "已开启" : "已关闭"}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function StatsCell({ label, value }: { label: string; value: number }) {
|
||||
return (
|
||||
<div className="rounded-xl bg-white py-3 text-center shadow-sm">
|
||||
<div className="text-xs text-gray-400">{label}</div>
|
||||
<div className="mt-1 text-lg font-semibold text-gray-900">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user