Web 界面
基础组件
控件来自 components/ui。 AGENTS.md 说明怎么使用和扩展它们。
不要把原生的 <button>、<input>、<select> 或 <textarea> 做成基础组件已经提供的那种控件的样子。元素本来就不是那种控件时,用原生元素才对:语义行、标签页、调整大小的手柄、色块、编辑器表面,以及任何行为或几何形状按设计就不一样的东西。
样式
在渲染该元素的组件里,用 Tailwind 类写样式。
- 全局 CSS,无论在
index.css还是其他样式表里,只用于所属组件上的类表达不了的东西:生成的 Markdown 或命令式 DOM、自定义元素和 shadow root、动画、运行时主题变量,以及浏览器或 Electron 的集成。 - 类已经能覆盖的静态值,不要用
style={{ ... }},也不要写行内的<style>。运行时算出来的值可以用。 - 只跟主题有关的规则用
@variant dark和@variant light,不用.dark选择器。
组合
Composer(编写器)的横幅是参照。
ComposerBanner.tsx导出一个由若干小槽位组成的对象。槽位自己持有类名,接收className,并展开其余 props,用cn合并类名。变体和密度是Root上的 props。需要控件的槽位渲染Button。ComposerBannerStack.tsx组合这些槽位,自己只负责堆叠行为:优先级、展开和收起、关闭时的过渡。- 调用方只有几行槽位,横幅样式由槽位自己带着:
ComposerPlanFollowUpBanner.tsx。
新界面照这样做。去组合或扩展组件,不要复制它的标记和类名。某一处调用重复了另一处已经在用的处理方式,就把它做成共享组件上的变体或槽位。新的 Composer 横幅或通知,使用 ComposerBanner 的槽位。
大型组件里的处理函数
V8 让同一次渲染中创建的每个闭包共用一个作用域。被记忆化的处理函数会留住创建它的那次渲染的作用域,而这个作用域还持有这次渲染里其他被记忆化的值。同一个组件里的处理函数如果在不同时间发生变化,每一个都会留着更早的作用域,里面是更早的处理函数;于是旧的渲染,连同其中的服务器配置和对话线程(thread)数据,会在组件保持挂载的整段时间里留在内存中。React Compiler 记忆化出来的闭包也是如此。
在 ChatView 和 ChatComposer 这类又大、又长期存在的组件里,把处理函数写成普通函数,只在它们离开组件的地方用 useStableCallback 包起来:props、hook 的依赖数组,以及 effect 或 memo 的函数体。组件内部的调用继续用这个普通函数,这样在 await 之后接着做的工作,仍然作用在它出发的那次渲染上。子组件在渲染期间调用的函数,或者在 await 之后调用的函数,来自一个按输入记忆化的模块级工厂,因此它的闭包只保留这些输入。
React Compiler
编译器只有在能证明代码遵守 React 的规则时,才会记忆化一个组件或 hook。它证明不了的任何写法,例如在渲染期间读取 ref,或在 effect 里调用 setState,都会让它跳过整个函数,而且构建不会报错。Lint 把每一次跳过报成 react(...) 警告,附注是 "React Compiler skipped optimizing this component or hook"。
apps/web/src 下的每个文件都必须能编译。Lint 在这里把这些规则定为错误,并且 t3code/no-manual-memo 会拒绝 useMemo 和 useCallback,因为编译器已经让它们变得多余;上游并没有哪条规则直接禁止它们。它也会拒绝 "use memo"。当一个值在某次 hook 调用的两侧都进了普通函数调用时,编译器不会给出缓存作用域。"use memo" 辅助函数能强行得到一个,但会把顺序问题藏起来。要改的是顺序:对该值的普通使用放在 hook 之前,或者把辅助函数内联。不调用任何 hook 的 use* 辅助函数会被当成普通函数编译,什么也不缓存,所以计算派生值的辅助函数应当自己去读来源 hook,而不是把这个值当作参数接进来。t3code/no-hookless-use-function 会拒绝不调用任何 hook 的 use* 函数。Lint 会漏掉一些放弃优化的情况,尤其是带 finally 的 try,以及 try 内部的 throw,因此 reactCompilerCoverage.test.ts 会编译每一个文件,只要有跳过就失败。异步处理函数很少需要 try:一条 atom 命令(command)会落定成一个结果,所以清理可以跟在 await 后面。如上所述,useStableCallback 仍然会标记那些离开大型组件的处理函数。