diff --git a/web/navi.css b/web/navi.css index f2bf999..6e11705 100644 --- a/web/navi.css +++ b/web/navi.css @@ -6,21 +6,44 @@ html { text-size-adjust: 100%; } +/* 禁用移动端/Chromium 对可点击元素注入的蓝色点击闪烁。 */ +html[data-theme] *, +html[data-theme] *::before, +html[data-theme] *::after { + -webkit-tap-highlight-color: transparent !important; +} + html[data-theme] body { + /* border-box 让 padding 计入 min-height:否则 min-height:100vh 与 + padding-bottom 相加,短页面(极简/工作空间)必然多出一屏滚动。 */ + box-sizing: border-box !important; min-width: 0; overflow-x: hidden; - /* 悬浮搜索栏固定在底部,给内容留出空间,避免遮挡页脚备案号。 */ - padding-bottom: 84px !important; + /* 列向 flex + 页脚 margin-top:auto,把备案号顶到页面底部而非浮在中间。 */ + display: flex !important; + flex-direction: column !important; + min-height: 100vh !important; + /* 备案号占据真正的页面最底部;悬浮控件在它上方,不再用底部 padding + 把页脚整体抬高。 */ + padding-bottom: 0 !important; } -/* Dashy 的 #dashy 本身撑满 100vh;不扣掉留给悬浮搜索栏的高度, - 短页面(极简视图/工作空间)就会凭空多出约 134px 的纵向滚动。 */ +html[data-theme] #app { + width: 100% !important; +} + +/* Dashy 的 #dashy 默认 min-height:100vh,短页面叠加页脚后必出滚动; + 交回内容自然撑高,视口高度由 body 兜住。 */ html[data-theme] #dashy { - min-height: calc(100vh - 84px) !important; + min-height: auto !important; } -/* Dashy's native filter/settings strip duplicates the portal search and breaks on phones. */ +/* Dashy's native filter/settings strip duplicates the portal search and breaks on phones. + 原生设置入口(齿轮/铅笔/调色板)与我们的 /admin/ 重复,统一隐藏。 */ html[data-theme] .home > .settings-outer, +html[data-theme] .config-launcher, +html[data-theme] .options-trigger, +html[data-theme] .settings-outer, html[data-theme] .minimal-home .title-and-search > form { display: none !important; } @@ -98,7 +121,7 @@ html[data-theme] .navi-search-bar { box-sizing: border-box !important; position: fixed !important; left: calc(16px + env(safe-area-inset-left, 0px)) !important; - bottom: calc(16px + env(safe-area-inset-bottom, 0px)) !important; + bottom: calc(76px + env(safe-area-inset-bottom, 0px)) !important; margin: 0 !important; display: flex !important; align-items: center !important; @@ -144,20 +167,37 @@ html[data-theme] .navi-search-toggle { border: 0 !important; border-radius: 50% !important; cursor: pointer; - transition: background .2s ease, color .2s ease, transform .2s cubic-bezier(.22, 1, .36, 1) !important; + appearance: none !important; + -webkit-appearance: none !important; + -webkit-tap-highlight-color: transparent; + transition: transform .28s cubic-bezier(.22, 1, .36, 1) !important; } -html[data-theme] .navi-search-toggle:hover { - color: #0284c7 !important; - background: rgba(14, 165, 233, .12) !important; - transform: scale(1.06); +/* 点击与聚焦都不变蓝,只保留轻微缩放反馈。 */ +html[data-theme] .navi-search-toggle:hover, +html[data-theme] .navi-search-toggle:focus, +html[data-theme] .navi-search-toggle:active { + color: #0f172a !important; + background: transparent !important; + outline: none !important; +} + +@media (hover: hover) { + html[data-theme] .navi-search-toggle:hover { + transform: scale(1.05); + } +} + +html[data-theme] .navi-search-toggle:active { + transform: scale(.96); } /* 右侧管理入口:Dashy 页面本身没有去 /admin/ 的链接 */ html[data-theme] .navi-admin-entry { + box-sizing: border-box !important; position: fixed !important; right: calc(16px + env(safe-area-inset-right, 0px)) !important; - bottom: calc(16px + env(safe-area-inset-bottom, 0px)) !important; + bottom: calc(76px + env(safe-area-inset-bottom, 0px)) !important; width: 52px !important; height: 52px !important; display: grid !important; @@ -170,13 +210,26 @@ html[data-theme] .navi-admin-entry { box-shadow: 0 14px 38px rgba(15, 23, 42, .16), inset 0 1px 0 rgba(255, 255, 255, .95) !important; backdrop-filter: blur(22px) saturate(180%) !important; -webkit-backdrop-filter: blur(22px) saturate(180%) !important; - transition: transform .22s cubic-bezier(.22, 1, .36, 1), color .2s ease, background .2s ease !important; + -webkit-tap-highlight-color: transparent !important; + transition: transform .22s cubic-bezier(.22, 1, .36, 1), background .2s ease !important; } -html[data-theme] .navi-admin-entry:hover { - color: #0284c7 !important; +html[data-theme] .navi-admin-entry:hover, +html[data-theme] .navi-admin-entry:focus, +html[data-theme] .navi-admin-entry:active { + color: #334155 !important; background: rgba(255, 255, 255, .94) !important; - transform: translateY(-1px) scale(1.05); + outline: none !important; +} + +@media (hover: hover) { + html[data-theme] .navi-admin-entry:hover { + transform: translateY(-1px) scale(1.05); + } +} + +html[data-theme] .navi-admin-entry:active { + transform: scale(.96); } html[data-theme] .navi-admin-entry svg { @@ -189,6 +242,114 @@ html[data-theme] body:has(.modal-overlay) > .navi-admin-entry { pointer-events: none !important; } +/* ── 视图切换:圆形悬浮按钮,紧贴管理入口左侧 ── + 原生 .switch-view-buttons 是侧栏底部的方形图标,这里一并隐藏。 */ +html[data-theme] nav.side-bar .switch-view-buttons { + display: none !important; +} + +html[data-theme] .navi-view-switch { + position: fixed !important; + right: calc(78px + env(safe-area-inset-right, 0px)) !important; + bottom: calc(76px + env(safe-area-inset-bottom, 0px)) !important; + z-index: 900 !important; +} + +html[data-theme] .navi-view-toggle { + box-sizing: border-box !important; + width: 52px !important; + height: 52px !important; + display: grid !important; + place-items: center !important; + padding: 0 !important; + color: #334155 !important; + background: rgba(255, 255, 255, .72) !important; + border: 1px solid rgba(255, 255, 255, .9) !important; + border-radius: 50% !important; + box-shadow: 0 14px 38px rgba(15, 23, 42, .16), inset 0 1px 0 rgba(255, 255, 255, .95) !important; + backdrop-filter: blur(22px) saturate(180%) !important; + -webkit-backdrop-filter: blur(22px) saturate(180%) !important; + -webkit-tap-highlight-color: transparent !important; + appearance: none !important; + -webkit-appearance: none !important; + outline: none !important; + cursor: pointer; + transition: transform .22s cubic-bezier(.22, 1, .36, 1), background .2s ease !important; +} + +html[data-theme] .navi-view-toggle:hover, +html[data-theme] .navi-view-toggle:focus, +html[data-theme] .navi-view-toggle:active { + color: #334155 !important; + background: rgba(255, 255, 255, .94) !important; +} + +@media (hover: hover) { + html[data-theme] .navi-view-toggle:hover { + transform: translateY(-1px) scale(1.05); + } +} + +html[data-theme] .navi-view-toggle:active { + transform: scale(.96); +} + +html[data-theme] .navi-view-toggle svg { + width: 22px !important; + height: 22px !important; +} + +html[data-theme] .navi-view-menu { + position: absolute !important; + right: 0 !important; + bottom: 62px !important; + min-width: 134px !important; + display: flex !important; + flex-direction: column !important; + gap: 2px !important; + padding: 7px !important; + background: rgba(255, 255, 255, .92) !important; + border: 1px solid rgba(255, 255, 255, .95) !important; + border-radius: 16px !important; + box-shadow: 0 18px 40px rgba(15, 23, 42, .18), inset 0 1px 0 rgba(255, 255, 255, .95) !important; + backdrop-filter: blur(20px) saturate(180%) !important; + -webkit-backdrop-filter: blur(20px) saturate(180%) !important; + animation: navi-fade-up .22s cubic-bezier(.22, 1, .36, 1) both; +} + +html[data-theme] .navi-view-menu[hidden] { + display: none !important; +} + +html[data-theme] .navi-view-option { + padding: 9px 12px !important; + color: #334155 !important; + font-size: 13.5px !important; + text-decoration: none !important; + border-radius: 10px !important; + white-space: nowrap !important; + -webkit-tap-highlight-color: transparent !important; +} + +html[data-theme] .navi-view-option:hover, +html[data-theme] .navi-view-option:focus, +html[data-theme] .navi-view-option:active { + color: #0f172a !important; + background: rgba(226, 232, 240, .7) !important; + outline: none !important; +} + +html[data-theme] .navi-view-option.is-active { + color: #0f172a !important; + background: rgba(186, 230, 253, .55) !important; + font-weight: 600 !important; +} + +html[data-theme] body:has(.modal-overlay) > .navi-view-switch { + visibility: hidden !important; + pointer-events: none !important; +} + html[data-theme] .navi-search-bar input { min-width: 0 !important; height: 42px !important; @@ -221,6 +382,8 @@ html[data-theme] .navi-search-submit { border-radius: 14px !important; box-shadow: 0 8px 20px rgba(37, 99, 235, .28) !important; cursor: pointer; + -webkit-tap-highlight-color: transparent !important; + outline: none !important; } html[data-theme] .navi-search-toggle svg, @@ -229,6 +392,39 @@ html[data-theme] .navi-search-submit svg { height: 20px; } +/* 放大镜与搜索框同步形变:圆环、手柄退场,向左收起箭头进入。 */ +html[data-theme] .navi-search-toggle svg { + overflow: visible; +} + +html[data-theme] .navi-magnifier-ring, +html[data-theme] .navi-magnifier-handle, +html[data-theme] .navi-collapse-arrow { + transform-box: fill-box; + transform-origin: center; + transition: opacity .22s ease, transform .28s cubic-bezier(.22, 1, .36, 1) !important; +} + +html[data-theme] .navi-collapse-arrow { + opacity: 0; + transform: translateX(5px) scale(.62); +} + +html[data-theme] .navi-search-bar[data-state="open"] .navi-magnifier-ring { + opacity: 0; + transform: translateX(-4px) scale(.28); +} + +html[data-theme] .navi-search-bar[data-state="open"] .navi-magnifier-handle { + opacity: 0; + transform: translate(-4px, -4px) rotate(-35deg) scale(.45); +} + +html[data-theme] .navi-search-bar[data-state="open"] .navi-collapse-arrow { + opacity: 1; + transform: translateX(0) scale(1); +} + /* Minimal view is the phone/tablet layout used by Dashy. */ html[data-theme] .minimal-home { box-sizing: border-box !important; @@ -244,13 +440,49 @@ html[data-theme] .minimal-home .title-and-search { padding: 0 8px !important; } +/* ── 顶部标题:三个视图统一(同字号/字重/字距/居中)── + 此前 /minimal 是 48px 居中,而 / 与 /workspace 是顶栏里 40px 左对齐, + 同一个站点的三个视图两套标题样式,很割裂。 */ +html[data-theme] #dashy > header { + display: flex !important; + align-items: center !important; + justify-content: center !important; + padding: 10px 12px !important; +} + +html[data-theme] #dashy > header .page-titles { + display: block !important; + width: auto !important; + max-width: 100% !important; + margin: 0 auto !important; + text-align: center !important; +} + +html[data-theme] #dashy > header .page-titles .text { + display: block !important; + text-align: center !important; +} + +html[data-theme] #dashy > header .page-titles .subtitle { + display: block !important; + margin: 6px 0 0 !important; + color: #64748b !important; + font-size: 13.5px !important; + font-weight: 400 !important; + letter-spacing: 0 !important; + text-align: center !important; +} + +html[data-theme] #dashy > header .page-titles h1, html[data-theme] .minimal-home .title-and-search h1 { max-width: 100%; margin: 0 !important; color: #0f172a !important; - font-size: clamp(2rem, 8vw, 3rem) !important; + font-size: clamp(1.75rem, 4vw, 2.75rem) !important; + font-weight: 700 !important; line-height: 1.08 !important; - letter-spacing: -.04em !important; + letter-spacing: -.035em !important; + text-align: center !important; overflow-wrap: anywhere; } @@ -331,8 +563,9 @@ html[data-theme] .minimal-section-inner a.item { padding-top: 22px !important; } + html[data-theme] #dashy > header .page-titles h1, html[data-theme] .minimal-home .title-and-search h1 { - font-size: clamp(1.9rem, 9.4vw, 2.55rem) !important; + font-size: clamp(1.6rem, 8vw, 2.2rem) !important; } html[data-theme] .minimal-home > .item-group-container { @@ -410,7 +643,7 @@ html[data-theme] .minimal-section-inner a.item { html[data-theme] .navi-search-bar { left: calc(10px + env(safe-area-inset-left, 0px)) !important; - bottom: calc(12px + env(safe-area-inset-bottom, 0px)) !important; + bottom: calc(76px + env(safe-area-inset-bottom, 0px)) !important; } html[data-theme] .navi-search-bar[data-state="collapsed"] { @@ -428,19 +661,25 @@ html[data-theme] .minimal-section-inner a.item { width: 48px !important; height: 48px !important; right: calc(10px + env(safe-area-inset-right, 0px)) !important; - bottom: calc(12px + env(safe-area-inset-bottom, 0px)) !important; + bottom: calc(76px + env(safe-area-inset-bottom, 0px)) !important; + } + + html[data-theme] .navi-view-switch { + right: calc(68px + env(safe-area-inset-right, 0px)) !important; + } + + html[data-theme] .navi-view-toggle { + width: 48px !important; + height: 48px !important; + } + + html[data-theme] .navi-view-toggle svg { + width: 20px !important; + height: 20px !important; } html[data-theme] body { - padding-bottom: 78px !important; - } - - html[data-theme] #dashy { - min-height: calc(100vh - 78px) !important; - } - - html[data-theme] .navi-footer-host { - bottom: calc(78px + env(safe-area-inset-bottom, 0px)) !important; + padding-bottom: 0 !important; } } @@ -449,7 +688,7 @@ html[data-theme] .minimal-section-inner a.item { 内联样式里的 margin-bottom:80px(那是给旧的流式搜索栏让位的)。 */ html[data-theme] footer { margin: 0 !important; - padding: 0 0 4px !important; + padding: 0 0 8px !important; background: transparent !important; } @@ -458,22 +697,13 @@ html[data-theme] footer > div { padding: 0 !important; } -/* 极简/工作空间视图补的备案号页脚:固定悬浮在搜索栏正上方。 - 它是「底部固定家具」的一部分,不能再参与文档流,否则会把页面撑高导致多余滚动。 */ +/* 极简/工作空间视图补的备案号页脚:留在文档流里,靠 body 的 flex + 把它顶到页面底部(贴着悬浮搜索栏上方),而不是 fixed 浮在内容中间。 */ html[data-theme] .navi-footer-host { - position: fixed !important; - left: 0 !important; - right: 0 !important; - bottom: calc(84px + env(safe-area-inset-bottom, 0px)) !important; - z-index: 890 !important; + margin-top: auto !important; display: flex !important; justify-content: center !important; - padding: 0 8px !important; - pointer-events: none !important; -} - -html[data-theme] .navi-footer-host .navi-footer { - pointer-events: auto !important; + padding: 8px !important; } html[data-theme] .navi-footer { @@ -501,18 +731,132 @@ html[data-theme] .navi-footer a { color: #64748b !important; text-decoration: none !important; border-bottom: 1px dashed rgba(100, 116, 139, .45) !important; - transition: color .2s ease, border-color .2s ease !important; + -webkit-tap-highlight-color: transparent !important; } -html[data-theme] .navi-footer a:hover { - color: #0284c7 !important; - border-bottom-color: rgba(2, 132, 199, .6) !important; +html[data-theme] .navi-footer a:hover, +html[data-theme] .navi-footer a:focus, +html[data-theme] .navi-footer a:active { + color: #64748b !important; + border-bottom-color: rgba(100, 116, 139, .45) !important; + outline: none !important; } html[data-theme] .navi-footer-sep { color: rgba(100, 116, 139, .45) !important; } +/* ── 工作空间:右侧改为「分组应用列表」面板 ── + Dashy 原生面板是 iframe,目标站点多数用 X-Frame-Options 拒绝被嵌, + 会呈现一片空白;这里改为直接展示我们自己的卡片列表。 + 面板走正常文档流(而不是 fixed 钉在中间),否则会把页面切成 + 「顶栏 / 大方块 / 底部」三段,看起来上下都被截断。 */ +html[data-theme] .work-space .web-content, +html[data-theme] .work-space .multi-taking-view, +html[data-theme] .work-space .workspace-widget-view { + display: none !important; +} + +html[data-theme] .work-space { + height: auto !important; + min-height: 0 !important; +} + +html[data-theme] .navi-ws-pane { + box-sizing: border-box !important; + position: static !important; + display: flex !important; + flex-direction: column !important; + gap: 14px !important; + min-height: 340px !important; + margin: 18px 18px 14px 72px !important; + padding: 20px !important; + background: rgba(255, 255, 255, .5) !important; + border: 1px solid rgba(255, 255, 255, .82) !important; + border-radius: 20px !important; + box-shadow: 0 18px 44px rgba(15, 23, 42, .12), inset 0 1px 0 rgba(255, 255, 255, .92) !important; + backdrop-filter: blur(18px) saturate(180%) !important; + -webkit-backdrop-filter: blur(18px) saturate(180%) !important; + z-index: 1 !important; +} + +html[data-theme] .navi-ws-title { + margin: 0 !important; + color: #0f172a !important; + font-size: 19px !important; + font-weight: 650 !important; + letter-spacing: -.01em !important; +} + +html[data-theme] .navi-ws-hint { + color: #64748b !important; + font-size: 13.5px !important; +} + +html[data-theme] .navi-ws-grid { + display: grid !important; + grid-template-columns: repeat(auto-fill, minmax(216px, 1fr)) !important; + gap: 12px !important; +} + +html[data-theme] .navi-ws-card { + box-sizing: border-box !important; + display: flex !important; + flex-direction: column !important; + gap: 3px !important; + min-height: 78px !important; + padding: 14px 16px !important; + color: #0f172a !important; + text-decoration: none !important; + background: rgba(255, 255, 255, .68) !important; + border: 1px solid rgba(255, 255, 255, .9) !important; + border-radius: 14px !important; + box-shadow: 0 8px 20px rgba(15, 23, 42, .07) !important; + -webkit-tap-highlight-color: transparent !important; + transition: transform .22s cubic-bezier(.22, 1, .36, 1), box-shadow .22s ease !important; +} + +@media (hover: hover) { + html[data-theme] .navi-ws-card:hover { + transform: translateY(-2px); + box-shadow: 0 14px 30px rgba(15, 23, 42, .12) !important; + } +} + +html[data-theme] .navi-ws-card:active { + transform: scale(.99); +} + +html[data-theme] .navi-ws-card-icon { + display: inline-flex !important; + align-items: center !important; + width: 26px !important; + height: 26px !important; + margin-bottom: 3px !important; +} + +html[data-theme] .navi-ws-card-icon img { + width: 26px !important; + height: 26px !important; + object-fit: contain !important; +} + +html[data-theme] .navi-ws-card-title { + font-size: 14.5px !important; + font-weight: 600 !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; +} + +html[data-theme] .navi-ws-card-url { + color: #64748b !important; + font-size: 12px !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; +} + /* ── 工作空间:侧栏与内容区同样走玻璃圆角 ── */ html[data-theme] nav.side-bar { background: rgba(255, 255, 255, .62) !important; @@ -585,6 +929,11 @@ html[data-theme] .navi-search-submit:hover { box-shadow: 0 12px 26px rgba(37, 99, 235, .34) !important; } +html[data-theme] .navi-search-submit:focus, +html[data-theme] .navi-search-submit:active { + outline: none !important; +} + html[data-theme] .navi-search-submit:active { transform: translateY(0) scale(.96); } diff --git a/web/navi.js b/web/navi.js index fa6f2a9..664ae32 100644 --- a/web/navi.js +++ b/web/navi.js @@ -28,9 +28,12 @@ return el; } + // 收起态是放大镜,展开时通过 transform/opacity 平滑形变为向左箭头, + // 让图标动效与搜索框的宽度动画保持一致。 const magnifierIcon = () => svg([ - { tag: 'circle', attrs: { cx: '11', cy: '11', r: '7.5' } }, - { tag: 'line', attrs: { x1: '20.5', y1: '20.5', x2: '16.2', y2: '16.2' } }, + { tag: 'circle', attrs: { class: 'navi-magnifier-ring', cx: '11', cy: '11', r: '7.5' } }, + { tag: 'line', attrs: { class: 'navi-magnifier-handle', x1: '20.5', y1: '20.5', x2: '16.2', y2: '16.2' } }, + { tag: 'polyline', attrs: { class: 'navi-collapse-arrow', points: '14.5 6.5 9 12 14.5 17.5' } }, ], { size: '20' }); const arrowIcon = () => svg([ @@ -134,6 +137,205 @@ document.body.append(link); } + // 视图切换:Dashy 原生的切换按钮挤在侧栏底部、且是方形图标。 + // 这里换成圆形悬浮按钮,紧挨管理入口左侧,点开是一组视图链接。 + const VIEWS = [ + { path: '/', label: '首页' }, + { path: '/minimal', label: '极简' }, + { path: '/workspace', label: '工作空间' }, + ]; + + const viewIcon = () => svg([ + { tag: 'rect', attrs: { x: '3', y: '3.5', width: '18', height: '17', rx: '3.4' } }, + { tag: 'line', attrs: { x1: '9.4', y1: '3.5', x2: '9.4', y2: '20.5' } }, + { tag: 'line', attrs: { x1: '12.8', y1: '9.4', x2: '17.8', y2: '9.4' } }, + { tag: 'line', attrs: { x1: '12.8', y1: '14.2', x2: '17.8', y2: '14.2' } }, + ], { size: '20', width: '1.9' }); + + function currentView() { + const p = location.pathname.replace(/\/+$/, '') || '/'; + if (p.endsWith('/minimal')) return '/minimal'; + if (p.endsWith('/workspace')) return '/workspace'; + return '/'; + } + + function mountViewSwitch() { + if (document.querySelector('.navi-view-switch')) return; + + const wrap = document.createElement('div'); + wrap.className = 'navi-view-switch'; + + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'navi-view-toggle'; + btn.setAttribute('aria-label', '切换视图'); + btn.setAttribute('aria-expanded', 'false'); + btn.append(viewIcon()); + + const menu = document.createElement('div'); + menu.className = 'navi-view-menu'; + menu.hidden = true; + const active = currentView(); + VIEWS.forEach((v) => { + const a = document.createElement('a'); + a.className = 'navi-view-option'; + a.href = v.path; + a.textContent = v.label; + if (v.path === active) { + a.classList.add('is-active'); + a.setAttribute('aria-current', 'page'); + } + menu.append(a); + }); + + const close = () => { + menu.hidden = true; + btn.setAttribute('aria-expanded', 'false'); + }; + + btn.addEventListener('click', (evt) => { + evt.preventDefault(); + evt.stopPropagation(); + const open = menu.hidden; + menu.hidden = !open; + btn.setAttribute('aria-expanded', String(open)); + }); + + document.addEventListener('click', (evt) => { + if (!menu.hidden && !wrap.contains(evt.target)) close(); + }); + + document.addEventListener('keydown', (evt) => { + if (evt.key === 'Escape' && !menu.hidden) close(); + }); + + wrap.append(btn, menu); + document.body.append(wrap); + } + + // Dashy 的「工作空间」右侧面板是一个 iframe:侧栏点应用时把目标 URL + // 内嵌进去。但多数应用(Gitea、VanBlog 等)用 X-Frame-Options 拒绝被嵌, + // 面板就是一片空白(实测:Refused to display … 'sameorigin')。 + // 所以这里改成:点左侧分组 → 右侧列出该分组的应用卡片,不依赖内嵌。 + const WS_ICON_RE = /\.(png|svg|jpe?g|webp|gif|ico)$/i; + let wsSections = null; + let wsPending = false; + + async function fetchWorkspaceSections() { + if (wsSections) return wsSections; + if (wsPending) return null; + wsPending = true; + try { + const res = await fetch('/admin/api/sections', { credentials: 'same-origin' }); + if (!res.ok) return null; + const data = await res.json(); + const list = Array.isArray(data) ? data : (data.sections || []); + // 与 Dashy 侧栏保持同序:剔除标记了 hideFromWorkspace 的分组。 + wsSections = list.filter((s) => !(s && s.displayData && s.displayData.hideFromWorkspace)); + return wsSections; + } catch (err) { + return null; + } finally { + wsPending = false; + } + } + + function workspacePane() { + const host = document.querySelector('.work-space'); + if (!host) return null; + let pane = host.querySelector('.navi-ws-pane'); + if (!pane) { + pane = document.createElement('div'); + pane.className = 'navi-ws-pane'; + host.append(pane); + } + return pane; + } + + function renderWorkspaceSection(section) { + const pane = workspacePane(); + if (!pane || !section) return; + while (pane.firstChild) pane.removeChild(pane.firstChild); + + const title = document.createElement('h2'); + title.className = 'navi-ws-title'; + title.textContent = section.name || ''; + pane.append(title); + + const items = (section.items || []).filter((it) => it && it.url); + if (!items.length) { + const empty = document.createElement('div'); + empty.className = 'navi-ws-hint'; + empty.textContent = '这个分组还没有项目'; + pane.append(empty); + return; + } + + const grid = document.createElement('div'); + grid.className = 'navi-ws-grid'; + items.forEach((it) => { + const card = document.createElement('a'); + card.className = 'navi-ws-card'; + card.href = it.url; + card.target = '_blank'; + card.rel = 'noopener noreferrer'; + + const icon = String(it.icon || ''); + if (WS_ICON_RE.test(icon)) { + const wrap = document.createElement('span'); + wrap.className = 'navi-ws-card-icon'; + const img = document.createElement('img'); + img.src = '/item-icons/' + icon; + img.alt = ''; + img.loading = 'lazy'; + wrap.append(img); + card.append(wrap); + } + + const name = document.createElement('span'); + name.className = 'navi-ws-card-title'; + name.textContent = it.title || it.url; + card.append(name); + + const sub = document.createElement('span'); + sub.className = 'navi-ws-card-url'; + sub.textContent = String(it.description || '').trim() + || it.url.replace(/^https?:\/\//, '').replace(/\/$/, ''); + card.append(sub); + + grid.append(card); + }); + pane.append(grid); + } + + function enableWorkspacePane() { + if (window.__naviWorkspacePane) return; + window.__naviWorkspacePane = true; + + document.addEventListener('click', async (evt) => { + if (!/^\/workspace/.test(location.pathname)) return; + const target = evt.target; + const head = target && target.closest ? target.closest('.side-bar-item-container') : null; + if (!head) return; + const sections = await fetchWorkspaceSections(); + if (!sections || !sections.length) return; + const heads = Array.from(document.querySelectorAll('nav.side-bar .side-bar-item-container')); + const idx = heads.indexOf(head); + if (idx >= 0 && sections[idx]) renderWorkspaceSection(sections[idx]); + }); + } + + // 工作空间初次打开时先把第一个分组的内容摆上,避免右侧一片空白。 + let wsSeeded = false; + async function seedWorkspacePane() { + if (wsSeeded) return; + if (!/^\/workspace/.test(location.pathname)) return; + if (!document.querySelector('.work-space')) return; + wsSeeded = true; + const sections = await fetchWorkspaceSections(); + if (sections && sections.length) renderWorkspaceSection(sections[0]); + } + // 极简视图(/minimal)与工作空间视图不渲染 Dashy 页脚,备案号会整段消失。 // 这里从只读的 pageinfo 取回页脚 HTML 补一份,保证每个视图都能看到备案信息。 let footerPending = false; @@ -166,6 +368,9 @@ function run() { mountSearch(); mountAdminEntry(); + mountViewSwitch(); + enableWorkspacePane(); + seedWorkspacePane(); hideInstallEntry(); mountFooter(); }