knight-ui 1.1.5 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -151,7 +151,7 @@ Knight-UI 通过 CSS 自定义属性(CSS Variables)实现主题系统。在
151
151
 
152
152
  > **例外:内容色板(`--ku-content-*`)不带 fallback** —— 它由 JS 按主题现值拼进内联样式(`rgb(var(--ku-content-blue))`),写不了兜底:过期的兜底会静默画出**旧主题**(表现成"主题切换失灵"),而缺失的兜底立刻可见。因此 `tokens.css` 必须被引入,见下。
153
153
 
154
- > **亮色主题状态**:色阶、语义重绑、缺失/写反的覆盖都已就位 —— 包括 `--ku-color-glass-menu` 与 `--ku-color-mask` 两处此前会继承暗色值、导致亮色下弹层是一块暗板 / 遮罩压过头的**真 bug**;`Monaco` 的代码面则刻意**不跟随**本库主题(固定用 VS 自带的 `vs-dark`,见「代码面」一节)。语义与方向现在在两个主题下都成立。**表面分档与玻璃分档都已重推过一轮**(见下方「亮色的表面分档」与「亮色的玻璃分档」),阴影重量也在同一轮补了亮色值。**已知缺口三处**:① `Markdown` 的代码块仍注入仅暗色的 highlight.js 样式表(亮色下浅底浅字,需第二份样式表);② 玻璃在**浅色内容**上方的对比度——这是暗色侧的问题,本轮按范围只重标了亮色,见「亮色的玻璃分档」末尾的实测表;③ 亮色 `--ku-color-glass-tip` 已连着两轮下调(`.48 → .38 → .28`),压在近黑内容上只有 **`2.51:1`**,**低于 WCAG AA 正文下限**,是有意接受的倒退(同节有说明与收回办法:单独把 `tip` 抬回 `.46–.48`)。同轮还把五个 `--ku-blur-*` 的半径整体砍半(`22/30/40/48/36` → `10/14/18/22/16px`)——**半径是两个主题共用的,所以暗色的玻璃也一起变清了**,见「同一轮的第二步」。之后又单独把亮色 `tip` 再降一档并把**亮色气泡的颗粒减半**(新增 `--ku-glass-grain-tip`),见「气泡的第二轮」。
154
+ > **亮色主题状态**:色阶、语义重绑、缺失/写反的覆盖都已就位 —— 包括 `--ku-color-glass-menu` 与 `--ku-color-mask` 两处此前会继承暗色值、导致亮色下弹层是一块暗板 / 遮罩压过头的**真 bug**;`Monaco` 的代码面则刻意**不跟随**本库主题(固定用 VS 自带的 `vs-dark`,见「代码面」一节)。语义与方向现在在两个主题下都成立。**表面分档与玻璃分档都已重推过一轮**(见下方「亮色的表面分档」与「亮色的玻璃分档」),阴影重量也在同一轮补了亮色值。**已知缺口三处**:① `Markdown` 的代码块仍注入仅暗色的 highlight.js 样式表(亮色下浅底浅字,需第二份样式表);② 玻璃在**浅色内容**上方的对比度——这是暗色侧的问题,本轮按范围只重标了亮色,见「亮色的玻璃分档」末尾的实测表;③ 亮色 `--ku-color-glass-tip` 的 α 现在停在 `.40`(沿革 `.48 → .38 → .28 → .40`,见「气泡的第二轮」),压在近黑内容上约 **`3.7:1`**,**仍低于 WCAG AA 正文下限**(`4.5`)——这是一处有意接受的、尚未收回的缺口,收回办法是把 `tip` 抬到 `.46–.48`。更早一轮里还把五个 `--ku-blur-*` 的半径整体砍半(`22/30/40/48/36` → `10/14/18/22/16px`)——**半径是两个主题共用的,所以暗色的玻璃也一起变清了**,见「同一轮的第二步」。
155
155
 
156
156
  ### Token 总览
157
157
 
@@ -166,22 +166,23 @@ Knight-UI 通过 CSS 自定义属性(CSS Variables)实现主题系统。在
166
166
  | `--ku-color-bg-0` … `--ku-color-bg-3` | 背景阶(0 最深) |
167
167
  | `--ku-color-fill-0` … `--ku-color-fill-3` | 交互面填充阶(输入框底、悬停底、滚动条) |
168
168
  | `--ku-color-border` | 描边色 |
169
+ | `--ku-color-chat-surface` | **对话区**共用的一档底,由 `.ku-chat-surface` 消费(`AIChatDialogue` / `AIChatInput` / `Chat`)。**不透明**,且刻意站在灰阶阶梯之外:暗色 `#151517`(比 `bg-0` 明显浅,也几乎不带蓝 —— 阶梯第 2 档 `#1a1f2b` 蓝相很重),亮色沿用"比页面深一档"的 `rgb(var(--ku-grey-2))` = `#ecedf0`。**两个主题不同值**,因为"近黑的面"在亮色下没有意义。见下方「对话区的面」 |
169
170
  | `--ku-color-link` | 链接色(与 `primary` 同档;需要 hover/active 直接用 primary 家族) |
170
171
  | `--ku-color-fill-translucent` / `-hover` | 半透明填充(玻璃面上的条纹/悬停底;用不透的 `fill-*` 会把玻璃糊死) |
171
172
  | `--ku-color-fill-translucent-strong` | 半透明填充的第二档(0.06)。给**必须读得出来**的静态带子用——目前只有代码三件套的工具条。0.03 那一档的定位是"叠在已有底上的极浅着色",暗色下够用,亮色下只有约 2/255,工具条会整条消失。值与 `-hover` 相同但**角色不同**,故独立命名(调 hover 的深浅不该顺手把工具条挪走)。 |
172
173
  | `--ku-content-{hue}` | **色板层**的分类内容色(`blue` / `cyan` / `teal` / `green` / `amber` / `orange` / `red` / `pink` / `violet` / `grey`)。组件这样用:`rgb(var(--ku-content-blue))`、`rgba(var(--ku-content-blue), .15)`。见下方「内容色板」 |
173
174
  | `--ku-color-mask` / `--ku-blur-mask` | 遮罩底色与模糊(铺满视口那一层,见 `.ku-mask`) |
174
175
  | `--ku-color-glass-tip` / `--ku-blur-tip` | 小浮层气泡(见 `.ku-glass-tip`) |
176
+ | `--ku-color-glass-tip-popconfirm` | Popconfirm 自己的膜,**只换膜、不单开一档**:暗色换成更实的一档 `rgba(grey-3, .70)`,亮色直接 `var(--ku-color-glass-tip)`(亮色的气泡膜本来就够实)。半径 / 颗粒 / `@supports` 兜底全部继承气泡档,所以 `Popconfirm` 要**同时挂 `.ku-glass-tip` 与 `.ku-glass-tip-popconfirm`** |
175
177
  | `--ku-color-glass-menu` / `--ku-blur-menu` | 选项面板(见 `.ku-glass-menu`) |
176
- | `--ku-glass-grain` | 磨砂颗粒(SVG 噪点 data URI,由浮层 / 选项面板两档玻璃类消费) |
177
- | `--ku-glass-grain-tip` | 小浮层档**自己的**颗粒。暗色 = `var(--ku-glass-grain)`(逐字相同),**亮色减半**(`0.10 → 0.05`)—— 颗粒在亮色下不做"磨砂"这份工(白膜上它只拉暗面板 + 盖掉背景的低频泛光),只有小面积气泡有"想看到背景泛光"这个诉求。见「气泡的第二轮」 |
178
+ | `--ku-glass-grain` | 磨砂颗粒(SVG 噪点 data URI)。由**浮层 / 气泡 / 选项面板三档**玻璃类消费,两个主题**共用一份**(单色噪点,亮暗同向)—— 曾经给气泡单开过一份"亮色减半"的 token,实测推翻后已删除,见「气泡的第二轮」 |
178
179
  | `--ku-glass-menu-film` | 选项面板的第二层背景("奶膜")。暗色是一条极淡的白渐变,**亮色是 `none`** —— 见 `.ku-glass-menu` |
179
180
  | `--ku-shadow-sm` / `--ku-shadow-elevated` / `--ku-shadow-lg` | 阴影三档(浮层请用 `lg`) |
180
181
  | `--ku-radius-xs` … `--ku-radius-xl` | 圆角阶(2 / 3 / 4 / 6 / 8 / 10px) |
181
182
  | `--ku-font-size-xs` … `--ku-font-size-lg` | 字号阶(10 / 11 / 12 / 13 / 14px) |
182
183
  | `--ku-spacing-xs` … `--ku-spacing-xl` | 间距阶(2 / 4 / 8 / 12 / 16 / 20px) |
183
184
  | `--ku-font-family` | 全局字体 |
184
- | `--ku-z-mask` / `-overlay` / `-modal` / `-toast` | 层级 |
185
+ | `--ku-z-mask` / `-sider` / `-overlay` / `-modal` / `-toast` | 层级。`-sider` 给可折叠侧栏的折叠钮用 —— 它要压在被裁住的侧栏内容之上,否则折叠动画期间会被菜单盖住 |
185
186
 
186
187
  ### 亮色的表面分档
187
188
 
@@ -221,6 +222,8 @@ Knight-UI 通过 CSS 自定义属性(CSS Variables)实现主题系统。在
221
222
 
222
223
  对应的 `Tokens.*` 访问器(`Tokens.info` / `infoHover` / `infoActive` / `primaryDisabled` / `bgBorder`)一并移除。
223
224
 
225
+ **后续又删了一个:`--ku-glass-grain-tip`**(小浮层档"亮色减半"的颗粒,见「气泡的第二轮」)。它不属于上面那 12 个——它是后来为一条**已被实测推翻的假设**新增的,随假设一起作废。同样属于公开 API,覆盖样式里写过它的要改回 `--ku-glass-grain`。
226
+
224
227
  ### 内容色板(分类色,`--ku-content-*`)
225
228
 
226
229
  `Tag` 的预设色名、`Avatar` 的哈希取色、`VChart` 的默认数据序列,用的是同一组十个色相。它们此前各自硬编码了一份**互不一致**的 hex 数组(同一个"蓝"在标签上是 `#5b9cf5`、在头像上是 `#4c90f0`、在图表上又是 `#4c90f0` 配 `#3b82f6`),而且两个主题下渲染成同一批颜色。现在三处都指向 `--ku-content-<hue>`:**一个色相一个档位**,由 `scripts/gen-palette.mjs` 按"等明度"选出来,且随主题变。
@@ -249,7 +252,7 @@ Token(`--ku-color-glass` / `-strong` / `-head`、`--ku-blur-base` … `-xl`)
249
252
  | `ku-glass-head` | 头部、顶栏 | `--ku-color-glass-head` / `--ku-blur-lg` |
250
253
  | `ku-glass-overlay` | 浮层(弹窗、抽屉、筛选面板、Toast / Notification / Feedback,以及 Cascader / AutoComplete / TreeSelect / DatePicker / TimePicker / ColorPicker 的弹层) | `--ku-color-glass-strong` / `--ku-blur-xl` |
