docs(agents): describe the corner and elevation prior art generically

The two styling notes name the surveyed product; the mechanism facts
(superellipse token, guard, full-round opt-out, stroke-in-shadow
elevation, 0.5px hairline) stand alone, so the notes now state them
without the product reference. Pairing records re-recorded.
This commit is contained in:
yx.zhang 2026-09-01 20:01:23 +08:00
parent 3ce5604a71
commit 8a97e817b5
6 changed files with 24 additions and 24 deletions

View file

@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.md
2026-09-01-web-elevation-stroke-shadows.md: d3622271f6656c8f7aef1d0f8681fe3cb2861854
2026-09-01-web-elevation-stroke-shadows.zh.md: 63f2213a0f8e50fb95d5f1b45507fba8c02d9619
2026-09-01-web-elevation-stroke-shadows.md: 2bc3105203b63c87aaf39e3d68780650b163dcca
2026-09-01-web-elevation-stroke-shadows.zh.md: bda3ee27af03d20937253fa23968afd0cd7d39c7

View file

@ -6,7 +6,7 @@ English | [中文](2026-09-01-web-elevation-stroke-shadows.zh.md)
## Problem
Elevated web-client surfaces — menus, popovers, modals, panels, floating buttons, the composer — each paired a real `border: 1px solid <neutral token>` with a `--dsw-shadow-lv2`/`lv3` shadow. The border consumes layout (1px per side, and it is the UA-default replacement on `<button>` elements), the light theme drew most floats with no stroke at all (`--dsw-alias-border-inverted` is transparent in light) while `lv3` faked one with a blurred 1px ring, and the composer wore a broad soft `lv2` patch that read as a smudge rather than a lifted surface. The unpacked ChatGPT desktop (Codex) UI instead draws elevation as one `box-shadow` list: a 0.5px hairline stroke plus two faint soft layers (`--elevation-stroke`/`--elevation-sidebar`/`--elevation-prominent`), with `border: 0` on the surface.
Elevated web-client surfaces — menus, popovers, modals, panels, floating buttons, the composer — each paired a real `border: 1px solid <neutral token>` with a `--dsw-shadow-lv2`/`lv3` shadow. The border consumes layout (1px per side, and it is the UA-default replacement on `<button>` elements), the light theme drew most floats with no stroke at all (`--dsw-alias-border-inverted` is transparent in light) while `lv3` faked one with a blurred 1px ring, and the composer wore a broad soft `lv2` patch that read as a smudge rather than a lifted surface. Current desktop chat UIs instead draw elevation as one `box-shadow` list: a 0.5px hairline stroke plus two faint soft layers, with `border: 0` on the surface.
## Decision
@ -16,7 +16,7 @@ Elevated web-client surfaces — menus, popovers, modals, panels, floating butto
- `--dsw-elevation-stroke: 0 0 0 0.5px var(--dsw-elevation-stroke-color)` — the stroke alone, used standalone by inline cards that want only an outline (the plugin-inventory card).
- `--dsw-elevation-panel` / `--dsw-elevation-prominent` — the stroke plus two faint soft layers (3px directional + 16/20px glow at 2–5% black), panel for small floating widgets and cards, prominent for floats, and soft — larger blur at lower alpha — for the composer.
Converted surfaces set `border: 0` and one elevation shadow: every `--dsw-shadow-lv3` float (Menu, Modal, popup selects, model select, usage/context popovers, feedback actions, schedule/job popovers, subagent lineage, settings panel, cordis panel, experimental team panel) takes prominent, and the lv2 surfaces (scroll-to-bottom button, turn-preview card, attachment-rail arrows, question composer, trajectory tooltip) take panel. The composer card takes the soft tier with the l2 stroke rebind, and its workspace-trigger state sets the stroke color `transparent` instead of the former `border-color: transparent`. Dark theme needs no shadow overrides: the soft layers are near-invisible there and the stroke carries the separation, as in Codex.
Converted surfaces set `border: 0` and one elevation shadow: every `--dsw-shadow-lv3` float (Menu, Modal, popup selects, model select, usage/context popovers, feedback actions, schedule/job popovers, subagent lineage, settings panel, cordis panel, experimental team panel) takes prominent, and the lv2 surfaces (scroll-to-bottom button, turn-preview card, attachment-rail arrows, question composer, trajectory tooltip) take panel. The composer card takes the soft tier with the l2 stroke rebind, and its workspace-trigger state sets the stroke color `transparent` instead of the former `border-color: transparent`. Dark theme needs no shadow overrides: the soft layers are near-invisible there and the stroke carries the separation.
`packages/client/ui-theme/tests/elevation-styles.client.spec.ts` pins the token composition and scans every stylesheet under `packages/`: a rule pairing an lv/elevation `box-shadow` with a `--dsw-alias-border-*` border fails, and every `solid` border on a neutral `--dsw-alias-border-*` token must be `0.5px` wide. Deliberate keeps: Toast and HoverCard (inverted fills where a theme-following stroke is meaningless), ImageLightbox (bare image), and the warn-bordered approval/plan panels, whose state-colored borders stay real borders and pass the scan.
@ -24,9 +24,9 @@ Flat widgets keep real borders at hairline width: every neutral-token `1px solid
## Alternatives considered
**Keeping 1px real borders and only softening the shadows.** Leaves the layout-consuming border, the light-theme stroke gap on `border-inverted` floats, and the double outline wherever both existed; the stroke-in-shadow form is what produces the crisp Codex edge.
**Keeping 1px real borders and only softening the shadows.** Leaves the layout-consuming border, the light-theme stroke gap on `border-inverted` floats, and the double outline wherever both existed; the stroke-in-shadow form is what produces the crisp hairline edge.
**A 1px stroke instead of 0.5px.** The Codex elevation stroke is 0.5px; at 2x displays it renders one physical pixel, and on 1x it blends lighter, which is the intended hairline. 1px reads as the old border.
**A 1px stroke instead of 0.5px.** At 2x displays 0.5px renders one physical pixel, and on 1x it blends lighter, which is the intended hairline. 1px reads as the old border.
**Drawing flat-widget hairlines as box-shadow strokes too.** Buttons and inputs swap `border-color` on hover and focus and several pair a box-shadow focus ring; moving their stroke into `box-shadow` would collide with those rings (one property) and rewrite every state rule, while `0.5px solid` keeps the whole state logic and changes only the weight.

View file

@ -6,7 +6,7 @@ Status: implemented
## Problem
Web 客户端的高层级表面——菜单、浮层、对话框、面板、悬浮按钮、输入框——原先都把真 `border: 1px solid <中性 token>` 与 `--dsw-shadow-lv2`/`lv3` 投影配对。border 占布局(每侧 1px,且在 `<button>` 上是对 UA 默认边框的替换);浅色主题下多数浮层实际没有描边(`--dsw-alias-border-inverted` 在浅色下是透明的),靠 `lv3` 里模糊的 1px 环冒充;输入框则披着一大片柔和的 `lv2` 投影,读起来更像污渍而非悬浮表面。解包的 ChatGPT 桌面端(Codex)UI 改用单个 `box-shadow` 列表绘制 elevation:0.5px 发丝描边加两层极淡柔光(`--elevation-stroke`/`--elevation-sidebar`/`--elevation-prominent`),表面本身 `border: 0`。
Web 客户端的高层级表面——菜单、浮层、对话框、面板、悬浮按钮、输入框——原先都把真 `border: 1px solid <中性 token>` 与 `--dsw-shadow-lv2`/`lv3` 投影配对。border 占布局(每侧 1px,且在 `<button>` 上是对 UA 默认边框的替换);浅色主题下多数浮层实际没有描边(`--dsw-alias-border-inverted` 在浅色下是透明的),靠 `lv3` 里模糊的 1px 环冒充;输入框则披着一大片柔和的 `lv2` 投影,读起来更像污渍而非悬浮表面。当前桌面聊天 UI 改用单个 `box-shadow` 列表绘制 elevation:0.5px 发丝描边加两层极淡柔光,表面本身 `border: 0`。
## Decision
@ -16,7 +16,7 @@ Web 客户端的高层级表面——菜单、浮层、对话框、面板、悬
- `--dsw-elevation-stroke: 0 0 0 0.5px var(--dsw-elevation-stroke-color)`——单独的描边,供只要轮廓的行内卡片独立使用(插件清单卡片)。
- `--dsw-elevation-panel` / `--dsw-elevation-prominent`——描边加两层极淡柔光(3px 方向光 + 16/20px 辉光,黑 2–5%),panel 用于小型悬浮部件与卡片,prominent 用于浮层,soft——更大模糊、更低透明度——用于输入框。
被转换的表面设 `border: 0` 加一个 elevation 投影:所有 `--dsw-shadow-lv3` 浮层(Menu、Modal、弹出选择、模型选择、用量/上下文浮层、反馈操作条、日程/任务浮层、子代理谱系、设置面板、cordis 面板、实验性 team 面板)取 prominent;lv2 表面(回到底部按钮、回合预览卡、附件栏箭头、问题 composer、轨迹 tooltip)取 panel。输入框卡片取 soft 档并重绑 l2 描边,其 workspace-trigger 态把描边色设为 `transparent`,替代原先的 `border-color: transparent`。深色主题无需投影覆盖:柔光在深色下几乎不可见,分离由描边承担,与 Codex 相同。
被转换的表面设 `border: 0` 加一个 elevation 投影:所有 `--dsw-shadow-lv3` 浮层(Menu、Modal、弹出选择、模型选择、用量/上下文浮层、反馈操作条、日程/任务浮层、子代理谱系、设置面板、cordis 面板、实验性 team 面板)取 prominent;lv2 表面(回到底部按钮、回合预览卡、附件栏箭头、问题 composer、轨迹 tooltip)取 panel。输入框卡片取 soft 档并重绑 l2 描边,其 workspace-trigger 态把描边色设为 `transparent`,替代原先的 `border-color: transparent`。深色主题无需投影覆盖:柔光在深色下几乎不可见,分离由描边承担。
`packages/client/ui-theme/tests/elevation-styles.client.spec.ts` 钉住 token 组成并扫描 `packages/` 下全部样式表:lv/elevation `box-shadow` 与 `--dsw-alias-border-*` border 配对的规则即失败;中性 `--dsw-alias-border-*` token 上的每个 `solid` border 必须为 `0.5px` 宽。有意保留:Toast 与 HoverCard(反色填充,跟随主题的描边色在其上无意义)、ImageLightbox(裸图片)、warn 描边的审批/计划面板——状态色 border 保持真 border,扫描放行。
@ -24,9 +24,9 @@ Web 客户端的高层级表面——菜单、浮层、对话框、面板、悬
## Alternatives considered
**保留 1px 真 border、只调柔投影。** 留下占布局的 border、浅色主题 `border-inverted` 浮层的描边缺口,以及两者并存处的双轮廓;描边入投影正是产生 Codex 式锐利边缘的形式。
**保留 1px 真 border、只调柔投影。** 留下占布局的 border、浅色主题 `border-inverted` 浮层的描边缺口,以及两者并存处的双轮廓;描边入投影正是产生锐利发丝边缘的形式。
**用 1px 而非 0.5px 描边。** Codex 的 elevation 描边是 0.5px;2x 屏渲染为一物理像素,1x 屏混合得更浅,正是发丝线意图。1px 读起来就是原来的 border。
**用 1px 而非 0.5px 描边。** 0.5px 在 2x 屏渲染为一物理像素,1x 屏混合得更浅,正是发丝线意图。1px 读起来就是原来的 border。
**平面部件的发丝线也用 box-shadow 描边画。** 按钮与输入框在 hover/focus 时切换 `border-color`,多处还配 box-shadow 焦点环;把描边挪进 `box-shadow`(单一属性)会与焦点环冲突并重写全部状态规则,而 `0.5px solid` 保留整套状态逻辑,只改粗细。

View file

@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.md
2026-09-01-web-superellipse-corner-smoothing.md: 1da15664d4c4d92476d7f9414b27c0812665bd60
2026-09-01-web-superellipse-corner-smoothing.zh.md: f2ff1690511fa530b66c53b4e62a53d1af0283ff
2026-09-01-web-superellipse-corner-smoothing.md: 7088438a8068343434a27495b771515d4a5158ee
2026-09-01-web-superellipse-corner-smoothing.zh.md: 26aa64fda9f557069cfc3040422c8e1b7e34676e

View file

@ -6,25 +6,25 @@ English | [中文](2026-09-01-web-superellipse-corner-smoothing.zh.md)
## Problem
Every rounded surface in the web client — cards, composer, buttons, popovers — draws its corners as plain circular arcs, which read as visibly harder than the smooth (squircle-like) corners current desktop chat UIs ship. An unpacked-`app.asar` survey of the ChatGPT desktop app's Codex UI found the smoothness comes from CSS `corner-shape: superellipse(1.5)` applied behind an `@supports` guard to every rounded-corner utility class except full-round (`rounded-full`), not from larger radii or masking tricks. This client has no utility classes: `border-radius` values are px literals spread across CSS Modules in every client package, so there is no single class list to attach the property to, and full-round shapes (`border-radius: 50%` circles, 999px pills) must keep circular arcs — a superellipse deforms a circle into a squircle, so a border-drawn spinner would visibly wobble, and it squares off capsule ends.
Every rounded surface in the web client — cards, composer, buttons, popovers — draws its corners as plain circular arcs, which read as visibly harder than the smooth (squircle-like) corners current desktop chat UIs ship. That smoothness comes from CSS `corner-shape: superellipse(1.5)` applied behind an `@supports` guard, not from larger radii or masking tricks; utility-class implementations attach it to every rounded-corner class except full-round. This client has no utility classes: `border-radius` values are px literals spread across CSS Modules in every client package, so there is no single class list to attach the property to, and full-round shapes (`border-radius: 50%` circles, 999px pills) must keep circular arcs — a superellipse deforms a circle into a squircle, so a border-drawn spinner would visibly wobble, and it squares off capsule ends.
## Decision
`packages/client/ui-theme/src/styles/corner-shape.css` is a global sheet mounted by ui-theme's client entry (after `base.css`). Inside `@supports (corner-shape: superellipse(1.5))` it defines `--dsw-corner-shape: superellipse(1.5)` on `:root` and applies `corner-shape: var(--dsw-corner-shape)` through `*, *::before, *::after` — `corner-shape` does not inherit, so the universal selector is the mechanism that reaches every rounded surface without a utility-class system. Engines without `corner-shape` keep circular corners because both declarations live inside the guard. `superellipse(1.5)` sits between `round` (`superellipse(1)`) and `squircle` (`superellipse(2)`), matching the Codex UI's value.
`packages/client/ui-theme/src/styles/corner-shape.css` is a global sheet mounted by ui-theme's client entry (after `base.css`). Inside `@supports (corner-shape: superellipse(1.5))` it defines `--dsw-corner-shape: superellipse(1.5)` on `:root` and applies `corner-shape: var(--dsw-corner-shape)` through `*, *::before, *::after` — `corner-shape` does not inherit, so the universal selector is the mechanism that reaches every rounded surface without a utility-class system. Engines without `corner-shape` keep circular corners because both declarations live inside the guard. `superellipse(1.5)` sits between `round` (`superellipse(1)`) and `squircle` (`superellipse(2)`), matching the smoothing current desktop chat UIs ship.
Full-round shapes opt back out at their declaration: every `border-radius` of `50%`, `100%`, or a pill radius (≥ 99px) pairs `corner-shape: round` in the same rule of its owning component sheet. The pairing is enforced by `packages/client/ui-theme/tests/corner-shape-styles.client.spec.ts`, which scans every stylesheet under `packages/` (the shared scan helpers live in `tests/stylesheet-scan.ts`, extracted from the scrollbar spec); the same spec pins the guard and the universal application in `corner-shape.css`. Component-local radius indirections (`--dsl-*-radius`) all hold values far below the pill threshold, so the lexical scan covers current usage.
The Codex UI also multiplies its radius tokens by 1.25 alongside the shape change; this client has no radius tokens (px literals per component), so radii are unchanged and only the corner curvature moves.
Token-based implementations pair the shape change with a 1.25× radius scale; this client has no radius tokens (px literals per component), so radii are unchanged and only the corner curvature moves.
## Alternatives considered
**A radius token system first, then per-token application (the literal Codex structure).** Faithful, but converting ~130 px-literal radii across every client package into tokens is a large refactor serving no other current need; the universal selector reaches the same surfaces with one rule.
**A radius token system first, then per-token application.** Faithful, but converting ~130 px-literal radii across every client package into tokens is a large refactor serving no other current need; the universal selector reaches the same surfaces with one rule.
**Applying superellipse to full-round shapes too (no opt-outs).** Fewer declarations, but spinners built from `border-radius: 50%` borders wobble when the rotating shape is not a circle, and capsule ends square off; the Codex UI equally excludes `rounded-full` from `corner-shape`.
**Applying superellipse to full-round shapes too (no opt-outs).** Fewer declarations, but spinners built from `border-radius: 50%` borders wobble when the rotating shape is not a circle, and capsule ends square off.
**Subtree opt-out via `--dsw-corner-shape: round` instead of per-declaration `corner-shape: round`.** The custom property inherits, so a pill's rounded descendants would silently lose smoothing; the explicit per-rule declaration keeps the opt-out exactly as wide as the full-round shape and is what the pairing spec can check.
**Scaling radii by 1.25 like Codex.** Requires the token system above; the curvature change alone already delivers the smoothness, and radii stay as designed.
**Scaling radii by 1.25 alongside the curvature change.** Requires the token system above; the curvature change alone already delivers the smoothness, and radii stay as designed.
## Consequences

View file

@ -6,25 +6,25 @@ Status: implemented
## Problem
Web 客户端里每个圆角表面——卡片、输入框、按钮、浮层——都以普通圆弧绘制圆角,观感明显硬于当前桌面聊天 UI 普遍采用的平滑(类 squircle)圆角。对 ChatGPT 桌面应用 Codex UI 解包 `app.asar` 的调研发现,其平滑感来自在 `@supports` 守卫内对除正圆(`rounded-full`)之外的所有圆角工具类应用 CSS `corner-shape: superellipse(1.5)`,而非更大的半径或遮罩技巧。本客户端没有工具类:`border-radius` 以 px 字面量散布在各客户端包的 CSS Modules 中,没有可以统一挂载该属性的类列表;而正圆形状(`border-radius: 50%` 的圆、999px 胶囊)必须保持圆弧——超级椭圆会把圆变形为 squircle,用 border 绘制的加载圈旋转时会明显晃动,胶囊两端也会变方。
Web 客户端里每个圆角表面——卡片、输入框、按钮、浮层——都以普通圆弧绘制圆角,观感明显硬于当前桌面聊天 UI 普遍采用的平滑(类 squircle)圆角。这种平滑感来自在 `@supports` 守卫内应用 CSS `corner-shape: superellipse(1.5)`,而非更大的半径或遮罩技巧;工具类体系的实现把它挂到除正圆之外的所有圆角工具类上。本客户端没有工具类:`border-radius` 以 px 字面量散布在各客户端包的 CSS Modules 中,没有可以统一挂载该属性的类列表;而正圆形状(`border-radius: 50%` 的圆、999px 胶囊)必须保持圆弧——超级椭圆会把圆变形为 squircle,用 border 绘制的加载圈旋转时会明显晃动,胶囊两端也会变方。
## Decision
`packages/client/ui-theme/src/styles/corner-shape.css` 是由 ui-theme 客户端 entry 挂载的全局样式表(位于 `base.css` 之后)。它在 `@supports (corner-shape: superellipse(1.5))` 内于 `:root` 定义 `--dsw-corner-shape: superellipse(1.5)`,并通过 `*, *::before, *::after` 应用 `corner-shape: var(--dsw-corner-shape)`——`corner-shape` 不继承,通配选择器正是在没有工具类系统的前提下触达每个圆角表面的机制。两条声明都在守卫内,因此不支持 `corner-shape` 的引擎保持普通圆弧。`superellipse(1.5)` 介于 `round`(`superellipse(1)`)与 `squircle`(`superellipse(2)`)之间,与 Codex UI 取值一致。
`packages/client/ui-theme/src/styles/corner-shape.css` 是由 ui-theme 客户端 entry 挂载的全局样式表(位于 `base.css` 之后)。它在 `@supports (corner-shape: superellipse(1.5))` 内于 `:root` 定义 `--dsw-corner-shape: superellipse(1.5)`,并通过 `*, *::before, *::after` 应用 `corner-shape: var(--dsw-corner-shape)`——`corner-shape` 不继承,通配选择器正是在没有工具类系统的前提下触达每个圆角表面的机制。两条声明都在守卫内,因此不支持 `corner-shape` 的引擎保持普通圆弧。`superellipse(1.5)` 介于 `round`(`superellipse(1)`)与 `squircle`(`superellipse(2)`)之间,与当前桌面聊天 UI 的平滑度一致。
正圆形状在其声明处退出:每个取值为 `50%`、`100%` 或胶囊半径(≥ 99px)的 `border-radius`,都在所属组件样式表的同一规则内配对 `corner-shape: round`。该配对由 `packages/client/ui-theme/tests/corner-shape-styles.client.spec.ts` 强制,它扫描 `packages/` 下的全部样式表(共享扫描辅助函数位于 `tests/stylesheet-scan.ts`,自 scrollbar spec 抽出);同一 spec 也钉住 `corner-shape.css` 的守卫与通配应用。组件局部半径变量(`--dsl-*-radius`)取值都远低于胶囊阈值,因此词法扫描覆盖当前用法。
Codex UI 在改变曲线的同时还把半径 token 乘以 1.25;本客户端没有半径 token(各组件 px 字面量),故半径不变,只改变圆角曲率。
基于 token 的实现会在改变曲线的同时把半径 token 乘以 1.25;本客户端没有半径 token(各组件 px 字面量),故半径不变,只改变圆角曲率。
## Alternatives considered
**先建半径 token 系统,再按 token 应用(照搬 Codex 结构)。** 更忠实,但把所有客户端包约 130 处 px 字面量半径改造成 token 是没有其他现实需求的大重构;通配选择器用一条规则触达同样的表面。
**先建半径 token 系统,再按 token 应用。** 更忠实,但把所有客户端包约 130 处 px 字面量半径改造成 token 是没有其他现实需求的大重构;通配选择器用一条规则触达同样的表面。
**对正圆形状也应用超级椭圆(不设豁免)。** 声明更少,但用 `border-radius: 50%` border 绘制的加载圈在形状不是圆时旋转会晃动,胶囊两端会变方;Codex UI 同样把 `rounded-full` 排除在 `corner-shape` 之外。
**对正圆形状也应用超级椭圆(不设豁免)。** 声明更少,但用 `border-radius: 50%` border 绘制的加载圈在形状不是圆时旋转会晃动,胶囊两端会变方。
**用子树级 `--dsw-corner-shape: round` 替代逐声明 `corner-shape: round` 豁免。** 自定义属性会继承,胶囊的圆角后代会静默失去平滑;逐规则显式声明让豁免范围恰好等于正圆形状本身,也是配对 spec 能检查的形式。
**像 Codex 一样把半径乘 1.25。** 需要上述 token 系统;仅曲率变化已带来平滑感,半径维持设计值。
**在改变曲率的同时把半径乘 1.25。** 需要上述 token 系统;仅曲率变化已带来平滑感,半径维持设计值。
## Consequences