Files
Momento/memento-note/components/tiptap-toggle-extension.tsx
Antigravity 940c3daf62
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m7s
CI / Deploy production (on server) (push) Has been skipped
feat: Wizard IA + Export PDF + fixes blocs
- Wizard IA: création de carnet complet (étudiant/prof/ingénieur)
  - 3 profils, choix niveau, nombre de notes (3-12)
  - Étapes numérotées, messages de progression, écran de succès
  - Notes riches avec callouts, toggles, équations, colonnes
  - Structured View auto-créé avec propriétés Statut/Difficulté
  - Embeddings en arrière-plan
- Export PDF: clone le DOM réel, rend KaTeX, préserve couleurs callouts
- Fix: boutons toggle/callout en hover-only
- Fix: parsing JSON robuste (backslashes LaTeX)
- Fix: flushSync warning (queueMicrotask)
- Fix: drag handle clamp viewport
- Bouton wizard dans la sidebar ( à côté du +)
- i18n FR/EN complet
2026-06-14 19:51:02 +00:00

173 lines
4.8 KiB
TypeScript

'use client'
import { Node, mergeAttributes } from '@tiptap/core'
import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap/react'
import { ChevronRight, X, Trash2 } from 'lucide-react'
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { useLanguage } from '@/lib/i18n'
const ToggleView = ({ node, updateAttributes, deleteNode, getPos, editor }: any) => {
const [opened, setOpened] = useState(node.attrs.opened !== false)
const { t } = useLanguage()
const toggle = () => {
const next = !opened
setOpened(next)
updateAttributes({ opened: next })
}
const unwrap = () => {
const pos = getPos()
if (typeof pos !== 'number') return
const { state, view } = editor
const toggleNode = state.doc.nodeAt(pos)
if (!toggleNode) return
const children: any[] = []
toggleNode.forEach((child: any) => {
children.push(child.toJSON())
})
if (children.length === 0) children.push({ type: 'paragraph' })
editor.chain().focus().deleteRange({ from: pos, to: pos + toggleNode.nodeSize }).run()
editor.chain().focus().insertContentAt(pos, children).run()
}
return (
<NodeViewWrapper className="toggle-block group/toggle my-1 rounded-lg border border-border/50 bg-muted/20" dir="auto">
<div className="flex items-center gap-1.5 px-2 py-1.5 border-b border-border/30">
<button
onClick={toggle}
contentEditable={false}
className="flex-shrink-0 p-0.5 rounded hover:bg-muted"
style={{
transform: opened ? 'rotate(90deg)' : 'rotate(0deg)',
transition: 'transform 150ms ease',
}}
>
<ChevronRight className="h-4 w-4 text-muted-foreground" />
</button>
<span
className="text-xs font-medium text-muted-foreground flex-1 cursor-pointer"
onClick={toggle}
contentEditable={false}
>
{opened ? t('richTextEditor.toggleOpened') : t('richTextEditor.toggleClosed')}
</span>
<div className="flex items-center gap-0.5 opacity-0 group-hover/toggle:opacity-100 transition-opacity">
<button
onClick={unwrap}
contentEditable={false}
className="p-0.5 rounded hover:bg-muted text-muted-foreground"
title={t('richTextEditor.toggleUnwrap')}
>
<X className="h-3.5 w-3.5" />
</button>
<button
onClick={deleteNode}
contentEditable={false}
className="p-0.5 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
title={t('richTextEditor.toggleDelete')}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
</div>
<div className={cn('px-3 py-2', !opened && 'hidden')}>
<NodeViewContent className="toggle-content space-y-1" />
</div>
</NodeViewWrapper>
)
}
export const ToggleExtension = Node.create({
name: 'toggleBlock',
group: 'block',
content: 'block+',
defining: true,
addAttributes() {
return {
opened: {
default: true,
parseHTML: (element) => element.getAttribute('data-opened') !== 'false',
renderHTML: (attributes) => ({
'data-opened': attributes.opened,
}),
},
}
},
parseHTML() {
return [
{
tag: 'div[data-type="toggle-block"]',
},
{
tag: 'details',
getAttrs: (element) => {
if (typeof element === 'string') return false
return { opened: !(element as HTMLElement).hasAttribute('closed') }
},
},
]
},
renderHTML({ HTMLAttributes }) {
return [
'div',
mergeAttributes(HTMLAttributes, {
'data-type': 'toggle-block',
class: 'toggle-block',
}),
0,
]
},
addNodeView() {
return ReactNodeViewRenderer(ToggleView)
},
addKeyboardShortcuts() {
return {
'Mod-Shift-T': () => this.editor.commands.insertContent({
type: this.name,
attrs: { opened: true },
content: [{ type: 'paragraph' }],
}),
}
},
})
export function insertToggleBlock(editor: any) {
editor.chain().focus().insertContent({
type: 'toggleBlock',
attrs: { opened: true },
content: [
{ type: 'paragraph' },
],
}).run()
}
/**
* Transforme un bloc existant en section repliable.
* Le contenu du bloc original est déplacé dans le toggle.
*/
export function turnIntoToggleBlock(editor: any, blockPos: number, blockNode: any) {
if (!blockNode || blockPos < 0) return
const nodeJson = blockNode.toJSON()
editor.chain().focus().deleteRange({
from: blockPos,
to: blockPos + blockNode.nodeSize,
}).insertContentAt(blockPos, {
type: 'toggleBlock',
attrs: { opened: true },
content: [nodeJson],
}).setTextSelection(blockPos + 2).run()
}