Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5bf1c24
feat(i18n): add translation keys for ordered list and indent/outdent …
nikolai-andree May 11, 2026
8d13243
feat(editor): add ordered list, indent and outdent toolbar buttons
nikolai-andree May 11, 2026
cf3c132
feat(editor): add ordered list entry to slash commands menu
nikolai-andree May 11, 2026
67b8ec4
fix(editor): correct indent/outdent button disabled logic
nikolai-andree May 11, 2026
9d6eb70
fix(editor): fix ordered list toggle and indent/outdent disabled state
nikolai-andree May 11, 2026
fd08136
fix(editor): add explicit Tab/Shift-Tab handling for list indentation
nikolai-andree May 11, 2026
8c6117d
fix(editor): reactive list state, robust toggle and depth check
nikolai-andree May 11, 2026
544345c
fix(editor): apply same empty-item toggle fix to bullet list button
nikolai-andree May 11, 2026
4dd25fa
chore(editor): remove stray comments
nikolai-andree May 11, 2026
9554d2b
fix(editor): align MD bullet/ordered list markers to Turndown convention
reniko May 12, 2026
357ef25
feat(editor): add ordered list continuation on Enter in MD mode
reniko May 12, 2026
24ae169
feat(editor): add indentLines/outdentLines helpers for MD mode
reniko May 12, 2026
c444cef
fix(editor): wire indent/outdent buttons for MD mode, fix disabled state
reniko May 12, 2026
f5596ca
feat(i18n): add editor.listOptions translation key in all 13 locales
reniko May 12, 2026
4263051
feat(editor): replace four list buttons with ListMenuDropdown
reniko May 12, 2026
1c60db6
fix(editor): handle indented list items in Enter continuation (MD mode)
reniko May 13, 2026
e345848
fix(editor): use can() checks for indent/outdent disabled state in RT…
reniko May 13, 2026
92e609e
fix(editor): renumber ordered list items on indent, fix RT roundtrip
reniko May 13, 2026
6de0f88
fix(editor): empty line gets list marker on toggle-on, collapse curso…
reniko May 15, 2026
320106d
fix(editor): ordered list button now works on empty lines in MD mode
reniko May 15, 2026
7dea9ff
fix(editor): exit all list levels when toggling list in RT mode
reniko May 15, 2026
3b2afd9
fix(editor): make MD list toggle buttons indent-aware and support typ…
reniko May 15, 2026
9e28f51
fix(editor): remove blank lines before nested lists in RT→MD conversion
reniko May 15, 2026
20a5bd2
fix(editor): preserve nesting depth when switching list type in RT mode
reniko May 15, 2026
29eb71f
Revert "fix(editor): preserve nesting depth when switching list type …
reniko May 15, 2026
cace53c
fix(editor): switch nested list type in place via setNodeMarkup
reniko May 15, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,24 @@ export const KeyboardShortcuts = Extension.create<KeyboardShortcutsOptions>({
if (this.editor.isActive("table")) {
return this.editor.chain().focus().goToNextCell().run();
}
if (this.editor.isActive("orderedList") || this.editor.isActive("bulletList")) {
this.editor.chain().focus().sinkListItem("listItem").run();
return true;
}
return false;
},
"Shift-Tab": () => {
if (this.editor.isActive("table")) {
return this.editor.chain().focus().goToPreviousCell().run();
}
if (this.editor.isActive("orderedList") || this.editor.isActive("bulletList")) {
const { $anchor } = this.editor.state.selection;
const isNested = $anchor.node($anchor.depth - 3)?.type.name === "listItem";
if (isNested) {
return this.editor.chain().focus().liftListItem("listItem").run();
}
return true;
}
return false;
},
"Mod-r": () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Heading01Icon,
Heading02Icon,
LeftToRightListBulletIcon,
LeftToRightListNumberIcon,
CheckmarkSquare04Icon,
SourceCodeIcon,
QuoteUpIcon,
Expand Down Expand Up @@ -75,6 +76,14 @@ const getSlashCommands = (t: (key: string) => string): SlashCommandItem[] => [
editor.chain().focus().deleteRange(range).toggleBulletList().run();
},
},
{
title: t("editor.orderedList"),
description: t("editor.createOrderedList"),
icon: <LeftToRightListNumberIcon className="h-4 w-4" />,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
},
},
{
title: t("editor.taskList"),
description: t("editor.createTaskList"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ export const SyntaxHighlightedEditor = ({
e.preventDefault();
onLinkRequest?.(textarea.selectionStart !== textarea.selectionEnd);
} else if (e.key === "Enter" && !isMod && !e.shiftKey && !e.altKey) {
const newContent = MarkdownUtils.handleBulletListEnter(textarea);
const newContent =
MarkdownUtils.handleBulletListEnter(textarea) ??
MarkdownUtils.handleOrderedListEnter(textarea);
if (newContent !== null) {
e.preventDefault();
const { scrollTop, scrollLeft, selectionStart, selectionEnd } =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
"use client";

import { Editor, useEditorState } from "@tiptap/react";
import {
ArrowDown01Icon,
LeftToRightListBulletIcon,
LeftToRightListNumberIcon,
TextIndentMoreIcon,
TextIndentLessIcon,
} from "hugeicons-react";
import { Button } from "@/app/_components/GlobalComponents/Buttons/Button";
import { ToolbarDropdown } from "./ToolbarDropdown";
import { useTranslations } from "next-intl";
import * as MarkdownUtils from "@/app/_utils/markdown-editor-utils";

interface ListMenuDropdownProps {
editor: Editor | null;
isMarkdownMode: boolean;
onMarkdownChange?: (content: string) => void;
listState: {
isInList: boolean;
isNested: boolean;
isInBulletList: boolean;
isInOrderedList: boolean;
currentItemIsEmpty: boolean;
};
}

export const ListMenuDropdown = ({
editor,
isMarkdownMode,
onMarkdownChange,
listState,
}: ListMenuDropdownProps) => {
const t = useTranslations();

const canSink = useEditorState({
editor,
selector: ({ editor: e }) => (e ? e.can().sinkListItem("listItem") : false),
}) ?? false;

const canLift = useEditorState({
editor,
selector: ({ editor: e }) => (e ? e.can().liftListItem("listItem") : false),
}) ?? false;

if (!editor) return null;

const applyMarkdown = (fn: (ta: HTMLTextAreaElement) => string) => {
const textarea = document.getElementById(
"markdown-editor-textarea"
) as HTMLTextAreaElement;
if (textarea && onMarkdownChange) {
const scrollTop = textarea.scrollTop;
const scrollLeft = textarea.scrollLeft;
const newContent = fn(textarea);
const selectionStart = textarea.selectionStart;
const selectionEnd = textarea.selectionEnd;
onMarkdownChange(newContent);
requestAnimationFrame(() => {
const ta = document.getElementById(
"markdown-editor-textarea"
) as HTMLTextAreaElement;
if (ta) {
ta.focus({ preventScroll: true });
ta.setSelectionRange(selectionStart, selectionEnd);
ta.scrollTop = scrollTop;
ta.scrollLeft = scrollLeft;
}
});
}
};

const getCurrentListType = (): "bulletList" | "orderedList" | null => {
const { $from } = editor.state.selection;
for (let d = $from.depth; d > 0; d--) {
const name = $from.node(d).type.name;
if (name === "bulletList" || name === "orderedList") return name;
}
return null;
};

const liftOutOfList = () => {
let safety = 20;
while (
safety-- > 0 &&
(editor.isActive("bulletList") || editor.isActive("orderedList"))
) {
if (!editor.chain().focus().liftListItem("listItem").run()) break;
}
};

const handleBulletList = () => {
if (isMarkdownMode) {
applyMarkdown(MarkdownUtils.insertBulletList);
return;
}
if (getCurrentListType() === "bulletList") {
liftOutOfList();
} else {
editor.chain().focus().toggleBulletList().run();
}
};

const handleOrderedList = () => {
if (isMarkdownMode) {
applyMarkdown(MarkdownUtils.insertOrderedList);
return;
}
if (getCurrentListType() === "orderedList") {
liftOutOfList();
} else {
editor.chain().focus().toggleOrderedList().run();
}
};

const handleIndent = () => {
if (isMarkdownMode) {
applyMarkdown(MarkdownUtils.indentLines);
return;
}
editor.chain().focus().sinkListItem("listItem").run();
};

const handleOutdent = () => {
if (isMarkdownMode) {
applyMarkdown(MarkdownUtils.outdentLines);
return;
}
editor.chain().focus().liftListItem("listItem").run();
};

const isActive = listState.isInList;

const trigger = (
<Button
variant={isActive ? "secondary" : "ghost"}
size="sm"
onMouseDown={(e) => e.preventDefault()}
className="flex items-center gap-1"
title={t("editor.listOptions")}
>
<LeftToRightListBulletIcon className="h-4 w-4" />
<ArrowDown01Icon className="h-3 w-3" />
</Button>
);

return (
<ToolbarDropdown trigger={trigger}>
<div className="py-1">
<button
className="w-full flex items-center gap-2 px-3 py-1.5 text-left hover:bg-accent text-sm"
onMouseDown={(e) => e.preventDefault()}
onClick={handleBulletList}
>
<LeftToRightListBulletIcon className="h-4 w-4 shrink-0" />
<span>{t("editor.toggleBulletList")}</span>
</button>
<button
className="w-full flex items-center gap-2 px-3 py-1.5 text-left hover:bg-accent text-sm"
onMouseDown={(e) => e.preventDefault()}
onClick={handleOrderedList}
>
<LeftToRightListNumberIcon className="h-4 w-4 shrink-0" />
<span>{t("editor.toggleOrderedList")}</span>
</button>
<div className="my-1 border-t border-border" />
<button
className="w-full flex items-center gap-2 px-3 py-1.5 text-left hover:bg-accent text-sm disabled:opacity-50 disabled:cursor-not-allowed"
onMouseDown={(e) => e.preventDefault()}
onClick={handleIndent}
disabled={!isMarkdownMode && !canSink}
>
<TextIndentMoreIcon className="h-4 w-4 shrink-0" />
<span>{t("editor.indentListItem")}</span>
</button>
<button
className="w-full flex items-center gap-2 px-3 py-1.5 text-left hover:bg-accent text-sm disabled:opacity-50 disabled:cursor-not-allowed"
onMouseDown={(e) => e.preventDefault()}
onClick={handleOutdent}
disabled={!isMarkdownMode && !canLift}
>
<TextIndentLessIcon className="h-4 w-4 shrink-0" />
<span>{t("editor.outdentListItem")}</span>
</button>
</div>
</ToolbarDropdown>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Editor } from "@tiptap/react";
import { Editor, useEditorState } from "@tiptap/react";
import {
TextBoldIcon,
TextItalicIcon,
Expand All @@ -13,13 +13,13 @@ import {
Tv02Icon,
TextUnderlineIcon,
Image02Icon,
LeftToRightListBulletIcon,
} from "hugeicons-react";
import { Button } from "@/app/_components/GlobalComponents/Buttons/Button";
import { FileModal } from "@/app/_components/GlobalComponents/Modals/FilesModal/FileModal";
import { ImageSizeModal } from "@/app/_components/GlobalComponents/Modals/ImageSizeModal";
import { CodeBlockDropdown } from "@/app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/CodeBlocksDropdown";
import { DiagramsDropdown } from "@/app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/DiagramsDropdown";
import { ListMenuDropdown } from "@/app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/ListMenuDropdown";
import { TableInsertModal } from "@/app/_components/FeatureComponents/Notes/Parts/Table/TableInsertModal";
import { FontFamilyDropdown } from "@/app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/FontFamilyDropdown";
import { useState, useEffect } from "react";
Expand Down Expand Up @@ -100,6 +100,29 @@ export const TiptapToolbar = ({
}
}, [linkRequestPending, linkRequestHasSelection, isMarkdownMode, editor, onLinkRequestHandled]);

const listState = useEditorState({
editor,
selector: ({ editor: e }) => {
if (!e) return { isInList: false, isNested: false, isInBulletList: false, isInOrderedList: false, currentItemIsEmpty: false };
const isInBulletList = e.isActive('bulletList');
const isInOrderedList = e.isActive('orderedList');
const isInList = isInBulletList || isInOrderedList;
let isNested = false;
if (isInList) {
const { $anchor } = e.state.selection;
outer: for (let d = $anchor.depth; d >= 0; d--) {
if ($anchor.node(d).type.name === 'listItem') {
for (let d2 = d - 1; d2 >= 0; d2--) {
if ($anchor.node(d2).type.name === 'listItem') { isNested = true; break outer; }
}
break;
}
}
}
return { isInList, isNested, isInBulletList, isInOrderedList, currentItemIsEmpty: e.state.selection.$anchor.parent.textContent === '' };
},
}) ?? { isInList: false, isNested: false, isInBulletList: false, isInOrderedList: false, currentItemIsEmpty: false };

if (!editor) {
return null;
}
Expand Down Expand Up @@ -459,20 +482,12 @@ export const TiptapToolbar = ({
>
<Heading02Icon className="h-4 w-4" />
</Button>
<Button
variant={editor && editor.isActive("bulletList") ? "secondary" : "ghost"}
size="sm"
onMouseDown={(e) => e.preventDefault()}
onClick={() =>
handleDualModeButton(
() => editor.chain().focus().toggleBulletList().run(),
MarkdownUtils.insertBulletList
)
}
title={`${t('editor.toggleBulletList')} (${mod}+Shift+8)`}
>
<LeftToRightListBulletIcon className="h-4 w-4" />
</Button>
<ListMenuDropdown
editor={editor}
isMarkdownMode={isMarkdownMode}
onMarkdownChange={onMarkdownChange}
listState={listState}
/>
<Button
variant={editor && editor.isActive("blockquote") ? "secondary" : "ghost"}
size="sm"
Expand Down
5 changes: 5 additions & 0 deletions app/_translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1140,7 +1140,11 @@
"toggleStrikethrough": "Durchstreichung umschalten",
"toggleInlineCode": "Code-Formatierung umschalten",
"toggleHeading2": "Überschrift umschalten",
"listOptions": "Liste",
"toggleBulletList": "Aufzählungsliste umschalten",
"toggleOrderedList": "Nummerierte Liste umschalten",
"indentListItem": "Listenelement einrücken",
"outdentListItem": "Listenelement ausrücken",
"toggleBlockquote": "Zitat umschalten",
"toggleLink": "Verknüpfung einfügen",
"editImageSize": "Bilgröße ändern",
Expand Down Expand Up @@ -1174,6 +1178,7 @@
"heading2": "Überschrift 2",
"mediumSectionHeading": "Mittlere Abschnittsüberschrift",
"createBulletedList": "Aufzählungsliste erstellen",
"createOrderedList": "Nummerierte Liste erstellen",
"taskList": "Aufgabenliste erstellen",
"createTaskList": "Aufgabenliste erstellen",
"createCodeBlock": "Code-Block erstellen",
Expand Down
5 changes: 5 additions & 0 deletions app/_translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1169,7 +1169,11 @@
"toggleStrikethrough": "Toggle strikethrough",
"toggleInlineCode": "Toggle inline code",
"toggleHeading2": "Toggle heading 2",
"listOptions": "List",
"toggleBulletList": "Toggle bullet list",
"toggleOrderedList": "Toggle ordered list",
"indentListItem": "Indent list item",
"outdentListItem": "Outdent list item",
"toggleBlockquote": "Toggle blockquote",
"toggleLink": "Toggle link",
"editImageSize": "Edit image size",
Expand Down Expand Up @@ -1203,6 +1207,7 @@
"heading2": "Heading 2",
"mediumSectionHeading": "Medium section heading",
"createBulletedList": "Create a bulleted list",
"createOrderedList": "Create a numbered list",
"taskList": "Task List",
"createTaskList": "Create a task list",
"createCodeBlock": "Create a code block",
Expand Down
5 changes: 5 additions & 0 deletions app/_translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1144,7 +1144,11 @@
"toggleStrikethrough": "Alternar tachado",
"toggleInlineCode": "Alternar código en línea",
"toggleHeading2": "Alternar encabezado 2",
"listOptions": "Lista",
"toggleBulletList": "Alternar lista con viñetas",
"toggleOrderedList": "Alternar lista numerada",
"indentListItem": "Sangrar elemento de lista",
"outdentListItem": "Quitar sangría del elemento",
"toggleBlockquote": "Alternar cita",
"toggleLink": "Alternar enlace",
"editImageSize": "Editar tamaño de imagen",
Expand Down Expand Up @@ -1178,6 +1182,7 @@
"heading2": "Encabezado 2",
"mediumSectionHeading": "Encabezado de sección mediana",
"createBulletedList": "Crear una lista con viñetas",
"createOrderedList": "Crear una lista numerada",
"taskList": "Lista de tareas",
"createTaskList": "Crear una lista de tareas",
"createCodeBlock": "Crear un bloque de código",
Expand Down
Loading
Loading