251
254
  | `ku-glass-menu` | 选项面板(`Dropdown` 与 `Select` 的菜单;其余下拉类弹层尚未换档) | `--ku-color-glass-menu` / `--ku-blur-menu` |
252
- | `ku-glass-tip` | 小浮层气泡(Popover / Tooltip / Popconfirm / AudioPlayer 与 VideoPlayer 的音量条与倍速菜单 / Chat 的表情面板;`FloatButton` 的按钮也走这一档) | `--ku-color-glass-tip` / `--ku-blur-tip` / `--ku-glass-grain-tip`(**唯一一个颗粒也单独成 token 的档**) |
255
+ | `ku-glass-tip` | 小浮层气泡(Popover / Tooltip / Popconfirm / AudioPlayer 与 VideoPlayer 的音量条与倍速菜单 / Chat 的表情面板;`FloatButton` 的按钮也走这一档) | `--ku-color-glass-tip` / `--ku-blur-tip` / `--ku-glass-grain`(`Popconfirm` 再叠一层 `ku-glass-tip-popconfirm` 换膜) |
253
256
  | `ku-glass-subtle` | 表格级(面积大,糊太重会糊掉内容) | `--ku-color-glass` / `--ku-blur-base` |
254
257
  | `ku-mask` | 遮罩(Modal / SideSheet / Image 预览 / UserGuide / Cropper 裁剪蒙版 / Spin 覆盖层) | `--ku-color-mask` / `--ku-blur-mask` |
255
258
 
@@ -271,13 +274,17 @@ Token(`--ku-color-glass` / `-strong` / `-head`、`--ku-blur-base` … `-xl`)
271
274
 
272
275
  矩形洞用**一条 SVG 路径**(外框一个子路径、洞一个子路径,`fill-rule='evenodd'` 挖掉中间,见 `rectHoleMaskImage`),`viewBox` 取遮罩自身的像素尺寸、`mask-size: 100% 100%` + `preserveAspectRatio='none'`,于是按布局尺寸一次性栅格化,全程没有二次缩放。圆形洞是单个 `radial-gradient`——它本来就只有一条图层,不存在接边。代价是要依赖 `mask` 裁掉 `backdrop-filter` 的结果(与元素自身填充同等对待),赌错的后果是整片均匀糊掉,一眼可见,不是那种难查的杂症。
273
276
 
274
- **磨砂颗粒(本库玻璃在默认底纹上唯一的磨砂来源)**:`ku-glass-overlay` 与 `ku-glass-menu` 叠的是 `--ku-glass-grain`,`ku-glass-tip` 叠的是它自己的 `--ku-glass-grain-tip`(暗色与前者逐字相同,**亮色减半**——见「气泡的第二轮」)。原因是背景若是平滑渐变,模糊它得到的还是同一片平滑渐变——**模糊本身不产生肌理**。这条对本库尤其致命:本库的**默认底纹 `--ku-bg-image` 就是一条平滑径向渐变**,于是任何一档玻璃在默认底纹上,`backdrop-filter` 的模糊几乎是个**视觉上的空操作**——透上来的只是同一片渐变稍微暗一点。用户看到的"透明"其实是渐变在漏,而不是"磨砂"。
277
+ **磨砂颗粒(本库玻璃在默认底纹上唯一的磨砂来源)**:`ku-glass-overlay`、`ku-glass-tip`、`ku-glass-menu` 三档叠的都是同一颗 `--ku-glass-grain`(单色噪点,两个主题共用一份)。原因是背景若是平滑渐变,模糊它得到的还是同一片平滑渐变——**模糊本身不产生肌理**。这条对本库尤其致命:本库的**默认底纹 `--ku-bg-image` 就是一条平滑径向渐变**,于是任何一档玻璃在默认底纹上,`backdrop-filter` 的模糊几乎是个**视觉上的空操作**——透上来的只是同一片渐变稍微暗一点。用户看到的"透明"其实是渐变在漏,而不是"磨砂"。
275
278
 
276
279
  结论是:**颗粒不是锦上添花的肌理,它是这套玻璃在默认底纹上唯一的磨砂来源**,所以强度必须给到看得见。`0.06`(上几轮的值)在暗面板上落在可见阈值以下,玻璃于是读成"换了个纯色面板";通用值是 `0.10`,刚好能看出磨砂、又还没到"电视雪花"的档位。噪声是单色的;只贴浮层和小面,大面积容器不贴(会显脏)。
277
280
 
278
- **但它跟主题是反向的**:暗色下它是承重的肌理;**亮色下它只是纯粹的成本**——白膜上一个通道都不会变亮,只有"把面板拉暗(α `.28` 时约 `13/255`)+ 铺一层抑制低频泛光的噪点"两个副作用。所以亮色的 `tip` 那一档把它减半。判定原则:**"这个面实不实"在暗色下问颗粒,在亮色下问 alpha 和阴影。**
281
+ **但它跟主题是反向的**(这里说的是它**该不该存在**,不是说它该分档):暗色下它是承重的肌理;**亮色下它只是纯粹的成本**——白膜上一个通道都不会变亮,只有"把面板拉暗(α `.40` 时约 `9/255`)+ 铺一层抑制低频泛光的噪点"两个副作用。判定原则:**"这个面实不实"在暗色下问颗粒,在亮色下问 alpha 和阴影。**
282
+
283
+ > ⚠️ **但"亮色下它只是成本"推不出"所以亮色该减半"** —— 照着这个推理给亮色气泡减半过一次,实测直接推翻,见「气泡的第二轮」。原因是**颗粒在两个主题下是同向的**(单色噪点:浅底上靠深点、深底上靠浅点,两边都看得见),减半只是让它变淡,不会让它"不做工"。
279
284
 
280
285
  > 换个底纹(照片、纹理、密集内容)时,模糊才会真正上场;在默认渐变底上别指望它。若要在默认底纹上继续加强磨砂感,调 `--ku-glass-grain` 的 `opacity`(data URI 里那个 `0.10`),不要再动模糊半径。
286
+ >
287
+ > **"颗粒太粗"要调的是 `baseFrequency` 和 `numOctaves`,不是 `opacity`,也不是把颗粒关掉。** 粗细由 `baseFrequency` 定(贴图 140 用户单位 = 140 CSS px,故基本颗粒 = `1/f` CSS px):现值 `f = 1.0` 是**不越过像素栅格的前提下能取到的最细**,再往上抬只是把细节推到像素以下,1× 下就已走样、缩放时更明显。`numOctaves` 取 `1`(只留基频):每多一阶频率翻倍、振幅减半,而**每一阶都会在放大时长出新细节**——分形噪声越放大越细是它的本性。这正是**画布场景**暴露的问题:ReactFlow 用 `transform: scale()` 缩放(不是页面缩放那套"按新分辨率重画"),贴图被整体放大,于是 1× 下渲染不出来的高阶细节全冒出来,观感就是"磨砂面的颗粒突然变得很重"。只留基频后,放大只把现有颗粒等比放大,不会凭空长新的。代价是少了泛音阶、对比度降一档——嫌淡先抬 `opacity`,**不要**把 `numOctaves` 加回去。这条治不了"放大后颗粒变大"(那是整张贴图被跟着放大),只能靠抵消缩放。
281
288
 
282
289
  **哪些面刻意不玻璃化**:`Markdown` 的**面板**是玻璃,但它块内的代码块 `pre` 保持不透明的 `bg-1`——`pre` 是嵌在玻璃面**里面**的一块内嵌面,跟着玻璃化就是玻璃套玻璃(使用前提 3),语法高亮色板也校准在实色暗底上,所以它读成一块实心内嵌面。同理不玻璃化的还有:`Lottie` / `AudioPlayer` / `VideoPlayer` 的"暂无内容"占位框(`fill-0` + 虚线边,是"这里没有东西"的示意而非面板),以及 `VideoPlayer` 的播放舞台与中央播放键(`#000` 与 rgba 黑白蒙层,校准在视频画面上而不是页面上)。
283
290
 
@@ -315,11 +322,11 @@ Token(`--ku-color-glass` / `-strong` / `-head`、`--ku-blur-base` … `-xl`)
315
322
 
316
323
  ### 亮色的玻璃分档
317
324
 
318
- 亮色五档的 alpha 现在是 **`.20 / .28 / .50 / .62 / .76`**(顺序是 `glass` / `tip` / `strong` / `head` / `menu`;`ku-glass-subtle` 与 `ku-glass` 同值)。沿革:`.30 / .36 / .42 / .54 / .78` → `.30 / .48 / .60 / .72 / .84`(按对比度下限重标)→ `.20 / .38 / .50 / .62 / .76`(按"面板、弹出的标签、提示气泡要更透"的反馈整体调透,每档约 `-.10`)→ 当前值(**只把 `tip` 再降一档**,理由见下方「气泡的第二轮」)。改动的理由从来不是"让档位表看起来均匀"——**亮色的玻璃档位靠 alpha 根本分不出来**,这一点要先说清楚,免得下次再来回试。
325
+ 亮色五档的 alpha 现在是 **`.20 / .40 / .50 / .62 / .76`**(顺序是 `glass` / `tip` / `strong` / `head` / `menu`;`ku-glass-subtle` 与 `ku-glass` 同值)。沿革:`.30 / .36 / .42 / .54 / .78` → `.30 / .48 / .60 / .72 / .84`(按对比度下限重标)→ `.20 / .38 / .50 / .62 / .76`(按"面板、弹出的标签、提示气泡要更透"的反馈整体调透,每档约 `-.10`)→ `.20 / .28 / .50 / .62 / .76`(只把 `tip` 再降一档)→ 当前值(**把 `tip` 抬回来**——那一轮降完之后气泡糊掉了背景的泛光,见下方「气泡的第二轮」)。改动的理由从来不是"让档位表看起来均匀"——**亮色的玻璃档位靠 alpha 根本分不出来**,这一点要先说清楚,免得下次再来回试。
319
326
 
320
327
  **为什么分不出来。** 亮色页面底是 `#ecedf0`(L `.946`),纯白是 `1.000`——白膜能拉开的总区间只有 **ΔL `.054`**。五档全塞进去,相邻档实测 ΔE 只有 `1.0 / 0.6 / 0.6 / 0.6` 这个量级(可见阈值是 2)。这不是取值没调好,是窗口宽度决定的:要让相邻档可辨,Δα 至少得 `.20`,五档就要跨 `0.80`,最厚一档将 ≥ `1.10` —— 而 α 超过 `.85` 的"白膜"已经是一块实心白板、不再是玻璃了。**也就是说,让五档互辨所需的 alpha 跨度落在 `[0, 1]` 之外——这个方向无解。** 暗色同样分不出来(相邻档实测 ΔE `0.8–2.8`,同量级,原因是膜和底同色系),所以这也不是亮色独有的毛病。
321
328
 
