From 7fedfa8f50b0393ebd839881f5b4f320ca9aa051 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Sun, 14 Jun 2026 18:49:15 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20colonnes=20multi-colonnes=20(layout=20c?= =?UTF-8?q?=C3=B4te=20=C3=A0=20c=C3=B4te=20style=20Notion)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Architecture nested nodes: columns container → column children - CSS seamless (pas de bordures, fin séparateur vertical entre colonnes) - isolating: true sur les deux nœuds (curseur reste dans sa colonne) - Commands addColumnBefore/addColumnAfter/deleteColumn - Slash menu + drag handle + raccourci Mod+Shift+L - i18n FR/EN complet --- memento-note/app/globals.css | 35 +++- memento-note/components/block-action-menu.tsx | 10 +- memento-note/components/rich-text-editor.tsx | 10 +- .../components/tiptap-columns-extension.tsx | 159 ++++++++++++++++++ memento-note/locales/en.json | 6 + memento-note/locales/fr.json | 6 + 6 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 memento-note/components/tiptap-columns-extension.tsx diff --git a/memento-note/app/globals.css b/memento-note/app/globals.css index cfe4540..981d8ff 100644 --- a/memento-note/app/globals.css +++ b/memento-note/app/globals.css @@ -3000,4 +3000,37 @@ html.font-system * { border-radius: 2px; box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.3); color: inherit; -} \ No newline at end of file +} + +/* Multi-column layout — seamless like Notion, no boxes */ +.tiptap-columns { + display: flex !important; + gap: 24px !important; + margin: 8px 0 !important; + padding: 0 !important; + border: none !important; + background: none !important; + border-radius: 0 !important; +} + +.tiptap-column { + flex: 1 1 0% !important; + min-width: 0 !important; + padding: 0 !important; + border: none !important; + border-radius: 0 !important; + background: none !important; + border-left: 1px solid color-mix(in oklab, var(--foreground) 8%, transparent) !important; + padding-left: 24px !important; + overflow-wrap: break-word !important; + word-break: break-word !important; +} + +.tiptap-column:first-child { + border-left: none !important; + padding-left: 0 !important; +} + +.tiptap-column > *:first-child { margin-top: 0; } +.tiptap-column > *:last-child { margin-bottom: 0; } +.tiptap-column p { margin: 4px 0; } \ No newline at end of file diff --git a/memento-note/components/block-action-menu.tsx b/memento-note/components/block-action-menu.tsx index ab61524..54ad401 100644 --- a/memento-note/components/block-action-menu.tsx +++ b/memento-note/components/block-action-menu.tsx @@ -14,12 +14,14 @@ import { Heading1, Heading2, Heading3, List, ListOrdered, CheckSquare, Quote, CodeXml, Database, ArrowUp, ArrowDown, AlignLeft, ClipboardCopy, Sparkles, - ChevronsRightLeft, MessageSquareWarning, ListTree, + ChevronsRightLeft, MessageSquareWarning, ListTree, Columns3, + Plus, Minus, } from 'lucide-react' import { replaceBlockWithStructuredView } from '@/components/tiptap-structured-view-block-extension' import { insertToggleBlock, turnIntoToggleBlock } from '@/components/tiptap-toggle-extension' import { turnIntoCalloutBlock } from '@/components/tiptap-callout-extension' import { insertOutlineBlock } from '@/components/tiptap-outline-extension' +import { insertColumnsBlock } from '@/components/tiptap-columns-extension' interface BlockActionMenuProps { editor: Editor @@ -391,6 +393,12 @@ export function BlockActionMenu({ {t('richTextEditor.slashOutline')} + {/* Colonnes */} + +
{/* Création de diagramme */} diff --git a/memento-note/components/rich-text-editor.tsx b/memento-note/components/rich-text-editor.tsx index 34fb9fa..4a1d344 100644 --- a/memento-note/components/rich-text-editor.tsx +++ b/memento-note/components/rich-text-editor.tsx @@ -32,6 +32,7 @@ import { OutlineExtension, insertOutlineBlock } from './tiptap-outline-extension import { FindReplaceBar, FindReplaceExtension } from './editor-find-replace-bar' import { LinkPreviewExtension, insertLinkPreview, isUrl } from './tiptap-link-preview-extension' import { MathEquationExtension, InlineMathExtension, insertMathEquation } from './tiptap-math-extension' +import { ColumnsExtension, ColumnNode, insertColumnsBlock } from './tiptap-columns-extension' import { RtlPreserveExtension } from './tiptap-rtl-preserve-extension' import { ClipArticleExtension } from './tiptap-clip-article-extension' import { BlockPicker, type BlockSuggestion } from './block-picker' @@ -68,7 +69,7 @@ import { FileText, Pilcrow, MessageSquare, AlignLeft, AlignCenter, AlignRight, Superscript as SuperscriptIcon, Subscript as SubscriptIcon, Expand, Plus, SpellCheck, Languages, BookOpen, Presentation, BarChart3, Database, - ChevronsRightLeft, MessageSquareWarning, ListTree, FunctionSquare + ChevronsRightLeft, MessageSquareWarning, ListTree, FunctionSquare, Columns3 } from 'lucide-react' import { cn } from '@/lib/utils' import { toast } from 'sonner' @@ -231,6 +232,10 @@ const slashCommands: SlashItem[] = [ title: 'Math', description: 'LaTeX equation block', icon: FunctionSquare, category: 'Basic blocks', shortcut: '$$', command: (e) => { insertMathEquation(e) }, }, + { + title: 'Columns', description: 'Side-by-side layout', icon: Columns3, category: 'Basic blocks', shortcut: '/cols', + command: (e) => { insertColumnsBlock(e, 2) }, + }, ] async function aiReformulate(text: string, option: string, t: any, language?: string): Promise { @@ -480,6 +485,8 @@ export const RichTextEditor = forwardRef Number(el.getAttribute('index')) || 0, + renderHTML: (attrs) => ({ index: attrs.index }), + }, + } + }, + + parseHTML() { + return [{ tag: 'div[data-type="column"]' }] + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'div', + mergeAttributes(HTMLAttributes, { + 'data-type': 'column', + class: 'tiptap-column', + }), + 0, + ] + }, +}) + +export const ColumnsExtension = Node.create({ + name: 'columns', + group: 'block', + content: 'column{1,}', + isolating: true, + defining: true, + allowGapCursor: false, + + addAttributes() { + return { + cols: { + default: 2, + parseHTML: (el) => Number(el.getAttribute('cols')) || 2, + renderHTML: (attrs) => ({ cols: attrs.cols }), + }, + } + }, + + parseHTML() { + return [{ tag: 'div[data-type="columns"]' }] + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'div', + mergeAttributes(HTMLAttributes, { + 'data-type': 'columns', + class: 'tiptap-columns', + }), + 0, + ] + }, + + addCommands() { + return { + addColumnAfter: () => ({ state, dispatch }: { state: EditorState; dispatch?: (tr: Transaction) => void }) => { + return addOrDeleteCol(state, dispatch, 'addAfter') + }, + addColumnBefore: () => ({ state, dispatch }: { state: EditorState; dispatch?: (tr: Transaction) => void }) => { + return addOrDeleteCol(state, dispatch, 'addBefore') + }, + deleteColumn: () => ({ state, dispatch }: { state: EditorState; dispatch?: (tr: Transaction) => void }) => { + return addOrDeleteCol(state, dispatch, 'delete') + }, + } + }, + + addKeyboardShortcuts() { + return { + 'Mod-Shift-L': () => this.editor.commands.insertContent({ + type: this.name, + attrs: { cols: 2 }, + content: [ + { type: 'column', attrs: { index: 0 }, content: [{ type: 'paragraph' }] }, + { type: 'column', attrs: { index: 1 }, content: [{ type: 'paragraph' }] }, + ], + }), + } + }, +}) + +function addOrDeleteCol(state: EditorState, dispatch?: (tr: Transaction) => void, type?: 'addBefore' | 'addAfter' | 'delete'): boolean { + const maybeColumns = findParentNode((node: PMNode) => node.type.name === 'columns')(state.selection) + const maybeColumn = findParentNode((node: PMNode) => node.type.name === 'column')(state.selection) + + if (!maybeColumns || !maybeColumn) return false + + const cols = maybeColumns.node + const colIndex = maybeColumn.node.attrs.index + const colsJSON = cols.toJSON() + + let nextIndex = colIndex + + if (type === 'delete') { + if (colsJSON.content.length <= 1) return false + nextIndex = Math.max(0, colIndex - 1) + colsJSON.content.splice(colIndex, 1) + } else { + nextIndex = type === 'addBefore' ? colIndex : colIndex + 1 + colsJSON.content.splice(nextIndex, 0, { + type: 'column', + attrs: { index: colIndex }, + content: [{ type: 'paragraph' }], + }) + } + + colsJSON.attrs.cols = colsJSON.content.length + colsJSON.content.forEach((colJSON: any, index: number) => { + colJSON.attrs.index = index + }) + + const nextCols = PMNode.fromJSON(state.schema, colsJSON) + + let nextSelectPos = maybeColumns.pos + 1 + nextCols.content.forEach((col: PMNode, _pos: number, index: number) => { + if (index < nextIndex) nextSelectPos += col.nodeSize + }) + + if (dispatch) { + const tr = state.tr + tr.replaceWith(maybeColumns.pos, maybeColumns.pos + maybeColumns.node.nodeSize, nextCols) + tr.setSelection(TextSelection.near(tr.doc.resolve(nextSelectPos + 1))) + dispatch(tr) + } + + return true +} + +export function insertColumnsBlock(editor: any, columns: number = 2) { + const content = Array.from({ length: columns }, (_, i) => ({ + type: 'column', + attrs: { index: i }, + content: [{ type: 'paragraph' }], + })) + editor.chain().focus().insertContent({ + type: 'columns', + attrs: { cols: columns }, + content, + }).run() +} diff --git a/memento-note/locales/en.json b/memento-note/locales/en.json index c3e2fe8..8cd651e 100644 --- a/memento-note/locales/en.json +++ b/memento-note/locales/en.json @@ -2445,6 +2445,12 @@ "mathCancel": "Cancel", "mathAi": "Write with AI", "mathAiPlaceholder": "Describe the equation in words... (e.g: quadratic formula)", + "slashColumns": "Columns", + "slashColumnsDesc": "Put content side by side", + "columnsRemove": "Remove a column", + "columnsAdd": "Add a column", + "columnsDelete": "Delete columns", + "columnsLabel": "columns", "calloutDelete": "Delete callout", "calloutUnwrap": "Disable callout", "calloutInfo": "Information", diff --git a/memento-note/locales/fr.json b/memento-note/locales/fr.json index 84b6d11..01bd3eb 100644 --- a/memento-note/locales/fr.json +++ b/memento-note/locales/fr.json @@ -2449,6 +2449,12 @@ "mathCancel": "Annuler", "mathAi": "Écrire avec l'IA", "mathAiPlaceholder": "Décris l'équation en mots... (ex: formule quadratique)", + "slashColumns": "Colonnes", + "slashColumnsDesc": "Mettre du contenu côte à côte", + "columnsRemove": "Retirer une colonne", + "columnsAdd": "Ajouter une colonne", + "columnsDelete": "Supprimer les colonnes", + "columnsLabel": "colonnes", "calloutDelete": "Supprimer l'encadré", "calloutUnwrap": "Désactiver l'encadré", "calloutInfo": "Information",