Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e839019f5 | ||
|
|
95e2799721 |
Binary file not shown.
|
Before Width: | Height: | Size: 3.6 MiB After Width: | Height: | Size: 9.2 MiB |
@@ -1,111 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
|
||||
<!-- 背景透明 -->
|
||||
|
||||
<defs>
|
||||
<!-- 渐变:高亮发光青蓝 到 科技蓝 -->
|
||||
<linearGradient id="magicCyan" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#00F2FE" />
|
||||
<stop offset="100%" stop-color="#3B82F6" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 渐变:深邃蓝 (指针反向切面与暗部) -->
|
||||
<linearGradient id="magicDark" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#1E293B" />
|
||||
<stop offset="100%" stop-color="#0F172A" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 霓虹发光 Glow 滤镜 -->
|
||||
<filter id="magicGlow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="4" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- ==================== 丰富高精度的极客魔法阵 (Geek Magic Array) ==================== -->
|
||||
|
||||
<!-- 1. 最外层:外边界细环与四角法术引线节点 -->
|
||||
<circle cx="256" cy="256" r="242" fill="none" stroke="#00F2FE" stroke-width="1.5" stroke-opacity="0.5" />
|
||||
<circle cx="256" cy="256" r="236" fill="none" stroke="#3B82F6" stroke-width="2.5" stroke-opacity="0.8" />
|
||||
|
||||
<!-- 外圈四极极客指示顶点 -->
|
||||
<path d="M 256 8 L 266 26 L 246 26 Z" fill="#00F2FE" filter="url(#magicGlow)" />
|
||||
<path d="M 256 504 L 266 486 L 246 486 Z" fill="#2563EB" opacity="0.6" />
|
||||
<path d="M 8 256 L 26 246 L 26 266 Z" fill="#2563EB" opacity="0.6" />
|
||||
<path d="M 504 256 L 486 246 L 486 266 Z" fill="#2563EB" opacity="0.6" />
|
||||
|
||||
<!-- 2. 精密仪表与符文刻度带 (外环与内环间) -->
|
||||
<circle cx="256" cy="256" r="226" fill="none" stroke="#00F2FE" stroke-width="6" stroke-dasharray="2 6" stroke-opacity="0.75" />
|
||||
<circle cx="256" cy="256" r="216" fill="none" stroke="#3B82F6" stroke-width="1.5" stroke-opacity="0.4" />
|
||||
|
||||
<!-- 精算刻度环:6个长粗虚线的中心点完全对齐正六边形与角点的 60° 几何角 -->
|
||||
<circle cx="256" cy="256" r="210" fill="none" stroke="#00F2FE" stroke-width="4.5"
|
||||
stroke-dasharray="25 8 6 15.977872" stroke-dashoffset="-12.5" stroke-opacity="0.85" filter="url(#magicGlow)" />
|
||||
|
||||
<!-- 3. 魔法阵三重嵌套多边形结界 (正三角形 + 正六边形 + 12角星线拓扑) -->
|
||||
<g fill="none" stroke="#3B82F6" stroke-width="1.5" stroke-opacity="0.5">
|
||||
<!-- 正六边形 A (顶角精确落于 r=210 粗虚线长段) -->
|
||||
<polygon points="256,46 437.86,151 437.86,361 256,466 74.14,361 74.14,151" stroke="#00F2FE" stroke-width="1.8" stroke-opacity="0.7" />
|
||||
<!-- 正六边形 B (旋转30度) -->
|
||||
<polygon points="361,74.14 466,256 361,437.86 151,437.86 46,256 151,74.14" />
|
||||
<!-- 正三角形 C (向上) -->
|
||||
<polygon points="256,46 437.86,361 74.14,361" stroke="#00F2FE" stroke-width="1" stroke-opacity="0.4" />
|
||||
<!-- 正三角形 D (向下) -->
|
||||
<polygon points="256,466 74.14,151 437.86,151" stroke="#00F2FE" stroke-width="1" stroke-opacity="0.4" />
|
||||
</g>
|
||||
|
||||
<!-- 4. 六方位魔法法阵节点圈 (在多边形顶点上的极客元素) -->
|
||||
<g fill="#0F172A" stroke="#00F2FE" stroke-width="2" opacity="0.95">
|
||||
<circle cx="256" cy="46" r="10" />
|
||||
<circle cx="437.86" cy="151" r="10" />
|
||||
<circle cx="437.86" cy="361" r="10" />
|
||||
<circle cx="256" cy="466" r="10" />
|
||||
<circle cx="74.14" cy="361" r="10" />
|
||||
<circle cx="74.14" cy="151" r="10" />
|
||||
</g>
|
||||
|
||||
<!-- 节点内发光核 -->
|
||||
<g fill="#00F2FE" filter="url(#magicGlow)">
|
||||
<circle cx="256" cy="46" r="4" />
|
||||
<circle cx="437.86" cy="151" r="4" />
|
||||
<circle cx="437.86" cy="361" r="4" />
|
||||
<circle cx="256" cy="466" r="4" />
|
||||
<circle cx="74.14" cy="361" r="4" />
|
||||
<circle cx="74.14" cy="151" r="4" />
|
||||
</g>
|
||||
|
||||
<!-- 5. 中阶魔法阵环与细分刻度 -->
|
||||
<circle cx="256" cy="256" r="186" fill="none" stroke="#00F2FE" stroke-width="2" stroke-opacity="0.8" filter="url(#magicGlow)" />
|
||||
<circle cx="256" cy="256" r="176" fill="none" stroke="#2563EB" stroke-width="3" stroke-dasharray="12 6 4 6" stroke-opacity="0.6" />
|
||||
<circle cx="256" cy="256" r="166" fill="none" stroke="#00F2FE" stroke-width="1" stroke-dasharray="2 4" stroke-opacity="0.5" />
|
||||
|
||||
<!-- 6. 魔法阵放射状流向线 (16等分星射线) -->
|
||||
<g stroke="#00F2FE" stroke-width="1" stroke-opacity="0.2">
|
||||
<line x1="256" y1="26" x2="256" y2="486" />
|
||||
<line x1="26" y1="256" x2="486" y2="256" />
|
||||
<line x1="93" y1="93" x2="419" y2="419" />
|
||||
<line x1="419" y1="93" x2="93" y2="419" />
|
||||
</g>
|
||||
|
||||
<!-- ==================== 中心:硬核极客机甲指针 (中间区域干净无符号) ==================== -->
|
||||
|
||||
<g transform="rotate(28, 256, 256)">
|
||||
<!-- 北向主指针(立体几何亮蓝切面) -->
|
||||
<path d="M 256 70 L 306 226 L 256 256 Z" fill="url(#magicCyan)" filter="url(#magicGlow)" />
|
||||
<!-- 北向副切面(深蓝阴影) -->
|
||||
<path d="M 256 70 L 206 226 L 256 256 Z" fill="#1D4ED8" />
|
||||
|
||||
<!-- 指针侧翼机甲组件 -->
|
||||
<path d="M 312 236 L 332 266 L 282 256 Z" fill="#00F2FE" opacity="0.7" />
|
||||
<path d="M 200 236 L 180 266 L 230 256 Z" fill="#2563EB" opacity="0.7" />
|
||||
|
||||
<!-- 南向反向指针(深色金属质感) -->
|
||||
<path d="M 256 442 L 220 286 L 256 256 Z" fill="url(#magicDark)" />
|
||||
<path d="M 256 442 L 292 286 L 256 256 Z" fill="#334155" />
|
||||
</g>
|
||||
|
||||
<!-- 中心核心:反应堆式同心轴 -->
|
||||
<circle cx="256" cy="256" r="32" fill="#0F172A" stroke="#00F2FE" stroke-width="3" />
|
||||
<circle cx="256" cy="256" r="18" fill="none" stroke="#3B82F6" stroke-width="2" stroke-dasharray="6 3" />
|
||||
<circle cx="256" cy="256" r="8" fill="#00F2FE" filter="url(#magicGlow)" />
|
||||
<circle cx="256" cy="256" r="3" fill="#FFFFFF" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-labelledby="title desc">
|
||||
<title id="title">北极星标志</title>
|
||||
<desc id="desc">透明背景的蓝色八角北极星标志,纵向星芒略长。</desc>
|
||||
<path fill="#205B91" d="M256 16 279 207 348 164 305 233 472 256 305 279 348 348 279 305 256 496 233 305 164 348 207 279 40 256 207 233 164 164 233 207Z"/>
|
||||
<path fill="#438CC4" d="M256 16 256 256 279 207Z M472 256 256 256 305 279Z M256 496 256 256 233 305Z M40 256 256 256 207 233Z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 559 B |
@@ -0,0 +1,111 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
|
||||
<!-- 背景透明 -->
|
||||
|
||||
<defs>
|
||||
<!-- 渐变:高亮发光青蓝 到 科技蓝 -->
|
||||
<linearGradient id="magicCyan" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#00F2FE" />
|
||||
<stop offset="100%" stop-color="#3B82F6" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 渐变:深邃蓝 (指针反向切面与暗部) -->
|
||||
<linearGradient id="magicDark" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#1E293B" />
|
||||
<stop offset="100%" stop-color="#0F172A" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 霓虹发光 Glow 滤镜 -->
|
||||
<filter id="magicGlow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="4" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- ==================== 丰富高精度的极客魔法阵 (Geek Magic Array) ==================== -->
|
||||
|
||||
<!-- 1. 最外层:外边界细环与四角法术引线节点 -->
|
||||
<circle cx="256" cy="256" r="242" fill="none" stroke="#00F2FE" stroke-width="1.5" stroke-opacity="0.5" />
|
||||
<circle cx="256" cy="256" r="236" fill="none" stroke="#3B82F6" stroke-width="2.5" stroke-opacity="0.8" />
|
||||
|
||||
<!-- 外圈四极极客指示顶点 -->
|
||||
<path d="M 256 8 L 266 26 L 246 26 Z" fill="#00F2FE" filter="url(#magicGlow)" />
|
||||
<path d="M 256 504 L 266 486 L 246 486 Z" fill="#2563EB" opacity="0.6" />
|
||||
<path d="M 8 256 L 26 246 L 26 266 Z" fill="#2563EB" opacity="0.6" />
|
||||
<path d="M 504 256 L 486 246 L 486 266 Z" fill="#2563EB" opacity="0.6" />
|
||||
|
||||
<!-- 2. 精密仪表与符文刻度带 (外环与内环间) -->
|
||||
<circle cx="256" cy="256" r="226" fill="none" stroke="#00F2FE" stroke-width="6" stroke-dasharray="2 6" stroke-opacity="0.75" />
|
||||
<circle cx="256" cy="256" r="216" fill="none" stroke="#3B82F6" stroke-width="1.5" stroke-opacity="0.4" />
|
||||
|
||||
<!-- 精算刻度环:6个长粗虚线的中心点完全对齐正六边形与角点的 60° 几何角 -->
|
||||
<circle cx="256" cy="256" r="210" fill="none" stroke="#00F2FE" stroke-width="4.5"
|
||||
stroke-dasharray="25 8 6 15.977872" stroke-dashoffset="-12.5" stroke-opacity="0.85" filter="url(#magicGlow)" />
|
||||
|
||||
<!-- 3. 魔法阵三重嵌套多边形结界 (正三角形 + 正六边形 + 12角星线拓扑) -->
|
||||
<g fill="none" stroke="#3B82F6" stroke-width="1.5" stroke-opacity="0.5">
|
||||
<!-- 正六边形 A (顶角精确落于 r=210 粗虚线长段) -->
|
||||
<polygon points="256,46 437.86,151 437.86,361 256,466 74.14,361 74.14,151" stroke="#00F2FE" stroke-width="1.8" stroke-opacity="0.7" />
|
||||
<!-- 正六边形 B (旋转30度) -->
|
||||
<polygon points="361,74.14 466,256 361,437.86 151,437.86 46,256 151,74.14" />
|
||||
<!-- 正三角形 C (向上) -->
|
||||
<polygon points="256,46 437.86,361 74.14,361" stroke="#00F2FE" stroke-width="1" stroke-opacity="0.4" />
|
||||
<!-- 正三角形 D (向下) -->
|
||||
<polygon points="256,466 74.14,151 437.86,151" stroke="#00F2FE" stroke-width="1" stroke-opacity="0.4" />
|
||||
</g>
|
||||
|
||||
<!-- 4. 六方位魔法法阵节点圈 (在多边形顶点上的极客元素) -->
|
||||
<g fill="#0F172A" stroke="#00F2FE" stroke-width="2" opacity="0.95">
|
||||
<circle cx="256" cy="46" r="10" />
|
||||
<circle cx="437.86" cy="151" r="10" />
|
||||
<circle cx="437.86" cy="361" r="10" />
|
||||
<circle cx="256" cy="466" r="10" />
|
||||
<circle cx="74.14" cy="361" r="10" />
|
||||
<circle cx="74.14" cy="151" r="10" />
|
||||
</g>
|
||||
|
||||
<!-- 节点内发光核 -->
|
||||
<g fill="#00F2FE" filter="url(#magicGlow)">
|
||||
<circle cx="256" cy="46" r="4" />
|
||||
<circle cx="437.86" cy="151" r="4" />
|
||||
<circle cx="437.86" cy="361" r="4" />
|
||||
<circle cx="256" cy="466" r="4" />
|
||||
<circle cx="74.14" cy="361" r="4" />
|
||||
<circle cx="74.14" cy="151" r="4" />
|
||||
</g>
|
||||
|
||||
<!-- 5. 中阶魔法阵环与细分刻度 -->
|
||||
<circle cx="256" cy="256" r="186" fill="none" stroke="#00F2FE" stroke-width="2" stroke-opacity="0.8" filter="url(#magicGlow)" />
|
||||
<circle cx="256" cy="256" r="176" fill="none" stroke="#2563EB" stroke-width="3" stroke-dasharray="12 6 4 6" stroke-opacity="0.6" />
|
||||
<circle cx="256" cy="256" r="166" fill="none" stroke="#00F2FE" stroke-width="1" stroke-dasharray="2 4" stroke-opacity="0.5" />
|
||||
|
||||
<!-- 6. 魔法阵放射状流向线 (16等分星射线) -->
|
||||
<g stroke="#00F2FE" stroke-width="1" stroke-opacity="0.2">
|
||||
<line x1="256" y1="26" x2="256" y2="486" />
|
||||
<line x1="26" y1="256" x2="486" y2="256" />
|
||||
<line x1="93" y1="93" x2="419" y2="419" />
|
||||
<line x1="419" y1="93" x2="93" y2="419" />
|
||||
</g>
|
||||
|
||||
<!-- ==================== 中心:硬核极客机甲指针 (中间区域干净无符号) ==================== -->
|
||||
|
||||
<g transform="rotate(28, 256, 256)">
|
||||
<!-- 北向主指针(立体几何亮蓝切面) -->
|
||||
<path d="M 256 70 L 306 226 L 256 256 Z" fill="url(#magicCyan)" filter="url(#magicGlow)" />
|
||||
<!-- 北向副切面(深蓝阴影) -->
|
||||
<path d="M 256 70 L 206 226 L 256 256 Z" fill="#1D4ED8" />
|
||||
|
||||
<!-- 指针侧翼机甲组件 -->
|
||||
<path d="M 312 236 L 332 266 L 282 256 Z" fill="#00F2FE" opacity="0.7" />
|
||||
<path d="M 200 236 L 180 266 L 230 256 Z" fill="#2563EB" opacity="0.7" />
|
||||
|
||||
<!-- 南向反向指针(深色金属质感) -->
|
||||
<path d="M 256 442 L 220 286 L 256 256 Z" fill="url(#magicDark)" />
|
||||
<path d="M 256 442 L 292 286 L 256 256 Z" fill="#334155" />
|
||||
</g>
|
||||
|
||||
<!-- 中心核心:反应堆式同心轴 -->
|
||||
<circle cx="256" cy="256" r="32" fill="#0F172A" stroke="#00F2FE" stroke-width="3" />
|
||||
<circle cx="256" cy="256" r="18" fill="none" stroke="#3B82F6" stroke-width="2" stroke-dasharray="6 3" />
|
||||
<circle cx="256" cy="256" r="8" fill="#00F2FE" filter="url(#magicGlow)" />
|
||||
<circle cx="256" cy="256" r="3" fill="#FFFFFF" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1791734628751" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5223" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1020.586667 361.813333c0-92.16-75.093333-167.253333-167.253334-167.253333h-266.24l23.893334 95.573333 228.693333 51.2c20.48 3.413333 37.546667 23.893333 37.546667 44.373334v245.76c0 20.48-17.066667 40.96-37.546667 44.373333l-228.693333 51.2-27.306667 92.16h266.24c92.16 0 167.253333-75.093333 167.253333-167.253333 3.413333 0 3.413333-290.133333 3.413334-290.133334zM187.733333 672.426667c-20.48-3.413333-37.546667-23.893333-37.546666-44.373334v-245.76c0-20.48 17.066667-40.96 37.546666-44.373333l228.693334-51.2 23.893333-95.573333H174.08C81.92 191.146667 6.826667 266.24 6.826667 358.4V648.533333c0 92.16 75.093333 167.253333 167.253333 167.253334h266.24l-23.893333-95.573334c0 3.413333-228.693333-47.786667-228.693334-47.786666z m215.04-211.626667h218.453334v88.746667h-218.453334v-88.746667z" fill="#ff6b01" p-id="5224"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -6,7 +6,7 @@ import { getLinksByCategory } from "@/services/links"
|
||||
import CategoryPage, { dynamicParams, generateStaticParams } from "./page"
|
||||
|
||||
describe("CategoryPage", () => {
|
||||
it("为全部 7 个分类生成静态参数并关闭运行时动态参数", () => {
|
||||
it("为全部 8 个分类生成静态参数并关闭运行时动态参数", () => {
|
||||
expect(generateStaticParams()).toEqual([
|
||||
{ categoryId: "ai" },
|
||||
{ categoryId: "dev" },
|
||||
@@ -14,11 +14,35 @@ describe("CategoryPage", () => {
|
||||
{ categoryId: "learning" },
|
||||
{ categoryId: "efficiency" },
|
||||
{ categoryId: "media" },
|
||||
{ categoryId: "game-resources" },
|
||||
{ categoryId: "life" },
|
||||
])
|
||||
expect(dynamicParams).toBe(false)
|
||||
})
|
||||
|
||||
it("归类边界:游戏资料页收录宝可梦维基,娱乐媒体页不再包含", async () => {
|
||||
const gamePage = await CategoryPage({
|
||||
params: Promise.resolve({ categoryId: "game-resources" }),
|
||||
})
|
||||
render(gamePage)
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", { level: 1, name: "游戏资料" }),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole("link", { name: /神奇宝贝百科/ }),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole("link", { name: /星露谷物语中文维基/ }),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole("link", { name: /萌娘百科/ }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(getLinksByCategory("media").map(({ id }) => id)).not.toContain(
|
||||
"wiki-52poke",
|
||||
)
|
||||
})
|
||||
|
||||
it("通过 service 组合开发工具分类页", async () => {
|
||||
const links = getLinksByCategory("dev")
|
||||
const page = await CategoryPage({
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--text-primary);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: #ffffff;
|
||||
--color-primary-foreground: #10223e;
|
||||
--color-muted-foreground: var(--text-secondary);
|
||||
--color-border: var(--border-subtle);
|
||||
--radius-lg: var(--radius-card);
|
||||
@@ -12,41 +12,44 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--background: #dcebf5;
|
||||
--text-primary: #0f172a;
|
||||
--text-secondary: #64748b;
|
||||
--text-muted: #94a3b8;
|
||||
--primary: #3b82f6;
|
||||
--primary-hover: #2563eb;
|
||||
--primary-soft: rgba(59, 130, 246, 0.1);
|
||||
color-scheme: dark;
|
||||
--background: #101a2a;
|
||||
--text-primary: #f4f7fc;
|
||||
--text-secondary: #d7e2f0;
|
||||
--text-muted: #bfd0e4;
|
||||
--primary: #a9c9ff;
|
||||
--primary-hover: #d3e3ff;
|
||||
--primary-soft: rgba(147, 190, 255, 0.15);
|
||||
/* Navigation stays translucent; a future reading surface can use this opaque tone. */
|
||||
--surface-reading: rgba(13, 23, 39, 0.96);
|
||||
--surface-sidebar: linear-gradient(
|
||||
180deg,
|
||||
rgba(235, 243, 251, 0.52),
|
||||
rgba(204, 220, 238, 0.34)
|
||||
155deg,
|
||||
rgba(23, 34, 53, 0.69),
|
||||
rgba(15, 26, 44, 0.64)
|
||||
);
|
||||
--surface-search: linear-gradient(
|
||||
135deg,
|
||||
rgba(240, 246, 252, 0.56),
|
||||
rgba(210, 225, 241, 0.4)
|
||||
145deg,
|
||||
rgba(30, 43, 62, 0.72),
|
||||
rgba(19, 31, 50, 0.68)
|
||||
);
|
||||
--surface-card: linear-gradient(
|
||||
135deg,
|
||||
rgba(231, 240, 249, 0.43),
|
||||
rgba(198, 216, 236, 0.32)
|
||||
145deg,
|
||||
rgba(36, 50, 70, 0.64),
|
||||
rgba(19, 31, 49, 0.61)
|
||||
);
|
||||
--surface-card-hover: linear-gradient(
|
||||
135deg,
|
||||
rgba(242, 248, 253, 0.6),
|
||||
rgba(216, 230, 244, 0.48)
|
||||
145deg,
|
||||
rgba(49, 65, 87, 0.73),
|
||||
rgba(27, 42, 63, 0.71)
|
||||
);
|
||||
--border-glass: rgba(255, 255, 255, 0.38);
|
||||
--border-subtle: rgba(15, 23, 42, 0.08);
|
||||
--shadow-soft: inset 0 1px 0 rgba(255, 255, 255, 0.34);
|
||||
--border-glass: rgba(222, 234, 255, 0.17);
|
||||
--border-subtle: rgba(213, 228, 250, 0.16);
|
||||
--shadow-soft:
|
||||
0 12px 32px rgba(3, 9, 23, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||
--shadow-medium:
|
||||
0 22px 58px rgba(4, 12, 34, 0.17), inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
||||
0 20px 48px rgba(3, 9, 23, 0.24), inset 0 1px 0 rgba(255, 255, 255, 0.14);
|
||||
--shadow-panel:
|
||||
0 28px 72px rgba(4, 12, 34, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.42);
|
||||
0 24px 64px rgba(3, 9, 23, 0.26), inset 0 1px 0 rgba(255, 255, 255, 0.13);
|
||||
--radius-card: 20px;
|
||||
--radius-panel: 24px;
|
||||
--sidebar-expanded: 240px;
|
||||
@@ -68,15 +71,15 @@ body {
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
color: var(--text-primary);
|
||||
background-color: #0b102b;
|
||||
background-color: var(--background);
|
||||
background-image: url("/backgrounds/test.jpg");
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
font-family:
|
||||
Inter,
|
||||
"SF Pro Display",
|
||||
"Avenir Next",
|
||||
Avenir,
|
||||
"SF Pro Text",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
@@ -95,9 +98,9 @@ body::before {
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(3, 7, 28, 0.2) 0%,
|
||||
rgba(9, 18, 54, 0.07) 48%,
|
||||
rgba(216, 230, 244, 0.08) 100%
|
||||
rgba(5, 12, 25, 0.43) 0%,
|
||||
rgba(8, 16, 31, 0.37) 48%,
|
||||
rgba(7, 15, 28, 0.47) 100%
|
||||
);
|
||||
-webkit-backdrop-filter: none;
|
||||
backdrop-filter: none;
|
||||
@@ -115,7 +118,7 @@ a {
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 3px solid rgba(59, 130, 246, 0.34);
|
||||
outline: 2px solid #c9dfff;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
@@ -124,8 +127,8 @@ a {
|
||||
border-radius: var(--radius-panel);
|
||||
background: var(--surface-card);
|
||||
box-shadow: var(--shadow-soft);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(134%);
|
||||
backdrop-filter: blur(16px) saturate(134%);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(108%);
|
||||
backdrop-filter: blur(18px) saturate(108%);
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
@@ -155,8 +158,8 @@ a {
|
||||
border-radius: var(--radius-panel);
|
||||
background: var(--surface-sidebar);
|
||||
box-shadow: var(--shadow-panel);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(132%);
|
||||
backdrop-filter: blur(18px) saturate(132%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(108%);
|
||||
backdrop-filter: blur(20px) saturate(108%);
|
||||
transition: width 180ms ease;
|
||||
}
|
||||
|
||||
@@ -167,7 +170,7 @@ a {
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.26);
|
||||
border-bottom: 1px solid rgba(222, 234, 255, 0.11);
|
||||
}
|
||||
|
||||
.sidebar__brand {
|
||||
@@ -233,7 +236,7 @@ a {
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
padding: 0 14px;
|
||||
color: #334155;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
@@ -252,6 +255,7 @@ a {
|
||||
.sidebar__nav-item--active {
|
||||
color: var(--primary-hover);
|
||||
background: var(--primary-soft);
|
||||
box-shadow: inset 0 0 0 1px rgba(169, 201, 255, 0.13);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
@@ -322,7 +326,7 @@ a {
|
||||
font-weight: 700;
|
||||
line-height: 1.16;
|
||||
letter-spacing: -0.035em;
|
||||
text-shadow: 0 3px 24px rgba(2, 6, 23, 0.48);
|
||||
text-shadow: 0 2px 12px rgba(2, 6, 23, 0.8);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
@@ -331,7 +335,7 @@ a {
|
||||
color: rgba(241, 245, 249, 0.86);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
text-shadow: 0 2px 16px rgba(2, 6, 23, 0.42);
|
||||
text-shadow: 0 2px 12px rgba(2, 6, 23, 0.8);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
@@ -349,8 +353,8 @@ a {
|
||||
border-radius: var(--radius-panel);
|
||||
background: var(--surface-search);
|
||||
box-shadow: var(--shadow-medium);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(136%);
|
||||
backdrop-filter: blur(18px) saturate(136%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(108%);
|
||||
backdrop-filter: blur(20px) saturate(108%);
|
||||
transition:
|
||||
border-color 180ms ease,
|
||||
box-shadow 180ms ease,
|
||||
@@ -358,14 +362,10 @@ a {
|
||||
}
|
||||
|
||||
.search-box__form:focus-within {
|
||||
border-color: rgba(191, 219, 254, 0.46);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(245, 249, 253, 0.66),
|
||||
rgba(218, 231, 245, 0.52)
|
||||
);
|
||||
border-color: rgba(185, 214, 255, 0.52);
|
||||
background: var(--surface-search);
|
||||
box-shadow:
|
||||
0 0 0 4px rgba(147, 197, 253, 0.12),
|
||||
0 0 0 3px rgba(147, 197, 253, 0.17),
|
||||
var(--shadow-medium);
|
||||
}
|
||||
|
||||
@@ -376,7 +376,7 @@ a {
|
||||
justify-content: center;
|
||||
margin-left: 10px;
|
||||
padding: 0;
|
||||
color: #334155;
|
||||
color: var(--text-secondary);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
@@ -387,18 +387,41 @@ a {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* The bundled GitHub mark is charcoal; lift only that mark on dark glass. */
|
||||
.search-provider-menu__icon[src*="github.svg"] {
|
||||
filter: brightness(0) invert(1);
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.search-provider-menu__trigger:hover,
|
||||
.search-provider-menu__trigger[aria-expanded="true"] {
|
||||
color: var(--primary-hover);
|
||||
background: var(--primary-soft);
|
||||
}
|
||||
|
||||
/* The provider menu is portalled outside the app shell. Match the search glass. */
|
||||
[data-slot="dropdown-menu-content"] {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-glass);
|
||||
background: rgba(19, 31, 49, 0.96);
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
|
||||
[data-slot="dropdown-menu-radio-item"]:focus {
|
||||
color: var(--primary-hover);
|
||||
background: var(--primary-soft);
|
||||
}
|
||||
|
||||
[data-slot="dropdown-menu-radio-item"][data-state="checked"] {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.search-box__icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex: none;
|
||||
margin-left: 10px;
|
||||
color: rgba(51, 65, 85, 0.58);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.search-box__input {
|
||||
@@ -415,10 +438,15 @@ a {
|
||||
}
|
||||
|
||||
.search-box__input::placeholder {
|
||||
color: rgba(51, 65, 85, 0.62);
|
||||
color: var(--text-muted);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.search-box__input:focus-visible {
|
||||
/* The entire search field receives a visible focus ring above. */
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-box__shortcut {
|
||||
display: inline-flex;
|
||||
min-width: 50px;
|
||||
@@ -432,10 +460,10 @@ a {
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
border: 1px solid rgba(100, 116, 139, 0.2);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 9px;
|
||||
background: rgba(255, 255, 255, 0.42);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.68);
|
||||
background: rgba(201, 220, 248, 0.08);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.local-search-results {
|
||||
@@ -449,10 +477,10 @@ a {
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(19, 31, 49, 0.95);
|
||||
box-shadow: var(--shadow-medium);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.local-search-results__option {
|
||||
@@ -473,10 +501,14 @@ a {
|
||||
|
||||
.local-search-results__option[aria-selected="true"] {
|
||||
color: var(--primary-hover);
|
||||
border-color: rgba(59, 130, 246, 0.08);
|
||||
border-color: rgba(169, 201, 255, 0.17);
|
||||
background: var(--primary-soft);
|
||||
}
|
||||
|
||||
.local-search-results__option:hover {
|
||||
background: rgba(147, 190, 255, 0.1);
|
||||
}
|
||||
|
||||
.local-search-results__copy {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -521,7 +553,7 @@ a {
|
||||
color: rgba(248, 250, 252, 0.96);
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
text-shadow: 0 2px 14px rgba(2, 6, 23, 0.44);
|
||||
text-shadow: 0 2px 12px rgba(2, 6, 23, 0.8);
|
||||
}
|
||||
|
||||
.quick-links__grid {
|
||||
@@ -554,9 +586,9 @@ a {
|
||||
|
||||
.quick-link-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(255, 255, 255, 0.54);
|
||||
border-color: rgba(222, 234, 255, 0.3);
|
||||
background: var(--surface-card-hover);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
|
||||
.quick-link-card > span:last-child {
|
||||
@@ -574,20 +606,24 @@ a {
|
||||
}
|
||||
|
||||
.category-page__panel {
|
||||
/* Leave the wallpaper sharp; only the text-bearing elements need glass. */
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
padding: 48px clamp(32px, 4vw, 64px) 56px;
|
||||
background: var(--surface-sidebar);
|
||||
box-shadow: var(--shadow-panel);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(132%);
|
||||
backdrop-filter: blur(18px) saturate(132%);
|
||||
padding: 40px clamp(24px, 3.2vw, 48px) 32px;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
max-width: 720px;
|
||||
padding: 16px 22px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.category-header__title {
|
||||
@@ -597,6 +633,7 @@ a {
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.03em;
|
||||
text-shadow: 0 2px 16px rgba(2, 8, 19, 0.85);
|
||||
}
|
||||
|
||||
.category-header__description {
|
||||
@@ -604,6 +641,7 @@ a {
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
text-shadow: 0 2px 12px rgba(2, 8, 19, 0.9);
|
||||
}
|
||||
|
||||
.category-tools {
|
||||
@@ -611,7 +649,7 @@ a {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
margin-top: 36px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.category-tools__viewport {
|
||||
@@ -620,19 +658,25 @@ a {
|
||||
overflow-y: auto;
|
||||
overscroll-behavior-y: contain;
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.36);
|
||||
padding: 14px;
|
||||
border: 1px solid rgba(222, 234, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
background: rgba(82, 113, 151, 0.09);
|
||||
background: rgba(5, 14, 29, 0.18);
|
||||
box-shadow:
|
||||
inset 0 2px 10px rgba(29, 56, 93, 0.07),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
||||
0 14px 36px rgba(2, 8, 19, 0.12),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
|
||||
.category-tools__viewport:focus-visible {
|
||||
outline-offset: -4px;
|
||||
}
|
||||
|
||||
.category-tools__viewport:has(> .category-empty-state) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.category-section-header {
|
||||
display: flex;
|
||||
flex: none;
|
||||
@@ -645,34 +689,35 @@ a {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 2px 12px rgba(2, 8, 19, 0.9);
|
||||
}
|
||||
|
||||
.category-section-header__count {
|
||||
padding: 4px 10px;
|
||||
color: #334155;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(71, 104, 138, 0.24);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 999px;
|
||||
background: rgba(238, 246, 253, 0.88);
|
||||
background: rgba(10, 21, 38, 0.65);
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
height: 96px;
|
||||
height: 82px;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px 22px;
|
||||
gap: 14px;
|
||||
padding: 14px 16px;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
border: 1px solid var(--border-glass);
|
||||
border: 1px solid rgba(222, 234, 255, 0.22);
|
||||
border-radius: 18px;
|
||||
background: var(--surface-card);
|
||||
background: rgba(12, 24, 42, 0.65);
|
||||
box-shadow: var(--shadow-soft);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(134%);
|
||||
backdrop-filter: blur(16px) saturate(134%);
|
||||
-webkit-backdrop-filter: blur(10px) saturate(108%);
|
||||
backdrop-filter: blur(10px) saturate(108%);
|
||||
transition:
|
||||
transform 180ms ease,
|
||||
background 180ms ease,
|
||||
@@ -681,8 +726,9 @@ a {
|
||||
|
||||
.tool-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(255, 255, 255, 0.54);
|
||||
background: var(--surface-card-hover);
|
||||
border-color: rgba(222, 234, 255, 0.42);
|
||||
background: rgba(25, 43, 65, 0.84);
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
|
||||
.tool-card__icon {
|
||||
@@ -739,7 +785,7 @@ a {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 20px 24px;
|
||||
gap: 14px 16px;
|
||||
}
|
||||
|
||||
.tool-grid__item {
|
||||
@@ -757,9 +803,12 @@ a {
|
||||
}
|
||||
|
||||
.category-tools__viewport > .category-empty-state {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding-block: 20px;
|
||||
width: min(100%, 440px);
|
||||
min-height: 200px;
|
||||
flex: none;
|
||||
padding: 20px;
|
||||
border-radius: 14px;
|
||||
background: rgba(10, 21, 38, 0.65);
|
||||
}
|
||||
|
||||
.category-empty-state > svg {
|
||||
@@ -822,14 +871,14 @@ a {
|
||||
color: var(--primary-hover);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
background: rgba(219, 234, 254, 0.7);
|
||||
box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.08);
|
||||
background: rgba(138, 180, 238, 0.16);
|
||||
box-shadow: inset 0 0 0 1px rgba(169, 201, 255, 0.13);
|
||||
}
|
||||
|
||||
.local-search-results,
|
||||
.sidebar__nav,
|
||||
.category-tools__viewport {
|
||||
scrollbar-color: rgba(100, 116, 139, 0.32) transparent;
|
||||
scrollbar-color: rgba(185, 206, 233, 0.34) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
@@ -860,45 +909,43 @@ a {
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
/* Keep category navigation usable without squeezing the boxed content. */
|
||||
.app-shell:has(.category-page) .sidebar {
|
||||
/* A shared top rail keeps home and category navigation usable on phones. */
|
||||
.app-shell .sidebar {
|
||||
inset: 12px 12px auto;
|
||||
width: auto;
|
||||
height: 124px;
|
||||
}
|
||||
|
||||
.app-shell:has(.category-page) .sidebar__header,
|
||||
.app-shell:has(.category-page)[data-sidebar-collapsed="true"]
|
||||
.sidebar__header {
|
||||
.app-shell .sidebar__header,
|
||||
.app-shell[data-sidebar-collapsed="true"] .sidebar__header {
|
||||
min-height: 56px;
|
||||
flex-direction: row;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.app-shell:has(.category-page) .sidebar__header > button {
|
||||
.app-shell .sidebar__header > button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-shell:has(.category-page) .sidebar__brand {
|
||||
.app-shell .sidebar__brand {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.app-shell:has(.category-page) .sidebar__nav,
|
||||
.app-shell:has(.category-page)[data-sidebar-collapsed="true"] .sidebar__nav {
|
||||
.app-shell .sidebar__nav,
|
||||
.app-shell[data-sidebar-collapsed="true"] .sidebar__nav {
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 8px 12px 12px;
|
||||
}
|
||||
|
||||
.app-shell:has(.category-page) .sidebar__categories {
|
||||
.app-shell .sidebar__categories {
|
||||
flex: none;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.app-shell:has(.category-page) .sidebar__nav-item,
|
||||
.app-shell:has(.category-page)[data-sidebar-collapsed="true"]
|
||||
.sidebar__nav-item {
|
||||
.app-shell .sidebar__nav-item,
|
||||
.app-shell[data-sidebar-collapsed="true"] .sidebar__nav-item {
|
||||
width: auto;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
@@ -907,8 +954,8 @@ a {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.app-shell:has(.category-page) .sidebar__site-name.sr-only,
|
||||
.app-shell:has(.category-page) .sidebar__label.sr-only {
|
||||
.app-shell .sidebar__site-name.sr-only,
|
||||
.app-shell .sidebar__label.sr-only {
|
||||
position: static;
|
||||
width: auto;
|
||||
height: auto;
|
||||
@@ -918,20 +965,44 @@ a {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-shell:has(.category-page) .app-shell__content {
|
||||
.app-shell .app-shell__content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.home-content {
|
||||
padding: 188px 20px 48px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.quick-links {
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
.quick-links__grid {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quick-link-card {
|
||||
padding-inline: 14px;
|
||||
}
|
||||
|
||||
.category-page {
|
||||
padding: 148px 12px 12px;
|
||||
}
|
||||
|
||||
.category-page__panel {
|
||||
padding: 28px 20px 36px;
|
||||
padding: 24px 12px 20px;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.category-tools__viewport {
|
||||
padding: 12px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.category-header__title {
|
||||
@@ -939,6 +1010,20 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.quick-links__grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.home-content {
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
.search-box__shortcut {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* When the header would leave only a sliver of cards, let the page scroll instead. */
|
||||
@media (max-height: 540px), (max-width: 767px) and (max-height: 700px) {
|
||||
.category-page {
|
||||
@@ -960,11 +1045,6 @@ a {
|
||||
overflow: visible;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.category-tools__viewport > .category-empty-state {
|
||||
height: auto;
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) and (max-height: 700px) {
|
||||
|
||||
@@ -11,8 +11,8 @@ export const metadata: Metadata = {
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
colorScheme: "light",
|
||||
themeColor: "#dcebf5",
|
||||
colorScheme: "dark",
|
||||
themeColor: "#101a2a",
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@@ -41,6 +41,13 @@
|
||||
"icon": "circle-play",
|
||||
"sort": 60
|
||||
},
|
||||
{
|
||||
"id": "game-resources",
|
||||
"title": "游戏资料",
|
||||
"description": "游戏攻略维基、资料速查与实用规划工具。",
|
||||
"icon": "book-open",
|
||||
"sort": 65
|
||||
},
|
||||
{
|
||||
"id": "life",
|
||||
"title": "生活服务",
|
||||
|
||||
@@ -9,9 +9,12 @@ const hasUniqueValues = (values: string[]) =>
|
||||
new Set(values).size === values.length
|
||||
|
||||
describe("Polaris 静态数据", () => {
|
||||
it("包含 7 个唯一且结构完整的分类", () => {
|
||||
expect(categories).toHaveLength(7)
|
||||
it("包含 8 个唯一且结构完整的分类", () => {
|
||||
expect(categories).toHaveLength(8)
|
||||
expect(hasUniqueValues(categories.map(({ id }) => id))).toBe(true)
|
||||
expect(hasUniqueValues(categories.map(({ sort }) => String(sort)))).toBe(
|
||||
true,
|
||||
)
|
||||
|
||||
for (const category of categories) {
|
||||
expect(category).toEqual({
|
||||
@@ -27,9 +30,9 @@ describe("Polaris 静态数据", () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("包含 45 条合法链接且分类引用均存在", () => {
|
||||
it("包含 51 条合法链接且分类引用均存在", () => {
|
||||
const categoryIds = new Set(categories.map(({ id }) => id))
|
||||
expect(links).toHaveLength(45)
|
||||
expect(links).toHaveLength(51)
|
||||
expect(hasUniqueValues(links.map(({ id }) => id))).toBe(true)
|
||||
|
||||
for (const link of links) {
|
||||
@@ -115,4 +118,134 @@ describe("Polaris 静态数据", () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("归类边界回归", () => {
|
||||
const linkIdsByCategory = (categoryId: string) =>
|
||||
links
|
||||
.filter((link) => link.categoryId === categoryId)
|
||||
.map(({ id }) => id)
|
||||
.sort()
|
||||
|
||||
it("游戏维基与攻略工具归入 game-resources", () => {
|
||||
expect(linkIdsByCategory("game-resources")).toEqual(
|
||||
expect.arrayContaining([
|
||||
"wiki-52poke",
|
||||
"wiki-blhx",
|
||||
"wiki-fgo",
|
||||
"wiki-stardew",
|
||||
"wiki-terraria",
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it("娱乐媒体保留影音社区与 ACG 百科,不混入游戏资料", () => {
|
||||
const mediaIds = linkIdsByCategory("media")
|
||||
|
||||
expect(mediaIds).toEqual(
|
||||
expect.arrayContaining([
|
||||
"youtube",
|
||||
"bilibili",
|
||||
"spotify",
|
||||
"nexusmods",
|
||||
"pokemon-showdown",
|
||||
"expreview",
|
||||
"chairo",
|
||||
"wiki-moegirl",
|
||||
]),
|
||||
)
|
||||
for (const gameId of [
|
||||
"wiki-52poke",
|
||||
"wiki-blhx",
|
||||
"wiki-fgo",
|
||||
"wiki-stardew",
|
||||
"wiki-terraria",
|
||||
]) {
|
||||
expect(mediaIds).not.toContain(gameId)
|
||||
}
|
||||
})
|
||||
|
||||
it("Nexus Mods 与 Pokemon Showdown 等实测工具仍留在娱乐媒体", () => {
|
||||
const mediaIds = linkIdsByCategory("media")
|
||||
|
||||
expect(mediaIds).toContain("nexusmods")
|
||||
expect(mediaIds).toContain("pokemon-showdown")
|
||||
expect(linkIdsByCategory("game-resources")).not.toContain("nexusmods")
|
||||
expect(linkIdsByCategory("game-resources")).not.toContain(
|
||||
"pokemon-showdown",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("新增九条链接回归", () => {
|
||||
const expectedNewLinks = [
|
||||
{
|
||||
id: "kzo",
|
||||
url: "https://kzo.moe",
|
||||
categoryId: "media",
|
||||
},
|
||||
{
|
||||
id: "manhuagui",
|
||||
url: "https://www.manhuagui.com",
|
||||
categoryId: "media",
|
||||
},
|
||||
{
|
||||
id: "linovelib",
|
||||
url: "https://www.linovelib.com",
|
||||
categoryId: "media",
|
||||
},
|
||||
{
|
||||
id: "steam",
|
||||
url: "https://store.steampowered.com",
|
||||
categoryId: "media",
|
||||
},
|
||||
{
|
||||
id: "tieba",
|
||||
url: "https://tieba.baidu.com",
|
||||
categoryId: "media",
|
||||
},
|
||||
{
|
||||
id: "manga-bilibili",
|
||||
url: "https://manga.bilibili.com",
|
||||
categoryId: "media",
|
||||
},
|
||||
{
|
||||
id: "zhihu",
|
||||
url: "https://www.zhihu.com",
|
||||
categoryId: "media",
|
||||
},
|
||||
{
|
||||
id: "pixiv",
|
||||
url: "https://www.pixiv.net",
|
||||
categoryId: "design",
|
||||
},
|
||||
{
|
||||
id: "aliyun-unified-console",
|
||||
url: "https://home.console.aliyun.com",
|
||||
categoryId: "efficiency",
|
||||
},
|
||||
] as const
|
||||
|
||||
it("九条新链接全部存在且总数为 51", () => {
|
||||
expect(links).toHaveLength(51)
|
||||
expect(
|
||||
expectedNewLinks.every(({ id }) =>
|
||||
links.some((link) => link.id === id),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it.each(expectedNewLinks)(
|
||||
"$id 归入 $categoryId 且 URL 正确",
|
||||
(expected) => {
|
||||
const link = links.find(({ id }) => id === expected.id)
|
||||
|
||||
expect(link).toBeDefined()
|
||||
expect(link).toMatchObject({
|
||||
url: expected.url,
|
||||
categoryId: expected.categoryId,
|
||||
pinned: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -266,47 +266,27 @@
|
||||
"title": "神奇宝贝百科",
|
||||
"description": "宝可梦中文资料维基",
|
||||
"url": "https://wiki.52poke.com/wiki/%E4%B8%BB%E9%A1%B5",
|
||||
"categoryId": "media",
|
||||
"categoryId": "game-resources",
|
||||
"pinned": false,
|
||||
"keywords": ["宝可梦", "口袋妖怪", "攻略", "资料"],
|
||||
"sort": 80
|
||||
},
|
||||
{
|
||||
"id": "wiki-52poke-typechart",
|
||||
"title": "宝可梦属性相克表",
|
||||
"description": "宝可梦全属性克制关系速查",
|
||||
"url": "https://wiki.52poke.com/wiki/%E5%B1%9E%E6%80%A7%E7%9B%B8%E5%85%8B%E8%A1%A8",
|
||||
"categoryId": "media",
|
||||
"pinned": false,
|
||||
"keywords": ["宝可梦", "属性", "相克", "速查"],
|
||||
"sort": 90
|
||||
},
|
||||
{
|
||||
"id": "wiki-blhx",
|
||||
"title": "碧蓝航线WIKI",
|
||||
"description": "碧蓝航线官方中文百科",
|
||||
"url": "https://wiki.biligame.com/blhx/%E9%A6%96%E9%A1%B5",
|
||||
"categoryId": "media",
|
||||
"categoryId": "game-resources",
|
||||
"pinned": false,
|
||||
"keywords": ["碧蓝航线", "手游", "攻略", "wiki"],
|
||||
"sort": 100
|
||||
},
|
||||
{
|
||||
"id": "wiki-blhx-boss",
|
||||
"title": "碧蓝航线月度BOSS解析",
|
||||
"description": "大型作战月度 BOSS 攻略解析",
|
||||
"url": "https://wiki.biligame.com/blhx/%E5%A4%A7%E5%9E%8B%E4%BD%9C%E6%88%98%E6%9C%88%E5%BA%A6BOSS%E8%A7%A3%E6%9E%90",
|
||||
"categoryId": "media",
|
||||
"pinned": false,
|
||||
"keywords": ["碧蓝航线", "boss", "攻略", "大型作战"],
|
||||
"sort": 110
|
||||
},
|
||||
{
|
||||
"id": "wiki-fgo",
|
||||
"title": "Mooncell",
|
||||
"description": "FGO 中文维基与攻略资料站",
|
||||
"url": "https://fgo.wiki/w/%E9%A6%96%E9%A1%B5",
|
||||
"categoryId": "media",
|
||||
"categoryId": "game-resources",
|
||||
"pinned": false,
|
||||
"keywords": ["fgo", "mooncell", "从者", "攻略"],
|
||||
"sort": 120
|
||||
@@ -316,7 +296,7 @@
|
||||
"title": "星露谷物语中文维基",
|
||||
"description": "星露谷物语官方中文资料维基",
|
||||
"url": "https://zh.stardewvalleywiki.com",
|
||||
"categoryId": "media",
|
||||
"categoryId": "game-resources",
|
||||
"pinned": false,
|
||||
"keywords": ["星露谷物语", "攻略", "作物", "wiki"],
|
||||
"sort": 130
|
||||
@@ -326,7 +306,7 @@
|
||||
"title": "泰拉瑞亚中文维基",
|
||||
"description": "泰拉瑞亚官方中文百科",
|
||||
"url": "https://terraria.wiki.gg/zh",
|
||||
"categoryId": "media",
|
||||
"categoryId": "game-resources",
|
||||
"pinned": false,
|
||||
"keywords": ["泰拉瑞亚", "攻略", "物品", "wiki"],
|
||||
"sort": 140
|
||||
@@ -341,16 +321,6 @@
|
||||
"keywords": ["动漫", "acg", "百科", "二次元"],
|
||||
"sort": 150
|
||||
},
|
||||
{
|
||||
"id": "farmgamehub",
|
||||
"title": "星露谷洒水器规划器",
|
||||
"description": "星露谷物语洒水器布局可视化工具",
|
||||
"url": "https://www.farmgamehub.com/zh/tools/stardew-sprinklers",
|
||||
"categoryId": "media",
|
||||
"pinned": false,
|
||||
"keywords": ["星露谷物语", "洒水器", "布局", "工具"],
|
||||
"sort": 160
|
||||
},
|
||||
{
|
||||
"id": "iodraw",
|
||||
"title": "iodraw",
|
||||
@@ -406,7 +376,8 @@
|
||||
"id": "aliyun-console",
|
||||
"title": "阿里云轻量控制台",
|
||||
"description": "阿里云轻量应用服务器控制台",
|
||||
"url": "https://swas.console.aliyun.com",
|
||||
"url": "https://console.aliyun.com",
|
||||
"icon": "local:ali-cloud",
|
||||
"categoryId": "efficiency",
|
||||
"pinned": false,
|
||||
"keywords": ["阿里云", "控制台", "轻量服务器", "云服务"],
|
||||
@@ -472,5 +443,96 @@
|
||||
"pinned": false,
|
||||
"keywords": ["地图", "导航", "出行", "路线"],
|
||||
"sort": 10
|
||||
},
|
||||
{
|
||||
"id": "kzo",
|
||||
"title": "Kmoe 漫画",
|
||||
"description": "Kindle/EPUB 漫画下载与推送",
|
||||
"url": "https://kzo.moe",
|
||||
"categoryId": "media",
|
||||
"pinned": false,
|
||||
"keywords": ["漫画", "kindle", "epub", "推送"],
|
||||
"sort": 160
|
||||
},
|
||||
{
|
||||
"id": "manhuagui",
|
||||
"title": "漫画柜",
|
||||
"description": "在线漫画阅读站点",
|
||||
"url": "https://www.manhuagui.com",
|
||||
"categoryId": "media",
|
||||
"pinned": false,
|
||||
"keywords": ["漫画", "在线阅读", "动漫"],
|
||||
"sort": 170
|
||||
},
|
||||
{
|
||||
"id": "linovelib",
|
||||
"title": "轻小说文库",
|
||||
"description": "在线轻小说阅读站点",
|
||||
"url": "https://www.linovelib.com",
|
||||
"categoryId": "media",
|
||||
"pinned": false,
|
||||
"keywords": ["轻小说", "在线阅读", "文库"],
|
||||
"sort": 180
|
||||
},
|
||||
{
|
||||
"id": "steam",
|
||||
"title": "Steam",
|
||||
"description": "PC 游戏数字发行商店",
|
||||
"url": "https://store.steampowered.com",
|
||||
"categoryId": "media",
|
||||
"pinned": false,
|
||||
"keywords": ["游戏", "商店", "平台"],
|
||||
"sort": 190
|
||||
},
|
||||
{
|
||||
"id": "tieba",
|
||||
"title": "百度贴吧",
|
||||
"description": "百度旗下主题兴趣社区",
|
||||
"url": "https://tieba.baidu.com",
|
||||
"categoryId": "media",
|
||||
"pinned": false,
|
||||
"keywords": ["贴吧", "论坛", "社区", "百度"],
|
||||
"sort": 200
|
||||
},
|
||||
{
|
||||
"id": "manga-bilibili",
|
||||
"title": "哔哩哔哩漫画",
|
||||
"description": "哔哩哔哩正版漫画平台",
|
||||
"url": "https://manga.bilibili.com",
|
||||
"categoryId": "media",
|
||||
"pinned": false,
|
||||
"keywords": ["漫画", "哔哩哔哩", "正版", "阅读"],
|
||||
"sort": 210
|
||||
},
|
||||
{
|
||||
"id": "zhihu",
|
||||
"title": "知乎",
|
||||
"description": "中文问答社区",
|
||||
"url": "https://www.zhihu.com",
|
||||
"categoryId": "media",
|
||||
"pinned": false,
|
||||
"keywords": ["问答", "社区", "知识"],
|
||||
"sort": 220
|
||||
},
|
||||
{
|
||||
"id": "pixiv",
|
||||
"title": "pixiv",
|
||||
"description": "插画作品分享社区",
|
||||
"url": "https://www.pixiv.net",
|
||||
"categoryId": "design",
|
||||
"pinned": false,
|
||||
"keywords": ["插画", "绘画", "灵感", "二次元"],
|
||||
"sort": 40
|
||||
},
|
||||
{
|
||||
"id": "aliyun-unified-console",
|
||||
"title": "阿里云统一控制台",
|
||||
"description": "阿里云产品统一管理控制台",
|
||||
"url": "https://home.console.aliyun.com",
|
||||
"icon": "local:ali-cloud",
|
||||
"categoryId": "efficiency",
|
||||
"pinned": false,
|
||||
"keywords": ["阿里云", "控制台", "云服务", "管理"],
|
||||
"sort": 100
|
||||
}
|
||||
]
|
||||
|
||||
@@ -72,6 +72,7 @@ describe("CategoryContent", () => {
|
||||
"learning",
|
||||
"efficiency",
|
||||
"media",
|
||||
"game-resources",
|
||||
"life",
|
||||
] as const)("%s 分类展示服务提供的数量", (categoryId) => {
|
||||
const category = getCategoryById(categoryId)
|
||||
@@ -82,4 +83,24 @@ describe("CategoryContent", () => {
|
||||
|
||||
expect(screen.getByText(`共 ${links.length} 个`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("归类边界:游戏资料面板只渲染 5 个游戏维基卡片", () => {
|
||||
const category = getCategoryById("game-resources")
|
||||
if (!category) throw new Error("测试数据缺少 game-resources 分类")
|
||||
const links = getLinksByCategory(category.id)
|
||||
|
||||
render(<CategoryContent category={category} links={links} />)
|
||||
|
||||
expect(links).toHaveLength(5)
|
||||
const toolLinks = screen
|
||||
.getByRole("list", { name: "工具列表" })
|
||||
.querySelectorAll("a")
|
||||
expect(toolLinks).toHaveLength(5)
|
||||
expect(
|
||||
screen.queryByRole("link", { name: /萌娘百科/ }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole("link", { name: /Nexus Mods/ }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
import type { Category } from "@/types/navigation"
|
||||
@@ -9,9 +10,18 @@ const categories: Category[] = [
|
||||
{ id: "dev", title: "开发工具", description: "开发", icon: "code", sort: 10 },
|
||||
]
|
||||
|
||||
class ResizeObserverStub {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
describe("AppShell", () => {
|
||||
beforeEach(() => window.localStorage.clear())
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it("切换侧栏并持久化状态", () => {
|
||||
render(
|
||||
@@ -198,4 +208,67 @@ describe("AppShell", () => {
|
||||
"page",
|
||||
)
|
||||
})
|
||||
|
||||
it("展开时分类文字可见,悬停或聚焦不显示提示", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<AppShell
|
||||
categories={categories}
|
||||
siteName="Polaris"
|
||||
defaultCollapsed={false}
|
||||
>
|
||||
<main>首页内容</main>
|
||||
</AppShell>,
|
||||
)
|
||||
|
||||
const link = screen.getByRole("link", { name: "开发工具" })
|
||||
expect(link.querySelector(".sidebar__label")).not.toHaveClass("sr-only")
|
||||
await user.hover(link)
|
||||
expect(link).not.toHaveAttribute("data-state")
|
||||
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument()
|
||||
|
||||
link.focus()
|
||||
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("收起时悬停分类图标显示分类名提示", async () => {
|
||||
vi.stubGlobal("ResizeObserver", ResizeObserverStub)
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<AppShell
|
||||
categories={categories}
|
||||
siteName="Polaris"
|
||||
defaultCollapsed={true}
|
||||
>
|
||||
<main>首页内容</main>
|
||||
</AppShell>,
|
||||
)
|
||||
|
||||
const link = screen.getByRole("link", { name: "开发工具" })
|
||||
expect(link.querySelector(".sidebar__label")).toHaveClass("sr-only")
|
||||
await user.hover(link)
|
||||
expect(await screen.findByRole("tooltip")).toHaveTextContent("开发工具")
|
||||
})
|
||||
|
||||
it("收起时键盘聚焦分类图标显示分类名提示", async () => {
|
||||
vi.stubGlobal("ResizeObserver", ResizeObserverStub)
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<AppShell
|
||||
categories={categories}
|
||||
siteName="Polaris"
|
||||
defaultCollapsed={true}
|
||||
>
|
||||
<main>首页内容</main>
|
||||
</AppShell>,
|
||||
)
|
||||
|
||||
await user.tab()
|
||||
await user.tab()
|
||||
await user.tab()
|
||||
await user.tab()
|
||||
const link = screen.getByRole("link", { name: "开发工具" })
|
||||
expect(link).toHaveFocus()
|
||||
expect(await screen.findByRole("tooltip")).toHaveTextContent("开发工具")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -81,28 +81,30 @@ export function Sidebar({
|
||||
<div className="sidebar__categories">
|
||||
{categories.map((category) => {
|
||||
const isActive = category.id === activeCategoryId
|
||||
const categoryLink = (
|
||||
<Link
|
||||
key={category.id}
|
||||
className={cn(
|
||||
"sidebar__nav-item",
|
||||
isActive && "sidebar__nav-item--active",
|
||||
)}
|
||||
href={`/categories/${category.id}`}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
>
|
||||
<CategoryIcon name={category.icon} />
|
||||
<span className={cn("sidebar__label", collapsed && "sr-only")}>
|
||||
{category.title}
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
|
||||
return (
|
||||
return collapsed ? (
|
||||
<Tooltip key={category.id}>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
className={cn(
|
||||
"sidebar__nav-item",
|
||||
isActive && "sidebar__nav-item--active",
|
||||
)}
|
||||
href={`/categories/${category.id}`}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
>
|
||||
<CategoryIcon name={category.icon} />
|
||||
<span
|
||||
className={cn("sidebar__label", collapsed && "sr-only")}
|
||||
>
|
||||
{category.title}
|
||||
</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger asChild>{categoryLink}</TooltipTrigger>
|
||||
<TooltipContent side="right">{category.title}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
categoryLink
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ describe("静态数据 service", () => {
|
||||
"learning",
|
||||
"efficiency",
|
||||
"media",
|
||||
"game-resources",
|
||||
"life",
|
||||
])
|
||||
expect(first).not.toBe(second)
|
||||
@@ -55,6 +56,7 @@ describe("静态数据 service", () => {
|
||||
"iconfont",
|
||||
"haowallpaper",
|
||||
"iodraw",
|
||||
"pixiv",
|
||||
])
|
||||
expect(getLinksByCategory("life").map(({ id }) => id)).toEqual(["amap"])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user