From 6cbd34dd4189c70af056da1bb5f342a835e0b576 Mon Sep 17 00:00:00 2001 From: RHZHZ Date: Sat, 1 Aug 2026 12:19:52 +0800 Subject: [PATCH] feat(code): add code sidebar preview Add desktop sidebar preview for long collapsed code blocks. Stabilize PrismMac initialization after hard refresh and document the code collapse behavior. --- __tests__/components/PrismMac.test.js | 174 ++++++++++ components/PrismMac.js | 323 ++++++++++++++--- .../issue-2941-code-block-ui-refinement.md | 326 ++++++++++++++++++ docs/developer/rfc/issue-2941-code-sidebar.md | 213 ++++++++++++ .../config/notion-next-code-style.md | 6 + docs/user-guide/reference/features.md | 2 +- public/css/prism-mac-style.css | 273 ++++++++++++++- themes/claude/style.js | 37 +- 8 files changed, 1302 insertions(+), 52 deletions(-) create mode 100644 __tests__/components/PrismMac.test.js create mode 100644 docs/developer/rfc/issue-2941-code-block-ui-refinement.md create mode 100644 docs/developer/rfc/issue-2941-code-sidebar.md diff --git a/__tests__/components/PrismMac.test.js b/__tests__/components/PrismMac.test.js new file mode 100644 index 00000000000..cbdaf0fc792 --- /dev/null +++ b/__tests__/components/PrismMac.test.js @@ -0,0 +1,174 @@ +import { fireEvent } from '@testing-library/react' +import { + closeCodeSidePanel, + isCodeSidePanelSupported, + openCodeSidePanel, + renderCollapseCode +} from '@/components/PrismMac' +import { siteConfig } from '@/lib/config' + +jest.mock('next/navigation', () => ({ + usePathname: jest.fn() +})) + +jest.mock('@/lib/global', () => ({ + useGlobal: jest.fn() +})) + +jest.mock('@/lib/utils', () => ({ + loadExternalResource: jest.fn() +})) + +jest.mock('@/lib/config', () => ({ + siteConfig: jest.fn((key, fallback) => { + if (key === 'CODE_COLLAPSE_MIN_LINES') return 3 + return fallback + }) +})) + +const originalMatchMedia = window.matchMedia + +const setDesktopViewport = matches => { + window.matchMedia = jest.fn().mockReturnValue({ + matches, + addEventListener: jest.fn(), + removeEventListener: jest.fn() + }) +} + +const appendCodeToolbar = (text = 'const one = 1\nconst two = 2\nconst three = 3') => { + const toolbar = document.createElement('div') + toolbar.className = 'code-toolbar' + + const pre = document.createElement('pre') + const code = document.createElement('code') + code.className = 'language-javascript' + code.textContent = text + + pre.appendChild(code) + toolbar.appendChild(pre) + document.body.appendChild(toolbar) + + return toolbar +} + +describe('PrismMac code side panel', () => { + beforeEach(() => { + document.body.innerHTML = '' + setDesktopViewport(true) + siteConfig.mockImplementation((key, fallback) => { + if (key === 'CODE_COLLAPSE_MIN_LINES') return 3 + return fallback + }) + }) + + afterEach(() => { + closeCodeSidePanel() + document.body.innerHTML = '' + window.matchMedia = originalMatchMedia + }) + + it('only supports the side panel on desktop viewports', () => { + setDesktopViewport(false) + + expect(isCodeSidePanelSupported()).toBe(false) + expect( + openCodeSidePanel({ + language: 'javascript', + lineCount: 3, + codeHtml: 'const value = 1', + text: 'const value = 1' + }) + ).toBe(false) + expect(document.querySelector('#notion-code-side-panel')).not.toBeInTheDocument() + }) + + it('opens, replaces, and closes a single sidebar instance', () => { + expect( + openCodeSidePanel({ + language: 'javascript', + lineCount: 3, + codeClassName: 'language-javascript', + codeHtml: 'const value = 1', + text: 'const value = 1' + }) + ).toBe(true) + + expect(document.querySelectorAll('#notion-code-side-panel')).toHaveLength(1) + expect(document.querySelector('.code-side-panel-title')).toHaveTextContent( + 'JAVASCRIPT' + ) + expect(document.querySelector('.code-side-panel-code code')).toHaveClass( + 'language-javascript' + ) + expect(document.querySelector('.code-side-panel-code').innerHTML).toContain( + 'token keyword' + ) + expect(document.querySelector('.code-side-panel-backdrop')).toBeInTheDocument() + + openCodeSidePanel({ + language: 'typescript', + lineCount: 5, + codeClassName: 'language-typescript', + codeHtml: 'type Value = string', + text: 'type Value = string' + }) + + expect(document.querySelectorAll('#notion-code-side-panel')).toHaveLength(1) + expect(document.querySelector('.code-side-panel-title')).toHaveTextContent( + 'TYPESCRIPT' + ) + + fireEvent.click(document.querySelector('.code-side-panel-backdrop')) + expect(document.querySelector('#notion-code-side-panel')).not.toBeInTheDocument() + + openCodeSidePanel({ + language: 'typescript', + lineCount: 5, + codeClassName: 'language-typescript', + codeHtml: 'type Value = string', + text: 'type Value = string' + }) + + fireEvent.click(document.querySelector('.code-side-panel-close')) + expect(document.querySelector('#notion-code-side-panel')).not.toBeInTheDocument() + }) + + it('closes the sidebar with Escape', () => { + openCodeSidePanel({ + language: 'javascript', + lineCount: 3, + codeHtml: 'const value = 1', + text: 'const value = 1' + }) + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })) + + expect(document.querySelector('#notion-code-side-panel')).not.toBeInTheDocument() + }) + + it('adds the sidebar button for long desktop code blocks', () => { + appendCodeToolbar() + + renderCollapseCode(true, false) + + const sidePanelButton = document.querySelector('.collapse-side-panel-button') + expect(sidePanelButton).toHaveTextContent('在侧栏查看') + + fireEvent.click(sidePanelButton) + + expect(document.querySelector('.code-side-panel-code code')).toHaveTextContent( + 'const one = 1 const two = 2 const three = 3' + ) + }) + + it('keeps the existing collapse behavior without a sidebar button on mobile', () => { + setDesktopViewport(false) + appendCodeToolbar() + + renderCollapseCode(true, false) + + expect(document.querySelector('.collapse-wrapper')).toBeInTheDocument() + expect(document.querySelector('.collapse-side-panel-button')).not.toBeInTheDocument() + }) +}) \ No newline at end of file diff --git a/components/PrismMac.js b/components/PrismMac.js index 52fcd74a467..c81488269b2 100644 --- a/components/PrismMac.js +++ b/components/PrismMac.js @@ -15,6 +15,8 @@ import { usePathname } from 'next/navigation' import { useGlobal } from '@/lib/global' import { siteConfig } from '@/lib/config' +const PRISM_MAC_STYLE_PATH = '/css/prism-mac-style.css' + /** * 代码美化相关 * @author https://github.com/txs/ @@ -41,49 +43,99 @@ const PrismMac = () => { useEffect(() => { let isDisposed = false let stopLineNumbers = () => {} + let observer = null + let initTimer = null + let hasInitialized = false - const article = getNotionArticle() - if (!article) return - const hasCodeBlocks = Boolean(article.querySelector('pre.notion-code')) - if (!hasCodeBlocks) return + const renderCodeEnhancements = () => { + if (isDisposed) return - if (codeMacBar || codeCollapse) { - loadExternalResource('/css/prism-mac-style.css', 'css') - } - // 加载prism样式 - loadPrismThemeCSS( - isDarkMode, - prismThemeSwitch, - prismThemeDarkPath, - prismThemeLightPath, - prismThemePrefixPath - ) - // 折叠代码 - loadExternalResource(prismjsAutoLoader, 'js') - .then(() => { - if (isDisposed) return - try { - if (typeof window !== 'undefined' && !window.Prism) { - window.Prism = Prism - } - if (window?.Prism?.plugins?.autoloader) { - window.Prism.plugins.autoloader.languages_path = prismjsPath - } + try { + if (typeof window !== 'undefined' && !window.Prism) { + window.Prism = Prism + } + if (window?.Prism?.plugins?.autoloader) { + window.Prism.plugins.autoloader.languages_path = prismjsPath + } - const dispose = renderPrismMac(codeLineNumbers, codeMacBar) - stopLineNumbers = typeof dispose === 'function' ? dispose : () => {} - renderMermaid(mermaidCDN) - renderCollapseCode(codeCollapse, codeCollapseExpandDefault) - } catch (err) { - console.warn('[PrismMac] render failed:', err) + try { + stopLineNumbers() + } catch (e) { + /* ignore */ } - }) - .catch(err => { - console.warn('[PrismMac] prism autoloader load failed:', err) - }) + + const dispose = renderPrismMac(codeLineNumbers, codeMacBar) + stopLineNumbers = typeof dispose === 'function' ? dispose : () => {} + renderMermaid(mermaidCDN) + renderCollapseCode(codeCollapse, codeCollapseExpandDefault) + } catch (err) { + console.warn('[PrismMac] render failed:', err) + } + } + + const loadCodeStyleSheets = () => { + // 加载 Prism 主题后再次移动 Mac 样式到最后,避免刷新时被异步主题 CSS 覆盖。 + const prismThemeReady = loadPrismThemeCSS( + isDarkMode, + prismThemeSwitch, + prismThemeDarkPath, + prismThemeLightPath, + prismThemePrefixPath + ) + if (codeMacBar || codeCollapse) { + loadPrismMacStyleCSS() + Promise.resolve(prismThemeReady) + .catch(err => { + console.warn('[PrismMac] prism theme load failed:', err) + }) + .finally(() => { + loadPrismMacStyleCSS() + }) + } + } + + const initCodeEnhancements = () => { + if (isDisposed || hasInitialized) return true + + const article = getNotionArticle() + const hasCodeBlocks = Boolean(article?.querySelector('pre.notion-code')) + if (!hasCodeBlocks) return false + + hasInitialized = true + observer?.disconnect() + observer = null + if (initTimer) { + clearTimeout(initTimer) + initTimer = null + } + + loadCodeStyleSheets() + + // 先用本地 Prism 渲染,避免外部 autoloader 阻塞基础代码增强。 + renderCodeEnhancements() + + loadExternalResource(prismjsAutoLoader, 'js') + .then(() => { + renderCodeEnhancements() + }) + .catch(err => { + console.warn('[PrismMac] prism autoloader load failed:', err) + }) + + return true + } + + if (!initCodeEnhancements() && typeof MutationObserver !== 'undefined') { + observer = new MutationObserver(initCodeEnhancements) + observer.observe(document.body, { childList: true, subtree: true }) + initTimer = setTimeout(initCodeEnhancements, 1000) + } return () => { isDisposed = true + observer?.disconnect() + if (initTimer) clearTimeout(initTimer) + closeCodeSidePanel() try { stopLineNumbers() } catch (e) { @@ -121,6 +173,183 @@ const getNotionArticles = () => { return Array.from(document.querySelectorAll('#notion-article')) } +const loadPrismMacStyleCSS = () => { + const existing = document.querySelector(`link[href="${PRISM_MAC_STYLE_PATH}"]`) + if (existing && existing.parentNode) { + document.head.appendChild(existing) + return Promise.resolve(PRISM_MAC_STYLE_PATH) + } + + return loadExternalResource(PRISM_MAC_STYLE_PATH, 'css') +} + +const CODE_SIDE_PANEL_ID = 'notion-code-side-panel' +const CODE_SIDE_PANEL_DESKTOP_QUERY = '(min-width: 1024px)' +const CODE_SIDE_PANEL_KEYDOWN = '__notionNextCodeSidePanelKeydown' + +export const isCodeSidePanelSupported = () => { + if (typeof window === 'undefined') return false + if (typeof window.matchMedia !== 'function') return true + + return window.matchMedia(CODE_SIDE_PANEL_DESKTOP_QUERY).matches +} + +export const closeCodeSidePanel = () => { + if (typeof document === 'undefined') return false + + const existing = document.getElementById(CODE_SIDE_PANEL_ID) + if (existing) existing.remove() + + if (typeof window !== 'undefined') { + const keydownHandler = window[CODE_SIDE_PANEL_KEYDOWN] + if (keydownHandler) { + document.removeEventListener('keydown', keydownHandler) + delete window[CODE_SIDE_PANEL_KEYDOWN] + } + } + + return Boolean(existing) +} + +const requestFrame = callback => { + if (typeof window === 'undefined') return callback() + + const raf = window.requestAnimationFrame || (cb => window.setTimeout(cb, 0)) + return raf(callback) +} + +export const openCodeSidePanel = ({ + language = '', + lineCount = 0, + codeClassName = '', + codeHtml = '', + text = '' +} = {}) => { + if (typeof document === 'undefined' || !isCodeSidePanelSupported()) { + return false + } + + closeCodeSidePanel() + + const root = document.createElement('div') + root.id = CODE_SIDE_PANEL_ID + root.className = 'code-side-panel-root' + + const backdrop = document.createElement('button') + backdrop.type = 'button' + backdrop.className = 'code-side-panel-backdrop' + backdrop.setAttribute('aria-label', '关闭代码预览侧栏') + backdrop.addEventListener('click', closeCodeSidePanel) + + const drawer = document.createElement('aside') + drawer.className = 'code-side-panel-drawer' + drawer.setAttribute('role', 'dialog') + drawer.setAttribute('aria-label', '代码预览侧栏') + drawer.setAttribute('aria-modal', 'false') + + const header = document.createElement('div') + header.className = 'code-side-panel-header' + + const heading = document.createElement('div') + heading.className = 'code-side-panel-heading' + + const title = document.createElement('div') + title.className = 'code-side-panel-title' + title.textContent = language ? language.toUpperCase() : 'CODE' + + const meta = document.createElement('div') + meta.className = 'code-side-panel-meta' + meta.textContent = lineCount ? `${lineCount} lines` : '' + + heading.appendChild(title) + heading.appendChild(meta) + + const actions = document.createElement('div') + actions.className = 'code-side-panel-actions' + + const copyButton = document.createElement('button') + copyButton.type = 'button' + copyButton.className = 'code-side-panel-copy' + copyButton.textContent = '复制' + const copyCode = async () => { + const originalText = copyButton.textContent + + try { + if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) { + throw new Error('Clipboard unavailable') + } + await navigator.clipboard.writeText(text) + copyButton.textContent = '已复制' + } catch { + copyButton.textContent = '复制失败' + } + + window.setTimeout(() => { + if (copyButton.isConnected) copyButton.textContent = originalText + }, 1200) + } + copyButton.addEventListener('click', () => { + void copyCode() + }) + + const closeButton = document.createElement('button') + closeButton.type = 'button' + closeButton.className = 'code-side-panel-close' + closeButton.setAttribute('aria-label', '关闭代码预览侧栏') + closeButton.textContent = '关闭' + closeButton.addEventListener('click', closeCodeSidePanel) + + actions.appendChild(copyButton) + actions.appendChild(closeButton) + header.appendChild(heading) + header.appendChild(actions) + + const pre = document.createElement('pre') + pre.className = 'code-side-panel-code' + const code = document.createElement('code') + code.className = codeClassName + code.innerHTML = codeHtml + pre.appendChild(code) + + drawer.appendChild(header) + drawer.appendChild(pre) + root.appendChild(backdrop) + root.appendChild(drawer) + + const keydownHandler = event => { + if (event.key === 'Escape') closeCodeSidePanel() + } + window[CODE_SIDE_PANEL_KEYDOWN] = keydownHandler + document.addEventListener('keydown', keydownHandler) + + document.body.appendChild(root) + requestFrame(() => root.classList.add('is-open')) + + return true +} + +const createCodeSidePanelButton = ({ language, label, lineCount, code }) => { + if (!isCodeSidePanelSupported()) return null + + const button = document.createElement('button') + button.type = 'button' + button.className = 'collapse-side-panel-button' + button.textContent = '在侧栏查看' + button.setAttribute('aria-label', `在侧栏查看 ${label}`) + button.addEventListener('click', event => { + event.stopPropagation() + openCodeSidePanel({ + language, + lineCount, + codeClassName: code.getAttribute('class') || '', + codeHtml: code.innerHTML, + text: code.textContent || '' + }) + }) + + return button +} + /** * 加载Prism主题样式 */ @@ -151,16 +380,16 @@ const loadPrismThemeCSS = ( ) { previousTheme.parentNode.removeChild(previousTheme) } - loadExternalResource(PRISM_THEME, 'css') + return loadExternalResource(PRISM_THEME, 'css') } else { - loadExternalResource(prismThemePrefixPath, 'css') + return loadExternalResource(prismThemePrefixPath, 'css') } } /* * 将代码块转为可折叠对象 */ -const renderCollapseCode = (codeCollapse, codeCollapseExpandDefault) => { +export const renderCollapseCode = (codeCollapse, codeCollapseExpandDefault) => { if (!codeCollapse) { return } @@ -202,6 +431,9 @@ const renderCollapseCode = (codeCollapse, codeCollapseExpandDefault) => { const panelWrapper = document.createElement('div') panelWrapper.className = 'collapse-panel-wrapper' + const headerRow = document.createElement('div') + headerRow.className = 'collapse-header-row' + const header = document.createElement('button') header.type = 'button' header.className = 'collapse-header' @@ -215,7 +447,18 @@ const renderCollapseCode = (codeCollapse, codeCollapseExpandDefault) => { const panel = document.createElement('div') panel.className = 'collapse-panel' - panelWrapper.appendChild(header) + headerRow.appendChild(header) + const sidePanelButton = createCodeSidePanelButton({ + language, + label, + lineCount, + code + }) + if (sidePanelButton) { + headerRow.appendChild(sidePanelButton) + } + + panelWrapper.appendChild(headerRow) panelWrapper.appendChild(panel) collapseWrapper.appendChild(panelWrapper) diff --git a/docs/developer/rfc/issue-2941-code-block-ui-refinement.md b/docs/developer/rfc/issue-2941-code-block-ui-refinement.md new file mode 100644 index 00000000000..43b9f8d563b --- /dev/null +++ b/docs/developer/rfc/issue-2941-code-block-ui-refinement.md @@ -0,0 +1,326 @@ +# RFC: 代码折叠块桌面与移动端 UI 收敛 + +- **作者**: @RHZHZ +- **日期**: 2026-08-01 +- **状态**: 已实现 +- **关联 Issue**: https://github.com/notionnext-org/NotionNext/issues/2941 +- **前置 RFC**: `docs/developer/rfc/issue-2941-code-sidebar.md` + +## 摘要 + +在现有长代码折叠和桌面端侧栏预览的基础上,进行一轮 CSS 级 UI 收敛。重点解决折叠头与代码内容之间存在额外空隙、桌面与移动端容器层级不一致、移动端代码横向溢出观感较强等问题。 + +本次改造不重写 `PrismMac` 的行为逻辑,不改变配置项,也不改变移动端“折叠/展开”的交互模型。目标是让折叠头、代码块和操作按钮呈现为一个连续、稳定、可扫描的代码组件。 + +## 背景与问题 + +当前实现已经具备以下能力: + +- 长代码块可折叠。 +- 桌面端可点击“在侧栏查看”。 +- 代码块支持 Mac 三色点和 Prism 高亮。 +- 移动端隐藏侧栏入口,保留原有折叠行为。 + +根据本地页面验证,主要问题集中在视觉层: + +1. 折叠头使用 `.collapse-panel-wrapper`,代码内容内部仍保留 `.code-toolbar` 的默认上下外边距,导致两者之间出现明显空隙。 +2. 折叠容器和代码容器分别拥有边框、圆角、背景和阴影,容易产生“卡片嵌套卡片”的观感。 +3. 桌面端按钮、折叠箭头和语言标签在同一行的优先级不够清晰,长语言名称或窄窗口下可能挤压内容。 +4. 移动端代码不换行是正确的,但当前左右裁切感较强,代码区内边距和滚动提示需要收敛。 +5. 主题级样式,特别是 Claude 主题的代码块覆盖规则,可能覆盖共享 `.collapse-*` 与 `.code-toolbar` 样式,造成不同主题的视觉不一致。 + +## 目标 + +1. 折叠头与展开后的代码内容共享同一个视觉外框。 +2. 消除折叠头与代码块之间不必要的垂直空隙。 +3. 桌面端和移动端使用一致的容器层级与间距语义。 +4. 保留代码横向滚动,不强制换行破坏代码可读性。 +5. 让“在侧栏查看”成为桌面端次要操作,不抢占折叠标题和箭头的注意力。 +6. 保持现有 `CODE_COLLAPSE`、`CODE_COLLAPSE_MIN_LINES` 和 `CODE_COLLAPSE_EXPAND_DEFAULT` 的行为不变。 +7. 不引入新的站长配置项,不增加额外运行时状态。 + +## 非目标 + +1. 不重写 `PrismMac` 的 DOM 注入流程。 +2. 不将现有 DOM helper 改造成 React Portal。 +3. 不改变移动端的侧栏策略。 +4. 不让代码块自动换行。 +5. 不重做各主题的整体排版系统。 +6. 不在本次改造中统一所有主题的代码配色。 + +## 设计原则 + +### 单一视觉容器 + +`.collapse-panel-wrapper` 作为代码折叠组件的唯一外框,负责: + +- 外部间距 +- 边框 +- 圆角 +- 背景 +- 阴影 +- 溢出裁切 + +内部的 `.code-toolbar` 不再承担独立卡片职责。展开状态下,它应当成为外框内部的代码内容区。 + +### 结构间距归属清晰 + +- `.collapse-wrapper` 负责组件与文章正文之间的间距。 +- `.collapse-panel-wrapper` 负责折叠头与代码内容的组合。 +- `.collapse-header-row` 负责头部高度和按钮布局。 +- `.collapse-panel` 负责展开动画和内容裁切。 +- `.code-toolbar` 与 `.notion-code` 不再额外贡献外部 margin。 + +### 代码优先于装饰 + +代码区域应优先保证: + +- 等宽字体稳定。 +- 代码行不被意外换行。 +- 长行可横向滚动。 +- 代码内容边界清晰。 +- 高亮颜色在浅色和深色模式下可读。 + +## 视觉方案 + +### 统一组件结构 + +目标结构保持现有 DOM 方向,仅通过 CSS 收敛视觉层级: + +```text +.collapse-wrapper +└── .collapse-panel-wrapper + ├── .collapse-header-row + │ ├── .collapse-header + │ └── .collapse-side-panel-button # 仅桌面端 + └── .collapse-panel + └── .code-toolbar + └── pre.notion-code +``` + +### 桌面端 + +#### 折叠头 + +- 高度保持在 `36px` 左右。 +- 语言和行数作为主要信息。 +- 折叠箭头固定在右侧。 +- “在侧栏查看”位于箭头左侧或头部操作区,作为次要按钮。 +- 语言标签过长时使用省略号,不推动代码块宽度变化。 + +#### 代码区 + +- 与折叠头共用外框。 +- 取消 `.code-toolbar` 的上下 margin。 +- 取消内部独立圆角和阴影。 +- 代码区保留 Mac 三色点和 Prism 高亮。 +- 代码内容区允许横向滚动。 + +#### 侧栏入口 + +- 继续只在 `min-width: 1024px` 显示。 +- 使用低对比度边框和背景,避免与正文主操作竞争。 +- 文本保持单行,避免窄桌面窗口中按钮高度变化。 + +### 移动端 + +- 隐藏“在侧栏查看”按钮。 +- 保留折叠头、箭头和展开/收起行为。 +- 折叠头与代码区仍使用同一个外框。 +- 代码区左右内边距收敛到约 `10–12px`。 +- 代码字号使用固定的小尺寸,不随视口宽度缩放。 +- 保持 `white-space: pre`,避免代码自动换行。 +- 使用 `overflow-x: auto`,允许用户通过横向滑动查看长行。 +- 代码块底部滚动条不应撑大组件高度或造成布局跳动。 +- 不新增移动端固定定位元素,避免遮挡文章内容。 + +### 间距建议 + +| 位置 | 当前风险 | 目标 | +| --- | --- | --- | +| 组件与正文 | 外层 margin 与主题规则叠加 | 由 `.collapse-wrapper` 单独控制 | +| 折叠头与代码区 | `.code-toolbar` margin 造成空隙 | `0–4px`,推荐 `0px` | +| 折叠头左右内边距 | 按钮与标题容易挤压 | 桌面 `12px`,移动 `10px` | +| 代码区左右内边距 | 手机端裁切感较强 | 桌面 `16–18px`,移动 `10–12px` | +| 代码区顶部内边距 | Mac 三色点与代码可能重叠 | 保留独立顶部安全区 | +| 侧栏入口与箭头 | 操作区层级不清 | 统一 `8px` 间距 | + +## 样式实现边界 + +### 共享样式 + +主要修改范围: + +- `public/css/prism-mac-style.css` + +建议优先处理以下规则: + +```css +.collapse-panel .code-toolbar { + margin: 0 !important; + border: 0; + border-radius: 0; + box-shadow: none; +} + +.collapse-panel .code-toolbar > pre.notion-code { + margin: 0 !important; + border-radius: 0; +} +``` + +实际实现应结合现有主题覆盖规则调整选择器优先级,避免仅通过扩大 `!important` 范围解决冲突。 + +### 主题覆盖 + +如果内置主题对 `.code-toolbar` 或 `.collapse-wrapper` 有专门样式,应采用以下顺序处理: + +1. 先确认共享样式的结构语义是否正确。 +2. 再为主题增加最小范围的覆盖。 +3. 避免在主题文件中复制整套折叠样式。 +4. 主题覆盖只处理颜色、字体和视觉风格,不改变折叠结构。 + +重点检查: + +- `themes/claude/style.js` +- `styles/notion.css` +- `public/css/prism-mac-style.css` + +## 交互与可访问性 + +1. 折叠头保持 `button` 元素,并同步维护 `aria-expanded`。 +2. “在侧栏查看”不能触发折叠头的点击事件。 +3. 移动端隐藏侧栏按钮时,不应留下空白占位。 +4. 长标题使用省略号,但完整语言和行数应保留在可访问名称中。 +5. 侧栏继续支持关闭按钮、Esc 和点击遮罩。 +6. 所有按钮保持可见的键盘焦点状态。 +7. 代码横向滚动不应阻止页面纵向滚动。 + +## 兼容性 + +本改造不改变以下行为: + +- `CODE_COLLAPSE = false` 时不创建折叠组件。 +- 未超过 `CODE_COLLAPSE_MIN_LINES` 的代码块不显示折叠头。 +- `CODE_COLLAPSE_EXPAND_DEFAULT` 继续控制默认展开状态。 +- 桌面端侧栏仍只展示当前代码块,并在关闭时清理节点和监听器。 +- 移动端不显示侧栏入口。 +- 普通代码块、Mermaid、行号和复制功能保持现有行为。 + +## 实施步骤 + +1. 调整共享 CSS,使折叠头和 `.code-toolbar` 形成单一视觉容器。 +2. 增加桌面和移动端的明确间距规则。 +3. 检查 Claude 主题和全局 Notion 样式的覆盖关系。 +4. 在桌面端验证展开、收起、侧栏入口和代码横向滚动。 +5. 在移动端验证折叠头、代码滚动、正文纵向滚动和无侧栏入口。 +6. 根据截图和 DOM 检查结果进行一次小范围视觉微调。 +7. 更新用户文档中的代码块行为说明。 + +## 验收标准 + +### 桌面端 + +- [ ] 折叠头与代码块之间没有明显空白卡层。 +- [ ] 折叠头和代码内容共享边框、圆角和背景层级。 +- [ ] “在侧栏查看”按钮不改变折叠头高度。 +- [ ] 长语言名称不会挤压箭头或按钮。 +- [ ] 代码长行可横向滚动,不发生页面整体横向溢出。 +- [ ] 侧栏打开后不会被主题代码样式透明穿透。 + +### 移动端 + +- [ ] 侧栏入口不显示。 +- [ ] 折叠头与代码块之间保持 `0–4px` 的视觉间距。 +- [ ] 代码块左右内边距适合小屏阅读。 +- [ ] 长代码行可以横向滑动查看。 +- [ ] 横向滚动不会导致页面整体横向滚动。 +- [ ] 折叠和展开不会造成明显布局跳动。 +- [ ] 代码块底部滚动条不遮挡最后一行代码。 + +### 回归 + +- [ ] `CODE_COLLAPSE` 关闭时页面保持原有代码块表现。 +- [ ] 普通短代码块不出现折叠头。 +- [ ] Prism 高亮仍正常。 +- [ ] 复制按钮仍正常。 +- [ ] Mermaid 代码块没有新增异常边框或空白。 +- [ ] 路由切换后侧栏节点和键盘监听器被清理。 + +## 测试计划 + +### 自动化测试 + +继续使用: + +```text +__tests__/components/PrismMac.test.js +``` + +重点保证: + +- 桌面端显示侧栏入口。 +- 移动端隐藏侧栏入口。 +- 折叠头点击不会被侧栏按钮干扰。 +- 侧栏关闭、Esc 和遮罩关闭行为不回归。 + +如 CSS 调整涉及稳定的 DOM class,可补充轻量 DOM 断言,但不建议在 Jest 中测试具体像素值。 + +### 手动验证 + +使用本地测试页面: + +```text +http://localhost:3000/article/notion-tabs-test +``` + +至少检查以下视口: + +- 桌面:`1440 × 900` +- 窄桌面:`1024 × 768` +- 手机:`390 × 844` +- 小屏手机:`375 × 667` + +验证内容: + +1. 长代码块默认展开和默认折叠两种状态。 +2. 代码块存在超长单行时的横向滚动。 +3. 代码块上下相邻正文时的间距。 +4. 浅色和深色模式。 +5. Claude 主题与至少一个非 Claude 主题。 + +## 风险与取舍 + +### 风险 + +1. 主题 CSS 选择器优先级可能覆盖共享样式。 +2. 取消内部 margin 后,某些主题的代码块可能显得过于贴边。 +3. 移动端滚动条样式在不同浏览器中表现不一致。 +4. 代码块内部使用 `min-width` 或 `white-space` 时,可能引入局部横向溢出。 + +### 取舍 + +- 选择 CSS 收敛而不是重写组件,降低 PR 范围和回归风险。 +- 保留代码横向滚动而不是强制换行,优先保证代码语义和复制体验。 +- 以共享容器统一结构,以主题覆盖处理风格差异。 + +## 开放问题 + +- [ ] 移动端代码字号最终采用 `12px` 还是 `13px`? +- [ ] 是否需要为横向滚动区域增加渐隐提示? +- [ ] 是否需要在所有主题中统一代码块外部上下间距? +- [ ] 是否将“在侧栏查看”按钮文案抽取到多语言字典? + +## 讨论记录 + +当前文档基于 2026-08-01 本地页面截图和 `smart-code-collapse` 分支现有实现整理,已完成对应的 CSS/UI 收敛。 + +## 实现记录 + +1. `public/css/prism-mac-style.css` + - 折叠面板内部的 `.code-toolbar` 不再额外贡献 margin、边框和阴影。 + - 手机端收紧了折叠头高度、代码区内边距和字号。 + +2. `themes/claude/style.js` + - 折叠外框改为统一承载代码 shell。 + - 只保留一层视觉容器,避免内部 pre 再次画框。 diff --git a/docs/developer/rfc/issue-2941-code-sidebar.md b/docs/developer/rfc/issue-2941-code-sidebar.md new file mode 100644 index 00000000000..a6e70eb30f5 --- /dev/null +++ b/docs/developer/rfc/issue-2941-code-sidebar.md @@ -0,0 +1,213 @@ +# RFC: 代码块侧栏预览 + +- **作者**: @RHZHZ +- **日期**: 2026-08-01 +- **状态**: 已实现 +- **关联 Issue**: https://github.com/notionnext-org/NotionNext/issues/2941 + +## 摘要 + +为长代码块增加“在侧栏查看”的交互,目标是让用户在阅读上下文时,不必把代码块完整展开到正文里。 + +本方案严格按维护者建议走最小版本: + +- 复用现有 `CODE_COLLAPSE` +- 只先覆盖桌面端 +- 移动端保持现有折叠行为 +- 不先引入 Portal、懒加载或复杂状态管理 + +## 问题 + +当前代码块直接占据正文流,会带来三个典型问题: + +1. 长代码块会打断文章阅读节奏。 +2. 用户需要频繁滚动,难以在看代码和看解释之间来回切换。 +3. 把代码块完全展开在正文中,会显著增加页面高度。 + +Issue #2941 的目标不是重做代码块体系,而是在现有折叠能力上补一个更适合桌面阅读的预览方式。 + +## 目标 + +1. 桌面端对长代码块提供“在侧栏查看”按钮。 +2. 点击后在右侧打开一个固定侧栏,展示当前代码。 +3. 关闭时清空侧栏内容并释放事件监听。 +4. 保留原有折叠代码块能力,不破坏现有行为。 + +## 非目标 + +1. 不做全新的代码块渲染器。 +2. 不做 React Portal 重构。 +3. 不引入全局状态管理。 +4. 不重写 Prism 高亮流程。 +5. 不改变移动端的折叠交互。 + +## 现状 + +当前代码相关链路已经存在: + +- `components/PrismMac.js` 负责 Prism、行号、Mermaid 和代码折叠注入。 +- `public/css/prism-mac-style.css` 已经承载 `.collapse-*` 的代码块样式。 +- `components/Collapse.js` 已有通用折叠动画,但更适合内容面板,不适合固定侧栏。 +- `docs/user-guide/config/notion-next-code-style.md` 已经说明 `CODE_COLLAPSE` 配置。 + +这意味着侧栏预览最适合落在 `PrismMac` 这条现有注入链路里,而不是单独开一套新的组件体系。 + +## 方案 + +### 核心思路 + +继续以 `renderCollapseCode()` 为入口,在检测到长代码块时: + +1. 保持当前折叠包装不变。 +2. 额外注入一个“在侧栏查看”按钮。 +3. 点击按钮时,把当前 code 的内容放入一个固定右侧侧栏。 +4. 侧栏以轻量 DOM 节点方式挂在 `document.body` 上,不使用 Portal。 +5. 侧栏外层提供遮罩,点击遮罩、Esc 或关闭按钮均可退出。 + +### 触发条件 + +1. `CODE_COLLAPSE` 开启。 +2. 当前代码块行数达到 `CODE_COLLAPSE_MIN_LINES`。 +3. 当前视口满足桌面端条件。 + +### 桌面 / 移动分流 + +- 桌面端:显示侧栏预览按钮。 +- 移动端:保持现有折叠/展开行为,不额外加侧栏入口。 + +### 侧栏结构 + +侧栏建议包含: + +- 语言标题 +- 行数信息 +- 关闭按钮 +- 复制按钮 +- 代码内容区 +- 背景遮罩 + +侧栏以固定定位展示,默认从右侧进入,关闭时向右收起。 + +### 内容策略 + +优先复用当前 Prism 已处理后的代码内容,保证: + +- 代码高亮风格一致 +- 语言 class 不丢失 +- 不需要重新跑一次高亮 + +### 生命周期 + +1. 打开时只保留一个侧栏实例。 +2. 再次点击同一个按钮时,直接更新侧栏内容或保持当前内容。 +3. 路由切换、组件卸载或 Esc 关闭时,移除侧栏与监听器。 + +## 实现落点 + +### `components/PrismMac.js` + +建议新增一组很小的 helper: + +- `openCodeSidePanel()` +- `closeCodeSidePanel()` +- `syncCodeSidePanelContent()` + +以及一个视口判断 helper,例如: + +- `isCodeSidePanelSupported()` + +`renderCollapseCode()` 继续负责遍历 `.code-toolbar`,只是在长代码块上额外挂一个按钮事件。 + +### `public/css/prism-mac-style.css` + +继续把样式集中在这里,新增: + +- 侧栏容器 +- 侧栏标题区 +- 侧栏关闭按钮 +- 侧栏复制按钮 +- 代码内容滚动区 +- 桌面/暗色模式适配 + +### `components/Collapse.js` + +保持现状,不强行复用到侧栏场景。 + +原因很简单: + +- 它适合内容展开/折叠 +- 不适合固定侧栏的宽度动画和固定定位 +- 直接复用会让职责变宽 + +## 实现记录 + +1. `components/PrismMac.js` + - 追加桌面端侧栏预览 helper。 + - 在长代码块折叠头部旁增加“在侧栏查看”按钮。 + - 路由切换和卸载时自动关闭侧栏。 + +2. `public/css/prism-mac-style.css` + - 补充折叠头部按钮位样式。 + - 增加 fixed 侧栏、背景遮罩与深色模式样式。 + +3. `__tests__/components/PrismMac.test.js` + - 覆盖桌面/移动端分流、打开/关闭、Esc 关闭、遮罩关闭、按钮注入。 + +4. `docs/user-guide/` + - 同步 `CODE_COLLAPSE` 的桌面侧栏预览说明。 + +## 兼容性 + +1. `CODE_COLLAPSE = false` 时,不出现侧栏入口。 +2. `CODE_COLLAPSE_EXPAND_DEFAULT` 继续只控制原有折叠默认状态。 +3. 现有复制、行号、Mac 风格、Mermaid 不受影响。 +4. 未命中的普通代码块不新增任何额外 DOM。 + +## 风险与约束 + +1. 代码内容来自 DOM,必须注意清理事件监听,避免路由切换残留。 +2. 代码块可能有多个实例,侧栏需要单实例覆盖,避免重复挂载。 +3. 如果直接复用高亮后的 `innerHTML`,要避免引入多余包装破坏复制体验。 +4. 桌面阈值需要和站点布局保持一致,避免在中等分辨率下误触发。 + +## 测试计划 + +### 自动化测试 + +建议新增或补充: + +- `components/PrismMac.js` 的 DOM helper 测试 +- 侧栏打开 / 关闭测试 +- 桌面端显示入口、移动端不显示入口测试 +- 多个代码块只生成一个侧栏实例的测试 + +### 手动验证 + +建议在本地文章页验证: + +- 长代码块出现“在侧栏查看” +- 点击后侧栏从右侧打开 +- 代码内容与正文一致 +- 关闭按钮、Esc 与点击遮罩均可关闭侧栏 +- 移动端仍保持现有折叠交互 + +### 文档验证 + +实现完成后,再补充更新: + +- `docs/user-guide/config/notion-next-code-style.md` +- `docs/user-guide/reference/features.md` + +## 实施顺序 + +1. 确认当前 `PrismMac` 注入链路和折叠逻辑。 +2. 先做设计级 helper 拆分。 +3. 再做侧栏 DOM 和样式。 +4. 最后补测试和文档。 + +## 开放问题 + +1. 侧栏宽度是固定值还是按视口比例自适应。 +2. 关闭按钮是否需要额外保留快捷键提示。 +3. 复制按钮是否必须放在首版里,还是与侧栏内容同步即可。 +4. 桌面断点是否直接沿用现有布局断点。 diff --git a/docs/user-guide/config/notion-next-code-style.md b/docs/user-guide/config/notion-next-code-style.md index 335b927d935..97388a193fc 100644 --- a/docs/user-guide/config/notion-next-code-style.md +++ b/docs/user-guide/config/notion-next-code-style.md @@ -21,10 +21,16 @@ PRISM_THEME_DARK_PATH: 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism CODE_MAC_BAR: process.env.NEXT_PUBLIC_CODE_MAC_BAR || true, // 代码左上角显示mac的红黄绿图标 CODE_LINE_NUMBERS: process.env.NEXT_PUBLIC_CODE_LINE_NUMBERS || false, // 是否显示行号 CODE_COLLAPSE: process.env.NEXT_PUBLIC_CODE_COLLAPSE || true, // 是否折叠代码框 +CODE_COLLAPSE_EXPAND_DEFAULT: process.env.NEXT_PUBLIC_CODE_COLLAPSE_EXPAND_DEFAULT || true, // 折叠代码默认是展开状态 +CODE_COLLAPSE_MIN_LINES: process.env.NEXT_PUBLIC_CODE_COLLAPSE_MIN_LINES || 20, // 只有超过该行数的代码块才显示折叠条 // END********代码相关******** ``` +### 代码折叠与侧栏预览 + +开启 `CODE_COLLAPSE` 后,超过 `CODE_COLLAPSE_MIN_LINES` 的长代码块会显示折叠条。桌面端会额外显示“在侧栏查看”按钮,点击后可在右侧固定侧栏中阅读完整代码,并支持复制、关闭按钮、Esc 或点击遮罩退出;移动端保持原有折叠/展开行为。 + ### 代码风格配置说明 浅色模式(日间模式)和深色模式(夜间模式)支持各配置一种代码风格; diff --git a/docs/user-guide/reference/features.md b/docs/user-guide/reference/features.md index 3931357e6a2..a5b6b5378d7 100644 --- a/docs/user-guide/reference/features.md +++ b/docs/user-guide/reference/features.md @@ -123,7 +123,7 @@ | `PRISM_THEME_*` | 高亮主题、深浅切换 | | `CODE_MAC_BAR` | Mac 窗口三色点 | | `CODE_LINE_NUMBERS` | 行号 | -| `CODE_COLLAPSE` | 折叠长代码 | +| `CODE_COLLAPSE` | 折叠长代码,桌面端支持侧栏预览 | | `MERMAID_CDN` | Mermaid 图表 | ## 广告(conf/ad.config.js) diff --git a/public/css/prism-mac-style.css b/public/css/prism-mac-style.css index 7858e3f087f..0d8bde32e4b 100644 --- a/public/css/prism-mac-style.css +++ b/public/css/prism-mac-style.css @@ -95,6 +95,8 @@ pre.notion-code { } .collapse-panel-wrapper { + position: relative; + min-width: 0; border-radius: 14px; border: 1px solid rgba(0, 0, 0, 0.08); background: rgba(255, 255, 255, 0.55); @@ -109,8 +111,16 @@ html.dark .collapse-panel-wrapper { background: rgba(27, 28, 32, 0.6); } +.collapse-header-row { + display: flex; + align-items: center; + min-width: 0; + min-height: 36px; +} + .collapse-header { - width: 100%; + flex: 1 1 auto; + min-width: 0; height: 36px; display: flex; align-items: center; @@ -121,6 +131,7 @@ html.dark .collapse-panel-wrapper { border: none; background: transparent; color: rgba(60, 60, 67, 0.6); + font: inherit; } html.dark .collapse-header { @@ -128,10 +139,51 @@ html.dark .collapse-header { } .collapse-label { + min-width: 0; + overflow: hidden; font-size: 13px; letter-spacing: 0.02em; + text-overflow: ellipsis; + white-space: nowrap; +} + +.collapse-side-panel-button { + flex: 0 0 auto; + height: 26px; + margin-right: 8px; + padding: 0 10px; + border: 1px solid rgba(60, 60, 67, 0.16); + border-radius: 8px; + background: rgba(60, 60, 67, 0.06); + color: rgba(60, 60, 67, 0.72); + font-size: 12px; + line-height: 1; + white-space: nowrap; + cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.collapse-side-panel-button:hover { + background: rgba(60, 60, 67, 0.12); + color: rgba(60, 60, 67, 0.92); +} + +html.dark .collapse-side-panel-button { + border-color: rgba(235, 235, 245, 0.16); + background: rgba(235, 235, 245, 0.08); + color: rgba(235, 235, 245, 0.74); } +html.dark .collapse-side-panel-button:hover { + background: rgba(235, 235, 245, 0.14); + color: rgba(235, 235, 245, 0.94); +} + +@media (max-width: 1023px) { + .collapse-side-panel-button { + display: none; + } +} .collapse-chevron { width: 18px; height: 18px; @@ -144,6 +196,7 @@ html.dark .collapse-header { } .collapse-panel { + min-width: 0; max-height: 0; overflow: hidden; border-top: 1px solid rgba(0, 0, 0, 0.06); @@ -158,7 +211,223 @@ html.dark .collapse-panel { max-height: none; } -/* 6. Prism 代码高亮补丁 (暗底优化) */ +/* 折叠块内只保留外层容器的边框、圆角和阴影 */ +.collapse-panel .code-toolbar, +.collapse-panel pre.notion-code { + width: 100%; + max-width: 100%; + min-width: 0; + margin: 0 !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + background: rgba(27, 28, 32, 0.94) !important; + color: rgba(255, 255, 255, 0.9) !important; +} + +html.dark .collapse-panel .code-toolbar, +html.dark .collapse-panel pre.notion-code { + background: rgba(27, 28, 32, 0.72) !important; + -webkit-backdrop-filter: saturate(140%) blur(12px); + backdrop-filter: saturate(140%) blur(12px); +} + +.collapse-panel .code-toolbar > pre.notion-code { + max-width: 100%; + margin: 0 !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; +} + +.collapse-panel pre.notion-code > code { + background: transparent !important; + color: inherit !important; + text-shadow: none !important; +} + +.collapse-header:focus-visible, +.collapse-side-panel-button:focus-visible, +.code-side-panel-copy:focus-visible, +.code-side-panel-close:focus-visible { + outline: 2px solid rgba(66, 153, 225, 0.9); + outline-offset: 2px; +} + +/* 6. 桌面端代码侧栏预览 */ +.code-side-panel-root { + position: fixed; + inset: 0; + z-index: 80; + pointer-events: none; +} + +.code-side-panel-backdrop { + position: absolute; + inset: 0; + padding: 0; + border: 0; + background: rgba(12, 14, 18, 0); + cursor: default; + opacity: 0; + pointer-events: auto; + transition: opacity 0.22s ease, background-color 0.22s ease; +} + +.code-side-panel-root.is-open .code-side-panel-backdrop { + background: rgba(12, 14, 18, 0.36); + opacity: 1; +} + +.code-side-panel-drawer { + position: absolute; + top: 0; + right: 0; + z-index: 1; + display: flex; + flex-direction: column; + width: clamp(480px, 44vw, 760px); + max-width: calc(100vw - 64px); + height: 100dvh; + max-height: 100vh; + border-left: 1px solid rgba(255, 255, 255, 0.14); + background: #20232b; + box-shadow: -20px 0 48px rgba(0, 0, 0, 0.32); + color-scheme: dark; + opacity: 0; + pointer-events: auto; + transform: translateX(24px); + transition: opacity 0.22s ease, transform 0.22s ease; +} + +.code-side-panel-root.is-open .code-side-panel-drawer { + opacity: 1; + transform: translateX(0); +} + +.code-side-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + min-height: 58px; + padding: 12px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.code-side-panel-heading { + min-width: 0; +} + +.code-side-panel-title { + overflow: hidden; + color: rgba(255, 255, 255, 0.92); + font-size: 13px; + font-weight: 700; + letter-spacing: 0.06em; + text-overflow: ellipsis; + white-space: nowrap; +} + +.code-side-panel-meta { + margin-top: 3px; + color: rgba(255, 255, 255, 0.48); + font-size: 12px; +} + +.code-side-panel-actions { + display: flex; + flex: 0 0 auto; + gap: 8px; +} + +.code-side-panel-copy, +.code-side-panel-close { + height: 30px; + min-width: 48px; + padding: 0 10px; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 8px; + background: rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.82); + font-size: 12px; + line-height: 1; + white-space: nowrap; + cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.code-side-panel-copy:hover, +.code-side-panel-close:hover { + background: rgba(255, 255, 255, 0.16); + color: #fff; +} + +.code-side-panel-code { + flex: 1 1 auto; + min-width: 0; + margin: 0 !important; + padding: 18px !important; + border: 0 !important; + border-radius: 0 !important; + background: transparent !important; + color: rgba(255, 255, 255, 0.9) !important; + font-size: 0.9em !important; + line-height: 1.65 !important; + overflow: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; +} + +.code-side-panel-code code { + display: block; + min-width: max-content; + color: inherit; + font-family: inherit; + white-space: pre; +} + +html.dark .code-side-panel-drawer { + border-left-color: rgba(255, 255, 255, 0.14); + background: #17191f; + box-shadow: -18px 0 44px rgba(0, 0, 0, 0.42); +} + +@media (max-width: 1023px) { + .collapse-wrapper { + margin: 0.75rem 0; + } + + .collapse-header-row { + min-height: 34px; + } + + .collapse-header { + height: 34px; + padding: 0 10px; + } + + .collapse-label { + font-size: 12px; + } + + .collapse-chevron { + width: 16px; + height: 16px; + } + + .collapse-panel pre.notion-code { + padding-right: 10px !important; + padding-left: 10px !important; + font-size: 0.82rem !important; + line-height: 1.55 !important; + } + + .code-side-panel-root { + display: none; + } +} +/* 7. Prism 代码高亮补丁 (暗底优化) */ .code-toolbar .token.comment, .code-toolbar .token.prolog, .code-toolbar .token.doctype, diff --git a/themes/claude/style.js b/themes/claude/style.js index c304fe55459..75a59ce3eca 100644 --- a/themes/claude/style.js +++ b/themes/claude/style.js @@ -124,7 +124,7 @@ const Style = () => { --claude-code-bg: var(--claude-bg); --claude-code-border: rgb(222 222 222); --claude-code-shell-bg: rgb(243 243 243); - --claude-code-shell-border: rgba(255, 255, 255, 0.1); + --claude-code-shell-border: rgb(222 222 222); --claude-code-shell-text: rgb(10 10 10); --claude-code-text: #657b83; --claude-code-token-comment: #93a1a1; @@ -2140,7 +2140,7 @@ const Style = () => { color: var(--claude-code-shell-text) !important; } - /* Collapse wrapper from PrismMac: remove extra header/borders to avoid double frame */ + /* Collapse wrapper from PrismMac: make the shell itself carry the code frame */ #theme-claude .collapse-wrapper { width: 100% !important; padding: 0 !important; @@ -2149,9 +2149,9 @@ const Style = () => { #theme-claude .collapse-wrapper > div { box-sizing: border-box !important; background-clip: border-box !important; - border: none !important; - border-radius: 0 !important; - background: transparent !important; + border: 1px solid var(--claude-code-shell-border) !important; + border-radius: 0.875rem !important; + background: var(--claude-code-shell-bg) !important; color: var(--claude-code-shell-text) !important; font-family: var(--claude-body-font) !important; font-size: 1rem !important; @@ -2159,13 +2159,14 @@ const Style = () => { line-height: 1.75rem !important; padding: 0 !important; box-shadow: none !important; + overflow: hidden !important; } .dark #theme-claude .collapse-wrapper > div { box-sizing: border-box !important; background-clip: border-box !important; - border: none !important; - border-radius: 0 !important; - background: transparent !important; + border: 1px solid var(--claude-code-shell-border) !important; + border-radius: 0.875rem !important; + background: var(--claude-code-shell-bg) !important; color: var(--claude-code-shell-text) !important; } #theme-claude .collapse-wrapper .code-toolbar { @@ -2173,7 +2174,25 @@ const Style = () => { padding: 0 !important; border: none !important; border-radius: 0 !important; - background: transparent !important; + background: var(--claude-code-shell-bg) !important; + } + #theme-claude .collapse-panel > .code-toolbar { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + margin: 0 !important; + border: none !important; + border-radius: 0 !important; + box-shadow: none !important; + background: var(--claude-code-shell-bg) !important; + } + #theme-claude .collapse-panel > .code-toolbar > pre.notion-code { + max-width: 100% !important; + margin: 0 !important; + border: none !important; + border-radius: 0 !important; + box-shadow: none !important; + background: var(--claude-code-bg) !important; } #theme-claude .collapse-wrapper > div > div.cursor-pointer.select-none { display: none !important;