322
- **反过来说:既然分层的活儿不由 alpha 承担,那 alpha 就只是一个"观感旋钮",不是"层次旋钮"。** 动它只改变"这个面多实 / 多透",被它压掉或拉开的那点档位差反正都在可见阈值以下——**所以这一轮整体调透,结构上几乎不损失什么。** 当前值的相邻档差是 `.18 / .12 / .12 / .14`,按同一条 ΔE ∝ Δα 的线性关系折算,约 `1.0 / 0.7 / 0.7 / 0.8`——仍然全部远低于 2。**这是估算值,不是实测**;实质结论是"再怎么压也还是低于阈值"。
329
+ **反过来说:既然分层的活儿不由 alpha 承担,那 alpha 就只是一个"观感旋钮",不是"层次旋钮"。** 动它只改变"这个面多实 / 多透",被它压掉或拉开的那点档位差反正都在可见阈值以下——**所以这一轮整体调透,结构上几乎不损失什么。** 当前值的相邻档差是 `.20 / .10 / .12 / .14`,按同一条 ΔE ∝ Δα 的线性关系折算,约 `1.1 / 0.6 / 0.7 / 0.8`——仍然全部远低于 2。**这是估算值,不是实测**;实质结论是"再怎么压也还是低于阈值"。
323
330
 
324
331
  亮色下调层靠的是另外两件东西,都在位:**遮罩**(`strong` 叠在 `.20` 遮罩压过的页面之上,实测 ΔL `.109`)与**阴影**(所有无遮罩的弹层——`Dropdown` / `Select` 菜单、`Popover` / `Tooltip` 气泡、`Cascader` / `DatePicker` 等——都带 `--ku-shadow-lg` / `-sm`)。
325
332
 
@@ -342,10 +349,11 @@ Token(`--ku-color-glass` / `-strong` / `-head`、`--ku-blur-base` … `-xl`)
342
349
  | alpha | 合成后 | 对比度 | 对应档位 |
343
350
  |---|---|---|---|
344
351
  | `.20` | `#4b4b4b` | `1.89:1` ✗ | **当前的 `glass` / `subtle`**(不受此表约束,见上) |
345
- | `.28` | `#5d5d5d` | `2.51:1` ✗ | **当前的 `tip`** —— 明知故犯,两轮连着降,见下 |
352
+ | `.28` | `#5d5d5d` | `2.51:1` ✗ | 气泡**降到过**这里(`tip` 的最低点,见下) |
346
353
  | `.30` | `#626262` | `2.69:1` ✗ | 改前的 `glass` |
347
354
  | `.36` | `#6f6f6f` | `3.29:1` ✗ | 最老的 `tip` |
348
355
  | `.38` | `#747474` | `3.51:1` ✗ | 上一轮的 `tip` |
356
+ | `.40` | `#787878` | `3.7:1` ✗ | **当前的 `tip`** —— 从 `.28` 抬回来的,仍在 AA 之下,见下 |
349
357
  | `.42` | `#7d7d7d` | `3.99:1` ✗ | 上上轮的 `glass` |
350
358
  | `.46` | `#868686` | `4.51:1` ← WCAG AA 正文下限 | —(仅作参照,未取用) |
351
359
  | `.48` | `#8a8a8a` | `4.79:1` ✓ | 上一轮的 `tip` |
@@ -357,9 +365,9 @@ Token(`--ku-color-glass` / `-strong` / `-head`、`--ku-blur-base` … `-xl`)
357
365
  | `.76` | `#c9c9c9` | `9.99:1` ✓ | **当前的 `menu`** |
358
366
  | `.84` | `#dbdbdb` | `11.95:1` ✓ | 改前的 `menu` |
359
367
 
360
- **下调已经连着让掉两轮下限——这是有意为之,不是漏算。** `tip` 先降到 `.38`(`3.51:1`),又降到 `.28`(**`2.51:1`**),都低于 AA 正文的 `4.5`,而 `.48` 正是更早一轮为修好它才取的值。四档里真正踩在线上的**只有 `tip` 这一类**:`strong`(`.50`)是 `5.08:1`,仍在线上;`head`(`.62`)`7.08:1`、`menu`(`.76`)`9.99:1` 都很稳。所以受影响的是**气泡**——`Tooltip` / `Popover` 一旦压在深色代码块或图片暗部上,正文会偏吃力。要收回就是**只把 `tip` 抬回 `.46–.48`,其余四档不动**——它仍是五档里唯一真骑在线上的一个,也是唯一一个可以单独重标而不牵动别处的档位。
368
+ **这一档已经连着两轮回落到 AA 之下——这是有意为之,不是漏算。** `tip` 先降到 `.38`(`3.51:1`),又降到 `.28`(**`2.51:1`**),都低于 AA 正文的 `4.5`,而 `.48` 正是更早一轮为修好它才取的值;后来那一轮的效果不成立,`tip` 被抬回 `.40`(**`3.7:1`**,见「气泡的第二轮」),**仍在线上之下**。四档里真正踩着线的**只有 `tip` 这一类**:`strong`(`.50`)是 `5.08:1`,在线上;`head`(`.62`)`7.08:1`、`menu`(`.76`)`9.99:1` 都很稳。所以受影响的是**气泡**——`Tooltip` / `Popover` 一旦压在深色代码块或图片暗部上,正文会偏吃力。要收回就是**只把 `tip` 抬到 `.46–.48`,其余四档不动**——它仍是五档里唯一真骑在线上的一个,也是唯一一个可以单独重标而不牵动别处的档位。(`Popconfirm` 在暗色下另走一层更实的膜,见 `--ku-color-glass-tip-popconfirm`。)
361
369
 
362
- > 表里这几个数是**只按 alpha 算的**(合成值 = `30 + 225α`),没有计入颗粒层。颗粒的均值在中灰附近,压在近黑面板上会把面板**略微提亮**(α `.28` 时约 `+1.7/255`),对深底上的深字是一点点帮助,减半则收回这 `1.7` 里的一半。量级在四舍五入以内,所以上表照旧可用,但别把它当成精确到小数第二位的值。
370
+ > 表里这几个数是**只按 alpha 算的**(合成值 = `30 + 225α`),没有计入颗粒层。颗粒的均值在中灰附近,压在近黑面板上会把面板**略微提亮**(约 `+1.7/255`),对深底上的深字是一点点帮助。量级在四舍五入以内,所以上表照旧可用,但别把它当成精确到小数第二位的值。
363
371
 
364
372
  **整体调透那一轮的下调不是等步长**,末尾一档少降一点:
365
373
 
@@ -371,7 +379,7 @@ Token(`--ku-color-glass` / `-strong` / `-head`、`--ku-blur-base` … `-xl`)
371
379
  | `head` | `.72` | `.62` | `-.10` | |
372
380
  | `menu` | `.84` | `.76` | `-.08` | 最实的一档,少降一点以保住逐行可读 |
373
381
 
374
- **之后又单独降了 `tip` 一档(`.38 → .28`)**,其余四档一个没动——理由见「气泡的第二轮」。所以「当前值」那一列里只有 `tip` 与上表不同。
382
+ **之后 `tip` 又单独走了两轮(`.38 → .28`,再抬回 `.40`)**,其余四档一个没动——理由与结果见「气泡的第二轮」。所以「当前值」那一列里只有 `tip` 与上表不同。
375
383
 
376
384
  次序仍是设计里那条 `glass < tip < strong < head < menu`(菜单是要逐行读的最密一档所以最实,顶栏 `head` 次之,气泡 `tip` 仍比弹层 `strong` 再透一档)。
377
385
 
@@ -394,7 +402,7 @@ Token(`--ku-color-glass` / `-strong` / `-head`、`--ku-blur-base` … `-xl`)
394
402
 
395
403
  **方向翻了一处要说清楚:这条跷跷板规则此前是"每降一档底色,模糊必须跟着涨一档"。** 那条规则服务的目标是"看得见背后有东西,但**认不出是什么**"——认不出,靠的就是大半径。现在的目标是"**背景的图和颜色要透得过来**",大半径本身就与它冲突,于是两个旋钮改成同向。**这是目标变了,不是规则被违反了。**
396
404
 
397
- **颗粒层(`--ku-glass-grain`,`0.10`)这一轮一个没动。** 10% 的单色噪点挡不住背后的图,它不是"图和颜色透不过来"的成因。但要说清楚:**它才是"磨砂感"这个词在本库里的实际载体** —— 模糊只负责"看不清",哑光的肌理全靠这颗颗粒(原因见「磨砂颗粒」一节:默认底纹是平滑渐变,模糊它对它是空操作)。所以**如果仍嫌"磨砂感太重",下一个旋钮是颗粒,而不是继续砍半径** —— 半径砍到 0 也去不掉那层哑光,而砍过头就不成其为玻璃了。**这条建议下一轮就被用上了,但落点在亮色的小浮层档(`0.05`,只动这一档),见「气泡的第二轮」。**
405
+ **颗粒层(`--ku-glass-grain`,`0.10`)这一轮一个没动。** 10% 的单色噪点挡不住背后的图,它不是"图和颜色透不过来"的成因。但要说清楚:**它才是"磨砂感"这个词在本库里的实际载体** —— 模糊只负责"看不清",哑光的肌理全靠这颗颗粒(原因见「磨砂颗粒」一节:默认底纹是平滑渐变,模糊它对它是空操作)。所以**如果仍嫌"磨砂感太重",下一个旋钮是颗粒,而不是继续砍半径** —— 半径砍到 0 也去不掉那层哑光,而砍过头就不成其为玻璃了。**这条建议下一轮就被用上了(把颗粒调细),但结论是"调细"比"调淡"更接近问题,见「气泡的第二轮」。**
398
406
 
399
407
  **注意这一档的半径次序变了**:`tip`(`16px`)现在比 `strong`(`18px`)还小。小面积的面本来就更容易看清背后,再叠大半径只是白糊一场 —— 它的 `saturate` `195%` 仍与 `menu` 并列五档最高。
400
408
 
@@ -407,38 +415,49 @@ Token(`--ku-color-glass` / `-strong` / `-head`、`--ku-blur-base` … `-xl`)
407
415
  > | 站在谁的角度 | 最坏背景 | tip | strong | head |
408
416
  > |---|---|---|---|---|
409
417
  > | 暗色(浅色文字压深膜) | 浅色内容 `#e8e8e8` | `1.73:1` | `2.24:1` | `1.98:1` |
410
- > | 亮色(深色文字压白膜) | 深色内容 `#1e1e1e` | `4.79:1` → **`2.51:1`** ✗ | `6.71:1` → **`5.08:1`** | `9.09:1` → **`7.08:1`** |
418
+ > | 亮色(深色文字压白膜) | 深色内容 `#1e1e1e` | `4.79:1` → **`3.7:1`** ✗ | `6.71:1` → **`5.08:1`** | `9.09:1` → **`7.08:1`** |
411
419
  >
