/* =========================================================================== 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" 防选中后拖出浏览器(默认 选中状态可拖) - 与 .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); }