style(templates): 优化模板中心 UI 视觉与文案展示
This commit is contained in:
1 parent
40ede9141a
commit
4594b5c377
5 files changed
+650
-93
No files matched your search
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "速记",
|
||||
|
||||
Reference in new issue
Block a user