From 197d53ea0bfa4d4165eb28fece2b7c640450f251 Mon Sep 17 00:00:00 2001 From: PathGao <42336971+PathGao@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:59:09 +0800 Subject: [PATCH 1/2] docs: add DESIGN.md and rename roadmap Trying to Doing DESIGN.md (with zh-Hans mirror) holds the decided glossary, product decisions and visual rules, each citing its source PR, doc or file. VISUAL-SYSTEM.md stays for its open window-width question and points to DESIGN.md. --- docs/DESIGN.md | 45 +++++++++++++++++++++++++++++++++++ docs/DESIGN.zh-Hans.md | 45 +++++++++++++++++++++++++++++++++++ docs/ROADMAP.md | 2 +- docs/ROADMAP.zh-Hans.md | 2 +- docs/VISUAL-SYSTEM.md | 2 ++ docs/VISUAL-SYSTEM.zh-Hans.md | 2 ++ 6 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 docs/DESIGN.md create mode 100644 docs/DESIGN.zh-Hans.md diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..a0e5ec3 --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,45 @@ +# Nifro Design + +[简体中文](DESIGN.zh-Hans.md) + +This document records the names, behaviour and visual rules that have been decided for Nifro's interface. Each entry cites the pull request, document or source file where it was decided. Questions that are still open stay out until they are decided. A pull request that changes the interface updates this file. + +## 1. Glossary + +| ID | Concept | English | 简体中文 | Do not use | Why | +|---|---|---|---|---|---| +| G1 | An ordered list of websites that a display selects | Playlist | 播放列表 | “Playlist” for the mechanism that moves a display between websites; that is rotation | The word named two things; the list is what a user calls a playlist (#62, #64) | +| G2 | A saved entry with its address and settings | Website | 网站 | — | Saved websites are kept distinct from the page currently shown (#123) | +| G3 | What a display is showing now | Page | 网页 | “No Website” / 无网站 for a display with nothing selected | Same distinction; the empty state reads “No page selected” / 未选择网页 (#123) | +| G4 | Choosing which part of a page fills the wallpaper | Crop | 裁切 | Choose Region | Help text names the panel's Crop button (#40, #123) | +| G5 | Using the wallpaper page with the pointer and keyboard | Browsing Mode | 浏览模式 | browsing mode | Capitalised as a name (#123) | +| G6 | How a display moves between websites | Rotate in order; Rotate randomly; Keep this page displayed | 按顺序轮播;随机轮播;固定显示此页面 | Rotating in order; Rotating in shuffled order; Staying on this website / 固定在这个网站 | #123 | +| G7 | Switching a state | Turn … on or off | 打开或关闭… | Toggle … / 切换… | #123 | +| G8 | Relaunching the app to apply a change | Restart | 重新启动 | Reopen / 重新打开 | #123 | +| G9 | Proposing a site for the gallery | Suggest a site… | 推荐网站… | Submit a Site… / 提交站点… | #123 | +| G10 | The display that holds the menu bar | Main Display | 主显示器 | — | The label and the behaviour mean the menu-bar display, never the screen with keyboard focus (#17) | +| G11 | The app | Nifro | Nifro | Plash as the app's own name, icon or artwork; the credit in About is the one mention | Nifro is an independent fork ([CONTRIBUTING](../.github/CONTRIBUTING.md)) | + +## 2. Product decisions + +- **B1 Displays and playlists.** Each display selects a playlist and keeps its own current website, rotation mode, interval, on/off state and Browsing Mode. A duplicated playlist gets new website IDs, so crop, sign-in and page storage stay separate. See [Playlist design](PLAYLIST-REFACTOR.md) and the roadmap's Done section. +- **B2 Panel and Websites window.** The menu-bar panel has one column per display and controls displays; the Websites window edits website records. Website editing and shortcut configuration stay out of the panel (#21, roadmap `X12`–`X14`). +- **B3 One commit point in the panel.** Choosing a website is the only commit, and it writes the playlist and the website together. Choosing a playlist in the chooser changes only what the website chooser offers (#99). +- **B4 Random rotation.** A display rotating randomly deals a shuffled order: every website is shown before any is shown again, and each new order starts with the website already on screen (#99). +- **B5 Crop.** A region is stored as page position and magnification, not as fixed pixels, so it adapts to displays of other shapes (roadmap Done). +- **B6 Settings panes.** General holds the app's own affairs: launch, language, version and author. Behavior holds what the desktop shows. Shortcuts holds the keyboard shortcuts. Advanced holds the settings that can leave the app in a state its owner did not intend, and Restore All Settings last (#34, #69). +- **B7 Help text.** Each setting's explanation is one sentence that says what the user needs in order to keep or change the default. Restore All Settings is the exception: its labelled Reset and Kept lines guard an irreversible action (#40). The explanation opens from an ⓘ button beside the setting's name, and the tooltip remains ([`SettingHelp`](../Sources/Nifro/Screens/SettingHelp.swift)). +- **B8 Destructive actions.** They ask first and name the cost. Clear All Website Data says every website will be signed out and that websites and their settings are kept (#30). Deleting a website asks because the app has no Undo for it, and the confirming button names the action (#37). Restore All Settings makes Cancel the default button ([`RestoreDefaults`](../Sources/Nifro/App/RestoreDefaults.swift)). +- **B9 Languages.** English and Simplified Chinese. Untranslated text stays in English (README; the language setting's help text). + +## 3. Visual design + +- **A1 System and custom surfaces.** Settings, Websites, Add Website and Site Gallery use stock controls in grouped forms, which follow the accent colour, Increase Contrast, Reduce Transparency and VoiceOver ([Visual vocabulary](VISUAL-SYSTEM.md) §3–4). +- **A2 Panel controls.** Stock bordered controls at the large control size. A control with a lit state is a toggle and lights in the user's accent colour; the app icon's orange is no longer a lit state. `accessoryBar` is not used, because it draws no bezel at rest over an arbitrary page (#73, #121). +- **A3 Panel surface.** The panel sits on the regular material and always draws its controls in the active appearance, including while Browsing Mode holds the keyboard (#73). +- **A4 Column metrics.** A column is 260 pt wide. The card behind it has a 12 pt radius and the picture inside it 8 pt; they are nested, so the radii differ on purpose. Controls in a row are 4 pt apart. Choosers size to their label up to a ceiling: three quarters of the column for the playlist chooser, seven eighths for the website chooser ([`PanelMetrics`](../Sources/Nifro/Screens/PanelControls.swift)). +- **A5 Column hover.** A quaternary fill and a 2 pt accent border, in both light and dark appearance (#29). +- **A6 Colour.** Captions and the panel's failure line use the secondary style, not red or the accent ([`DisplayPanel`](../Sources/Nifro/Screens/DisplayPanel.swift)). The confirming button of a destructive dialog is marked destructive (#30, [`RestoreDefaults`](../Sources/Nifro/App/RestoreDefaults.swift)). +- **A7 Crop overlay.** Black and white are contrast decisions against an arbitrary page; theme colours are the wrong answer there (#29, [Visual vocabulary](VISUAL-SYSTEM.md) §3). +- **A8 Menu bar.** The colour band has no palette; its colour is sampled from the wallpaper. The icon is drawn dim when no wallpaper is showing and pulses while a page loads ([`AppState`](../Sources/Nifro/App/AppState.swift), #73). +- **A9 Symbols.** SF Symbols throughout ([`PanelControls`](../Sources/Nifro/Screens/PanelControls.swift)). diff --git a/docs/DESIGN.zh-Hans.md b/docs/DESIGN.zh-Hans.md new file mode 100644 index 0000000..040023d --- /dev/null +++ b/docs/DESIGN.zh-Hans.md @@ -0,0 +1,45 @@ +# Nifro 设计 + +[English](DESIGN.md) + +这份文档记录 Nifro 界面已经定下来的叫法、行为和视觉规则。每一条都注明在哪个 PR、文档或源文件中定下。尚未决定的问题不写进来,定了再补。改界面的 PR 同步更新这份文档。 + +## 1. 术语表 + +| ID | 概念 | English | 简体中文 | 不用 | 理由 | +|---|---|---|---|---|---| +| G1 | 显示器所选的一组有序网站 | Playlist | 播放列表 | 用「Playlist」指让显示器在网站间切换的机制;那叫轮播(rotation) | 这个词曾同时指两件事;用户说的播放列表是那组网站(#62、#64) | +| G2 | 保存下来的条目,含地址和设置 | Website | 网站 | — | 保存的网站与当前显示的网页分开称呼(#123) | +| G3 | 显示器此刻显示的内容 | Page | 网页 | 显示器未选内容时写「No Website」/ 无网站 | 同上;空状态写「No page selected」/ 未选择网页(#123) | +| G4 | 选择网页的哪一部分铺满壁纸 | Crop | 裁切 | Choose Region | 帮助文案指向面板上的裁切按钮(#40、#123) | +| G5 | 用指针和键盘操作壁纸网页 | Browsing Mode | 浏览模式 | browsing mode | 作为名称首字母大写(#123) | +| G6 | 显示器在网站间切换的方式 | Rotate in order; Rotate randomly; Keep this page displayed | 按顺序轮播;随机轮播;固定显示此页面 | Rotating in order; Rotating in shuffled order; Staying on this website / 固定在这个网站 | #123 | +| G7 | 切换某个状态 | Turn … on or off | 打开或关闭… | Toggle … / 切换… | #123 | +| G8 | 重新启动 App 以应用更改 | Restart | 重新启动 | Reopen / 重新打开 | #123 | +| G9 | 向推荐站点提议一个网站 | Suggest a site… | 推荐网站… | Submit a Site… / 提交站点… | #123 | +| G10 | 有菜单栏的那块显示器 | Main Display | 主显示器 | — | 文案和行为都指菜单栏所在的显示器,不是键盘焦点所在的屏幕(#17) | +| G11 | 本 App | Nifro | Nifro | 把 Plash 当作本 App 的名称、图标或图像;关于页的致谢是唯一提及 | Nifro 是独立分支([CONTRIBUTING](../.github/CONTRIBUTING.md)) | + +## 2. 产品决策 + +- **B1 显示器与播放列表。** 每块显示器选择一个播放列表,并各自保存当前网站、轮播方式、间隔、开关状态和浏览模式。复制播放列表会生成新的网站 ID,因此裁切、登录和页面存储彼此隔离。见 [播放列表设计](PLAYLIST-REFACTOR.md) 和路线图的 Done 段。 +- **B2 面板与 Websites 窗口。** 菜单栏面板每块显示器一列,控制的是显示器;Websites 窗口编辑网站记录。网站编辑和快捷键配置不进面板(#21,路线图 `X12`–`X14`)。 +- **B3 面板只有一个提交点。** 只有选择网站才会生效,并同时写入播放列表和网站。在选择器里切换播放列表,只改变下方网站选择器列出的内容(#99)。 +- **B4 随机轮播。** 随机轮播的显示器会先洗好一轮顺序:每个网站都出现一次之后才会重复,每轮新顺序都从当前屏幕上的网站开始(#99)。 +- **B5 裁切。** 区域存为页面位置和放大倍数,而不是固定像素,所以能适配其他形状的显示器(路线图 Done)。 +- **B6 设置面板。** 通用放 App 自身的事务:启动、语言、版本和作者。行为放决定桌面显示什么的设置。快捷键放键盘快捷键。高级放可能让 App 处于用户未预期状态的设置,还原所有设置放在最后(#34、#69)。 +- **B7 帮助文案。** 每个设置的说明只写一句,说清用户保留或修改默认值需要知道的事。还原所有设置是例外:它的「重置」「保留」分行结构是不可撤销操作的保护(#40)。说明从设置名称旁的 ⓘ 按钮打开,同时保留悬停提示([`SettingHelp`](../Sources/Nifro/Screens/SettingHelp.swift))。 +- **B8 破坏性操作。** 先询问,并说明代价。清除所有网站数据会说明所有网站都将退出登录,网站及其设置保留(#30)。删除网站要询问,因为 App 无法撤销这一操作,确认按钮写明动作本身(#37)。还原所有设置以取消为默认按钮([`RestoreDefaults`](../Sources/Nifro/App/RestoreDefaults.swift))。 +- **B9 语言。** 英文和简体中文。尚未翻译的文字显示为英文(README;语言设置的帮助文案)。 + +## 3. 视觉设计 + +- **A1 系统界面与自绘界面。** 设置、Websites、添加网站和推荐站点使用分组表单中的系统控件,它们跟随强调色、增强对比度、降低透明度和 VoiceOver([视觉词汇](VISUAL-SYSTEM.zh-Hans.md) §3–4)。 +- **A2 面板控件。** 大尺寸的系统 bordered 控件。有点亮状态的控件是开关,点亮时使用用户的强调色;App 图标的橙色不再表示点亮。不用 `accessoryBar`,因为它静止时不画边框,放在任意网页上会看不见(#73、#121)。 +- **A3 面板底板。** 面板使用 regular material,并始终以活跃外观绘制控件,浏览模式占用键盘时也一样(#73)。 +- **A4 列的尺寸。** 每列宽 260 pt。列背后的卡片圆角 12 pt,列内图片圆角 8 pt;两者嵌套,所以圆角有意不同。同一行控件间距 4 pt。选择器按标签宽度伸缩,有上限:播放列表选择器为列宽的四分之三,网站选择器为八分之七([`PanelMetrics`](../Sources/Nifro/Screens/PanelControls.swift))。 +- **A5 列的悬停。** quaternary 填充加 2 pt 强调色描边,浅色和深色外观相同(#29)。 +- **A6 颜色。** 说明文字和面板的加载失败行使用 secondary 样式,不用红色或强调色([`DisplayPanel`](../Sources/Nifro/Screens/DisplayPanel.swift))。破坏性对话框的确认按钮标为 destructive(#30,[`RestoreDefaults`](../Sources/Nifro/App/RestoreDefaults.swift))。 +- **A7 裁切遮罩。** 黑白两色是针对任意网页的对比度选择;这里用随主题变化的颜色是错的(#29,[视觉词汇](VISUAL-SYSTEM.zh-Hans.md) §3)。 +- **A8 菜单栏。** 颜色带没有调色板,颜色取自壁纸。没有壁纸在显示时图标变暗,网页加载时图标闪动([`AppState`](../Sources/Nifro/App/AppState.swift),#73)。 +- **A9 图标。** 全部使用 SF Symbols([`PanelControls`](../Sources/Nifro/Screens/PanelControls.swift))。 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 9cfee9c..63fe52a 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -4,7 +4,7 @@ Nifro makes a web page part of the desktop. This roadmap separates hypotheses needing evidence from work ready to build, records the decisions that shape the current app, and keeps rejected directions from returning as new proposals. Legacy labels such as `P`, `L1`, and `K7` remain as indexes for code comments and older design notes. -## Trying +## Doing ### Lower idle-wallpaper cost (`P`) diff --git a/docs/ROADMAP.zh-Hans.md b/docs/ROADMAP.zh-Hans.md index ee0a6c7..fcc83b2 100644 --- a/docs/ROADMAP.zh-Hans.md +++ b/docs/ROADMAP.zh-Hans.md @@ -4,7 +4,7 @@ Nifro 的核心想法是:网页可以成为桌面的一部分。这份路线图区分还需要证据的假设、已经准备实施的工作、已经塑造当前 App 的决定,以及明确不再做的方向。旧编号如 `P`、`L1`、`K7` 保留为代码注释和旧设计文档的索引,不代表优先级。 -## Trying +## Doing ### 降低闲置壁纸的资源消耗(`P`) diff --git a/docs/VISUAL-SYSTEM.md b/docs/VISUAL-SYSTEM.md index 976abc5..f434f7f 100644 --- a/docs/VISUAL-SYSTEM.md +++ b/docs/VISUAL-SYSTEM.md @@ -1,5 +1,7 @@ # The App's Visual Vocabulary +> Decided rules are in [DESIGN.md](DESIGN.md). This audit is kept because §7 Q5 is still open. + [简体中文](VISUAL-SYSTEM.zh-Hans.md) > Written 2026-08-27 against `dead3a6` (v0.1.3). Every number below was produced by a command that is diff --git a/docs/VISUAL-SYSTEM.zh-Hans.md b/docs/VISUAL-SYSTEM.zh-Hans.md index e3a3569..61e4b9b 100644 --- a/docs/VISUAL-SYSTEM.zh-Hans.md +++ b/docs/VISUAL-SYSTEM.zh-Hans.md @@ -1,5 +1,7 @@ # 这个 app 的视觉词汇 +> 已定的规则以 [DESIGN.zh-Hans.md](DESIGN.zh-Hans.md) 为准。本文因 §7 Q5 尚未决定而保留。 + [English](VISUAL-SYSTEM.md) > 2026-08-27 对着 `dead3a6`(v0.1.3)写。下面每个数字旁边都附了产生它的命令,可以重跑,不必相信。 From b575ab2139d84aff35aa6359d1a6758ce2d0a33d Mon Sep 17 00:00:00 2001 From: PathGao <42336971+PathGao@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:12:10 +0800 Subject: [PATCH 2/2] docs: record site, gallery, reload and shortcut wording; drop the visual audit The audit's last open question (window widths) is closed as-is, so VISUAL-SYSTEM.md and its mirror are removed and DESIGN.md cites the PRs instead. --- docs/DESIGN.md | 9 +- docs/DESIGN.zh-Hans.md | 11 +- docs/VISUAL-SYSTEM.md | 397 ---------------------------------- docs/VISUAL-SYSTEM.zh-Hans.md | 346 ----------------------------- 4 files changed, 13 insertions(+), 750 deletions(-) delete mode 100644 docs/VISUAL-SYSTEM.md delete mode 100644 docs/VISUAL-SYSTEM.zh-Hans.md diff --git a/docs/DESIGN.md b/docs/DESIGN.md index a0e5ec3..5064741 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -14,9 +14,12 @@ This document records the names, behaviour and visual rules that have been decid | G4 | Choosing which part of a page fills the wallpaper | Crop | 裁切 | Choose Region | Help text names the panel's Crop button (#40, #123) | | G5 | Using the wallpaper page with the pointer and keyboard | Browsing Mode | 浏览模式 | browsing mode | Capitalised as a name (#123) | | G6 | How a display moves between websites | Rotate in order; Rotate randomly; Keep this page displayed | 按顺序轮播;随机轮播;固定显示此页面 | Rotating in order; Rotating in shuffled order; Staying on this website / 固定在这个网站 | #123 | -| G7 | Switching a state | Turn … on or off | 打开或关闭… | Toggle … / 切换… | #123 | +| G7 | Switching a state | Turn … on or off | 打开或关闭… | Toggle … / 切换… | #123; Shortcuts follow it too (2026-10-10, decided by agent at the user's request) | | G8 | Relaunching the app to apply a change | Restart | 重新启动 | Reopen / 重新打开 | #123 | | G9 | Proposing a site for the gallery | Suggest a site… | 推荐网站… | Submit a Site… / 提交站点… | #123 | +| G12 | A website, in any wording that says “site” | Site | 网站 | 站点 | 2026-10-10 user decision | +| G13 | The curated list of wallpaper-ready sites | Site Gallery | 网站库 | 推荐站点 | 2026-10-10 user decision; 推荐 is kept for Suggest a site… (G9) | +| G14 | Loading a page again | Reload | 刷新 | 重新加载 | 2026-10-10 user decision | | G10 | The display that holds the menu bar | Main Display | 主显示器 | — | The label and the behaviour mean the menu-bar display, never the screen with keyboard focus (#17) | | G11 | The app | Nifro | Nifro | Plash as the app's own name, icon or artwork; the credit in About is the one mention | Nifro is an independent fork ([CONTRIBUTING](../.github/CONTRIBUTING.md)) | @@ -34,12 +37,12 @@ This document records the names, behaviour and visual rules that have been decid ## 3. Visual design -- **A1 System and custom surfaces.** Settings, Websites, Add Website and Site Gallery use stock controls in grouped forms, which follow the accent colour, Increase Contrast, Reduce Transparency and VoiceOver ([Visual vocabulary](VISUAL-SYSTEM.md) §3–4). +- **A1 System and custom surfaces.** Settings, Websites, Add Website and Site Gallery use stock controls in grouped forms, which follow the accent colour, Increase Contrast, Reduce Transparency and VoiceOver (#73). - **A2 Panel controls.** Stock bordered controls at the large control size. A control with a lit state is a toggle and lights in the user's accent colour; the app icon's orange is no longer a lit state. `accessoryBar` is not used, because it draws no bezel at rest over an arbitrary page (#73, #121). - **A3 Panel surface.** The panel sits on the regular material and always draws its controls in the active appearance, including while Browsing Mode holds the keyboard (#73). - **A4 Column metrics.** A column is 260 pt wide. The card behind it has a 12 pt radius and the picture inside it 8 pt; they are nested, so the radii differ on purpose. Controls in a row are 4 pt apart. Choosers size to their label up to a ceiling: three quarters of the column for the playlist chooser, seven eighths for the website chooser ([`PanelMetrics`](../Sources/Nifro/Screens/PanelControls.swift)). - **A5 Column hover.** A quaternary fill and a 2 pt accent border, in both light and dark appearance (#29). - **A6 Colour.** Captions and the panel's failure line use the secondary style, not red or the accent ([`DisplayPanel`](../Sources/Nifro/Screens/DisplayPanel.swift)). The confirming button of a destructive dialog is marked destructive (#30, [`RestoreDefaults`](../Sources/Nifro/App/RestoreDefaults.swift)). -- **A7 Crop overlay.** Black and white are contrast decisions against an arbitrary page; theme colours are the wrong answer there (#29, [Visual vocabulary](VISUAL-SYSTEM.md) §3). +- **A7 Crop overlay.** Black and white are contrast decisions against an arbitrary page; theme colours are the wrong answer there (#29, #73). - **A8 Menu bar.** The colour band has no palette; its colour is sampled from the wallpaper. The icon is drawn dim when no wallpaper is showing and pulses while a page loads ([`AppState`](../Sources/Nifro/App/AppState.swift), #73). - **A9 Symbols.** SF Symbols throughout ([`PanelControls`](../Sources/Nifro/Screens/PanelControls.swift)). diff --git a/docs/DESIGN.zh-Hans.md b/docs/DESIGN.zh-Hans.md index 040023d..cb0a1ef 100644 --- a/docs/DESIGN.zh-Hans.md +++ b/docs/DESIGN.zh-Hans.md @@ -14,9 +14,12 @@ | G4 | 选择网页的哪一部分铺满壁纸 | Crop | 裁切 | Choose Region | 帮助文案指向面板上的裁切按钮(#40、#123) | | G5 | 用指针和键盘操作壁纸网页 | Browsing Mode | 浏览模式 | browsing mode | 作为名称首字母大写(#123) | | G6 | 显示器在网站间切换的方式 | Rotate in order; Rotate randomly; Keep this page displayed | 按顺序轮播;随机轮播;固定显示此页面 | Rotating in order; Rotating in shuffled order; Staying on this website / 固定在这个网站 | #123 | -| G7 | 切换某个状态 | Turn … on or off | 打开或关闭… | Toggle … / 切换… | #123 | +| G7 | 切换某个状态 | Turn … on or off | 打开或关闭… | Toggle … / 切换… | #123;快捷指令同样适用(2026-10-10,受用户委托由 agent 决定) | | G8 | 重新启动 App 以应用更改 | Restart | 重新启动 | Reopen / 重新打开 | #123 | -| G9 | 向推荐站点提议一个网站 | Suggest a site… | 推荐网站… | Submit a Site… / 提交站点… | #123 | +| G9 | 向网站库提议一个网站 | Suggest a site… | 推荐网站… | Submit a Site… / 提交站点… | #123 | +| G12 | 文案中说到 site 时的网站 | Site | 网站 | 站点 | 2026-10-10 用户决定 | +| G13 | 精选的适合做壁纸的网站列表 | Site Gallery | 网站库 | 推荐站点 | 2026-10-10 用户决定;「推荐」留给推荐网站…(G9) | +| G14 | 重新载入页面 | Reload | 刷新 | 重新加载 | 2026-10-10 用户决定 | | G10 | 有菜单栏的那块显示器 | Main Display | 主显示器 | — | 文案和行为都指菜单栏所在的显示器,不是键盘焦点所在的屏幕(#17) | | G11 | 本 App | Nifro | Nifro | 把 Plash 当作本 App 的名称、图标或图像;关于页的致谢是唯一提及 | Nifro 是独立分支([CONTRIBUTING](../.github/CONTRIBUTING.md)) | @@ -34,12 +37,12 @@ ## 3. 视觉设计 -- **A1 系统界面与自绘界面。** 设置、Websites、添加网站和推荐站点使用分组表单中的系统控件,它们跟随强调色、增强对比度、降低透明度和 VoiceOver([视觉词汇](VISUAL-SYSTEM.zh-Hans.md) §3–4)。 +- **A1 系统界面与自绘界面。** 设置、Websites、添加网站和网站库使用分组表单中的系统控件,它们跟随强调色、增强对比度、降低透明度和 VoiceOver(#73)。 - **A2 面板控件。** 大尺寸的系统 bordered 控件。有点亮状态的控件是开关,点亮时使用用户的强调色;App 图标的橙色不再表示点亮。不用 `accessoryBar`,因为它静止时不画边框,放在任意网页上会看不见(#73、#121)。 - **A3 面板底板。** 面板使用 regular material,并始终以活跃外观绘制控件,浏览模式占用键盘时也一样(#73)。 - **A4 列的尺寸。** 每列宽 260 pt。列背后的卡片圆角 12 pt,列内图片圆角 8 pt;两者嵌套,所以圆角有意不同。同一行控件间距 4 pt。选择器按标签宽度伸缩,有上限:播放列表选择器为列宽的四分之三,网站选择器为八分之七([`PanelMetrics`](../Sources/Nifro/Screens/PanelControls.swift))。 - **A5 列的悬停。** quaternary 填充加 2 pt 强调色描边,浅色和深色外观相同(#29)。 - **A6 颜色。** 说明文字和面板的加载失败行使用 secondary 样式,不用红色或强调色([`DisplayPanel`](../Sources/Nifro/Screens/DisplayPanel.swift))。破坏性对话框的确认按钮标为 destructive(#30,[`RestoreDefaults`](../Sources/Nifro/App/RestoreDefaults.swift))。 -- **A7 裁切遮罩。** 黑白两色是针对任意网页的对比度选择;这里用随主题变化的颜色是错的(#29,[视觉词汇](VISUAL-SYSTEM.zh-Hans.md) §3)。 +- **A7 裁切遮罩。** 黑白两色是针对任意网页的对比度选择;这里用随主题变化的颜色是错的(#29、#73)。 - **A8 菜单栏。** 颜色带没有调色板,颜色取自壁纸。没有壁纸在显示时图标变暗,网页加载时图标闪动([`AppState`](../Sources/Nifro/App/AppState.swift),#73)。 - **A9 图标。** 全部使用 SF Symbols([`PanelControls`](../Sources/Nifro/Screens/PanelControls.swift))。 diff --git a/docs/VISUAL-SYSTEM.md b/docs/VISUAL-SYSTEM.md deleted file mode 100644 index f434f7f..0000000 --- a/docs/VISUAL-SYSTEM.md +++ /dev/null @@ -1,397 +0,0 @@ -# The App's Visual Vocabulary - -> Decided rules are in [DESIGN.md](DESIGN.md). This audit is kept because §7 Q5 is still open. - -[简体中文](VISUAL-SYSTEM.zh-Hans.md) - -> Written 2026-08-27 against `dead3a6` (v0.1.3). Every number below was produced by a command that is -> printed next to it, so it can be re-run rather than believed. - -The panel and the settings windows do not look like one app, and no mechanism requires them to. This -document says how far apart they actually are, which of the differences are decisions and which are -accidents, and proposes the smallest thing that would make "somebody hardcoded a colour" fail CI -instead of waiting for an eye to catch it. - -Nothing here has been implemented. Sections 5 and 6 are a plan, and section 7 lists what needs your -decision before any of it is worth starting. - ---- - -## 1. How many hardcoded visual literals there are - -```sh -# Per file, per category. Run from the repo root. -for f in $(git ls-files 'Nifro/*.swift' | grep -v generated); do - r=$(grep -cE "(cornerRadius|xRadius|yRadius): *[0-9]" $f) - o=$(grep -cE "\.(system|systemFont|monospacedSystemFont|monospacedDigitSystemFont)\((of)?[Ss]ize: *[1-9]" $f) - s=$(grep -cE "spacing: *[1-9]" $f) - p=$(grep -cE "\.padding\((\.[a-zA-Z]+, *)?[1-9]" $f) - w=$(grep -cE "\.frame\(.*(width|height): *[1-9]" $f) - c=$(grep -cE "Color\(red:|(NS)?Color\.(white|black)|withAlphaComponent\(|\.opacity\(0?\.[0-9]" $f) - [ $((r+o+s+p+w+c)) -gt 0 ] && printf "%-42s %2s %2s %2s %2s %2s %2s = %s\n" $f $r $o $s $p $w $c $((r+o+s+p+w+c)) -done -``` - -| File | radius | font pt | spacing | padding | frame | colour | total | -|---|---:|---:|---:|---:|---:|---:|---:| -| `Screens/DisplayPanel.swift` | 9 | 1 | 8 | 7 | 5 | 1 | **31** | -| `Screens/SiteGalleryScreen.swift` | 0 | 0 | 5 | 4 | 2 | 0 | 11 | -| `Screens/PanelControls.swift` | 2 | 3 | 1 | 0 | 1 | 1 | 8 | -| `Screens/WebsitesScreen.swift` | 1 | 0 | 2 | 1 | 3 | 1 | 8 | -| `Zoom/CropSelectionView.swift` | 1 | 2 | 0 | 0 | 0 | 5 | 8 | -| `Screens/AddWebsiteScreen.swift` | 0 | 2 | 1 | 0 | 4 | 0 | 7 | -| `Screens/AboutSettings.swift` | 0 | 0 | 2 | 1 | 1 | 0 | 4 | -| `Screens/SettingsScreen.swift` | 0 | 0 | 2 | 0 | 1 | 0 | 3 | -| `Screens/SettingHelp.swift` | 0 | 0 | 1 | 0 | 1 | 0 | 2 | -| `Screens/IntervalField.swift` | 0 | 0 | 0 | 0 | 2 | 0 | 2 | -| `Support/ScrollableTextView.swift` | 0 | 0 | 0 | 0 | 1 | 0 | 1 | -| `Visibility/MenuBarBand.swift` | 0 | 0 | 0 | 0 | 0 | 0 | **0** | -| | | | | | | | **85** | - -Two rows carry most of the argument. `DisplayPanel.swift` holds 31 of the 85 — more than a third of -the app's visual literals are in the one file that also holds the only named metrics. And -`MenuBarBand.swift` holds none: it takes its colour off the page, so it has nothing a design system -could give it. It is exempt on the strongest ground available, not by concession. - -**Which values repeat.** Multiplicity is what turns a literal into a defect; a number used once is -just a number. - -```sh -grep -rhoE --include='*.swift' "(cornerRadius|xRadius|yRadius): *[0-9]+" Sources/Nifro/Screens Sources/Nifro/Zoom | - grep -oE "[0-9]+$" | sort -n | uniq -c -``` - -- **Corner radii: 5 (×4), 7 (×1), 8 (×4), 12 (×5)** — and `PanelMetrics.cornerRadius = 6`, a fifth - radius with exactly one user. Five corner radii inside one popover. -- **Point sizes: 11 (×4), 12, 13 (×2), 22** — and `PanelMetrics` already names both 11 and 13. -- **Widths: 260 (×3), 44 (×3), 560 (×3), 500/56/70/22/16 (×2 each).** 260 is the panel column and is - named nowhere. - ---- - -## 2. The inconsistencies, by the three shapes - -Against `WORKSPACE_GUIDE.md` § "缺陷的三个形状". - -### ① The same expression, copied - -- **`Font.system(size: 11, weight: .semibold)`** is `PanelMetrics.symbolFont` - (`PanelControls.swift:25`) and is also written out character for character at - `DisplayPanel.swift:256`. One is a copy of the other. -- **Size 13** is `PanelMetrics.font` (`PanelControls.swift:21`) and `.system(size: 13, weight: - .medium)` at `PanelControls.swift:57` — the same size declared twice in the same file, differing - only in weight. -- **The column width, 260**, at `DisplayPanel.swift:122`, `:139` and `:299`. Worse, `PanelMetrics - .chooserWidth = 195` (`PanelControls.swift:32`) is documented as "three quarters of the picture - above it" — that is `0.75 × 260`, a derivation recorded as prose instead of as code. Change the - column width and the comment becomes a lie silently. -- **The on-state foreground, `AnyShapeStyle(.white)`**, at `PanelControls.swift:82`, `:131` and - `DisplayPanel.swift:258`. `onTint` is named; the colour that goes on top of it is not. -- **Radius 12 ×3** (`DisplayPanel.swift:142,149,152`) and **radius 8 ×4** (`:285,292,300,302`) — in - both cases fill, clip shape and stroke of one element, which must agree or the border misses the - corner. - -### ② A mechanism with members that never joined - -- **`PanelMetrics` has six members and `PanelMetrics.` appears 16 times**, eight in each of the two - panel files: - - ```sh - grep -rc "PanelMetrics\." Sources/Nifro/Screens/*.swift # DisplayPanel 8, PanelControls 8 - ``` - - Against 39 visual literals in those same two files. The mechanism covers roughly a fifth of what - it exists for. -- **The sync-link button (`DisplayPanel.swift:254-267`) redraws `PanelButton` by hand.** It sets its - own font (line 256, the copy noted above), its own frame `22×20` where `PanelButton` uses `26×22` - (`PanelControls.swift:58`), its own `cornerRadius: 5`, its own on-tint branch — and it reads no - hover state at all, while both of its neighbours dip and light on hover. It is not a `PanelButton` - because `PanelButton` wraps a `Button` and this needs a `Menu`; nothing else about it wanted to be - different. -- **`PanelMetrics.cornerRadius = 6` has one user**, `PanelWideButton`. `PanelButton` — which sits in - the same footer row, 10 points away (`DisplayPanel.swift:60`) — uses 5. The token existed and the - sibling control did not join it. - -### ③ Two independent answers to one question - -- **The on-state colour's own rule is not applied consistently.** `PanelMetrics.onTint`'s comment - (`PanelControls.swift:37-40`) states a real judgement: the system accent says *this is selected*, - the icon's orange says *this is running*. Checked against every caller, one disagrees. - `DisplayPanel.swift:319-321` passes `isOn: !column.isShowing`, so the power button lights orange - **while that display's wallpaper is switched off**. Under the written rule that is backwards; under - a different rule ("the button is toggled on") it is right. Both readings are defensible and only - one is written down, which is the defect. The other four callers agree with the comment. -- **One hover state, two colour systems, and one only works in the dark.** Hovering a column draws a - border in `Color.accentColor` (`DisplayPanel.swift:150`) and a fill of - `Color.white.opacity(0.15)` (`:143`). The border follows the theme and the user's accent; the fill - is fixed white, so over a light-appearance popover it is close to invisible, while the buttons - inside the same column use `.quaternary` for the same purpose. Three answers to "what does hover - look like", one of them appearance-dependent. -- **Five windows, five widths, decided independently.** 400 (`SettingsScreen.swift:19`), 480 - (`WebsitesScreen.swift:71`), 500 (`AddWebsiteScreen.swift:60`), 520×560 - (`AddWebsiteScreen.swift:494`), 560×560 (`SiteGalleryScreen.swift:47`). Nothing requires any two to - agree. They may legitimately differ — see § 4 — but no one has ever decided that they do. -- **A stale comment about the boundary itself.** `DisplayPanelController.swift:8` says "The old menu - is still there on a right-click, and stays until the panel carries everything it does." - `AppState.swift:30` says "The menu is gone." `Menus.swift` was deleted in #21 and - `sendAction(on: [.leftMouseUp, .rightMouseUp])` (`AppState.swift:24`) sends both buttons to the - same handler. The first comment describes an app that no longer exists, and it is the one a reader - reaches first when asking which surface owns what. (The scaffolding behind it is ROADMAP W9, - which is now down to `CallbackMenuItem.validateCallback`; this entry is the comment, not the code.) - ---- - -## 3. What is *not* wrong - -Recorded so the next pass does not re-raise them. - -- **`MenuBarBand`** has no visual literals and should never acquire any. Its one colour is sampled - from the wallpaper. -- **The four `Form(.formStyle(.grouped))` screens** are right to use stock controls. They are - ordinary windows about ordinary settings; stock controls are what follow the user's accent colour, - Increase Contrast, Reduce Transparency and VoiceOver for free. -- **`CropSelectionView`'s black and white** are not brand colours that escaped the system. They are - contrast decisions against an arbitrary web page, which is the one surface where a theme-aware - colour would be the wrong answer. -- **The animation durations** — 0.12 (×2), 0.2, and the 0.2/0.82 spring. ROADMAP N3 already ruled - that two durations changing for different reasons should not be merged; the same test applies - here, and the press dips and the marquee reset fail it. Not tokens. -- **Repeated `spacing:` and `padding:` inside one view.** `VStack(spacing: 9)` twice in - `DisplayColumn` is one layout expressed twice, not a shared vocabulary. § 5 deliberately leaves - these unguarded and § 6 says what it would cost to change that. - ---- - -## 4. Where the line between system and custom should be - -The line that exists today is right in three places and undrawn in one. - -| Surface | Should be | Why | -|---|---|---| -| Settings, Websites, Add Website, Site Gallery | **system** | Ordinary windows about ordinary settings. Stock controls are the only ones that follow accent colour, Increase Contrast, Reduce Transparency and VoiceOver without being asked | -| The display panel | **custom** | Argued already at `PanelControls.swift:6-9`: the control acts on a page the user cannot see, so the button has to be the whole of the feedback | -| `CropSelectionView` | **custom** | It sits over an arbitrary web page. Nothing system-drawn stays legible over unknown content | -| `MenuBarBand` | **neither** | Not a control. Its colour comes from the page | - -**Where it is accidental rather than drawn:** inside the panel. Three of the panel's controls are -system `Menu`s wearing hand-drawn labels — the website chooser (`DisplayPanel.swift:344-380`), the -sync link (`:239-267`) — so the popup list is system-drawn inside a hand-drawn panel and the token -layer only ever reaches the label. That is a reasonable place to stop, and nothing says so, which -means the next control added has no rule to follow. - -**The judgement that does exist is worth keeping.** `onTint`'s comment — accent means selected, -orange means running — is a genuine semantic distinction, not decoration, and it is the reason the -token layer below is *semantic* (`onTint`, `hoverFill`) rather than a palette of colour names. It -just needs to be enforced by something other than the person who remembers it. § 7 Q1 asks you to -settle the one caller that disagrees with it. - ---- - -## 5. The proposal - -Deliberately small. `PanelMetrics` is already the design-token layer; the problem is not that it is -missing but that it is named for one screen, lives beside the views it serves, and covers a fifth of -them. So this is a rename, a handful of additions each of which has at least two existing users, and -one shared modifier — not a new framework. - -### 5.1 One file: `Sources/Nifro/Screens/Appearance.swift` - -`PanelMetrics` moves here and becomes `Appearance`, keeping its documentation. It gets its own file -so that the lint rule in § 5.3 can exempt one stable path; leaving it in `PanelControls.swift` would -exempt `PanelButton` and `PanelWideButton` too, which is the hole this is meant to close. - -```swift -enum Appearance { - // Control chrome. Unchanged from PanelMetrics. - static let controlFont = Font.system(size: 13) - static let symbolFont = Font.system(size: 11, weight: .semibold) - static let controlHeight = 28.0 - static let controlPadding = 15.0 - static let controlRadius = 6.0 // ← § 7 Q2: is the icon button's 5 meant to differ? - static let iconButtonSize = CGSize(width: 26, height: 22) - - // The column. - static let columnWidth = 260.0 - static let cardRadius = 12.0 - static let pictureRadius = 8.0 - static let chooserWidth = columnWidth * 0.75 // The existing comment, made true by construction. - - // State, named for what it means rather than what colour it is. - static let onTint = Color(red: 234 / 255, green: 115 / 255, blue: 63 / 255) - static let onForeground = AnyShapeStyle(.white) - static let hoverFill = AnyShapeStyle(.quaternary) - static let restFill = AnyShapeStyle(.quinary) -} -``` - -Fourteen names. Every one has at least two existing call sites today except `controlRadius`, which -has one and is the subject of Q2. Nothing is added for symmetry: there is no `windowWidth`, because -§ 3's N3 test says five windows sized for five different contents are five decisions, not one. - -### 5.2 One modifier, three call sites - -```swift -extension View { - /// Paint a control the way the panel paints controls. - func panelChrome(isOn: Bool, isHovering: Bool, rest: AnyShapeStyle = Appearance.restFill) -> some View -} -``` - -It sets foreground (`onForeground` / `.primary`), background (`onTint` / `hoverFill` / `rest`) and -the rounded shape. `PanelButton` passes `rest: AnyShapeStyle(.clear)`; `PanelWideButton` takes the -default; **the sync-link `Menu` label uses it too**, which is what folds the second icon button back -into the mechanism. After that the only thing separating an icon button from a pill is its frame and -padding, which stay at the call site because that is the real difference. - -No protocol, no style registry, no `ButtonStyle` hierarchy. There is one panel. - -### 5.3 The guardrail: three SwiftLint custom rules - -SwiftLint is already pinned at 0.65.1 and already runs `--strict` in CI (`ci.yml:99`), so no new tool -and no new job. Added to `Tools/Config/SwiftLint.yml`: - -```yaml -custom_rules: - hardcoded_corner_radius: - included: 'Sources/Nifro/(Screens|Zoom|Visibility)/.*\.swift' - excluded: 'Nifro/(Screens/Appearance|Zoom/CropSelectionView)\.swift' - regex: '(cornerRadius|xRadius|yRadius): *[0-9]' - message: 'Corner radii belong in Appearance. Name it there, or disable this line with the reason it is not shared.' - - hardcoded_font_size: - included: 'Sources/Nifro/(Screens|Zoom|Visibility)/.*\.swift' - excluded: 'Nifro/(Screens/Appearance|Zoom/CropSelectionView)\.swift' - regex: '\.(system|systemFont|monospacedSystemFont|monospacedDigitSystemFont)\((of)?[Ss]ize: *[1-9]' - message: 'Point sizes belong in Appearance.' - - hardcoded_ui_colour: - included: 'Sources/Nifro/(Screens|Zoom|Visibility)/.*\.swift' - excluded: 'Nifro/(Screens/Appearance|Zoom/CropSelectionView)\.swift' - regex: 'Color\(red:|(NS)?Color\.(white|black)\b|AnyShapeStyle\(\.white\)' - message: 'A literal colour follows neither the theme nor the user accent. Use Appearance, or a semantic style.' -``` - -**Why these regexes and not others.** Every one matches a *framework* symbol — SwiftUI's and -AppKit's own public parameter labels and initialisers. None matches a name anybody in this repo -chose. Renaming `PanelButton`, `isHovering` or `PanelMetrics` itself cannot turn any of them red, -which is the distinction `WORKSPACE_GUIDE.md` draws between 白红 and 真红. The `[1-9]` in the font -rule is not cosmetic: `NSFont.controlContentFont(ofSize: 0)` (`ScrollableTextView.swift:47`) is -AppKit's idiom for "the system default size", the opposite of a hardcoded size, and `[0-9]` would -red it. - -**Measured, not predicted.** Both halves were run locally against a 0.65.1 that matches the CI pin: - -```sh -swiftlint lint --quiet --config /path/to/probe.yml | grep -c 'warning:' -``` - -- The three rules fire **32 times** on `dead3a6`. Section 6 lists every one. -- Per-rule `excluded:` works: pointing it at `PanelControls.swift` drops the count 32 → 24, exactly - that file's eight. -- `// swiftlint:disable:next hardcoded_corner_radius - ` silences one line, and - `superfluous_disable_command` — already enabled in `Tools/Config/SwiftLint.yml` — reports the disable as a - violation once the line under it stops violating. So an allowlist entry whose reason has expired - fails, which is the property `WORKSPACE_GUIDE.md` asks allowlists to have and the reason this is a - lint rule rather than a source-shape test. - -**Why not a source-shape test in `Tests/`.** The `WORKSPACE_GUIDE.md` criterion is whether the -proposition is *unrunnable* or the code is merely *unreachable*. "No colour literal exists outside -`Appearance.swift`" is an absence claim over source text, so a shape test would be the right tool — -but SwiftLint already reads every file, already runs `--strict`, already has a per-line exemption -with an expiry check, and points at the offending column in the PR diff. A test would reimplement all -of that, and the `NifroLogic` package target compiles seven files and would have to reach outside -itself to read the rest. Use the tool that is already there. - -### 5.4 What stays unguarded, and when to add it - -`spacing:`, `.padding(…)` and `.frame(width:height:)` are not covered. Extending the rules to them -fires **45 more times** (measured the same way), and most of those are a local layout expressed once. -An allowlist of 45 entries reading "this is a layout number" is exactly the shape -`WORKSPACE_GUIDE.md` forbids. Add -them if and when a second panel exists and a spacing has to agree across the two — at that point the -number has a name to be given, and today it does not. - ---- - -## 6. Migration, and the 32 violations - -**There is no "warn first" phase.** CI already runs `swiftlint lint --config Tools/Config/SwiftLint.yml --strict`, so a custom rule at -default severity is red the moment it lands. The order below exists to bring the count to zero before -the rule is added, not to ease it in. - -| Step | What | Verified by | -|---|---|---| -| 1 | `Appearance.swift`: move `PanelMetrics`, rename, add the eight names from § 5.1 | `git grep -c PanelMetrics` → 0; the app builds; `swift test` unchanged (the package target does not compile these files, so this step cannot break it) | -| 2 | `panelChrome`, and the sync-link button adopts it | The literal count in `DisplayPanel.swift` falls from 31 to ~20. Visible change: the sync button becomes 26×22 and gains a hover state | -| 3 | Q1 and Q2 in § 7 — decisions, not renames | Nothing to measure. Needs you | -| 4 | Add the three rules. Three violations remain and each takes a `disable:next` with its reason | `swiftlint lint --config Tools/Config/SwiftLint.yml --strict` exits 0. From here, a hardcoded literal is red on the pull request that writes it | - -### The 32, each with its disposition - -This is the list `WORKSPACE_GUIDE.md` requires: every first-run violation, argued, with nothing -swept into the allowlist unexamined. - -**Fixed by step 1 — a token exists or is being added (19).** -`PanelControls.swift:21,25,41` become the token definitions themselves. `PanelControls.swift:57` -(size 13) and `DisplayPanel.swift:256` (size 11 semibold) are verbatim copies of tokens that already -exist. `PanelControls.swift:60,61` and `DisplayPanel.swift:261` are the radius-5 group, which -becomes `controlRadius` under Q2. `DisplayPanel.swift:142,149,152` become `cardRadius`; -`:285,292,300,302` become `pictureRadius`. `DisplayPanel.swift:327` (radius 7) becomes -`pictureRadius - 1` — it is a pill nested inside the 8-radius picture and 7 is that relationship, -written as a number. `PanelControls.swift:82,131` and `DisplayPanel.swift:258` become -`onForeground`. - -**Fixed by step 3 — a decision, not a rename (1).** -`DisplayPanel.swift:143`, `Color.white.opacity(0.15)`. This is Q3: the hover fill that only works in -dark appearance. It should almost certainly become `hoverFill`, matching the buttons inside the same -column, but that changes how the panel looks and is your call. - -**Exempted as a surface, with one reason (9).** -All of `CropSelectionView.swift` — `:152,157,170,171,174,175,190,191(×2)`. Its black scrim, white -outline and 22-point readout are contrast decisions against an arbitrary web page, the one surface in -the app where a theme-aware colour is the *wrong* answer. Nine identical per-line reasons would say -less than one file-level one, so the exemption is a path in `excluded:` with the reason written above -it in the YAML. If you would rather see nine `disable:next` lines, say so — it is a real choice, not -an oversight. - -**Permanently allowed, one line each (3).** -- `AddWebsiteScreen.swift:460` and `:481`, `monospacedSystemFont(ofSize: 11)`. These are the CSS and - JavaScript editors. A code editor's font is a code editor's font; it has no relationship to the - chrome, and putting it in `Appearance` would couple an editor to a panel. -- `WebsitesScreen.swift:246`, `cornerRadius: 5` on a website's favicon. It is a masked 44×44 image in - a system `Form`, not a panel control. It should arguably be macOS's own icon radius rather than - `Appearance`'s, which is why it is allowed rather than migrated. - ---- - -## 7. Needs your decision - -- **Q1 — The power button's orange.** `DisplayPanel.swift:319-321` lights `onTint` while the display - is switched **off**, which contradicts `onTint`'s own comment ("the icon's orange says *this is - running*"). Either the call site inverts or the comment is rewritten to "this button is engaged". - Whichever you pick, the other four callers must be re-read against it. Note this is adjacent to, - but not the same as, ROADMAP K22 — K22 is about the button being *wrong*, this is about it being - *the wrong colour*. -- **Q2 — Five corner radii, or fewer.** `PanelButton` uses 5, `PanelWideButton` uses 6, and they sit - 10 points apart in the footer. § 5 assumes they become one `controlRadius`; picking 5 or 6 is a - look decision I should not make for you. The card's 12 and the picture's 8 stay separate either - way. -- **Q3 — The column hover fill.** Making `Color.white.opacity(0.15)` into `.quaternary` fixes light - appearance and changes how the panel looks in dark. Confirm before step 3. -- **Q4 — Whether `CropSelectionView` is exempted as a file or line by line.** § 6 argues for the - file. The cost of being wrong is that a genuinely shared value could later be hardcoded there - unnoticed. -- **Q5 — Window widths.** Five windows, five widths, never decided. § 5 deliberately does *not* - tokenise them, on ROADMAP N3's reasoning. If you think Settings at 400 and Websites at 480 should - match, that is a separate, smaller change and should not ride along with this one. - -## 8. Explicitly not proposed - -- A `ButtonStyle` or `ViewModifier` library. There is one panel; a second implementation is what - would justify the abstraction, and it does not exist (ROADMAP X4's reasoning). -- Restyling the `Form` screens to match the panel. That would trade away the accessibility and - appearance behaviour § 4 says is the reason to use stock controls. -- A colour asset catalogue. One colour is app-specific; the rest are semantic system styles that a - catalogue would only obscure. -- Touching `MenuBarBand`, `DimWhenUnfocused`, or anything under `Wallpaper/`. None of them draws - chrome. diff --git a/docs/VISUAL-SYSTEM.zh-Hans.md b/docs/VISUAL-SYSTEM.zh-Hans.md deleted file mode 100644 index 61e4b9b..0000000 --- a/docs/VISUAL-SYSTEM.zh-Hans.md +++ /dev/null @@ -1,346 +0,0 @@ -# 这个 app 的视觉词汇 - -> 已定的规则以 [DESIGN.zh-Hans.md](DESIGN.zh-Hans.md) 为准。本文因 §7 Q5 尚未决定而保留。 - -[English](VISUAL-SYSTEM.md) - -> 2026-08-27 对着 `dead3a6`(v0.1.3)写。下面每个数字旁边都附了产生它的命令,可以重跑,不必相信。 - -面板和设置窗口看起来不像同一个 app,而且没有任何机制要求它们像。这份文档说清楚它们实际差多远、哪些差异 -是决定、哪些是意外,并提出**最小**的那个东西——让「有人硬编码了一个颜色」直接让 CI 变红,而不是等一双 -眼睛发现。 - -这里没有任何东西已经实现。第 5、6 节是方案,第 7 节列出动手前需要你拍板的事。 - ---- - -## 1. 硬编码视觉字面量有多少 - -```sh -# 按文件、按类别。在仓库根目录跑。 -for f in $(git ls-files 'Nifro/*.swift' | grep -v generated); do - r=$(grep -cE "(cornerRadius|xRadius|yRadius): *[0-9]" $f) - o=$(grep -cE "\.(system|systemFont|monospacedSystemFont|monospacedDigitSystemFont)\((of)?[Ss]ize: *[1-9]" $f) - s=$(grep -cE "spacing: *[1-9]" $f) - p=$(grep -cE "\.padding\((\.[a-zA-Z]+, *)?[1-9]" $f) - w=$(grep -cE "\.frame\(.*(width|height): *[1-9]" $f) - c=$(grep -cE "Color\(red:|(NS)?Color\.(white|black)|withAlphaComponent\(|\.opacity\(0?\.[0-9]" $f) - [ $((r+o+s+p+w+c)) -gt 0 ] && printf "%-42s %2s %2s %2s %2s %2s %2s = %s\n" $f $r $o $s $p $w $c $((r+o+s+p+w+c)) -done -``` - -| 文件 | 圆角 | 字号 | spacing | padding | frame | 颜色 | 合计 | -|---|---:|---:|---:|---:|---:|---:|---:| -| `Screens/DisplayPanel.swift` | 9 | 1 | 8 | 7 | 5 | 1 | **31** | -| `Screens/SiteGalleryScreen.swift` | 0 | 0 | 5 | 4 | 2 | 0 | 11 | -| `Screens/PanelControls.swift` | 2 | 3 | 1 | 0 | 1 | 1 | 8 | -| `Screens/WebsitesScreen.swift` | 1 | 0 | 2 | 1 | 3 | 1 | 8 | -| `Zoom/CropSelectionView.swift` | 1 | 2 | 0 | 0 | 0 | 5 | 8 | -| `Screens/AddWebsiteScreen.swift` | 0 | 2 | 1 | 0 | 4 | 0 | 7 | -| `Screens/AboutSettings.swift` | 0 | 0 | 2 | 1 | 1 | 0 | 4 | -| `Screens/SettingsScreen.swift` | 0 | 0 | 2 | 0 | 1 | 0 | 3 | -| `Screens/SettingHelp.swift` | 0 | 0 | 1 | 0 | 1 | 0 | 2 | -| `Screens/IntervalField.swift` | 0 | 0 | 0 | 0 | 2 | 0 | 2 | -| `Support/ScrollableTextView.swift` | 0 | 0 | 0 | 0 | 1 | 0 | 1 | -| `Visibility/MenuBarBand.swift` | 0 | 0 | 0 | 0 | 0 | 0 | **0** | -| | | | | | | | **85** | - -两行承担了大部分论据。`DisplayPanel.swift` 占 85 里的 31——全 app 三分之一以上的视觉字面量,都在那个 -同时也放着唯一一套具名度量的文件里。而 `MenuBarBand.swift` 是 0:它的颜色从页面上取,设计系统没有任何 -东西能给它。它的豁免建立在最强的理由上,不是妥协。 - -**哪些值是重复的。** 让字面量变成缺陷的是重数;只用一次的数字就只是个数字。 - -```sh -grep -rhoE --include='*.swift' "(cornerRadius|xRadius|yRadius): *[0-9]+" Sources/Nifro/Screens Sources/Nifro/Zoom | - grep -oE "[0-9]+$" | sort -n | uniq -c -``` - -- **圆角:5(×4)、7(×1)、8(×4)、12(×5)** —— 外加 `PanelMetrics.cornerRadius = 6`,第五个圆角, - 只有一个使用者。一个 popover 里五种圆角。 -- **字号:11(×4)、12、13(×2)、22** —— 而 `PanelMetrics` 已经把 11 和 13 都命名了。 -- **宽度:260(×3)、44(×3)、560(×3)、500/56/70/22/16(各 ×2)。** 260 是面板列宽,没有任何地方 - 给它名字。 - ---- - -## 2. 按「缺陷的三个形状」分类 - -对照 `WORKSPACE_GUIDE.md` 的「缺陷的三个形状」。 - -### ① 同一个表达式被抄了 N 份 - -- **`Font.system(size: 11, weight: .semibold)`** 既是 `PanelMetrics.symbolFont` - (`PanelControls.swift:25`),又在 `DisplayPanel.swift:256` 一个字符不差地写了一遍。其中一份是抄的。 -- **字号 13** 既是 `PanelMetrics.font`(`PanelControls.swift:21`),又是 `PanelControls.swift:57` 的 - `.system(size: 13, weight: .medium)` —— 同一个文件里同一个字号声明了两次,只差一个字重。 -- **列宽 260**,在 `DisplayPanel.swift:122`、`:139`、`:299`。更糟的是 - `PanelMetrics.chooserWidth = 195`(`PanelControls.swift:32`)的注释写着「上面那张图的四分之三」—— - 也就是 `0.75 × 260`,一条推导被记成了散文而不是代码。改了列宽,那句注释就静默变成谎话。 -- **开启态前景 `AnyShapeStyle(.white)`**,在 `PanelControls.swift:82`、`:131` 和 - `DisplayPanel.swift:258`。`onTint` 有名字,压在它上面的那个颜色没有。 -- **圆角 12 ×3**(`DisplayPanel.swift:142,149,152`)和 **圆角 8 ×4**(`:285,292,300,302`)——两处都是 - 同一个元素的填充、裁剪和描边,它们必须一致,否则边框会错过拐角。 - -### ② 建了一套机制,有成员没加入 - -- **`PanelMetrics` 有六个成员,`PanelMetrics.` 出现 16 次**,两个面板文件各 8 次: - - ```sh - grep -rc "PanelMetrics\." Sources/Nifro/Screens/*.swift # DisplayPanel 8, PanelControls 8 - ``` - - 而这两个文件里有 39 个视觉字面量。这套机制覆盖了它存在理由的大约五分之一。 -- **同步链接按钮(`DisplayPanel.swift:254-267`)把 `PanelButton` 手抄了一遍。** 它自己写字体(第 256 - 行,就是上面那份抄件)、自己写 `22×20` 的 frame——而 `PanelButton` 是 `26×22` - (`PanelControls.swift:58`)、自己写 `cornerRadius: 5`、自己写 on-tint 分支——**而且完全不读 hover - 状态**,尽管它左右两边的邻居都会在 hover 时下沉和点亮。它没做成 `PanelButton` 是因为 `PanelButton` - 包的是 `Button` 而这里需要 `Menu`;除此之外它没有任何一处是想要不一样的。 -- **`PanelMetrics.cornerRadius = 6` 只有一个使用者**,`PanelWideButton`。而 `PanelButton`——就在同一行 - 页脚里、隔 10 点(`DisplayPanel.swift:60`)——用的是 5。token 已经在那儿了,兄弟控件没加入。 - -### ③ 两处独立回答同一个问题,答案不同 - -- **开启态颜色自己那条规则,没有被一致地应用。** `PanelMetrics.onTint` 的注释 - (`PanelControls.swift:37-40`)给出了一条真实判据:系统 accent 说「这个被选中」,图标的橙色说 - 「这个在运行」。逐个调用点核过,有一个不符。`DisplayPanel.swift:319-321` 传的是 - `isOn: !column.isShowing`,所以电源按钮是在**那块屏幕的壁纸被关掉时**亮橙色。按写下来的规则这是反的; - 按另一条规则(「这个按钮被按下了」)它是对的。两种读法都站得住,而只有一种被写下来——这才是缺陷。 - 另外四个调用点和注释一致。 -- **一个 hover 状态,两套颜色体系,其中一套只在深色下有效。** hover 一列会画一条 - `Color.accentColor` 的边框(`DisplayPanel.swift:150`)和一块 `Color.white.opacity(0.15)` 的填充 - (`:143`)。边框跟随主题和用户的 accent;填充是固定的白色,压在浅色外观的 popover 上几乎看不见—— - 而同一列内部的按钮做同一件事用的是 `.quaternary`。「hover 长什么样」有三个答案,其中一个依赖外观。 -- **五个窗口,五个宽度,各自独立决定的。** 400(`SettingsScreen.swift:19`)、480 - (`WebsitesScreen.swift:71`)、500(`AddWebsiteScreen.swift:60`)、520×560 - (`AddWebsiteScreen.swift:494`)、560×560(`SiteGalleryScreen.swift:47`)。没有任何东西要求其中任意 - 两个一致。它们**可以**合理地不同——见第 4 节——但从来没有人决定过它们是不同的。 -- **关于这条边界本身的一条过期注释。** `DisplayPanelController.swift:8` 写着「旧菜单右键还在,会一直留到 - 面板承担它的全部为止」。`AppState.swift:30` 写着「菜单已经没了」。`Menus.swift` 在 #21 里删掉了, - `sendAction(on: [.leftMouseUp, .rightMouseUp])`(`AppState.swift:24`)把左右键都送进同一个处理函数。 - 前一条注释描述的是一个不再存在的 app,而它恰好是读者问「哪个界面管什么」时最先读到的那条。 - (它背后的脚手架是 ROADMAP W9,那一条现在只剩 `CallbackMenuItem.validateCallback`;这一条说的是注释,不是代码。) - ---- - -## 3. 哪些**不是**问题 - -记下来,免得下一轮重新提。 - -- **`MenuBarBand`** 没有任何视觉字面量,也永远不该有。它唯一的颜色是从壁纸上采的。 -- **四个 `Form(.formStyle(.grouped))` 屏幕**用系统控件是对的。它们是关于普通设置的普通窗口;系统控件是 - 唯一能免费跟随用户 accent 颜色、增强对比度、降低透明度和 VoiceOver 的东西。 -- **`CropSelectionView` 的黑与白**不是从系统里逃出来的品牌色。它们是针对**任意一个网页**的对比度决策, - 而这恰恰是全 app 里唯一一处「跟随主题的颜色」是错误答案的界面。 -- **动画时长** —— 0.12(×2)、0.2,以及 0.2/0.82 的 spring。ROADMAP N3 已经裁定过:两个因不同原因而变的 - 时长不该合并;同一条判据在这里适用,按压下沉和跑马灯回位没通过它。不做 token。 -- **同一个视图内重复的 `spacing:` 和 `padding:`。** `DisplayColumn` 里两次 `VStack(spacing: 9)` 是同一个 - 布局写了两遍,不是共享词汇。第 5 节刻意不管这些,第 6 节说了要管的代价。 - ---- - -## 4. 系统控件与自制控件的界线该划在哪 - -今天这条线三处是对的,一处根本没划。 - -| 界面 | 应该是 | 理由 | -|---|---|---| -| Settings、Websites、Add Website、Site Gallery | **系统** | 关于普通设置的普通窗口。只有系统控件不需要被要求就跟随 accent 颜色、增强对比度、降低透明度和 VoiceOver | -| 显示面板 | **自制** | `PanelControls.swift:6-9` 已经论证过:控件作用在用户看不见的页面上,所以按钮必须是反馈的全部 | -| `CropSelectionView` | **自制** | 它压在一个任意网页上。没有任何系统绘制的东西能在未知内容上保持可读 | -| `MenuBarBand` | **两者都不是** | 它不是控件。颜色来自页面 | - -**哪里是碰巧长成这样而不是被划出来的:面板内部。** 面板的三个控件是**系统 `Menu` 套着手绘标签**—— -网站选择器(`DisplayPanel.swift:344-380`)、同步链接(`:239-267`)——所以弹出列表是系统绘制的、装在一个 -手绘面板里,而 token 层永远只够到标签。这是个合理的停止位置,但没有任何地方这么说,也就意味着下一个加进来 -的控件没有规则可循。 - -**已经存在的那条判据值得保留。** `onTint` 的注释——accent 表示被选中、橙色表示在运行——是一条真实的语义 -区分,不是装饰,而且它正是下面那套 token 做成**语义的**(`onTint`、`hoverFill`)而不是一堆颜色名的原因。 -它只是需要被「记得它的那个人」以外的东西来强制执行。第 7 节 Q1 请你裁决那个唯一不符的调用点。 - ---- - -## 5. 方案 - -刻意做小。`PanelMetrics` 本身**已经**是那层设计 token;问题不是它不存在,而是它以一个屏幕命名、和它服务的 -视图放在一起、只覆盖了其中五分之一。所以这是一次重命名、几个新增(每个都有至少两个既有使用者),加一个共享 -modifier——不是一套新框架。 - -### 5.1 一个文件:`Sources/Nifro/Screens/Appearance.swift` - -`PanelMetrics` 搬过来,改名 `Appearance`,保留它的文档。它要有自己的文件,是为了让 5.3 的 lint 规则能豁免 -一条稳定路径;留在 `PanelControls.swift` 里会连带豁免 `PanelButton` 和 `PanelWideButton`——而那正是这件事 -要堵的洞。 - -```swift -enum Appearance { - // 控件外观。和 PanelMetrics 一样,没有改动。 - static let controlFont = Font.system(size: 13) - static let symbolFont = Font.system(size: 11, weight: .semibold) - static let controlHeight = 28.0 - static let controlPadding = 15.0 - static let controlRadius = 6.0 // ← 第 7 节 Q2:图标按钮的 5 是有意不同吗? - static let iconButtonSize = CGSize(width: 26, height: 22) - - // 列。 - static let columnWidth = 260.0 - static let cardRadius = 12.0 - static let pictureRadius = 8.0 - static let chooserWidth = columnWidth * 0.75 // 把既有那条注释变成由构造保证的事实。 - - // 状态。按它的含义命名,而不是按它是什么颜色。 - static let onTint = Color(red: 234 / 255, green: 115 / 255, blue: 63 / 255) - static let onForeground = AnyShapeStyle(.white) - static let hoverFill = AnyShapeStyle(.quaternary) - static let restFill = AnyShapeStyle(.quinary) -} -``` - -十四个名字。除了 `controlRadius`(只有一个使用者,就是 Q2 的议题),其余每一个今天都至少有两个调用点。 -没有任何东西是为了对称加进去的:**没有 `windowWidth`**,因为第 3 节的 N3 判据说,五个为五种内容定尺寸的 -窗口是五个决定,不是一个。 - -### 5.2 一个 modifier,三个调用点 - -```swift -extension View { - /// 按面板画控件的方式画这个控件。 - func panelChrome(isOn: Bool, isHovering: Bool, rest: AnyShapeStyle = Appearance.restFill) -> some View -} -``` - -它负责前景(`onForeground` / `.primary`)、背景(`onTint` / `hoverFill` / `rest`)和圆角形状。 -`PanelButton` 传 `rest: AnyShapeStyle(.clear)`;`PanelWideButton` 用默认值;**同步链接的 `Menu` 标签也用 -它**——这就是把第二个图标按钮折回机制里的那一步。之后,图标按钮和药丸按钮之间唯一的差别就是 frame 和 -padding,它们留在调用点,因为那才是真正的差别。 - -不做 protocol,不做样式注册表,不做 `ButtonStyle` 层级。只有一个面板。 - -### 5.3 护栏:三条 SwiftLint 自定义规则 - -SwiftLint 已经 pin 在 0.65.1,CI 里已经跑 `--strict`(`ci.yml:99`),所以不引入新工具、不新增 job。 -加进 `Tools/Config/SwiftLint.yml`: - -```yaml -custom_rules: - hardcoded_corner_radius: - included: 'Sources/Nifro/(Screens|Zoom|Visibility)/.*\.swift' - excluded: 'Nifro/(Screens/Appearance|Zoom/CropSelectionView)\.swift' - regex: '(cornerRadius|xRadius|yRadius): *[0-9]' - message: '圆角属于 Appearance。在那里给它一个名字,或者用 disable 注明这一次为什么不是共享的。' - - hardcoded_font_size: - included: 'Sources/Nifro/(Screens|Zoom|Visibility)/.*\.swift' - excluded: 'Nifro/(Screens/Appearance|Zoom/CropSelectionView)\.swift' - regex: '\.(system|systemFont|monospacedSystemFont|monospacedDigitSystemFont)\((of)?[Ss]ize: *[1-9]' - message: '字号属于 Appearance。' - - hardcoded_ui_colour: - included: 'Sources/Nifro/(Screens|Zoom|Visibility)/.*\.swift' - excluded: 'Nifro/(Screens/Appearance|Zoom/CropSelectionView)\.swift' - regex: 'Color\(red:|(NS)?Color\.(white|black)\b|AnyShapeStyle\(\.white\)' - message: '字面颜色既不跟随主题也不跟随用户 accent。用 Appearance,或者用语义样式。' -``` - -**为什么是这几条正则而不是别的。** 每一条匹配的都是**框架**符号——SwiftUI 和 AppKit 自己的公开参数标签和 -初始化器。没有一条匹配这个仓库里任何人取的名字。把 `PanelButton`、`isHovering` 或者 `PanelMetrics` 本身 -改名,都不可能让它们中的任何一条变红——这正是 `WORKSPACE_GUIDE.md` 划的「白红」与「真红」的界线。字号规则 -里的 `[1-9]` 不是修饰:`NSFont.controlContentFont(ofSize: 0)`(`ScrollableTextView.swift:47`)是 AppKit -表示「系统默认字号」的惯用写法,是硬编码字号的反面,用 `[0-9]` 会把它误红。 - -**这些是量出来的,不是预测的。** 两半都在本机用与 CI pin 相同的 0.65.1 跑过: - -```sh -swiftlint lint --quiet --config /path/to/probe.yml | grep -c 'warning:' -``` - -- 三条规则在 `dead3a6` 上触发 **32 次**。第 6 节逐条列出。 -- 按规则的 `excluded:` 生效:把它指向 `PanelControls.swift`,计数 32 → 24,正好是那个文件的 8 条。 -- `// swiftlint:disable:next hardcoded_corner_radius - <理由>` 能消掉一行;而 - `superfluous_disable_command`——`Tools/Config/SwiftLint.yml` 里**已经**开着——会在下面那行不再违规时把这条 disable - 本身报成违规。也就是说,**理由过期的允许清单条目会失败**,这正是 `WORKSPACE_GUIDE.md` 要求允许清单具备 - 的性质,也是这件事做成 lint 规则而不是源码形状测试的原因。 - -**为什么不写成 `Tests/` 里的源码形状测试。** `WORKSPACE_GUIDE.md` 的判据是命题**不可运行**还是代码只是 -**不可达**。「`Appearance.swift` 之外不存在颜色字面量」是一条关于源码文本的缺席命题,所以形状测试本来是 -对的工具——但 SwiftLint 已经读每一个文件、已经跑 `--strict`、已经有带过期检查的逐行豁免,而且能在 PR -diff 里指到出问题的那一列。测试要把这些全部重写一遍,而 `NifroLogic` 这个 package target 只编译七个文件, -得伸到自己外面去读其余的。用已经在那儿的工具。 - -### 5.4 哪些暂时不管,什么时候再管 - -`spacing:`、`.padding(…)` 和 `.frame(width:height:)` 不在覆盖范围内。把规则扩到它们上面会**多触发 45 -次**(用同样的方法量的),而其中大部分是只写了一次的局部布局。一份 45 条、每条写着「这是个布局数字」的 -允许清单,正是 `WORKSPACE_GUIDE.md` 禁止的形状。等到出现第二个面板、某个 spacing 必须跨两者一致时再加—— -那时候这个数字才有名字可取,今天没有。 - ---- - -## 6. 迁移顺序,以及那 32 条 - -**没有「先只告警」这个阶段。** CI 已经跑 `swiftlint lint --config Tools/Config/SwiftLint.yml --strict`,所以一条默认严重度的自定义规则落地 -当天就是红的。下面这个顺序的作用是在规则加进去之前把计数降到零,不是让它慢慢过渡。 - -| 步骤 | 做什么 | 验证判据 | -|---|---|---| -| 1 | `Appearance.swift`:搬 `PanelMetrics`、改名、加 5.1 里那八个名字 | `git grep -c PanelMetrics` → 0;app 能构建;`swift test` 不变(package target 不编译这些文件,所以这一步不可能弄坏它) | -| 2 | `panelChrome`,同步链接按钮采用它 | `DisplayPanel.swift` 的字面量数从 31 降到约 20。可见变化:同步按钮变成 26×22 并获得 hover 状态 | -| 3 | 第 7 节的 Q1 和 Q2 —— 是决定,不是改名 | 没有可量的东西。需要你 | -| 4 | 加那三条规则。剩三条违规,每条一行 `disable:next` 附理由 | `swiftlint lint --config Tools/Config/SwiftLint.yml --strict` 退出码 0。从此之后,写下一个硬编码字面量的那个 PR 当场变红 | - -### 那 32 条,逐条给出处置 - -这就是 `WORKSPACE_GUIDE.md` 要求的清单:首次运行的每一条,都要有说法,没有一条不经审视被塞进允许清单。 - -**第 1 步解决 —— token 已存在或正在加(19 条)。** -`PanelControls.swift:21,25,41` 本身就变成 token 定义。`PanelControls.swift:57`(字号 13)和 -`DisplayPanel.swift:256`(11 semibold)是已存在 token 的逐字抄件。`PanelControls.swift:60,61` 和 -`DisplayPanel.swift:261` 是圆角 5 那一组,按 Q2 变成 `controlRadius`。`DisplayPanel.swift:142,149,152` -变成 `cardRadius`;`:285,292,300,302` 变成 `pictureRadius`。`DisplayPanel.swift:327`(圆角 7)变成 -`pictureRadius - 1` —— 它是嵌在圆角 8 的图片里的一颗药丸,7 就是这个关系,被写成了一个数字。 -`PanelControls.swift:82,131` 和 `DisplayPanel.swift:258` 变成 `onForeground`。 - -**第 3 步解决 —— 是决定,不是改名(1 条)。** -`DisplayPanel.swift:143`,`Color.white.opacity(0.15)`。这是 Q3:只在深色外观下有效的 hover 填充。它几乎 -肯定应该变成 `hoverFill`,和同一列内部的按钮一致——但这会改变面板的样子,你来定。 - -**作为一个界面整体豁免,只写一条理由(9 条)。** -`CropSelectionView.swift` 全部 —— `:152,157,170,171,174,175,190,191(×2)`。它的黑色遮罩、白色描边和 22 点 -读数都是针对任意网页的对比度决策,是全 app 里唯一一处「跟随主题的颜色」是**错误**答案的界面。九条一模一样 -的逐行理由说明的东西还不如一条文件级的多,所以豁免写成 `excluded:` 里的一条路径,理由写在 YAML 里它上面。 -如果你更想看到九行 `disable:next`,说一声——这是一个真实的选择,不是疏忽。 - -**永久允许,各一行(3 条)。** -- `AddWebsiteScreen.swift:460` 和 `:481`,`monospacedSystemFont(ofSize: 11)`。这两处是 CSS 和 JavaScript - 编辑器。代码编辑器的字体就是代码编辑器的字体,和外观没有关系;放进 `Appearance` 等于把一个编辑器耦合到 - 一个面板上。 -- `WebsitesScreen.swift:246`,网站图标上的 `cornerRadius: 5`。它是系统 `Form` 里一张 44×44 的遮罩图片, - 不是面板控件。它更应该用 macOS 自己的图标圆角而不是 `Appearance` 的——这也是它被「允许」而不是被「迁移」 - 的原因。 - ---- - -## 7. 需要你拍板 - -- **Q1 —— 电源按钮的橙色。** `DisplayPanel.swift:319-321` 在那块屏幕被**关掉**时点亮 `onTint`,这和 - `onTint` 自己的注释(「图标的橙色说*这个在运行*」)矛盾。要么调用点取反,要么把注释改写成「这个按钮被 - 按下了」。无论选哪个,另外四个调用点都要照着重读一遍。注意这件事**挨着**但不等于 ROADMAP K22 —— - K22 说的是这个按钮**不对**,这里说的是它**颜色不对**。 -- **Q2 —— 五种圆角,还是更少。** `PanelButton` 用 5,`PanelWideButton` 用 6,两者在页脚里隔 10 点。 - 第 5 节假定它们合并成一个 `controlRadius`;选 5 还是选 6 是审美决定,我不该替你做。卡片的 12 和图片的 - 8 无论如何都保持独立。 -- **Q3 —— 列的 hover 填充。** 把 `Color.white.opacity(0.15)` 换成 `.quaternary` 会修好浅色外观,同时改变 - 面板在深色下的样子。第 3 步之前确认。 -- **Q4 —— `CropSelectionView` 是整个文件豁免还是逐行豁免。** 第 6 节论证的是整个文件。判断错的代价是: - 以后真正共享的值在那里被硬编码,不会被发现。 -- **Q5 —— 窗口宽度。** 五个窗口五个宽度,从来没决定过。第 5 节按 ROADMAP N3 的理由**刻意不**把它们做成 - token。如果你认为 Settings 的 400 和 Websites 的 480 应该一致,那是一个独立的、更小的改动,不该搭这趟车。 - -## 8. 明确不做 - -- 一套 `ButtonStyle` 或 `ViewModifier` 库。只有一个面板;能证明这个抽象成立的是第二个实现,而它不存在 - (ROADMAP X4 的理由)。 -- 把 `Form` 屏幕改成面板的样子。那等于交换掉第 4 节所说的、使用系统控件的全部理由:辅助功能和外观行为。 -- 一个颜色资源目录。只有一个颜色是 app 特有的,其余都是语义系统样式,目录只会把它们盖住。 -- 动 `MenuBarBand`、`DimWhenUnfocused` 或 `Wallpaper/` 下的任何东西。它们都不画外观。