diff --git a/frontend/src/components/FloatingToc/FloatingToc.css b/frontend/src/components/FloatingToc/FloatingToc.css new file mode 100644 index 0000000..188573f --- /dev/null +++ b/frontend/src/components/FloatingToc/FloatingToc.css @@ -0,0 +1,189 @@ +.floating-toc { + position: fixed; + top: 50%; + right: 24px; + z-index: 30; + width: 48px; + height: 180px; + transform: translateY(-50%); + outline: none; +} + +.floating-toc-tab { + position: absolute; + top: 0; + right: 0; + width: 48px; + height: 180px; + border: 1px solid color-mix(in srgb, var(--border-color) 82%, var(--text-color-secondary)); + border-radius: 8px; + background: color-mix(in srgb, var(--card-bg) 92%, transparent); + color: var(--text-color-secondary); + box-shadow: 0 12px 30px rgba(15, 23, 42, 0.12); + backdrop-filter: blur(12px); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + cursor: default; + transition: opacity 0.18s ease, transform 0.18s ease, border-color 0.18s ease, color 0.18s ease; +} + +.floating-toc-tab span { + writing-mode: vertical-rl; + text-orientation: mixed; + font-size: 12px; + line-height: 1; + letter-spacing: 0; + white-space: nowrap; +} + +.floating-toc-panel { + position: absolute; + top: 0; + right: 0; + width: min(420px, calc(100vw - 320px)); + min-width: 300px; + max-height: min(58vh, 460px); + border: 1px solid color-mix(in srgb, var(--border-color) 78%, var(--text-color-secondary)); + border-radius: 8px; + background: color-mix(in srgb, var(--card-bg) 96%, transparent); + color: var(--text-color); + box-shadow: 0 22px 48px rgba(15, 23, 42, 0.18); + backdrop-filter: blur(16px); + opacity: 0; + pointer-events: none; + transform: translateX(10px) scale(0.98); + transform-origin: top right; + overflow: hidden; + transition: opacity 0.18s ease, transform 0.18s ease; +} + +.floating-toc:hover .floating-toc-tab, +.floating-toc:focus-within .floating-toc-tab { + opacity: 0; + transform: translateX(8px) scale(0.96); + pointer-events: none; +} + +.floating-toc:hover .floating-toc-panel, +.floating-toc:focus-within .floating-toc-panel { + opacity: 1; + pointer-events: auto; + transform: translateX(0) scale(1); +} + +.floating-toc-header { + min-height: 48px; + padding: 0 16px; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + color: var(--text-color); + font-size: 14px; + font-weight: 600; +} + +.floating-toc-count { + min-width: 34px; + height: 22px; + padding: 0 8px; + border-radius: 999px; + background: var(--item-hover-bg); + color: var(--text-color-secondary); + font-size: 12px; + font-weight: 600; + line-height: 22px; + text-align: center; +} + +.floating-toc-content { + max-height: calc(min(58vh, 460px) - 49px); + overflow-y: auto; + overflow-x: hidden; + padding: 10px 8px 12px; +} + +.floating-toc-content .ant-anchor { + padding-left: 0; +} + +.floating-toc-content .ant-anchor::before { + display: none; +} + +.floating-toc-content .ant-anchor-ink { + display: none; +} + +.floating-toc-content .ant-anchor-link { + padding: 2px 0; +} + +.floating-toc-content .ant-anchor-link-title { + border-radius: 6px; + color: var(--text-color-secondary); + line-height: 1.45; + transition: background 0.16s ease, color 0.16s ease; +} + +.floating-toc-content .ant-anchor-link-title:hover, +.floating-toc-content .ant-anchor-link-active > .ant-anchor-link-title { + background: var(--item-hover-bg); + color: var(--link-color); +} + +.floating-toc-item { + min-height: 34px; + padding: 7px 8px; + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.floating-toc-item-icon { + flex: none; + font-size: 12px; + color: currentColor; + opacity: 0.72; +} + +.floating-toc-item-title { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 13px; +} + +.floating-toc-empty { + padding: 28px 12px; + color: var(--text-color-secondary); + text-align: center; + font-size: 13px; +} + +body.dark .floating-toc-tab, +body.dark .floating-toc-panel { + box-shadow: 0 22px 48px rgba(0, 0, 0, 0.38); +} + +@media (max-width: 1024px) { + .floating-toc { + right: 16px; + } + + .floating-toc-panel { + width: min(360px, calc(100vw - 300px)); + } +} + +@media (max-width: 768px) { + .floating-toc { + display: none; + } +} diff --git a/frontend/src/components/FloatingToc/FloatingToc.jsx b/frontend/src/components/FloatingToc/FloatingToc.jsx new file mode 100644 index 0000000..ea56046 --- /dev/null +++ b/frontend/src/components/FloatingToc/FloatingToc.jsx @@ -0,0 +1,55 @@ +import { Anchor } from 'antd' +import { FileTextOutlined, MenuOutlined } from '@ant-design/icons' +import './FloatingToc.css' + +export default function FloatingToc({ + items = [], + getContainer, + searchKeyword = '', + renderTitle, + className = '', +}) { + const anchorItems = items.map((item) => ({ + key: item.key, + href: item.href, + title: ( +