# 界面设计 前端的视觉与交互权威文档。协议、接口以 [protocol.md](protocol.md)、[api.md](api.md) 为准,本文只定义它们在界面上如何呈现。 ## 1. 原则 1. **工具感的实用极简**:界面是载体,内容是主角。信息密度适中,层级靠字号、字重、间距,不靠装饰。 2. **诚实**:安全状态如实呈现,不夸大。服务端能看到的、不能保证的,界面必须说清楚(见 §7)。 3. **手机与电脑同一套骨架**:同一组组件,按宽度重排,不做两套界面。 4. **克制的质感**:只在两处用 blur,只在浮层用阴影。 ## 2. 反 AI 味清单(禁止项) | 禁止 | 原因 | |---|---| | 装饰性渐变、霓虹色、彩色大色块 | 默认 AI 模板特征。渐变只允许用来表达状态或层次,见 §4 | | 超过 12px 的圆角、pill 形容器 | 同上 | | 卡片套卡片、满屏悬浮玻璃面板 | 层级混乱,且耗性能 | | 眉标签(eyebrow)、装饰性副标题、营销式文案 | 工具不需要 | | Tailwind 默认式重阴影 | 见 §4 | | 默认的 Inter / Roboto,默认细线图标集原样使用 | 无个性 | ## 3. 色彩 灰白为主,亮暗两套。默认跟随系统(`prefers-color-scheme`),顶栏的外观菜单可手动指定「跟随系统 / 亮色 / 暗色」,选择只存在本机 `localStorage`,启动时在渲染前应用,避免闪烁。暗色变量在 `tokens.css` 里出现两次(跟随系统与手动指定),修改必须同步。 | token | 用途 | |---|---| | `--bg` / `--surface` | 页面底 / 输入、浮层底 | | `--line` | 1px 分隔线,替代卡片边框 | | `--text` / `--text-dim` | 正文 / 次要信息(时间、设备名) | | `--accent` | 主按钮、焦点、选中态,与正文同为墨色 | | `--ok` | enc 与已认证完整 | | `--warn` | raw 模式、不完整、下载次数将尽 | | `--danger` | 篡改、错误、删除 | | `--busy` | 进行中、重连中 | **语义色只表达安全与状态,不做装饰。** 颜色字面量只允许出现在 `src/styles/tokens.css`,其余 CSS 一律 `var(--*)`,由 stylelint 强制。亮暗两套的语义色都要过对比度(正文 ≥ 4.5:1)。 ## 4. 形状、阴影、blur | 项 | 规则 | |---|---| | 圆角 | 三档:`--radius-sm` 4、`--radius-md` 8、`--radius-lg` 12。只能用 token | | 分隔 | 列表用 1px `--line`,不用阴影、不用卡片 | | 阴影 | 只有 `--shadow-pop`,只给菜单、弹窗、Toast | | blur | **仅顶栏与手机底部动作栏**使用 `backdrop-filter: var(--blur-bar)`,半径 10px | 渐变:低饱和、小面积,只表达状态或层次,定义为 `tokens.css` 里的 token(`--fade-down`、`--tile-gradient`、`--progress-gradient`、`--warn-bar`、`--*-soft` 底色): | 用处 | 做法 | |---|---| | 顶栏下沿 | 向下渐隐到页面底色,代替硬分隔线 | | raw 警示 | 顶栏下沿换成暖色渐隐条 | | 传输进度 | 细条,左浅右深 | | 冲突、篡改、警示行 | 左侧色条 + 向右渐隐的同色底 | | 在线点 | 缓慢呼吸,不用发光阴影 | blur 的性能约束: - 全页面同时存在的 `backdrop-filter` 元素不超过 2 个。 - 禁止用于滚动列表中的元素,以及弹窗遮罩(遮罩用纯色 `--scrim`)。 - 必须有回退:`@supports not (backdrop-filter: blur(1px))` 与 `prefers-reduced-transparency: reduce` 时,背景退化为不透明的 `--bg`。 - 玻璃背景下的文字对比度按「最差底色」验证,不得依赖背后内容。 ## 5. 字体与图标 | 用途 | 字体 | |---|---| | 界面文字 | 思源黑体(`--font-sans`,系统无衬线兜底) | | 链接、哈希、密钥、文件大小 | 等宽(`--font-mono`) | 字号档位不超过 5 档,字重只用 400 与 600。图标自绘 SVG,统一 1.5px 描边、24 网格,放在 `ui/icons/`,不引入图标库。 ## 6. 布局与页面 WebDrop 是**暂存区**,不是聊天室:内容按类型与待办动作组织,不按「谁发的」;发送设备只作为一行小字。 ### 6.1 手机(< 900px):最新 + 分类 + 底部动作栏 ``` ┌────────────────────────────┐ │▒ abc123 🔒加密 ●●○ 2/3 🔍 ◐ ⇧│ ← 模糊顶栏 ├────────────────────────────┤ │ 最新 │ 最近一项放大,拇指区一键取走 │ [图标] 屏幕录制.webm │ │ iPhone · 14:20 · 1.8 MB │ │ [ 预览 ] [ 下载 ] │ 文本则是 [ 复制 ] ├────────────────────────────┤ │ ▸ 便签 待办… │ 折叠,点开编辑 ├────────────────────────────┤ │ [全部][文件][文本][图片]… │ 类型分组常驻(可横向滑动) │ [缩略] project-assets.zip ⬇│ │ [缩略] 设计稿.svg v3 有更新 ⬇│ │ [文本] 明天 10 点开会… ⧉ │ ├────────────────────────────┤ │▒ 粘贴 文件 相册 拍照 扫码 写 │ ← 模糊底部动作栏 └────────────────────────────┘ ``` - 底部是**动作**而不是输入框:粘贴(读剪贴板,文字或图片)、文件、相册、拍照、扫码、写。点「写」才弹出输入。 - 点列表项或「最新」卡打开底部抽屉详情(预览、操作、历史)。 ### 6.2 电脑(≥ 900px):列表 + 内联预览 + 窄栏 ``` ┌───────────────────────────────────────────────────┬───┐ │ abc123 🔒加密 ●●○ 2/3 🔍 ◐ │ ‹ │ ├────────────────────┬──────────────────────────────┤2/3│ │ [粘贴][文件][扫码][写]│ 屏幕录制.webm │ ⇧ │ │ ▸ 便签 待办… │ iPhone · 14:20 · 剩 8 次下载 │ │ │ [全部 文件 文本 …] │ [下载] [删除] │ │ │ ☐ [缩略] zip ⬇ │ ┌────────────────────────┐ │ │ │ ☐ [缩略] 设计稿 ⬇ │ │ 内联预览(视频/图片/目录) │ │ │ │ ☐ [文本] 开会… ⧉ │ └────────────────────────┘ │ │ │ 已选 2 项 [下载] │ 修改历史 v3 当前 / v2 / v1 │ │ └────────────────────┴──────────────────────────────┴───┘ 全局:Ctrl+V 粘贴即发送;拖到窗口任意位置即上传 ``` - 点左侧一行,右侧内联预览,不弹窗来回开关;默认选中最新一项。 - 多选后可批量下载。 - 设备与分享是低频操作,默认收成右侧窄栏(在线数、分享图标),需要时展开。 - 窗口任意位置 `Ctrl+V` 都能直接发送(在输入框内粘贴时不拦截);拖到窗口任意位置即上传。 ### 6.3 顶栏 | 控件 | 作用 | |---|---| | 频道 id + 加密/明文徽标 | 当前频道与安全状态(§7.1) | | 在线点 + `N/M` | 设备在线情况;手机上点开设备与分享抽屉 | | 连接状态 | 仅在重连中或离线时显示 | | 搜索 | 展开关键字输入(文本内容或文件名);再次点击关闭并清除关键字。类型分组一直可见 | | 外观 | 跟随系统 / 亮色 / 暗色 | | 设备与分享(仅手机) | 打开抽屉 | ### 6.4 首页(初始状态)与页面 首页把「临时分享」与「设备频道」作为两个并列入口:前者用于发一段临时文本或文件,后者用于多设备长期同步。 ``` ┌──────────────────────────────────┐ │ WebDrop │ │ 在自己的设备之间传文件和文本,也可分享给别人。│ │ ┌ 临时分享 ───────────────────┐ │ │ │ 输入或粘贴临时文本… │ │ │ │ 拖入文件 / 文件名列表 │ │ │ │ [选择文件] [粘贴] [创建临时分享]│ │ │ └────────────────────────────┘ │ │ 我的频道(有才显示) │ │ abc123… 🔒加密 所有者 3 台设备 │ │ ┌ 设备频道 ───────────────────┐ │ │ │ 多台设备之间长期同步 │ │ │ │ [ 新建频道 ] [ 加入频道 ] │ │ │ └────────────────────────────┘ │ └──────────────────────────────────┘ ``` | 操作 | 结果 | |---|---| | 快速分享(输入文本、拖入、粘贴、选择、相册、拍照) | 先在首页编辑临时文本并收集文件;点「创建临时分享」后建立加密草稿并带入内容;点「完成分享」后生成链接与二维码 | | 新建频道 | 抽屉:模式(加密 / 明文)、设备数、保留时间、允许成员上传;创建后直接展示分享面板与二维码 | | 加入频道 | 扫码或粘贴链接 → 加入预览 → 加入 | | 我的频道 | 点一行进入;垃圾桶只是从本机移除,**不会删除频道**,需二次确认 | | 路由 | 页面 | |---|---| | `/` | 首页 | | `/c/:id` | 已在「我的频道」里 → 频道暂存区;否则链接里带密钥 → 整页加入预览;否则「无法打开此频道」 | 别人发来的链接打开时,页面一加载就把 `#` 后面的密钥取走并清掉地址栏,再显示加入预览,确认后才加入。 频道分享面板与分享页里的二维码只给**成员链接**;所有者链接含管理权限,只提供复制,不出二维码。二维码固定深色码点、浅色底,暗色主题下也不反转,否则很多扫码器识别不了。 ### 6.4.1 分享(快速分享的结果) **分享和频道是两种东西,底层只维护一套**:分享是频道的一个预设——成员不可上传、删除策略仅所有者,并且创建者在点「完成分享」的那一刻**丢弃所有者密钥**,从此没有房主,任何人(包括创建者)都不能再追加、修改或删除,只能等 TTL 到期。 ``` 草稿(创建者) 已封存(创建者与访客) ┌────────────────────────────────┐ ┌──────────────────────┐ │ 新建分享 · 草稿 [粘贴][文件][写]│ │ 我的分享 · 已封存只读 │ ├───────────────┬────────────────┤ │ 分享链接 [复制][二维码]│ │ 内容清单 │ 选中的内容 │ │ 3 项内容 [全部下载] │ │ ▢ a.zip 46MB ✕│ [文件预览] │ │ 〃 开会地址… │ │ 〃 文本 120B ✕ │ [替换文件] │ │ ▢ a.zip [下载] │ │ │ │ └──────────────────────┘ │ │ 文本可直接编辑 │ ├───────────────┴────────────────┤ │ 有效期 · 最多访客 · 下载次数 │ │ □ 我已了解,完成后无法再修改 │ │ [ 完成分享 ] │ └────────────────────────────────┘ ``` | 阶段 | 说明 | |---|---| | 草稿 | 左侧列出内容,选择后在右侧预览;创建者还握着所有者密钥,可直接修改文本、替换或移除文件,并调整有效期、最多访客、每个文件下载次数。**此时没有分享链接**,不会被别人看到 | | 完成分享 | 页面内勾选「完成后会生成分享链接和二维码。此后任何人(包括你)都不能再添加、修改或删除内容,只能等它过期」后,按钮才可用;点击即生成链接与二维码 | | 已封存(创建者) | 只读列表 + 「分享给别人」卡片(链接、二维码、说明) | | 已封存(访客) | 只读列表,只能预览、下载、复制;「全部下载」按钮 | - 访客打开链接后先看加入预览,确认才占席位。访客那一端如何识别「分享」:`members_can_upload=false` 即渲染为只读收件页,不需要新协议字段。 - 创建者的所有者链接不展示、不出二维码。 - 首页分成「我的分享」(草稿 / 已封存 / 收到的,带剩余时间)和「我的频道」。 其他弹层: | 名称 | 说明 | |---|---| | 条目详情 | 手机为底部抽屉,电脑为内联预览区;内容一致 | | 扫码加入 | 相机识别二维码(浏览器自带 `BarcodeDetector`,不支持或无权限时退回「粘贴链接」)→ 加入预览(模式、权限)→ 加入后进入该频道 | | 写文本 | 抽屉,`Ctrl+Enter` 发送;首页上用于快速分享 | | 频道设置 | 尚未实现(席位管理等) | 扫到的链接含密钥,只放在内存里,不写日志、不持久化。 > 原型说明:前端目前是验证设计的框架,频道列表只在内存里,刷新即清空;真实实现把链接存进 IndexedDB(architecture §6)。 ### 6.5 条目 列表里的一行按内容类型决定画法,**不大的文本、JSON、视频直接画在行里,不用点开**: | 内容 | 列表里的画法 | 快捷操作 | |---|---|---| | 文本 | 内容本身(没有标题);太长只显示前 6 行,点击进详情 | 复制 | | JSON(能解析成对象或数组) | 格式化后等宽显示;超过 14 行或 1200 字符截断 | 复制 | | 不大的视频(≤ 25 MB) | 行内播放器(`controls`、`preload="metadata"`) | 下载 | | 图片 | 缩略图 + 标题 + 小字 | 下载 | | 压缩包、其他文件、大视频 | 类型图标 + 标题 + 小字 | 下载 | 小字依次为:设备 · 时间 · 大小 · 剩余下载次数,后面跟状态标签:`v3`(有历史)、`有更新`、`冲突`。传输中在行内显示细进度条。点标题行或文本区域打开详情(手机底部抽屉,电脑内联预览区)。 **不能烧下载次数**:按协议,文件超过 1 MiB 或设了下载上限就是 GATED,每次拉取都计一次下载(api.md)。所以: - 文本与 JSON 内容本来就在内存里,一律直接画。 - 没有下载上限的视频、图片自动加载;**有下载上限的**不自动拉取:图片显示类型图标,视频显示占位「点击播放 · 会消耗 1 次下载」,点了才加载。 - 滚动列表、刷新页面都不应该消耗下载次数。 - 长列表使用 `content-visibility: auto`。注意它会让后代的 `position: fixed` 失效,弹层必须用 portal 挂到 `body`。 - **新的在前**。 - 内联渲染一律是纯文本或原生媒体元素,不执行内容(threat-model §4)。 预览: | 类型 | 呈现 | |---|---| | 图片、视频、音频 | 原生元素播放 blob URL,不执行内容 | | 文本、JSON | 纯文本等宽展示,JSON 格式化,最多读取前 200,000 字符 | | 压缩包 | 只列出目录树与大小,不解压、不执行 | | 其他 | 提示「暂不支持预览」,保留下载 | ### 6.6 修改、历史与便签 - **修改 = 追加新版本,后写者胜**(不改协议)。文本编辑后显示「已编辑」;两台设备同时改同一条时,后到者生效,先到者的内容以冲突提示展示:「某设备同时改过:… [用他的][保留我的]」。 - **文件修改历史**:新版本是一个替代旧条目的新条目,界面把它们折叠成同一个文件。条目显示 `v3`;别的设备更新了你还没看过的文件时显示「有更新」,详情里有一行「某设备 更新了此文件 [知道了]」;详情下方列出历史版本,可单独下载旧版本。需要在加密的 `meta_json` 里加可选字段 `replaces`(旧 `item_id`),后端不解析 meta,待写入 protocol §7。 - **便签**是频道唯一的快照槽(protocol §9),多设备实时同步,最后写入者胜,不做字符级合并。默认折叠,折叠时显示第一行;他人正在编辑时显示「某设备 正在编辑」。 - 暂不做字符级协同编辑(需要新增协议条目类型,属于后续议题)。 ## 7. 状态与诚实提示 ### 7.1 频道安全徽标(顶栏) | 状态 | 呈现 | |---|---| | enc | 小号锁图标 + 「加密」,`--ok`,点开说明「内容在浏览器内加密,服务端只见密文」 | | raw | **整条顶栏下沿加 `--warn` 色条**,文字「明文 · 服务端可见」,不可收起 | ### 7.2 条目状态 条目状态只有以下几种,由帧推导,组件按判别联合穷尽处理: | 状态 | 条件 | 呈现 | |---|---|---| | `receiving` | 无 FINAL,帧连续 | `--busy` 进度,标注「不完整」 | | `complete` | 已认证 FINAL 且 0..n 连续 | 正常 | | `incomplete` | 缺口,或长时间无 FINAL | `--warn`,可重试拉取 | | `tampered` | 认证失败 | **整条替换为警示行**,`--danger`,说明「内容被篡改,已丢弃」,不展示任何内容 | | `expired` / `removed` | tombstone | 淡出后移除,可短暂保留一条「已过期 / 已删除」提示 | ### 7.3 连接状态(顶栏) `在线` · `重连中` · `离线(N 条待发送)`。离线时仍可添加内容,发送进入队列,恢复后自动补发,无需用户操作。 ### 7.4 必须如实表述的内容 | 场景 | 文案原则 | |---|---| | `ui_restrict_copy_download` | 说明这是界面限制,「成员仍可通过其他方式保存内容」,不暗示安全保证 | | 下载次数 | 下载前提示「此文件剩余 N 次下载」;达上限提示「已被删除」 | | 链接 | 分享前说明「持有链接即可访问」;成员链接与所有者链接分开展示,所有者链接标注「含管理权限」 | | 历史可读 | 加入说明处写明「持有链接的人可读取此频道全部历史」 | | 元数据 | 创建与设置页注明「不隐藏 IP 与访问时间」 | ### 7.5 空与错误状态 | 码 | 页面 | |---|---| | `seats_full` | 加入预览页:「席位已满」,告知联系所有者 | | `not_joined`(被释放) | 频道页整体替换为「你已被移出此频道」 | | `not_found` / `gone` | 「频道不存在或已过期」 | | `rate_limited` | Toast,带重试倒计时 | | 链接缺密钥 / 模式不符 | 拒绝进入,明确说明原因(降级防御,见 protocol §2) | ## 8. 交互 | 场景 | 行为 | |---|---| | 发送文本 | 点「写」弹出输入,`Ctrl+Enter` 发送 | | 粘贴 | 窗口任意位置 `Ctrl+V` 直接发送;手机点「粘贴」读取剪贴板(无权限时退回「写」)。文件或图片直接入队,先出现「传输中」条目 | | 拖放 | 桌面整页接收拖放;拖入时出现覆盖提示,不改变布局 | | 复制 | 点击复制,按钮短暂变为「已复制」,不弹 Toast | | 删除 | 单击删除后条目消失并给 5 秒「撤销」(撤销只在本地延迟提交删除) | | 新条目到达 | 若用户已滚到底部则自动跟随,否则底部出现「N 条新内容」 | | 触控 | 可点击区域 ≥ 44px;底栏避开安全区(`env(safe-area-inset-*)`) | ## 9. 动效 使用 React `ViewTransition`(稳定版 React 19.3 已包含),原则是**只在有空间关系处动**: | 场景 | 动画 | |---|---| | 首页 ↔ 频道 ↔ 设置(层级导航) | 方向性滑入 `nav-forward` / `nav-back` | | 条目新增 | CSS 淡入(`row-in`)。store 经 `useSyncExternalStore` 订阅,更新不在 transition 内,`ViewTransition` 不会触发,所以不用它 | | 开屏 | 单线手写 `WebDrop` 逐笔画出,约 1.2s,仅每个会话首次出现,点击或按键可跳过;加载未完成时停在末笔 | | 桌面左栏切换频道 | 淡入淡出,不滑动 | | 条目状态变化、重连、后台同步 | **无动画**(`default="none"`) | 规则: - 一律 `default="none"`,显式开启需要的触发。 - 时长:退出 150ms、进入 210ms、移动 400ms,取自 `--duration-*`。 - 必须尊重 `prefers-reduced-motion`(已在 `base.css`)。 - 顶栏、底栏用 `viewTransitionName` 隔离,并采用 backdrop-blur 的专用写法,避免闪烁。 - 不支持的浏览器直接无动画,功能不受影响。 ## 10. 无障碍 - 所有交互元素可键盘操作,焦点环可见(`--accent`)。 - 状态不只靠颜色,配图标与文字。 - 动态内容(新条目、连接状态)用 `aria-live="polite"`。 - 触控目标 ≥ 44px;文本可缩放到 200% 不破版。 ## 11. 文案 中文为默认语言;文案用语直白,不用感叹号与营销口吻。暂不做多语言,文案直接写在组件里。 ## 12. 手机适配(iPhone / Android) | 方面 | 做法 | |---|---| | 刘海与手势条 | `viewport-fit=cover`;顶栏用 `env(safe-area-inset-top)`,底部动作栏与抽屉用 `inset-bottom`,手机布局两侧用 `inset-left/right`(横屏) | | 视口高度 | `100vh` 兜底 + `100dvh`,避开地址栏伸缩 | | 软键盘 | 需要输入的抽屉(写文本)从**顶部**出来,iOS 不缩小布局视口,底部抽屉会被键盘遮住;Android 用 `interactive-widget=resizes-content` | | 输入框缩放 | 触屏上 input / textarea 字号 16px,避免 iOS 聚焦时自动放大 | | 点按 | `touch-action: manipulation` 去掉双击缩放延迟;去掉点按高亮;触屏上可点区域 ≥ 44px;`hover` 样式只在支持 hover 的设备生效,避免粘在上一次点击处 | | 返回键 | 弹层打开时压入一条历史记录,Android 返回键 / iOS 边缘滑动关闭弹层而不是离开页面(`lib/useBackToClose.ts`,这是唯一直接操作 history 的地方) | | 状态栏颜色 | `theme-color` 跟随当前页面底色,手动切换亮暗和系统切换时都会更新 | | Safari 旧版 | `backdrop-filter` 同时写 `-webkit-` 前缀 | | 横屏 / 矮屏 | 高度 < 500px 时隐藏「最新」卡、动作栏改为横排;宽 ≥ 900 **且**高 ≥ 500 才用电脑布局,横屏手机不会被当成电脑 | | 窄屏 | < 360px 时顶栏只留在线点,条目小字允许换行 | | 相机 | 「拍照」用 ``,iOS 与 Android 都会直接拉起相机;扫码在 Android Chrome 用 `BarcodeDetector`,iOS Safari 不支持,提示用系统相机扫码后打开链接,或粘贴链接。相机、剪贴板读取都要求 https(`localhost` 除外) | | 剪贴板 | iOS 读取时会弹出系统「粘贴」确认,Android 会要求权限;被拒绝时退回「写」 | | 图片格式 | iPhone 的 HEIC 照片在非 Safari 浏览器里无法显示,预览失败时提示直接下载 | | 下载 | iOS 上 `a[download]` 配合 blob 会弹出存储菜单;大文件要走 Service Worker 流式下载(架构 §6,阶段 4) |