Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,8 @@
"remove": "Remove language"
},
"richText": {
"undo": "Undo",
"redo": "Redo",
"bold": "Bold",
"italic": "Italic",
"bulletList": "Bullet list",
Expand Down
2 changes: 2 additions & 0 deletions messages/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,8 @@
"remove": "Hapus bahasa"
},
"richText": {
"undo": "Urungkan",
"redo": "Ulangi",
"bold": "Tebal",
"italic": "Miring",
"bulletList": "Daftar poin",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/components/editor/rich-text/rich-text-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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.
Expand Down
27 changes: 26 additions & 1 deletion src/components/editor/rich-text/rich-text-toolbar.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -22,13 +24,36 @@ 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(),
}),
});

const has = (feature: RichTextFeature) => props.features.includes(feature);

return (
<div className="flex items-center gap-0.5 rounded-md border border-input p-0.5">
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={t("undo")}
disabled={!state.canUndo}
onClick={() => props.editor.chain().focus().undo().run()}
>
<Undo2 />
</Button>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={t("redo")}
disabled={!state.canRedo}
onClick={() => props.editor.chain().focus().redo().run()}
>
<Redo2 />
</Button>
<Separator orientation="vertical" className="mx-0.5 my-1" />
{has("bold") && (
<Toggle
size="xs"
Expand Down