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

View File

@@ -274,6 +274,7 @@ window.MeNav = {
newSite.href = siteUrl;
newSite.title = siteName + (siteDescription ? ' - ' + siteDescription : '');
newSite.setAttribute('data-tooltip', siteName + (siteDescription ? ' - ' + siteDescription : '')); // 添加自定义 tooltip
if (/^https?:\/\//i.test(siteUrl)) {
newSite.target = '_blank';
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}}
<a href="{{url}}" class="site-card{{#if style}} site-card-{{style}}{{/if}}"
{{#if external}}target="_blank" rel="noopener"{{/if}}
data-type="{{#if type}}{{type}}{{else}}site{{/if}}"
data-name="{{name}}"
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}}
<a href="{{url}}" class="site-card{{#if style}} site-card-{{style}}{{/if}}" {{#if external}}target="_blank"
rel="noopener" {{/if}} data-type="{{#if type}}{{type}}{{else}}site{{/if}}" data-name="{{name}}" 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}}"
{{#if publishedAt}}data-published-at="{{publishedAt}}"{{/if}}
{{#if source}}data-source="{{source}}"{{/if}}>
data-tooltip="{{#if name}}{{name}}{{else}}未命名站点{{/if}}{{#if description}} - {{description}}{{else}} - {{extractDomain url}}{{/if}}"
{{#if publishedAt}}data-published-at="{{publishedAt}}" {{/if}} {{#if source}}data-source="{{source}}" {{/if}}>
{{!-- articles首行图标+标题;下方“时间/来源 + 简介”全宽对齐,不被图标列缩进 --}}
{{#ifEquals type "article"}}
<div class="article-card-header">
@@ -18,14 +14,9 @@
{{#if faviconUrl}}
<div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img
class="favicon-icon"
src="{{faviconUrl}}"
alt="{{name}} favicon"
loading="lazy"
<img class="favicon-icon" src="{{faviconUrl}}" alt="{{name}} favicon" loading="lazy"
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>
</div>
{{else}}
@@ -36,14 +27,9 @@
{{#ifHttpUrl url}}
<div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img
class="favicon-icon"
src="{{faviconUrl url}}"
alt="{{name}} favicon"
loading="lazy"
<img class="favicon-icon" src="{{faviconUrl url}}" alt="{{name}} favicon" loading="lazy"
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>
</div>
{{else}}
@@ -54,14 +40,9 @@
{{#ifHttpUrl url}}
<div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img
class="favicon-icon"
src="{{faviconUrl url}}"
alt="{{name}} favicon"
loading="lazy"
<img class="favicon-icon" src="{{faviconUrl url}}" alt="{{name}} favicon" loading="lazy"
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>
</div>
{{else}}
@@ -93,7 +74,8 @@
{{/if}}
</div>
{{/ifCond}}
<p>{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}</p>
<p>{{#if
description}}{{description}}{{else}}{{extractDomain url}}{{/if}}</p>
</div>
{{else}}
{{!-- projects代码仓库风格卡片保留 data-* 结构,便于扩展识别与写回) --}}
@@ -103,13 +85,15 @@
<div class="repo-title">{{#if name}}{{name}}{{else}}未命名项目{{/if}}</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}}
<div class="repo-stats">
{{#if language}}
<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}}
</div>
{{/if}}
@@ -136,14 +120,9 @@
{{#if faviconUrl}}
<div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img
class="favicon-icon"
src="{{faviconUrl}}"
alt="{{name}} favicon"
loading="lazy"
<img class="favicon-icon" src="{{faviconUrl}}" alt="{{name}} favicon" loading="lazy"
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>
</div>
{{else}}
@@ -154,14 +133,9 @@
{{#ifHttpUrl url}}
<div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img
class="favicon-icon"
src="{{faviconUrl url}}"
alt="{{name}} favicon"
loading="lazy"
<img class="favicon-icon" src="{{faviconUrl url}}" alt="{{name}} favicon" loading="lazy"
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>
</div>
{{else}}
@@ -172,14 +146,9 @@
{{#ifHttpUrl url}}
<div class="icon-container">
<i class="fas fa-circle-notch fa-spin icon-placeholder" aria-hidden="true"></i>
<img
class="favicon-icon"
src="{{faviconUrl url}}"
alt="{{name}} favicon"
loading="lazy"
<img class="favicon-icon" src="{{faviconUrl url}}" alt="{{name}} favicon" loading="lazy"
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>
</div>
{{else}}
@@ -195,7 +164,8 @@
<div class="site-card-content">
<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>
{{/ifEquals}}
{{/ifEquals}}