Files
TrueGrowth/specs/002-unified-toolbar/research.md

8.4 KiB

Research: 统一左侧工具栏容器

Feature: 001-unified-toolbar | Date: 2025-12-01

Overview

本文档记录统一工具栏实现的技术研究成果,包括架构决策、最佳实践和备选方案评估。

Research Items

1. 响应式工具栏高度检测

Decision: 使用 ResizeObserver API 监听工具栏容器高度变化

Rationale:

  • ResizeObserver 是现代浏览器原生支持的 API,性能优于基于事件监听的方案
  • 支持精确检测元素尺寸变化,避免布局抖动
  • 可在 useEffect 中清理,符合 React 最佳实践
  • 浏览器兼容性良好 (Chrome 64+, Firefox 69+, Safari 13.1+)

Alternatives Considered:

  • window.addEventListener('resize') - 仅检测窗口变化,无法精确监听容器高度
  • IntersectionObserver - 设计用于可见性检测,不适合尺寸监控
  • 基于 CSS media queries - 无法动态检测容器高度,仅能检测视口尺寸

Implementation Pattern:

useEffect(() => {
  const observer = new ResizeObserver((entries) => {
    const height = entries[0].contentRect.height;
    // 根据高度切换图标模式
  });
  observer.observe(containerRef.current);
  return () => observer.disconnect();
}, []);

2. 工具栏分区视觉分隔实现

Decision: 使用 CSS border-top 实现 1px 水平分割线

Rationale:

  • 简单直接,无需额外 DOM 元素
  • 使用设计系统变量 var(--color-border) 保持一致性
  • 易于响应主题变化(浅色/深色模式)
  • 性能优于使用单独的 divider 组件

Alternatives Considered:

  • 单独的 <div> 分割线元素 - 增加 DOM 复杂度,影响可访问性
  • 固定间距(margin/padding) - 无明显视觉分隔,不符合需求
  • box-shadow 模拟分割线 - 渲染开销较大,不适合固定UI

Implementation Pattern:

.unified-toolbar__section {
  &:not(:first-child) {
    border-top: 1px solid var(--color-border);
    padding-top: 8px;
  }
}

3. 桌面端/移动端条件渲染策略

Decision: 使用现有 appState.isMobile 状态控制渲染逻辑

Rationale:

  • 复用现有 MobileDetect 检测逻辑,避免重复实现
  • 符合项目现有架构模式
  • 检测在应用初始化时完成,运行时无性能开销
  • 避免使用 CSS media queries 导致的 DOM 冗余

Alternatives Considered:

  • CSS display: none + media queries - 渲染两套DOM浪费资源
  • useMediaQuery hook - 增加运行时检测开销,不如初始化检测
  • 用户代理字符串解析 - 已由 MobileDetect 库处理

Implementation Pattern:

{!appState.isMobile ? (
  <UnifiedToolbar />
) : (
  <>
    <AppToolbar />
    <CreationToolbar />
    <ZoomToolbar />
    <ThemeToolbar />
  </>
)}

4. Icon-only 模式实现

Decision: 通过 className 切换 + CSS 隐藏文本标签

Rationale:

  • 保持语义化 HTML,屏幕阅读器仍可访问标签
  • CSS 控制显示/隐藏,性能优于条件渲染
  • 支持平滑过渡动画
  • 图标元素保持可点击,无需调整交互逻辑

Alternatives Considered:

  • 条件渲染不同组件 - 导致状态重置,影响用户体验
  • 动态调整 fontSize - 无法完全隐藏文本,布局计算复杂
  • SVG icon 替换 - 需要维护两套图标资源

Implementation Pattern:

.tool-button__label {
  display: inline-block;
  margin-left: 8px;

  .unified-toolbar--icon-only & {
    display: none;
  }
}

5. 工具栏组件解耦策略

Decision: 保持现有工具栏组件独立,通过 props 控制是否应用定位样式

Rationale:

  • 最小化代码修改,降低回归风险
  • 支持独立测试工具栏组件
  • 符合单一职责原则(容器负责布局,组件负责功能)
  • 便于未来扩展和维护

Alternatives Considered:

  • 合并所有工具栏到单一组件 - 违反单文件500行约束,难以维护
  • 完全重写工具栏组件 - 重写风险高,测试成本大
  • 使用 wrapper HOC - 增加抽象层次,不符合 React 现代模式

Implementation Pattern:

