diff --git a/VERSION b/VERSION index 4e8f395f..30f6cf8d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.26.0 +0.26.1 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0b6b22ce..e66fdf5e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,27 +7,21 @@ This project follows the repository versioning rule: - `feature` -> `+0.1.0` - `bugfix` -> `+0.0.1` -## 0.25.3 +## 0.26.1 -Released: 2026-04-11 +Released: 2026-04-12 ### Highlights -- Refined the Earth HUD visual system into a calmer operator-facing style, turned the top-left Earth brand into a real reusable component with language-driven rendering, and cleaned up duplicated brand assets so the page now has a single source of truth for HUD branding. - -### Added - -- Added [frontend/public/earth/js/brand.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/brand.js) as a reusable Earth `brand` component that renders the top-left logo/title/subtitle block from a shared config instead of hardcoding the structure in HTML. -- Added [frontend/public/earth/assets/brand/earth-logo.svg](/home/ray/dev/linkong/planet/frontend/public/earth/assets/brand/earth-logo.svg), [frontend/public/earth/assets/brand/title-zh.svg](/home/ray/dev/linkong/planet/frontend/public/earth/assets/brand/title-zh.svg), and [frontend/public/earth/assets/brand/title-en.svg](/home/ray/dev/linkong/planet/frontend/public/earth/assets/brand/title-en.svg) as the canonical Earth brand asset set. +- Cleaned up the first TV follow-up and replaced the dashboard sidebar's brittle one-off scroll behavior with a reusable `Scrollbar` component, so the new live module code is easier to maintain and the console navigation can scroll without layout jitter. ### Improved -- Improved [frontend/public/earth/css/base.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/base.css), [frontend/public/earth/css/hud.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/hud.css), [frontend/public/earth/css/coordinates-display.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/coordinates-display.css), [frontend/public/earth/css/legend.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/legend.css), [frontend/public/earth/css/earth-stats.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/earth-stats.css), and [frontend/public/earth/css/toolbar.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/toolbar.css) by rebalancing the Earth HUD into a more restrained deep-blue control-room look instead of the earlier over-layered glass-and-neon mix. -- Improved [frontend/public/earth/index.html](/home/ray/dev/linkong/planet/frontend/public/earth/index.html), [frontend/public/earth/js/main.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/main.js), and [frontend/public/earth/js/constants.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/constants.js) by moving the Earth brand mount to a dedicated root and letting `HUD_CONFIG.brandLanguage` choose between `zh` and `en` without embedding the language decision in the DOM. +- Improved [frontend/src/components/Scrollbar/Scrollbar.tsx](/home/ray/dev/linkong/planet/frontend/src/components/Scrollbar/Scrollbar.tsx), [frontend/src/components/AppLayout/AppLayout.tsx](/home/ray/dev/linkong/planet/frontend/src/components/AppLayout/AppLayout.tsx), and [frontend/src/index.css](/home/ray/dev/linkong/planet/frontend/src/index.css) by extracting the sidebar scrollbar into a dedicated component with explicit `x / y / both` axis support, hidden native scrollbars, compact account/version rows, and a sidebar-only vertical setup instead of the earlier patchwork CSS glued directly onto the layout. ### Fixed -- Fixed [frontend/public/earth/css/info-panel.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/info-panel.css) so the Earth brand area now uses consistent `earth-brand` component selectors and English-specific typography hooks, avoiding the earlier one-off `brand-banner` naming drift and duplicated title styles. +- Fixed [frontend/public/earth/js/tv.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/tv.js) by refactoring repeated iframe/video reset paths into shared helpers, so TV source switching, empty-state fallback, and playback retry handling no longer duplicate cleanup logic across multiple branches. ## 0.26.0 @@ -53,6 +47,28 @@ Released: 2026-04-12 - Fixed [frontend/public/earth/js/tv.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/tv.js) and [frontend/public/earth/index.html](/home/ray/dev/linkong/planet/frontend/public/earth/index.html) so HLS/video playback now actively attempts autoplay in the TV panel instead of only loading metadata and leaving the player visually idle. - Fixed [frontend/public/earth/css/tv-panel.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/tv-panel.css) so the TV source selector and action controls better match the Earth HUD dark theme instead of falling back to a bright native dropdown presentation. +## 0.25.3 + +Released: 2026-04-11 + +### Highlights + +- Refined the Earth HUD visual system into a calmer operator-facing style, turned the top-left Earth brand into a real reusable component with language-driven rendering, and cleaned up duplicated brand assets so the page now has a single source of truth for HUD branding. + +### Added + +- Added [frontend/public/earth/js/brand.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/brand.js) as a reusable Earth `brand` component that renders the top-left logo/title/subtitle block from a shared config instead of hardcoding the structure in HTML. +- Added [frontend/public/earth/assets/brand/earth-logo.svg](/home/ray/dev/linkong/planet/frontend/public/earth/assets/brand/earth-logo.svg), [frontend/public/earth/assets/brand/title-zh.svg](/home/ray/dev/linkong/planet/frontend/public/earth/assets/brand/title-zh.svg), and [frontend/public/earth/assets/brand/title-en.svg](/home/ray/dev/linkong/planet/frontend/public/earth/assets/brand/title-en.svg) as the canonical Earth brand asset set. + +### Improved + +- Improved [frontend/public/earth/css/base.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/base.css), [frontend/public/earth/css/hud.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/hud.css), [frontend/public/earth/css/coordinates-display.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/coordinates-display.css), [frontend/public/earth/css/legend.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/legend.css), [frontend/public/earth/css/earth-stats.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/earth-stats.css), and [frontend/public/earth/css/toolbar.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/toolbar.css) by rebalancing the Earth HUD into a more restrained deep-blue control-room look instead of the earlier over-layered glass-and-neon mix. +- Improved [frontend/public/earth/index.html](/home/ray/dev/linkong/planet/frontend/public/earth/index.html), [frontend/public/earth/js/main.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/main.js), and [frontend/public/earth/js/constants.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/constants.js) by moving the Earth brand mount to a dedicated root and letting `HUD_CONFIG.brandLanguage` choose between `zh` and `en` without embedding the language decision in the DOM. + +### Fixed + +- Fixed [frontend/public/earth/css/info-panel.css](/home/ray/dev/linkong/planet/frontend/public/earth/css/info-panel.css) so the Earth brand area now uses consistent `earth-brand` component selectors and English-specific typography hooks, avoiding the earlier one-off `brand-banner` naming drift and duplicated title styles. + ## 0.25.2 Released: 2026-04-10 diff --git a/docs/version-history.md b/docs/version-history.md index 5bb3e5bb..633b4a58 100644 --- a/docs/version-history.md +++ b/docs/version-history.md @@ -16,7 +16,7 @@ ## Current Version - `main` 当前主线历史推导到:`0.16.5` -- `dev` 当前开发分支历史推导到:`0.26.0` +- `dev` 当前开发分支历史推导到:`0.26.1` ## Timeline @@ -86,6 +86,7 @@ | `0.25.2` | bugfix | `dev` | `pending` | refine Earth settings modal sizing, eliminate first-frame HUD scale flicker, and make dragged HUD panels animate cleanly through maximized layout transitions | | `0.25.3` | bugfix | `dev` | `pending` | refactor the Earth HUD visual system, extract the top-left Earth brand into a reusable language-driven component, and consolidate duplicated brand assets into a single canonical set | | `0.26.0` | feature | `dev` | `pending` | add the Earth TV live module with backend-configurable sources, a draggable/resizable TV HUD window, a first curated news channel catalog, and TV source management hooks in system settings | +| `0.26.1` | bugfix | `dev` | `pending` | extract the dashboard sidebar scrollbar into a reusable component and clean duplicated Earth TV player reset logic after the first live-module rollout | ## Maintenance Commits Not Counted as Version Bumps diff --git a/frontend/package.json b/frontend/package.json index bc8aab64..4c79e57d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "planet-frontend", - "version": "0.26.0", + "version": "0.26.1", "private": true, "packageManager": "bun@1", "dependencies": { diff --git a/frontend/public/earth/js/tv.js b/frontend/public/earth/js/tv.js index d0f1fcfb..1b52edcd 100644 --- a/frontend/public/earth/js/tv.js +++ b/frontend/public/earth/js/tv.js @@ -371,6 +371,10 @@ function findSourceById(sourceId) { return tvPayload?.sources?.find((source) => source.id === sourceId) || null; } +function getCurrentSource() { + return findSourceById(currentSourceId); +} + function setPanelMessage(message) { const { status } = getElements(); if (status) { @@ -378,6 +382,50 @@ function setPanelMessage(message) { } } +function resetIframe(iframe) { + if (!(iframe instanceof HTMLIFrameElement)) return; + iframe.removeAttribute("src"); + iframe.hidden = true; +} + +function resetVideo(video) { + if (!(video instanceof HTMLVideoElement)) return; + video.removeAttribute("src"); + video.hidden = true; + video.load(); +} + +function showEmptyState(empty, message) { + if (!(empty instanceof HTMLElement)) return; + empty.hidden = false; + empty.textContent = message; +} + +function hideEmptyState(empty) { + if (empty instanceof HTMLElement) { + empty.hidden = true; + } +} + +function renderVideoSource(video, iframe, source) { + resetIframe(iframe); + if (video instanceof HTMLVideoElement) { + video.hidden = false; + attachVideoSource(video, source); + } +} + +function renderEmbeddedSource(iframe, video, embeddedUrl) { + destroyHlsPlayer(); + resetVideo(video); + if (iframe instanceof HTMLIFrameElement) { + iframe.hidden = false; + if (iframe.src !== embeddedUrl) { + iframe.src = embeddedUrl; + } + } +} + function renderSourceOptions() { const { select } = getElements(); if (!select) return; @@ -429,53 +477,26 @@ function renderSource(source) { if (!source || (!embeddedUrl && !videoUrl)) { destroyHlsPlayer(); - if (iframe) { - iframe.removeAttribute("src"); - iframe.hidden = true; - } - if (video) { - video.removeAttribute("src"); - video.hidden = true; - video.load(); - } - if (empty) { - empty.hidden = false; - empty.textContent = isExternalOnly + resetIframe(iframe); + resetVideo(video); + showEmptyState( + empty, + isExternalOnly ? "当前频道仅支持跳转官网或外部播放器打开。" - : "暂无可播放直播源,请先在系统配置中添加频道。"; - } + : "暂无可播放直播源,请先在系统配置中添加频道。", + ); setPanelMessage(isExternalOnly ? TV_STATUS_MESSAGE.externalOnly : TV_STATUS_MESSAGE.empty); updateOpenButton(source); return; } if (isVideo && videoUrl) { - if (iframe) { - iframe.removeAttribute("src"); - iframe.hidden = true; - } - if (video) { - video.hidden = false; - attachVideoSource(video, source); - } + renderVideoSource(video, iframe, source); } else { - destroyHlsPlayer(); - if (video) { - video.removeAttribute("src"); - video.hidden = true; - video.load(); - } - if (iframe) { - iframe.hidden = false; - if (iframe.src !== embeddedUrl) { - iframe.src = embeddedUrl; - } - } + renderEmbeddedSource(iframe, video, embeddedUrl); } - if (empty) { - empty.hidden = true; - } + hideEmptyState(empty); setPanelMessage( source.id === tvPayload?.default_source_id ? "当前正在播放默认源" : "当前正在播放已选频道", @@ -586,7 +607,7 @@ export function initTVPanel() { }); video?.addEventListener("error", () => { - const currentSource = findSourceById(currentSourceId); + const currentSource = getCurrentSource(); if (!showEmbeddedFallback(currentSource)) { setPanelMessage(TV_STATUS_MESSAGE.videoError); } diff --git a/frontend/src/components/AppLayout/AppLayout.tsx b/frontend/src/components/AppLayout/AppLayout.tsx index 79724870..515372dd 100644 --- a/frontend/src/components/AppLayout/AppLayout.tsx +++ b/frontend/src/components/AppLayout/AppLayout.tsx @@ -20,6 +20,7 @@ import { useLocation, useNavigate } from 'react-router-dom' import type { ItemType, MenuItemType } from 'antd/es/menu/interface' import { useAuthStore } from '../../stores/auth' import packageJson from '../../../package.json' +import Scrollbar from '../Scrollbar/Scrollbar' const { Sider, Content } = Layout const { Text } = Typography @@ -117,7 +118,7 @@ function AppLayout({ children }: AppLayoutProps) { className="dashboard-sider" >
-
+
setCollapsed(false) : undefined}>
- { - updateOpenKeys(keys as string[]) - }} - onClick={({ key }) => { - if (key !== selectedKey) { - navigate(key) - } - }} - /> + + { + updateOpenKeys(keys as string[]) + }} + onClick={({ key }) => { + if (key !== selectedKey) { + navigate(key) + } + }} + /> +
{showBanner && !collapsed ? (
-
+
当前账号 {user?.username}
-
+
版本号 {appVersion}
diff --git a/frontend/src/components/Scrollbar/Scrollbar.tsx b/frontend/src/components/Scrollbar/Scrollbar.tsx new file mode 100644 index 00000000..a1f293fe --- /dev/null +++ b/frontend/src/components/Scrollbar/Scrollbar.tsx @@ -0,0 +1,301 @@ +import { + type PointerEvent as ReactPointerEvent, + type ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from 'react' + +type Axis = 'x' | 'y' +type ScrollbarAxis = 'x' | 'y' | 'both' + +interface AxisScrollbarState { + visible: boolean + dragging: boolean + thumbSize: number + thumbOffset: number +} + +interface ScrollbarState { + x: AxisScrollbarState + y: AxisScrollbarState +} + +interface ScrollbarProps { + children: ReactNode + className?: string + minThumbSize?: number + axis?: ScrollbarAxis + suppressScrollX?: boolean + suppressScrollY?: boolean +} + +interface DragState { + axis: Axis + pointerId: number + pointerOffset: number +} + +const INITIAL_AXIS_STATE: AxisScrollbarState = { + visible: false, + dragging: false, + thumbSize: 0, + thumbOffset: 0, +} + +const INITIAL_STATE: ScrollbarState = { + x: INITIAL_AXIS_STATE, + y: INITIAL_AXIS_STATE, +} + +function Scrollbar({ + children, + className = '', + minThumbSize = 28, + axis = 'both', + suppressScrollX = false, + suppressScrollY = false, +}: ScrollbarProps) { + const viewportRef = useRef(null) + const trackXRef = useRef(null) + const trackYRef = useRef(null) + const dragStateRef = useRef(null) + const [scrollbar, setScrollbar] = useState(INITIAL_STATE) + + const updateScrollbar = useCallback(() => { + const viewport = viewportRef.current + const trackX = trackXRef.current + const trackY = trackYRef.current + if (!viewport || !trackX || !trackY) return + + const { + clientWidth, + clientHeight, + scrollWidth, + scrollHeight, + scrollLeft, + scrollTop, + } = viewport + + const enableX = axis === 'x' || axis === 'both' + const enableY = axis === 'y' || axis === 'both' + const hasOverflowX = enableX && !suppressScrollX && scrollWidth - clientWidth > 1 + const hasOverflowY = enableY && !suppressScrollY && scrollHeight - clientHeight > 1 + + const nextX: AxisScrollbarState = hasOverflowX && clientWidth > 0 + ? (() => { + const trackSize = trackX.clientWidth + const thumbSize = Math.max(minThumbSize, (clientWidth / scrollWidth) * trackSize) + const maxThumbOffset = Math.max(0, trackSize - thumbSize) + const scrollRange = Math.max(1, scrollWidth - clientWidth) + return { + visible: true, + dragging: scrollbar.x.dragging && dragStateRef.current?.axis === 'x', + thumbSize, + thumbOffset: (scrollLeft / scrollRange) * maxThumbOffset, + } + })() + : { ...INITIAL_AXIS_STATE } + + const nextY: AxisScrollbarState = hasOverflowY && clientHeight > 0 + ? (() => { + const trackSize = trackY.clientHeight + const thumbSize = Math.max(minThumbSize, (clientHeight / scrollHeight) * trackSize) + const maxThumbOffset = Math.max(0, trackSize - thumbSize) + const scrollRange = Math.max(1, scrollHeight - clientHeight) + return { + visible: true, + dragging: scrollbar.y.dragging && dragStateRef.current?.axis === 'y', + thumbSize, + thumbOffset: (scrollTop / scrollRange) * maxThumbOffset, + } + })() + : { ...INITIAL_AXIS_STATE } + + setScrollbar({ x: nextX, y: nextY }) + }, [minThumbSize, scrollbar.x.dragging, scrollbar.y.dragging, suppressScrollX, suppressScrollY]) + + useEffect(() => { + const viewport = viewportRef.current + const trackX = trackXRef.current + const trackY = trackYRef.current + if (!viewport || !trackX || !trackY) return + + updateScrollbar() + + const resizeObserver = new ResizeObserver(() => { + updateScrollbar() + }) + + resizeObserver.observe(viewport) + resizeObserver.observe(trackX) + resizeObserver.observe(trackY) + Array.from(viewport.children).forEach((child) => resizeObserver.observe(child)) + viewport.addEventListener('scroll', updateScrollbar, { passive: true }) + window.addEventListener('resize', updateScrollbar) + + return () => { + resizeObserver.disconnect() + viewport.removeEventListener('scroll', updateScrollbar) + window.removeEventListener('resize', updateScrollbar) + } + }, [children, updateScrollbar]) + + useEffect(() => { + const stopDragging = () => { + dragStateRef.current = null + document.body.style.userSelect = '' + setScrollbar((previous) => ({ + x: { ...previous.x, dragging: false }, + y: { ...previous.y, dragging: false }, + })) + } + + window.addEventListener('pointerup', stopDragging) + window.addEventListener('pointercancel', stopDragging) + + return () => { + window.removeEventListener('pointerup', stopDragging) + window.removeEventListener('pointercancel', stopDragging) + } + }, []) + + const syncScrollFromThumbOffset = (axis: Axis, nextThumbOffset: number) => { + const viewport = viewportRef.current + const track = axis === 'x' ? trackXRef.current : trackYRef.current + const axisState = scrollbar[axis] + if (!viewport || !track) return + + const trackSize = axis === 'x' ? track.clientWidth : track.clientHeight + const maxThumbOffset = Math.max(0, trackSize - axisState.thumbSize) + if (maxThumbOffset <= 0) { + if (axis === 'x') viewport.scrollLeft = 0 + else viewport.scrollTop = 0 + return + } + + const progress = Math.max(0, Math.min(nextThumbOffset, maxThumbOffset)) / maxThumbOffset + if (axis === 'x') { + const scrollRange = Math.max(0, viewport.scrollWidth - viewport.clientWidth) + viewport.scrollLeft = progress * scrollRange + return + } + const scrollRange = Math.max(0, viewport.scrollHeight - viewport.clientHeight) + viewport.scrollTop = progress * scrollRange + } + + const handleThumbPointerDown = (axis: Axis) => (event: ReactPointerEvent) => { + const rect = event.currentTarget.getBoundingClientRect() + const axisState = scrollbar[axis] + if (!axisState.visible) return + + dragStateRef.current = { + axis, + pointerId: event.pointerId, + pointerOffset: axis === 'x' ? event.clientX - rect.left : event.clientY - rect.top, + } + + setScrollbar((previous) => ({ + x: { ...previous.x, dragging: axis === 'x' }, + y: { ...previous.y, dragging: axis === 'y' }, + })) + document.body.style.userSelect = 'none' + event.currentTarget.setPointerCapture(event.pointerId) + event.stopPropagation() + event.preventDefault() + } + + const handleThumbPointerMove = (axis: Axis) => (event: ReactPointerEvent) => { + const track = axis === 'x' ? trackXRef.current : trackYRef.current + const dragState = dragStateRef.current + if (!track || !dragState || dragState.pointerId !== event.pointerId || dragState.axis !== axis) return + + const rect = track.getBoundingClientRect() + const nextThumbOffset = axis === 'x' + ? event.clientX - rect.left - dragState.pointerOffset + : event.clientY - rect.top - dragState.pointerOffset + syncScrollFromThumbOffset(axis, nextThumbOffset) + } + + const handleThumbPointerUp = (axis: Axis) => (event: ReactPointerEvent) => { + if (dragStateRef.current?.pointerId !== event.pointerId || dragStateRef.current.axis !== axis) return + dragStateRef.current = null + document.body.style.userSelect = '' + setScrollbar((previous) => ({ + x: { ...previous.x, dragging: false }, + y: { ...previous.y, dragging: false }, + })) + event.currentTarget.releasePointerCapture?.(event.pointerId) + } + + const handleTrackPointerDown = (axis: Axis) => (event: ReactPointerEvent) => { + const axisState = scrollbar[axis] + if (!axisState.visible || event.target !== event.currentTarget) return + + const rect = event.currentTarget.getBoundingClientRect() + const nextThumbOffset = axis === 'x' + ? event.clientX - rect.left - axisState.thumbSize / 2 + : event.clientY - rect.top - axisState.thumbSize / 2 + syncScrollFromThumbOffset(axis, nextThumbOffset) + } + + const showCorner = scrollbar.x.visible && scrollbar.y.visible + const scrollbarClassName = [ + 'scrollbar', + `scrollbar--${axis}`, + className, + ].filter(Boolean).join(' ') + + return ( +
+
+ {children} +
+ + {(axis === 'y' || axis === 'both') ? ( +
+ {scrollbar.y.visible ? ( +
+ ) : null} +
+ ) : null} + + {(axis === 'x' || axis === 'both') ? ( +
+ {scrollbar.x.visible ? ( +
+ ) : null} +
+ ) : null} +
+ ) +} + +export default Scrollbar diff --git a/frontend/src/index.css b/frontend/src/index.css index 0e6a8f44..e72973c6 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -55,11 +55,132 @@ body { background: #001529 !important; } +.dashboard-sider.ant-layout-sider-has-trigger { + padding-bottom: 0 !important; +} + .dashboard-sider-inner { height: 100%; display: flex; flex-direction: column; - justify-content: space-between; + min-height: 0; +} + +.dashboard-sider-main { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.dashboard-sider-menu-scroll { + flex: 1 1 auto; + min-height: 0; +} + +.dashboard-sider-menu-scroll .scrollbar__viewport > .ant-menu { + width: 100%; + min-width: 0; +} + +.scrollbar { + position: relative; + flex: 1 1 auto; + min-height: 0; + overflow: hidden; +} + +.scrollbar--y .scrollbar__track--x, +.scrollbar--x .scrollbar__track--y { + display: none; +} + +.scrollbar__viewport { + height: 100%; + overflow: auto; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.scrollbar__viewport::-webkit-scrollbar { + width: 0; + height: 0; +} + +.scrollbar__track { + position: absolute; + z-index: 2; + pointer-events: none; + overflow: visible; +} + +.scrollbar__track--y { + top: 0; + right: 8px; + width: 0; + height: 100%; +} + +.scrollbar__track--x { + left: 0; + bottom: 8px; + width: 100%; + height: 0; +} + +.scrollbar__thumb { + position: absolute; + min-height: 28px; + min-width: 28px; + border-radius: 999px; + background: rgba(196, 208, 224, 0.46); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); + opacity: 0; + cursor: pointer; + pointer-events: auto; + transition: opacity 0.18s ease, background 0.18s ease, width 0.18s ease; +} + +.scrollbar__thumb::after { + content: ""; + position: absolute; +} + +.scrollbar__thumb--y { + top: 8px; + right: 0; + width: 6px; + min-width: 6px; +} + +.scrollbar__thumb--y::after { + inset: 0 -6px; +} + +.scrollbar__thumb--x { + left: 8px; + bottom: 0; + height: 6px; + min-height: 6px; +} + +.scrollbar__thumb--x::after { + inset: -6px 0; +} + +.scrollbar:hover .scrollbar__track--visible .scrollbar__thumb, +.scrollbar__track--visible .scrollbar__thumb, +.scrollbar__track--dragging .scrollbar__thumb { + opacity: 1; +} + +.scrollbar__thumb:hover { + background: rgba(216, 226, 240, 0.68); +} + +.scrollbar__track--dragging .scrollbar__thumb { + background: rgba(226, 235, 246, 0.82); } .dashboard-brand { @@ -106,24 +227,31 @@ body { } .dashboard-sider-banner { - margin: 12px; + flex: 0 0 auto; + margin: 12px 12px 0; padding: 14px 12px; border-radius: 14px; background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08); } +.dashboard-sider-banner-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + .dashboard-sider-banner-label { - display: block; - margin-bottom: 4px; color: rgba(255, 255, 255, 0.62); font-size: 12px; + white-space: nowrap; } .dashboard-sider-banner-value { - display: block; color: white !important; font-size: 14px; + text-align: right; } .dashboard-sider-logout { diff --git a/pyproject.toml b/pyproject.toml index 257b2443..8341bf27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "planet" -version = "0.26.0" +version = "0.26.1" description = "智能星球计划 - 态势感知系统" requires-python = ">=3.14" dependencies = [ diff --git a/uv.lock b/uv.lock index e2c3cd02..83aca449 100644 --- a/uv.lock +++ b/uv.lock @@ -475,7 +475,7 @@ wheels = [ [[package]] name = "planet" -version = "0.26.0" +version = "0.26.1" source = { virtual = "." } dependencies = [ { name = "aiofiles" },