From 945157424daed6b00584b0701cf5e0c27d1a1548 Mon Sep 17 00:00:00 2001 From: zs Date: Tue, 3 Mar 2026 16:14:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=89=A9=E5=B1=95=E5=BC=B9=E6=A1=86?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extension/src/content/Sidebar.vue | 135 +++---- extension/src/content/sidebar-mount.ts | 464 ++++++++++++++++++++++--- 2 files changed, 463 insertions(+), 136 deletions(-) diff --git a/extension/src/content/Sidebar.vue b/extension/src/content/Sidebar.vue index 29724a4..fb72ba7 100644 --- a/extension/src/content/Sidebar.vue +++ b/extension/src/content/Sidebar.vue @@ -106,131 +106,96 @@ const copyToClipboard = async (reply: any) => { - - diff --git a/extension/src/content/sidebar-mount.ts b/extension/src/content/sidebar-mount.ts index 9b7fe9c..40346ea 100644 --- a/extension/src/content/sidebar-mount.ts +++ b/extension/src/content/sidebar-mount.ts @@ -10,6 +10,405 @@ const sidebarVisible = ref(false) let isMounted = false let isMounting = false +// All CSS is embedded here so it works inside Shadow DOM +// No dependency on Tailwind or external stylesheets +const SIDEBAR_CSS = ` +/* ===== Reset & Base ===== */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +/* ===== Sidebar Panel ===== */ +.ir-panel { + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: 400px; + display: flex; + flex-direction: column; + background: #09090b; + border-left: 1px solid rgba(255,255,255,0.08); + box-shadow: -12px 0 40px rgba(0,0,0,0.6); + color: #f8fafc; + font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif; + z-index: 2147483647; + overflow: hidden; + pointer-events: auto; + animation: ir-slide-in 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) forwards; +} + +.ir-panel.ir-closing { + animation: ir-slide-out 0.35s cubic-bezier(0.2, 0.8, 0.2, 1) forwards; +} + +@keyframes ir-slide-in { + from { transform: translateX(100%); } + to { transform: translateX(0); } +} +@keyframes ir-slide-out { + from { transform: translateX(0); } + to { transform: translateX(100%); } +} + +/* ===== Header ===== */ +.ir-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + background: rgba(255,255,255,0.02); + border-bottom: 1px solid rgba(255,255,255,0.06); + flex-shrink: 0; +} + +.ir-brand { + display: flex; + align-items: center; + gap: 12px; +} + +.ir-logo { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 10px; + background: linear-gradient(135deg, #e11d48, #be123c); + box-shadow: 0 4px 12px rgba(225,29,72,0.25); +} + +.ir-logo svg { + color: white; +} + +.ir-brand-name { + font-size: 15px; + font-weight: 800; + letter-spacing: -0.02em; + color: #fff; +} + +.ir-brand-sub { + font-size: 9px; + color: #71717a; + text-transform: uppercase; + letter-spacing: 0.2em; + font-weight: 700; +} + +.ir-close-btn { + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 50%; + border: 1px solid rgba(255,255,255,0.06); + background: rgba(255,255,255,0.04); + cursor: pointer; + transition: all 0.2s ease; + color: #a1a1aa; +} + +.ir-close-btn:hover { + background: rgba(225,29,72,0.15); + color: #fb7185; + border-color: rgba(225,29,72,0.3); +} + +/* ===== Scrollable Content ===== */ +.ir-content { + flex: 1; + overflow-y: auto; + padding: 24px; + display: flex; + flex-direction: column; + gap: 28px; +} + +.ir-content::-webkit-scrollbar { + width: 4px; +} +.ir-content::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.06); + border-radius: 10px; +} +.ir-content:hover::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.12); +} + +/* ===== Section Labels ===== */ +.ir-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 10px; + font-weight: 700; + color: #71717a; + text-transform: uppercase; + letter-spacing: 0.12em; + padding: 0 4px; + margin-bottom: 12px; +} + +.ir-label svg { + color: #e11d48; +} + +/* ===== Tweet Context ===== */ +.ir-tweet-card { + background: rgba(0,0,0,0.3); + border-radius: 16px; + padding: 16px; + border: 1px solid rgba(255,255,255,0.04); +} + +.ir-tweet-text { + font-size: 13px; + color: #a1a1aa; + line-height: 1.6; + font-weight: 500; + font-style: italic; +} + +/* ===== Strategy Buttons ===== */ +.ir-strategy-grid { + display: flex; + flex-direction: column; + gap: 8px; +} + +.ir-strategy-btn { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 16px; + border-radius: 14px; + border: 1px solid rgba(255,255,255,0.06); + background: rgba(255,255,255,0.02); + cursor: pointer; + transition: all 0.2s ease; + font-size: 14px; + font-weight: 600; + color: #a1a1aa; + font-family: inherit; + text-align: left; + width: 100%; +} + +.ir-strategy-btn:hover { + background: rgba(255,255,255,0.05); + border-color: rgba(255,255,255,0.1); +} + +.ir-strategy-btn.active { + background: rgba(225,29,72,0.08); + border-color: rgba(225,29,72,0.25); + color: #fff; +} + +.ir-strategy-icon { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 8px; + background: rgba(0,0,0,0.3); + color: #71717a; + flex-shrink: 0; + transition: all 0.2s ease; +} + +.ir-strategy-btn.active .ir-strategy-icon { + background: rgba(225,29,72,0.15); + color: #fb7185; +} + +.ir-strategy-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #e11d48; + box-shadow: 0 0 8px #f43f5e; + margin-left: auto; + flex-shrink: 0; +} + +/* ===== Reply Results ===== */ +.ir-reply-card { + background: rgba(255,255,255,0.03); + border-radius: 16px; + padding: 20px; + border: 1px solid rgba(255,255,255,0.05); + transition: border-color 0.2s ease; +} + +.ir-reply-card:hover { + border-color: rgba(225,29,72,0.2); +} + +.ir-reply-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.ir-reply-tag { + font-size: 9px; + font-weight: 700; + color: #fb7185; + background: rgba(251,113,133,0.1); + padding: 4px 10px; + border-radius: 100px; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.ir-copy-btn { + opacity: 0; + font-size: 10px; + font-weight: 700; + color: #71717a; + background: none; + border: none; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border-radius: 6px; + transition: all 0.2s ease; + font-family: inherit; +} + +.ir-reply-card:hover .ir-copy-btn { + opacity: 1; +} + +.ir-copy-btn:hover { + color: #fff; + background: rgba(255,255,255,0.05); +} + +.ir-reply-content { + font-size: 13px; + line-height: 1.7; + white-space: pre-wrap; + color: #d4d4d8; + font-weight: 500; + letter-spacing: -0.01em; +} + +/* ===== Footer ===== */ +.ir-footer { + padding: 24px; + background: rgba(255,255,255,0.02); + border-top: 1px solid rgba(255,255,255,0.06); + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 12px; +} + +.ir-tip { + display: flex; + gap: 10px; + align-items: flex-start; + background: rgba(225,29,72,0.06); + border: 1px solid rgba(225,29,72,0.15); + border-radius: 14px; + padding: 14px; + font-size: 11px; + color: #fb7185; + font-weight: 500; + line-height: 1.5; + position: relative; +} + +.ir-tip-close { + position: absolute; + top: 10px; + right: 10px; + background: none; + border: none; + color: #fb7185; + opacity: 0.5; + cursor: pointer; + font-size: 12px; + font-family: inherit; +} + +.ir-tip-close:hover { + opacity: 1; +} + +.ir-generate-btn { + width: 100%; + padding: 16px; + border: none; + border-radius: 18px; + font-size: 15px; + font-weight: 700; + color: #fff; + background: linear-gradient(135deg, #e11d48, #be123c); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + transition: all 0.2s ease; + box-shadow: 0 8px 20px rgba(225,29,72,0.25); + font-family: inherit; +} + +.ir-generate-btn:hover { + background: linear-gradient(135deg, #f43f5e, #e11d48); + box-shadow: 0 12px 28px rgba(225,29,72,0.35); + transform: translateY(-1px); +} + +.ir-generate-btn:active { + transform: scale(0.97); +} + +.ir-generate-btn:disabled { + background: #18181b; + color: #52525b; + cursor: not-allowed; + box-shadow: none; + transform: none; +} + +.ir-spinner { + width: 18px; + height: 18px; + border: 2px solid rgba(255,255,255,0.2); + border-top-color: white; + border-radius: 50%; + animation: ir-spin 0.8s linear infinite; +} + +@keyframes ir-spin { + to { transform: rotate(360deg); } +} + +/* ===== Fade-In for Results ===== */ +.ir-fade-in { + animation: ir-fadein 0.5s ease forwards; +} + +@keyframes ir-fadein { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} +` + async function initSidebar() { if (isMounted || isMounting) return @@ -18,7 +417,7 @@ async function initSidebar() { isMounting = true - // 1. Create Host Element + // 1. Create Host Element - positioned for right side only host = document.createElement('div') host.id = MOUNT_ID host.style.cssText = ` @@ -26,7 +425,7 @@ async function initSidebar() { top: 0; right: 0; bottom: 0; - left: 0; + width: 400px; z-index: 2147483647; pointer-events: none; ` @@ -35,7 +434,12 @@ async function initSidebar() { // 2. Create Shadow Root const shadowRoot = host.attachShadow({ mode: 'open' }) - // 3. Create Container + // 3. Inject self-contained CSS directly (no external files needed) + const styleEl = document.createElement('style') + styleEl.textContent = SIDEBAR_CSS + shadowRoot.appendChild(styleEl) + + // 4. Create Container const container = document.createElement('div') container.id = 'app' container.style.cssText = ` @@ -44,65 +448,23 @@ async function initSidebar() { top: 0; right: 0; bottom: 0; - left: 0; + width: 100%; ` shadowRoot.appendChild(container) - // 4. Inject Styles (Bypassing X.com CSP via fetch) - const injectStyles = async () => { - try { - // Check for Vite dev mode styles first - const devStyles = document.querySelectorAll('style[data-vite-dev-id]') - if (devStyles.length > 0) { - devStyles.forEach(style => shadowRoot.appendChild(style.cloneNode(true))) - console.log('[InsightReply] Injected dev styles into shadow DOM') - return - } - - // Production mode: fetch the CSS file directly to bypass rigid CSP - // CRXJS usually puts built CSS in assets/index-[hash].css - const cssUrl = chrome.runtime.getURL('assets/index.css') - // In Vite dev mode the raw file might be available - const devUrl = chrome.runtime.getURL('src/assets/tailwind.css') - - const urlToFetch = chrome.runtime.id.includes('extension') ? cssUrl : devUrl - - const response = await fetch(urlToFetch) - if (response.ok) { - const cssText = await response.text() - const styleEl = document.createElement('style') - styleEl.textContent = cssText - shadowRoot.appendChild(styleEl) - console.log('[InsightReply] Successfully injected fetched CSS into shadow DOM') - } else { - console.warn('[InsightReply] Failed to fetch CSS file directly, falling back to basic link tag') - const linkEl = document.createElement('link') - linkEl.rel = 'stylesheet' - linkEl.href = chrome.runtime.getURL('src/assets/tailwind.css') - shadowRoot.appendChild(linkEl) - } - } catch (err) { - console.error('[InsightReply] Error injecting styles:', err) - } - } - - // Inject and wait - await injectStyles() - // 5. Create Vue App with reactive provide const app = createApp(Sidebar) - - isMounting = false app.provide('currentTweetData', currentTweetData) app.provide('sidebarVisible', sidebarVisible) app.mount(container) isMounted = true + isMounting = false console.log('[InsightReply] Sidebar mounted successfully') } -function showSidebar(tweetData?: any) { - initSidebar() +async function showSidebar(tweetData?: any) { + await initSidebar() if (tweetData) { currentTweetData.value = tweetData @@ -112,8 +474,8 @@ function showSidebar(tweetData?: any) { sidebarVisible.value = true } -function toggleSidebar() { - initSidebar() +async function toggleSidebar() { + await initSidebar() sidebarVisible.value = !sidebarVisible.value }