feat: 首页对齐 main SPA 草绿壳与 Feed v2

顶栏搜索胶囊/发帖绿钮/Ctrl+K,侧栏板块色槽,列表 post-row--v2 与 SPA 同密度。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 06:06:04 +08:00
parent 6e7b46a2e9
commit 2ba010f3ea
11 changed files with 1549 additions and 258 deletions

View File

@@ -10,8 +10,8 @@
| 项 | 值 | | 项 | 值 |
|----|-----| |----|-----|
| **上一刀** | 首页门面精修Feed 头像/摘要/互动 + 高级搜索收起 + 侧栏分组 | | **上一刀** | 首页对齐 main SPA 视觉与交互(草绿壳 + Feed v2 |
| **下一刀** | 需点名(移动侧栏抽屉 / 帖详情抛光 / OIDC Admin / S3…) | | **下一刀** | 需点名(移动抽屉 / 帖详情抛光 / OIDC Admin… |
| **工作区** | 应干净 | | **工作区** | 应干净 |
--- ---
@@ -32,7 +32,7 @@
| Markdown 工具栏 + preview | 已迁 | | Markdown 工具栏 + preview | 已迁 |
| 浅色 / 暗色 / 跟随系统 | 已迁CSS 媒体查询,无 head 防闪脚本) | | 浅色 / 暗色 / 跟随系统 | 已迁CSS 媒体查询,无 head 防闪脚本) |
| 未挂载论坛 JSON API | 已删(对照 `main` | | 未挂载论坛 JSON API | 已删(对照 `main` |
| 公开页设计系统 | 已迁(清青 chrome + `?v=` + 首页 Feed 门面密度 | | 公开页设计系统 | 已迁(首页对齐 main SPA 草绿壳;[10-design-system.md](10-design-system.md) |
--- ---

View File

@@ -3,7 +3,7 @@
> **读者**:实现公开页视觉 / CSS 的 AI > **读者**:实现公开页视觉 / CSS 的 AI
> **前置**[01-product.md](01-product.md)、[06-pages-ux.md](06-pages-ux.md) > **前置**[01-product.md](01-product.md)、[06-pages-ux.md](06-pages-ux.md)
> **实现**[`web_src/css/site.css`](../../web_src/css/site.css)、[`templates/base/`](../../templates/base/) > **实现**[`web_src/css/site.css`](../../web_src/css/site.css)、[`templates/base/`](../../templates/base/)
> **品牌**:默认「姜十三论坛」(安装/后台可改 `site_name`);强调色取 **青**(纪念向,非 SPA 草绿 > **品牌**:默认「姜十三论坛」;首页视觉对齐 `main` SPA 草绿产品壳
视觉可重设;**信息架构与关键操作流以 06 为准**。本文件只约定公开页壳层与内容表面的视觉语言。 视觉可重设;**信息架构与关键操作流以 06 为准**。本文件只约定公开页壳层与内容表面的视觉语言。
@@ -15,9 +15,9 @@
| 项 | 约定 | | 项 | 约定 |
|----|------| |----|------|
| 关键词 | 清青、产品感、冷静、可读、偏内容 | | 关键词 | 对齐 `main` SPA草绿 `#18a058`、清新产品壳 |
| 密度 | 接近 V2EX / NGA同时顶栏对齐 SPA 式工具条 | | 密度 | SPA 同款顶栏工具条 + Feed `post-row--v2` |
| 反模式 | 紫渐变、暖奶油底、大圆角卡片墙、全幅 hero、Inter/Roboto、顶栏链接墙、依赖本机 IBM Plex | | 反模式 | 紫渐变、暖奶油底、大圆角卡片墙、全幅 hero、Inter/Roboto |
--- ---
@@ -27,16 +27,12 @@
| Token | 浅色 | 暗色 | 用途 | | Token | 浅色 | 暗色 | 用途 |
|-------|------|------|------| |-------|------|------|------|
| `--j13-bg` | `#f4f8fa` | `#0c1216` | 页底(微青灰) | | `--j13-bg` / page | `#f5f7fa` | `#141416` | 页底 |
| `--j13-surface` | `#ffffff` | `#152028` | 顶栏、主栏 | | `--j13-surface` | `#ffffff` | `#1f1f23` | 顶栏、主栏 |
| `--j13-text` | `#14212b` | `#e8eef2` | 正文 | | `--j13-accent` / green | `#18a058` | `#23c36b` | 主色(对齐 main SPA |
| `--j13-muted` | `#5a6b75` | `#8b9aab` | 次要 | | `--j13-accent-ink` | `#138f4c` | `#6ee7a0` | 强强调 |
| `--j13-accent` | `#0e8a9a` | `#2ec4d6` | 链接 / 主按钮 / 分段 active | | `--j13-soft` / green-bg | `#edfbf3` / `rgba(24,160,88,.08)` | 绿 soft | active / hover |
| `--j13-accent-ink` | `#0b5f6b` | `#9ae8f0` | 品牌字、强强调 | | `--j13-border` | `#e8edf2` | `#2e2e32` | 分隔 |
| `--j13-soft` | `#e6f7f9` | `#12343a` | 侧栏 active 底、focus ring |
| `--j13-on-accent` | `#ffffff` | `#0b5f6b` | 主按钮字 |
| `--j13-border` | `#d3e0e6` | `#2a3a44` | 分隔 |
| `--j13-hover` | `#e8f4f6` | `#1c2d36` | 行 hover |
语义色ok / warn / err / pin / feat随主题成对定义feat 用青 soft不用草绿。 语义色ok / warn / err / pin / feat随主题成对定义feat 用青 soft不用草绿。
@@ -73,7 +69,7 @@
| 断点 | 行为 | | 断点 | 行为 |
|------|------| |------|------|
| ≥901px | 三栏:左 ~200px / 中 1fr / 右 ~260px总宽 ~1320px | | ≥901px | 三栏:左 210px / 中 1fr / 右 280px总宽 ~1400px(对齐 SPA |
| ≤900px | 隐藏右栏;顶栏搜索折行(抽屉化 **下一刀** | | ≤900px | 隐藏右栏;顶栏搜索折行(抽屉化 **下一刀** |
--- ---
@@ -82,12 +78,11 @@
| 组件 | 规则 | | 组件 | 规则 |
|------|------| |------|------|
| 顶栏 | 品牌 \| 搜索条 \|「+ 发帖」主按钮 + 主题 + 私信 + 用户 +「更多」;告别链接墙 | | 顶栏 | SPA品牌 \| 搜索胶囊(筛选钮 + Ctrl+K\| 发帖绿钮 \| 主题/私信/头像菜单 |
| 左栏 | 分区「浏览 / 板块active = soft 底 + 左边青条 | | 左栏 | 浏览 / 板块active 绿底;板块色槽 + 帖数 |
| Feed 行 | 头像 · 作者+相对时间 / 标题 / 摘要(最多 2 行)/ 标签 · 右栏回复/赞/阅读;细线分隔非卡片墙 | | Feed 行 | `post-row--v2`:头像 · 徽章+标题 · 摘要(依 feed_list_style· 元信息 · 回复数 |
| 高级搜索 | `<details>` 默认收起;有筛选条件或错误时展开;顶栏搜索仍 `/?keyword=` | | 高级搜索 | 顶栏筛选打开;有条件时 details 展开 |
| 排序 | 分段控件 | | 排序 | SPA `feed-sort-tab` 绿 active |
| 标签 / 按钮 / 评论 / 门控 | 同青 token |
--- ---

View File

@@ -1,16 +1,16 @@
/* 姜十三论坛公开页样式 — 对齐 docs/rebuild-spec/10-design-system.md清青产品) */ /* 姜十三论坛公开页样式 — 对齐 main SPA 首页视觉(草绿产品 */
:root { :root {
color-scheme: light; color-scheme: light;
--j13-bg: #f4f8fa; --j13-bg: #f5f7fa;
--j13-surface: #ffffff; --j13-surface: #ffffff;
--j13-text: #14212b; --j13-text: #14212b;
--j13-muted: #5a6b75; --j13-muted: #5a6b75;
--j13-accent: #0e8a9a; --j13-accent: #18a058;
--j13-accent-ink: #0b5f6b; --j13-accent-ink: #138f4c;
--j13-on-accent: #ffffff; --j13-on-accent: #ffffff;
--j13-soft: #e6f7f9; --j13-soft: #edfbf3;
--j13-border: #d3e0e6; --j13-border: #e8edf2;
--j13-hover: #e8f4f6; --j13-hover: #f0faf5;
--j13-radius: 8px; --j13-radius: 8px;
--j13-font: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", "Noto Sans SC", "Segoe UI", system-ui, sans-serif; --j13-font: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", "Noto Sans SC", "Segoe UI", system-ui, sans-serif;
--j13-font-mono: "Cascadia Code", "Consolas", "SF Mono", ui-monospace, monospace; --j13-font-mono: "Cascadia Code", "Consolas", "SF Mono", ui-monospace, monospace;
@@ -25,34 +25,34 @@
--j13-err-border: #fecaca; --j13-err-border: #fecaca;
--j13-pin-bg: #fef3c7; --j13-pin-bg: #fef3c7;
--j13-pin-fg: #92400e; --j13-pin-fg: #92400e;
--j13-feat-bg: #e6f7f9; --j13-feat-bg: #edfbf3;
--j13-feat-fg: #0b5f6b; --j13-feat-fg: #138f4c;
--j13-neutral-bg: #eef2f6; --j13-neutral-bg: #eef2f6;
--j13-neutral-fg: #475569; --j13-neutral-fg: #475569;
--j13-self-bg: #e6f7f9; --j13-self-bg: #edfbf3;
--j13-self-border: #7dd3de; --j13-self-border: #86efac;
--j13-delta-pos: #15803d; --j13-delta-pos: #15803d;
--j13-delta-neg: #b91c1c; --j13-delta-neg: #b91c1c;
--j13-overlay: rgba(244, 248, 250, 0.92); --j13-overlay: rgba(244, 248, 250, 0.92);
--j13-header-shadow: 0 1px 0 var(--j13-border), 0 6px 18px rgba(11, 95, 107, 0.06); --j13-header-shadow: 0 1px 0 var(--j13-border), 0 6px 18px rgba(11, 95, 107, 0.06);
--j13-ease: 150ms ease; --j13-ease: 150ms ease;
--j13-frame: 1320px; --j13-frame: 1400px;
--j13-header-h: 3.5rem; --j13-header-h: 3.5rem;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
color-scheme: dark; color-scheme: dark;
--j13-bg: #0c1216; --j13-bg: #141416;
--j13-surface: #152028; --j13-surface: #1f1f23;
--j13-text: #e8eef2; --j13-text: #e8eef2;
--j13-muted: #8b9aab; --j13-muted: #8b9aab;
--j13-accent: #2ec4d6; --j13-accent: #23c36b;
--j13-accent-ink: #9ae8f0; --j13-accent-ink: #6ee7a0;
--j13-on-accent: #0b5f6b; --j13-on-accent: #138f4c;
--j13-soft: #12343a; --j13-soft: rgba(35, 195, 107, 0.08);
--j13-border: #2a3a44; --j13-border: #2e2e32;
--j13-hover: #1c2d36; --j13-hover: #232328;
--j13-danger: #f87171; --j13-danger: #f87171;
--j13-ok-bg: #064e3b; --j13-ok-bg: #064e3b;
--j13-ok-fg: #a7f3d0; --j13-ok-fg: #a7f3d0;
@@ -64,12 +64,12 @@
--j13-err-border: #991b1b; --j13-err-border: #991b1b;
--j13-pin-bg: #78350f; --j13-pin-bg: #78350f;
--j13-pin-fg: #fde68a; --j13-pin-fg: #fde68a;
--j13-feat-bg: #0b5f6b; --j13-feat-bg: #138f4c;
--j13-feat-fg: #9ae8f0; --j13-feat-fg: #6ee7a0;
--j13-neutral-bg: #243040; --j13-neutral-bg: #243040;
--j13-neutral-fg: #cbd5e1; --j13-neutral-fg: #cbd5e1;
--j13-self-bg: #0b5f6b; --j13-self-bg: #138f4c;
--j13-self-border: #0e8a9a; --j13-self-border: #18a058;
--j13-delta-pos: #4ade80; --j13-delta-pos: #4ade80;
--j13-delta-neg: #f87171; --j13-delta-neg: #f87171;
--j13-overlay: rgba(12, 18, 22, 0.9); --j13-overlay: rgba(12, 18, 22, 0.9);
@@ -79,16 +79,16 @@
html[data-theme="light"] { html[data-theme="light"] {
color-scheme: light; color-scheme: light;
--j13-bg: #f4f8fa; --j13-bg: #f5f7fa;
--j13-surface: #ffffff; --j13-surface: #ffffff;
--j13-text: #14212b; --j13-text: #14212b;
--j13-muted: #5a6b75; --j13-muted: #5a6b75;
--j13-accent: #0e8a9a; --j13-accent: #18a058;
--j13-accent-ink: #0b5f6b; --j13-accent-ink: #138f4c;
--j13-on-accent: #ffffff; --j13-on-accent: #ffffff;
--j13-soft: #e6f7f9; --j13-soft: #edfbf3;
--j13-border: #d3e0e6; --j13-border: #e8edf2;
--j13-hover: #e8f4f6; --j13-hover: #f0faf5;
--j13-danger: #b91c1c; --j13-danger: #b91c1c;
--j13-ok-bg: #ecfdf5; --j13-ok-bg: #ecfdf5;
--j13-ok-fg: #065f46; --j13-ok-fg: #065f46;
@@ -100,12 +100,12 @@ html[data-theme="light"] {
--j13-err-border: #fecaca; --j13-err-border: #fecaca;
--j13-pin-bg: #fef3c7; --j13-pin-bg: #fef3c7;
--j13-pin-fg: #92400e; --j13-pin-fg: #92400e;
--j13-feat-bg: #e6f7f9; --j13-feat-bg: #edfbf3;
--j13-feat-fg: #0b5f6b; --j13-feat-fg: #138f4c;
--j13-neutral-bg: #eef2f6; --j13-neutral-bg: #eef2f6;
--j13-neutral-fg: #475569; --j13-neutral-fg: #475569;
--j13-self-bg: #e6f7f9; --j13-self-bg: #edfbf3;
--j13-self-border: #7dd3de; --j13-self-border: #86efac;
--j13-delta-pos: #15803d; --j13-delta-pos: #15803d;
--j13-delta-neg: #b91c1c; --j13-delta-neg: #b91c1c;
--j13-overlay: rgba(244, 248, 250, 0.92); --j13-overlay: rgba(244, 248, 250, 0.92);
@@ -114,16 +114,16 @@ html[data-theme="light"] {
html[data-theme="dark"] { html[data-theme="dark"] {
color-scheme: dark; color-scheme: dark;
--j13-bg: #0c1216; --j13-bg: #141416;
--j13-surface: #152028; --j13-surface: #1f1f23;
--j13-text: #e8eef2; --j13-text: #e8eef2;
--j13-muted: #8b9aab; --j13-muted: #8b9aab;
--j13-accent: #2ec4d6; --j13-accent: #23c36b;
--j13-accent-ink: #9ae8f0; --j13-accent-ink: #6ee7a0;
--j13-on-accent: #0b5f6b; --j13-on-accent: #138f4c;
--j13-soft: #12343a; --j13-soft: rgba(35, 195, 107, 0.08);
--j13-border: #2a3a44; --j13-border: #2e2e32;
--j13-hover: #1c2d36; --j13-hover: #232328;
--j13-danger: #f87171; --j13-danger: #f87171;
--j13-ok-bg: #064e3b; --j13-ok-bg: #064e3b;
--j13-ok-fg: #a7f3d0; --j13-ok-fg: #a7f3d0;
@@ -135,12 +135,12 @@ html[data-theme="dark"] {
--j13-err-border: #991b1b; --j13-err-border: #991b1b;
--j13-pin-bg: #78350f; --j13-pin-bg: #78350f;
--j13-pin-fg: #fde68a; --j13-pin-fg: #fde68a;
--j13-feat-bg: #0b5f6b; --j13-feat-bg: #138f4c;
--j13-feat-fg: #9ae8f0; --j13-feat-fg: #6ee7a0;
--j13-neutral-bg: #243040; --j13-neutral-bg: #243040;
--j13-neutral-fg: #cbd5e1; --j13-neutral-fg: #cbd5e1;
--j13-self-bg: #0b5f6b; --j13-self-bg: #138f4c;
--j13-self-border: #0e8a9a; --j13-self-border: #18a058;
--j13-delta-pos: #4ade80; --j13-delta-pos: #4ade80;
--j13-delta-neg: #f87171; --j13-delta-neg: #f87171;
--j13-overlay: rgba(12, 18, 22, 0.9); --j13-overlay: rgba(12, 18, 22, 0.9);
@@ -219,11 +219,11 @@ code, pre, .j13-md-editor__input {
letter-spacing: -0.02em; letter-spacing: -0.02em;
} }
html[data-theme="dark"] .j13-brand__mark { html[data-theme="dark"] .j13-brand__mark {
color: #0c1216; color: #141416;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
html:not([data-theme="light"]) .j13-brand__mark { html:not([data-theme="light"]) .j13-brand__mark {
color: #0c1216; color: #141416;
} }
} }
.j13-brand__logo { .j13-brand__logo {
@@ -1792,3 +1792,567 @@ points-only[data-locked="false"] {
} }
.j13-rev-item a:hover strong { color: var(--j13-accent); } .j13-rev-item a:hover strong { color: var(--j13-accent); }
.j13-rev-snapshot { margin-top: 1rem; } .j13-rev-snapshot { margin-top: 1rem; }
/* —— main SPA 首页壳层对齐覆盖 —— */
:root {
--j13-green: #18a058;
--j13-green-hover: #138f4c;
--j13-green-bg: rgba(24, 160, 88, 0.08);
--j13-green-soft: #edfbf3;
--j13-border-light: #f0f3f7;
--j13-sidebar-w: 210px;
--j13-aside-w: 280px;
--j13-header-h: 56px;
--j13-max-w: 1400px;
--j13-bg-page: #f5f7fa;
--j13-bg-block-muted: #f1f4f8;
--j13-bg-block-accent: #f0faf5;
--board-0-color: #18a058; --board-0-bg: rgba(24,160,88,.1);
--board-1-color: #3498db; --board-1-bg: rgba(52,152,219,.1);
--board-2-color: #9b59b6; --board-2-bg: rgba(155,89,182,.1);
--board-3-color: #e67e22; --board-3-bg: rgba(230,126,34,.1);
--board-4-color: #e74c3c; --board-4-bg: rgba(231,76,60,.1);
--board-5-color: #1abc9c; --board-5-bg: rgba(26,188,156,.1);
--board-6-color: #6366f1; --board-6-bg: rgba(99,102,241,.1);
--board-7-color: #f59e0b; --board-7-bg: rgba(245,158,11,.12);
--j13-frame: var(--j13-max-w);
}
html[data-theme="dark"] {
--j13-green: #23c36b;
--j13-green-hover: #1fb35f;
--j13-green-bg: rgba(35,195,107,.12);
--j13-green-soft: rgba(35,195,107,.08);
--j13-border-light: #262629;
--j13-bg-page: #141416;
--j13-bg-block-muted: #1c1c20;
--j13-bg-block-accent: rgba(35,195,107,.06);
}
@media (prefers-color-scheme: dark) {
html:not([data-theme="light"]) {
--j13-green: #23c36b;
--j13-green-hover: #1fb35f;
--j13-green-bg: rgba(35,195,107,.12);
--j13-green-soft: rgba(35,195,107,.08);
--j13-border-light: #262629;
--j13-bg-page: #141416;
--j13-bg-block-muted: #1c1c20;
--j13-bg-block-accent: rgba(35,195,107,.06);
}
}
body.j13-body {
background:
radial-gradient(ellipse 70% 45% at 50% -10%, rgba(24, 160, 88, 0.07), transparent 70%),
var(--j13-bg-page);
font-size: 14px;
}
html[data-theme="dark"] body.j13-body {
background:
radial-gradient(ellipse 70% 45% at 50% -10%, rgba(35, 195, 107, 0.08), transparent 70%),
var(--j13-bg-page);
}
@media (prefers-color-scheme: dark) {
html:not([data-theme="light"]) body.j13-body {
background:
radial-gradient(ellipse 70% 45% at 50% -10%, rgba(35, 195, 107, 0.08), transparent 70%),
var(--j13-bg-page);
}
}
.j13-header.app-header {
height: var(--j13-header-h);
box-shadow: none;
}
.j13-header__inner.header-inner {
max-width: var(--j13-max-w);
height: 100%;
min-height: var(--j13-header-h);
padding: 0 20px;
display: flex;
grid-template-columns: none;
gap: 20px;
}
.header-brand { gap: 10px; }
.header-logo-mark,
.j13-brand__mark.header-logo-mark {
width: 34px;
height: 34px;
border-radius: 10px;
background: linear-gradient(135deg, var(--j13-green) 0%, #34c471 100%);
color: #fff;
font-size: 17px;
}
.header-logo-text {
font-size: 15px;
font-weight: 600;
color: var(--j13-text);
letter-spacing: 0.02em;
}
.header-brand:hover .header-logo-text { color: var(--j13-green); }
.j13-brand__text { display: none; }
.header-search-wrap {
flex: 1;
min-width: 0;
max-width: 420px;
margin: 0;
display: flex;
align-items: center;
min-height: 36px;
padding: 0 10px 0 12px;
border: 1px solid var(--j13-border);
border-radius: 999px;
background: var(--j13-bg-block-muted);
transition: border-color var(--j13-ease), box-shadow var(--j13-ease), background var(--j13-ease);
}
.header-search-wrap:focus-within {
background: var(--j13-surface);
border-color: color-mix(in srgb, var(--j13-green) 30%, transparent);
box-shadow: 0 0 0 3px var(--j13-green-bg);
}
.header-search-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.header-search-icon { color: var(--j13-muted); flex-shrink: 0; }
.header-search-input {
flex: 1;
min-width: 0;
border: 0;
outline: none;
background: transparent;
font: inherit;
font-size: 14px;
color: var(--j13-text);
padding: 0;
max-width: none;
margin: 0;
border-radius: 0;
}
.header-search-filter-btn {
flex-shrink: 0;
width: 28px;
height: 28px;
border: 0;
border-radius: 999px;
background: transparent;
color: var(--j13-muted);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}
.header-search-filter-btn:hover,
.header-search-filter-btn--active {
background: var(--j13-green-bg);
color: var(--j13-green);
}
.header-search-kbd {
display: none;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid var(--j13-border-light);
background: var(--j13-surface);
font-size: 10px;
color: var(--j13-muted);
font-family: inherit;
}
@media (min-width: 1100px) {
.header-search-kbd { display: inline-flex; }
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
margin-left: auto;
flex-shrink: 0;
}
.header-compose-btn {
display: inline-flex;
align-items: center;
gap: 6px;
height: 34px;
padding: 0 16px;
border: none;
border-radius: 999px;
background: var(--j13-green);
color: #fff;
font: inherit;
font-size: 13px;
font-weight: 600;
text-decoration: none;
box-shadow: 0 2px 8px rgba(26, 127, 75, 0.22);
transition: background var(--j13-ease), box-shadow var(--j13-ease), transform var(--j13-ease);
}
.header-compose-btn:hover {
background: var(--j13-green-hover);
color: #fff;
text-decoration: none;
box-shadow: 0 3px 10px rgba(26, 127, 75, 0.3);
}
.header-action-group {
display: flex;
align-items: center;
gap: 4px;
padding: 3px;
border-radius: 999px;
background: var(--j13-bg-block-muted);
border: 1px solid var(--j13-border-light);
}
.header-icon-btn {
position: relative;
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: transparent;
color: var(--j13-muted);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
padding: 0;
}
.header-icon-btn:hover {
background: var(--j13-hover);
color: var(--j13-text);
}
.header-icon-btn .j13-badge {
position: absolute;
top: -2px;
right: -2px;
font-size: 10px;
min-width: 1rem;
padding: 0 4px;
}
.header-user-btn {
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
padding: 0;
cursor: pointer;
overflow: hidden;
background: var(--j13-green);
list-style: none;
display: grid;
place-items: center;
}
.header-user-btn::-webkit-details-marker { display: none; }
.header-user-avatar { width: 100%; height: 100%; object-fit: cover; }
.header-user-initial { color: #fff; font-size: 14px; font-weight: 600; }
.header-login-btn {
height: 32px;
padding: 0 14px;
border-radius: 999px;
color: var(--j13-green);
font-size: 13px;
font-weight: 500;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.header-login-btn:hover { background: var(--j13-green-bg); }
.header-user-menu > summary { list-style: none; }
.j13-theme-toggle.header-icon-btn {
border: 0;
border-radius: 50%;
padding: 0;
width: 32px;
height: 32px;
}
.j13-layout {
max-width: var(--j13-max-w);
grid-template-columns: var(--j13-sidebar-w) minmax(0, 1fr) var(--j13-aside-w);
gap: 0;
padding: 0;
background: var(--j13-surface);
border-left: 1px solid var(--j13-border-light);
border-right: 1px solid var(--j13-border-light);
min-height: calc(100vh - var(--j13-header-h) - 4rem);
}
.j13-aside--left.sidebar {
width: var(--j13-sidebar-w);
border-right: 1px solid var(--j13-border-light);
padding: 12px 0;
position: sticky;
top: var(--j13-header-h);
max-height: calc(100vh - var(--j13-header-h));
overflow-y: auto;
}
.j13-aside--right {
width: var(--j13-aside-w);
border-left: 1px solid var(--j13-border-light);
padding: 12px;
gap: 10px;
}
.j13-main {
border: 0;
border-radius: 0;
padding: 0;
background: var(--j13-surface);
}
.sidebar-section {
margin: 0;
padding: 4px 12px 8px;
font-size: 11px;
font-weight: 600;
color: var(--j13-muted);
text-transform: none;
letter-spacing: 0;
}
.sidebar-section--boards { margin-top: 8px; }
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 2px;
padding: 0 8px;
}
.sidebar-nav-item {
display: flex;
align-items: center;
gap: 8px;
height: 36px;
padding: 0 12px;
border-radius: 8px;
color: var(--j13-text);
text-decoration: none;
font-size: 13px;
border-left: 0;
}
.sidebar-nav-item:hover { background: var(--j13-bg-block-muted); color: var(--j13-text); }
.sidebar-nav-item.active {
background: var(--j13-green-bg);
color: var(--j13-green);
font-weight: 600;
}
.sidebar-nav-item__meta {
margin-left: auto;
color: var(--j13-muted);
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.sidebar-board-icon { font-size: 10px; opacity: 0.88; }
.sidebar-board-icon--0 { color: var(--board-0-color); }
.sidebar-board-icon--1 { color: var(--board-1-color); }
.sidebar-board-icon--2 { color: var(--board-2-color); }
.sidebar-board-icon--3 { color: var(--board-3-color); }
.sidebar-board-icon--4 { color: var(--board-4-color); }
.sidebar-board-icon--5 { color: var(--board-5-color); }
.sidebar-board-icon--6 { color: var(--board-6-color); }
.sidebar-board-icon--7 { color: var(--board-7-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-0 { background: var(--board-0-bg); color: var(--board-0-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-1 { background: var(--board-1-bg); color: var(--board-1-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-2 { background: var(--board-2-bg); color: var(--board-2-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-3 { background: var(--board-3-bg); color: var(--board-3-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-4 { background: var(--board-4-bg); color: var(--board-4-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-5 { background: var(--board-5-bg); color: var(--board-5-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-6 { background: var(--board-6-bg); color: var(--board-6-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-7 { background: var(--board-7-bg); color: var(--board-7-color); }
.j13-aside__section, .j13-aside__link { display: none; }
.feed-toolbar {
display: flex;
align-items: center;
padding: 10px 16px;
border-bottom: 1px solid var(--j13-border-light);
margin: 0;
}
.feed-sort-bar {
display: flex;
gap: 2px;
border: 0;
padding: 0;
background: transparent;
border-radius: 0;
}
.feed-sort-tab {
display: inline-flex;
align-items: center;
height: 28px;
padding: 0 10px;
border-radius: 6px;
color: var(--j13-muted);
text-decoration: none;
font-size: 13px;
background: transparent;
}
.feed-sort-tab:hover { background: var(--j13-bg-block-muted); color: var(--j13-text); }
.feed-sort-tab.active,
.feed-sort-tab.is-active {
background: var(--j13-green-bg);
color: var(--j13-green);
font-weight: 600;
}
.j13-search-advanced {
margin: 0;
padding: 0 16px 10px;
border-bottom: 1px solid var(--j13-border-light);
}
.post-list { list-style: none; margin: 0; padding: 0; }
.post-row {
display: flex;
align-items: flex-start;
gap: 12px;
width: 100%;
padding: 16px 20px;
border-bottom: 1px solid var(--j13-border-light);
border-radius: 0;
margin: 0;
border-top: 0;
background: transparent;
transition: background var(--j13-ease);
}
.post-row:hover { background: var(--j13-bg-block-accent); }
.post-row--v2 { padding: 8px 16px; gap: 8px; }
.post-row--v2.post-row--title-only { padding: 12px 16px; }
.post-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
background: var(--j13-green-bg);
color: var(--j13-green);
display: grid;
place-items: center;
font-weight: 600;
font-size: 14px;
}
.post-avatar img { width: 100%; height: 100%; object-fit: cover; }
.post-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.post-main--with-thumb {
flex-direction: row;
align-items: stretch;
gap: 10px;
}
.post-content { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
.post-title-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.post-title {
color: var(--j13-text);
text-decoration: none;
font-weight: 600;
font-size: 15px;
line-height: 1.4;
letter-spacing: 0;
}
.post-row:hover .post-title { color: var(--j13-green); }
.post-excerpt {
margin: 0;
font-size: 13px;
line-height: 1.45;
color: var(--j13-muted);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.post-meta--inline { display: flex; margin-top: 2px; }
.post-meta-left {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 6px;
font-size: 12px;
color: var(--j13-muted);
min-width: 0;
}
.post-meta-author { color: var(--j13-muted); text-decoration: none; font-weight: 500; }
.post-meta-author:hover { color: var(--j13-green); }
.post-meta-sep { opacity: 0.6; }
.post-meta-last-reply { display: inline-flex; gap: 4px; align-items: center; }
.post-list-board-badge {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-decoration: none;
background: var(--board-0-bg);
color: var(--board-0-color);
}
.board-badge--1 { background: var(--board-1-bg); color: var(--board-1-color); }
.board-badge--2 { background: var(--board-2-bg); color: var(--board-2-color); }
.board-badge--3 { background: var(--board-3-bg); color: var(--board-3-color); }
.board-badge--4 { background: var(--board-4-bg); color: var(--board-4-color); }
.board-badge--5 { background: var(--board-5-bg); color: var(--board-5-color); }
.board-badge--6 { background: var(--board-6-bg); color: var(--board-6-color); }
.board-badge--7 { background: var(--board-7-bg); color: var(--board-7-color); }
.post-pin-badge,
.post-feature-badge {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
background: #fef3c7;
color: #92400e;
}
.post-feature-badge { background: var(--j13-green-bg); color: var(--j13-green); }
.post-pin-badge--board { background: #e0f2fe; color: #075985; }
.post-aside { display: flex; align-items: flex-end; gap: 10px; flex-shrink: 0; }
.post-thumb {
width: 64px;
height: 48px;
border-radius: 4px;
overflow: hidden;
background: var(--j13-bg-block-muted);
display: block;
}
.post-thumb img { width: 100%; height: 100%; object-fit: cover; }
.post-stats {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--j13-muted);
font-variant-numeric: tabular-nums;
}
.post-stat { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
.j13-post-item { display: none; } /* 旧列表类停用 */
.j13-widget,
.j13-checkin {
border: 1px solid var(--j13-border-light);
border-radius: 10px;
background: var(--j13-surface);
padding: 12px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.j13-widget__title {
text-transform: none;
letter-spacing: 0;
font-size: 13px;
color: var(--j13-text);
font-weight: 600;
}
.j13-btn, .j13-btn--compose, .header-compose-btn {
background: var(--j13-green);
border-color: var(--j13-green);
}
.j13-btn:hover { background: var(--j13-green-hover); border-color: var(--j13-green-hover); }
a { color: var(--j13-green); }
@media (max-width: 900px) {
.j13-layout { grid-template-columns: var(--j13-sidebar-w) minmax(0, 1fr); }
.j13-aside--right { display: none; }
.j13-header__inner.header-inner { display: flex; flex-wrap: wrap; }
.header-search-wrap { order: 3; flex: 1 1 100%; max-width: none; }
}

View File

@@ -60,6 +60,33 @@ document.documentElement.dataset.j13Ssr = "1";
} }
})(); })();
(function headerSearchUX() {
var input = document.getElementById("j13-header-search-input");
var filterBtn = document.getElementById("j13-search-filter-btn");
var advanced = document.getElementById("search");
if (filterBtn && advanced) {
filterBtn.addEventListener("click", function () {
advanced.open = true;
filterBtn.classList.add("header-search-filter-btn--active");
var kw = advanced.querySelector('input[name="keyword"]');
if (kw) kw.focus();
advanced.scrollIntoView({ behavior: "smooth", block: "nearest" });
});
}
document.addEventListener("keydown", function (e) {
if (!(e.ctrlKey || e.metaKey) || (e.key !== "k" && e.key !== "K")) return;
var tag = (e.target && e.target.tagName) || "";
if (tag === "INPUT" || tag === "TEXTAREA" || (e.target && e.target.isContentEditable)) {
if (e.target === input) return;
}
e.preventDefault();
if (input) {
input.focus();
input.select();
}
});
})();
(function mdEditors() { (function mdEditors() {
function insertAtCursor(textarea, text) { function insertAtCursor(textarea, text) {
const start = textarea.selectionStart || 0; const start = textarea.selectionStart || 0;

View File

@@ -44,9 +44,11 @@ type SitePageLink struct {
// BoardView 侧栏 // BoardView 侧栏
type BoardView struct { type BoardView struct {
ID uint ID uint
Name string Name string
Href string Href string
PostCount int
ColorSlot int // 07 板块色槽
} }
// RightAsideHotPost 右栏热门帖 // RightAsideHotPost 右栏热门帖
@@ -122,6 +124,8 @@ type PageChrome struct {
LoggedIn bool LoggedIn bool
IsAdmin bool IsAdmin bool
ViewerName string ViewerName string
ViewerAvatar string
ViewerMark string
Boards []BoardView Boards []BoardView
ActiveBoard uint ActiveBoard uint
Path string // 当前请求路径,供侧栏高亮 Path string // 当前请求路径,供侧栏高亮
@@ -158,19 +162,30 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome
name = ctx.Doer.Username name = ctx.Doer.Username
} }
} }
boards, _ := d.Board.List() boards, _ := d.Board.ListWithStats()
bv := make([]BoardView, 0, len(boards)) bv := make([]BoardView, 0, len(boards))
pl := d.permalink() pl := d.permalink()
for _, b := range boards { for _, b := range boards {
bv = append(bv, BoardView{ID: b.ID, Name: b.Name, Href: pl.BoardPath(b.ID)}) slot := b.ColorIndex
if slot < 0 {
slot = int(b.ID % 8)
}
bv = append(bv, BoardView{
ID: b.ID, Name: b.Name, Href: pl.BoardPath(b.ID),
PostCount: b.PostCount, ColorSlot: slot % 8,
})
} }
var unread int64 var unread int64
if ctx.IsSigned() && d.Message != nil { if ctx.IsSigned() && d.Message != nil {
unread, _ = d.Message.UnreadCount(ctx.UserID()) unread, _ = d.Message.UnreadCount(ctx.UserID())
} }
viewerPoints := 0 viewerPoints := 0
viewerAvatar := ""
viewerMark := "我"
if ctx.IsSigned() && ctx.Doer != nil { if ctx.IsSigned() && ctx.Doer != nil {
viewerPoints = ctx.Doer.Points viewerPoints = ctx.Doer.Points
viewerAvatar = strings.TrimSpace(ctx.Doer.Avatar)
viewerMark = firstRuneOr(name, "我")
} }
path := "" path := ""
if ctx.C != nil && ctx.C.Request != nil && ctx.C.Request.URL != nil { if ctx.C != nil && ctx.C.Request != nil && ctx.C.Request.URL != nil {
@@ -195,6 +210,8 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome
LoggedIn: ctx.IsSigned(), LoggedIn: ctx.IsSigned(),
IsAdmin: ctx.IsAdmin(), IsAdmin: ctx.IsAdmin(),
ViewerName: name, ViewerName: name,
ViewerAvatar: viewerAvatar,
ViewerMark: viewerMark,
Boards: bv, Boards: bv,
Path: path, Path: path,
Inner: inner, Inner: inner,

View File

@@ -148,40 +148,52 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
// HomePageData Feed // HomePageData Feed
type HomePageData struct { type HomePageData struct {
PageChrome PageChrome
BoardName string BoardName string
Sort string Sort string
Keyword string Keyword string
Tag string Tag string
Author string Author string
TitleOnly bool TitleOnly bool
HasSearch bool HasSearch bool
SearchError string SearchError string
Posts []PostListItem FeedListStyle string // title|excerpt|thumbnail
Page int ShowExcerpt bool
PrevPage int Posts []PostListItem
NextPage int Page int
HasPrev bool PrevPage int
HasMore bool NextPage int
HasPrev bool
HasMore bool
} }
// PostListItem 列表项 // PostListItem 列表项(对齐 main SPA post-row--v2
type PostListItem struct { type PostListItem struct {
ID uint ID uint
Title string Title string
Href string Href string
AuthorID uint AuthorID uint
AuthorName string AuthorName string
AuthorHref string AuthorHref string
AvatarURL string AvatarURL string
AuthorMark string // 无头像时的单字占位 AuthorMark string
BoardName string BoardID uint
Excerpt string BoardName string
Pinned bool BoardHref string
Featured bool BoardColorSlot int
CommentCount int ShowBoardBadge bool
LikeCount int Excerpt string
ViewCount int ThumbURL string
CreatedLabel string Pinned bool
BoardPinned bool
Featured bool
CommentCount int
LikeCount int
ViewCount int
CreatedLabel string
LastReplyLabel string
LastReplyName string
LastReplyHref string
ShowLastReply bool
} }
// FormAction 搜索表单提交路径(当前 Feed // FormAction 搜索表单提交路径(当前 Feed
@@ -274,19 +286,24 @@ func (d Deps) Home(c *gin.Context) {
chrome.Title = boardName + " · " + chrome.SiteName chrome.Title = boardName + " · " + chrome.SiteName
} }
feedStyle := d.Settings.FeedListStyle()
showExcerpt := feedStyle == "excerpt" || feedStyle == "thumbnail"
data := HomePageData{ data := HomePageData{
PageChrome: chrome, PageChrome: chrome,
BoardName: boardName, BoardName: boardName,
Sort: sort, Sort: sort,
Keyword: keyword, Keyword: keyword,
Tag: tag, Tag: tag,
Author: author, Author: author,
TitleOnly: titleOnly, TitleOnly: titleOnly,
HasSearch: hasSearch, HasSearch: hasSearch,
Page: page, FeedListStyle: feedStyle,
PrevPage: page - 1, ShowExcerpt: showExcerpt,
NextPage: page + 1, Page: page,
HasPrev: page > 1, PrevPage: page - 1,
NextPage: page + 1,
HasPrev: page > 1,
} }
listKW := keyword listKW := keyword
@@ -333,14 +350,29 @@ func (d Deps) Home(c *gin.Context) {
authorName = it.User.Username authorName = it.User.Username
} }
bname := "" bname := ""
boardHref := ""
boardSlot := 0
if it.Board.ID > 0 { if it.Board.ID > 0 {
bname = it.Board.Name bname = it.Board.Name
boardHref = pl.BoardPath(it.Board.ID)
boardSlot = it.Board.ColorIndex
if boardSlot < 0 {
boardSlot = int(it.Board.ID % 8)
}
boardSlot = boardSlot % 8
} }
excerptSrc := strings.TrimSpace(it.ContentPlain) excerptSrc := ""
if excerptSrc == "" { thumbURL := ""
excerptSrc = services.ExcerptFromHTML(it.Content, 100) if showExcerpt {
} else { excerptSrc = strings.TrimSpace(it.ContentPlain)
excerptSrc = services.TruncateRunes(excerptSrc, 100) if excerptSrc == "" {
excerptSrc = services.ExcerptFromHTML(it.Content, 60)
} else {
excerptSrc = services.TruncateRunes(excerptSrc, 60)
}
}
if feedStyle == "thumbnail" {
thumbURL = services.FirstImageURL(it.Content)
} }
avatar := strings.TrimSpace(it.User.Avatar) avatar := strings.TrimSpace(it.User.Avatar)
authorID := it.UserID authorID := it.UserID
@@ -348,14 +380,44 @@ func (d Deps) Home(c *gin.Context) {
if authorID > 0 { if authorID > 0 {
authorHref = pl.UserPath(authorID) authorHref = pl.UserPath(authorID)
} }
createdLabel := formatRelativeTime(it.CreatedAt)
if sort == "reply" && it.LastReplyAt == nil {
createdLabel = "暂无回复"
}
lastReplyName := ""
lastReplyHref := ""
lastReplyLabel := ""
showLast := false
if it.LastReplyAt != nil {
showLast = true
lastReplyLabel = formatRelativeTime(*it.LastReplyAt)
if it.LastReplyUser != nil {
lastReplyName = strings.TrimSpace(it.LastReplyUser.Nickname)
if lastReplyName == "" {
lastReplyName = it.LastReplyUser.Username
}
if it.LastReplyUser.ID > 0 {
lastReplyHref = pl.UserPath(it.LastReplyUser.ID)
}
} else {
lastReplyName = strings.TrimSpace(it.LastReplyGuestNick)
}
if lastReplyName == "" {
showLast = false
}
}
posts = append(posts, PostListItem{ posts = append(posts, PostListItem{
ID: it.ID, Title: it.Title, Href: pl.PostPath(it.ID), ID: it.ID, Title: it.Title, Href: pl.PostPath(it.ID),
AuthorID: authorID, AuthorName: authorName, AuthorHref: authorHref, AuthorID: authorID, AuthorName: authorName, AuthorHref: authorHref,
AvatarURL: avatar, AuthorMark: firstRuneOr(authorName, "姜"), AvatarURL: avatar, AuthorMark: firstRuneOr(authorName, "姜"),
BoardName: bname, Excerpt: excerptSrc, BoardID: it.BoardID, BoardName: bname, BoardHref: boardHref,
Pinned: it.Pinned, Featured: it.Featured, BoardColorSlot: boardSlot, ShowBoardBadge: boardID == 0 && it.Board.ID > 0,
Excerpt: excerptSrc, ThumbURL: thumbURL,
Pinned: it.Pinned, BoardPinned: it.BoardPinned, Featured: it.Featured,
CommentCount: it.CommentCount, LikeCount: it.LikeCount, ViewCount: it.ViewCount, CommentCount: it.CommentCount, LikeCount: it.LikeCount, ViewCount: it.ViewCount,
CreatedLabel: formatRelativeTime(it.CreatedAt), CreatedLabel: createdLabel,
LastReplyLabel: lastReplyLabel, LastReplyName: lastReplyName,
LastReplyHref: lastReplyHref, ShowLastReply: showLast,
}) })
} }

View File

@@ -1,56 +1,66 @@
{{define "base/navbar"}} {{define "base/navbar"}}
<header class="j13-header"> <header class="j13-header app-header">
<div class="j13-header__inner"> <div class="j13-header__inner header-inner">
<a class="j13-brand" href="/"> <a class="j13-brand header-brand" href="/">
{{if .LogoURL}} {{if .LogoURL}}
<img class="j13-brand__logo" src="{{.LogoURL}}" alt="{{.SiteName}}"/> <img class="j13-brand__logo header-logo-mark" src="{{.LogoURL}}" alt="{{.SiteName}}"/>
{{else}} {{else}}
<span class="j13-brand__mark">{{.LogoMark}}</span> <span class="j13-brand__mark header-logo-mark">{{.LogoMark}}</span>
{{end}} {{end}}
<span class="j13-brand__text"> <span class="header-logo-text">{{.SiteName}}</span>
<strong>{{.SiteName}}</strong>
{{if .Slogan}}<span class="j13-brand__slogan">{{.Slogan}}</span>{{end}}
</span>
</a> </a>
<form class="j13-header-search" method="get" action="/" role="search"> <form class="header-search-wrap" method="get" action="/" role="search" id="j13-header-search">
<label class="j13-header-search__label"> <div class="header-search-row">
<span class="j13-visually-hidden">搜索帖子</span> <svg class="header-search-icon" width="15" height="15" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M10.5 3a7.5 7.5 0 015.95 12.1l4.22 4.23a1 1 0 01-1.42 1.41l-4.22-4.22A7.5 7.5 0 1110.5 3zm0 2a5.5 5.5 0 100 11 5.5 5.5 0 000-11z"/></svg>
<input type="search" name="keyword" placeholder="搜索帖子…" autocomplete="off"/> <input class="header-search-input" id="j13-header-search-input" type="search" name="keyword" value="" placeholder="搜索帖子…" autocomplete="off"/>
</label> <button type="button" class="header-search-filter-btn" id="j13-search-filter-btn" aria-label="高级筛选" title="高级筛选">
<svg width="14" height="14" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M3 5h18v2.17L14 14v5l-4 2v-7L3 7.17V5z"/></svg>
</button>
<kbd class="header-search-kbd" aria-hidden="true">Ctrl K</kbd>
</div>
</form> </form>
<div class="j13-header__actions"> <div class="header-actions">
{{if .LoggedIn}} {{if .LoggedIn}}
<a class="j13-btn j13-btn--compose" href="/compose">+ 发帖</a> <a class="header-compose-btn" href="/compose"><span aria-hidden="true">+</span> 发帖</a>
{{else}} {{else}}
<a class="j13-btn j13-btn--compose" href="/login">登录</a> <a class="header-compose-btn" href="/login">登录</a>
<a class="j13-header__textlink" href="/register">注册</a>
{{end}} {{end}}
<button type="button" class="j13-theme-toggle" id="j13-theme-toggle" aria-label="切换主题" title="切换主题"> <div class="header-action-group">
<svg class="j13-theme-toggle__icon" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> <button type="button" class="header-icon-btn j13-theme-toggle" id="j13-theme-toggle" aria-label="切换主题" title="切换主题">
<path fill="currentColor" d="M12 3a1 1 0 0 1 1 1v1.06a7.5 7.5 0 1 1-2 0V4a1 1 0 0 1 1-1Zm0 4.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11Z"/> <svg class="j13-theme-toggle__icon" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M21 14.3A8.5 8.5 0 019.7 3 7 7 0 1019 16.5c.7-.6 1.3-1.4 2-2.2z"/></svg>
</svg> <span class="j13-visually-hidden j13-theme-toggle__label">主题</span>
<span class="j13-theme-toggle__label">主题</span> </button>
</button> {{if .LoggedIn}}
{{if .LoggedIn}} <a class="header-icon-btn" href="/messages" title="私信" aria-label="私信">
<a class="j13-header__iconlink" href="/messages" title="私信">私信{{if gt .UnreadCount 0}} <span class="j13-badge">{{.UnreadCount}}</span>{{end}}</a> <svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M4 4h16a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm0 2v.51l8 5.33 8-5.33V6H4zm16 3.04l-7.45 4.97a1 1 0 01-1.1 0L4 9.04V18h16V9.04z"/></svg>
<a class="j13-header__user" href="/profile" title="个人中心">{{.ViewerName}}</a> {{if gt .UnreadCount 0}}<span class="j13-badge">{{.UnreadCount}}</span>{{end}}
{{end}} </a>
<details class="j13-more"> <details class="j13-more header-user-menu">
<summary>更多</summary> <summary class="header-user-btn" title="{{.ViewerName}}">
<div class="j13-more__menu" role="menu"> {{if .ViewerAvatar}}
<a href="/boards" role="menuitem">板块</a> <img class="header-user-avatar" src="{{.ViewerAvatar}}" alt=""/>
{{if .ShowFriendLinksNav}}<a href="/links" role="menuitem">友链</a>{{end}} {{else}}
{{range .NavPages}}<a href="/page/{{.Slug}}" role="menuitem">{{.Title}}</a>{{end}} <span class="header-user-initial">{{.ViewerMark}}</span>
{{if .LoggedIn}} {{end}}
<a href="/favorites" role="menuitem">收藏</a> </summary>
{{if .IsAdmin}}<a href="/admin/dashboard" role="menuitem">后台</a>{{end}} <div class="j13-more__menu" role="menu">
<form method="post" action="/logout"> <a href="/profile" role="menuitem">{{.ViewerName}} · {{.ViewerPoints}} 积分</a>
<input type="hidden" name="_csrf" value="{{.CSRF}}"/> <a href="/favorites" role="menuitem">收藏</a>
<button type="submit" role="menuitem">退出</button> <a href="/messages" role="menuitem">私信</a>
</form> {{if .IsAdmin}}<a href="/admin/dashboard" role="menuitem">后台</a>{{end}}
{{end}} {{if .ShowFriendLinksNav}}<a href="/links" role="menuitem">友链</a>{{end}}
</div> {{range .NavPages}}<a href="/page/{{.Slug}}" role="menuitem">{{.Title}}</a>{{end}}
</details> <a href="/boards" role="menuitem">板块</a>
<form method="post" action="/logout">
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
<button type="submit" role="menuitem">退出</button>
</form>
</div>
</details>
{{else}}
<a class="header-login-btn" href="/register">注册</a>
{{end}}
</div>
</div> </div>
</div> </div>
</header> </header>

View File

@@ -1,14 +1,13 @@
{{define "home/feed"}} {{define "home/feed"}}
<section class="j13-feed"> <section class="j13-feed">
<header class="j13-feed__header"> <header class="feed-toolbar j13-feed__header">
<h1 class="j13-feed__title">{{if .BoardName}}{{.BoardName}}{{else}}全部帖子{{end}}</h1> <div class="feed-sort-bar j13-sort" role="tablist" aria-label="排序">
<div class="j13-sort" role="navigation" aria-label="排序"> <a role="tab" class="feed-sort-tab{{if eq .Sort "latest"}} active is-active{{end}}" href="{{.SortHref "latest"}}">最新发帖</a>
<a class="{{if eq .Sort "latest"}}is-active{{end}}" href="{{.SortHref "latest"}}">最新发帖</a> <a role="tab" class="feed-sort-tab{{if eq .Sort "reply"}} active is-active{{end}}" href="{{.SortHref "reply"}}">最新回复</a>
<a class="{{if eq .Sort "reply"}}is-active{{end}}" href="{{.SortHref "reply"}}">最新回复</a> <a role="tab" class="feed-sort-tab{{if eq .Sort "hot"}} active is-active{{end}}" href="{{.SortHref "hot"}}">热门讨论</a>
<a class="{{if eq .Sort "hot"}}is-active{{end}}" href="{{.SortHref "hot"}}">热门讨论</a>
</div> </div>
</header> </header>
<details class="j13-search-advanced" id="search"{{if or .HasSearch .TitleOnly .SearchError}} open{{end}}> <details class="j13-search-advanced post-search-panel" id="search"{{if or .HasSearch .TitleOnly .SearchError}} open{{end}}>
<summary class="j13-search-advanced__summary">高级搜索</summary> <summary class="j13-search-advanced__summary">高级搜索</summary>
<form class="j13-search j13-search--toolbar" method="get" action="{{.FormAction}}" role="search"> <form class="j13-search j13-search--toolbar" method="get" action="{{.FormAction}}" role="search">
{{if and .Sort (ne .Sort "latest")}}<input type="hidden" name="sort" value="{{.Sort}}"/>{{end}} {{if and .Sort (ne .Sort "latest")}}<input type="hidden" name="sort" value="{{.Sort}}"/>{{end}}
@@ -37,34 +36,52 @@
{{if not .Posts}} {{if not .Posts}}
<p class="j13-empty">{{if .SearchError}}请调整搜索条件后重试。{{else if .HasSearch}}未找到匹配帖子。{{else}}暂无帖子。{{end}}</p> <p class="j13-empty">{{if .SearchError}}请调整搜索条件后重试。{{else if .HasSearch}}未找到匹配帖子。{{else}}暂无帖子。{{end}}</p>
{{else}} {{else}}
<ul class="j13-post-list"> <ul class="j13-post-list post-list">
{{range .Posts}} {{range .Posts}}
<li class="j13-post-item"> <li class="post-row post-row--v2{{if eq $.FeedListStyle "title"}} post-row--title-only{{end}}{{if .ThumbURL}} post-row--has-thumb{{end}}">
<div class="j13-post-item__avatar"> <div class="post-avatar">
{{if .AvatarURL}} {{if .AvatarURL}}
<img src="{{.AvatarURL}}" alt="" width="40" height="40"/> <img src="{{.AvatarURL}}" alt=""/>
{{else}} {{else}}
<span class="j13-post-item__mark" aria-hidden="true">{{.AuthorMark}}</span> <span>{{.AuthorMark}}</span>
{{end}} {{end}}
</div> </div>
<div class="j13-post-item__body"> <div class="post-main{{if .ThumbURL}} post-main--with-thumb{{end}}">
<div class="j13-post-item__byline"> <div class="post-content">
{{if .AuthorHref}}<a href="{{.AuthorHref}}">{{.AuthorName}}</a>{{else}}<span>{{.AuthorName}}</span>{{end}} <div class="post-title-row">
<span class="j13-post-item__time">{{.CreatedLabel}}</span> {{if .Pinned}}<span class="post-pin-badge">全局置顶</span>{{end}}
{{if .BoardPinned}}<span class="post-pin-badge post-pin-badge--board">板块置顶</span>{{end}}
{{if .Featured}}<span class="post-feature-badge">精华</span>{{end}}
<a class="post-title" href="{{.Href}}"{{if $.OpenPostsInNewTab}} target="_blank" rel="noopener"{{end}}>{{.Title}}</a>
</div>
{{if and $.ShowExcerpt .Excerpt}}<p class="post-excerpt">{{.Excerpt}}</p>{{end}}
<div class="post-meta post-meta--inline">
<div class="post-meta-left">
{{if .ShowBoardBadge}}
<a class="post-list-board-badge board-badge--{{.BoardColorSlot}}" href="{{.BoardHref}}">{{.BoardName}}</a>
{{end}}
{{if .AuthorHref}}<a class="post-meta-author" href="{{.AuthorHref}}">{{.AuthorName}}</a>{{else}}<span class="post-meta-author">{{.AuthorName}}</span>{{end}}
<span class="post-meta-sep" aria-hidden>·</span>
<span class="post-meta-time">{{.CreatedLabel}}</span>
{{if .ShowLastReply}}
<span class="post-meta-last-reply">
<span aria-hidden>←</span>
{{if .LastReplyHref}}<a href="{{.LastReplyHref}}">{{.LastReplyName}}</a>{{else}}<span>{{.LastReplyName}}</span>{{end}}
<span>{{.LastReplyLabel}}</span>
</span>
{{end}}
</div>
</div>
</div> </div>
<a class="j13-post-item__title" href="{{.Href}}"{{if $.OpenPostsInNewTab}} target="_blank" rel="noopener"{{end}}>{{.Title}}</a> <div class="post-aside">
{{if .Excerpt}}<p class="j13-post-item__excerpt">{{.Excerpt}}</p>{{end}} {{if .ThumbURL}}
<div class="j13-post-item__meta"> <a class="post-thumb" href="{{.Href}}" tabindex="-1" aria-hidden="true"><img src="{{.ThumbURL}}" alt=""/></a>
{{if .Pinned}}<span class="j13-tag j13-tag--pin">置顶</span>{{end}} {{end}}
{{if .Featured}}<span class="j13-tag j13-tag--feat">精华</span>{{end}} <div class="post-stats">
{{if .BoardName}}<span class="j13-tag">{{.BoardName}}</span>{{end}} <span class="post-stat" title="回复"><svg width="12" height="12" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M4 4h16a2 2 0 012 2v9a2 2 0 01-2 2H9l-5 4v-4H4a2 2 0 01-2-2V6a2 2 0 012-2z"/></svg> {{.CommentCount}}</span>
</div>
</div> </div>
</div> </div>
<div class="j13-post-item__stats" aria-label="互动">
<span title="回复"><strong>{{.CommentCount}}</strong> 回复</span>
<span title="赞"><strong>{{.LikeCount}}</strong> 赞</span>
<span title="阅读"><strong>{{.ViewCount}}</strong> 阅读</span>
</div>
</li> </li>
{{end}} {{end}}
</ul> </ul>

View File

@@ -1,14 +1,22 @@
{{define "shared/board_aside"}} {{define "shared/board_aside"}}
<aside class="j13-aside j13-aside--left"> <aside class="j13-aside j13-aside--left sidebar" aria-label="导航">
<p class="j13-aside__section">浏览</p> <p class="sidebar-section">浏览</p>
<a class="j13-aside__link{{if and (eq .ActiveBoard 0) (ne .Path "/boards") (ne .Path "/favorites")}} is-active{{end}}" href="/">全部帖子</a> <nav class="sidebar-nav">
{{if .LoggedIn}} <a class="sidebar-nav-item{{if and (eq .ActiveBoard 0) (ne .Path "/boards") (ne .Path "/favorites")}} active{{end}}" href="/">全部帖子</a>
<a class="j13-aside__link{{if eq .Path "/favorites"}} is-active{{end}}" href="/favorites">我的收藏</a> {{if .LoggedIn}}
{{end}} <a class="sidebar-nav-item{{if eq .Path "/favorites"}} active{{end}}" href="/favorites">我的收藏</a>
<a class="j13-aside__link{{if eq .Path "/boards"}} is-active{{end}}" href="/boards">全部板块</a> {{end}}
<p class="j13-aside__section">板块</p> <a class="sidebar-nav-item{{if eq .Path "/boards"}} active{{end}}" href="/boards">全部板块</a>
{{range .Boards}} </nav>
<a class="j13-aside__link{{if eq $.ActiveBoard .ID}} is-active{{end}}" href="{{.Href}}">{{.Name}}</a> <p class="sidebar-section sidebar-section--boards">板块</p>
{{end}} <nav class="sidebar-nav">
{{range .Boards}}
<a class="sidebar-nav-item sidebar-nav-item--board sidebar-nav-item--board-{{.ColorSlot}}{{if eq $.ActiveBoard .ID}} active{{end}}" href="{{.Href}}">
<span class="sidebar-board-icon sidebar-board-icon--{{.ColorSlot}}" aria-hidden="true">◆</span>
<span>{{.Name}}</span>
<span class="sidebar-nav-item__meta">{{.PostCount}}</span>
</a>
{{end}}
</nav>
</aside> </aside>
{{end}} {{end}}

View File

@@ -1,16 +1,16 @@
/* 姜十三论坛公开页样式 — 对齐 docs/rebuild-spec/10-design-system.md清青产品) */ /* 姜十三论坛公开页样式 — 对齐 main SPA 首页视觉(草绿产品 */
:root { :root {
color-scheme: light; color-scheme: light;
--j13-bg: #f4f8fa; --j13-bg: #f5f7fa;
--j13-surface: #ffffff; --j13-surface: #ffffff;
--j13-text: #14212b; --j13-text: #14212b;
--j13-muted: #5a6b75; --j13-muted: #5a6b75;
--j13-accent: #0e8a9a; --j13-accent: #18a058;
--j13-accent-ink: #0b5f6b; --j13-accent-ink: #138f4c;
--j13-on-accent: #ffffff; --j13-on-accent: #ffffff;
--j13-soft: #e6f7f9; --j13-soft: #edfbf3;
--j13-border: #d3e0e6; --j13-border: #e8edf2;
--j13-hover: #e8f4f6; --j13-hover: #f0faf5;
--j13-radius: 8px; --j13-radius: 8px;
--j13-font: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", "Noto Sans SC", "Segoe UI", system-ui, sans-serif; --j13-font: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", "Noto Sans SC", "Segoe UI", system-ui, sans-serif;
--j13-font-mono: "Cascadia Code", "Consolas", "SF Mono", ui-monospace, monospace; --j13-font-mono: "Cascadia Code", "Consolas", "SF Mono", ui-monospace, monospace;
@@ -25,34 +25,34 @@
--j13-err-border: #fecaca; --j13-err-border: #fecaca;
--j13-pin-bg: #fef3c7; --j13-pin-bg: #fef3c7;
--j13-pin-fg: #92400e; --j13-pin-fg: #92400e;
--j13-feat-bg: #e6f7f9; --j13-feat-bg: #edfbf3;
--j13-feat-fg: #0b5f6b; --j13-feat-fg: #138f4c;
--j13-neutral-bg: #eef2f6; --j13-neutral-bg: #eef2f6;
--j13-neutral-fg: #475569; --j13-neutral-fg: #475569;
--j13-self-bg: #e6f7f9; --j13-self-bg: #edfbf3;
--j13-self-border: #7dd3de; --j13-self-border: #86efac;
--j13-delta-pos: #15803d; --j13-delta-pos: #15803d;
--j13-delta-neg: #b91c1c; --j13-delta-neg: #b91c1c;
--j13-overlay: rgba(244, 248, 250, 0.92); --j13-overlay: rgba(244, 248, 250, 0.92);
--j13-header-shadow: 0 1px 0 var(--j13-border), 0 6px 18px rgba(11, 95, 107, 0.06); --j13-header-shadow: 0 1px 0 var(--j13-border), 0 6px 18px rgba(11, 95, 107, 0.06);
--j13-ease: 150ms ease; --j13-ease: 150ms ease;
--j13-frame: 1320px; --j13-frame: 1400px;
--j13-header-h: 3.5rem; --j13-header-h: 3.5rem;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
color-scheme: dark; color-scheme: dark;
--j13-bg: #0c1216; --j13-bg: #141416;
--j13-surface: #152028; --j13-surface: #1f1f23;
--j13-text: #e8eef2; --j13-text: #e8eef2;
--j13-muted: #8b9aab; --j13-muted: #8b9aab;
--j13-accent: #2ec4d6; --j13-accent: #23c36b;
--j13-accent-ink: #9ae8f0; --j13-accent-ink: #6ee7a0;
--j13-on-accent: #0b5f6b; --j13-on-accent: #138f4c;
--j13-soft: #12343a; --j13-soft: rgba(35, 195, 107, 0.08);
--j13-border: #2a3a44; --j13-border: #2e2e32;
--j13-hover: #1c2d36; --j13-hover: #232328;
--j13-danger: #f87171; --j13-danger: #f87171;
--j13-ok-bg: #064e3b; --j13-ok-bg: #064e3b;
--j13-ok-fg: #a7f3d0; --j13-ok-fg: #a7f3d0;
@@ -64,12 +64,12 @@
--j13-err-border: #991b1b; --j13-err-border: #991b1b;
--j13-pin-bg: #78350f; --j13-pin-bg: #78350f;
--j13-pin-fg: #fde68a; --j13-pin-fg: #fde68a;
--j13-feat-bg: #0b5f6b; --j13-feat-bg: #138f4c;
--j13-feat-fg: #9ae8f0; --j13-feat-fg: #6ee7a0;
--j13-neutral-bg: #243040; --j13-neutral-bg: #243040;
--j13-neutral-fg: #cbd5e1; --j13-neutral-fg: #cbd5e1;
--j13-self-bg: #0b5f6b; --j13-self-bg: #138f4c;
--j13-self-border: #0e8a9a; --j13-self-border: #18a058;
--j13-delta-pos: #4ade80; --j13-delta-pos: #4ade80;
--j13-delta-neg: #f87171; --j13-delta-neg: #f87171;
--j13-overlay: rgba(12, 18, 22, 0.9); --j13-overlay: rgba(12, 18, 22, 0.9);
@@ -79,16 +79,16 @@
html[data-theme="light"] { html[data-theme="light"] {
color-scheme: light; color-scheme: light;
--j13-bg: #f4f8fa; --j13-bg: #f5f7fa;
--j13-surface: #ffffff; --j13-surface: #ffffff;
--j13-text: #14212b; --j13-text: #14212b;
--j13-muted: #5a6b75; --j13-muted: #5a6b75;
--j13-accent: #0e8a9a; --j13-accent: #18a058;
--j13-accent-ink: #0b5f6b; --j13-accent-ink: #138f4c;
--j13-on-accent: #ffffff; --j13-on-accent: #ffffff;
--j13-soft: #e6f7f9; --j13-soft: #edfbf3;
--j13-border: #d3e0e6; --j13-border: #e8edf2;
--j13-hover: #e8f4f6; --j13-hover: #f0faf5;
--j13-danger: #b91c1c; --j13-danger: #b91c1c;
--j13-ok-bg: #ecfdf5; --j13-ok-bg: #ecfdf5;
--j13-ok-fg: #065f46; --j13-ok-fg: #065f46;
@@ -100,12 +100,12 @@ html[data-theme="light"] {
--j13-err-border: #fecaca; --j13-err-border: #fecaca;
--j13-pin-bg: #fef3c7; --j13-pin-bg: #fef3c7;
--j13-pin-fg: #92400e; --j13-pin-fg: #92400e;
--j13-feat-bg: #e6f7f9; --j13-feat-bg: #edfbf3;
--j13-feat-fg: #0b5f6b; --j13-feat-fg: #138f4c;
--j13-neutral-bg: #eef2f6; --j13-neutral-bg: #eef2f6;
--j13-neutral-fg: #475569; --j13-neutral-fg: #475569;
--j13-self-bg: #e6f7f9; --j13-self-bg: #edfbf3;
--j13-self-border: #7dd3de; --j13-self-border: #86efac;
--j13-delta-pos: #15803d; --j13-delta-pos: #15803d;
--j13-delta-neg: #b91c1c; --j13-delta-neg: #b91c1c;
--j13-overlay: rgba(244, 248, 250, 0.92); --j13-overlay: rgba(244, 248, 250, 0.92);
@@ -114,16 +114,16 @@ html[data-theme="light"] {
html[data-theme="dark"] { html[data-theme="dark"] {
color-scheme: dark; color-scheme: dark;
--j13-bg: #0c1216; --j13-bg: #141416;
--j13-surface: #152028; --j13-surface: #1f1f23;
--j13-text: #e8eef2; --j13-text: #e8eef2;
--j13-muted: #8b9aab; --j13-muted: #8b9aab;
--j13-accent: #2ec4d6; --j13-accent: #23c36b;
--j13-accent-ink: #9ae8f0; --j13-accent-ink: #6ee7a0;
--j13-on-accent: #0b5f6b; --j13-on-accent: #138f4c;
--j13-soft: #12343a; --j13-soft: rgba(35, 195, 107, 0.08);
--j13-border: #2a3a44; --j13-border: #2e2e32;
--j13-hover: #1c2d36; --j13-hover: #232328;
--j13-danger: #f87171; --j13-danger: #f87171;
--j13-ok-bg: #064e3b; --j13-ok-bg: #064e3b;
--j13-ok-fg: #a7f3d0; --j13-ok-fg: #a7f3d0;
@@ -135,12 +135,12 @@ html[data-theme="dark"] {
--j13-err-border: #991b1b; --j13-err-border: #991b1b;
--j13-pin-bg: #78350f; --j13-pin-bg: #78350f;
--j13-pin-fg: #fde68a; --j13-pin-fg: #fde68a;
--j13-feat-bg: #0b5f6b; --j13-feat-bg: #138f4c;
--j13-feat-fg: #9ae8f0; --j13-feat-fg: #6ee7a0;
--j13-neutral-bg: #243040; --j13-neutral-bg: #243040;
--j13-neutral-fg: #cbd5e1; --j13-neutral-fg: #cbd5e1;
--j13-self-bg: #0b5f6b; --j13-self-bg: #138f4c;
--j13-self-border: #0e8a9a; --j13-self-border: #18a058;
--j13-delta-pos: #4ade80; --j13-delta-pos: #4ade80;
--j13-delta-neg: #f87171; --j13-delta-neg: #f87171;
--j13-overlay: rgba(12, 18, 22, 0.9); --j13-overlay: rgba(12, 18, 22, 0.9);
@@ -219,11 +219,11 @@ code, pre, .j13-md-editor__input {
letter-spacing: -0.02em; letter-spacing: -0.02em;
} }
html[data-theme="dark"] .j13-brand__mark { html[data-theme="dark"] .j13-brand__mark {
color: #0c1216; color: #141416;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
html:not([data-theme="light"]) .j13-brand__mark { html:not([data-theme="light"]) .j13-brand__mark {
color: #0c1216; color: #141416;
} }
} }
.j13-brand__logo { .j13-brand__logo {
@@ -1792,3 +1792,567 @@ points-only[data-locked="false"] {
} }
.j13-rev-item a:hover strong { color: var(--j13-accent); } .j13-rev-item a:hover strong { color: var(--j13-accent); }
.j13-rev-snapshot { margin-top: 1rem; } .j13-rev-snapshot { margin-top: 1rem; }
/* —— main SPA 首页壳层对齐覆盖 —— */
:root {
--j13-green: #18a058;
--j13-green-hover: #138f4c;
--j13-green-bg: rgba(24, 160, 88, 0.08);
--j13-green-soft: #edfbf3;
--j13-border-light: #f0f3f7;
--j13-sidebar-w: 210px;
--j13-aside-w: 280px;
--j13-header-h: 56px;
--j13-max-w: 1400px;
--j13-bg-page: #f5f7fa;
--j13-bg-block-muted: #f1f4f8;
--j13-bg-block-accent: #f0faf5;
--board-0-color: #18a058; --board-0-bg: rgba(24,160,88,.1);
--board-1-color: #3498db; --board-1-bg: rgba(52,152,219,.1);
--board-2-color: #9b59b6; --board-2-bg: rgba(155,89,182,.1);
--board-3-color: #e67e22; --board-3-bg: rgba(230,126,34,.1);
--board-4-color: #e74c3c; --board-4-bg: rgba(231,76,60,.1);
--board-5-color: #1abc9c; --board-5-bg: rgba(26,188,156,.1);
--board-6-color: #6366f1; --board-6-bg: rgba(99,102,241,.1);
--board-7-color: #f59e0b; --board-7-bg: rgba(245,158,11,.12);
--j13-frame: var(--j13-max-w);
}
html[data-theme="dark"] {
--j13-green: #23c36b;
--j13-green-hover: #1fb35f;
--j13-green-bg: rgba(35,195,107,.12);
--j13-green-soft: rgba(35,195,107,.08);
--j13-border-light: #262629;
--j13-bg-page: #141416;
--j13-bg-block-muted: #1c1c20;
--j13-bg-block-accent: rgba(35,195,107,.06);
}
@media (prefers-color-scheme: dark) {
html:not([data-theme="light"]) {
--j13-green: #23c36b;
--j13-green-hover: #1fb35f;
--j13-green-bg: rgba(35,195,107,.12);
--j13-green-soft: rgba(35,195,107,.08);
--j13-border-light: #262629;
--j13-bg-page: #141416;
--j13-bg-block-muted: #1c1c20;
--j13-bg-block-accent: rgba(35,195,107,.06);
}
}
body.j13-body {
background:
radial-gradient(ellipse 70% 45% at 50% -10%, rgba(24, 160, 88, 0.07), transparent 70%),
var(--j13-bg-page);
font-size: 14px;
}
html[data-theme="dark"] body.j13-body {
background:
radial-gradient(ellipse 70% 45% at 50% -10%, rgba(35, 195, 107, 0.08), transparent 70%),
var(--j13-bg-page);
}
@media (prefers-color-scheme: dark) {
html:not([data-theme="light"]) body.j13-body {
background:
radial-gradient(ellipse 70% 45% at 50% -10%, rgba(35, 195, 107, 0.08), transparent 70%),
var(--j13-bg-page);
}
}
.j13-header.app-header {
height: var(--j13-header-h);
box-shadow: none;
}
.j13-header__inner.header-inner {
max-width: var(--j13-max-w);
height: 100%;
min-height: var(--j13-header-h);
padding: 0 20px;
display: flex;
grid-template-columns: none;
gap: 20px;
}
.header-brand { gap: 10px; }
.header-logo-mark,
.j13-brand__mark.header-logo-mark {
width: 34px;
height: 34px;
border-radius: 10px;
background: linear-gradient(135deg, var(--j13-green) 0%, #34c471 100%);
color: #fff;
font-size: 17px;
}
.header-logo-text {
font-size: 15px;
font-weight: 600;
color: var(--j13-text);
letter-spacing: 0.02em;
}
.header-brand:hover .header-logo-text { color: var(--j13-green); }
.j13-brand__text { display: none; }
.header-search-wrap {
flex: 1;
min-width: 0;
max-width: 420px;
margin: 0;
display: flex;
align-items: center;
min-height: 36px;
padding: 0 10px 0 12px;
border: 1px solid var(--j13-border);
border-radius: 999px;
background: var(--j13-bg-block-muted);
transition: border-color var(--j13-ease), box-shadow var(--j13-ease), background var(--j13-ease);
}
.header-search-wrap:focus-within {
background: var(--j13-surface);
border-color: color-mix(in srgb, var(--j13-green) 30%, transparent);
box-shadow: 0 0 0 3px var(--j13-green-bg);
}
.header-search-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.header-search-icon { color: var(--j13-muted); flex-shrink: 0; }
.header-search-input {
flex: 1;
min-width: 0;
border: 0;
outline: none;
background: transparent;
font: inherit;
font-size: 14px;
color: var(--j13-text);
padding: 0;
max-width: none;
margin: 0;
border-radius: 0;
}
.header-search-filter-btn {
flex-shrink: 0;
width: 28px;
height: 28px;
border: 0;
border-radius: 999px;
background: transparent;
color: var(--j13-muted);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}
.header-search-filter-btn:hover,
.header-search-filter-btn--active {
background: var(--j13-green-bg);
color: var(--j13-green);
}
.header-search-kbd {
display: none;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid var(--j13-border-light);
background: var(--j13-surface);
font-size: 10px;
color: var(--j13-muted);
font-family: inherit;
}
@media (min-width: 1100px) {
.header-search-kbd { display: inline-flex; }
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
margin-left: auto;
flex-shrink: 0;
}
.header-compose-btn {
display: inline-flex;
align-items: center;
gap: 6px;
height: 34px;
padding: 0 16px;
border: none;
border-radius: 999px;
background: var(--j13-green);
color: #fff;
font: inherit;
font-size: 13px;
font-weight: 600;
text-decoration: none;
box-shadow: 0 2px 8px rgba(26, 127, 75, 0.22);
transition: background var(--j13-ease), box-shadow var(--j13-ease), transform var(--j13-ease);
}
.header-compose-btn:hover {
background: var(--j13-green-hover);
color: #fff;
text-decoration: none;
box-shadow: 0 3px 10px rgba(26, 127, 75, 0.3);
}
.header-action-group {
display: flex;
align-items: center;
gap: 4px;
padding: 3px;
border-radius: 999px;
background: var(--j13-bg-block-muted);
border: 1px solid var(--j13-border-light);
}
.header-icon-btn {
position: relative;
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: transparent;
color: var(--j13-muted);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
padding: 0;
}
.header-icon-btn:hover {
background: var(--j13-hover);
color: var(--j13-text);
}
.header-icon-btn .j13-badge {
position: absolute;
top: -2px;
right: -2px;
font-size: 10px;
min-width: 1rem;
padding: 0 4px;
}
.header-user-btn {
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
padding: 0;
cursor: pointer;
overflow: hidden;
background: var(--j13-green);
list-style: none;
display: grid;
place-items: center;
}
.header-user-btn::-webkit-details-marker { display: none; }
.header-user-avatar { width: 100%; height: 100%; object-fit: cover; }
.header-user-initial { color: #fff; font-size: 14px; font-weight: 600; }
.header-login-btn {
height: 32px;
padding: 0 14px;
border-radius: 999px;
color: var(--j13-green);
font-size: 13px;
font-weight: 500;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.header-login-btn:hover { background: var(--j13-green-bg); }
.header-user-menu > summary { list-style: none; }
.j13-theme-toggle.header-icon-btn {
border: 0;
border-radius: 50%;
padding: 0;
width: 32px;
height: 32px;
}
.j13-layout {
max-width: var(--j13-max-w);
grid-template-columns: var(--j13-sidebar-w) minmax(0, 1fr) var(--j13-aside-w);
gap: 0;
padding: 0;
background: var(--j13-surface);
border-left: 1px solid var(--j13-border-light);
border-right: 1px solid var(--j13-border-light);
min-height: calc(100vh - var(--j13-header-h) - 4rem);
}
.j13-aside--left.sidebar {
width: var(--j13-sidebar-w);
border-right: 1px solid var(--j13-border-light);
padding: 12px 0;
position: sticky;
top: var(--j13-header-h);
max-height: calc(100vh - var(--j13-header-h));
overflow-y: auto;
}
.j13-aside--right {
width: var(--j13-aside-w);
border-left: 1px solid var(--j13-border-light);
padding: 12px;
gap: 10px;
}
.j13-main {
border: 0;
border-radius: 0;
padding: 0;
background: var(--j13-surface);
}
.sidebar-section {
margin: 0;
padding: 4px 12px 8px;
font-size: 11px;
font-weight: 600;
color: var(--j13-muted);
text-transform: none;
letter-spacing: 0;
}
.sidebar-section--boards { margin-top: 8px; }
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 2px;
padding: 0 8px;
}
.sidebar-nav-item {
display: flex;
align-items: center;
gap: 8px;
height: 36px;
padding: 0 12px;
border-radius: 8px;
color: var(--j13-text);
text-decoration: none;
font-size: 13px;
border-left: 0;
}
.sidebar-nav-item:hover { background: var(--j13-bg-block-muted); color: var(--j13-text); }
.sidebar-nav-item.active {
background: var(--j13-green-bg);
color: var(--j13-green);
font-weight: 600;
}
.sidebar-nav-item__meta {
margin-left: auto;
color: var(--j13-muted);
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.sidebar-board-icon { font-size: 10px; opacity: 0.88; }
.sidebar-board-icon--0 { color: var(--board-0-color); }
.sidebar-board-icon--1 { color: var(--board-1-color); }
.sidebar-board-icon--2 { color: var(--board-2-color); }
.sidebar-board-icon--3 { color: var(--board-3-color); }
.sidebar-board-icon--4 { color: var(--board-4-color); }
.sidebar-board-icon--5 { color: var(--board-5-color); }
.sidebar-board-icon--6 { color: var(--board-6-color); }
.sidebar-board-icon--7 { color: var(--board-7-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-0 { background: var(--board-0-bg); color: var(--board-0-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-1 { background: var(--board-1-bg); color: var(--board-1-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-2 { background: var(--board-2-bg); color: var(--board-2-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-3 { background: var(--board-3-bg); color: var(--board-3-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-4 { background: var(--board-4-bg); color: var(--board-4-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-5 { background: var(--board-5-bg); color: var(--board-5-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-6 { background: var(--board-6-bg); color: var(--board-6-color); }
.sidebar-nav-item--board.active.sidebar-nav-item--board-7 { background: var(--board-7-bg); color: var(--board-7-color); }
.j13-aside__section, .j13-aside__link { display: none; }
.feed-toolbar {
display: flex;
align-items: center;
padding: 10px 16px;
border-bottom: 1px solid var(--j13-border-light);
margin: 0;
}
.feed-sort-bar {
display: flex;
gap: 2px;
border: 0;
padding: 0;
background: transparent;
border-radius: 0;
}
.feed-sort-tab {
display: inline-flex;
align-items: center;
height: 28px;
padding: 0 10px;
border-radius: 6px;
color: var(--j13-muted);
text-decoration: none;
font-size: 13px;
background: transparent;
}
.feed-sort-tab:hover { background: var(--j13-bg-block-muted); color: var(--j13-text); }
.feed-sort-tab.active,
.feed-sort-tab.is-active {
background: var(--j13-green-bg);
color: var(--j13-green);
font-weight: 600;
}
.j13-search-advanced {
margin: 0;
padding: 0 16px 10px;
border-bottom: 1px solid var(--j13-border-light);
}
.post-list { list-style: none; margin: 0; padding: 0; }
.post-row {
display: flex;
align-items: flex-start;
gap: 12px;
width: 100%;
padding: 16px 20px;
border-bottom: 1px solid var(--j13-border-light);
border-radius: 0;
margin: 0;
border-top: 0;
background: transparent;
transition: background var(--j13-ease);
}
.post-row:hover { background: var(--j13-bg-block-accent); }
.post-row--v2 { padding: 8px 16px; gap: 8px; }
.post-row--v2.post-row--title-only { padding: 12px 16px; }
.post-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
background: var(--j13-green-bg);
color: var(--j13-green);
display: grid;
place-items: center;
font-weight: 600;
font-size: 14px;
}
.post-avatar img { width: 100%; height: 100%; object-fit: cover; }
.post-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.post-main--with-thumb {
flex-direction: row;
align-items: stretch;
gap: 10px;
}
.post-content { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
.post-title-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.post-title {
color: var(--j13-text);
text-decoration: none;
font-weight: 600;
font-size: 15px;
line-height: 1.4;
letter-spacing: 0;
}
.post-row:hover .post-title { color: var(--j13-green); }
.post-excerpt {
margin: 0;
font-size: 13px;
line-height: 1.45;
color: var(--j13-muted);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.post-meta--inline { display: flex; margin-top: 2px; }
.post-meta-left {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 6px;
font-size: 12px;
color: var(--j13-muted);
min-width: 0;
}
.post-meta-author { color: var(--j13-muted); text-decoration: none; font-weight: 500; }
.post-meta-author:hover { color: var(--j13-green); }
.post-meta-sep { opacity: 0.6; }
.post-meta-last-reply { display: inline-flex; gap: 4px; align-items: center; }
.post-list-board-badge {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-decoration: none;
background: var(--board-0-bg);
color: var(--board-0-color);
}
.board-badge--1 { background: var(--board-1-bg); color: var(--board-1-color); }
.board-badge--2 { background: var(--board-2-bg); color: var(--board-2-color); }
.board-badge--3 { background: var(--board-3-bg); color: var(--board-3-color); }
.board-badge--4 { background: var(--board-4-bg); color: var(--board-4-color); }
.board-badge--5 { background: var(--board-5-bg); color: var(--board-5-color); }
.board-badge--6 { background: var(--board-6-bg); color: var(--board-6-color); }
.board-badge--7 { background: var(--board-7-bg); color: var(--board-7-color); }
.post-pin-badge,
.post-feature-badge {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
background: #fef3c7;
color: #92400e;
}
.post-feature-badge { background: var(--j13-green-bg); color: var(--j13-green); }
.post-pin-badge--board { background: #e0f2fe; color: #075985; }
.post-aside { display: flex; align-items: flex-end; gap: 10px; flex-shrink: 0; }
.post-thumb {
width: 64px;
height: 48px;
border-radius: 4px;
overflow: hidden;
background: var(--j13-bg-block-muted);
display: block;
}
.post-thumb img { width: 100%; height: 100%; object-fit: cover; }
.post-stats {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--j13-muted);
font-variant-numeric: tabular-nums;
}
.post-stat { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
.j13-post-item { display: none; } /* 旧列表类停用 */
.j13-widget,
.j13-checkin {
border: 1px solid var(--j13-border-light);
border-radius: 10px;
background: var(--j13-surface);
padding: 12px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.j13-widget__title {
text-transform: none;
letter-spacing: 0;
font-size: 13px;
color: var(--j13-text);
font-weight: 600;
}
.j13-btn, .j13-btn--compose, .header-compose-btn {
background: var(--j13-green);
border-color: var(--j13-green);
}
.j13-btn:hover { background: var(--j13-green-hover); border-color: var(--j13-green-hover); }
a { color: var(--j13-green); }
@media (max-width: 900px) {
.j13-layout { grid-template-columns: var(--j13-sidebar-w) minmax(0, 1fr); }
.j13-aside--right { display: none; }
.j13-header__inner.header-inner { display: flex; flex-wrap: wrap; }
.header-search-wrap { order: 3; flex: 1 1 100%; max-width: none; }
}

View File

@@ -60,6 +60,33 @@ document.documentElement.dataset.j13Ssr = "1";
} }
})(); })();
(function headerSearchUX() {
var input = document.getElementById("j13-header-search-input");
var filterBtn = document.getElementById("j13-search-filter-btn");
var advanced = document.getElementById("search");
if (filterBtn && advanced) {
filterBtn.addEventListener("click", function () {
advanced.open = true;
filterBtn.classList.add("header-search-filter-btn--active");
var kw = advanced.querySelector('input[name="keyword"]');
if (kw) kw.focus();
advanced.scrollIntoView({ behavior: "smooth", block: "nearest" });
});
}
document.addEventListener("keydown", function (e) {
if (!(e.ctrlKey || e.metaKey) || (e.key !== "k" && e.key !== "K")) return;
var tag = (e.target && e.target.tagName) || "";
if (tag === "INPUT" || tag === "TEXTAREA" || (e.target && e.target.isContentEditable)) {
if (e.target === input) return;
}
e.preventDefault();
if (input) {
input.focus();
input.select();
}
});
})();
(function mdEditors() { (function mdEditors() {
function insertAtCursor(textarea, text) { function insertAtCursor(textarea, text) {
const start = textarea.selectionStart || 0; const start = textarea.selectionStart || 0;