style(templates): 优化模板中心 UI 视觉与文案展示

This commit is contained in:
tianma-if committed 2026-07-25 12:30:49 +08:00
1 parent 40ede9141a
commit 4594b5c377
5 files changed
+650 -93

No files matched your search

+492 -48
View File
@@ -1,12 +1,50 @@
import { useMemo, useState } from "react";
import { Check, ChevronLeft, File as FileIcon, LayoutList, Pencil, Plus, Trash2, X } from "lucide-react";
import {
BookOpen,
Calendar,
Check,
CheckSquare,
ChevronLeft,
Eye,
FileText,
LayoutList,
Pencil,
Plus,
Sparkles,
Sun,
Trash2,
X,
Zap,
Tag,
ArrowRight,
FilePlus
} from "lucide-react";
import { useTranslation } from "react-i18next";
import { marked } from "marked";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { getMemoTemplates, type MemoTemplate } from "@/lib/app-helpers";
import { WORKSPACE_PAGE_TITLE_CLASSNAME } from "@/lib/workspace-ui";
import type { MemoTemplate as SavedMemoTemplate } from "@edgeever/shared";
const renderTemplateIcon = (iconName?: string) => {
switch (iconName) {
case "zap":
return <Zap className="h-4.5 w-4.5 text-amber-600" />;
case "calendar":
return <Calendar className="h-4.5 w-4.5 text-blue-600" />;
case "checklist":
return <CheckSquare className="h-4.5 w-4.5 text-emerald-600" />;
case "book":
return <BookOpen className="h-4.5 w-4.5 text-purple-600" />;
case "sun":
return <Sun className="h-4.5 w-4.5 text-rose-600" />;
default:
return <FileText className="h-4.5 w-4.5 text-emerald-700" />;
}
};
export const TemplatesPane = ({
canCreateMemo,
isCreating,
@@ -33,26 +71,53 @@ export const TemplatesPane = ({
const [editingTemplate, setEditingTemplate] = useState<SavedMemoTemplate | null>(null);
const [creatingTemplate, setCreatingTemplate] = useState(false);
const [draft, setDraft] = useState({ name: "", description: "", title: "", contentMarkdown: "", tags: "" });
const [editorTab, setEditorTab] = useState<"raw" | "preview">("raw");
const [previewTemplate, setPreviewTemplate] = useState<{
title: string;
description: string | null;
noteTitle?: string | null;
tags: string[];
contentMarkdown: string;
badge: string;
onUse: () => void;
} | null>(null);
const [deleteConfirmTemplate, setDeleteConfirmTemplate] = useState<SavedMemoTemplate | null>(null);
const startEditing = (template: SavedMemoTemplate) => {
setCreatingTemplate(false);
setEditingTemplate(template);
setDraft({ name: template.name, description: template.description ?? "", title: template.title ?? "", contentMarkdown: template.contentMarkdown, tags: template.tags.join(", ") });
setEditorTab("raw");
setDraft({
name: template.name,
description: template.description ?? "",
title: template.title ?? "",
contentMarkdown: template.contentMarkdown,
tags: template.tags.join(", "),
});
};
const startCreating = () => {
setEditingTemplate(null);
setCreatingTemplate(true);
setEditorTab("raw");
setDraft({ name: "", description: "", title: "", contentMarkdown: "", tags: "" });
};
const cancelEditing = () => {
setEditingTemplate(null);
setCreatingTemplate(false);
};
const saveEditing = async () => {
if (!draft.name.trim()) return;
const payload = {
name: draft.name.trim(), description: draft.description.trim() || null, title: draft.title.trim() || null,
contentMarkdown: draft.contentMarkdown, tags: draft.tags.split(",").map((tag) => tag.trim()).filter(Boolean),
name: draft.name.trim(),
description: draft.description.trim() || null,
title: draft.title.trim() || null,
contentMarkdown: draft.contentMarkdown,
tags: draft.tags.split(",").map((tag) => tag.trim()).filter(Boolean),
};
if (creatingTemplate) {
await onCreateSavedTemplate(payload);
@@ -60,80 +125,459 @@ export const TemplatesPane = ({
return;
}
if (!editingTemplate) return;
await onUpdateSavedTemplate(editingTemplate.id, {
...payload,
});
await onUpdateSavedTemplate(editingTemplate.id, payload);
cancelEditing();
};
const draftHtmlPreview = useMemo(
() => (draft.contentMarkdown ? (marked.parse(draft.contentMarkdown) as string) : ""),
[draft.contentMarkdown]
);
const previewHtmlContent = useMemo(
() => (previewTemplate?.contentMarkdown ? (marked.parse(previewTemplate.contentMarkdown) as string) : ""),
[previewTemplate?.contentMarkdown]
);
return (
<div className="flex h-full min-h-0 min-w-0 flex-col bg-white">
<header className="flex h-[calc(4rem+env(safe-area-inset-top))] shrink-0 items-end border-b border-slate-200 px-6 pb-3 pt-[env(safe-area-inset-top)] lg:h-16 lg:items-center lg:pb-0 lg:pt-0">
<div className="flex h-full min-h-0 min-w-0 flex-col bg-slate-50/60">
{/* Header */}
<header className="flex h-[calc(3.75rem+env(safe-area-inset-top))] shrink-0 items-end border-b border-slate-200/80 bg-white px-6 pb-3 pt-[env(safe-area-inset-top)] lg:h-16 lg:items-center lg:pb-0 lg:pt-0 shadow-2xs">
<div className="flex min-w-0 items-center gap-3">
<Button size="icon" variant="ghost" title={t("common.back")} aria-label={t("common.back")} onClick={onClose}>
<ChevronLeft className="h-5 w-5 text-slate-500" />
</Button>
<div className="min-w-0">
<h1 className={`flex items-center gap-2 ${WORKSPACE_PAGE_TITLE_CLASSNAME}`}><LayoutList className="h-4 w-4 text-emerald-700" />{t("templates.title")}</h1>
<h1 className={`flex items-center gap-2 text-slate-900 ${WORKSPACE_PAGE_TITLE_CLASSNAME}`}>
<LayoutList className="h-4.5 w-4.5 text-emerald-600" />
{t("templates.title")}
</h1>
<p className="mt-0.5 text-xs text-slate-500">{t("templates.description")}</p>
</div>
</div>
</header>
<main className="min-h-0 flex-1 overflow-y-auto px-4 py-4 lg:px-6 lg:py-6">
<div className="mx-auto w-full max-w-4xl">
<section className="mb-8">
<div className="mb-3 flex items-center justify-start gap-3">
<h2 className="text-xs font-semibold uppercase tracking-wide text-slate-500">{t("templates.myTemplates")}</h2>
<Button type="button" size="sm" onClick={startCreating} disabled={isCreating || creatingTemplate || Boolean(editingTemplate)}>
<Plus className="mr-1 h-4 w-4" />{t("templates.create")}
</Button>
{/* Main Content Area */}
<main className="min-h-0 flex-1 overflow-y-auto px-4 py-6 lg:px-8 lg:py-8">
<div className="mx-auto w-full max-w-5xl space-y-8">
{/* Section 1: My Custom Templates */}
<section>
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="flex items-center gap-2 text-sm font-bold text-slate-900">
<span>{t("templates.myTemplates")}</span>
<span className="rounded-full bg-emerald-100/70 px-2 py-0.5 text-[11px] font-semibold text-emerald-800">
{savedTemplates.length}
</span>
</h2>
<p className="mt-0.5 text-xs text-slate-500">{t("templates.myTemplatesSubtitle")}</p>
</div>
{!creatingTemplate && !editingTemplate && (
<Button
type="button"
size="sm"
variant="outline"
className="gap-1.5 border-slate-200 bg-white text-slate-700 hover:bg-slate-50 hover:text-slate-900 text-xs font-semibold shadow-2xs"
onClick={startCreating}
disabled={isCreating}
>
<Plus className="h-3.5 w-3.5 text-emerald-600" />
{t("templates.create")}
</Button>
)}
</div>
{(editingTemplate || creatingTemplate) && (
<div className="mb-4 space-y-3 rounded-lg border border-slate-200 bg-slate-50 p-4">
{/* Form for Creating / Editing Template */}
{(editingTemplate || creatingTemplate) && (
<div className="mb-6 rounded-xl border border-emerald-200 bg-white p-5 shadow-xs transition-all">
<div className="mb-4 flex items-center justify-between border-b border-slate-100 pb-3">
<h3 className="flex items-center gap-2 text-sm font-bold text-slate-900">
<Pencil className="h-4 w-4 text-emerald-600" />
{creatingTemplate ? t("templates.createTemplateTitle") : t("templates.editTemplateTitle")}
</h3>
<Button size="icon" variant="ghost" className="h-7 w-7 text-slate-400 hover:text-slate-600" onClick={cancelEditing}>
<X className="h-4 w-4" />
</Button>
</div>
<div className="space-y-4">
<div className="grid gap-3 sm:grid-cols-2">
<Input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={t("templates.name")} />
<Input value={draft.description} onChange={(event) => setDraft((current) => ({ ...current, description: event.target.value }))} placeholder={t("templates.descriptionField")} />
<Input value={draft.title} onChange={(event) => setDraft((current) => ({ ...current, title: event.target.value }))} placeholder={t("templates.noteTitle")} />
<Input value={draft.tags} onChange={(event) => setDraft((current) => ({ ...current, tags: event.target.value }))} placeholder={t("templates.tags")} />
<div>
<label className="mb-1 block text-xs font-medium text-slate-700">{t("templates.name")}</label>
<Input
value={draft.name}
onChange={(e) => setDraft((c) => ({ ...c, name: e.target.value }))}
placeholder={t("templates.namePlaceholder")}
/>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-slate-700">{t("templates.noteTitle")}</label>
<Input
value={draft.title}
onChange={(e) => setDraft((c) => ({ ...c, title: e.target.value }))}
placeholder={t("templates.noteTitlePlaceholder")}
/>
</div>
</div>
<textarea className="min-h-48 w-full resize-y rounded-md border border-slate-200 bg-white px-3 py-2 font-mono text-xs text-slate-950 outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-emerald-500/20" value={draft.contentMarkdown} onChange={(event) => setDraft((current) => ({ ...current, contentMarkdown: event.target.value }))} aria-label={t("templates.content")} />
<div className="flex justify-end gap-2">
<Button type="button" size="sm" variant="ghost" onClick={cancelEditing}><X className="mr-1 h-4 w-4" />{t("common.cancel")}</Button>
<Button type="button" size="sm" onClick={() => void saveEditing()} disabled={!draft.name.trim()}><Check className="mr-1 h-4 w-4" />{t("common.save")}</Button>
<div className="grid gap-3 sm:grid-cols-2">
<div>
<label className="mb-1 block text-xs font-medium text-slate-700">{t("templates.descriptionField")}</label>
<Input
value={draft.description}
onChange={(e) => setDraft((c) => ({ ...c, description: e.target.value }))}
placeholder={t("templates.descriptionPlaceholder")}
/>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-slate-700">{t("templates.tags")}</label>
<Input
value={draft.tags}
onChange={(e) => setDraft((c) => ({ ...c, tags: e.target.value }))}
placeholder={t("templates.tagsPlaceholder")}
/>
</div>
</div>
{/* Content Area with Editor / Preview Toggle */}
<div>
<div className="mb-1.5 flex items-center justify-between">
<label className="text-xs font-medium text-slate-700">{t("templates.content")}</label>
<div className="inline-flex rounded-lg bg-slate-100 p-0.5 text-xs font-medium text-slate-600">
<button
type="button"
className={`rounded-md px-2.5 py-1 transition ${editorTab === "raw" ? "bg-white text-slate-900 shadow-2xs" : "hover:text-slate-900"}`}
onClick={() => setEditorTab("raw")}
>
{t("templates.rawEditor")}
</button>
<button
type="button"
className={`rounded-md px-2.5 py-1 transition ${editorTab === "preview" ? "bg-white text-slate-900 shadow-2xs" : "hover:text-slate-900"}`}
onClick={() => setEditorTab("preview")}
>
{t("templates.previewEditor")}
</button>
</div>
</div>
{editorTab === "raw" ? (
<textarea
className="min-h-48 w-full resize-y rounded-lg border border-slate-200 bg-white p-3.5 font-mono text-xs text-slate-900 outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-emerald-500/20"
value={draft.contentMarkdown}
onChange={(e) => setDraft((c) => ({ ...c, contentMarkdown: e.target.value }))}
placeholder={t("templates.contentPlaceholder")}
/>
) : (
<div className="min-h-48 w-full overflow-y-auto rounded-lg border border-slate-200 bg-slate-50 p-4 text-xs text-slate-800">
{draftHtmlPreview ? (
<div className="prose prose-xs max-w-none" dangerouslySetInnerHTML={{ __html: draftHtmlPreview }} />
) : (
<span className="italic text-slate-400">{t("templates.contentPlaceholder")}</span>
)}
</div>
)}
</div>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" size="sm" variant="outline" onClick={cancelEditing}>
<X className="mr-1 h-3.5 w-3.5" />
{t("common.cancel")}
</Button>
<Button
type="button"
size="sm"
className="bg-emerald-600 hover:bg-emerald-700 text-white gap-1.5 font-semibold"
onClick={() => void saveEditing()}
disabled={!draft.name.trim() || isCreating}
>
<Check className="h-3.5 w-3.5" />
{t("common.save")}
</Button>
</div>
</div>
)}
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
</div>
)}
{/* Custom Templates Grid */}
{savedTemplates.length > 0 ? (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{savedTemplates.map((template) => (
<div key={template.id} className="rounded-lg border border-emerald-100 bg-emerald-50/40 p-4">
<button className="block w-full text-left disabled:opacity-50" type="button" disabled={!canCreateMemo || isCreating} onClick={() => onUseSavedTemplate(template)}>
<span className="text-sm font-semibold text-slate-950">{template.name}</span>
<span className="mt-1 block line-clamp-2 text-xs leading-5 text-slate-500">{template.description || template.title || t("templates.savedDescription")}</span>
</button>
<div className="mt-3 flex gap-3 text-xs">
<button className="inline-flex items-center gap-1 text-slate-600 hover:text-slate-800" type="button" disabled={isCreating} onClick={() => startEditing(template)}><Pencil className="h-3 w-3" />{t("templates.edit")}</button>
<button className="inline-flex items-center gap-1 text-rose-600 hover:text-rose-700" type="button" disabled={isCreating} onClick={() => onDeleteSavedTemplate(template)}><Trash2 className="h-3 w-3" />{t("templates.delete")}</button>
<div
key={template.id}
className="group relative flex flex-col justify-between rounded-xl border border-slate-200 bg-white p-4 transition-all duration-200 hover:border-emerald-300 hover:shadow-md"
>
<div>
<div className="mb-2 flex items-start justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700">
<FilePlus className="h-4 w-4" />
</span>
<h3 className="truncate font-bold text-slate-900 text-sm">{template.name}</h3>
</div>
<span className="shrink-0 rounded-full bg-emerald-50 px-2 py-0.5 text-[10px] font-semibold text-emerald-700 border border-emerald-200/60">
{t("templates.badgeCustom")}
</span>
</div>
<p className="mt-1 line-clamp-2 text-xs leading-relaxed text-slate-500 min-h-[2.25rem]">
{template.description || template.title || t("templates.savedDescription")}
</p>
{template.tags.length > 0 && (
<div className="mt-2.5 flex flex-wrap gap-1">
{template.tags.map((tag) => (
<span key={tag} className="inline-flex items-center gap-0.5 rounded-md bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">
<Tag className="h-2.5 w-2.5 text-slate-400" />
{tag}
</span>
))}
</div>
)}
</div>
<div className="mt-4 border-t border-slate-100 pt-3 flex items-center justify-between gap-2">
<div className="flex items-center gap-1 text-slate-400">
<button
type="button"
className="p-1 hover:text-slate-700 transition rounded-md hover:bg-slate-100"
title={t("templates.edit")}
disabled={isCreating}
onClick={() => startEditing(template)}
>
<Pencil className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="p-1 hover:text-rose-600 transition rounded-md hover:bg-rose-50"
title={t("templates.delete")}
disabled={isCreating}
onClick={() => setDeleteConfirmTemplate(template)}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="p-1 hover:text-slate-700 transition rounded-md hover:bg-slate-100"
title={t("templates.previewTemplate")}
onClick={() =>
setPreviewTemplate({
title: template.name,
description: template.description,
noteTitle: template.title,
tags: template.tags,
contentMarkdown: template.contentMarkdown,
badge: t("templates.badgeCustom"),
onUse: () => onUseSavedTemplate(template),
})
}
>
<Eye className="h-3.5 w-3.5" />
</button>
</div>
<button
type="button"
className="inline-flex items-center gap-1.5 rounded-lg border border-emerald-200/80 bg-emerald-50/70 px-3 py-1.5 text-xs font-semibold text-emerald-800 hover:bg-emerald-600 hover:text-white hover:border-emerald-600 transition-all duration-200 disabled:opacity-50"
disabled={!canCreateMemo || isCreating}
onClick={() => onUseSavedTemplate(template)}
>
{t("templates.useThisTemplate")}
<ArrowRight className="h-3 w-3" />
</button>
</div>
</div>
))}
</div>
) : !creatingTemplate && !editingTemplate ? (
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-slate-200 bg-white p-7 text-center shadow-2xs">
<div className="mb-2.5 flex h-11 w-11 items-center justify-center rounded-full bg-emerald-50 text-emerald-600">
<Sparkles className="h-5 w-5" />
</div>
<h3 className="text-sm font-bold text-slate-900">{t("templates.emptyMyTemplates")}</h3>
<p className="mt-1 max-w-sm text-xs leading-relaxed text-slate-500">{t("templates.emptyMyTemplatesHint")}</p>
<Button type="button" size="sm" variant="outline" className="mt-3.5 gap-1.5 border-slate-200 text-xs font-semibold" onClick={startCreating}>
<Plus className="h-3.5 w-3.5 text-emerald-600" />
{t("templates.create")}
</Button>
</div>
) : null}
</section>
{/* Section 2: Built-in Templates */}
<section>
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-500">{t("templates.builtIn")}</h2>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{memoTemplates.map((template) => (
<button key={template.id} className="group flex min-h-32 flex-col rounded-lg border border-slate-200 bg-white p-4 text-left transition hover:border-slate-300 hover:bg-slate-50 disabled:opacity-50" type="button" disabled={!canCreateMemo || isCreating} onClick={() => onCreateMemo(template)}>
<span className="flex h-9 w-9 items-center justify-center rounded-md border border-slate-200 bg-white text-emerald-700 transition group-hover:border-slate-300"><FileIcon className="h-4 w-4" /></span>
<span className="mt-4 text-sm font-semibold text-slate-950">{template.title}</span>
<span className="mt-1 line-clamp-2 text-xs leading-5 text-slate-500">{template.description}</span>
</button>
))}
<div className="mb-4">
<h2 className="text-sm font-bold text-slate-900">{t("templates.builtIn")}</h2>
<p className="mt-0.5 text-xs text-slate-500">{t("templates.builtInSubtitle")}</p>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{memoTemplates.map((template) => {
const theme = template.themeColor ?? {
iconBg: "bg-emerald-100/80 text-emerald-700",
iconText: "text-emerald-700",
badgeBg: "bg-emerald-50 text-emerald-800 border-emerald-200/60",
badgeText: "text-emerald-800",
accentBorder: "hover:border-emerald-300",
};
return (
<div
key={template.id}
className={`group relative flex flex-col justify-between rounded-xl border border-slate-200 bg-white p-4 transition-all duration-200 hover:shadow-md ${theme.accentBorder}`}
>
<div>
<div className="mb-2.5 flex items-center justify-between gap-2">
<div className="flex items-center gap-2.5 min-w-0">
<span className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${theme.iconBg}`}>
{renderTemplateIcon(template.iconName)}
</span>
<h3 className="truncate font-bold text-slate-900 text-sm">{template.title}</h3>
</div>
<span className="shrink-0 rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-600">
{t("templates.badgeBuiltIn")}
</span>
</div>
<p className="line-clamp-2 text-xs leading-relaxed text-slate-500 min-h-[2.25rem]">
{template.description}
</p>
</div>
<div className="mt-4 border-t border-slate-100 pt-3 flex items-center justify-between gap-2">
<button
type="button"
className="inline-flex items-center gap-1 text-xs text-slate-500 hover:text-slate-800 transition py-1"
onClick={() =>
setPreviewTemplate({
title: template.title,
description: template.description,
tags: template.tags,
contentMarkdown: template.contentMarkdown,
badge: t("templates.badgeBuiltIn"),
onUse: () => onCreateMemo(template),
})
}
>
<Eye className="h-3.5 w-3.5 text-slate-400" />
{t("templates.previewTemplate")}
</button>
<button
type="button"
className="inline-flex items-center gap-1.5 rounded-lg border border-emerald-200/80 bg-emerald-50/70 px-3 py-1.5 text-xs font-semibold text-emerald-800 hover:bg-emerald-600 hover:text-white hover:border-emerald-600 transition-all duration-200 disabled:opacity-50"
disabled={!canCreateMemo || isCreating}
onClick={() => onCreateMemo(template)}
>
{t("templates.useThisTemplate")}
<ArrowRight className="h-3 w-3" />
</button>
</div>
</div>
);
})}
</div>
</section>
{!canCreateMemo && <div className="mt-5 rounded-md border border-amber-100 bg-amber-50 px-3 py-2 text-xs text-amber-800">{t("templates.unavailable")}</div>}
{!canCreateMemo && (
<div className="rounded-xl border border-amber-200 bg-amber-50 p-3.5 text-xs font-medium text-amber-900">
{t("templates.unavailable")}
</div>
)}
</div>
</main>
{/* Preview Dialog */}
{previewTemplate && (
<Dialog open={true} onOpenChange={(open) => { if (!open) setPreviewTemplate(null); }}>
<DialogContent className="max-w-xl bg-white p-0 overflow-hidden border border-slate-200 rounded-xl shadow-xl">
<DialogHeader className="border-b border-slate-100 px-6 py-4 text-left bg-slate-50/50">
<div className="flex items-center gap-2">
<span className="rounded-md bg-emerald-100 px-2 py-0.5 text-[11px] font-semibold text-emerald-800">
{previewTemplate.badge}
</span>
<DialogTitle className="text-base font-bold text-slate-900">{previewTemplate.title}</DialogTitle>
</div>
{previewTemplate.description && (
<DialogDescription className="mt-1 text-xs text-slate-500 leading-relaxed">
{previewTemplate.description}
</DialogDescription>
)}
</DialogHeader>
<div className="max-h-[60vh] overflow-y-auto p-6 space-y-4">
{previewTemplate.noteTitle && (
<div className="rounded-lg bg-slate-100/70 p-2.5 text-xs text-slate-700">
<span className="font-semibold text-slate-900">{t("templates.noteTitle")}: </span>
{previewTemplate.noteTitle}
</div>
)}
<div>
<h4 className="mb-2 text-xs font-semibold text-slate-500 uppercase tracking-wider">{t("templates.previewTitle")}</h4>
<div className="rounded-xl border border-slate-200 bg-white p-4 text-xs text-slate-800 shadow-2xs min-h-[120px]">
<div className="prose prose-xs max-w-none" dangerouslySetInnerHTML={{ __html: previewHtmlContent }} />
</div>
</div>
</div>
<DialogFooter className="border-t border-slate-100 px-6 py-3.5 bg-slate-50/50 flex justify-end gap-2">
<Button type="button" variant="outline" size="sm" onClick={() => setPreviewTemplate(null)}>
{t("common.close")}
</Button>
<Button
type="button"
size="sm"
className="bg-emerald-600 hover:bg-emerald-700 text-white gap-1.5 font-semibold"
disabled={!canCreateMemo || isCreating}
onClick={() => {
const onUse = previewTemplate.onUse;
setPreviewTemplate(null);
onUse();
}}
>
{t("templates.useThisTemplate")}
<ArrowRight className="h-3.5 w-3.5" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
{/* Delete Confirmation Dialog */}
{deleteConfirmTemplate && (
<Dialog open={true} onOpenChange={(open) => { if (!open) setDeleteConfirmTemplate(null); }}>
<DialogContent className="max-w-md bg-white p-6 border border-slate-200 rounded-xl shadow-xl">
<DialogHeader className="text-left">
<DialogTitle className="text-base font-bold text-slate-900">
{t("templates.deleteConfirmTitle", { name: deleteConfirmTemplate.name })}
</DialogTitle>
<DialogDescription className="mt-2 text-xs text-slate-500 leading-relaxed">
{t("templates.deleteConfirmDescription")}
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-6 flex justify-end gap-2">
<Button type="button" variant="outline" size="sm" onClick={() => setDeleteConfirmTemplate(null)}>
{t("common.cancel")}
</Button>
<Button
type="button"
size="sm"
variant="danger"
onClick={() => {
const target = deleteConfirmTemplate;
setDeleteConfirmTemplate(null);
onDeleteSavedTemplate(target);
}}
>
{t("common.delete")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</div>
);
};
@@ -73,46 +73,62 @@ export const TemplatesDialog = ({
<div className="max-h-[60vh] overflow-y-auto p-5">
{savedTemplates.length > 0 && (
<section className="mb-5">
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">{t("templates.myTemplates")}</h3>
<section className="mb-6">
<h3 className="mb-2 text-xs font-bold text-slate-900">{t("templates.myTemplates")}</h3>
{editingTemplate && (
<div className="mb-3 space-y-3 rounded-md border border-slate-200 bg-slate-50 p-3">
<div className="grid gap-3 sm:grid-cols-2">
<Input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={t("templates.name")} />
<Input value={draft.description} onChange={(event) => setDraft((current) => ({ ...current, description: event.target.value }))} placeholder={t("templates.descriptionField")} />
<Input value={draft.title} onChange={(event) => setDraft((current) => ({ ...current, title: event.target.value }))} placeholder={t("templates.noteTitle")} />
<Input value={draft.tags} onChange={(event) => setDraft((current) => ({ ...current, tags: event.target.value }))} placeholder={t("templates.tags")} />
<div className="mb-4 space-y-3 rounded-xl border border-emerald-200/80 bg-white p-4 shadow-xs">
<h4 className="text-xs font-semibold text-slate-800">{t("templates.editTemplateTitle")}</h4>
<div className="grid gap-2.5 sm:grid-cols-2">
<Input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={t("templates.namePlaceholder")} />
<Input value={draft.title} onChange={(event) => setDraft((current) => ({ ...current, title: event.target.value }))} placeholder={t("templates.noteTitlePlaceholder")} />
<Input value={draft.description} onChange={(event) => setDraft((current) => ({ ...current, description: event.target.value }))} placeholder={t("templates.descriptionPlaceholder")} />
<Input value={draft.tags} onChange={(event) => setDraft((current) => ({ ...current, tags: event.target.value }))} placeholder={t("templates.tagsPlaceholder")} />
</div>
<textarea
className="min-h-40 w-full resize-y rounded-md border border-slate-200 bg-white px-3 py-2 font-mono text-xs text-slate-950 outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-emerald-500/20"
className="min-h-36 w-full resize-y rounded-lg border border-slate-200 bg-white p-3 font-mono text-xs text-slate-900 outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-emerald-500/20"
value={draft.contentMarkdown}
onChange={(event) => setDraft((current) => ({ ...current, contentMarkdown: event.target.value }))}
aria-label={t("templates.content")}
placeholder={t("templates.contentPlaceholder")}
/>
<div className="flex justify-end gap-2">
<Button type="button" size="sm" variant="ghost" onClick={cancelEditing}><X className="mr-1 h-4 w-4" />{t("common.cancel")}</Button>
<Button type="button" size="sm" onClick={() => void saveEditing()} disabled={!draft.name.trim()}><Check className="mr-1 h-4 w-4" />{t("common.save")}</Button>
<Button type="button" size="sm" className="bg-emerald-600 hover:bg-emerald-700 text-white" onClick={() => void saveEditing()} disabled={!draft.name.trim()}><Check className="mr-1 h-4 w-4" />{t("common.save")}</Button>
</div>
</div>
)}
<div className="grid gap-3 sm:grid-cols-2">
{savedTemplates.map((template) => (
<div key={template.id} className="rounded-md border border-emerald-100 bg-emerald-50/40 p-3">
<div key={template.id} className="rounded-xl border border-slate-200 bg-white p-3.5 shadow-2xs hover:border-emerald-200 transition">
<button
className="block w-full text-left disabled:opacity-50"
type="button"
disabled={!canCreateMemo || isCreating}
onClick={() => onUseSavedTemplate(template)}
>
<span className="text-sm font-semibold text-slate-950">{template.name}</span>
<span className="mt-1 block line-clamp-2 text-xs leading-5 text-slate-500">{template.description || template.title || t("templates.savedDescription")}</span>
<div className="flex items-center justify-between gap-1">
<span className="text-sm font-bold text-slate-900 truncate">{template.name}</span>
<span className="shrink-0 rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-700">
{t("templates.badgeCustom")}
</span>
</div>
<span className="mt-1 block line-clamp-2 text-xs leading-relaxed text-slate-500">{template.description || template.title || t("templates.savedDescription")}</span>
</button>
<div className="mt-2 flex gap-3 text-xs">
<button className="inline-flex items-center gap-1 text-slate-600 hover:text-slate-800" type="button" disabled={isCreating} onClick={() => startEditing(template)}>
<Pencil className="h-3 w-3" />{t("templates.edit")}
</button>
<button className="text-rose-600 hover:text-rose-700" type="button" disabled={isCreating} onClick={() => onDeleteSavedTemplate(template)}>
{t("templates.delete")}
<div className="mt-2.5 flex items-center justify-between border-t border-slate-100 pt-2 text-xs">
<div className="flex gap-2">
<button className="inline-flex items-center gap-1 text-slate-500 hover:text-slate-800 transition" type="button" disabled={isCreating} onClick={() => startEditing(template)}>
<Pencil className="h-3 w-3" />{t("templates.edit")}
</button>
<button className="text-rose-600 hover:text-rose-700 transition" type="button" disabled={isCreating} onClick={() => onDeleteSavedTemplate(template)}>
{t("templates.delete")}
</button>
</div>
<button
type="button"
className="text-xs font-semibold text-emerald-700 hover:text-emerald-800 transition"
disabled={!canCreateMemo || isCreating}
onClick={() => onUseSavedTemplate(template)}
>
{t("templates.useThisTemplate")}
</button>
</div>
</div>
@@ -120,26 +136,33 @@ export const TemplatesDialog = ({
</div>
</section>
)}
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">{t("templates.builtIn")}</h3>
<h3 className="mb-2 text-xs font-bold text-slate-900">{t("templates.builtIn")}</h3>
<div className="grid gap-3 sm:grid-cols-2">
{memoTemplates.map((template) => (
<button
key={template.id}
className="group flex min-h-28 flex-col rounded-md border border-slate-200 bg-white p-3 text-left transition hover:border-slate-300 hover:bg-slate-50 disabled:opacity-50"
className="group flex flex-col justify-between rounded-xl border border-slate-200 bg-white p-3.5 text-left transition hover:border-emerald-300 hover:shadow-xs disabled:opacity-50"
type="button"
disabled={!canCreateMemo || isCreating}
onClick={() => onCreateMemo(template)}
>
<span className="flex h-9 w-9 items-center justify-center rounded-md border border-slate-200 bg-white text-emerald-700 transition group-hover:border-slate-300">
<FileIcon className="h-4 w-4" />
</span>
<span className="mt-3 text-sm font-semibold text-slate-950">{template.title}</span>
<span className="mt-1 line-clamp-2 text-xs leading-5 text-slate-500">{template.description}</span>
<div>
<div className="flex items-center gap-2">
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700 font-medium">
<FileIcon className="h-4 w-4" />
</span>
<span className="text-sm font-bold text-slate-900">{template.title}</span>
</div>
<span className="mt-2 line-clamp-2 text-xs leading-relaxed text-slate-500">{template.description}</span>
</div>
<div className="mt-3 flex items-center justify-end text-xs font-semibold text-emerald-700 group-hover:text-emerald-800">
{t("templates.useThisTemplate")}
</div>
</button>
))}
</div>
{!canCreateMemo && (
<div className="mt-4 rounded-md border border-amber-100 bg-amber-50 px-3 py-2 text-xs text-amber-800">
<div className="mt-4 rounded-xl border border-amber-200 bg-amber-50 px-3.5 py-2.5 text-xs font-medium text-amber-900">
{t("templates.unavailable")}
</div>
)}
+48
View File
@@ -38,6 +38,14 @@ export type MemoTemplate = {
description: string;
contentMarkdown: string;
tags: string[];
iconName?: "zap" | "calendar" | "checklist" | "book" | "sun" | "file";
themeColor?: {
iconBg: string;
iconText: string;
badgeBg: string;
badgeText: string;
accentBorder: string;
};
};
export const getMemoTemplates = (t: TFunction): MemoTemplate[] => [
@@ -47,6 +55,14 @@ export const getMemoTemplates = (t: TFunction): MemoTemplate[] => [
description: t("templates.items.quickNote.description"),
contentMarkdown: t("templates.items.quickNote.contentMarkdown"),
tags: ["template", "quick-note"],
iconName: "zap",
themeColor: {
iconBg: "bg-amber-100/80 text-amber-700",
iconText: "text-amber-700",
badgeBg: "bg-amber-50 text-amber-800 border-amber-200/60",
badgeText: "text-amber-800",
accentBorder: "hover:border-amber-300/80",
},
},
{
id: "meeting",
@@ -54,6 +70,14 @@ export const getMemoTemplates = (t: TFunction): MemoTemplate[] => [
description: t("templates.items.meeting.description"),
contentMarkdown: t("templates.items.meeting.contentMarkdown"),
tags: ["template", "meeting"],
iconName: "calendar",
themeColor: {
iconBg: "bg-blue-100/80 text-blue-700",
iconText: "text-blue-700",
badgeBg: "bg-blue-50 text-blue-800 border-blue-200/60",
badgeText: "text-blue-800",
accentBorder: "hover:border-blue-300/80",
},
},
{
id: "checklist",
@@ -61,6 +85,14 @@ export const getMemoTemplates = (t: TFunction): MemoTemplate[] => [
description: t("templates.items.checklist.description"),
contentMarkdown: t("templates.items.checklist.contentMarkdown"),
tags: ["template", "checklist"],
iconName: "checklist",
themeColor: {
iconBg: "bg-emerald-100/80 text-emerald-700",
iconText: "text-emerald-700",
badgeBg: "bg-emerald-50 text-emerald-800 border-emerald-200/60",
badgeText: "text-emerald-800",
accentBorder: "hover:border-emerald-300/80",
},
},
{
id: "reading",
@@ -68,6 +100,14 @@ export const getMemoTemplates = (t: TFunction): MemoTemplate[] => [
description: t("templates.items.reading.description"),
contentMarkdown: t("templates.items.reading.contentMarkdown"),
tags: ["template", "reading"],
iconName: "book",
themeColor: {
iconBg: "bg-purple-100/80 text-purple-700",
iconText: "text-purple-700",
badgeBg: "bg-purple-50 text-purple-800 border-purple-200/60",
badgeText: "text-purple-800",
accentBorder: "hover:border-purple-300/80",
},
},
{
id: "daily",
@@ -75,6 +115,14 @@ export const getMemoTemplates = (t: TFunction): MemoTemplate[] => [
description: t("templates.items.daily.description"),
contentMarkdown: t("templates.items.daily.contentMarkdown"),
tags: ["template", "daily"],
iconName: "sun",
themeColor: {
iconBg: "bg-rose-100/80 text-rose-700",
iconText: "text-rose-700",
badgeBg: "bg-rose-50 text-rose-800 border-rose-200/60",
badgeText: "text-rose-800",
accentBorder: "hover:border-rose-300/80",
},
},
];
+30 -9
View File
@@ -798,26 +798,47 @@ export const enUS = {
},
},
templates: {
title: "Templates",
description: "Choose a template to create a note instantly.",
create: "New template",
title: "Template Center",
description: "Start notes instantly with preset structures or customize your reusable templates.",
create: "New Custom Template",
useTemplate: "New note from template",
useThisTemplate: "Use Template",
previewTemplate: "Preview",
previewTitle: "Template Preview",
createDescription: "Create a reusable template from scratch.",
myTemplates: "My templates",
builtIn: "Built-in templates",
savedDescription: "A custom template saved from a note.",
myTemplates: "My Custom Templates",
myTemplatesSubtitle: "Saved personal templates ready to reuse and edit anytime",
builtIn: "Recommended Templates",
builtInSubtitle: "Ready-to-use standard structures to jumpstart your writing",
savedDescription: "Custom template saved from a note.",
emptyMyTemplates: "No custom templates yet",
emptyMyTemplatesHint: "Click \"New Custom Template\" above or save any note as a template directly in the editor.",
delete: "Delete template",
deleteConfirmTitle: "Delete template \"{{name}}\"",
deleteConfirmDescription: "Are you sure you want to delete this template? This operation cannot be undone.",
saveAsTemplate: "Save as template",
saveAsTemplateSuccess: "Saved as custom template",
edit: "Edit template",
editTemplateTitle: "Edit Custom Template",
createTemplateTitle: "Create Custom Template",
name: "Template name",
namePlaceholder: "e.g., Weekly Summary / Reading Card",
descriptionField: "Template description",
noteTitle: "New note title",
tags: "Tags (comma-separated)",
content: "Template content",
descriptionPlaceholder: "Briefly describe when and how to use this template",
noteTitle: "Default note title",
noteTitlePlaceholder: "e.g., [Weekly] Untitled Note (leave blank to use template name)",
tags: "Preset tags",
tagsPlaceholder: "e.g., work, summary (comma separated)",
content: "Template Markdown content",
contentPlaceholder: "Enter preset Markdown content here...",
rawEditor: "Markdown Source",
previewEditor: "Preview",
templateNamePrompt: "Enter a template name",
templateSaved: "Template saved",
templateCreated: "Template created",
unavailable: "Select an available notebook before creating a note.",
badgeBuiltIn: "Built-in",
badgeCustom: "Custom",
items: {
quickNote: {
title: "Quick note",
+29 -8
View File
@@ -798,26 +798,47 @@ export const zhCN = {
},
},
templates: {
title: "模板",
description: "选择一个模板,直接创建新笔记。",
create: "新建模板",
title: "模板中心",
description: "选择预设结构快捷创建笔记,或打造你的专属自定义模板。",
create: "新建自定义模板",
useTemplate: "从模板新建笔记",
useThisTemplate: "使用此模板新建",
previewTemplate: "预览",
previewTitle: "模板结构预览",
createDescription: "从零开始创建一个可重复使用的模板。",
myTemplates: "我的模板",
builtIn: "内置模板",
myTemplates: "我的自定义模板",
myTemplatesSubtitle: "已保存的专属笔记模板,可随时复用与修改",
builtIn: "内置推荐模板",
builtInSubtitle: "开箱即用的标准笔记结构,点击即可快捷生成",
savedDescription: "基于已保存笔记的自定义模板。",
emptyMyTemplates: "暂无自定义模板",
emptyMyTemplatesHint: "你可以点击上方「新建自定义模板」,或在笔记编辑器中将常用笔记另存为模板。",
delete: "删除模板",
deleteConfirmTitle: "删除模板「{{name}}」",
deleteConfirmDescription: "确定要删除该模板吗?此操作不可恢复。",
saveAsTemplate: "另存为模板",
saveAsTemplateSuccess: "已成功保存为自定义模板",
edit: "编辑模板",
editTemplateTitle: "编辑自定义模板",
createTemplateTitle: "新建自定义模板",
name: "模板名称",
namePlaceholder: "例如:每周工作总结 / 论文阅读卡片",
descriptionField: "模板说明",
noteTitle: "新笔记标题",
tags: "标签(逗号分隔)",
content: "模板正文",
descriptionPlaceholder: "简要描述该模板的使用场景与用途",
noteTitle: "生成笔记时的默认标题",
noteTitlePlaceholder: "例如:【周报】未命名笔记(留空则跟随模板名)",
tags: "预设标签",
tagsPlaceholder: "例如:工作, 总结 (多个用逗号隔开)",
content: "模板预设正文 (Markdown)",
contentPlaceholder: "在此输入 Markdown 结构的预设正文...",
rawEditor: "源码编辑",
previewEditor: "效果预览",
templateNamePrompt: "请输入模板名称",
templateSaved: "模板已保存",
templateCreated: "模板已创建",
unavailable: "当前无法创建笔记,请先选择可用笔记本。",
badgeBuiltIn: "内置预设",
badgeCustom: "自定义",
items: {
quickNote: {
title: "速记",