interface ToolbarProps {
  embedded?: boolean; // 是否嵌入统一容器
}

export const AppToolbar: React.FC<ToolbarProps> = ({ embedded = false }) => {
  return (
    <Island className={classNames('app-toolbar', {
      'app-toolbar--embedded': embedded
    })}>
      {/* 工具栏内容 */}
    </Island>
  );
};

6. 性能优化策略

Decision: 使用 React.memo + useCallback 优化重渲染

Rationale:

  • 工具栏是固定UI,无需频繁更新
  • memo 可避免父组件重渲染导致的不必要渲染
  • useCallback 稳定回调引用,配合 memo 使用效果最佳
  • 符合项目性能优化原则(宪章V)

Alternatives Considered:

  • useMemo 缓存 JSX - 对于简单组件收益有限,代码复杂度增加
  • 不做优化 - 工具栏在每次画布更新时重渲染,浪费资源
  • PureComponent - 使用函数式组件,memo 是更现代的选择

Implementation Pattern:

export const UnifiedToolbar = React.memo(() => {
  const handleResize = useCallback(() => {
    // 响应式逻辑
  }, []);

  // 组件实现
});

7. 样式组织与BEM命名

Decision: 创建 .unified-toolbar BEM 块,子元素使用 __ 连接符

Rationale:

  • 符合项目 BEM 命名约定 (宪章 CSS/SCSS 标准)
  • 清晰的命名层次,易于理解和维护
  • 避免样式冲突,提升可读性
  • 与现有样式系统一致

Alternatives Considered:

  • CSS Modules - 项目未采用,引入新模式成本高
  • styled-components - 项目使用SCSS,保持技术栈一致性
  • Tailwind CSS - 违反项目设计系统原则

Implementation Pattern:

.unified-toolbar {
  // 容器样式
  position: absolute;
  left: 36px;
  top: 36px;

  &__section {
    // 分区样式
  }

  &__divider {
    // 分割线样式
  }

  &--icon-only {
    // 图标模式修饰符
  }
}

8. 可访问性(a11y)最佳实践

Decision: 保持现有 aria-label 和 title 属性,图标模式不移除

Rationale:

  • 屏幕阅读器依赖 aria-label 理解按钮功能
  • title 属性提供悬停提示,图标模式下更重要
  • 符合 WCAG 2.1 AA 标准
  • 无需额外开发,复用现有实现

Alternatives Considered:

  • 图标模式移除文本标签后添加新 aria-label - 重复且不必要
  • 使用 aria-hidden 隐藏文本 - 破坏可访问性
  • 仅依赖视觉图标 - 违反可访问性标准

Implementation Pattern:

<ToolButton
  icon={HandIcon}
  title={t('toolbar.hand')}        // 保留
  aria-label={t('toolbar.hand')}    // 保留
  // ...其他props
/>

Technology Stack Summary

Category Technology Version Purpose
Language TypeScript 5.x 类型安全,严格模式
Framework React 18+ UI组件框架
UI Library TDesign React latest 设计系统组件
Board Framework @plait/core current 白板核心框架
Testing Jest + RTL current 组件单元测试
E2E Testing Playwright current 端到端测试
Styling SCSS + BEM N/A 样式组织方法论
Build Tool Nx + Vite current Monorepo构建工具

Risk Assessment

Risk Probability Impact Mitigation
移动端布局意外受影响 Low High 条件渲染隔离,E2E测试验证
响应式切换导致布局抖动 Medium Medium 使用ResizeObserver精确检测,CSS transition平滑过渡
现有快捷键失效 Low High 无修改快捷键逻辑,仅调整UI布局
性能回归(工具栏渲染慢) Low Medium React.memo优化,性能基准测试
文件超过500行限制 Low Medium 组件保持独立,拆分为多个文件

Dependencies & Compatibility

Browser Compatibility:

  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+
  • Mobile Safari/Chrome

Critical Dependencies:

  • ResizeObserver API (native browser API)
  • CSS Flexbox (widely supported)
  • React 18 concurrent features (已在项目中使用)

No New External Dependencies Required


Next Steps

Phase 0 研究完成,所有技术决策已明确。继续进行:

  1. Phase 1: 生成 data-model.md (本feature为UI重构,数据模型简化)
  2. Phase 1: 生成 quickstart.md (开发者快速开始指南)
  3. Phase 1: 更新 agent context (技术栈信息)
  4. → Phase 2: 执行 /speckit.tasks 生成任务分解