feat: 添加站点卡片悬停提示功能

- 为所有站点卡片添加 data-tooltip 属性,包含完整的标题和描述信息
- tooltip 显示逻辑:
  * 鼠标悬停在整个卡片上即可触发(触发区域大,操作自然)
  * 跟随鼠标移动,实时更新位置
  * 智能边界检测,避免 tooltip 超出视口范围
  * 鼠标移出时自动隐藏
- 解决文本截断问题,用户可通过悬停查看完整内容

实现:
- 模板层:在 site-card.hbs 中为卡片添加 data-tooltip 属性
- 交互层:在 script.js 中实现 tooltip 的创建、显示、移动和隐藏逻辑
- 样式层:通过 CSS 类控制 tooltip 的可见性和位置

Issue: #31
This commit is contained in:
rbetree
2026-01-03 18:02:37 +08:00
parent 3473aaebd7
commit 2bebefbfe8
3 changed files with 592 additions and 428 deletions

View File

@@ -92,8 +92,10 @@ body.light-theme {
/* 预加载主题 - 在JS完全加载前显示正确的主题 */ /* 预加载主题 - 在JS完全加载前显示正确的主题 */
html.theme-preload body { html.theme-preload body {
background-color: #e0e0d8; /* 明亮主题背景色 */ background-color: #e0e0d8;
color: #333333; /* 明亮主题文本色 */ /* 明亮主题背景色 */
color: #333333;
/* 明亮主题文本色 */
} }
/* 预加载侧边栏状态 - 在JS完全加载前显示正确的侧边栏宽度 */ /* 预加载侧边栏状态 - 在JS完全加载前显示正确的侧边栏宽度 */
@@ -169,13 +171,16 @@ html.preload * {
/* 通用滚动条样式 */ /* 通用滚动条样式 */
.custom-scrollbar { .custom-scrollbar {
scrollbar-width: thin; /* Firefox */ scrollbar-width: thin;
scrollbar-color: var(--scrollbar-color) transparent; /* Firefox */ /* Firefox */
scrollbar-color: var(--scrollbar-color) transparent;
/* Firefox */
} }
/* Webkit滚动条样式Chrome, Safari, Edge等 */ /* Webkit滚动条样式Chrome, Safari, Edge等 */
.custom-scrollbar::-webkit-scrollbar { .custom-scrollbar::-webkit-scrollbar {
width: 7px; /* 统一滚动条宽度 */ width: 7px;
/* 统一滚动条宽度 */
} }
.custom-scrollbar::-webkit-scrollbar-track { .custom-scrollbar::-webkit-scrollbar-track {
@@ -183,18 +188,22 @@ html.preload * {
} }
.custom-scrollbar::-webkit-scrollbar-thumb { .custom-scrollbar::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-color); /* 使用变量 */ background-color: var(--scrollbar-color);
/* 使用变量 */
border-radius: 4px; border-radius: 4px;
} }
.custom-scrollbar::-webkit-scrollbar-thumb:hover { .custom-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: var(--scrollbar-hover-color); /* 使用变量 */ background-color: var(--scrollbar-hover-color);
/* 使用变量 */
} }
/* 防止滚动条导致的布局偏移 */ /* 防止滚动条导致的布局偏移 */
html { html {
overflow-y: hidden; /* 改为hidden移除强制显示的滚动条 */ overflow-y: hidden;
scrollbar-width: thin; /* Firefox */ /* 改为hidden移除强制显示的滚动条 */
scrollbar-width: thin;
/* Firefox */
/* 明确 rem 基准字号:便于用 rem 统一管理字号1rem = 16px */ /* 明确 rem 基准字号:便于用 rem 统一管理字号1rem = 16px */
font-size: 16px; font-size: 16px;
} }
@@ -216,8 +225,10 @@ body {
background-color: var(--bg-color); background-color: var(--bg-color);
color: var(--text-color); color: var(--text-color);
min-height: var(--app-height, 100vh); min-height: var(--app-height, 100vh);
overflow: hidden; /* 防止body滚动 */ overflow: hidden;
padding-right: 0 !important; /* 防止滚动条导致的布局偏移 */ /* 防止body滚动 */
padding-right: 0 !important;
/* 防止滚动条导致的布局偏移 */
transition: background-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease;
} }
@@ -227,7 +238,8 @@ body {
min-height: var(--app-height, 100vh); min-height: var(--app-height, 100vh);
position: relative; position: relative;
z-index: 1; z-index: 1;
overflow: hidden; /* 防止layout滚动 */ overflow: hidden;
/* 防止layout滚动 */
opacity: 0; opacity: 0;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
} }
@@ -291,7 +303,8 @@ body.loaded .layout {
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 950; /* 调整遮罩层z-index处于按钮与弹出面板之间 */ z-index: 950;
/* 调整遮罩层z-index处于按钮与弹出面板之间 */
} }
.overlay.active { .overlay.active {
@@ -338,14 +351,17 @@ body.loaded .layout {
justify-content: center; justify-content: center;
display: flex; display: flex;
align-items: center; align-items: center;
height: 3.75rem; /* 确保与展开状态高度一致 */ height: 3.75rem;
margin-bottom: 0.8rem; /* 收起态同样拉开与按钮的间距 */ /* 确保与展开状态高度一致 */
margin-bottom: 0.8rem;
/* 收起态同样拉开与按钮的间距 */
} }
/* 折叠状态下的侧边栏内容区域调整 */ /* 折叠状态下的侧边栏内容区域调整 */
.sidebar.collapsed .sidebar-content { .sidebar.collapsed .sidebar-content {
padding: 0; padding: 0;
scrollbar-width: none; /* 隐藏滚动条 */ scrollbar-width: none;
/* 隐藏滚动条 */
} }
/* 调整折叠侧边栏的部分元素间距 */ /* 调整折叠侧边栏的部分元素间距 */
@@ -364,20 +380,27 @@ body.loaded .layout {
} }
.sidebar.collapsed .sidebar-content::-webkit-scrollbar { .sidebar.collapsed .sidebar-content::-webkit-scrollbar {
display: none; /* 隐藏WebKit浏览器的滚动条 */ display: none;
/* 隐藏WebKit浏览器的滚动条 */
} }
/* 侧边栏头部区域 */ /* 侧边栏头部区域 */
.sidebar .logo { .sidebar .logo {
grid-area: header; grid-area: header;
padding: 1.2rem 1.2rem 0.6rem; /* 调整上下padding更紧凑 */ padding: 1.2rem 1.2rem 0.6rem;
/* 调整上下padding更紧凑 */
display: flex; display: flex;
align-items: center; align-items: center;
overflow: hidden; /* 防止内容溢出 */ overflow: hidden;
position: relative; /* 添加相对定位,作为按钮的参考 */ /* 防止内容溢出 */
height: 3.75rem; /* 固定高度 60px */ position: relative;
margin-bottom: 0.8rem; /* 与下方按钮区域拉开间距 */ /* 添加相对定位,作为按钮的参考 */
transition: padding 0.3s ease; /* 添加padding过渡避免突变 */ height: 3.75rem;
/* 固定高度 60px */
margin-bottom: 0.8rem;
/* 与下方按钮区域拉开间距 */
transition: padding 0.3s ease;
/* 添加padding过渡避免突变 */
} }
.logo-brand { .logo-brand {
@@ -386,7 +409,8 @@ body.loaded .layout {
gap: 0.6rem; gap: 0.6rem;
min-width: 0; min-width: 0;
flex: 1; flex: 1;
padding-right: 2.2rem; /* 预留右侧折叠按钮空间 */ padding-right: 2.2rem;
/* 预留右侧折叠按钮空间 */
} }
.logo-brand h1 { .logo-brand h1 {
@@ -428,13 +452,18 @@ body.loaded .layout {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
transition: background 0.3s ease; /* 只过渡背景色移除all避免位置过渡 */ transition: background 0.3s ease;
/* 只过渡背景色移除all避免位置过渡 */
padding: 0; padding: 0;
flex-shrink: 0; /* 防止按钮被压缩 */ flex-shrink: 0;
position: absolute; /* 在两种状态下都使用绝对定位 */ /* 防止按钮被压缩 */
right: 1.2rem; /* 展开状态下固定在右侧 */ position: absolute;
/* 在两种状态下都使用绝对定位 */
right: 1.2rem;
/* 展开状态下固定在右侧 */
top: 60%; top: 60%;
transform: translateY(-50%); /* 垂直居中 */ transform: translateY(-50%);
/* 垂直居中 */
} }
.sidebar-toggle .toggle-icon { .sidebar-toggle .toggle-icon {
@@ -457,9 +486,12 @@ body.loaded .layout {
/* 收起状态下按钮居中 */ /* 收起状态下按钮居中 */
.sidebar.collapsed .sidebar-toggle { .sidebar.collapsed .sidebar-toggle {
left: 50%; /* 水平居中 */ left: 50%;
right: auto; /* 移除右侧定位 */ /* 水平居中 */
transform: translate(-50%, -50%); /* 同时水平和垂直居中 */ right: auto;
/* 移除右侧定位 */
transform: translate(-50%, -50%);
/* 同时水平和垂直居中 */
} }
.sidebar.collapsed .sidebar-toggle:hover { .sidebar.collapsed .sidebar-toggle:hover {
@@ -478,28 +510,35 @@ body.loaded .layout {
/* 侧边栏内容区域 - 可滚动 */ /* 侧边栏内容区域 - 可滚动 */
.sidebar-content { .sidebar-content {
grid-area: content; grid-area: content;
min-height: 0; /* 允许在 CSS Grid 内正确收缩与滚动,避免把 footer 挤出可视区域 */ min-height: 0;
overflow-y: auto; /* 只有内容区域可滚动 */ /* 允许在 CSS Grid 内正确收缩与滚动,避免把 footer 挤出可视区域 */
overflow-y: auto;
/* 只有内容区域可滚动 */
padding: 0 1.2rem; padding: 0 1.2rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.6rem; /* 从1rem减小到0.6rem */ gap: 0.6rem;
/* 从1rem减小到0.6rem */
/* 隐藏滚动条但保持滚动功能 */ /* 隐藏滚动条但保持滚动功能 */
scrollbar-width: none; /* Firefox */ scrollbar-width: none;
/* Firefox */
} }
.sidebar-content::-webkit-scrollbar { .sidebar-content::-webkit-scrollbar {
display: none; /* Webkit browsers */ display: none;
/* Webkit browsers */
} }
/* 折叠状态下的内容区域调整 */ /* 折叠状态下的内容区域调整 */
.sidebar.collapsed .sidebar-content { .sidebar.collapsed .sidebar-content {
padding: 0 0.5rem; padding: 0 0.5rem;
scrollbar-width: none; /* 隐藏滚动条 */ scrollbar-width: none;
/* 隐藏滚动条 */
} }
.sidebar.collapsed .sidebar-content::-webkit-scrollbar { .sidebar.collapsed .sidebar-content::-webkit-scrollbar {
display: none; /* 隐藏WebKit浏览器的滚动条 */ display: none;
/* 隐藏WebKit浏览器的滚动条 */
} }
/* 折叠状态下的Logo文本 */ /* 折叠状态下的Logo文本 */
@@ -507,22 +546,27 @@ body.loaded .layout {
opacity: 0; opacity: 0;
transform: translateX(-20px); transform: translateX(-20px);
width: 0; width: 0;
visibility: hidden; /* 确保完全隐藏,防止干扰布局 */ visibility: hidden;
pointer-events: none; /* 禁用交互,避免影响布局 */ /* 确保完全隐藏,防止干扰布局 */
pointer-events: none;
/* 禁用交互,避免影响布局 */
} }
/* 导航区域样式 */ /* 导航区域样式 */
.nav-section { .nav-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.4rem; /* 增大按钮间距 */ gap: 0.4rem;
/* 增大按钮间距 */
} }
.section-title { .section-title {
font-size: 1rem; font-size: 1rem;
color: var(--accent-color); color: var(--accent-color);
padding: 0.4rem 0.5rem; /* 减小上下padding */ padding: 0.4rem 0.5rem;
margin-bottom: 0.2rem; /* 增大与下方按钮组的间距 */ /* 减小上下padding */
margin-bottom: 0.2rem;
/* 增大与下方按钮组的间距 */
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
@@ -539,7 +583,8 @@ body.loaded .layout {
/* 统一与展开态的垂直间距 */ /* 统一与展开态的垂直间距 */
padding: 0.4rem 0; padding: 0.4rem 0;
text-align: center; text-align: center;
margin-bottom: 0.2rem; /* 与展开态保持一致且更大 */ margin-bottom: 0.2rem;
/* 与展开态保持一致且更大 */
} }
.sidebar.collapsed .section-title i { .sidebar.collapsed .section-title i {
@@ -556,12 +601,15 @@ body.loaded .layout {
.sidebar.collapsed .nav-item { .sidebar.collapsed .nav-item {
padding: 0; padding: 0;
justify-content: center; justify-content: center;
width: 2.75rem; /* 增大按钮方块尺寸 44px */ width: 2.75rem;
height: 2.75rem; /* 增大按钮方块尺寸 44px */ /* 增大按钮方块尺寸 44px */
height: 2.75rem;
/* 增大按钮方块尺寸 44px */
text-align: center; text-align: center;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
border-radius: var(--radius-md); /* 略增圆角 */ border-radius: var(--radius-md);
/* 略增圆角 */
display: flex; display: flex;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
@@ -589,7 +637,8 @@ body.loaded .layout {
opacity: 0; opacity: 0;
transform: translateX(-10px); transform: translateX(-10px);
width: 0; width: 0;
display: none; /* 完全移除,防止干扰布局 */ display: none;
/* 完全移除,防止干扰布局 */
visibility: hidden; visibility: hidden;
} }
@@ -662,19 +711,23 @@ body.loaded .layout {
padding: 2rem 1.5rem; padding: 2rem 1.5rem;
background-color: var(--bg-color); background-color: var(--bg-color);
position: relative; position: relative;
height: var(--app-height, 100vh); /* 固定高度(移动端避免 100vh 问题) */ height: var(--app-height, 100vh);
overflow-y: auto; /* 使用auto替代scroll只在需要时显示滚动条 */ /* 固定高度(移动端避免 100vh 问题) */
overflow-y: auto;
/* 使用auto替代scroll只在需要时显示滚动条 */
overflow-x: hidden; overflow-x: hidden;
width: calc(100vw - var(--sidebar-width)); width: calc(100vw - var(--sidebar-width));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
/* 隐藏滚动条但保持滚动功能 */ /* 隐藏滚动条但保持滚动功能 */
scrollbar-width: none; /* Firefox */ scrollbar-width: none;
/* Firefox */
} }
.content::-webkit-scrollbar { .content::-webkit-scrollbar {
display: none; /* Webkit browsers */ display: none;
/* Webkit browsers */
} }
/* 优化内容区域在侧边栏折叠状态下的边距 */ /* 优化内容区域在侧边栏折叠状态下的边距 */
@@ -1097,12 +1150,11 @@ body .content.expanded {
@keyframes glow { @keyframes glow {
from { from {
filter: drop-shadow(0 0 2px rgba(118, 148, 185, 0.2)) filter: drop-shadow(0 0 2px rgba(118, 148, 185, 0.2)) drop-shadow(0 0 4px rgba(168, 85, 247, 0.2));
drop-shadow(0 0 4px rgba(168, 85, 247, 0.2));
} }
to { to {
filter: drop-shadow(0 0 4px rgba(118, 148, 185, 0.4)) filter: drop-shadow(0 0 4px rgba(118, 148, 185, 0.4)) drop-shadow(0 0 8px rgba(168, 85, 247, 0.4));
drop-shadow(0 0 8px rgba(168, 85, 247, 0.4));
} }
} }
@@ -1240,7 +1292,8 @@ body .content.expanded {
} }
/* 层级3: 分组 */ /* 层级3: 分组 */
.group-level-3, .category-level-3 { .group-level-3,
.category-level-3 {
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
padding-left: 0.5rem; padding-left: 0.5rem;
@@ -1271,7 +1324,8 @@ body .content.expanded {
} }
/* 层级4: 子分组 */ /* 层级4: 子分组 */
.group-level-4, .category-level-4 { .group-level-4,
.category-level-4 {
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
padding-left: 0.5rem; padding-left: 0.5rem;
@@ -1358,6 +1412,7 @@ body .content.expanded {
/* 分类/分组折叠图标:桌面端默认隐藏,悬停/收起时显示,避免按钮过多 */ /* 分类/分组折叠图标:桌面端默认隐藏,悬停/收起时显示,避免按钮过多 */
@media (hover: hover) and (pointer: fine) { @media (hover: hover) and (pointer: fine) {
.category-header .toggle-icon, .category-header .toggle-icon,
.group-header .toggle-icon { .group-header .toggle-icon {
opacity: 0; opacity: 0;
@@ -1373,7 +1428,8 @@ body .content.expanded {
} }
/* 展开/折叠动画 */ /* 展开/折叠动画 */
.category-content, .group-content { .category-content,
.group-content {
overflow: visible; overflow: visible;
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1), transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@@ -1464,11 +1520,13 @@ body .content.expanded {
padding-left: 0; padding-left: 0;
} }
.group-level-3, .category-level-3 { .group-level-3,
.category-level-3 {
padding-left: 0; padding-left: 0;
} }
.group-level-4, .category-level-4 { .group-level-4,
.category-level-4 {
padding-left: 0; padding-left: 0;
} }
@@ -1503,13 +1561,16 @@ body .content.expanded {
padding: 1rem; padding: 1rem;
} }
.category-level-2, .group-level-3, .category-level-3 { .category-level-2,
.group-level-3,
.category-level-3 {
margin-left: 0; margin-left: 0;
padding-left: 0; padding-left: 0;
width: 100%; width: 100%;
} }
.group-level-4, .category-level-4 { .group-level-4,
.category-level-4 {
margin-left: 0; margin-left: 0;
padding-left: 0; padding-left: 0;
width: 100%; width: 100%;
@@ -1611,6 +1672,8 @@ body .content.expanded {
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
position: relative;
/* Ensure tooltip positioning context */
} }
.site-card.site-card-repo .repo-stats { .site-card.site-card-repo .repo-stats {
@@ -1928,6 +1991,43 @@ body .content.expanded {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 100%; width: 100%;
position: relative;
/* Ensure tooltip positioning context */
}
/* Tooltip styles */
/* Tooltip styles */
.site-card p[data-tooltip],
.site-card .repo-desc[data-tooltip] {
cursor: default;
/* Indicate interactivity */
}
.custom-tooltip {
position: fixed;
background: rgba(47, 48, 53, 0.95);
/* Fallback dark */
background: rgba(var(--card-bg-rgb), 0.95);
color: var(--text-bright);
padding: 0.5rem 0.8rem;
border-radius: var(--radius-md);
box-shadow: 0 4px 12px var(--shadow-color);
border: 1px solid var(--border-color);
font-size: 0.85rem;
white-space: normal;
line-height: 1.4;
z-index: 9999;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease-out;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
max-width: 300px;
word-break: break-word;
}
.custom-tooltip.visible {
opacity: 1;
} }
/* 添加编辑按钮 */ /* 添加编辑按钮 */
@@ -1945,7 +2045,8 @@ body .content.expanded {
opacity: 1; opacity: 1;
} }
.edit-btn, .delete-btn { .edit-btn,
.delete-btn {
background: none; background: none;
border: none; border: none;
color: var(--text-muted); color: var(--text-muted);
@@ -1955,7 +2056,8 @@ body .content.expanded {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.edit-btn:hover, .delete-btn:hover { .edit-btn:hover,
.delete-btn:hover {
color: var(--text-bright); color: var(--text-bright);
background-color: var(--secondary-bg); background-color: var(--secondary-bg);
} }
@@ -2080,7 +2182,8 @@ body .content.expanded {
transition: color 0.3s ease; transition: color 0.3s ease;
} }
.form-group input, .form-group select { .form-group input,
.form-group select {
background-color: var(--secondary-bg); background-color: var(--secondary-bg);
border: 1px solid transparent; border: 1px solid transparent;
border-radius: var(--radius-md); border-radius: var(--radius-md);
@@ -2091,7 +2194,8 @@ body .content.expanded {
box-shadow: 0 2px 6px var(--shadow-color); box-shadow: 0 2px 6px var(--shadow-color);
} }
.form-group input:focus, .form-group select:focus { .form-group input:focus,
.form-group select:focus {
outline: none; outline: none;
background-color: var(--secondary-bg); background-color: var(--secondary-bg);
border-color: var(--accent-color); border-color: var(--accent-color);
@@ -2191,7 +2295,8 @@ body .content.expanded {
.sidebar.active { .sidebar.active {
transform: translateX(0); transform: translateX(0);
box-shadow: 2px 0 10px var(--shadow-color); box-shadow: 2px 0 10px var(--shadow-color);
z-index: 1000; /* 增加侧边栏激活时的z-index确保显示在按钮之上 */ z-index: 1000;
/* 增加侧边栏激活时的z-index确保显示在按钮之上 */
} }
/* 重置移动端下的侧边栏展开状态 */ /* 重置移动端下的侧边栏展开状态 */
@@ -2243,7 +2348,8 @@ body .content.expanded {
.search-container.active { .search-container.active {
transform: translateY(0); transform: translateY(0);
z-index: 1000; /* 增加搜索容器激活时的z-index确保显示在按钮之上 */ z-index: 1000;
/* 增加搜索容器激活时的z-index确保显示在按钮之上 */
} }
.search-box { .search-box {
@@ -2284,7 +2390,8 @@ body .content.expanded {
/* 欢迎区域样式 */ /* 欢迎区域样式 */
.welcome-section { .welcome-section {
padding: 0 1rem; padding: 0 1rem;
margin-top: 1rem; /* 增加顶部间距 */ margin-top: 1rem;
/* 增加顶部间距 */
} }
.page { .page {
@@ -2383,7 +2490,8 @@ body .content.expanded {
/* 移动端滚动进度条调整 */ /* 移动端滚动进度条调整 */
.scroll-progress { .scroll-progress {
height: var(--radius-sm); /* 移动端略粗一些 */ height: var(--radius-sm);
/* 移动端略粗一些 */
} }
.sidebar .submenu { .sidebar .submenu {
@@ -2538,6 +2646,7 @@ body .content.expanded {
opacity: 0; opacity: 0;
transform: translateY(10px); transform: translateY(10px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
@@ -2552,8 +2661,10 @@ body .content.expanded {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
z-index: 15; z-index: 15;
transform: none !important; /* 确保没有变换 */ transform: none !important;
min-height: 400px; /* 确保最小高度,防止内容过少时的布局跳动 */ /* 确保没有变换 */
min-height: 400px;
/* 确保最小高度,防止内容过少时的布局跳动 */
} }
#search-results.active { #search-results.active {
@@ -2620,6 +2731,7 @@ body .content.expanded {
from { from {
opacity: 0; opacity: 0;
} }
to { to {
opacity: 1; opacity: 1;
} }
@@ -2630,6 +2742,7 @@ body .content.expanded {
opacity: 0; opacity: 0;
transform: scale(0.9); transform: scale(0.9);
} }
to { to {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
@@ -2689,7 +2802,8 @@ body .content.expanded {
color: var(--text-muted); color: var(--text-muted);
font-size: 0.85rem; font-size: 0.85rem;
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
background-color: var(--sidebar-bg); /* 使用变量 */ background-color: var(--sidebar-bg);
/* 使用变量 */
transition: background-color 0.3s ease, color 0.3s ease, opacity 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease, opacity 0.3s ease;
} }
@@ -2747,11 +2861,14 @@ body .content.expanded {
/* 子菜单展开状态 */ /* 子菜单展开状态 */
.nav-item-wrapper.expanded .submenu { .nav-item-wrapper.expanded .submenu {
max-height: 300px; /* 设置合理的最大高度 */ max-height: 300px;
overflow-y: scroll; /* 改为scroll确保始终能滚动 */ /* 设置合理的最大高度 */
overflow-y: scroll;
/* 改为scroll确保始终能滚动 */
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
scrollbar-width: none; /* Firefox隐藏滚动条 */ scrollbar-width: none;
/* Firefox隐藏滚动条 */
} }
/* 为WebKit浏览器隐藏滚动条 */ /* 为WebKit浏览器隐藏滚动条 */
@@ -2800,7 +2917,8 @@ body .content.expanded {
使用display: none确保完全隐藏避免任何可能的视觉问题 */ 使用display: none确保完全隐藏避免任何可能的视觉问题 */
.sidebar.collapsed .submenu { .sidebar.collapsed .submenu {
position: absolute; position: absolute;
left: var(--sidebar-collapsed-width); /* 使用变量确保与侧边栏宽度一致 */ left: var(--sidebar-collapsed-width);
/* 使用变量确保与侧边栏宽度一致 */
top: 0; top: 0;
background-color: var(--sidebar-bg); background-color: var(--sidebar-bg);
border-radius: 0 8px 8px 0; border-radius: 0 8px 8px 0;
@@ -2814,7 +2932,8 @@ body .content.expanded {
z-index: 200; z-index: 200;
pointer-events: none; pointer-events: none;
transition: all 0.3s ease; transition: all 0.3s ease;
display: none; /* 添加 display: none 确保完全隐藏 */ display: none;
/* 添加 display: none 确保完全隐藏 */
} }
/* 确保子菜单项在悬停时不会漏出 /* 确保子菜单项在悬停时不会漏出
@@ -2831,7 +2950,8 @@ body .content.expanded {
使用static定位是为了让子菜单相对于侧边栏定位而不是相对于nav-item-wrapper 使用static定位是为了让子菜单相对于侧边栏定位而不是相对于nav-item-wrapper
这样可以避免子菜单在折叠状态下漏出的问题 */ 这样可以避免子菜单在折叠状态下漏出的问题 */
.sidebar.collapsed .nav-item-wrapper { .sidebar.collapsed .nav-item-wrapper {
position: static; /* 改为static防止子菜单定位问题 */ position: static;
/* 改为static防止子菜单定位问题 */
} }
/* 修改子菜单在悬停时的显示位置 /* 修改子菜单在悬停时的显示位置
@@ -2845,9 +2965,12 @@ body .content.expanded {
scrollbar-width: none; scrollbar-width: none;
pointer-events: auto; pointer-events: auto;
display: block; display: block;
left: var(--sidebar-collapsed-width); /* 确保与侧边栏宽度一致 */ left: var(--sidebar-collapsed-width);
top: 0; /* 确保从顶部开始 */ /* 确保与侧边栏宽度一致 */
position: absolute; /* 使用绝对定位,更符合文档流 */ top: 0;
/* 确保从顶部开始 */
position: absolute;
/* 使用绝对定位,更符合文档流 */
} }
/* 为WebKit浏览器隐藏滚动条 */ /* 为WebKit浏览器隐藏滚动条 */

View File

@@ -274,6 +274,7 @@ window.MeNav = {
newSite.href = siteUrl; newSite.href = siteUrl;
newSite.title = siteName + (siteDescription ? ' - ' + siteDescription : ''); newSite.title = siteName + (siteDescription ? ' - ' + siteDescription : '');
newSite.setAttribute('data-tooltip', siteName + (siteDescription ? ' - ' + siteDescription : '')); // 添加自定义 tooltip
if (/^https?:\/\//i.test(siteUrl)) { if (/^https?:\/\//i.test(siteUrl)) {
newSite.target = '_blank'; newSite.target = '_blank';
newSite.rel = 'noopener'; newSite.rel = 'noopener';
@@ -1909,3 +1910,73 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
}); });
// Tooltip functionality for truncated text
document.addEventListener('DOMContentLoaded', () => {
// Create tooltip element
const tooltip = document.createElement('div');
tooltip.className = 'custom-tooltip';
document.body.appendChild(tooltip);
let activeElement = null;
// Show tooltip on hover
document.addEventListener('mouseover', (e) => {
const target = e.target.closest('[data-tooltip]');
if (target) {
const tooltipText = target.getAttribute('data-tooltip');
if (tooltipText) {
activeElement = target;
tooltip.textContent = tooltipText;
tooltip.classList.add('visible');
updateTooltipPosition(e);
}
}
});
// Move tooltip with cursor
document.addEventListener('mousemove', (e) => {
if (activeElement) {
updateTooltipPosition(e);
}
});
// Hide tooltip on mouse out
document.addEventListener('mouseout', (e) => {
const target = e.target.closest('[data-tooltip]');
if (target && target === activeElement) {
// Check if we really left the element (not just went to a child)
if (!target.contains(e.relatedTarget)) {
activeElement = null;
tooltip.classList.remove('visible');
}
}
});
function updateTooltipPosition(e) {
// Position tooltip 15px below/right of cursor
const x = e.clientX + 15;
const y = e.clientY + 15;
// Boundary checks to keep inside viewport
const rect = tooltip.getBoundingClientRect();
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
let finalX = x;
let finalY = y;
// If tooltip goes off right edge
if (x + rect.width > winWidth) {
finalX = e.clientX - rect.width - 10;
}
// If tooltip goes off bottom edge
if (y + rect.height > winHeight) {
finalY = e.clientY - rect.height - 10;
}
tooltip.style.left = finalX + 'px';
tooltip.style.top = finalY + 'px';
}
});

View File

@@ -1,15 +1,11 @@
{{#if url}} {{#if url}}
<a href="{{url}}" class="site-card{{#if style}} site-card-{{style}}{{/if}}" <a href="{{url}}" class="site-card{{#if style}} site-card-{{style}}{{/if}}" {{#if external}}target="_blank"
{{#if external}}target="_blank" rel="noopener"{{/if}} rel="noopener" {{/if}} data-type="{{#if type}}{{type}}{{else}}site{{/if}}" data-name="{{name}}" data-url="{{url}}"
data-type="{{#if type}}{{type}}{{else}}site{{/if}}" data-icon="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}" {{#if faviconUrl}}data-favicon-url="{{faviconUrl}}"
data-name="{{name}}" {{/if}} {{#if forceIconMode}}data-force-icon-mode="{{forceIconMode}}" {{/if}}
data-url="{{url}}"
data-icon="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"
{{#if faviconUrl}}data-favicon-url="{{faviconUrl}}"{{/if}}
{{#if forceIconMode}}data-force-icon-mode="{{forceIconMode}}"{{/if}}
data-description="{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}" data-description="{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}"
{{#if publishedAt}}data-published-at="{{publishedAt}}"{{/if}} data-tooltip="{{#if name}}{{name}}{{else}}未命名站点{{/if}}{{#if description}} - {{description}}{{else}} - {{extractDomain url}}{{/if}}"
{{#if source}}data-source="{{source}}"{{/if}}> {{#if publishedAt}}data-published-at="{{publishedAt}}" {{/if}} {{#if source}}data-source="{{source}}" {{/if}}>
{{!-- articles首行图标+标题;下方“时间/来源 + 简介”全宽对齐,不被图标列缩进 --}} {{!-- articles首行图标+标题;下方“时间/来源 + 简介”全宽对齐,不被图标列缩进 --}}
{{#ifEquals type "article"}} {{#ifEquals type "article"}}
<div class="article-card-header"> <div class="article-card-header">
@@ -18,14 +14,9 @@
{{#if faviconUrl}} {{#if faviconUrl}}
<div class="icon-container"> <div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i> <i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img <img class="favicon-icon" src="{{faviconUrl}}" alt="{{name}} favicon" loading="lazy"
class="favicon-icon"
src="{{faviconUrl}}"
alt="{{name}} favicon"
loading="lazy"
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');" onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
onerror="this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" onerror="this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" />
/>
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i> <i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
</div> </div>
{{else}} {{else}}
@@ -36,14 +27,9 @@
{{#ifHttpUrl url}} {{#ifHttpUrl url}}
<div class="icon-container"> <div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i> <i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img <img class="favicon-icon" src="{{faviconUrl url}}" alt="{{name}} favicon" loading="lazy"
class="favicon-icon"
src="{{faviconUrl url}}"
alt="{{name}} favicon"
loading="lazy"
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');" onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" />
/>
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i> <i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
</div> </div>
{{else}} {{else}}
@@ -54,14 +40,9 @@
{{#ifHttpUrl url}} {{#ifHttpUrl url}}
<div class="icon-container"> <div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i> <i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img <img class="favicon-icon" src="{{faviconUrl url}}" alt="{{name}} favicon" loading="lazy"
class="favicon-icon"
src="{{faviconUrl url}}"
alt="{{name}} favicon"
loading="lazy"
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');" onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" />
/>
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i> <i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
</div> </div>
{{else}} {{else}}
@@ -93,7 +74,8 @@
{{/if}} {{/if}}
</div> </div>
{{/ifCond}} {{/ifCond}}
<p>{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}</p> <p>{{#if
description}}{{description}}{{else}}{{extractDomain url}}{{/if}}</p>
</div> </div>
{{else}} {{else}}
{{!-- projects代码仓库风格卡片保留 data-* 结构,便于扩展识别与写回) --}} {{!-- projects代码仓库风格卡片保留 data-* 结构,便于扩展识别与写回) --}}
@@ -103,13 +85,15 @@
<div class="repo-title">{{#if name}}{{name}}{{else}}未命名项目{{/if}}</div> <div class="repo-title">{{#if name}}{{name}}{{else}}未命名项目{{/if}}</div>
</div> </div>
<div class="repo-desc">{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}</div> <div class="repo-desc">{{#if
description}}{{description}}{{else}}{{extractDomain url}}{{/if}}</div>
{{#ifCond language '||' stars}} {{#ifCond language '||' stars}}
<div class="repo-stats"> <div class="repo-stats">
{{#if language}} {{#if language}}
<div class="stat-item"> <div class="stat-item">
<span class="lang-dot" style="background-color: {{#if languageColor}}{{languageColor}}{{else}}#909296{{/if}};"></span> <span class="lang-dot"
style="background-color: {{#if languageColor}}{{languageColor}}{{else}}#909296{{/if}};"></span>
{{language}} {{language}}
</div> </div>
{{/if}} {{/if}}
@@ -136,14 +120,9 @@
{{#if faviconUrl}} {{#if faviconUrl}}
<div class="icon-container"> <div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i> <i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img <img class="favicon-icon" src="{{faviconUrl}}" alt="{{name}} favicon" loading="lazy"
class="favicon-icon"
src="{{faviconUrl}}"
alt="{{name}} favicon"
loading="lazy"
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');" onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
onerror="this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" onerror="this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" />
/>
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i> <i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
</div> </div>
{{else}} {{else}}
@@ -154,14 +133,9 @@
{{#ifHttpUrl url}} {{#ifHttpUrl url}}
<div class="icon-container"> <div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i> <i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img <img class="favicon-icon" src="{{faviconUrl url}}" alt="{{name}} favicon" loading="lazy"
class="favicon-icon"
src="{{faviconUrl url}}"
alt="{{name}} favicon"
loading="lazy"
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');" onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" />
/>
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i> <i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
</div> </div>
{{else}} {{else}}
@@ -172,14 +146,9 @@
{{#ifHttpUrl url}} {{#ifHttpUrl url}}
<div class="icon-container"> <div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i> <i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img <img class="favicon-icon" src="{{faviconUrl url}}" alt="{{name}} favicon" loading="lazy"
class="favicon-icon"
src="{{faviconUrl url}}"
alt="{{name}} favicon"
loading="lazy"
onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');" onload="this.classList.add('loaded'); this.previousElementSibling.classList.add('hidden');"
onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" onerror="if (!this.dataset.faviconFallbackTried) { this.dataset.faviconFallbackTried = '1'; this.src = '{{faviconFallbackUrl url}}'; return; } this.classList.add('error'); this.previousElementSibling.classList.add('hidden'); this.nextElementSibling.classList.add('visible');" />
/>
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i> <i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}} icon-fallback" aria-hidden="true"></i>
</div> </div>
{{else}} {{else}}
@@ -195,7 +164,8 @@
<div class="site-card-content"> <div class="site-card-content">
<h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3> <h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3>
<p>{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}</p> <p>{{#if
description}}{{description}}{{else}}{{extractDomain url}}{{/if}}</p>
</div> </div>
{{/ifEquals}} {{/ifEquals}}
{{/ifEquals}} {{/ifEquals}}