diff --git a/resources/views/rd/planning-design/index.blade.php b/resources/views/rd/planning-design/index.blade.php index ea0a525f..1b31754a 100644 --- a/resources/views/rd/planning-design/index.blade.php +++ b/resources/views/rd/planning-design/index.blade.php @@ -712,6 +712,78 @@ pointer-events: none; z-index: 100; } +/* Floating Format Toolbar (Notion-style) */ +.sb-format-bar { + position: fixed; z-index: 9999; + display: flex; align-items: center; gap: 2px; + padding: 4px 6px; background: #1e293b; border-radius: 8px; + box-shadow: 0 4px 16px rgba(0,0,0,0.25); + animation: sbFormatFadeIn 0.12s ease; +} +@keyframes sbFormatFadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } } +.sb-fmt-btn { + width: 28px; height: 28px; border: none; background: transparent; color: #94a3b8; + border-radius: 5px; cursor: pointer; font-size: 12px; + display: flex; align-items: center; justify-content: center; transition: all 0.1s; + position: relative; +} +.sb-fmt-btn:hover { background: rgba(255,255,255,0.12); color: #e2e8f0; } +.sb-fmt-btn.active { background: rgba(99,102,241,0.3); color: #a5b4fc; } +.sb-fmt-sep { width: 1px; height: 20px; background: rgba(255,255,255,0.12); margin: 0 2px; } +.sb-fmt-color-dot { + width: 14px; height: 14px; border-radius: 50%; border: 2px solid transparent; + cursor: pointer; transition: all 0.1s; +} +.sb-fmt-color-dot:hover { transform: scale(1.2); } +.sb-fmt-color-dot.active { border-color: #fff; } +.sb-fmt-dropdown { + position: absolute; top: 100%; left: 50%; transform: translateX(-50%); + margin-top: 6px; background: #1e293b; border-radius: 8px; padding: 8px; + box-shadow: 0 8px 24px rgba(0,0,0,0.3); min-width: 140px; z-index: 10; +} +.sb-fmt-dropdown-title { font-size: 9px; color: #64748b; font-weight: 700; text-transform: uppercase; margin-bottom: 6px; letter-spacing: 0.5px; } +.sb-fmt-color-grid { display: flex; flex-wrap: wrap; gap: 4px; } +.sb-fmt-size-option { + padding: 4px 8px; border-radius: 4px; font-size: 11px; color: #cbd5e1; + cursor: pointer; transition: all 0.1s; display: flex; align-items: center; justify-content: space-between; +} +.sb-fmt-size-option:hover { background: rgba(255,255,255,0.1); } +.sb-fmt-size-option.active { background: rgba(99,102,241,0.3); color: #a5b4fc; } + +/* Right-Click Context Menu */ +.sb-ctx-menu { + position: fixed; z-index: 10000; + background: #fff; border: 1px solid #e2e8f0; border-radius: 10px; + box-shadow: 0 8px 30px rgba(0,0,0,0.15); padding: 4px 0; + min-width: 200px; animation: sbCtxFadeIn 0.1s ease; +} +@keyframes sbCtxFadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } +.sb-ctx-item { + display: flex; align-items: center; gap: 8px; padding: 7px 14px; + font-size: 12px; color: #334155; cursor: pointer; transition: background 0.08s; +} +.sb-ctx-item:hover { background: #f1f5f9; } +.sb-ctx-item.danger { color: #ef4444; } +.sb-ctx-item.danger:hover { background: #fef2f2; } +.sb-ctx-icon { width: 18px; text-align: center; font-size: 13px; flex-shrink: 0; } +.sb-ctx-label { flex: 1; } +.sb-ctx-shortcut { font-size: 10px; color: #94a3b8; } +.sb-ctx-sep { height: 1px; background: #e2e8f0; margin: 4px 0; } +.sb-ctx-sub { position: relative; } +.sb-ctx-sub-panel { + position: absolute; left: 100%; top: -4px; + background: #fff; border: 1px solid #e2e8f0; border-radius: 10px; + box-shadow: 0 8px 24px rgba(0,0,0,0.12); padding: 8px; min-width: 160px; +} +.sb-ctx-sub-panel .sb-ctx-item { padding: 5px 10px; } +.sb-ctx-color-grid { display: flex; flex-wrap: wrap; gap: 5px; padding: 4px; } +.sb-ctx-color-swatch { + width: 22px; height: 22px; border-radius: 5px; cursor: pointer; + border: 2px solid transparent; transition: all 0.1s; +} +.sb-ctx-color-swatch:hover { transform: scale(1.15); } +.sb-ctx-color-swatch.active { border-color: #4338ca; } + /* Block type styles */ .sb-blk-text { padding: 6px 8px; font-size: 13px; line-height: 1.7; min-height: 24px; outline: none; color: #334155; } .sb-blk-text:empty::before { content: attr(data-placeholder); color: #c8d0da; } @@ -1555,7 +1627,8 @@ @mousedown="sbCanvasMouseDown($event)" @mousemove="sbCanvasMouseMove($event)" @mouseup="sbCanvasMouseUp($event)" - @click.self="if(!_sbLassoDone){ sbSelectedBlock = null; sbMultiSelected = []; } _sbLassoDone = false;" + @click.self="if(!_sbLassoDone){ sbSelectedBlock = null; sbMultiSelected = []; sbFormatBar = null; sbCtxMenu = null; sbFmtDropdown = null; } _sbLassoDone = false;" + @contextmenu.self.prevent="sbCtxMenu = null; sbCtxSub = null;" @dragover.prevent="$event.dataTransfer.dropEffect = 'copy'" @drop.prevent="sbDropMarker($event)" style="min-height: 600px; flex: 1;"> @@ -1572,9 +1645,20 @@ + {{-- Floating Format Toolbar (블록 선택 시 위에 나타남) --}} + + + {{-- Right-Click Context Menu --}} + + {{-- Desc Resizer --}}
@@ -2046,6 +2292,11 @@ function planningCanvas() { _sbHistory: [], _sbHistoryIdx: -1, _sbHistoryPaused: false, + // 서식 툴바 / 컨텍스트 메뉴 + sbFormatBar: null, // { x, y } — 선택된 블록 위에 표시 + sbFmtDropdown: null, // 'color' | 'bgColor' | 'fontSize' | null + sbCtxMenu: null, // { x, y, blockId, blockIdx } — 우클릭 메뉴 + sbCtxSub: null, // 'color' | 'bgColor' | 'align' | null — 하위 메뉴 sbTplOpen: false, sbTplTab: 'preset', sbTplSearch: '', @@ -3125,6 +3376,8 @@ function planningCanvas() { // 단일 선택 this.sbMultiSelected = []; this.sbSelectedBlock = blk.id; + this.sbCtxMenu = null; // 컨텍스트 메뉴 닫기 + this.sbFmtDropdown = null; this._sbDrag = { blk, startX: e.clientX, @@ -3132,6 +3385,7 @@ function planningCanvas() { origX: blk.x || 0, origY: blk.y || 0, }; + this.sbUpdateFormatBar(); e.preventDefault(); }, @@ -3166,6 +3420,10 @@ function planningCanvas() { }, sbCanvasMouseMove(e) { + // 드래그/리사이즈 중에는 서식 툴바 숨기기 + if (this._sbDrag || this._sbResize || this._sbMultiDrag || this._sbLasso) { + this.sbFormatBar = null; + } // 올가미 드래그 if (this._sbLasso) { const rect = this.$refs.sbCanvas.getBoundingClientRect(); @@ -3253,10 +3511,12 @@ function planningCanvas() { if (this._sbDrag) { this._sbDrag = null; this.autoSave(); + this.sbUpdateFormatBar(); } if (this._sbResize) { this._sbResize = null; this.autoSave(); + this.sbUpdateFormatBar(); } }, @@ -3391,6 +3651,172 @@ function planningCanvas() { this.autoSave(); }, + // ===== Format Toolbar & Context Menu ===== + sbGetBlk() { + const page = this.sbCurrentPage; + if (!page || !page.blocks) return null; + return page.blocks.find(b => b.id === this.sbSelectedBlock) || null; + }, + + sbCtxGetBlk() { + if (!this.sbCtxMenu) return null; + const page = this.sbCurrentPage; + if (!page || !page.blocks) return null; + return page.blocks.find(b => b.id === this.sbCtxMenu.blockId) || null; + }, + + sbUpdateFormatBar() { + if (!this.sbSelectedBlock || this.sbMultiSelected.length > 0) { + this.sbFormatBar = null; + return; + } + this.$nextTick(() => { + const canvas = this.$refs.sbCanvas; + if (!canvas) return; + const blockEl = canvas.querySelector('.sb-block.selected'); + if (!blockEl) { this.sbFormatBar = null; return; } + const rect = blockEl.getBoundingClientRect(); + // 블록 위 중앙에 배치 + const barW = 440; + let x = rect.left + rect.width / 2 - barW / 2; + let y = rect.top - 44; + // 화면 밖 보정 + if (x < 8) x = 8; + if (x + barW > window.innerWidth - 8) x = window.innerWidth - barW - 8; + if (y < 8) y = rect.bottom + 8; // 위에 공간 없으면 아래에 표시 + this.sbFormatBar = { x, y }; + }); + }, + + sbShowCtxMenu(blk, bi, e) { + this.sbSelectedBlock = blk.id; + this.sbMultiSelected = []; + this.sbFormatBar = null; + this.sbFmtDropdown = null; + // 화면 밖 보정 + let x = e.clientX, y = e.clientY; + const mw = 220, mh = 480; + if (x + mw > window.innerWidth) x = window.innerWidth - mw - 8; + if (y + mh > window.innerHeight) y = window.innerHeight - mh - 8; + this.sbCtxMenu = { x, y, blockId: blk.id, blockIdx: bi }; + this.sbCtxSub = null; + }, + + sbEnsureStyle(blk) { + if (!blk) return; + if (!blk.style) blk.style = {}; + }, + + sbSetStyle(key, value) { + const blk = this.sbGetBlk(); + if (!blk) return; + this.sbPushHistory(); + this.sbEnsureStyle(blk); + blk.style[key] = value; + this.autoSave(); + }, + + sbToggleStyle(key) { + const blk = this.sbGetBlk(); + if (!blk) return; + this.sbPushHistory(); + this.sbEnsureStyle(blk); + blk.style[key] = !blk.style[key]; + this.autoSave(); + }, + + sbResetStyle() { + const blk = this.sbGetBlk(); + if (!blk) return; + this.sbPushHistory(); + blk.style = {}; + this.sbFmtDropdown = null; + this.autoSave(); + }, + + sbBringForward() { + const page = this.sbCurrentPage; + const blk = this.sbGetBlk(); + if (!page || !blk) return; + this.sbPushHistory(); + this.sbEnsureStyle(blk); + blk.style.zIndex = (blk.style.zIndex || 10) + 1; + this.autoSave(); + }, + + sbSendBackward() { + const page = this.sbCurrentPage; + const blk = this.sbGetBlk(); + if (!page || !blk) return; + this.sbPushHistory(); + this.sbEnsureStyle(blk); + blk.style.zIndex = Math.max(1, (blk.style.zIndex || 10) - 1); + this.autoSave(); + }, + + // Context menu actions + sbCtxCopy() { + this.sbSelectedBlock = this.sbCtxMenu.blockId; + this.sbCopyBlock(); + this.sbPasteBlock(); + this.sbCtxMenu = null; + }, + sbCtxCut() { + this.sbSelectedBlock = this.sbCtxMenu.blockId; + this.sbCutBlock(); + this.sbCtxMenu = null; + }, + sbCtxDelete() { + this.sbSelectedBlock = this.sbCtxMenu.blockId; + this.sbDeleteSelectedBlock(); + this.sbCtxMenu = null; + }, + sbCtxSetStyle(key, value) { + const blk = this.sbCtxGetBlk(); + if (!blk) return; + this.sbPushHistory(); + this.sbEnsureStyle(blk); + blk.style[key] = value; + this.sbCtxMenu = null; + this.sbCtxSub = null; + this.autoSave(); + }, + sbCtxToggleStyle(key) { + const blk = this.sbCtxGetBlk(); + if (!blk) return; + this.sbPushHistory(); + this.sbEnsureStyle(blk); + blk.style[key] = !blk.style[key]; + this.sbCtxMenu = null; + this.autoSave(); + }, + sbCtxResetStyle() { + const blk = this.sbCtxGetBlk(); + if (!blk) return; + this.sbPushHistory(); + blk.style = {}; + this.sbCtxMenu = null; + this.autoSave(); + }, + sbCtxBringForward() { + const blk = this.sbCtxGetBlk(); + if (!blk) return; + this.sbPushHistory(); + this.sbEnsureStyle(blk); + blk.style.zIndex = (blk.style.zIndex || 10) + 1; + this.sbCtxMenu = null; + this.autoSave(); + }, + sbCtxSendBackward() { + const blk = this.sbCtxGetBlk(); + if (!blk) return; + this.sbPushHistory(); + this.sbEnsureStyle(blk); + blk.style.zIndex = Math.max(1, (blk.style.zIndex || 10) - 1); + this.sbCtxMenu = null; + this.autoSave(); + }, + sbTableAddRow(blk) { const colCount = blk.cols.length; blk.rows.push(Array(colCount).fill('')); @@ -3719,10 +4145,21 @@ function planningCanvas() { sbExportBlock(blk) { const esc = (s) => (s || '').replace(//g, '>'); + // 블록 스타일 CSS 문자열 생성 + let sty = ''; + if (blk.style) { + if (blk.style.fontColor) sty += 'color:' + blk.style.fontColor + ';'; + if (blk.style.bgColor) sty += 'background:' + blk.style.bgColor + ';'; + if (blk.style.fontSize) sty += 'font-size:' + blk.style.fontSize + 'px;'; + if (blk.style.bold) sty += 'font-weight:700;'; + if (blk.style.italic) sty += 'font-style:italic;'; + if (blk.style.textAlign) sty += 'text-align:' + blk.style.textAlign + ';'; + } + const wrapStyle = sty ? ' style="' + sty + '"' : ''; switch (blk.type) { - case 'heading': return '

' + esc(blk.content) + '

'; - case 'heading2': return '

' + esc(blk.content) + '

'; - case 'text': return '

' + esc(blk.content).replace(/\n/g, '
') + '

'; + case 'heading': return '

' + esc(blk.content) + '

'; + case 'heading2': return '

' + esc(blk.content) + '

'; + case 'text': return '

' + esc(blk.content).replace(/\n/g, '
') + '

'; case 'divider': return '
'; case 'callout': return '
' + esc(blk.icon || '💡') + '' + esc(blk.content) + '
'; case 'code': return '
' + esc(blk.content) + '
';