diff --git a/README.md b/README.md index 31713e6..8715d6f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Customization applies to how the resume looks. It does not extend to layouts tha - Template gallery with search, sort, and live seed-data previews - Résumé library with live first-page thumbnails, rename, and delete - Print-accurate A4 preview with automatic pagination -- Rich text editing per field, scoped to ATS-safe formatting (bold, italic, lists, links) +- Rich text editing per field, scoped to ATS-safe formatting (bold, italic, lists, links), with undo and redo per field - Custom sections alongside the fixed types, all sharing the same restricted schema - Drag to reorder sections and toggle any section's visibility (entries within a section sort by date automatically) - Document-level presentation controls: font, font size, section spacing, line height, letter spacing, contact-icon visibility, and a one-click style reset diff --git a/messages/en.json b/messages/en.json index 449bf16..a594c13 100644 --- a/messages/en.json +++ b/messages/en.json @@ -790,6 +790,8 @@ "remove": "Remove language" }, "richText": { + "undo": "Undo", + "redo": "Redo", "bold": "Bold", "italic": "Italic", "bulletList": "Bullet list", diff --git a/messages/id.json b/messages/id.json index 0b905b4..bf8487c 100644 --- a/messages/id.json +++ b/messages/id.json @@ -790,6 +790,8 @@ "remove": "Hapus bahasa" }, "richText": { + "undo": "Urungkan", + "redo": "Ulangi", "bold": "Tebal", "italic": "Miring", "bulletList": "Daftar poin", diff --git a/package.json b/package.json index 6e2aee0..38d7c8d 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@tiptap/extension-paragraph": "^3.27.1", "@tiptap/extension-placeholder": "^3.27.1", "@tiptap/extension-text": "^3.27.1", + "@tiptap/extensions": "^3.27.1", "@tiptap/pm": "^3.27.1", "@tiptap/react": "^3.27.1", "class-variance-authority": "^0.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aabd12f..415bd82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,6 +79,9 @@ importers: '@tiptap/extension-text': specifier: ^3.27.1 version: 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1)) + '@tiptap/extensions': + specifier: ^3.27.1 + version: 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1) '@tiptap/pm': specifier: ^3.27.1 version: 3.27.1 diff --git a/src/components/editor/rich-text/rich-text-extensions.ts b/src/components/editor/rich-text/rich-text-extensions.ts index 55b0372..31cae17 100644 --- a/src/components/editor/rich-text/rich-text-extensions.ts +++ b/src/components/editor/rich-text/rich-text-extensions.ts @@ -9,6 +9,7 @@ import OrderedList from "@tiptap/extension-ordered-list"; import Paragraph from "@tiptap/extension-paragraph"; import Placeholder from "@tiptap/extension-placeholder"; import Text from "@tiptap/extension-text"; +import { UndoRedo } from "@tiptap/extensions"; import type { RichTextFeature } from "@/lib/resume/schema-registry"; /** @@ -22,7 +23,9 @@ export function buildRichTextExtensions( placeholder?: string, ): Extensions { const has = (feature: RichTextFeature) => features.includes(feature); - const extensions: Extensions = [Document, Paragraph, Text]; + // UndoRedo is a plugin over the transaction stream, not a node or mark, so + // per-field history costs the schema nothing. + const extensions: Extensions = [Document, Paragraph, Text, UndoRedo]; // Presentation-only decoration: renders empty-state text, adds no node or mark // to the schema, so it never affects parse/export structure. diff --git a/src/components/editor/rich-text/rich-text-toolbar.tsx b/src/components/editor/rich-text/rich-text-toolbar.tsx index 49a1b5e..837d60c 100644 --- a/src/components/editor/rich-text/rich-text-toolbar.tsx +++ b/src/components/editor/rich-text/rich-text-toolbar.tsx @@ -1,8 +1,10 @@ "use client"; import { type Editor, useEditorState } from "@tiptap/react"; -import { Bold, Italic, List, ListOrdered } from "lucide-react"; +import { Bold, Italic, List, ListOrdered, Redo2, Undo2 } from "lucide-react"; import { useTranslations } from "next-intl"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; import { Toggle } from "@/components/ui/toggle"; import type { RichTextFeature } from "@/lib/resume/schema-registry"; import { RichTextLinkPopover } from "./rich-text-link-popover"; @@ -22,6 +24,8 @@ export function RichTextToolbar(props: RichTextToolbarProps) { bulletList: editor.isActive("bulletList"), orderedList: editor.isActive("orderedList"), link: editor.isActive("link"), + canUndo: editor.can().undo(), + canRedo: editor.can().redo(), }), }); @@ -29,6 +33,27 @@ export function RichTextToolbar(props: RichTextToolbarProps) { return (