Files
Notes/src/styles/04-toolbar.css
2026-10-18 04:39:37 +08:00

639 lines
22 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ===========================================================================
Toolbar + App Shell
自动从 src/styles.css 拆分(Stage 4a)
=========================================================================== */
/* ============================================================
Toolbar (custom titlebar)
============================================================ */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
height: var(--h-toolbar);
/* 右侧用更小的内边距,避免在默认 1200px 窗口下按钮组被裁;
顶部标题区仍保留 --space-5 的呼吸感 */
padding: 0 4px 0 var(--space-5);
background: var(--bg-toolbar);
/* audit fix (P2):去掉 backdrop-filter —— toolbar 在 bg-toolbar 实色之上,
毛玻璃的 saturate(180%) blur(20px) 强制把整条 1200×40 px 区域放到独立
compositor layer,且每帧滚动主内容时 GPU 都要重算这条带覆盖区域的模糊。
视觉上完全无差异(实色背景上看不出毛玻璃),但每次滚动都是几十 KB 像素的
重复 GPU 工作。 */
border-bottom: 1px solid var(--border-subtle);
/* 切主题时与 body 同步淡入淡出:03-base.css body 已声明
transition: background var(--transition-slow), color var(--transition-slow);
不在 .toolbar 显式声明会让 --bg-toolbar 跳色而 body 还在过渡,
形成 ~320ms 的色块错位(顶部硬切、主区淡变)。
border-color 同理:1px 底分隔线用 --border-subtle 不加 transition 会
出现"主色在淡、这条 1px 边线硬切"的视觉跳变。background 已经在 body 那条
慢节奏下淡变,border-color 用同样的 --transition-slow 节奏保持一致。 */
transition:
background var(--transition-slow),
border-color var(--transition-slow);
z-index: var(--z-toolbar);
position: relative;
/*
长文件名截断(fix:长文件名导致 toolbar 偏移):
- toolbar 是 body grid 子元素,min-width 默认是 auto = min-content。
.app-title-text 的 white-space:nowrap + 长文本让 min-content 极大,
toolbar 默认会撑到比视口还宽,把 .toolbar-right(关闭等按钮)挤出视口。
- min-width:0 覆盖默认 min-content,让 toolbar 接受 body grid 给它的宽度
- overflow:hidden 兜底:万一有其他内容溢出,clip 在 toolbar 内部而不是溢出到 body
(body 也是 overflow:hidden,但那是最后一道防线,视觉上能看到 toolbar 自身的偏移)
*/
min-width: 0;
overflow: hidden;
}
.toolbar::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -1px;
height: 1px;
background: var(--toolbar-divider);
}
.drag-region {
-webkit-app-region: drag;
flex: 1;
display: flex;
align-items: center;
gap: var(--space-3);
min-width: 0;
height: 100%;
padding-left: 2px;
}
.no-drag {
-webkit-app-region: no-drag;
display: flex;
align-items: center;
gap: var(--space-1);
}
/* 工具栏分组:将同一类别的按钮聚拢(视觉相邻),组间用 divider 隔开 */
.toolbar-group {
display: inline-flex;
align-items: center;
gap: 2px;
}
.toolbar-divider {
display: inline-block;
width: 1px;
height: 18px;
/* 用半透明白色渐变,比纯色 border 更精致;亮主题用对应浅色 */
background: linear-gradient(180deg,
transparent 0%,
var(--border-default) 25%,
var(--border-default) 75%,
transparent 100%);
/* 从 6px 减到 5px:默认 1200px 窗口下 6 个分组 + 7 个 divider 累计 84px gap,
改 5px 后 70px,让工具栏横向更紧凑 */
margin: 0 5px;
flex-shrink: 0;
}
.app-title {
font-size: var(--fs-base);
font-weight: 600;
color: var(--fg-primary);
letter-spacing: var(--tracking-wide);
display: flex;
align-items: center;
gap: 10px;
/* 选中态:避免拖拽时被高亮 */
user-select: none;
/*
长文件名截断(fix:长文件名导致 toolbar 偏移):
- min-width:0 是关键 —— flex 容器默认 min-content 是整段文本,
没有它 .app-title 会拒绝收缩,把 .toolbar-right(按钮组)挤出视口右侧
- overflow:hidden 让超出部分不可见,配合 .app-title-text 的 ellipsis 截断
*/
min-width: 0;
flex: 1 1 auto;
overflow: hidden;
}
/*
长文件名截断(fix:长文件名导致 toolbar 偏移):
- 真正承载文本的子 span —— 直接文本节点在 flex 容器里无法触发 ellipsis
- min-width:0 + flex:1 1 auto 让它优先吃光可用空间,超出才走截断
- text-overflow:ellipsis + white-space:nowrap 让长文件名从右侧省略
*/
.app-title-text {
display: block;
min-width: 0;
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.app-title::before {
content: "";
width: 20px;
height: 20px;
border-radius: 6px;
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.18),
0 0 0 1px color-mix(in srgb, var(--accent) 30%, transparent),
inset 0 1px 0 rgba(255, 255, 255, 0.18);
/* 固定图标尺寸,不参与收缩(flex-shrink:0) */
flex: 0 0 auto;
}
/* 应用图标(来自 icon.ico 的 PNG data URL):
- 20×20 + 6px 圆角,与 .app-title::before 占位形状一致,替换它而不是叠加
- draggable="false" 防选中后拖出浏览器(默认 <img> 选中状态可拖)
- 与 .app-title 共享同一行(display:flex 在 .toolbar-left) */
.app-icon {
width: 20px;
height: 20px;
border-radius: 6px;
flex: 0 0 auto;
/* 与 ::before 占位的多层 box-shadow 对齐,亮 / 暗主题下都不漂浮 */
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.18),
0 0 0 1px color-mix(in srgb, var(--accent) 30%, transparent),
inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
/* 真图标加载成功 → 隐藏 .app-title::before 占位,避免双图标 */
body.has-app-icon .app-title::before {
display: none;
}
/* Toolbar 按钮:图标按钮 + 悬浮效果
默认色用 secondary(暗色 ~8.5:1, 亮色 ~8:1),
旧值 secondary 在暗色仅 ~5.5:1,加上 13px 字号看着发灰,2026-08 统一提亮 */
.btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: var(--radius-sm);
color: var(--fg-secondary);
/* 加 transition 阴影和位移,让 hover 反馈更细腻 */
transition:
background var(--transition-fast),
color var(--transition-fast),
transform var(--transition-fast),
box-shadow var(--transition-fast);
}
.btn-icon:hover {
background: var(--bg-hover);
color: var(--fg-primary);
}
.btn-icon:active {
/* scale 从 0.94 减到 0.96:原值 0.94 在 34px 按钮上缩 2px,
视觉跳动明显;0.96 只缩 1px,反馈更克制 */
background: var(--bg-active);
transform: scale(0.96);
}
.btn-icon.is-active {
background: var(--accent-soft);
color: var(--accent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent);
}
.btn-icon.is-active:hover {
background: var(--accent-soft-hover);
}
.window-controls {
display: flex;
align-items: center;
/* 左侧 4px padding + 0 右侧:让最小化按钮与上一组 toolbar-divider 之间
留出和 chip 一致的呼吸(原本用 margin-left:4px),但保留右侧 flush
视觉(贴近视口右沿,沿用 Win/Linux 平台 UX)。原来用 margin-right:-2px
把关闭按钮"挤"到视口边;改 padding-right:0 让容器本身在 toolbar 4px
右 padding 之内、关闭按钮不再依赖负 margin 也能落在合理边缘。 */
padding-left: 4px;
padding-right: 0;
margin-left: 0;
gap: 0;
/* 用渐变分隔线替代纯色 —— 比 border 精致,符合 macOS 视觉 */
position: relative;
}
.window-controls::before {
/* 分隔线相对容器定位在 -4px:与 padding-left 等距,让「上一组按钮 →
divider → 窗口控制组」节奏与工具栏内其他 divider 间距一致。 */
content: "";
position: absolute;
left: -4px;
top: 50%;
transform: translateY(-50%);
height: 20px;
width: 1px;
background: linear-gradient(180deg,
transparent 0%,
var(--border-default) 25%,
var(--border-default) 75%,
transparent 100%);
}
.btn-window {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: var(--radius-sm);
/* 同 .btn-icon:用 secondary 替代 tertiary,避免暗色下窗口控制按钮发灰 */
color: var(--fg-secondary);
transition: background var(--transition-fast), color var(--transition-fast);
}
.btn-window:hover {
background: var(--bg-hover);
color: var(--fg-primary);
}
.btn-window.btn-close:hover {
background: var(--danger);
color: var(--btn-on-danger, #ffffff);
}
/* 工具栏按钮组(视图模式三连按钮) */
.btn-group {
display: inline-flex;
align-items: center;
/* 按钮组之间的间距由 .toolbar-divider 提供,这里去掉额外 margin-left */
margin-left: 0;
gap: 1px;
/* padding: 2px + 按钮 30px = 34px,与外层独立 .btn-icon 同高,保证视觉对齐 */
padding: 2px;
/* 容器底色比 hover 更轻,避免和单个按钮 hover 态重叠太深 */
background: color-mix(in srgb, var(--bg-hover) 70%, transparent);
border-radius: var(--radius-sm);
box-shadow: inset 0 0 0 1px var(--border-subtle);
}
.btn-group .btn-icon {
width: 30px;
height: 30px;
border-radius: 5px;
}
.btn-group .btn-icon:hover:not(.is-active) {
background: var(--bg-hover);
}
.btn-group .btn-icon.is-active {
background: var(--bg-elevated);
color: var(--accent);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.12),
inset 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent);
}
/* 图标 + 文字按钮(视图模式专用):
让核心交互(预览/编辑/双栏)一眼可识别,
同时保留 segmented control 的紧凑感。
12px 字号 + secondary(暗色 ~8.5:1)才能保证文字不糊;
2026-08 把 secondary 从 ~5.5:1 提到 ~8.5:1 后视图模式按钮显著更清晰 */
.btn-icon-text {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
height: 30px;
padding: 0 10px;
border-radius: 5px;
color: var(--fg-secondary);
font-size: var(--fs-xs-readable);
font-weight: 500;
letter-spacing: var(--tracking-wide);
transition:
background var(--transition-fast),
color var(--transition-fast),
transform var(--transition-fast);
white-space: nowrap;
}
.btn-icon-text:hover:not(.is-active) {
background: var(--bg-hover);
}
.btn-icon-text:active {
transform: scale(0.96);
}
.btn-icon-text .btn-label {
line-height: 1;
/* 与按钮对齐基线 */
}
.btn-icon-text.is-active {
background: var(--bg-elevated);
/* 文字版(--accent-text):view mode "预览 / 编辑 / 双栏"、autosave "自动保存"
这种带字按钮 active 时若用纯 accent,"预览" "自动保存" 在工具栏里整片
accent 短词同时亮起来会让眼睛辛苦。降一档后 bg-elevated + inset 1px
accent 边框仍清楚表达"按下",文字不再贴脸 */
color: var(--accent-text);
font-weight: 600;
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.12),
inset 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent);
}
/* (audit fix Round 9: .toolbar-status-dot / .btn-icon-text > .toolbar-status-dot
死 CSS —— 保存按钮改 is-dirty 状态后该指示点彻底弃用,见
feedback-save-button-always-visible.md。iOS pill switch 替代品见下) */
/* iOS pill switch —— 替代 .toolbar-status-dot 作为开关语义。
26×14px 嵌入 .btn-icon-text,与现有 24px 按钮中线对齐。
视觉态由 [aria-pressed="true|false"] 选择器驱动,与项目
「aria-pressed 表示开关态」的模式一致(参考 btn-theme)。
保留 .btn-icon-text 外壳让按钮仍享有 hover/active 视觉反馈。 */
.toolbar-toggle .toggle-switch {
position: relative;
display: inline-block;
width: 26px;
height: 14px;
margin-left: 4px;
flex: 0 0 auto;
}
.toolbar-toggle .toggle-switch-track {
position: absolute;
inset: 0;
background: var(--fg-muted);
opacity: 0.5;
border-radius: 7px;
transition: background var(--transition-fast), opacity var(--transition-fast);
}
.toolbar-toggle .toggle-switch-thumb {
position: absolute;
top: 1px;
left: 1px;
width: 12px;
height: 12px;
background: var(--bg-elevated);
border-radius: 50%;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
transition: transform var(--transition-fast);
}
.toolbar-toggle[aria-pressed="true"] .toggle-switch-track {
background: var(--accent);
opacity: 1;
}
.toolbar-toggle[aria-pressed="true"] .toggle-switch-thumb {
transform: translateX(12px);
}
/* hover 微微提亮,与其它 btn-icon-text hover 一致 */
.toolbar-toggle:hover .toggle-switch-track { opacity: 0.85; }
.toolbar-toggle[aria-pressed="true"]:hover .toggle-switch-track { opacity: 1; }
/* 关态时整体暗化,与工具栏 fg-tertiary 一致,强调「关闭」语义 */
.toolbar-toggle[aria-pressed="false"] {
color: var(--fg-tertiary);
}
/* a11y:prefers-reduced-motion 下关闭 transform 过渡 */
@media (prefers-reduced-motion: reduce) {
.toolbar-toggle .toggle-switch-track,
.toolbar-toggle .toggle-switch-thumb { transition: none; }
}
.btn-icon.is-dirty,
.btn-icon-text.is-dirty {
/* 脏状态:使用 warning 色作为视觉提示,并轻微脉动 */
color: var(--warning);
background: color-mix(in srgb, var(--warning) 12%, transparent);
animation: dirty-pulse 1.8s ease-in-out infinite;
}
.btn-icon.is-dirty:hover,
.btn-icon-text.is-dirty:hover {
color: var(--warning);
background: color-mix(in srgb, var(--warning) 18%, transparent);
}
/* 保存中:与 is-dirty 的 warning 区分,用 fg-secondary 中性色 + 微微透明度。
cursor: progress 给鼠标用户明确提示(参见 [[feedback-save-button-always-visible]]
——按钮始终可见,这里只是临时视觉变化,不切换 hidden)。 */
.btn-icon.is-saving,
.btn-icon-text.is-saving {
cursor: progress;
opacity: 0.6;
color: var(--fg-secondary);
background: var(--bg-hover);
/* 关掉 dirty-pulse 动画:避免 is-saving + is-dirty 叠加造成闪烁 */
animation: none;
/* Round 13 fix (M22):正在保存时锁住点击 —— 否则双击 save 会触发两次
writeFile,第二次 mtime 比预期新 → FILE_CHANGED_EXTERNALLY 误判。
savingCount 防并发应该兜底,但鼠标双击事件触发极快,savingCount 在第二次
进入前未必 ++;UI 层锁一道最稳。 */
pointer-events: none;
}
/* a11y:prefers-reduced-motion 下保持稳定视觉,不透明度也保留 */
@media (prefers-reduced-motion: reduce) {
.btn-icon.is-saving,
.btn-icon-text.is-saving {
opacity: 0.7;
cursor: progress;
}
}
/* 主题按钮图标:按当前主题显示"代表当前状态"的那一态——
暗色主题下显示月亮,亮色主题下显示太阳(点击图标会切到对侧主题)。
动画:两个 SVG 同位置叠加,靠 opacity + 180° rotate 切换。
- opacity 让图形柔和渐入渐出(避免硬切)
- rotate 让图形过渡时有"翻转"暗示:从太阳转到月亮是 180° 翻转的语义
- 用 position:absolute 把两者钉到 button 中心,flex 自然布局让其中一个盖在另一个之上
用 [data-theme] 选择器控制「哪一态」当前显示,状态切换由 renderer
通过 toggleTheme() 改 body.dataset.theme 完成,CSS 自动追平。 */
#btn-theme {
position: relative;
}
#btn-theme .theme-icon {
position: absolute;
inset: 0;
margin: auto;
display: block;
opacity: 0;
transform: rotate(-90deg) scale(0.6);
transition:
opacity var(--transition-fast),
transform var(--transition-fast);
}
body[data-theme="dark"] #btn-theme .theme-icon-moon,
body[data-theme="light"] #btn-theme .theme-icon-sun {
opacity: 1;
transform: rotate(0) scale(1);
}
/* (audit fix Round 9: .toolbar-status-dot 死 CSS,详见 04-toolbar.css 顶部注释) */
/* dirty-pulse:脉动从 1 → 0.65 太深,warning 色按钮在 50% 帧会瞬间
看着像"warning 没了"。改成 1 → 0.78:仍能看出脉动,但始终保持 warning
视觉在场,"未保存"语义不中断。1.8s 节奏不变 */
@keyframes dirty-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.78; }
}
/* 工具栏图标按钮的 disabled 态(如非 md 文件下 preview/split 按钮)
复用 .context-menu-item 的禁用色(fg-tertiary + opacity),保持一致 */
.btn-icon:disabled,
.btn-icon[disabled] {
color: var(--fg-tertiary);
cursor: not-allowed;
opacity: 0.4;
pointer-events: none;
}
/* 图标+文字按钮(视图模式 预览/编辑/双栏)的 disabled 态。
非 Markdown 文件打开时 preview/split 按钮会被禁用 —— 这里让灰态视觉一致 */
.btn-icon-text:disabled,
.btn-icon-text[disabled] {
color: var(--fg-tertiary);
cursor: not-allowed;
opacity: 0.4;
pointer-events: none;
}
/* ============================================================
Toolbar Search —— 已迁回侧栏(设计回调 2026-10)
------------------------------------------------------------
搜索文件框从工具栏首位移回左侧栏顶部,紧贴它过滤的 .file-list
(见 [[feedback-prefer-near-affected-area]])。样式跟随 HTML 一起
搬到 05-sidebar.css 的 .sidebar-search;id="search-input" 不变,
shortcuts.js / file-list.js 接线零改动。
工具栏腾出的空间:撤销 / 重做按钮组并入操作组(#btn-undo / #btn-redo),
极窄窗口的腾挪策略见文件底部 720px 断点。
============================================================ */
/* 窄窗口(< 820px):toolbar 文字标签隐藏,只保留图标。
与 sidebar 720px 阈值错开,留出中间过渡区,避免一次性把全部 toolbar 砍塌。 */
@media (max-width: 820px) {
.btn-icon-text .btn-label {
display: none;
}
.btn-icon-text {
/* 没文字时收紧 padding,让按钮保持图标按钮的紧凑感 */
padding: 0 6px;
}
}
/* 极窄窗口(≤ 720px,含 600px 最小窗口):工具栏右簇(外观 + 视图 +
重置布局 + 自动保存 + 撤销/重做/保存/关闭/设置 + 窗口控件)在 600px
视口下空间紧张,frameless 窗口的关闭入口(最小化/最大化/关闭)一旦被
挤出视口等于「关不掉窗口」。这里只收起最低优先级的控件腾出空间:
- 隐藏「重置布局」按钮组 + 它后面的分隔线(低频操作,仍可经右键菜单/设置恢复)
- 自动保存折叠为纯图标(隐藏文字标签 + iOS 开关,保留按钮本体与 aria-pressed
状态色;完整开关仍在设置里),约省 27px
撤销 / 重做本就是 icon-only 紧凑按钮组(~60px),不折叠。 */
@media (max-width: 720px) {
#tg-reset-layout,
#tg-reset-layout + .toolbar-divider {
display: none;
}
#toolbar-autosave .btn-label,
#toolbar-autosave .toggle-switch {
display: none;
}
#toolbar-autosave {
padding: 0 6px;
}
}
/* ============================================================
App Shell
============================================================ */
.app-shell {
display: grid;
/* 侧栏宽度走 --sidebar-width(运行时可拖拽调整),默认值来自 --w-sidebar;
这样改 token 不必同时改 grid 模板,也避免与窄屏媒体查询耦合 */
grid-template-columns: var(--sidebar-width) 4px 1fr;
min-height: 0;
overflow: hidden;
}
/* 侧栏拖拽分割条:只在 split / preview / edit 单栏模式下显示。
视觉宽度 4px。命中区扩到 ±6px(共 12px):由 app.js 的 mountSidebarSplitter
监听 document mousedown + mousemove,在鼠标距 splitter 中线 ≤6px 时启动拖动,
并把光标改成 col-resize 提示可拖。这是 grid track 4px 限制下的标准做法。 */
.sidebar-splitter {
background: var(--border-subtle);
cursor: col-resize;
position: relative;
width: 4px;
transition: background var(--transition-fast);
}
.sidebar-splitter:hover,
.sidebar-splitter.is-dragging {
background: var(--accent-soft);
}
.sidebar-splitter::after {
/* 抓握提示:两条短竖线 */
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 1px;
height: 24px;
background: var(--border-default);
box-shadow: 3px 0 0 var(--border-default);
}
.sidebar-splitter:hover::after,
.sidebar-splitter.is-dragging::after {
background: var(--accent);
box-shadow: 3px 0 0 var(--accent);
}
/* 侧栏恢复默认宽度按钮(hover splitter 时显形)
- 4px splitter 视觉宽度里塞不下按钮,所以按钮是 absolute 定位居中 + 视觉溢出;
但 splitter 本身 hover 区域扩到 ±6px(HIT_RADIUS),按钮顺势接管这块区域
- 默认 opacity:0 + pointer-events:none → 完全不挡 drag 命中区;
hover splitter / 按钮自身 / focus-visible 时显形 */
.sidebar-splitter-reset {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 18px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-card);
border: 1px solid var(--border-default);
border-radius: 3px;
color: var(--fg-secondary);
cursor: pointer;
padding: 0;
opacity: 0;
pointer-events: none;
transition: opacity var(--transition-fast), background var(--transition-fast), color var(--transition-fast);
}
.sidebar-splitter:hover .sidebar-splitter-reset,
.sidebar-splitter-reset:focus-visible {
opacity: 1;
pointer-events: auto;
}
.sidebar-splitter-reset:hover {
background: var(--accent-soft);
color: var(--accent);
border-color: var(--accent);
}
.sidebar-splitter-reset:active {
background: var(--accent);
color: var(--bg-card);
}