/** * Icons: Lucide (https://lucide.dev) * ISC License - Copyright (c) Lucide Contributors 2026 */ (function () { 'use strict'; var SCRIPT = document.currentScript; var API_BASE = SCRIPT ? SCRIPT.src.replace(/\/widget\.js.*$/, '') : ''; var API_TOKEN = SCRIPT ? (SCRIPT.dataset.token || '') : ''; var USERNAME_KEY = 'fb-username'; var SIDEBAR_WIDTH_KEY = 'fb-sidebar-width'; var PRIORITY_CYCLE = { must: 'better', better: 'want', want: 'must' }; var PRIORITY_COLORS = { must: { bg: '#ef4444', text: '#fff', light: 'rgba(239,68,68,0.1)', border: 'rgba(239,68,68,0.3)' }, better: { bg: '#f59e0b', text: '#fff', light: 'rgba(245,158,11,0.1)', border: 'rgba(245,158,11,0.3)' }, want: { bg: '#22c55e', text: '#fff', light: 'rgba(34,197,94,0.1)', border: 'rgba(34,197,94,0.3)' }, }; var SVG = { message: '', check: '', rotateCcw: '', pencil: '', trash: '', user: '', x: '', panelRight: '', }; function icon(name, size, strokeWidth) { size = size || 14; strokeWidth = strokeWidth || 2; return (SVG[name] || '').replace(/SIZE/g, size).replace(/SW/g, strokeWidth); } var state = { username: localStorage.getItem(USERNAME_KEY) || '', comments: [], filter: 'unresolved', sidebarOpen: false, selectedText: '', selectedQuoteContext: { beforeText: '', afterText: '' }, selectedRect: null, popupContent: '', editingId: null, editContent: '', editPriority: 'want', replyingTo: null, replyText: '', editingName: false, nameInput: '', popupPriority: 'must', sidebarWidth: parseInt(localStorage.getItem(SIDEBAR_WIDTH_KEY), 10) || 400, }; var slug = (function () { return window.location.href .replace(/^https?:\/\//, '') .replace(/[^a-zA-Z0-9\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF]/g, '_') .substring(0, 100); })(); function genId() { return Date.now().toString(36) + Math.random().toString(36).substring(2, 8); } function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } function fmtTime(ts) { var d = Date.now() - ts; if (d < 60000) return 'たった今'; if (d < 3600000) return Math.floor(d / 60000) + '分前'; if (d < 86400000) return Math.floor(d / 3600000) + '時間前'; return new Date(ts).toLocaleDateString('ja-JP', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } // ===== API ===== function api(method, params) { var url = API_BASE + '/api/comments'; var hdrs = { 'Content-Type': 'application/json' }; if (API_TOKEN) hdrs['Authorization'] = 'Bearer ' + API_TOKEN; var opts = { method: method, headers: hdrs }; if (method === 'GET') { url += '?slug=' + encodeURIComponent(params.slug); } else if (method === 'DELETE') { url += '?id=' + encodeURIComponent(params.id); } else { opts.body = JSON.stringify(params); } return fetch(url, opts).then(function (r) { return r.json(); }); } function loadComments() { return api('GET', { slug: slug }).then(function (c) { if (Array.isArray(c)) state.comments = c; render(); applyHighlights(); }).catch(function () { render(); applyHighlights(); }); } // ===== CSS ===== function injectStyles() { var style = document.createElement('style'); style.id = 'fb-widget-styles'; style.textContent = [ ':root{--fb-bg:#ffffff;--fb-fg:#0a0a0a;--fb-muted:#f5f5f5;--fb-muted-fg:#737373;--fb-border:#e5e5e5;--fb-primary:#171717;--fb-primary-fg:#fafafa;--fb-accent:#3b82f6;--fb-destructive:#ef4444;--fb-font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Hiragino Sans",sans-serif}', /* Toggle button */ '#fb-toggle{position:fixed;right:0;top:50%;transform:translateY(-50%);width:36px;background:var(--fb-bg);border:1px solid var(--fb-border);border-right:none;border-radius:8px 0 0 8px;cursor:pointer;z-index:99999;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-family:var(--fb-font);transition:all .15s;box-shadow:-2px 0 8px rgba(0,0,0,0.06);padding:8px 0}', '#fb-toggle:hover{background:var(--fb-muted);box-shadow:-2px 0 12px rgba(0,0,0,0.1)}', '#fb-toggle .fb-toggle-icon{color:var(--fb-muted-fg);display:flex;align-items:center;justify-content:center;position:relative}', '#fb-toggle .fb-toggle-label{font-size:10px;color:var(--fb-accent);font-weight:700;writing-mode:vertical-rl;letter-spacing:1px;line-height:1;white-space:nowrap}', '#fb-toggle .fb-badge{position:absolute;top:-6px;left:-6px;background:var(--fb-destructive);color:#fff;font-size:9px;font-weight:700;min-width:16px;height:16px;border-radius:8px;display:flex;align-items:center;justify-content:center;padding:0 3px;line-height:1}', /* Sidebar */ '#fb-sidebar{position:fixed;top:0;height:100vh;background:rgba(245,245,245,0.5);border-left:1px solid var(--fb-border);z-index:99998;transition:right .3s ease;display:flex;flex-direction:column;font-family:var(--fb-font);font-size:14px;color:var(--fb-fg)}', '#fb-sidebar *{box-sizing:border-box}', /* Resize handle */ '.fb-resize-handle{position:absolute;left:-3px;top:0;width:6px;height:100%;cursor:col-resize;z-index:1}', '.fb-resize-handle:hover{background:rgba(59,130,246,0.15)}', '.fb-resize-handle.active{background:rgba(59,130,246,0.3)}', 'body.fb-resizing{cursor:col-resize !important;-webkit-user-select:none !important;user-select:none !important}', 'body.fb-resizing *{cursor:col-resize !important}', /* Header */ '.fb-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--fb-border)}', '.fb-header-left{display:flex;align-items:center;gap:8px}', '.fb-header-title{font-size:14px;font-weight:700;color:var(--fb-accent)}', '.fb-header-count{background:var(--fb-muted);color:var(--fb-muted-fg);font-size:11px;padding:1px 7px;border-radius:10px}', '.fb-header-actions{display:flex;align-items:center;gap:2px}', '.fb-hdr-btn{background:none;border:none;cursor:pointer;padding:6px;border-radius:6px;color:var(--fb-muted-fg);transition:all .15s;display:inline-flex;align-items:center;gap:4px;font-family:var(--fb-font);font-size:12px}', '.fb-hdr-btn:hover{background:var(--fb-muted);color:var(--fb-fg)}', /* User row */ '.fb-user-row{display:flex;align-items:center;padding:8px 16px;border-bottom:1px solid var(--fb-border);font-size:13px;color:var(--fb-muted-fg);cursor:pointer;transition:color .15s;gap:6px}', '.fb-user-row:hover{color:var(--fb-fg)}', /* Filters */ '.fb-filters{display:flex;border-bottom:1px solid var(--fb-border)}', '.fb-filter{flex:1;padding:10px 0;text-align:center;font-size:13px;color:var(--fb-muted-fg);border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;transition:all .15s;font-family:var(--fb-font)}', '.fb-filter:hover{color:var(--fb-fg)}', '.fb-filter.active{color:var(--fb-fg);border-bottom-color:var(--fb-accent)}', '.fb-filter .cnt{font-size:11px;margin-left:4px;padding:1px 5px;border-radius:8px;background:var(--fb-muted);color:var(--fb-muted-fg)}', '.fb-filter.active .cnt{background:rgba(59,130,246,0.1);color:var(--fb-accent)}', /* List */ '.fb-list{flex:1;overflow-y:auto;padding:8px}', '.fb-empty{text-align:center;padding:60px 20px;color:var(--fb-muted-fg);font-size:13px}', '.fb-empty svg{margin:0 auto 12px;display:block;color:var(--fb-border)}', /* Card */ '.fb-card{background:var(--fb-bg);border:1px solid var(--fb-border);border-left:3px solid var(--fb-border);border-radius:12px;padding:14px;margin-bottom:6px;transition:box-shadow .3s,background .3s;cursor:pointer}', '.fb-card:hover{box-shadow:0 1px 3px rgba(0,0,0,0.05)}', '.fb-card.resolved{opacity:.5}', /* Card header */ '.fb-card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}', '.fb-card-head-left{display:flex;align-items:center;gap:6px}', '.fb-avatar{width:22px;height:22px;border-radius:50%;background:var(--fb-primary);color:var(--fb-primary-fg);display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0}', '.fb-author{font-size:13px;font-weight:700;color:var(--fb-fg)}', '.fb-time{font-size:11px;color:var(--fb-muted-fg)}', '.fb-resolved-mark{font-size:11px;color:#22c55e;margin-left:4px;display:inline-flex;align-items:center;gap:2px}', /* Priority badge */ '.fb-badge-p{font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;color:#fff;cursor:default;transition:all .15s}', '.fb-badge-p.own{cursor:pointer}', '.fb-badge-p.own:hover{transform:scale(1.1);box-shadow:0 0 0 2px rgba(0,0,0,0.08)}', /* Quote */ '.fb-quote{font-size:12px;color:var(--fb-muted-fg);padding:6px 10px;background:var(--fb-muted);border-left:2px solid var(--fb-primary);border-radius:0 4px 4px 0;margin-bottom:8px;font-style:italic;line-height:1.5;cursor:pointer;transition:background .15s}', '.fb-quote:hover{background:var(--fb-border)}', /* Body */ '.fb-body{font-size:13px;color:var(--fb-muted-fg);line-height:1.6;margin-bottom:6px;white-space:pre-wrap}', /* Actions */ '.fb-actions{display:flex;gap:2px;flex-wrap:wrap}', '.fb-act{font-size:12px;color:#a3a3a3;background:none;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:3px;font-family:var(--fb-font)}', '.fb-act:hover{color:var(--fb-fg)}', '.fb-act.del:hover{color:var(--fb-destructive)}', '.fb-act.res:hover{color:#22c55e}', /* Replies */ '.fb-replies{margin-top:8px;padding-top:8px;border-top:1px solid var(--fb-muted)}', '.fb-reply-item{display:flex;gap:8px;padding:6px 0}', '.fb-reply-item+.fb-reply-item{border-top:1px solid var(--fb-muted)}', '.fb-reply-avatar{width:20px;height:20px;border-radius:50%;background:var(--fb-primary);color:var(--fb-primary-fg);display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;flex-shrink:0;margin-top:2px}', '.fb-reply-meta{font-size:12px;color:var(--fb-muted-fg);margin-bottom:2px}', '.fb-reply-meta strong{color:#525252;font-weight:700}', '.fb-reply-text{font-size:13px;color:var(--fb-muted-fg);line-height:1.5}', /* Reply input */ '.fb-reply-input{display:flex;gap:8px;margin-top:8px}', '.fb-reply-input textarea{flex:1;padding:8px 10px;border:1px solid var(--fb-border);border-radius:6px;font-size:13px;font-family:var(--fb-font);resize:none;outline:none;min-height:36px;color:var(--fb-fg)}', '.fb-reply-input textarea:focus{border-color:var(--fb-accent)}', '.fb-reply-input button{padding:6px 14px;background:var(--fb-primary);color:var(--fb-primary-fg);border:none;border-radius:6px;font-size:12px;cursor:pointer;font-family:var(--fb-font);align-self:flex-end}', '.fb-reply-input button:disabled{opacity:.4;cursor:default}', /* Edit area */ '.fb-edit-area textarea{width:100%;padding:8px 10px;border:1px solid var(--fb-border);border-radius:6px;font-size:13px;font-family:var(--fb-font);resize:vertical;outline:none;min-height:50px;margin-bottom:6px;color:var(--fb-fg)}', '.fb-edit-area textarea:focus{border-color:var(--fb-accent)}', '.fb-edit-btns{display:flex;gap:6px}', '.fb-edit-btns button{padding:4px 12px;border-radius:6px;font-size:12px;cursor:pointer;border:1px solid var(--fb-border);background:var(--fb-bg);color:var(--fb-muted-fg);font-family:var(--fb-font)}', '.fb-edit-btns button.save{background:var(--fb-primary);color:var(--fb-primary-fg);border-color:var(--fb-primary)}', '.fb-edit-pri{display:flex;gap:4px;margin-bottom:6px}', '.fb-edit-pri button{padding:2px 10px;border-radius:4px;font-size:11px;font-weight:700;cursor:pointer;border:1px solid var(--fb-border);background:var(--fb-bg);color:var(--fb-muted-fg);font-family:var(--fb-font);transition:all .15s}', /* Popup */ '.fb-popup{position:fixed;z-index:100000;width:400px;background:var(--fb-bg);border:1px solid var(--fb-border);border-radius:12px;padding:16px;box-shadow:0 10px 25px rgba(0,0,0,0.1);font-family:var(--fb-font);display:none}', '.fb-popup.show{display:block}', '.fb-popup-head{margin-bottom:10px}', '.fb-popup-head span{font-size:14px;font-weight:700;color:var(--fb-fg)}', '.fb-popup-quote{font-size:13px;color:var(--fb-muted-fg);padding:8px 12px;background:var(--fb-muted);border-left:2px solid var(--fb-accent);border-radius:0 6px 6px 0;margin-bottom:10px;font-style:italic;line-height:1.5}', '.fb-popup textarea{width:100%;min-height:70px;padding:10px 12px;border:1px solid var(--fb-border);border-radius:8px;font-size:14px;font-family:var(--fb-font);resize:vertical;outline:none;margin-bottom:10px;color:var(--fb-fg)}', '.fb-popup textarea:focus{border-color:var(--fb-accent)}', '.fb-popup textarea::placeholder{color:var(--fb-muted-fg)}', '.fb-popup-pri{display:flex;gap:6px;margin-bottom:10px}', '.fb-popup-pri button{flex:1;padding:7px 8px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;border:2px solid transparent;transition:all .15s;font-family:var(--fb-font)}', '.fb-popup-actions{display:flex;gap:8px;justify-content:flex-end}', '.fb-popup-actions button{padding:8px 18px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;transition:all .15s;font-family:var(--fb-font)}', '.fb-popup-actions .cancel{background:none;border:1px solid var(--fb-border);color:var(--fb-muted-fg)}', '.fb-popup-actions .cancel:hover{background:var(--fb-muted);color:var(--fb-fg)}', '.fb-popup-actions .submit{border:none;color:#fff}', /* Name dialog */ '.fb-name-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;z-index:100001}', '.fb-name-box{background:var(--fb-bg);border-radius:12px;padding:28px;width:360px;box-shadow:0 20px 40px rgba(0,0,0,0.15)}', '.fb-name-box h2{font-size:18px;font-weight:700;margin:0 0 6px;color:var(--fb-fg)}', '.fb-name-box p{font-size:14px;color:var(--fb-muted-fg);margin:0 0 16px}', '.fb-name-box input{width:100%;padding:10px 14px;border:1px solid var(--fb-border);border-radius:8px;font-size:15px;outline:none;margin-bottom:12px;font-family:var(--fb-font);color:var(--fb-fg)}', '.fb-name-box input:focus{border-color:var(--fb-accent)}', '.fb-name-box input::placeholder{color:var(--fb-muted-fg)}', '.fb-name-box button{width:100%;padding:10px;background:var(--fb-primary);color:var(--fb-primary-fg);border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;font-family:var(--fb-font)}', '.fb-name-box button:disabled{opacity:.4;cursor:default}', /* Inline name edit */ '.fb-name-input{width:100%;padding:4px 8px;border:1px solid var(--fb-border);border-radius:4px;font-size:12px;outline:none;font-family:var(--fb-font);color:var(--fb-fg)}', '.fb-name-input:focus{border-color:var(--fb-accent)}', /* Highlights */ '.fb-highlight{padding:1px 0;cursor:pointer;transition:background .15s}', '.fb-highlight-must{background:rgba(239,68,68,0.15);border-bottom:2px solid #ef4444}', '.fb-highlight-must:hover{background:rgba(239,68,68,0.25)}', '.fb-highlight-better{background:rgba(245,158,11,0.15);border-bottom:2px solid #f59e0b}', '.fb-highlight-better:hover{background:rgba(245,158,11,0.25)}', '.fb-highlight-want{background:rgba(34,197,94,0.15);border-bottom:2px solid #22c55e}', '.fb-highlight-want:hover{background:rgba(34,197,94,0.25)}', '@keyframes fb-pulse{0%,100%{opacity:1}50%{opacity:0.4}}', '.fb-highlight.fb-pulse{animation:fb-pulse 1s ease-in-out infinite}', '.fb-card.fb-focused{box-shadow:0 0 0 2px var(--fb-accent);background:rgba(59,130,246,0.04)}', '#fb-sidebar svg,#fb-toggle svg,.fb-popup svg{pointer-events:none}', ].join('\n'); document.head.appendChild(style); } // ===== DOM ===== function el(tag, attrs, children) { var e = document.createElement(tag); if (attrs) Object.keys(attrs).forEach(function (k) { if (k === 'className') e.className = attrs[k]; else if (k === 'innerHTML') e.innerHTML = attrs[k]; else if (k.startsWith('on')) e.addEventListener(k.substring(2).toLowerCase(), attrs[k]); else e.setAttribute(k, attrs[k]); }); if (children) { if (typeof children === 'string') e.textContent = children; else if (Array.isArray(children)) children.forEach(function (c) { if (c) e.appendChild(c); }); else e.appendChild(children); } return e; } // ===== Render ===== function render() { renderToggle(); renderSidebar(); renderPopup(); renderNameDialog(); } function renderToggle() { var btn = document.getElementById('fb-toggle'); if (!btn) { btn = el('button', { id: 'fb-toggle', onClick: toggleSidebar }); document.body.appendChild(btn); } var unresolvedCount = state.comments.filter(function (c) { return !c.parentId && !c.resolved; }).length; var h = '' + icon('panelRight', 16); if (unresolvedCount > 0) h += '' + unresolvedCount + ''; h += ''; h += 'コメント'; btn.innerHTML = h; btn.style.display = state.sidebarOpen ? 'none' : ''; } function applySidebarWidth() { var sb = document.getElementById('fb-sidebar'); if (!sb) return; var w = state.sidebarWidth; sb.style.width = w + 'px'; sb.style.right = state.sidebarOpen ? '0px' : (-(w + 20)) + 'px'; document.body.style.marginRight = state.sidebarOpen ? w + 'px' : ''; } function toggleSidebar() { state.sidebarOpen = !state.sidebarOpen; var btn = document.getElementById('fb-toggle'); if (btn) btn.style.display = state.sidebarOpen ? 'none' : ''; document.body.style.transition = 'margin-right 0.3s ease'; applySidebarWidth(); } function topLevel() { return state.comments.filter(function (c) { return !c.parentId; }); } function getReplies(id) { return state.comments.filter(function (c) { return c.parentId === id; }).sort(function (a, b) { return a.timestamp - b.timestamp; }); } function filtered() { var tl = topLevel(); if (state.filter === 'resolved') return tl.filter(function (c) { return c.resolved; }); if (state.filter === 'all') return tl; return tl.filter(function (c) { return !c.resolved; }); } function renderSidebar() { var sb = document.getElementById('fb-sidebar'); var isNew = !sb; if (isNew) { sb = el('div', { id: 'fb-sidebar' }); document.body.appendChild(sb); sb.addEventListener('mouseover', function (e) { var card = e.target.closest('.fb-card'); if (!card) return; var mark = document.querySelector('.fb-highlight[data-comment-id="' + card.dataset.id + '"]'); if (mark) mark.classList.add('fb-pulse'); }); sb.addEventListener('mouseout', function (e) { var card = e.target.closest('.fb-card'); if (!card) return; if (e.relatedTarget && card.contains(e.relatedTarget)) return; var mark = document.querySelector('.fb-highlight[data-comment-id="' + card.dataset.id + '"]'); if (mark) mark.classList.remove('fb-pulse'); }); } applySidebarWidth(); var tl = topLevel(); var counts = { unresolved: tl.filter(function (c) { return !c.resolved; }).length, resolved: tl.filter(function (c) { return c.resolved; }).length, all: tl.length, }; var html = '
'; // Header html += '
'; html += '
コメント' + counts.all + '
'; html += '
'; html += ''; html += '
'; // User if (state.username) { if (state.editingName) { html += '
'; } else { html += '
' + icon('user', 14) + esc(state.username) + '
'; } } // Filters html += '
'; ['unresolved', 'resolved', 'all'].forEach(function (f) { var label = f === 'unresolved' ? '未解決' : f === 'resolved' ? '解決済' : 'すべて'; html += ''; }); html += '
'; // List var items = filtered().sort(function (a, b) { return b.timestamp - a.timestamp; }); html += '
'; if (items.length === 0) { html += '
' + icon('message', 40) + 'コメントはまだありません
テキストを選択してコメントを追加
'; } else { items.forEach(function (c) { html += renderCard(c); }); } html += '
'; sb.innerHTML = html; if (isNew) bindSidebarEvents(sb); } function renderCard(c) { var isOwn = c.author === state.username; var pc = PRIORITY_COLORS[c.priority] || PRIORITY_COLORS.want; var h = '
'; // Header h += '
'; h += '
' + esc(c.author.charAt(0)) + '
'; h += '' + esc(c.author) + ''; h += '' + fmtTime(c.timestamp) + ''; if (c.resolved) h += '' + icon('check', 12) + ' 解決済'; h += '
'; h += '' + esc(c.priority.charAt(0).toUpperCase() + c.priority.slice(1)) + ''; h += '
'; // Quote if (c.quote) { var q = c.quote.length > 100 ? c.quote.substring(0, 100) + '...' : c.quote; h += '
' + esc(q) + '
'; } // Body / Edit if (state.editingId === c.id) { h += '
'; h += '
'; ['must', 'better', 'want'].forEach(function (p) { var sel = state.editPriority === p; var pc2 = PRIORITY_COLORS[p]; h += ''; }); h += '
'; h += ''; h += '
'; h += '
'; } else { h += '
' + esc(c.content) + '
'; } // Actions if (state.editingId !== c.id) { h += '
'; h += ''; h += ''; if (isOwn) h += ''; if (isOwn) h += ''; h += '
'; } // Replies var replies = getReplies(c.id); if (replies.length > 0) { h += '
'; replies.forEach(function (r) { var isOwnReply = r.author === state.username; h += '
' + esc(r.author.charAt(0)) + '
' + esc(r.author) + ' · ' + fmtTime(r.timestamp) + '
'; if (state.editingId === r.id) { h += '
'; } else { h += '
' + esc(r.content) + '
'; h += '
'; h += ''; if (isOwnReply) h += ''; if (isOwnReply) h += ''; h += '
'; } h += '
'; }); h += '
'; } // Reply input if (state.replyingTo === c.id) { h += '
'; } h += '
'; return h; } function bindSidebarEvents(sb) { sb.addEventListener('click', function (e) { var t = e.target.closest('[data-action]'); if (t) { var action = t.dataset.action; var id = t.dataset.id; if (action === 'close') { toggleSidebar(); } else if (action === 'edit-name') { state.editingName = true; state.nameInput = state.username; render(); } else if (action === 'cycle' && id) { cyclePriority(id); } else if (action === 'scroll-quote' && id) { scrollToQuote(id); } else if (action === 'reply' && id) { state.replyingTo = state.replyingTo === id ? null : id; state.replyText = ''; render(); } else if (action === 'resolve' && id) { resolveComment(id); } else if (action === 'edit' && id) { var c = state.comments.find(function (x) { return x.id === id; }); if (c) { state.editingId = id; state.editContent = c.content; state.editPriority = c.priority; render(); } } else if (action === 'delete' && id) { deleteComment(id); } else if (action === 'delete-reply' && id) { deleteReply(id); } else if (action === 'cancel-edit') { state.editingId = null; render(); } else if (action === 'save-edit' && id) { saveEdit(id); } else if (action === 'submit-reply' && id) { submitReply(id); } else if (action === 'set-edit-pri') { state.editPriority = t.dataset.pri; render(); } return; } var filterBtn = e.target.closest('[data-filter]'); if (filterBtn) { state.filter = filterBtn.dataset.filter; render(); return; } var card = e.target.closest('.fb-card'); if (card && card.dataset.id) { scrollToQuote(card.dataset.id); } }); sb.addEventListener('mousedown', function (e) { if (!e.target.closest('.fb-resize-handle')) return; e.preventDefault(); var startX = e.clientX; var startWidth = state.sidebarWidth; var handle = e.target.closest('.fb-resize-handle'); document.body.classList.add('fb-resizing'); handle.classList.add('active'); function onMove(ev) { state.sidebarWidth = Math.min(800, Math.max(300, startWidth + (startX - ev.clientX))); var s = document.getElementById('fb-sidebar'); if (s) s.style.width = state.sidebarWidth + 'px'; document.body.style.transition = 'none'; document.body.style.marginRight = state.sidebarWidth + 'px'; } function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.classList.remove('fb-resizing'); handle.classList.remove('active'); document.body.style.transition = ''; localStorage.setItem(SIDEBAR_WIDTH_KEY, String(state.sidebarWidth)); } document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); sb.addEventListener('input', function (e) { var t = e.target; if (t.dataset.action === 'name-input') { state.nameInput = t.value; } else if (t.dataset.action === 'edit-textarea') { state.editContent = t.value; } else if (t.dataset.action === 'reply-textarea') { state.replyText = t.value; var btn = sb.querySelector('[data-action="submit-reply"]'); if (btn) btn.disabled = !state.replyText.trim(); } }); sb.addEventListener('keydown', function (e) { var t = e.target; if (t.dataset.action === 'name-input') { if (e.key === 'Enter') { finishNameEdit(); } else if (e.key === 'Escape') { state.editingName = false; render(); } } if (t.dataset.action === 'reply-textarea' && (e.metaKey || e.ctrlKey) && e.key === 'Enter') { var id = sb.querySelector('[data-action="submit-reply"]')?.dataset.id; if (id) submitReply(id); } }); sb.addEventListener('blur', function (e) { if (e.target.dataset?.action === 'name-input') { finishNameEdit(); } }, true); } // ===== Popup ===== function renderPopup() { var popup = document.getElementById('fb-popup'); var isNew = !popup; if (isNew) { popup = el('div', { id: 'fb-popup', className: 'fb-popup' }); document.body.appendChild(popup); popup.addEventListener('click', function (e) { var t = e.target.closest('[data-action]'); if (!t) return; if (t.dataset.action === 'cancel-popup') { closePopup(); } else if (t.dataset.action === 'set-popup-pri') { state.popupPriority = t.dataset.pri; render(); } else if (t.dataset.action === 'submit-popup') { submitComment(state.popupPriority); } }); popup.addEventListener('input', function (e) { if (e.target.dataset.action === 'popup-textarea') state.popupContent = e.target.value; }); popup.addEventListener('keydown', function (e) { if (e.target.dataset.action === 'popup-textarea' && (e.metaKey || e.ctrlKey) && e.key === 'Enter') { submitComment(state.popupPriority); } }); } if (!state.selectedRect) { popup.classList.remove('show'); return; } popup.classList.add('show'); var q = state.selectedText.length > 120 ? state.selectedText.substring(0, 120) + '...' : state.selectedText; var selPc = PRIORITY_COLORS[state.popupPriority]; var h = '
コメントを追加
'; h += '
' + esc(q) + '
'; h += ''; h += '
'; ['must', 'better', 'want'].forEach(function (p) { var pc = PRIORITY_COLORS[p]; var sel = state.popupPriority === p; var style = sel ? 'background:' + pc.light + ';color:' + pc.bg + ';border-color:' + pc.bg : 'background:var(--fb-bg);color:var(--fb-muted-fg);border-color:var(--fb-border)'; h += ''; }); h += '
'; h += '
'; h += ''; h += ''; h += '
'; popup.innerHTML = h; var rect = state.selectedRect; var pw = 400, ph = 300, m = 12; var top = rect.bottom + m; if (top + ph > window.innerHeight) top = rect.top - ph - m; if (top < m) top = Math.max(m, (window.innerHeight - ph) / 2); var availW = state.sidebarOpen ? window.innerWidth - state.sidebarWidth : window.innerWidth; var left = Math.max(m, Math.min(rect.left, availW - pw - m)); popup.style.top = top + 'px'; popup.style.left = left + 'px'; } function closePopup() { state.selectedText = ''; state.selectedRect = null; state.popupContent = ''; state.popupPriority = 'must'; render(); } function renderNameDialog() { var existing = document.getElementById('fb-name-overlay'); if (state.username) { if (existing) existing.remove(); return; } if (existing) return; var overlay = el('div', { id: 'fb-name-overlay', className: 'fb-name-overlay' }); overlay.innerHTML = '

ようこそ

コメントに表示される名前を入力してください。

'; document.body.appendChild(overlay); var input = document.getElementById('fb-name-field'); var btn = document.getElementById('fb-name-submit'); input.addEventListener('input', function () { btn.disabled = !input.value.trim(); }); input.addEventListener('keydown', function (e) { if (e.key === 'Enter' && input.value.trim()) { setName(input.value.trim()); } }); btn.addEventListener('click', function () { if (input.value.trim()) setName(input.value.trim()); }); setTimeout(function () { input.focus(); }, 100); } function setName(name) { state.username = name; localStorage.setItem(USERNAME_KEY, name); var overlay = document.getElementById('fb-name-overlay'); if (overlay) overlay.remove(); render(); } function finishNameEdit() { if (state.nameInput.trim() && state.nameInput.trim() !== state.username) { var oldName = state.username; state.username = state.nameInput.trim(); localStorage.setItem(USERNAME_KEY, state.username); api('PUT', { id: '_rename', author: state.username, oldAuthor: oldName, projectSlug: slug }).then(loadComments); } state.editingName = false; render(); } // ===== Actions ===== function submitComment(priority) { if (!state.username || !state.selectedText) return; var c = { id: genId(), author: state.username, type: 'comment', quote: state.selectedText, quoteContext: state.selectedQuoteContext, content: state.popupContent.trim(), priority: priority, parentId: null, pageUrl: window.location.href, projectSlug: slug, timestamp: Date.now(), }; state.comments.push(c); closePopup(); applyHighlights(); api('POST', c).then(loadComments); } function resolveComment(id) { var c = state.comments.find(function (x) { return x.id === id; }); if (!c) return; var now = !c.resolved; c.resolved = now; c.resolvedBy = now ? state.username : null; c.resolvedAt = now ? Date.now() : null; render(); applyHighlights(); api('PUT', { id: id, resolved: now, resolvedBy: c.resolvedBy, resolvedAt: c.resolvedAt }); } function cyclePriority(id) { var c = state.comments.find(function (x) { return x.id === id; }); if (!c || c.author !== state.username) return; c.priority = PRIORITY_CYCLE[c.priority] || 'must'; render(); applyHighlights(); api('PUT', { id: id, priority: c.priority }); } function deleteComment(id) { state.comments = state.comments.filter(function (c) { return c.id !== id && c.parentId !== id; }); render(); applyHighlights(); api('DELETE', { id: id }); } function deleteReply(id) { state.comments = state.comments.filter(function (c) { return c.id !== id; }); render(); api('DELETE', { id: id }); } function saveEdit(id) { var c = state.comments.find(function (x) { return x.id === id; }); if (!c) return; c.content = state.editContent; c.priority = state.editPriority; state.editingId = null; render(); applyHighlights(); api('PUT', { id: id, content: c.content, priority: c.priority }); } function submitReply(parentId) { if (!state.replyText.trim() || !state.username) return; var r = { id: genId(), author: state.username, type: 'comment', quote: '', quoteContext: { beforeText: '', afterText: '' }, content: state.replyText.trim(), priority: 'want', parentId: parentId, pageUrl: window.location.href, projectSlug: slug, timestamp: Date.now(), }; state.comments.push(r); state.replyingTo = null; state.replyText = ''; render(); api('POST', r); } // ===== Text Selection ===== function setupTextSelection() { document.addEventListener('mouseup', function (e) { if (e.target.closest('#fb-sidebar,#fb-toggle,#fb-popup')) return; var sel = window.getSelection(); var text = sel?.toString().trim(); if (!text || !sel || sel.rangeCount === 0 || !state.username) return; var range = sel.getRangeAt(0); state.selectedText = text.replace(/[\s\u00A0]+/g, ' ').substring(0, 200); state.selectedRect = range.getBoundingClientRect(); state.selectedQuoteContext = getQuoteContext(range); state.popupContent = ''; render(); }); document.addEventListener('mousedown', function (e) { if (e.target.closest('#fb-popup')) return; if (state.selectedRect) closePopup(); }); } function getQuoteContext(range) { var before = '', after = ''; try { var br = document.createRange(); br.setStart(document.body, 0); br.setEnd(range.startContainer, range.startOffset); before = br.toString().slice(-50).replace(/[\s\u00A0]+/g, ' ').trim(); var ar = document.createRange(); ar.setStart(range.endContainer, range.endOffset); ar.setEnd(document.body, document.body.childNodes.length); after = ar.toString().slice(0, 50).replace(/[\s\u00A0]+/g, ' ').trim(); } catch (e) { /* ignore */ } return { beforeText: before, afterText: after }; } // ===== Highlights ===== function collectTextNodes() { var nodes = []; var tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { acceptNode: function (n) { var p = n.parentElement; if (!p) return NodeFilter.FILTER_REJECT; if (p.tagName === 'SCRIPT' || p.tagName === 'STYLE') return NodeFilter.FILTER_REJECT; if (p.closest('#fb-sidebar,#fb-toggle,#fb-popup,.fb-highlight')) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } }); var n; while ((n = tw.nextNode())) nodes.push(n); return nodes; } function mapNormToOrig(orig, normStart, normEnd) { var ws = /[\s\u00A0]+/g; var normalized = orig.replace(ws, ' '); if (normStart >= normalized.length) return null; var origIdx = 0, normIdx = 0; var origStart = -1, origEnd = -1; while (origIdx < orig.length) { if (normIdx === normStart && origStart === -1) origStart = origIdx; if (normIdx === normEnd) { origEnd = origIdx; break; } if (normIdx < normalized.length && orig[origIdx] === normalized[normIdx]) { origIdx++; normIdx++; } else { origIdx++; } } if (origEnd === -1 && normIdx >= normEnd) origEnd = origIdx; if (origStart === -1 || origEnd === -1 || origStart >= origEnd) return null; return [origStart, origEnd]; } function wrapTextRange(node, start, end, comment) { var orig = node.textContent; var before = document.createTextNode(orig.substring(0, start)); var mark = document.createElement('mark'); mark.className = 'fb-highlight fb-highlight-' + comment.priority; mark.dataset.commentId = comment.id; mark.textContent = orig.substring(start, end); (function (id) { mark.addEventListener('click', function () { scrollToCard(id); }); })(comment.id); var after = document.createTextNode(orig.substring(end)); node.parentNode.insertBefore(before, node); node.parentNode.insertBefore(mark, node); node.parentNode.insertBefore(after, node); node.parentNode.removeChild(node); } function applyHighlights() { document.querySelectorAll('.fb-highlight').forEach(function (el) { var t = document.createTextNode(el.textContent); el.parentNode.replaceChild(t, el); }); document.body.normalize(); state.comments.filter(function (c) { return !c.parentId && !c.resolved && c.quote && c.quote.length >= 2; }).forEach(function (c) { var search = c.quote.replace(/[\s\u00A0]+/g, ' ').trim(); var textNodes = collectTextNodes(); var found = false; for (var i = 0; i < textNodes.length; i++) { var node = textNodes[i]; var orig = node.textContent; var di = orig.indexOf(search); if (di !== -1) { try { wrapTextRange(node, di, di + search.length, c); found = true; } catch (e) {} break; } var norm = orig.replace(/[\s\u00A0]+/g, ' '); var ni = norm.indexOf(search); if (ni === -1) continue; var range = mapNormToOrig(orig, ni, ni + search.length); if (range) { try { wrapTextRange(node, range[0], range[1], c); found = true; } catch (e) {} break; } } if (!found) { var concat = ''; var nodeMap = []; for (var j = 0; j < textNodes.length; j++) { var s = concat.length; concat += textNodes[j].textContent; nodeMap.push({ node: textNodes[j], start: s, end: concat.length }); } var concatNorm = concat.replace(/[\s\u00A0]+/g, ' '); var ci = concatNorm.indexOf(search); if (ci !== -1) { var range = mapNormToOrig(concat, ci, ci + search.length); if (range) { var mStart = range[0], mEnd = range[1]; for (var k = nodeMap.length - 1; k >= 0; k--) { var nm = nodeMap[k]; if (nm.end <= mStart || nm.start >= mEnd) continue; var ls = Math.max(0, mStart - nm.start); var le = Math.min(nm.node.textContent.length, mEnd - nm.start); try { wrapTextRange(nm.node, ls, le, c); } catch (e) {} } } } } }); } var PRIORITY_FLASH = { must: 'rgba(239,68,68,0.4)', better: 'rgba(245,158,11,0.4)', want: 'rgba(34,197,94,0.4)' }; function scrollToQuote(id) { var mark = document.querySelector('.fb-highlight[data-comment-id="' + id + '"]'); if (!mark) return; mark.scrollIntoView({ behavior: 'smooth', block: 'center' }); var c = state.comments.find(function (x) { return x.id === id; }); var flashColor = c ? (PRIORITY_FLASH[c.priority] || PRIORITY_FLASH.want) : PRIORITY_FLASH.want; var orig = mark.style.backgroundColor; mark.style.backgroundColor = flashColor; mark.style.transition = 'background-color 0.3s'; setTimeout(function () { mark.style.backgroundColor = orig; }, 1500); } function scrollToCard(id) { var wasClosed = !state.sidebarOpen; if (wasClosed) { toggleSidebar(); } setTimeout(function () { var card = document.querySelector('.fb-card[data-id="' + id + '"]'); if (!card) return; card.scrollIntoView({ behavior: 'smooth', block: 'center' }); card.classList.add('fb-focused'); setTimeout(function () { card.classList.remove('fb-focused'); }, 1500); }, wasClosed ? 350 : 0); } // ===== Init ===== function init() { injectStyles(); render(); setupTextSelection(); loadComments(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();