规则 1verb-first
按钮写后果,
不写「确定」
标题提出问题,说明讲清代价,按钮直接给出答案。用户只读按钮,也能知道点下去会发生什么。
Esc 取消不可撤销
v0.5.2 Moonlight Dialogs · 弹窗系统重设计
每一次打断,都安静、清楚、可预期。
alune 的 29 个模态框、7 个气泡确认、4 个浮层菜单与反馈中心,统一为一套「月光」弹窗语言:双层玻璃外壳、按风险分级的确认、键盘优先的操作路径,以及只动 transform 与 opacity 的弹性动效。
向下滚动,逐层查看
01 Principles
现有弹窗来自 antd 默认样式、手写 Popconfirm 与原生 dialog 三套来源,标题、按钮文案和危险程度各不相同。重设计先统一规则,再统一外观。
规则 1verb-first
标题提出问题,说明讲清代价,按钮直接给出答案。用户只读按钮,也能知道点下去会发生什么。
Esc 取消不可撤销
规则 2L0 · L1 · L2
可撤销的操作不打断;有代价的用气泡确认;不可撤销的必须勾选或输入后才能提交。
规则 3double bezel
6px 半透明外壳托住实心内核,再加一圈色调光晕。弹窗像一块放在托盘里的玻璃,而不是一张投了重阴影的纸。
规则 4keyboard
Esc 关闭,↵ 提交,多行输入中用 ⌘↵。L2 默认聚焦「取消」,并禁用回车直接提交。
规则 5rhythm
遮罩先淡入,外壳随弹簧曲线升起,内容以 40ms 错峰。退出更快,永远不挡下一步操作。
规则 6one skin
所有 Modal、Popconfirm、Popover 与原生 dialog 共用同一套令牌和外壳。新增弹窗只写内容,不再单独写样式。
02 Tokens
保留 alune 的蓝 / 靛月光色系与现有主题令牌,只新增弹窗专用的外壳、内核、遮罩与光晕层。浅色与深色逐一配对。
色彩light / dark
层级e0 → e3
阴影只用低不透明度的蓝黑色,深度主要来自外壳描边与遮罩后的光晕。
圆角concentric
r(core) = r(shell) − shell-pad(6)
字体Geist · PingFang SC · Geist Mono
缓动no linear · no ease-in-out
尺寸--w + shell-pad × 2
单字段用 sm,常规表单用 md,列表与结果用 lg,差异预览用 xl。窗口不足时一律收为 100%,560px 以下改为底部抽屉。
03 Anatomy
所有模态框都由同一副骨架组成:头部说明「是什么」,正文说明「会怎样」,底栏给出「怎么做」。打开红线可查看间距。
推送alune · feature/dialogsL0
当前分支还没有上游。选择远程后,将以 -u 推送并建立跟踪关系。
之后可直接拉取和推送,不必再次选择远程。
.dlg-shell.dlg-coreEsc 等价。04 Levels
现有代码中「删除分支」用 Popconfirm,「丢弃改动」用 Modal + 勾选,「删除令牌」用 modal.confirm,强度并不对应风险。统一为三级后,形态、颜色和键盘行为都由等级决定。
结果可见、可回退。表单弹窗只负责收集信息,不再额外确认。
蓝色主按钮 · ↵ 直接提交
可以恢复,但需要额外操作或会影响他处。用气泡确认,或说明影响范围的小弹窗。
柔和红按钮 · 聚焦主操作 · 需二次点击
数据会永久丢失或覆盖远程历史。必须勾选或输入名称,才能启用主操作。
实心红按钮 · 默认聚焦「取消」 · 禁用 ↵
按真实代码整理的对照表。「形态」列是重设计后的呈现方式,「来源」是当前实现位置。
| 场景 | 现状 | 新形态 | 等级 | 主操作 | 键盘 |
|---|---|---|---|---|---|
| 新建 / 重命名分支BranchesView · BranchPicker | Modal,按钮「确定」 | sm 表单,实时校验 | L0 | 创建并切换 / 重命名 | ↵ |
| 本地分支名称冲突useBranchSwitch | modal.confirm + Radio | 选项卡片 + 条件输入 | L0 | 继续 | ↵ |
| 合并分支BranchesView | Popconfirm | 气泡确认,提示冲突 | L1 | 合并 | ↵ |
| 删除分支 / 储藏BranchesView · StashesView | Popconfirm | 气泡确认,柔和红 | L1 | 删除分支 / 删除储藏 | ↵ |
| 删除令牌 / 模型AccessTokenSettings · ProviderSettings | modal.confirm | sm 弹窗,列出影响仓库 | L1 | 删除令牌 | ↵ |
| 丢弃改动DiscardChangesDialog · ChangesView | Modal + Checkbox | 放弃 / 保留对账 + 勾选确认 | L2 | 放弃全部并删除(N) | 聚焦取消 |
| 删除新文件DeleteNewFileDialog | Modal | 文件清单 + 勾选确认 | L2 | 删除文件 | 聚焦取消 |
| 强制推送RepositoryDetailPage | modal.confirm | 提交对比 + 输入分支名 | L2 | 强制推送 | 聚焦取消 |
| 合并 / 关闭 PR/MRPullRequestActions | Modal,关闭时 danger | 合并方式分段;关闭为 L2 | L1 L2 | 确认合并 / 确认关闭 | ↵ / 聚焦取消 |
| 通知、分支、Worktree 浮层BranchPicker · WorktreesPanel | Popover / Dropdown | 双层浮层,可筛选 | — | — | ↑↓ ↵ |
| 反馈中心Feedback | 原生 dialog + 托盘 | 托盘 + 收件箱 | — | 复制诊断信息 | Esc |
05 Form dialogs
表单弹窗全部属于 L0:字段就地校验,主按钮在信息完整前保持禁用,提交中以月相加载替代转圈。每张画板右上角可以切换状态,或点击「实时预览」在真实遮罩中操作。
连接SSH
保存后可以在这台主机上扫描和打开远程仓库。
请输入主机地址
使用本机 SSH Agent 中已加载的密钥,alune 不保存任何凭据。
仓库本机
选择已有 Git 仓库,或粘贴本机的完整目录路径。Git 操作在运行 alune 的电脑上执行。
也可以直接粘贴 Windows 路径,例如 C:\Projects\repository
正在读取仓库状态、分支与远程…
尚未配置提交作者,可以在工作区中补充。
此目录不是 Git 仓库,或当前用户没有读取权限。
仓库SSH · 构建服务器
从指定路径向下搜索最多四层,查找包含 .git 目录的文件夹。
执行扫描后,将在这里列出发现的仓库。
在 /home/developer 下没有找到 Git 仓库。可以换一个上级目录,或检查此用户的读取权限。
正在搜索第 2 / 4 层 · 已检查 184 个目录
发现 4 个仓库/home/developer
分支alune
feature/login从当前 HEAD main · 6a35428 创建,并立即切换
名称未变化
名称可用,跟踪关系保持不变
请输入分支名称
分支origin/feature/login
本地已有同名分支 feature/login。请选择如何检出远程分支。
请输入新的本地分支名称
储藏alune · main
保存改动并还原工作区,之后可以随时恢复。
将储藏 7 项已跟踪改动
将储藏 10 项改动,包括 3 个未跟踪文件
远程本机 · aluneSSH · 构建服务器
使用本机的 Git 凭据、SSH Agent 和 known_hosts 配置同步。
同步在远程主机上执行,使用该主机的 Git 认证配置。
提交alune
只写入此仓库的 Git 配置,不修改系统全局配置。
填写后,提交记录会显示为 clean <邮箱>
将写入 .git/config 的 [user] 段
推送拉取alune · feature/dialogs
当前分支还没有上游。确认远程和分支后执行。
Worktreealune · main
从当前 HEAD 创建新分支和独立的工作目录。请选择当前仓库之外的目录。
此目录位于当前仓库内,请选择仓库之外的位置
设置访问令牌
这里只保存配置;实际访问权限会在仓库读取 PR/MR 时检查。
保存后,3 个仓库远端关联的新请求会使用新值。
设置模型服务商 · OpenAI 兼容
点击「保存配置」后生效。显示名称留空时使用模型 ID。
此模型 ID 已存在
必须与服务商文档一致,不能包含空格
更改 ID 并保存配置后,需要在「提交生成」中重新选择默认模型。
PRShaoClean/Alune · #134
fix(settings): 复用工作区外壳并统一设置控件与响应式布局
保留全部 5 个提交,并在 development 上创建一个合并提交
将 5 个提交压缩为一个提交写入 development
把 5 个提交逐个变基到 development,不创建合并提交
提交时会再次核对代码版本与合并条件,最终由托管平台执行权限、检查和分支保护规则。
需要 1 项必需检查通过后才能合并。
06 Destructive
危险确认统一为「将放弃 / 将保留」对账单:用户先看到数量和范围,再看到按钮。L2 弹窗默认聚焦「取消」,↵ 不会触发主操作,必须显式勾选或输入名称。
工作区alune · mainL2 · 不可撤销
作用于整个当前仓库 / worktree,不受文件列表筛选影响。其他仓库和 worktree 不受影响。
正在核验整个仓库的改动…
当前没有可放弃的未暂存更改。
已恢复 7 个文件,已删除 0 个未跟踪文件;2 个文件未完成。请核对刷新后的状态,再重新确认。
SSH构建服务器 · aluneL2 · 不可撤销
正在核验远端文件和暂存内容…
将从远端磁盘删除此新增文件。
此新文件同时存在已暂存和未暂存改动,删除范围包含两部分的全部内容。
远端磁盘文件已不存在。本次将清理此路径残留的暂存新增内容。
直接删除,不会进入回收站。此操作不可撤销。
本机alune · feature/dialogsL2 · 覆盖远程
将更新远程历史,覆盖上次获取后已知的远程提交。
4e1f0a2调整弹窗间距b83c9d1临时修复c2d7e45弹窗外壳9a01b3f危险确认77f5e0c动效令牌使用 --force-with-lease:若远程又有变化,Git 会拒绝此次推送。
与当前分支名不一致
分支名一致
WorktreealuneL2 · 删除磁盘目录
存在改动、未跟踪或忽略文件时,Git 会拒绝删除。请先在此 Worktree 中提交或储藏。
设置访问令牌L1
只删除 alune 保存的配置,不会在 GitHub / GitLab 撤销令牌。
以下仓库远端将解除关联3
删除后,这些远端需要重新选择令牌才能读取 PR/MR。
尚未关联仓库,删除不会影响任何远端。
设置模型服务商L1
确定从此服务商移除“Sonnet 5.5”(claude-sonnet-5-5)吗?点击「保存配置」后生效。
删除并保存配置后,需要在「提交生成」中重新选择默认模型。
设置网络代理 · 2 项修改
离开前保存配置,或放弃这次修改。已有 SSH 工作不会被保存操作中断。
需要以 http:// 或 https:// 开头。继续编辑后再保存。
改动alune · 未跟踪目录L1
目录及其内容会从当前仓库的未跟踪列表中隐藏,磁盘文件会保留。
# git ls-files --others --exclude-from=.git/info/exclude/.playwright-cache/写入 Git 本地忽略文件,对同一仓库的 Worktree 生效,不会修改团队的 .gitignore。
改动alune · mainL1
将跳过以下 2 项,请在对应仓库中处理。
PRShaoClean/Alune · #133L2
关闭会结束此 PR/MR,代码不会合入目标分支。
托管平台返回 403:当前令牌没有关闭此 PR 的权限。
07 Info & panels
信息类弹窗不要求决定,只负责把上下文讲清楚:路径、归属和下一步入口。状态直接写进标题,用户扫一眼就知道现在处于哪一步。
改动alune · 未跟踪
此目录有独立的 Git 状态。请打开对应仓库查看改动;当前仓库的批量操作会跳过此目录。
此路径是目录,无法作为单个文件预览、暂存或删除。请在文件树中查看内容并单独处理。
需要移除此工作目录时,请使用 Worktree 管理中的移除操作。
changeActions() 一致,子模块不能本地忽略。设置访问令牌 · GitLab
以下仓库远端使用此令牌读取和操作 PR/MR。
尚未关联仓库。
储藏alune · main · 2 小时前
stash@{0}On main: 弹窗外壳草稿
@@ -18,9 +18,18 @@ export function DialogShell({ level, tone, children }: Props) { const titleId = useId(); return <Modal title={title} {...rest}>{children}</Modal>; const critical = level === 2 || tone === 'danger'; return ( <Modal role={critical ? 'alertdialog' : 'dialog'} keyboard={!critical || !pending} modalRender={(node) => <Bezel tone={tone}>{node}</Bezel>} >@@ -4,6 +4,9 @@ .ant-modal-content { border-radius: 8px; border-radius: calc(var(--dlg-radius) - var(--shell-pad)); box-shadow: 0 0 0 1px var(--hair), inset 0 1px 0 var(--dlg-core-hi);此储藏没有文本差异。
<pre> 纯文本改为 按文件分组的差异:文件头吸附显示增删计数,统一复用 DiffViewer 的行号与色板。+/- 符号列,色弱用户同样能分辨。设置版本更新 · GitHub Releases
38.6 MB / 96.4 MB · 6.1 MB/s
网络连接已重置,已下载的部分已丢弃。可以重新下载,或稍后再试。
更新说明v0.6.0 · 2026-09-28
下载完成后,点击「重启安装」将关闭当前 SSH 连接,安装新版本并重新打开应用。普通退出不会自动安装。
aria-live;操作按钮从正文移到底栏,每个状态只保留一个主操作。translateX 推进,不改宽度;检查中显示流光,不显示虚假的百分比。工作区此设备
侧边面板
面板宽度窗口较小时自动适配
差异视图也可在差异工具栏切换
08 Popconfirm & layers
气泡确认只用于影响单个对象、且可以说清后果的操作;一旦涉及多个对象或不可撤销,就升级为 L2 弹窗。浮层和弹窗用同一套双层外壳,但不加遮罩、不锁滚动。Esc 或点击外部都会关闭浮层,并把焦点还给触发按钮。
标题直接写出对象名,说明只写一句后果,确认按钮写动作本身。只有影响单个文件的丢弃仍用气泡,但需要先勾选确认。
删除“feature/login”?
已合并到 main,删除后可从 main 的历史中找回提交。
有 2 个提交尚未合并,Git 会拒绝删除。请先合并,或在分支页使用强制删除。
将“feature/login”合并到 main?
将合并 3 个提交。如有冲突,会停在冲突状态,可以随时中止。
建议先储藏
删除 stash@{1}?
“On main: 弹窗外壳草稿”包含 3 个文件的改动,删除后无法恢复。
丢弃此文件的未暂存改动?
恢复为暂存区的内容,已暂存的改动保留。此操作不可撤销。
apps/web/src/components/Feedback.tsx +12 −4
从列表移除“alune”?
只移除应用内登记,仓库目录与文件保持不变,之后可以重新打开。
/Users/clean/Desktop/alune
删除连接“开发机”?
以下仓库将无法访问,远程主机上的文件不受影响。
没有仓库使用此连接,可以放心删除。
浮层在打开时把焦点移到面板内的第一个可交互元素,↑↓ 在选项间移动,Esc 关闭并回到触发按钮。点「重播」可以查看出现动画。
正在读取分支…
aria-current 标记,不能再次选择;远程分支用地球图标,和本地分支区分开。按仓库来源分组 · ↑↓ 选择,↵ 打开
transform-origin 固定在左下角,与触发位置对应。在独立标签页打开,并加入仓库列表。关闭标签页会保留目录。
暂无其他关联 Worktree
本地存储空间不足,最近的布局修改只在本次运行中有效。
弹窗重设计、强制推送预览等 4 项更新。
暂无新通知
role=menu 支持 ↑↓、Home 和 End 键。09 Feedback centre
反馈中心汇总整个应用的错误和仓库说明。modal 模式的错误会直接弹出,一次只显示一条,其余排队;notification 和 manual 模式只进入右下角托盘。三种提示用同一套外壳,差别只在色调和第一个按钮。
alune仓库配置
分支、远程和作者信息暂时不可用,其他视图不受影响。
fatal: unable to read config file '/Users/clean/Desktop/alune/.git/config': Permission denied error: 读取 .git/config 失败(退出码 128)
role=status 播报原因。moonlight-api仓库说明
user.name 和 user.email 都未配置,Git 会拒绝提交。设置只写入此仓库的 .git/config,不影响全局配置。这类提示不会自动弹出,只留在托盘里;问题解决后自动移除。
aria-expanded,名称写明条数;色点只作装饰,读屏只读条数。10 Motion
所有动效只改 transform 和 opacity。入场用弹簧曲线,让弹窗有一点重量;退出用加速曲线,比入场快一倍以上。左侧是真实的弹窗层,可以按 4 倍慢放查看,也可以直接点弹窗里的按钮。
alune基于 main
translateY(12px) scale(.97) → 原位,360 ms --ease-spring;头、正文、页脚依次错开 40 ms。--ease-exit,内容不再错开,避免关闭时拖尾。scale(.98),弹窗再上移 8 px 淡出;方向和取消的下沉相反,一眼能分辨结果。--ease-glide,从 0.96 缩放,transform-origin 对准触发按钮,不加遮罩。prefers-reduced-motion 时取消位移和缩放,只保留 120 ms 淡入淡出。backdrop-filter,滚动内容和弹窗本体都不加。11 Responsive
Electron 窗口可以拖得很窄,通过浏览器远程访问时也可能在手机上打开。宽度不足 560 px 时,弹窗贴底变成面板:按钮纵向排列、高 44 px,取消固定在最下方;键盘提示隐藏,底部留出安全区。
工作区alune · main
alune基于 main
创建后立即切换到新分支。
分支本地
已合并到 main,删除后可从 main 的历史中找回提交。
桌面:按 400 / 520 / 680 / 880 四档宽度居中,距窗口边缘至少 32 px;气泡确认和浮层都锚定在触发按钮旁。
紧凑:lg、xl 降为 min(100vw − 32px),正文区域内部滚动;页脚提示保留,按钮仍横排。
面板:贴底,只保留上方圆角;按钮纵排、44 px 高,隐藏键盘提示,留出 safe-area-inset-bottom。气泡确认也改为面板。
12 Implementation
不引入新的组件库。沿用 antd 6 的语义化 classNames / styles,把双层外壳、语气色和动效令牌集中到一个 AluneModal 和一份全局样式里。下文的键名已按仓库中 antd 6.3.7 的类型定义核对。
设计稿中的每种容器都对应一个现有的 antd 组件或原生元素,调用方基本不用改。
| 设计组件 | 实现 | 挂载点 | 语义键 / 钩子 | 默认语气 |
|---|---|---|---|---|
弹窗 .dlg19 个文件使用 Modal | Modal → AluneModal | ConfigProvider.modal + modalRender | root mask container header title body footer | 月光蓝 |
| 确认弹窗modal.confirm · App.useApp | useAluneConfirm | 包装 modal.confirm | modalRender · focusable.autoFocusButton | 按等级 |
| 危险确认 L2放弃更改 · 强制推送 · 删除工作树 | AluneModal level={2} | data-level · data-tone | okButtonProps.danger · 勾选或输入名称解锁 | 危险 |
| 气泡确认分支 · 储藏 · 连接 · 仓库 | Popconfirm | ConfigProvider.popconfirm | root container title content · arrow: false | 柔和红 |
| 浮层与菜单分支选择 · 仓库切换 · 工作树 · 通知 | Popover · Dropdown | ConfigProvider.popover / dropdown | root container · item itemIcon | 月光蓝 |
| 反馈中心Feedback.tsx:193 · :204 | 原生 <dialog> · popover="manual" | .feedback-dialog · .feedback-tray | ::backdrop · :popover-open | 随提示类型 |
全局只改外观;需要分级、语气和解锁逻辑的地方换成 AluneModal / useAluneConfirm。
// 只改外观,不改调用方 <ConfigProvider theme={config} // 替换 ConfigProvider 中的 fontFamily → Geist modal={{ centered: true, mask: { blur: true }, closeIcon: <X weight="light" />, classNames: { root: 'a-dlg', mask: 'a-dlg-scrim', container: 'a-dlg-core', header: 'a-dlg-head', body: 'a-dlg-body', footer: 'a-dlg-foot', }, }} popconfirm={{ arrow: false, classNames: { root: 'a-pop' } }} popover={{ arrow: false, classNames: { root: 'a-pop' } }} dropdown={{ classNames: { root: 'a-menu' } }} >
export function AluneModal({ tone = 'info', level = 0, ...props }: AluneModalProps) { return ( <Modal {...props} transitionName="alune-dlg" okButtonProps={{ danger: tone === 'danger', ...props.okButtonProps, }} modalRender={(node) => ( <div className="a-dlg-shell" data-tone={tone} data-level={level}> {node} </div> )} /> ); } // L2:不自动聚焦主按钮,由勾选或输入名称解锁 modal.confirm({ ...options, focusable: { autoFocusButton: null }, });
.a-dlg-shell { padding: 6px; border-radius: 26px; background: var(--dlg-shell-bg); box-shadow: inset 0 0 0 1px var(--dlg-shell-ring), var(--dlg-shadow); } .a-dlg-core { border-radius: 20px; background: var(--dlg-core-bg); } .a-dlg-scrim { background: var(--scrim); } /* 固定层,模糊只作用于这里 */ /* rc-motion:prepare → start → active */ .alune-dlg-enter-start, .alune-dlg-appear-start { opacity: 0; transform: translateY(12px) scale(.97); } .alune-dlg-enter-active, .alune-dlg-appear-active { transition: transform 360ms var(--ease-spring), opacity 240ms var(--ease-enter); } .alune-dlg-leave-active { opacity: 0; transform: scale(.98); transition: transform 160ms var(--ease-exit), opacity 160ms var(--ease-exit); } dialog.feedback-dialog::backdrop { background: var(--scrim); backdrop-filter: blur(6px); } @media (max-width: 560px) { .a-dlg .ant-modal { top: auto; max-width: 100vw; margin: 0; padding-bottom: env(safe-area-inset-bottom); } .a-dlg-shell { border-radius: 26px 26px 0 0; } .a-dlg-foot { flex-direction: column-reverse; } } @media (prefers-reduced-motion: reduce) { [class*='alune-dlg-'] { transform: none !important; } }
把 --ease-*、--t-*、--tone-* 加入全局令牌;替换 ConfigProvider 中的 fontFamily,用 @fontsource-variable/geist 本地打包字体。
接入 ConfigProvider.modal,新增 AluneModal 与 useAluneConfirm,逐个替换 19 处 Modal 和确认调用。
按第 04 节的矩阵标注 L0 / L1 / L2。L2 增加确认勾选或输入名称,取消默认聚焦主按钮。
Popconfirm、Popover、Dropdown 统一使用 a-pop 皮肤并去掉箭头,补齐预检信息:是否已合并、工作区是否干净、被哪些仓库引用。
原生 <dialog> 套用同一套类名和 ::backdrop;加入 ≤ 560 px 面板样式;Playwright 覆盖 Esc、焦点恢复和减弱动效。