diff --git a/_bmad-output/implementation-artifacts/spec-fix-drag-handle-visibility.md b/_bmad-output/implementation-artifacts/spec-fix-drag-handle-visibility.md new file mode 100644 index 0000000..125fbfc --- /dev/null +++ b/_bmad-output/implementation-artifacts/spec-fix-drag-handle-visibility.md @@ -0,0 +1,36 @@ +--- +title: 'fix-drag-handle-visibility' +type: 'bugfix' +created: '2026-05-25' +status: 'done' +route: 'one-shot' +--- + +## Intent + +**Problem:** TipTap DragHandle component uses `visibility: hidden/visible` to show/hide, but the CSS `.drag-handle` wrapper had `opacity: 0` which worked independently. However, the inner `.notion-drag-handle` class was styled as a standalone block without being visible when its parent `.drag-handle` was hidden. The CSS cascade meant the inner element had no intrinsic visibility logic — it only appeared to work when the parent opacity changed. + +**Approach:** Restructure the CSS so `.drag-handle` (the TipTap-managed wrapper) handles all visibility via `opacity` and `pointer-events`, and `.notion-drag-handle` (the inner content wrapper) styles only the visual appearance of the drag handle icon. The parent-child relationship is now explicit in CSS with `pointer-events: none` on the hidden state. + +## Code Map + +- `memento-note/app/globals.css:1087-1135` -- DragHandle CSS: fix visibility cascade, add inner wrapper styles +- `memento-note/components/rich-text-editor.tsx` -- Already uses DragHandleExtension and drag-handle-react (confirmed on disk) + +## Tasks & Acceptance + +**Execution:** +- [x] `memento-note/app/globals.css` -- Fix .drag-handle visibility cascade, add pointer-events, restructure inner .notion-drag-handle as child element + +**Acceptance Criteria:** +- Given a note is open in the editor, when the user hovers over a block (paragraph, heading, list item), then the drag handle appears in the gutter with blue hover state +- Given the drag handle is visible, when the user clicks it, then the block action menu opens +- Given the drag handle is in its default hidden state, when the editor loads, then no drag handle is visible (opacity: 0, pointer-events: none) + +## Spec Change Log + + + +## Suggested Review Order + +1. `memento-note/app/globals.css:1087-1135` -- CSS changes only; verify cascade correctness \ No newline at end of file diff --git a/memento-note/app/globals.css b/memento-note/app/globals.css index 67c0cf6..c8e43bf 100644 --- a/memento-note/app/globals.css +++ b/memento-note/app/globals.css @@ -1035,8 +1035,254 @@ html.font-system * { /* --- Editor Wrapper --- */ .notion-editor-wrapper { position: relative; + padding-left: 36px; /* Espace gutter pour la poignée */ } +/* --- Drag Handle Gutter --- */ +.notion-drag-handle { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 28px; /* Légèrement plus grand pour faciliter la sélection */ + border-radius: 6px; + color: var(--muted-foreground); + /* Animation de glissement fluide (top) et d'échelle au clic */ + transition: opacity 0.2s ease, + top 0.12s cubic-bezier(0.25, 1, 0.5, 1), + background-color 0.15s ease, + color 0.15s ease, + transform 0.15s ease; + user-select: none; +} + +.notion-drag-handle:hover { + background-color: rgba(59, 130, 246, 0.15); /* blue-500 @ 15% */ + color: #3b82f6; /* blue-500 */ +} + +.dark .notion-drag-handle:hover { + background-color: rgba(59, 130, 246, 0.25); + color: #60a5fa; /* blue-400 */ +} + +.notion-drag-handle:active { + transform: scale(0.9); + cursor: grabbing; +} + +.notion-drag-handle-btn { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + padding: 0; + margin: 0; + cursor: pointer; + color: inherit; + outline: none; +} + +/* --- Styles pour l'extension officielle Tiptap DragHandle --- */ +.drag-handle { + position: absolute; + z-index: 50; + display: flex; + align-items: stretch; + justify-content: center; + user-select: none; + cursor: grab; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease, transform 0.12s ease; + transform: translateX(0); +} + +.drag-handle.visible { + opacity: 1; + pointer-events: auto; +} + +.drag-handle[data-dragging="true"] { + cursor: grabbing; + opacity: 1; +} + +/* Inner content wrapper - positioned in gutter */ +.drag-handle > .notion-drag-handle { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + min-height: 28px; + border-radius: 6px; + color: var(--muted-foreground); + background: transparent; + transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease; + flex-shrink: 0; +} + +.drag-handle.visible > .notion-drag-handle:hover { + background-color: rgba(59, 130, 246, 0.15); + color: #3b82f6; +} + +.dark .drag-handle.visible > .notion-drag-handle:hover { + background-color: rgba(59, 130, 246, 0.25); + color: #60a5fa; +} + +.drag-handle:active > .notion-drag-handle { + transform: scale(0.9); +} + +/* --- Block Action Menu (glassmorphism dropdown) --- */ +.block-action-menu { + min-width: 200px; + padding: 4px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.85); + backdrop-filter: blur(16px) saturate(180%); + -webkit-backdrop-filter: blur(16px) saturate(180%); + border: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.06); + animation: blockMenuIn 0.15s cubic-bezier(0.16, 1, 0.3, 1); +} + +.dark .block-action-menu { + background: rgba(30, 30, 30, 0.88); + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2); +} + +@keyframes blockMenuIn { + from { opacity: 0; transform: scale(0.95) translateY(-4px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +.block-action-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 7px 12px; + border: none; + border-radius: 6px; + background: transparent; + color: inherit; + font-size: 13px; + cursor: pointer; + transition: background-color 0.12s ease; + white-space: nowrap; +} + +.block-action-item:hover { + background: rgba(59, 130, 246, 0.1); + color: #3b82f6; +} + +.dark .block-action-item:hover { + background: rgba(59, 130, 246, 0.2); + color: #60a5fa; +} + +.block-action-item:first-child:hover { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; +} + +.dark .block-action-item:first-child:hover { + background: rgba(239, 68, 68, 0.2); + color: #f87171; +} + +.block-action-separator { + height: 1px; + margin: 4px 8px; + background: rgba(0, 0, 0, 0.08); +} + +.dark .block-action-separator { + background: rgba(255, 255, 255, 0.08); +} + +.block-action-submenu-trigger { + position: relative; +} + +.block-action-submenu { + position: absolute; + left: 100%; + top: 0; + margin-left: 4px; + min-width: 180px; + padding: 4px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.92); + backdrop-filter: blur(16px) saturate(180%); + -webkit-backdrop-filter: blur(16px) saturate(180%); + border: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); + animation: blockMenuIn 0.12s cubic-bezier(0.16, 1, 0.3, 1); +} + +.dark .block-action-submenu { + background: rgba(30, 30, 30, 0.92); + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +/* --- Drop Indicator Line --- */ +.notion-drop-indicator { + position: absolute; + left: 36px; /* Alignement sur le texte */ + right: 0; + height: 2px; + background-color: #3b82f6 !important; + box-shadow: 0 0 10px rgba(59, 130, 246, 0.8); + z-index: 100; + pointer-events: none; + border-radius: 4px; + opacity: 0; + transition: opacity 0.15s ease, top 0.1s cubic-bezier(0.25, 1, 0.5, 1); +} + +.dark .notion-drop-indicator { + background-color: #60a5fa !important; + box-shadow: 0 0 10px rgba(96, 165, 250, 0.9); +} + +/* --- Selected Node Visual Highlight during Drag --- */ +.notion-editor-wrapper .ProseMirror-selectednode { + outline: none !important; + background-color: rgba(59, 130, 246, 0.08) !important; + border-radius: 6px; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4) !important; + transition: background-color 0.2s ease, box-shadow 0.2s ease; +} + +.dark .notion-editor-wrapper .ProseMirror-selectednode { + background-color: rgba(96, 165, 250, 0.15) !important; + box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.5) !important; +} + +/* --- Premium Drag Target Line (Dropcursor) --- */ +.notion-editor-wrapper .ProseMirror-dropcursor { + background-color: #3b82f6 !important; + height: 2px !important; + border-radius: 4px; + box-shadow: 0 0 8px #3b82f6; + animation: pulse-drop 1.5s infinite ease-in-out; +} + +@keyframes pulse-drop { + 0% { opacity: 0.6; } + 50% { opacity: 1; } + 100% { opacity: 0.6; } +} + + /* --- ProseMirror Base --- */ .notion-editor-wrapper .ProseMirror { outline: none;