412
420
  > 暗色五档**在自己的页面底上**是 `16.3–17.5:1`(比亮色还高),所以单看自家底纹会以为"暗色没这个问题"。两个主题唯一的区别是**哪种背景更常见**:亮色下深色内容近在眼前(`Monaco` 按既定决策固定暗色,代码块就是一块近黑),暗色下浅色内容要遇到浅色图片才会出现。所以按范围只重标了亮色,暗色那三个 alpha 原样保留——**上面暗色那一行是一处已知的、尚未处理的缺口**,记在这里,免得下次又被"暗色看起来没问题"骗过去。
413
421
  >
414
- > **注意本轮的方向:亮色那一行是往下走的**(调透 → 膜变薄 → 深底上垫得不够),`tip` 因此从 `✓` 翻成 `✗`。这是本轮唯一一处质量倒退,记录在案;暗色那行不受影响。反过来,**以后再抬亮色 alpha 会让亮色这行变宽松**,但暗色那行永远不动它。
422
+ > **注意这一行的方向:亮色是往下走的**(调透 → 膜变薄 → 深底上垫得不够),`tip` 因此从 `✓` 翻成 `✗`;它后来最低走到 `.28`(`2.51:1`),又抬回 `.40`(上表的 **`3.7:1`**,见「气泡的第二轮」)——**回来了大半,但没有回到 `✓`**。暗色那行始终不受影响。反过来,**以后再抬亮色 alpha 会让亮色这行变宽松**,但暗色那行永远不动它。
415
423
 
416
- ### 气泡的第二轮:亮色 `tip` 再透一档 + 颗粒减半
424
+ ### 气泡的第二轮:两次分档尝试都被实测推翻
417
425
 
418
- 反馈是"**`Popover` 在亮色下磨砂透明看着更通透一点,可以看到背景的泛光**"。**只降 `tip` 一档是合理的** —— 它整个亮色梯子里**面积小到可以只看背景**的那一档(一块 300px 的气泡里没有几行字),其余四档身后都压着要读的内容,跟着降会连带伤可读性。
426
+ 反馈是"**`Popover` 在亮色下磨砂透明看着更通透一点,可以看到背景的泛光**"。**只动 `tip` 一档是合理的** —— 它是整个亮色梯子里**面积小到可以只看背景**的那一档(一块 300px 的气泡里没有几行字),其余四档身后都压着要读的内容,跟着降会连带伤可读性。
419
427
 
420
- **但这一轮的结论是:光降 alpha 解决不了这条反馈,真正的旋钮是颗粒层。** 这一条值得单独记住,因为它是「alpha 在亮色近白底上只是观感旋钮」那条规律的第三个实例。
428
+ **两次尝试都按同一条推理走,两次都被推翻。** 这条记录留着的价值就在这里:推理每一步都成立,结论仍然错了。
421
429
 
422
- 把 `tip` 从 `.38` 降到 `.28`,面板自身在页面底纹上只挪了约 **`3/255`**(`243.2 → 240.0`)—— 又一次"改了但看不出"。真正的问题在于信噪比:
430
+ **第一次:降 alpha + 亮色颗粒减半(新增 `--ku-glass-grain-tip`,已删除)。**
431
+
432
+ 先按「alpha 在亮色近白底上只是观感旋钮」那条规律估了一下:把 `tip` 从 `.38` 降到 `.28`,面板自身在页面底纹上只挪约 **`3/255`**(`243.2 → 240.0`)—— 又是"改了但看不出"。于是判定真正的问题在信噪比:
423
433
 
424
434
  | | 泛光振幅(面板两端的差) | 颗粒噪点 | 信噪比 |
425
435
  |---|---|---|---|
426
436
  | 改前(α `.38`,颗粒 `0.10`) | 约 `17/255` | **±8/255** | 约 `2` |
427
437
  | 改后(α `.28`,颗粒 `0.05`) | 约 `21/255` | **±4/255** | 约 `5` |
428
438
 
429
- **背景的泛光是一条 `20/255` 量级的低频渐变,埋在 ±`8` 的噪点里读不出来。** 颗粒一减半,噪点让开、振幅上浮,渐变才成形。同期颗粒层自身的**平均拉暗**也从约 `13/255` 降到约 `6/255`,所以面板整体更亮、更"飘"。
430
-
431
- **颗粒层在这里跟主题是反向的,这点要写清楚:** 它在暗色下承重(深面板靠它才不是一块纯色板),在亮色下**只是纯粹的成本** —— 白膜上一个通道都不会变亮,只有"拉暗 + 铺噪点"两个副作用。所以这一次**只动亮色、只动 `tip` 那一档**,新增一个 token 承载:
439
+ 理由是:背景的泛光是一条 `20/255` 量级的低频渐变,埋在 ±`8` 的噪点里读不出来;颗粒一减半,噪点让开、振幅上浮,渐变才成形。同期颗粒层自身的**平均拉暗**也从约 `13/255` 降到约 `6/255`,面板整体更亮、更"飘"。所以那一轮**只动亮色、只动 `tip`**,为此新增了一个 token:
432
440
 
433
441
  | token | `:root`(暗色) | `:root[data-ku-theme="light"]` | 消费者 |
434
442
  |---|---|---|---|
435
- | `--ku-glass-grain-tip` | `var(--ku-glass-grain)`(与通用值逐字相同) | 同一份 SVG,`opacity` `0.10 → 0.05` | `ku-glass-tip` |
443
+ | `--ku-glass-grain-tip` | `var(--ku-glass-grain)`(逐字相同) | 同一份 SVG,`opacity` `0.10 → 0.05` | `ku-glass-tip` |
436
444
 
437
- **暗色一行不受影响**(`--ku-glass-grain-tip` 在 `:root` 里就是 `var(--ku-glass-grain)`),其余四档的颗粒也原样。亮色那份 data URI 与 `--ku-glass-grain` **只有最后一个 `opacity` 值不同**,改其中一份时记得两份一起改。
445
+ **实测不成立,整条回退。** 根因是那条"颗粒在亮色下只是成本"的推理**推不出"所以亮色该减半"**:颗粒是**单色**噪点,浅底上靠深点、深底上靠浅点,**两个主题下它都是同向做工的**,减半只是让它变淡,并不会让它"不做这份工"。上表那套信噪比换算因此没有兑现,气泡反而更糊。`--ku-glass-grain-tip` 随之删除(属公开 API 变更),三档玻璃重新共用一颗 `--ku-glass-grain`。
446
+
447
+ **第二次:把颗粒调细(`baseFrequency` / `numOctaves`)去治"颗粒感太重"。**
448
+
449
+ 这一条起于点上的观察:"在常规界面里颗粒感会显得很重"。方向是对的——**颗粒粗细由 `baseFrequency` 定,不是由 `opacity` 定**(见「磨砂颗粒」一节的展开)。但真把噪声往细里调之后**出现了网格纹**(细密噪点与像素栅格拍出摩尔纹),比原来的颗粒更显眼,同样整条回退。结论是:**`f = 1.0`(一个周期 = 一个 CSS 像素)已经是不越过像素栅格的前提下能取到的最细**,再往上抬只是把细节推到像素以下,得到的不是"更细"而是"走样"。
450
+
451
+ **收敛后的值**(随后基于 `test/glass-lab.html` 重新标定,一次定下三个数):
452
+
453
+ | token | 暗色 | 亮色 |
454
+ |---|---|---|
455
+ | `--ku-color-glass-tip` | `rgba(grey-3, .34)`(由 `.22` 抬高) | `rgba(white, .40)`(由 `.28` 抬高) |
456
+ | `--ku-color-glass-tip-popconfirm` | `rgba(grey-3, .70)` | `var(--ku-color-glass-tip)`(不另设) |
438
457
 
439
- **收到的效果是"信噪比 ×2.5",不是"面板明显变透明"。** 如果后面还嫌不够,下一个旋钮仍是颗粒(`0.05 → 0`,即亮色气泡完全不铺颗粒)—— 那时亮色气泡就变成一层纯白膜,是"最透"的形态。继续动 alpha 是最没效率的一步。
458
+ **`Popconfirm` 因此多出一层膜而不是第六档**:它在暗色下要压住弹层里的字,膜得比气泡更实;亮色下气泡自身的 `.40` 已经够实,那一层直接沿用(写 `var(...)` 而不是再抄一个值,是为了让"亮色不另设"这件事在 token 表里自己说出来)。它换的只是 `background-color`,半径 / 颗粒 / `@supports` 兜底全继承气泡档 —— 所以它必须**和 `.ku-glass-tip` 一起挂**。
440
459
 
441
- **代价**:`tip` 到 `.28` 后压在近黑内容上只有 **`2.51:1`**,连续第二轮跌破 AA(见上一节的实测表)。气泡压在深色代码块或图片暗部上时正文会偏吃力。要收回就抬回 `.46–.48`。
460
+ **留下的账**:抬回 `.40` 之后 `tip` 压在近黑内容上约 **`3.7:1`**,仍低于 AA 正文的 `4.5`(见上一节的实测表)。这条反馈的诉求("看到背景的泛光")与可读性本来就对拉,本轮选了观感;要收回就是抬到 `.46–.48`。
442
461
 
443
462
  ### 类规则与内联样式(谁能赢)
444
463
 
@@ -453,6 +472,7 @@ Token(`--ku-color-glass` / `-strong` / `-head`、`--ku-blur-base` … `-xl`)
453
472
  | `ku-tag` / `ku-scrollbar` / `ku-float-button` | 过渡、滚动条伪元素、悬浮/展开态、玻璃档 | 内联未涉及这些属性;`ku-float-button` 还独占了三项:**底色**(内联已不再写 `background-color`)、**UA 重置**(`appearance: none` / `border: none` —— 少了下标就会露出 UA 的原生 `outset` 斜角环)、以及**阴影的两个状态**(`:hover` 的 `-lg` 与 `box-shadow` 过渡都写不了内联,组件里那句内联 `boxShadow` 因此是删掉的) |
454
473
  | `ku-layout-sider-trigger` | 折叠钮的**几何**(绝对定位骑在侧栏右边缘、24px 圆钮)、**UA 重置**(`appearance: none` 等)与不透明底色 | 这些属性内联没写——钮要压出侧栏边缘,位置只能由类给;UA 重置同理,写不进内联的语义里 |
455
474
  | `ku-userguide-card` | UserGuide 引导卡片的**不透明**底(`--ku-color-bg-3`) | 卡片不挂玻璃,底色改由类提供(内联原本就没写 `background-color`) |
475
+ | `ku-chat-surface` | 对话区(`AIChatDialogue` / `AIChatInput` / `Chat`)的**不透明**底(`--ku-color-chat-surface`) | 同上:三个组件都不挂玻璃,底色由类提供。内联只落一个**有条件**的 `backgroundColor`——调用方没传时那一行根本不写,否则内联会把类盖死(见「对话区的面」) |
456
476
  | `ku-focus-ring` | 键盘焦点环(见下) | `Button` / `FloatButton` 原先内联 `outline: none` 关掉了 UA 环又不给替代,现已移除该内联——不删内联,挂类也出不来环 |
457
477
  | `ku-modal-close` | 关闭按钮的悬停底 | 内联没写 `background-color`(`color` 被内联占了,所以悬停只改底、不改色) |
