ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Pomotroid 的 Tooltip 组件体系:组件契约、交互模型与全量挂载清单

Pomotroid 的 Tooltip 组件体系:组件契约、交互模型与全量挂载清单 【免费下载链接】pomotroid:tomato: Simple and visually-pleasing Pomodoro timer项目地址https://gitcode.com/gh_mirrors/po/pomotroid点击查看免费下载Pomotroid 是一款基于 Tauri Svelte 的番茄钟桌面应用其计时器窗口大量使用纯图标按钮、设置窗口使用极简的开关标签很多控件的具体行为与平台注意事项仅靠图标难以自明。本指南以 openspec/specs/tooltips/spec.md 为核心完整讲解 Pomotroid 的 Tooltip 组件契约、延迟与即时两种交互模型、视图边缘翻转定位算法、主题适配与无障碍实现并给出计时器窗口与设置窗口的完整 tooltip 挂载清单含 i18n key 与触发延迟使读者既能直接复用这套组件模式也能对照源码理解其底层原理。一、为什么需要一套 Tooltip 体系背景与设计动机Pomotroid 的 UI 设计遵循极简原则计时器窗口中的设置、统计、重开本轮、跳过、重置、静音等控件全部是图标按钮其aria-label虽可供屏幕阅读器使用但对普通用户来说控件的精确行为如重置会清空整个会话进度、跳过会进入下一轮并不可直接发现设置窗口中则以短标签 开关为主诸如 Verbose Logging、WebSocket Server 这类需要额外说明的开关也没有空间在标签旁展开解释。原提案 openspec/changes/archive/2026-03-26-ui-tooltips/proposal.md 明确指出两个核心诉求即时上下文just-in-time context在不增加 UI 密度不 clutter的前提下让用户在悬停的瞬间获得控件用途与后果说明平台相关提示的精确投放GNOME 下托盘图标需要安装 AppIndicator 扩展这一提示此前以静态.note段落显示给所有平台用户属于信息噪音。改用 info 图标 即时 tooltip 后该提示只在 Linux 平台出现恰好出现在用户即将使用该开关的时刻。设计目标与非目标摘自 design.md也值得注意目标可复用的Tooltip.svelte组件延迟按钮/开关与即时info 图标两种交互模式CSS 驱动定位 JS 在视图边缘翻转全部文案走 Paraglide i18n用 info 图标替换 GNOME 托盘提示段落。非目标不支持富内容HTML/图片仅纯文本不针对触屏桌面应用不引入第三方 tooltip 库保持零依赖。从仓库结构看最终实现完全落在这套约束内src/lib/components/Tooltip.svelte与src/lib/components/TooltipInfo.svelte是两个新增组件i18n key 全部集中在src/messages/*.json未在package.json引入任何定位库该功能为纯前端实现后端 Rust 侧无改动。二、Tooltip.svelte 组件契约原规格文档定义了Tooltip.svelte的完整组件契约实现位于 src/lib/components/Tooltip.svelte。2.1 Props 与插槽Prop类型默认值说明textstring必填tooltip 文案由调用方通过 Paraglide 翻译后的消息函数传入如m.tooltip_reset()delaynumber600悬停多少毫秒后显示 tooltipplacementabove \| belowabove首选显示方位触发元素通过默认插槽default slot传入即插槽内容可以是任意元素按钮、span、甚至一组元素。源码中的 Props 定义与解构如下interface Props { text: string; delay?: number; placement?: above | below; children: Snippet; // Svelte 5 的 Snippet 类型对应默认插槽 } let { text, delay 600, placement above, children }: Props $props();在模板中使用时调用方只需要这样写以 src/lib/components/Timer.svelte 的 Restart Round 按钮为例Tooltip text{m.tooltip_restart_round()} button classbtn-side onclick{timerRestartRound} aria-labelRestart round svg width18 height18 viewBox0 0 16 16…/svg /button /Tooltip2.2 触发与隐藏的交互模型规格文档用四个场景定义了交互模型源码实现show()/hide()函数逐一对应场景规格要求源码实现延迟出现悬停达到delay毫秒后显示show()中若delay 0则setTimeout(run, delay)且visible || timer ! undefined时直接返回避免重复计时即时出现delay{0}时悬停立即显示delay 0分支直接执行run()不走定时器鼠标离开立即消失移出触发元素即刻隐藏hide()中clearTimeout(timer)取消未触发的定时器并将visible、positioned、flipped、tooltipStyle全部复位视图边缘翻转首选方向会溢出视口时反向渲染updatePosition()中计算flipped见下节触发事件绑定在包装层span.tooltip-wrapper上onpointerenter{show}、onpointermove{show}、onpointerleave{hide}。使用 Pointer Events 而非 mouseenter/mouseleave 是合理的——onpointermove的重复触发会被show()顶部的守卫条件visible || timer ! undefined拦截不会重复创建定时器。三、定位算法CSS 定位 视口边缘翻转规格文档要求触发元素位于会使 tooltip 在首选方向溢出视口的位置时tooltip 应在相反方向渲染。原设计文档 openspec/changes/archive/2026-03-26-ui-tooltips/design.md 记录了决策过程曾考虑use:action 与第三方定位库Floating UI / Popper.js最终选择Svelte 组件包装 position: fixed 数行 JS 翻转逻辑理由有二use:action 难以在元素外渲染 DOM 节点且会让无障碍接线aria-describedby更复杂Pomotroid 窗口尺寸固定且较小纯 CSS 无法读取视口位置但几行getBoundingClientRect()检查足以覆盖边界情况无需引入完整定位库。3.1 核心实现源码级updatePosition()的实现要点src/lib/components/Tooltip.sveltefunction updatePosition() { if (!wrapper || !tooltipEl) return; const wRect wrapper.getBoundingClientRect(); const tRect tooltipEl.getBoundingClientRect(); const centerX wRect.left wRect.width / 2; const pad 8; // 垂直翻转触发元素距视口顶部不足 70px 且首选方位为 above 时翻转到下方 flipped wRect.top 70 placement above; let top: number; if (actualPlacement below) { top wRect.bottom 8; } else { top wRect.top - 8 - tRect.height; } // 水平方向以触发元素中心为基准夹取在视口内左右各留 8px 内边距 let left centerX - tRect.width / 2; if (left pad) left pad; if (left tRect.width window.innerWidth - pad) { left window.innerWidth - pad - tRect.width; } // 箭头偏移始终指向触发元素的水平中心 const arrowLeft centerX - left; tooltipStyle top:${top}px;left:${left}px;--arrow-left:${arrowLeft}px; positioned true; }几个值得注意的实现细节翻转阈值 70px当触发元素顶部距视口顶部不足 70px 且首选方位为above时翻转为below——这覆盖了标题栏Titlebar等高居窗口顶部的按钮场景实际方位由$derived计算actualPlacement flipped ? below : placement并在模板中通过class:below切换箭头方向position: fixed规避裁剪规格要求 tooltip 使用position: fixed确保其不被任何overflow容器裁剪。这一点在统计页的年度热力图src/lib/components/stats/YearlyView.svelte中体现得尤为明显——SVG 内部的单元格 tooltip 同样采用视口固定 手动算坐标的模式注释明确写着 viewport-fixed so it escapes SVG/overflow clipping防闪现tooltip 默认visibility: hidden直到 JS 完成定位后才通过.positioned类显示避免在左上角闪现一帧箭头随触发元素移动通过 CSS 变量--arrow-left传入箭头始终对准触发元素的水平中心即使 tooltip 被水平夹取偏移后也是如此。四、TooltipInfo 组件设置窗口的即时说明图标规格文档要求系统提供TooltipInfo.svelte渲染一个样式化的信息图标悬停时立即显示 tooltipdelay{0}仅接受text: string一个 prop。它被设计为放置在设置窗口的开关标签旁用于承载上下文说明而不使布局拥挤。实现位于 src/lib/components/TooltipInfo.svelte源码极简Tooltip {text} delay{0} span classinfo-icon roleimg aria-labelMore information onclick{(e) e.stopPropagation()} svg width13 height13 viewBox0 0 13 13 fillnone aria-hiddentrue…/svg /span /Tooltip两个与规格呼应的实现要点delay{0}即时显示与按钮的 600ms 延迟形成对比。设计文档的 rationale 是info 图标存在的唯一目的就是被悬停以获取信息用户期待的是即时反馈而 600ms 延迟是为了防止 tooltip 在鼠标随意划过按钮时频繁弹出造成噪音stopPropagation防误触注释说明必须使用span而非button因为 info 图标可能位于button标签内部见 SettingsToggle.svelte 的行结构span 是合法的 phrasing contentonclick中stopPropagation阻止 info 图标的点击冒泡触发父级按钮的切换逻辑。4.1 视觉层级规格要求 info 图标静止时使用--color-foreground-darker、悬停时使用--color-foreground保持相对相邻标签的次要地位。最终实现通过opacity达成同等的次要但可发现效果静止时opacity: 0.55悬停时opacity: 1配合transition: opacity 0.15s平滑过渡且设置cursor: default、user-select: none避免误导用户点击。4.2 在 SettingsToggle 中的集成设置窗口的开关行统一由 src/lib/components/settings/SettingsToggle.svelte 渲染它新增了可选的tooltip?: stringpropspan classlabel {label}{#if tooltip}TooltipInfo text{tooltip} /{/if}/span 调用方如 SystemSection 的 WebSocket 开关只需传入tooltip{m.tooltip_websocket()}info 图标即自动出现在标签旁。这种一行接入的设计让设置窗口的说明性提示实现了零样板boilerplate-free挂载。五、计时器窗口Tooltip 挂载全量清单规格文档给出了计时器窗口的完整 tooltip 清单以下表格完整继承i18n key 与延迟均可在 src/messages/en.json 与对应组件中逐一核对元素位置i18n key延迟Settings 按钮Titlebartooltip_settings600 msStatistics 按钮Titlebartooltip_statistics600 msRestart Round 按钮Timer controlstooltip_restart_round600 msSkip 按钮Timer controlstooltip_skip600 msReset 按钮Footertooltip_reset600 msMute/Unmute 按钮Footertooltip_mute/tooltip_unmute600 msRound indicatorFootertooltip_round_counter/tooltip_round_counter_session600 ms5.1 标题栏Settings 与 Statistics在 src/lib/components/Titlebar.svelte 中两个按钮以{#snippet}定义并通过Tooltip包装且会根据平台摆放在标题栏左/右侧macOS 的交通灯按钮占位左侧非 macOS 在左侧显示这两个按钮。这是规格中 600ms 延迟的标准用法。5.2 计时控制区Restart Round 与 Skipsrc/lib/components/Timer.svelte 中重开本轮回退图标与跳过前进图标按钮分别包裹在Tooltip text{m.tooltip_restart_round()}与Tooltip text{m.tooltip_skip()}中。规格为这两个按钮定义了明确的场景语义Skip tooltip 描述动作悬停 600ms 后显示tooltip_skip说明跳过当前轮次并开始下一轮Skip to the next round.值得注意的是规格与设计文档对Play/Pause 按钮有意不加 tooltip——其功能被普遍理解避免对明显控件过度 tooltipover-tooltippingdesign.md 的 Open Questions 中明确记录了这一取舍并留待反馈复审。5.3 底部栏Reset、Mute/Unmute 与 Round indicatorsrc/lib/components/TimerFooter.svelte 集中了三类工具提示其中两类是状态条件化的Reset规格场景传达后果悬停 600ms 显示tooltip_reset文案明确告知破坏性后果——Reset the timer to the first work round. Current session progress will be cleared.计时器回到第一个工作轮当前会话进度将被清除。这类后果前置的 tooltip 文案设计正是 tooltip 体系价值的典型体现。Mute/Unmute规格场景反映当前状态文案随音量状态动态切换Tooltip text{localVolume 0 ? m.tooltip_unmute() : m.tooltip_mute()}即音量大于 0 显示tooltip_muteMute alert sounds音量为 0 显示tooltip_unmuteUnmute alert sounds。配合premuteVolume记住静音前的音量取消静音时能恢复原音量而非固定回 1.0。Round indicator规格场景反映长休息模式同一触发元素按long_breaks_enabled状态切换两个 keyTooltip text{$settings.long_breaks_enabled ? m.tooltip_round_counter() : m.tooltip_round_counter_session()} span classrounds {#if $settings.long_breaks_enabled} {snap.work_round_number} nbsp;|nbsp; {snap.work_rounds_total} {:else} {m.timer_session_round({ n: snap.session_work_count })} {/if} /span /Tooltip启用长休息时显示形如 2 / 4tooltip 为tooltip_round_counter——Current work round out of the total rounds before a long break当前工作轮在长休息前总轮次中的位置禁用长休息时显示形如 Round 3tooltip 为tooltip_round_counter_session——Continuous session round count. Resets only when the timer is reset.连续会话轮次计数仅重置计时器时归零。六、设置窗口TooltipInfo 挂载清单与平台守卫规格文档合并版给出设置窗口的 info 图标清单。结合当前仓库源码实际生效的挂载点如下设置项所属分区i18n key说明Show in System TraySystem托盘组system_tray_gnome_hint仅 Linux 渲染isLinux守卫复用既有 keyVerbose LoggingSystemAdvanced 日志组tooltip_verbose_logging说明开启详细调试日志的用途与日志位置WebSocket ServerSystem集成组tooltip_websocket说明本地 WebSocket 服务的用途如直播覆盖层注归档版 spec 还包含 Dial Countdown、Auto-start Work、Auto-start Break 三项tooltip_dial_countdown、tooltip_auto_start_work、tooltip_auto_start_break对应 key 在 src/messages/en.json 中仍存在但在当前 src/lib/components/settings/sections/TimerSection.svelte 中尚未通过tooltipprop 挂载——这些 key 属于既有 i18n 资产可为后续 UI 复用。6.1 GNOME 提示的 Linux-only 守卫规格场景对 Show in System Tray 的守卫有明确要求在 Linux 上TooltipInfo图标出现在托盘开关标签旁在 macOS / Windows 上该开关不显示 info 图标。源码实现位于 src/lib/components/settings/sections/SystemSection.svelteSettingsToggle label{m.system_toggle_show_tray()} description{m.system_toggle_show_tray_desc()} tooltip{isLinux ? m.system_tray_gnome_hint() : undefined} checked{$settings.tray_icon_enabled} onclick{() toggle(tray_icon_enabled, $settings.tray_icon_enabled)} /isLinux来自 src/lib/utils/platform.ts/Linux/.test(navigator.userAgent)。当tooltip为undefined时SettingsToggle 的{#if tooltip}分支不渲染 info 图标从而实现平台条件化。GNOME 提示的文案本身en.json是操作性的提醒 GNOME 下托盘图标需要 AppIndicator 扩展可通过包管理器安装gnome-shell-extension-appindicator或从 extensions.gnome.org 安装然后重新登录生效。同时规格要求移除原先对所有平台显示该文本的静态.note段落——当前 SystemSection 中已无该段落仅剩 WebSocket 端口说明仍使用.note印证了迁移完成。6.2 设置窗口 info 图标即时显示所有设置窗口的TooltipInfo都继承delay{0}行为——悬停即显无延迟与规格场景Settings info icons show tooltip immediately一致。七、样式规范与主题适配规格文档定义了 tooltip 的样式契约最终实现src/lib/components/Tooltip.svelte逐项落实规格要求实现值背景色--color-background-light未定义时使用计算回退color-mix(in oklch, var(--color-foreground) 10%, var(--color-background))文字颜色var(--color-foreground)字号0.72rem行高 1.4圆角4px最大宽度240pxwhite-space: normal自动换行视觉分隔box-shadow: 0 2px 8px color-mix(in oklch, black 30%, transparent) 1px 边框前景色 12% 透明度混合小箭头::after伪元素实现5px 三角颜色取var(--tooltip-bg)--arrow-left控制水平位置定位position: fixed规避 overflow 容器裁剪z-index: 9999防遮挡pointer-events: nonetooltip 不拦截鼠标事件7.1 主题联动机制规格场景要求用户切换主题时任何可见 tooltip 立即反映新主题的 CSS 自定义属性。这一目标通过两层设计达成tooltip 的样式完全基于 CSS 自定义属性--color-background-light、--color-foreground等构建这些变量由主题系统注入到:root例如 src/app.css 中定义--color-background-light: #3e4a5d等主题切换本质上是替换这些变量由于 tooltip 是内联渲染在触发元素旁的sibling 节点而非通过 portal 挂到document.body之外它天然继承当前作用域内的主题变量——切换主题后 CSS 变量重新求值无需任何额外逻辑即可即时生效。从源码结构看主题适配还包含一层健壮性设计--color-background-light未定义时回退值color-mix(in oklch, var(--color-foreground) 10%, var(--color-background))基于前景色 10% 混合背景色计算保证任何主题下 tooltip 背景与前景都有足够对比度。八、无障碍aria-describedby 接线规格文档对无障碍有明确要求可见时触发元素包装层设置aria-describedby指向 tooltip 元素的id屏幕阅读器在播报控件标签的同时播报 tooltip 文本隐藏时aria-describedby属性必须不存在。源码实现src/lib/components/Tooltip.sveltespan classtooltip-wrapper bind:this{wrapper} onpointerenter{show} onpointermove{show} onpointerleave{hide} aria-describedby{visible ? tooltipId : undefined} {render children()} {#if visible} span bind:this{tooltipEl} classtooltip class:below{actualPlacement below} class:positioned style{tooltipStyle} id{tooltipId} roletooltip{text}/span {/if} /span实现细节tooltipId使用运行时生成的唯一 IDtooltip-${Math.random().toString(36).slice(2, 9)}避免多实例 ID 冲突aria-describedby仅在visible为 true 时设置visible ? tooltipId : undefined隐藏时属性被移除——与规格隐藏时属性应不存在完全一致tooltip 元素标注roletooltip与aria-describedby配对构成标准的 ARIA tooltip 模式模板中!-- svelte-ignore a11y_no_static_element_interactions --注释用于豁免对静态 span 上 pointer 事件的 Svelte a11y 检查触发通过指针事件而非键盘事件这是 tooltip 的固有交互模型。九、i18n全量文案与多语言覆盖规格文档要求所有 tooltip 文案通过 Paraglide 翻译并由调用方传入组件本身只接受翻译后的字符串。仓库的 8 个语言文件src/messages/下的en.json、zh.json、fr.json、de.json、es.json、pt.json、ja.json、tr.json均包含全部 tooltip key。以 src/messages/en.json 为例核心 tooltip key 全集如下中文对照取自 src/messages/zh.jsonkey英文文案中文文案tooltip_settingsOpen Settings打开设置tooltip_statisticsOpen Statistics打开统计tooltip_restart_roundRestart the current round from the beginning.从头开始重启当前轮次。tooltip_skipSkip to the next round.跳过到下一轮。tooltip_resetReset the timer to the first work round. Current session progress will be cleared.将计时器重置到第一个工作轮次。当前会话进度将被清除。tooltip_muteMute alert sounds静音提示音tooltip_unmuteUnmute alert sounds取消静音tooltip_round_counterCurrent work round out of the total rounds before a long break.当前工作轮次显示在长休息前的总轮次中的位置。tooltip_round_counter_sessionContinuous session round count. Resets only when the timer is reset.连续会话轮次计数。仅在重置计时器时重置。tooltip_verbose_loggingEnables detailed debug logging. Use when reporting issues. Log files are accessible via Open Log Folder in Settings → About.启用详细调试日志。用于报告问题时使用。日志文件可通过设置 → 关于中的打开日志文件夹访问。tooltip_websocketEnables a local WebSocket server for external integrations such as stream overlays. Disabled by default.启用本地 WebSocket 服务器用于直播覆盖层等外部集成。默认禁用。system_tray_gnome_hintOn GNOME, tray icons require the AppIndicator extension…在 GNOME 中托盘图标需要安装 AppIndicator 扩展…设计文档design.md的 Risks 一节也坦诚记录了翻译质量风险7 个非英语 locale 的 tooltip 字符串初期由机器翻译可接受后续由社区改进。这是事实层面的诚实表述——tooltip 体系的价值上限直接取决于文案质量长字符串在 240px 最大宽度下的换行效果也是未来可优化的点design.md 的 Open Questions 提及此点。十、实现验证与工作流对照归档任务清单 openspec/changes/archive/2026-03-26-ui-tooltips/tasks.md 完整记录了落地过程可作为对照验证清单组件层创建Tooltip.svelteprops、slot、延迟定时器、视口翻转、aria 接线、主题样式与TooltipInfo.sveltedelay{0} 样式化图标——均已完成i18n 层14 个 tooltip key 加入 en.json 并翻译到 7 个其他 locale——已完成计时器窗口Restart Round、Skip、Reset、Mute/Unmute状态条件、Round indicator模式条件、Settings、Statistics 全部完成包裹设置窗口GNOME 提示Linux-only 移除旧.note、Verbose Logging、WebSocket 均完成接入验证项两窗口 tooltip 显示/消失、窗口缩小后顶部翻转、Linux 构建显示 GNOME 提示而 macOS/Windows 不显示、npm run check无 TypeScript 错误——均已勾选。十一、设计取舍总结这套 Tooltip 体系的可复用要点从 design.md 的决策记录中可以提炼出这套体系可复用的核心取舍组件包装优于use:action插槽模式让触发元素与 tooltip 的从属关系在模板中显式可见且无障碍接线aria-describedby指向动态 id更易维护双延迟模式600ms 防止噪音、0ms 服务信息图标两种交互语义清晰零依赖定位position: fixedgetBoundingClientRect()翻转检查避开 Popper/Floating UI 这类库——对固定尺寸的小窗口足够且保持依赖面扁平内联定位而非 portal避免 DOM 传送带来的 scoped CSS 复杂度在 Pomotroid 的紧凑窗口下配合 overflow 容器审查即可可靠工作YearlyView 的热力图是唯一需要手写视口定位的特殊场景状态条件文案Mute 与 Round indicator 的 tooltip 文本随应用状态动态切换使 tooltip 始终描述当前会发生的动作而非静态功能描述——这是 tooltip 文案设计的高级实践后果前置文案Reset 这类破坏性操作的 tooltip 明确告知会清除会话进度在用户点击前完成风险沟通。如需在自有 Svelte 应用中复现这套模式可直接参照 src/lib/components/Tooltip.svelte 与 src/lib/components/TooltipInfo.svelte 两个文件配合 openspec/specs/tooltips/spec.md 的场景化契约即可逐条验证实现是否完备。赞分享【免费下载链接】pomotroid:tomato: Simple and visually-pleasing Pomodoro timer项目地址https://gitcode.com/gh_mirrors/po/pomotroid点击查看免费下载相关推荐OpenDesign Skeumorphism 设计系统包实战指南契约文件、Token 体系与组件清单OpenDesign Skeumorphism 设计系统包实战指南契约文件、Token 体系与组件清单 导读 本文以 design systems/skeuAI 应用人工智能AI 技能设计系统媒体生成A2UI WebAppFrame 组件规范v0.9深度解读沙箱化富交互组件的运行时契约与安全模型A2UI WebAppFrame 组件规范v0.9深度解读沙箱化富交互组件的运行时契约与安全模型 导读 本文基于仓库中的 WebAppFrame 组件规范人工智能AI AgentAI 应用前端UI组件3步解决Milkdown编辑器Tooltip组件挂载难题从配置到交互全攻略3步解决Milkdown编辑器Tooltip组件挂载难题从配置到交互全攻略 Milkdown作为一款插件驱动的所见即所得Markdown编辑器框架其Tool前端富文本插件系统创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进