458
478
  | `ku-modal-btn-primary` | 主按钮的悬停 / 按下 | 底色内联恒有值(还要随 `confirmLoading` 变),只有 `filter` 没被内联占住 |
@@ -479,6 +499,18 @@ Token(`--ku-color-glass` / `-strong` / `-head`、`--ku-blur-base` … `-xl`)
479
499
 
480
500
  `--ku-radius-base`(4px)给**控件**:输入框、按钮、小控件,以及像 `Toast` 这种又扁又小的临时提示条。`--ku-radius-md`(6px)给**容器**:卡片、表格、下拉面板、导航项、分页项、Tab、标签、上传卡、视频外框等"一整块面板 / 一整行列表"级别的面,`Notification` / `Popconfirm` / `Tooltip` / `Dropdown` 这类浮层气泡也归这一档。`xs`/`sm` 留给更小的内嵌元素,`lg`/`xl` 给弹窗、抽屉与引导卡片。
481
501
 
502
+ ### 对话区的面(`.ku-chat-surface`)
503
+
504
+ `AIChatDialogue`(会话列表)/ `AIChatInput`(输入框)/ `Chat`(通用对话列表)**共用一条类规则、一个 token**(`--ku-color-chat-surface`,暗 `#151517` / 亮 `#ecedf0`)。三个组件都是同一类面 —— 一块要读字的对话区 —— 底色必须一致,所以是一条规则三个消费者,而不是各写一条。
505
+
506
+ **跑回玻璃档是走不通的,这是这块面必须单列的原因。** 膜只能往**自己主题的浅端**加厚:暗色是灰膜压暗底(`grey-3` 比 `grey-2` 亮,α 越大越亮),亮色是白膜压亮底,**两个主题都往"更浮起来"走**。而这里要的是**沉下去**的一面 —— 加深只能在阶梯之外取一个值。取 `#151517` 而不是直接退到 `bg-0`:它比暗色 `bg-0`(`#0c0e12`)明显浅,也**几乎不带蓝** —— 阶梯第 2 档是 `#1a1f2b`,蓝相很重。这是实测挑出来的值,所以给它自己的名字,不去动阶梯本身。
507
+
508
+ **它是唯一一处"不透明的用途面",与玻璃家族并列、而不是其中一员。** 顺带省掉一次 `backdrop-filter`:不透明的面上那层模糊本来就糊不到东西。视觉上另有一个连带效果值得记:面板从玻璃换成不透明面之后,`AIChatDialogue` 内的 Markdown 更要按平了 —— 它自己的那层玻璃膜现在只会白付一次合成,并在每条助手消息周围画出一个**更亮**的矩形(暗色膜是加亮的),所以 `FLAT_MARKDOWN_STYLE` 里的 `backdropFilter: none` 要留着。
509
+
510
+ **三处都可以用 `backgroundColor` prop 覆盖默认值,但要点在"什么时候才落内联"**:写的是 `...(backgroundColor ? { backgroundColor } : {})`,**不能无条件写内联 `backgroundColor`** —— 内联优先级高于类,无条件写就把 token 那一档盖死了(调 `--ku-color-chat-surface` 会看起来"改了没生效")。三个组件要**一起改**:`AIChatDialogue` 与 `AIChatInput` 是同一个对话区的上下两半,只改一边会在中间露出一条分界线。
511
+
512
+ `Chat` 面板**内部**另有一条两级半透的小阶梯:输入区底栏走 `--ku-color-fill-translucent`(`.03`)、里面的输入框走 `-hover`(`.06`)。**借的是这两个值的浓度差,不是 hover 语义** —— 输入框的底色不该跟着鼠标变。半透中性膜叠在**不透明**的面板上正好:暗色是 3% / 6% 的白膜(带子比面板略亮),亮色是黑膜(带子比面板略沉),两个主题各自都能读出一档台阶,而底本身不参与 —— 面板换成什么色都不影响这两档。
513
+
482
514
  ## 通用 Props(UIProps)
483
515
 
484
516
  所有组件继承以下通用属性:
@@ -559,7 +591,7 @@ import { FloatButton } from "knight-ui";
559
591
 
560
592
  **边界靠阴影,不靠描边。** 一圈 `1px var(--ku-color-border)` 会把磨砂面**框成一个实心圆**——边缘越硬,"背后有东西"越读不出来,与磨砂的意图正好相反;柔影才是"浮起来"的语义,与磨砂同向。早前那一版用的是描边 + `boxSizing: border-box`,现已去掉,`box-sizing` 也随之不再需要(没有边框和内外边距时它是个空操作)。
561
593
 
562
- > **亮色下这枚按钮偏淡,是结构性的,两个原因叠加。** 一是白膜叠在近白页面上本来就挪不动几个数(见「亮色的玻璃分档」里那条警告);二是它的演示区是**刻意留空的**,身后没有东西可糊。**「气泡的第二轮」把亮色 `tip` 档的颗粒减半,正好也缓解了这一点**(颗粒层在亮色下的平均拉暗从约 `13/255` 降到约 `6/255`)。**若还嫌淡,下一步是"给它单独一档"而不是继续动共用的 `tip`** —— `tip` 是 Tooltip / Popconfirm / 音视频条 / 表情面板共用的,为了一枚按钮继续往下降会连带把它们的可读性一起拉下去。
594
+ > **亮色下这枚按钮偏淡,是结构性的,两个原因叠加。** 一是白膜叠在近白页面上本来就挪不动几个数(见「亮色的玻璃分档」里那条警告);二是它的演示区是**刻意留空的**,身后没有东西可糊。**`tip` 档后来把亮色 α 抬回 `.40`(见「气泡的第二轮」),也顺带把这一枚托起来了一点。** **若还嫌淡,下一步是"给它单独一档"而不是继续动共用的 `tip`** —— `tip` 是 Tooltip / Popconfirm / 音视频条 / 表情面板共用的,为了一枚按钮继续往下降会连带把它们的可读性一起拉下去。
563
595
 
564
596
  ### ⚠️ 去掉描边必须同时做 UA 重置,否则会露出一圈"半亮半暗的环"
565
597
 
@@ -719,14 +751,35 @@ import { Layout, Text } from "knight-ui";
719
751
 
720
752
  子组件:`Layout.Header`、`Layout.Sider`、`Layout.Content`、`Layout.Footer`
721
753
 
722
- **Layout.Sider**:`width`、`collapsedWidth`、`defaultCollapsed`、`trigger`
754
+ **Layout.Sider**:`width`(默认 `200`)、`collapsedWidth`(默认 `60`)、`defaultCollapsed`、`collapsed`(受控)、`onCollapseChange`、`trigger`、`triggerPosition`、`transitionDuration`。
755
+
756
+ ```tsx
757
+ const [collapsed, setCollapsed] = useState(false);
758
+ <Layout.Sider width={200} collapsedWidth={56} collapsed={collapsed} onCollapseChange={setCollapsed}
759
+ trigger triggerPosition="top" transitionDuration={200}>
760
+ <Navigation items={items} mode="vertical" isCollapsed={collapsed} />
761
+ </Layout.Sider>
762
+ ```
723
763
 
724
- **折叠钮是一枚骑在侧栏右边缘、垂直居中的 24px 圆钮**(`trigger` 为真时出现在 `.ku-layout-sider-trigger` 上)。它压在 `--ku-color-fill-1` 的**不透明**底上、hover 走 `fill-2`,配 `1px var(--ku-color-border)` 与 `--ku-shadow-sm`;图标是 `IconChevronLeft` / `IconChevronRight`(展开态显示左箭头,折叠态显示右箭头)。三点都值得记:
764
+ **折叠钮是一枚骑在侧栏右边缘的 24px 圆钮**(`trigger` 为真时出现在 `.ku-layout-sider-trigger` 上)。它压在 `--ku-color-fill-1` 的**不透明**底上、hover 走 `fill-2`,配 `1px var(--ku-color-border)` 与 `--ku-shadow-sm`;图标是 `IconChevronLeft` / `IconChevronRight`(展开态显示左箭头,折叠态显示右箭头)。几点都值得记:
725
765
 
726
- - **底色必须不透明。** 圆钮**同时骑在两种底色上**——左半边是玻璃侧栏,右半边是内容区。半透底会让同一颗钮的左右两半各取各的背景,显出两种颜色。这与 `UserGuide` 卡片是同一个病根("浮层身后有两种底色"),只是面积小到不容易被指名。上一版是一条铺满侧栏宽度的底横条,当时的注释论证"横条必须半透否则会在玻璃上切出死色横缝"——那个论证只对横条成立(它完全位于玻璃内部,上下要连续),对骑边缘的圆钮不适用。
766
+ - **底色必须不透明。** 圆钮**同时骑在两种底色上**——左半边是玻璃侧栏,右半边是内容区。半透底会让同一颗钮的左右两半各取各的背景,显出两种颜色。这与 `UserGuide` 卡片是同一个病根("浮层身后有两种底色"),只是面积小到不容易被指名。上一版是一条铺满侧栏宽度的底横条,当时的注释论证"横条必须半透否则会在玻璃上切出死色横缝"——那个论证只对横条成立(它完全位于内部,上下要连续),对骑边缘的圆钮不适用。
767
+ - **纵向落点归组件,横向与尺寸归类。** `triggerPosition` 取 `"top" | "middle" | "bottom"`(默认 `"middle"`);顶 / 底各内缩 `8px`(钮是定高 24px 的定尺控件,再往下压就压到第一行了),中档是 `top: 50%` + `translateY(-50%)`(只写 `top: 50%` 会往下偏半格)。这三个值由组件的 `triggerVerticalStyle()` 输出**内联**,`.ku-layout-sider-trigger` 只剩横向(`right: -12px`)、尺寸、圆角与配色——**同一个位置不写成两处规则**(类里写死再让内联去盖,是这类 bug 的常见来源)。
727
768
  - **裁剪挪到了内层。** 钮要压出侧栏边缘(`right: -12px`),所以 `<aside>` 是 `position: relative` + `overflow: visible`,而 `overflow: hidden` 下移到内层那一列上——折叠过渡期间仍然夹得住内容。
769
+ - **侧栏自己带层级**(`zIndex: "var(--ku-z-sider)"`)。钮探出去的那一半压在内容区上,而内容区在 DOM 里排在后面;不给侧栏层级的话,内容区里一个带 `position: relative` 的卡片就会盖住它。**给侧栏而不是只给钮**——钮的层级只在自己那个定位祖先的层叠上下文里有效,祖先不参与排序的话它再高也没用。
728
770
  - **它是真 `<button>`**,带 `aria-label` / `aria-expanded` 与 `ku-focus-ring`(走 `:focus-visible`,键盘 Tab 才出现)。上一版是个裸 `<div onClick>`,键盘够不到。
729
771
 
772
+ **收起时"内容换挡"要晚一个过渡时长,展开时立刻换。** 这是折叠手感的关键一笔,也是 `LayoutSiderContext` 存在的原因:
773
+
774
+ - **收起**:宽度立刻开始收(几何马上走),但**内容等它走完再换成图标**。否则点下去的第一帧整排标签就消失,侧栏看起来是"先被清空、再滑进去"。
775
+ - **展开**:**立刻**放回标签。宽度一有就开始长,标签越早出现越自然;反过来等宽度长完,那一段就是一个"空侧栏在变宽"的空白动画。
776
+
777
+ 实现上,`Layout.Sider` 对外递出的 `LayoutSiderContext`(`{ collapsed }`)是**延迟版**(收起时晚 `transitionDuration`,展开时立刻),而不是它自己那个马上的 `collapsed`。消费方(`Navigation` 之类)读这个值来决定"该不该换挡",于是"标签消失"和"图标出现"落在同一帧上;中间那段过渡里它们仍是普通的一行,只是被自己那一行的盒宽一点点裁窄——**所以消费方要负责让自己的内容可裁**。不在 `Sider` 里时 context 为 `null`,消费方按"没有折叠这回事"处理。
778
+
779
+ `transitionDuration` 默认 `200`,且**必须是唯一取值口**:宽度那条 `transition` 与上面这个"换挡延迟"定时器共用它(组件里只留 `transitionMs()` 一处读 props)。两处各写一个数、改一处忘另一处就是错拍——内容先换完(标签没了、宽度还在动),或者后换完(宽度停住一拍,标签才消失)。
780
+
781
+ > 挂载时就已经是收起的(`collapsed` / `defaultCollapsed` 为真),**不会再走一遍延迟换挡** —— 否则内容会先按展开态画一帧再跳成图标。内部那两个状态在构造时同源,之后才分叉。
782
+
730
783
  ### Grid 栅格
731
784
 
732
785
  ```tsx
@@ -1348,6 +1401,24 @@ const [collapsed, setCollapsed] = useState(false);
1348
1401
  | `onSelect` | `(key: string) => void` | — | 选中回调 |
1349
1402
  | `onOpenChange` | `(openKeys: string[]) => void` | — | 展开变化回调 |
1350
1403
 
1404
+ **折叠态是三级取值**:显式 `isCollapsed` > 所在 `Layout.Sider` 的折叠态(走 `LayoutSiderContext`)> 自己的 `defaultIsCollapsed`。所以放在可折叠侧栏里时**不用把 `isCollapsed` 接一遍**,菜单自己跟着侧栏走;不在 `Sider` 里时 context 为 `null`,退到第三条。
1405
+
1406
+ **`onCollapseChange` 的时机取决于折叠态从哪来。** 它只在折叠态**真的翻转**那一帧发(挂载时不发,与 `onSelect` / `onOpenChange` 一致),而且**不管是谁改的都发**——外部传进来的 `isCollapsed`、侧栏的折叠钮、还是自己的默认值,外部项目要的是"菜单现在是不是只剩图标了"这一个事实,不是"谁按的按钮"。要注意的是:**在 `Sider` 里它会比按钮晚一个过渡时长**,因为 context 递过来的就是那个延迟值(见 `Layout.Sider` 的"内容换挡")。要"点下去那一刻"就用 **`Sider` 自己的 `onCollapseChange`**,两个时机各有一个出口,这里不重复发。
1407
+
1408
+ **折叠时会顺手把展开的子菜单收起来。** 折叠态下子项本来就画不出来(`iconOnly` 会把内联子项整段拿掉),但 `openKeys` 还留着——不再收的话,侧栏一展开子菜单会**自己弹回来**。用户按的是"折叠",期望的是收起来,而不是"暂时看不见"。只在"展开 → 折叠"这一个方向收;展开时不还原(还原等于把状态藏起来又偷偷放回去)。
1409
+
1410
+ **只剩图标时的三条几何约定**(这一组调的是"换挡那一帧不跳",不是好看):
1411
+
1412
+ - **内边距两态同值,折叠也不改。** 图标因此停在同一个 x 上(`4px` 行边距 + `16px` 内边距 = `x = 20`),换挡那一帧没有横向位移。先前折叠态改成 `padding: 8px 0` + `justifyContent: center` 去求"绝对居中",代价是图标在换挡那一下横跳 `2px`(展开态左内边距把图标放在 `x = 16`,而 60px 栏的正中是 `x = 18`)——每收一次就动一下,比常年偏 `2px` 显眼得多。**两者都要就把 `Sider` 的 `collapsedWidth` 设成 `56`**,那时 `16px` 内边距算出来正好落在栏的中线上(上面的示例就是这么写的)。
1413
+ - **标签始终留在 DOM 里,折叠时只关 `visibility`,不拿掉。** 拿掉它会连带拿掉这一行的**行盒高度**:只剩图标时行高由图标(16px)定,有标签时由标签的行盒(13px 字 × `normal` 行高,约 17px)定,每行矮 1px 上下,一列菜单累起来就是换挡那一下**整排往上缩**——看起来像图标自己跳,其实是它下面的行全往上挪了一格。
1414
+ - **标签那个 span 必须带 `overflow: hidden`。** 一为截断长标签;二为收起动画里侧栏在变窄,标签要**在自己的盒子边缘**被裁掉,而不是溢出去盖住右边的箭头、再被侧栏边缘生切一刀;三为它顺带把该 span 的 `min-width: auto` 归零(`nowrap` 的文字撑着一个不可压缩的宽度,不归零的话收起动画期间整行会拒绝变窄、一路溢出到侧栏边缘)。
1415
+
1416
+ **折叠后的每一行是什么,全靠右侧那个 Tooltip**(`position="right"`)。挂 `Tooltip` 而不是原生 `title`:原生气泡的延迟 / 样式 / 位置都不受控(指定不了"右侧"),也和库里其余气泡不同档。展开态把 `trigger` 换成 `"custom"`——`Tooltip` 没有 `disabled`,而 `custom` + 不给 `visible` 就是**永不弹出、也不挂任何监听**,比按条件换整棵子树省事,折叠那一刻 DOM 也不必重建。气泡挂在 `fixed` 的 Portal 上,所以它照样能压出侧栏那圈 `overflow: hidden`。
1417
+
1418
+ **竖排时菜单要铺满侧栏**(`minHeight: 100%` + `boxSizing: border-box`,仅在 `Sider` 内生效)。菜单通常比侧栏矮,不铺满时那条 `borderRight` 只画到菜单底部——面板里悬着**一条半截竖线**,而不像面板自己的边缘。用 `minHeight` 而不是 `height`:菜单比侧栏高时照样撑开去滚动。`border-box` 不能省,否则上下那 8px 内边距会加在 `100%` **之外**,内容不多不少正好顶出一条本不该有的纵向滚动条。
1419
+
1420
+ > **在 `Sider` 里菜单不要自己定宽**(`width` / `maxWidth` 一个都别写)。宽度是 `Sider` 在动画的,菜单再写一个数(展开 220 / 折叠 64)会跟它打架:展开态 `220 > Sider` 默认的 `200` 会被裁掉一截,折叠态 `64 > collapsedWidth`(默认 `60`)会溢出 `4px`、图标因此不居中。不写宽度就是铺满它的内容区,侧栏动画时菜单跟着走。
1421
+
1351
1422
  ### Tabs 标签页
1352
1423
 
1353
1424
  ```tsx
@@ -2563,12 +2634,19 @@ const messages = [
2563
2634
  | `rows` | `number` | `1` | 输入框初始行数(最小高度) |
2564
2635
  | `maxRows` | `number` | `5` | 输入框最多撑到几行,超出后框内滚动 |
2565
2636
  | `showEmoji` | `boolean` | `true` | 输入框左下角的表情按钮与表情面板(仅 `showInput` 时有效) |
2637
+ | `backgroundColor` | `string` | — | 面板底色。缺省走 `.ku-chat-surface`(暗 `#151517` / 亮 `#ecedf0`),与 `AIChatDialogue` / `AIChatInput` 同一档 |
2566
2638
 
2567
- **输入框**:多行 `textarea`,在 `rows` ~ `maxRows` 行之间随内容自动长高,越过 `maxRows` 后框内竖直滚动(`overflow-y: auto`)。`Enter` 发送、`Shift+Enter` 换行、`Esc` 关掉表情面板。发送按钮在输入框**右下角**,只有文字、没有图标——「发送」两个字已经把动作说全了,再加一个箭头/纸飞机是同一句话说两遍;没有内容时压成浅底并禁用。表情按钮与发送键**等高 26px**,图标 16px——一左一右站在同一条基线上,差几像素就会显歪;表情按钮悬停**只提亮图标本身**(`color` 切到 `text-0`;面板开着时回落成主色),不铺底块——与播放器那排按钮同一条规矩,底块会把右下角这块留白切成两个方块。表情面板走 Portal 渲染到 `body`——面板自身带 `overflow: hidden`,就地渲染会被裁掉;这也让它能挂 `.ku-glass-tip` 而不与面板的玻璃底嵌套。点中一个表情后面板**自动收起**(微信/QQ 的同一手感),要连插再点一次按钮即可。
2639
+ > **面**:面板挂 `.ku-chat-surface`(**不透明**底,与 `AIChatDialogue` / `AIChatInput` 共用一条规则一个 token)。原先挂的是 `.ku-glass`(容器档),改成不透明面的理由是这一档要**沉下去**,而膜只能往自己主题的浅端加厚——见「对话区的面」。
2640
+ >
2641
+ > 传了 `backgroundColor` 它就落成**内联**、盖过类;不传时那一行根本不写(写的是 `...(backgroundColor ? { backgroundColor } : {})`)。**不能无条件写死一个内联默认值**——内联优先级高于类,那样调 `--ku-color-chat-surface` 会看起来"改了没生效"。
2642
+ >
2643
+ > 面板**内部**另有一条两级半透的小阶梯:输入区底栏 `--ku-color-fill-translucent`(`.03`)、里面的输入框 `-hover`(`.06`),借的是两个值的浓度差而不是 hover 语义(输入框的底色不该跟着鼠标变)。展开见「对话区的面」。
2568
2644
 
2569
- 输入框的 `textarea` 必须显式写 `backgroundColor: transparent`:它的 UA 默认底是白的(`field`),不撤掉就是一块白板压在玻璃面上。
2645
+ **输入框**:多行 `textarea`,在 `rows` ~ `maxRows` 行之间随内容自动长高,越过 `maxRows` 后框内竖直滚动(`overflow-y: auto`)。`Enter` 发送、`Shift+Enter` 换行、`Esc` 关掉表情面板。发送按钮在输入框**右下角**,只有文字、没有图标——「发送」两个字已经把动作说全了,再加一个箭头/纸飞机是同一句话说两遍;没有内容时压成浅底并禁用。表情按钮与发送键**等高 26px**,图标 16px——一左一右站在同一条基线上,差几像素就会显歪;表情按钮悬停**只提亮图标本身**(`color` 切到 `text-0`;面板开着时回落成主色),不铺底块——与播放器那排按钮同一条规矩,底块会把右下角这块留白切成两个方块。表情面板走 Portal 渲染到 `body`——面板自身带 `overflow: hidden`,就地渲染会被裁掉(它挂 `.ku-glass-tip`,得能糊到页面而不是只糊到面板)。点中一个表情后面板**自动收起**(微信/QQ 的同一手感),要连插再点一次按钮即可。
2570
2646
 
2571
- 越过 `maxRows` 之后的滚动条跟库里其余滚动区一致:挂 `.ku-scrollbar` 类(4px 细条、滑块 `--ku-color-fill-2`)+ 内联 `scrollbarWidth: "thin"` / `scrollbarColor`(Firefox 侧)。UA 默认那条灰白粗槽压在玻璃输入框上非常扎眼。
2647
+ 输入框的 `textarea` 必须显式写 `backgroundColor: transparent`:它的 UA 默认底是白的(`field`),不撤掉就是一块白板压在面板上。
2648
+
2649
+ 越过 `maxRows` 之后的滚动条跟库里其余滚动区一致:挂 `.ku-scrollbar` 类(4px 细条、滑块 `--ku-color-fill-2`)+ 内联 `scrollbarWidth: "thin"` / `scrollbarColor`(Firefox 侧)。UA 默认那条灰白粗槽压在对话面板上非常扎眼。
2572
2650
 
2573
2651
  > **输入框的字比气泡大一号**:textarea 走 `--ku-font-size-lg`(14px),气泡仍是 `--ku-font-size-base`(12px)。气泡是"读"的、输入框是"写"的,写字的地方字大一点更顺手;只动输入框,气泡不变。
2574
2652
  >
@@ -2752,8 +2830,11 @@ import { AIChatInput } from "knight-ui";
2752
2830
  | `maxRows` | `number` | `4` | 最大行数,超出后框内竖直滚动 |
2753
2831
  | `attachments` | `Attachment[]` | — | 附件列表 |
2754
2832
  | `onSend` | `(value, attachments) => void` | — | 发送回调 |
2833
+ | `backgroundColor` | `string` | — | 输入框底色。缺省走 `.ku-chat-surface`(暗 `#151517` / 亮 `#ecedf0`)。**要与 `AIChatDialogue` 一起改** |
2755
2834
 
2756
- > **面**:面板挂 `.ku-glass`(容器档)。它是一个**面板**——自带内边距,里面装着 textarea + 附件条 + 发送键——不是 `Input` 那种单字段控件;单字段控件继续走不透明的 `fill-0`(见 `input/Input.tsx`),两者的分野在这里。内部 textarea 本来就是透明的,附件标签走不透明的 `fill-1`(在玻璃面上是**更亮**的一档,读起来是一枚筹码)。
2835
+ > **面**:面板挂 `.ku-chat-surface`(**不透明**底,暗 `#151517` / 亮 `#ecedf0`),与 `AIChatDialogue` / `Chat` **共用这一条类规则**。它是**同一个对话区的下半块**——两块底色必须一致,只改一边会在中间露出一条分界线,所以它跟着对话面板走,而不是像别的输入类控件那样走 `fill-0`(见 `input/Input.tsx`)。它仍是一个**面板**——自带内边距,里面装着 textarea + 附件条 + 发送键——不是 `Input` 那种单字段控件,那个分野没变。内部 textarea 本来就是透明的,附件标签走不透明的 `fill-1`(在这块面上是**更亮**的一档,读起来是一枚筹码)。
2836
+ >
2837
+ > **`backgroundColor` 只在传了的时候落内联**(`...(backgroundColor ? { backgroundColor } : {})`)。**不能无条件写内联 `backgroundColor`**——内联优先级高于类,写死就把 `.ku-chat-surface` 那一档盖死了。要与 `AIChatDialogue` 一起改,理由同上一段。展开见「对话区的面」。
2757
2838
  >
2758
2839
  > 内边距 `10px 12px`,**横向略大于纵向**:纵向那 10px 会跟 textarea 下面 20px 的垫高叠在一起(发送键就落在右下角那个垫高层里),再大就把键从右下角推离边框;横向是文字起笔的位置,12px 才不显得贴在框上。附件条的横向缩进交给外框提供——它原来自己带 12px,于是比 textarea 多缩进一层、两个左边缘对不齐,现在只留底边距把附件和正文隔开。
2759
2840
  >
@@ -2795,8 +2876,9 @@ const messages = [
2795
2876
  | `onCopy` | `(msg) => void` | — | 复制回调(复制按钮仅在传入时出现) |
2796
2877
  | `copyTip` | `React.ReactNode \| false` | `"已复制"` | 复制后的 Toast 文案;传 `false` 关闭提示 |
2797
2878
  | `onRetry` | `(msg, index) => void` | — | 重试回调 |
2879
+ | `backgroundColor` | `string` | — | 面板底色。缺省走 `.ku-chat-surface`(暗 `#151517` / 亮 `#ecedf0`)。**要与 `AIChatInput` 一起改** —— 两块是同一对话区的上下两半 |
2798
2880
 
2799
- > **AIMessage 思考内容支持(assistant 专用)**:`thinkingContent?: React.ReactNode` 思考/推理内容(Markdown 字符串或节点),展示为消息上方可折叠的灰色思考块;`thinkingTokens?: number` 真实 token 数(缺省按 ~3.5 字符/token 估算);`thinkingDurationMs?: number` 思考耗时(ms),完整消息可传,流式思考期间由组件自动计时无需传入。
2881
+ > **AIMessage 思考内容支持(assistant 专用)**:`thinkingContent?: React.ReactNode` 思考/推理内容(Markdown 字符串或节点),展示为消息上方可折叠的灰色思考块;`thinkingTokens?: number` 真实 token 数(缺省按 ~3.5 字符/token 估算,展示时**千位进制、保留一位小数**:`1200 → "1.2 k"`,不足 `1000` 原样显示,再拼上 `" token"`,即渲染成 `12.3 k token`。**不做单位换算**(不出现 `万` / `M`)——这一列是同一量级的横向对比,统一乘数比"换个单位"更好读);`thinkingDurationMs?: number` 思考耗时(ms),完整消息可传,流式思考期间由组件自动计时无需传入。
2800
2882
  > 流式阶段约定:仅思考时消息为 `{ role:"assistant", thinkingContent: 增量, status:"streaming" }`(正文留空、与 content 互斥增长);结束后替换为带 `content`(与可选 `thinkingDurationMs`)的完整消息。
2801
2883
  >
2802
2884
  > ```tsx
@@ -2810,7 +2892,11 @@ const messages = [
2810
2892
  > **复制反馈**:点"复制"会在调用 `onCopy` 之后弹 `Toast.success("已复制")`(文案用 `copyTip` 改,传 `copyTip={false}` 关掉,反馈交回调用方)。
2811
2893
  > **组件自己不碰剪贴板**——`content` 是 `React.ReactNode`,可能是节点而不是文本,"该拷什么"只有调用方说得清,所以写入始终发生在 `onCopy` 里(见上方示例)。也正因如此,这个提示代表的是"复制动作已发出",**不是组件核实过的结果**:`onCopy` 里若是异步写入且被浏览器拒绝,组件无从得知。要拿到真结果,就得把组件改成自己写剪贴板,并把 `content` 收窄成 `string`。
2812
2894
 
2813
- > **面**:面板挂 `.ku-glass`(容器档)。**面板内的 Markdown 必须按平**——`Markdown` 的根节点自己就是一块玻璃面(`.ku-glass`),两层玻璃相叠既违反本库"不嵌套玻璃"的规矩(内层的 backdrop 只能糊到外层已经画出来的那层半透底,糊出来还是同一片),又让每一条助手消息白付一次整块面积的 `backdrop-filter`。组件用**内联** `backdropFilter: none` 就地关掉它(内联优先级高于类),与本来就有的 `backgroundColor: transparent` 一起把 Markdown 还原成"只有内容"。正文与思考正文两处都走同一个 `FLAT_MARKDOWN_STYLE`。
2895
+ > **面**:面板挂 `.ku-chat-surface`(**不透明**底,暗 `#151517` / 亮 `#ecedf0`),与 `AIChatInput` / `Chat` 共用一条规则一个 token。原先挂的是 `.ku-glass`(容器档);改成不透明面是因为这一档要**沉下去**,而膜只能往自己主题的浅端加厚——见「对话区的面」。
2896
+ >
2897
+ > `backgroundColor` 传了才落**内联**、盖过类;不传时那一行根本不写。**不能无条件写一个内联默认值**——内联优先级高于类,那样调 `--ku-color-chat-surface` 会看起来"改了没生效"。要改就与 `AIChatInput` **一起改**:两块是同一对话区的上下两半,只改一边会在中间露出一条分界线。
2898
+ >
2899
+ > **面板内的 Markdown 仍然必须按平**——`Markdown` 的根节点自己就是一块玻璃面(`.ku-glass`)。面板换成不透明面之后,那一层玻璃的理由从"不嵌套玻璃"变成了两个更直接的:它在不透明的面上**白付一次整块面积的 `backdrop-filter`**,而且会在每条助手消息周围画出一个**更亮**的矩形(膜在自己的主题里总是往浅端走,暗色的灰膜是加亮的)。组件用**内联** `backdropFilter: none` 就地关掉它(内联优先级高于类),与本来就有的 `backgroundColor: transparent` 一起把 Markdown 还原成"只有内容"。正文与思考正文两处都走同一个 `FLAT_MARKDOWN_STYLE`。
2814
2900
 
2815
2901
  ### Sidebar AI 侧栏
2816
2902
 
@@ -98,6 +98,9 @@ export interface AIChatDialogueProps extends UIProps {
98
98
  /** 思考正文最大高度(px),超出后内部滚动 @default 320 */
99
99
  thinkingBodyMaxHeight?: number;
100
100
  emptyContent?: React.ReactNode;
101
+ /** 面板底色。缺省走 .ku-chat-surface(暗 #151517 / 亮 #ecedf0)。要与 AIChatInput 一起改 —— 两块是同一
102
+ 对话区的上下两半, 只改一边会在中间露出一条分界线。 */
103
+ backgroundColor?: string;
101
104
  onCopy?: (msg: AIMessage) => void;
102
105
  /** 点击复制后弹出的 Toast 文案;传 false 关闭提示,反馈由调用方自理 @default "已复制" */
103
106
  copyTip?: React.ReactNode | false;
@@ -30,6 +30,9 @@ export interface AIChatInputProps extends UIProps {
30
30
  onChange?: (value: string) => void;
31
31
  onSend?: (value: string, attachments: Attachment[]) => void;
32
32
  placeholder?: string;
33
+ /** 输入框底色。缺省走 .ku-chat-surface(暗 #151517 / 亮 #ecedf0)。要与 AIChatDialogue 一起改 —— 两块是
34
+ 同一对话区的上下两半, 只改一边会在中间露出一条分界线。 */
35
+ backgroundColor?: string;
33
36
  disabled?: boolean;
34
37
  loading?: boolean;
35
38
  attachments?: Attachment[];
@@ -1,22 +1,74 @@
1
1
  import React from "react";
2
2
  import { AbstractUI, type UIProps, type UIState } from "../../base";
3
+ /**
4
+ * 把 Sider 的折叠态递给它里面的子组件。
5
+ *
6
+ * 为什么需要它: 折叠钮改的是 Sider 自己的 state, 而**子组件看不见** —— 最典型的是放进侧栏的 Navigation:
7
+ * 侧栏收到 60px 了, 菜单还在画 220px 的标签。原先唯一的办法是调用方自己复制一份折叠态传给 Navigation 的
8
+ * isCollapsed, 而 Sider 连 onCollapseChange 都没有, 复制都无从谈起。现在 Sider 把折叠态放到 context 上,
9
+ * Navigation 自己读 —— 放进去就跟着收成图标, 不需要手接。 (同 antd: SiderContext → Menu 的 inlineCollapsed。)
10
+ *
11
+ * 值为 null 表示"不在 Sider 里", 消费方据此区分"没折叠"与"没有 Sider": 前者是 false, 后者要退到自己的默认值。
12
+ *
13
+ * **这里给的是"内容该换挡了"的折叠态, 不是"宽度开始动了"**: 收起时它比真实折叠态晚一个过渡时长
14
+ * (展开时不延迟)。消费方拿它决定自己的内容形态即可, 不必也不能再等一次。
15
+ */
16
+ export interface LayoutSiderContextValue {
17
+ collapsed: boolean;
18
+ }
19
+ export declare const LayoutSiderContext: React.Context<LayoutSiderContextValue>;
3
20
  declare class LayoutHeader extends AbstractUI<UIProps, UIState> {
4
21
  static displayName: string;
5
22
  protected OnRenderContent(): React.ReactElement;
6
23
  }
7
24
  declare class LayoutSider extends AbstractUI<LayoutSiderProps, LayoutSiderState> {
8
25
  static displayName: string;
26
+ /**
27
+ * 上一次更新时的折叠态。**在构造函数里就落地**, 不是等第一次更新 —— 否则"挂载之后第一次变化就是折叠"
28
+ * 会被当成"记基线"而整条丢掉: 该延迟的没延迟, 子组件的子菜单也不会收。
29
+ */
30
+ private _lastCollapsed;
31
+ /** 收起时那个"等过渡走完"的定时器; 期间又展开就地取消。 */
32
+ private _latchTimer;
9
33
  constructor(props: LayoutSiderProps);
34
+ componentDidUpdate(prevProps: Readonly<LayoutSiderProps>): void;
35
+ componentWillUnmount(): void;
36
+ /** 过渡时长的唯一取值口: 渲染那条 transition 与"内容换挡"的定时器都走这里, 保证同源。 */
37
+ private transitionMs;
38
+ /**
39
+ * 折叠钮的纵向落点。三档只决定它骑在**哪一段内容**上 —— 钮是探出侧栏边缘的, 哪一档都压在内容之上。
40
+ * 横向(right: -12px)与尺寸/配色仍归 .ku-layout-sider-trigger, 这里只给纵向的那一个属性, 免得
41
+ * 同一个位置有两处规则(类里写死再让内联去盖)。
42
+ */
43
+ private triggerVerticalStyle;
10
44
  protected OnRenderContent(): React.ReactElement;
45
+ /** 受控优先: 传了 collapsed 就完全听 props(内部 state 不再参与), 否则才用它。 */
46
+ private isCollapsed;
47
+ private toggleCollapsed;
11
48
  }
49
+ /** 折叠钮的纵向落点: 顶 / 中 / 底。 */
50
+ type SiderTriggerPosition = "top" | "middle" | "bottom";
12
51
  export interface LayoutSiderProps extends UIProps {
13
52
  width?: number;
14
53
  collapsedWidth?: number;
54
+ /** 非受控默认值。传了 collapsed 时它只作为初始值。 */
15
55
  defaultCollapsed?: boolean;
56
+ /** 受控折叠态 —— 传了它, 折叠钮就只发 onCollapseChange, 不再自己改 state。 */
57
+ collapsed?: boolean;
58
+ onCollapseChange?: (collapsed: boolean) => void;
16
59
  trigger?: boolean;
60
+ /** 折叠钮的纵向落点, 默认 "middle"(骑在侧栏中线)。顶/底离边缘 8px。 */
61
+ triggerPosition?: SiderTriggerPosition;
62
+ /**
63
+ * 宽度过渡时长(ms), 默认 200。**同时决定内容换挡的时机**: 收起时子组件(如 Navigation)要等宽度走完
64
+ * 才换成图标形态, 那个等待就是这个值(见 componentDidUpdate)。传 0 = 不要动画, 内容随之立刻换挡。
65
+ */
66
+ transitionDuration?: number;
17
67
  }
18
68
  interface LayoutSiderState extends UIState {
19
69
  collapsed: boolean;
70
+ /** 给子组件看的折叠态 —— 收起时比 collapsed 慢一个过渡时长(见 componentDidUpdate)。 */
71
+ contentCollapsed: boolean;
20
72
  }
21
73
  declare class LayoutContent extends AbstractUI<UIProps, UIState> {
22
74
  static displayName: string;
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { AbstractUI, type UIProps, type UIState } from "../../base";
3
+ import { type LayoutSiderContextValue } from "../basic/Layout";
3
4
  export interface NavItem {
4
5
  key: string;
5
6
  label: string;
@@ -12,7 +13,26 @@ type TriggerMode = "hover" | "click";
12
13
  * 导航菜单。
13
14
  */
14
15
  export declare class Navigation extends AbstractUI<NavigationProps, NavigationState> {
16
+ /** 放进 Layout.Sider 时自动跟着折叠 —— 折叠钮只改 Sider 自己的 state, 子组件靠这个 context 才看得见。 */
17
+ static contextType: React.Context<LayoutSiderContextValue>;
18
+ context: LayoutSiderContextValue | null;
15
19
  constructor(props: NavigationProps);
20
+ /** 上一次更新时的折叠态。null 只在 componentDidMount 之前成立(那时不可能有更新)。 */
21
+ private _lastCollapsed;
22
+ componentDidMount(): void;
23
+ /** 折叠态三级: 显式 isCollapsed > 所在 Sider 的折叠态 > 自己的 defaultIsCollapsed。 */
24
+ private isCollapsed;
25
+ /**
26
+ * 折叠态**翻转那一帧**要做的两件事: 通知外部, 以及把展开的子菜单收起来。
27
+ *
28
+ * 判断"那一刻"要知道上一次的折叠态, 而折叠态可能来自 context —— context 不在 prevProps 里, 所以自己记一笔
29
+ * (基线在 componentDidMount 落地)。注意"折叠态"在 Sider 里是**延迟版**的: 侧栏宽度先走完, context 才翻,
30
+ * 所以这两件事都落在"图标出现"那一帧上, 而不是点下去就凭空发生。
31
+ *
32
+ * 为什么收起子菜单: 折叠态下子项本来就画不出来(iconOnly 会把内联子项整段拿掉), 但 openKeys 还留着 ——
33
+ * 再展开侧栏时子菜单会**自己弹回来**。用户按了折叠, 期望的是"收起来", 而不是"暂时看不见"。
34
+ */
35
+ private syncCollapseState;
16
36
  componentDidUpdate(prevProps: Readonly<NavigationProps>): void;
17
37
  protected OnRenderContent(): React.ReactElement;
18
38
  private renderItem;
@@ -24,19 +44,39 @@ export declare class Navigation extends AbstractUI<NavigationProps, NavigationSt
24
44
  private select;
25
45
  }
26
46
  export interface NavigationProps extends UIProps {
47
+ /** 菜单数据。**key 在一份菜单里必须唯一**(选中/展开/回调都用它)。折叠后每行只剩图标, 所以每一项都该配
48
+ icon —— 没有图标的那一行在折叠态就是**一行空白**(描边和 hover 气泡还在, 但看起来像坏了)。
49
+ 子菜单写在 NavItem.children 里: 竖排逐级内联(每级多缩进 16px), 横排的浮层只画一层。 */
27
50
  items?: NavItem[];
51
+ /** 竖排(默认): 子菜单**内联**在下方, 点父项展开; 横排: 子菜单是浮层, 由 trigger 决定怎么开。
52
+ 折叠只对竖排有意义 —— 横排没有"收成图标"这回事, 传了 isCollapsed 也不会把标签收掉。 */
28
53
  mode?: "horizontal" | "vertical";
54
+ /** 折叠态(收成只显示图标)。不传时读所在 Layout.Sider 的折叠态, 再退到 defaultIsCollapsed。 */
29
55
  isCollapsed?: boolean;
56
+ /** 非受控初始值。放进 Sider 时以 Sider 的折叠态为准, 它只是独立使用时的起点。 */
30
57
  defaultIsCollapsed?: boolean;
58
+ /**
59
+ * 折叠态变化回调 —— 不管这一下是谁改的(传进来的 isCollapsed / 所在 Sider 的折叠钮 / 自己的默认值)。
60
+ * 要"用户点下按钮那一刻"就用 Layout.Sider 的 onCollapseChange; 这个是"菜单内容真的换成图标形态了"
61
+ * 那一刻(在 Sider 里比按钮晚一个过渡时长)。挂载时不发。
62
+ */
31
63
  onCollapseChange?: (isCollapsed: boolean) => void;
32
- /** 水平模式子菜单触发方式,默认 "hover" */
64
+ /** 横排子菜单的触发方式, 默认 "hover"(鼠标移入就开)。竖排不看它 —— 竖排一律点击展开。 */
33
65
  trigger?: TriggerMode;
66
+ /** 受控: 当前展开的子菜单 key。传了它菜单就不自己改内部 state, 只发 onOpenChange, 由调用方回写。
67
+ **要传新数组**(内部按引用比较, 原地 push 不会同步)。折叠那一刻会发一次 [] —— 受控方要照着自己置空。 */
34
68
  openKeys?: string[];
69
+ /** 非受控初始展开项。只在挂载时读一次(受控的 openKeys 优先)。 */
35
70
  defaultOpenKeys?: string[];
71
+ /** 受控: 当前选中项。单选时点谁换谁; multiple 时叠加/取消。传新数组的理由同 openKeys。 */
36
72
  selectedKeys?: string[];
73
+ /** 非受控初始选中项。只在挂载时读一次(受控的 selectedKeys 优先)。 */
37
74
  defaultSelectedKeys?: string[];
75
+ /** 多选: 再点已选中项就取消, 选中项可以有好几个。默认单选 —— 点谁谁亮, 与已有选中项无关。 */
38
76
  multiple?: boolean;
77
+ /** 选中回调。受控/非受控都会发(受控方靠它拿新值回写), 点已选中项也会发一次。 */
39
78
  onSelect?: (key: string) => void;
79
+ /** 展开集合变化回调: 点父项开/关, 以及折叠那一刻被清空(收到 [])。叶子项永远不发。 */
40
80
  onOpenChange?: (openKeys: string[]) => void;
41
81
  }
42
82
  interface NavigationState extends UIState {