Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# `Dialog.Content` gains a `width` prop; `max-w-*` alone cannot widen a panel

Status: implemented
Translation: current

[中文](2026-09-25-dialog-content-width-prop.zh.md)

## Abstract

The `@lody/ui` migration changed the dialog panel from fluid width capped by
`max-w-lg` to a fixed `width: 512px` StyleX declaration. Every dialog that had
widened itself with a `max-w-{xl,2xl,3xl,4xl}` class — and no `width` — was
silently clamped back to 512px, because `max-width` can only narrow a fixed
width, never raise it. `@lody/ui`'s `ModalContentProps` — the props type shared
by `Dialog.Content` and `AlertDialog.Content` — now exposes `width`, which
lands on inline `style`, and every panel needing a non-default width uses it.
The product adapter forwards it untouched. The cmdk palette's 512px is
intentional post-rework and was left alone.

## The trap

`modal.popup` in `@lody/ui` states `width: dialog.width` (512px) plus
`maxWidth: calc(100vw - 32px)`. Three override spellings behave differently:

- `max-w-2xl` alone: `max-width` only caps, so the panel stays 512px — the
regression this note records.
- A `w-[…]` class: works today only because `tailwind/index.css` declares
`@layer …, stylex, …, utilities`, putting utilities after the component's
sheet. Reliable but subtle, and overriding `max-w-*` also replaces the
rung's `100vw - 32px` viewport cap.
- A second StyleX `width` class: unordered against the panel's own declaration
— `settings/surface.ts` already documented this.

`width` → inline `style` sidesteps all three: inline style always wins, and it
never touches the panel's `max-width`, so the viewport cap still bounds the
panel on narrow windows. `desktop-settings-modal`'s `style` object (width plus
height/padding/gap) stays on `style`; the prop is for the one width dial.

## Changes

- `packages/ui`: `ModalContentProps` gains `width`; `mergePanelWidth` in
`dialog/parts.tsx` composes it into the popup's `style`, whether the caller's
style is an object or a state callback (Base UI permits both). The product
adapter in `components/src/ui/dialog.tsx` forwards it untouched; `Drawer`
keeps its own props type — a drawer's cross-axis size is `drawerSize`, a
different dial. `test/dialog.test.tsx` pins the inline-style contract.
- Restored widths: composer paste preview 48rem, usage share image 56rem,
session file quick-open 42rem, machine pairing 36rem, operation reply 42rem.
- Unified the surviving mixed spellings onto the prop:
`chat-failed-detail` and `session-file-preview` (`w-[calc(100vw-2rem)]
max-w-*` → `width="42rem"`/`"48rem"`), `chat-share-image` and
`update-changelog` (`style={{width}}` → prop), `open-source-attributions`
(`style` const → `width="1024px"`), `skill-detail` (`width`+`maxWidth` pair →
`width="768px"`).
- `SETTINGS_EDITOR_DIALOG_LAYOUT` split: `SETTINGS_EDITOR_DIALOG_WIDTH`
(`620px`) rides the prop; the class keeps only the height cap.
- The guardrail ("`max-w-*` alone can only narrow, never widen") is a line in
the adapter's docblock, not `components/src/ui/AGENTS.md`: that file is
already 8168 bytes against its 8192-byte budget, so nothing can be added to
it.

## Evidence and limits

Cascade facts verified in source: `tailwind/index.css` layer order, the
`w-[620px]` precedent in `settings/surface.ts`, and `modal.popup`'s fixed
`width` token. Behavioral widths match pre-migration `max-w-*` values on
desktop and improve narrow-viewport behavior (the rung's cap replaces
hand-rolled `100vw - 2rem` math). Not yet run: `tsgo`/`oxlint` (nested
checkout without installed dependencies) and visual passes of each dialog.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# `Dialog.Content` 增加 `width` prop;单独的 `max-w-*` 无法加宽面板

Status: implemented
Translation: current

[English](2026-09-25-dialog-content-width-prop.md)

## 摘要

`@lody/ui` 迁移把对话框面板从「流式宽度 + `max-w-lg` 封顶」改成了固定的
`width: 512px` StyleX 声明。所有只靠 `max-w-{xl,2xl,3xl,4xl}` 类、没有写
`width` 的对话框都被静默钳回 512px —— `max-width` 只能收窄固定宽度,不能抬高
它。`@lody/ui` 的 `ModalContentProps`(`Dialog.Content` 与
`AlertDialog.Content` 共用的 props 类型)现在暴露 `width`,落到 inline
`style` 上;所有需要非默认宽度的面板统一走它。产品层 adapter 原样透传。
cmdk 面板的 512px 是重构后的有意设计,保持不变。

## 坑在哪

`@lody/ui` 的 `modal.popup` 声明了 `width: dialog.width`(512px)和
`maxWidth: calc(100vw - 32px)`。三种覆盖写法效果不同:

- 单独 `max-w-2xl`:`max-width` 只能封顶,面板停在 512px —— 本次回归。
- `w-[…]` 类:今天能生效只是因为 `tailwind/index.css` 声明了
`@layer …, stylex, …, utilities`,把 utilities 排在组件样式表之后。可靠但
隐晦,而且覆盖 `max-w-*` 会把 rung 自带的 `100vw - 32px` 视口封顶一起换掉。
- 第二个 StyleX `width` 类:与面板自身声明同层,谁赢由样式表顺序决定 ——
`settings/surface.ts` 里已有同样结论的注释。

`width` → inline `style` 绕开这三条:inline style 永远生效,且不触碰面板的
`max-width`,窄视口下视口封顶依旧生效。`desktop-settings-modal` 的 `style`
对象(除宽度外还设了 height/padding/gap)保留在 `style`;prop 只管宽度这一个
旋钮。

## 改动

- `packages/ui`:`ModalContentProps` 增加 `width`;`dialog/parts.tsx` 的
`mergePanelWidth` 把它合进 popup 的 `style`,调用方传对象或状态回调两种
形式都兼容(Base UI 两种都允许)。`components/src/ui/dialog.tsx` 的产品
adapter 原样透传;`Drawer` 保留自己的 props 类型 —— drawer 的横向尺寸是
`drawerSize`,是另一个旋钮。`test/dialog.test.tsx` 钉住了 inline-style
契约。
- 恢复宽度:composer 粘贴预览 48rem、用量分享图 56rem、文件快速打开 42rem、
机器配对 36rem、operation 回复 42rem。
- 现存混用写法统一收口到 prop:`chat-failed-detail`、`session-file-preview`
(`w-[calc(100vw-2rem)] max-w-*` → `width="42rem"`/`"48rem"`)、
`chat-share-image`、`update-changelog`(`style={{width}}` → prop)、
`open-source-attributions`(`style` 常量 → `width="1024px"`)、
`skill-detail`(`width`+`maxWidth` 组合 → `width="768px"`)。
- `SETTINGS_EDITOR_DIALOG_LAYOUT` 拆分:`SETTINGS_EDITOR_DIALOG_WIDTH`
(`620px`)走 prop,类只保留高度上限。
- 「单独的 `max-w-*` 只能收窄、不能放宽」这条防线写在 adapter 的 docblock
里,不在 `components/src/ui/AGENTS.md`:该文件已达 8168 字节,紧贴 8192
上限,一个字都加不进去。

## 证据与边界

层叠事实均从源码核实:`tailwind/index.css` 的层顺序、`settings/surface.ts`
里 `w-[620px]` 的先例注释、`modal.popup` 的固定 `width` token。桌面端行为宽度
与迁移前 `max-w-*` 值一致,窄视口行为反而更稳(rung 自带封顶替代手写的
`100vw - 2rem`)。尚未执行:`tsgo`/`oxlint`(此 nested checkout 未装依赖)与
各对话框的实机视觉确认。
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ export function ChatFailedDetailDialog({

return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
{/* `w-[calc(100vw-2rem)]` keeps the raw error readable on phones, where the
shared dialog default reserves 2rem of margin on each side. */}
<Dialog.Content className="w-[calc(100vw-2rem)] max-w-2xl gap-3">
{/* A wider panel than the modal default keeps the raw error readable; the
rung's own viewport cap still bounds it on phones. */}
<Dialog.Content width="42rem" className="gap-3">
<Dialog.Header className="pr-6 text-left sm:text-left">
<Dialog.Title className="flex items-start gap-2 text-base">
<AlertCircle className="mt-0.5 size-4 shrink-0 text-destructive" aria-hidden="true" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export function OperationReplyCard(props: OperationReplyCardProps) {

{reply ? (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Content className="flex max-h-[80vh] max-w-2xl flex-col gap-0 p-0 sm:p-0">
<Dialog.Content width="42rem" className="flex max-h-[80vh] flex-col gap-0 p-0 sm:p-0">
<div className="flex min-w-0 items-center gap-3 border-b border-border/60 py-3 pl-5 pr-12">
<div className="min-w-0 flex-1">
<Dialog.Description className="text-xs">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function SessionFilePreviewPanel({
// `min-w-0` is load-bearing: this panel is a grid child of Dialog.Content
// (`display: grid`). Grid/flex items default to `min-width: auto`, so wide
// content (long code lines, tables, unbreakable tokens) would stretch the
// panel past the dialog's `max-w-3xl` and bleed out of the modal. Capping
// panel past the dialog's `width` cap and bleed out of the modal. Capping
// the min width forces content to wrap/scroll within the modal width.
<div className="flex min-h-0 min-w-0 flex-col">
<div className="flex items-center justify-between gap-3 border-b border-border/50 pb-3">
Expand Down Expand Up @@ -207,7 +207,7 @@ export function SessionFilePreviewDialog({
const panelKey = useMemo(() => panelProps.file.fileId, [panelProps.file.fileId]);
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Content closeButton={false} className="w-[calc(100vw-2rem)] max-w-3xl">
<Dialog.Content closeButton={false} width="48rem">
<SessionFilePreviewPanel key={panelKey} {...panelProps} />
</Dialog.Content>
</Dialog.Root>
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/components/chat/chat-composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1170,7 +1170,8 @@ export function ChatComposer({
>
<Dialog.Content
closeButton={false}
className="flex h-[85vh] max-h-[85vh] max-w-3xl flex-col gap-0 overflow-hidden p-0"
width="48rem"
className="flex h-[85vh] max-h-[85vh] flex-col gap-0 overflow-hidden p-0"
>
{previewPastedTextDraft ? (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function MachinePairingDialog({

return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Content className="max-w-xl">
<Dialog.Content width="36rem">
{connected ? (
<>
<Dialog.Header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,7 @@ export function ChatShareImageDialog({
<Dialog.Root open={open} onOpenChange={requestOpenChange}>
{/* The preview is the card at a readable scale, so the panel is wider than a
dialog's column of prose. */}
<Dialog.Content style={{ width: '720px' }}>
<Dialog.Content width="720px">
<Dialog.Header>
<Dialog.Title>{dialogTitle}</Dialog.Title>
<Dialog.Description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export function SessionFileQuickOpen({

return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Content className="overflow-hidden p-0 sm:max-w-2xl">
<Dialog.Content width="42rem" className="overflow-hidden p-0">
<Dialog.Title className="sr-only">
{t('sessions.fileQuickOpen.title', 'Quick open file')}
</Dialog.Title>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import { capturePostHogEvent } from '@/lib/posthog-analytics';
import { Dialog } from '@/ui/dialog';
import { AgentRoleForm } from './agent-role-form';
import { SETTINGS_EDITOR_DIALOG_LAYOUT } from './surface';
import { SETTINGS_EDITOR_DIALOG_LAYOUT, SETTINGS_EDITOR_DIALOG_WIDTH } from './surface';

/**
* A `create` carries its id from the moment the form opens.
Expand Down Expand Up @@ -208,7 +208,10 @@ export function AgentRoleEditorDialog({
if (!open) close();
}}
>
<Dialog.Content className={SETTINGS_EDITOR_DIALOG_LAYOUT}>
<Dialog.Content
width={SETTINGS_EDITOR_DIALOG_WIDTH}
className={SETTINGS_EDITOR_DIALOG_LAYOUT}
>
<Dialog.Header>
<Dialog.Title>
{editor?.mode === 'edit'
Expand Down
6 changes: 5 additions & 1 deletion packages/components/src/components/settings/mcp-setting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { space } from '@lody/ui/tokens/scales.stylex';
import { McpConnectionForm, type McpConnectionFormValue } from './mcp-connection-form';
import {
SETTINGS_EDITOR_DIALOG_LAYOUT,
SETTINGS_EDITOR_DIALOG_WIDTH,
settingsCatalog as catalog,
settingsSurface as surface,
} from './surface';
Expand Down Expand Up @@ -182,7 +183,10 @@ export function McpSetting() {
setEditor(null);
}}
>
<Dialog.Content className={SETTINGS_EDITOR_DIALOG_LAYOUT}>
<Dialog.Content
width={SETTINGS_EDITOR_DIALOG_WIDTH}
className={SETTINGS_EDITOR_DIALOG_LAYOUT}
>
<Dialog.Header>
<Dialog.Title>
{editor?.mode === 'edit' ? t('settings.mcp.editTitle') : t('settings.mcp.addTitle')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@ const REGION = `color-mix(in oklab, transparent, ${colors.label} 3%)`;

/**
* The panel's own width: a license list reads as a table of names and notes, so
* it takes a wider panel than a question does. The panel keeps its own cap
* against the window.
* it takes a wider panel than a question does.
*/
const PANEL_STYLE = { width: '1024px' } as const;
const PANEL_WIDTH = '1024px';

const styles = stylex.create({
summary: {
Expand Down Expand Up @@ -253,7 +252,7 @@ export function OpenSourceAttributionsDialog({
<ScrollText {...stylex.props(styles.smallIcon)} />
{t('settings.about.viewAttributions', 'View notices')}
</Dialog.Trigger>
<Dialog.Content style={PANEL_STYLE}>
<Dialog.Content width={PANEL_WIDTH}>
<Dialog.Header>
<Dialog.Title>
{t('settings.about.openSourceAttributions', 'Open Source Licenses')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
import { ScopePills } from './prompt-shortcut-scope';
import {
SETTINGS_EDITOR_DIALOG_LAYOUT,
SETTINGS_EDITOR_DIALOG_WIDTH,
settingsCatalog as catalog,
settingsSurface as surface,
} from './surface';
Expand Down Expand Up @@ -253,7 +254,10 @@ function PromptShortcutsSettingContent({
if (!open && !busy) setEditor(null);
}}
>
<Dialog.Content className={SETTINGS_EDITOR_DIALOG_LAYOUT}>
<Dialog.Content
width={SETTINGS_EDITOR_DIALOG_WIDTH}
className={SETTINGS_EDITOR_DIALOG_LAYOUT}
>
<Dialog.Header>
<Dialog.Title>
{!editor?.base
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/components/settings/skill-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const styles = stylex.create({
});

/** The panel reads a SKILL.md, so it keeps a reading width rather than a form's. */
const SKILL_DIALOG_STYLE = { width: 'calc(100vw - 2rem)', maxWidth: '768px' } as const;
const SKILL_DIALOG_WIDTH = '768px';

/**
* Shared skill detail body: badges + metadata + the rendered SKILL.md markdown
Expand Down Expand Up @@ -135,7 +135,7 @@ export function SkillDetailDialog({
}) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Content style={SKILL_DIALOG_STYLE}>
<Dialog.Content width={SKILL_DIALOG_WIDTH}>
<Dialog.Header>
<Dialog.Title {...stylex.props(styles.title)}>{skill?.name}</Dialog.Title>
</Dialog.Header>
Expand Down
10 changes: 6 additions & 4 deletions packages/components/src/components/settings/surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,8 +526,10 @@ export const settingsCatalog = stylex.create({

/**
* The size of a settings editor dialog (MCP server, Agent Role, Prompt
* Shortcut). Layout only, and a Tailwind class on purpose: the panel states its
* own width in StyleX, and a second StyleX width on the same element is ordered
* by the stylesheet, while utilities sit in a later layer and win.
* Shortcut). The width rides `Dialog.Content`'s `width` prop — the panel's own
* width is a StyleX declaration, and a second one on the same element is
* ordered by the stylesheet rather than the caller. This class keeps only the
* height cap.
*/
export const SETTINGS_EDITOR_DIALOG_LAYOUT = 'w-[620px] max-h-[min(680px,88dvh)]';
export const SETTINGS_EDITOR_DIALOG_WIDTH = '620px';
export const SETTINGS_EDITOR_DIALOG_LAYOUT = 'max-h-[min(680px,88dvh)]';
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,10 @@ export function UsageShareImageDialog({
if (!exportingRef.current) onOpenChange(next);
}}
>
<Dialog.Content className="flex max-h-[85vh] max-w-4xl flex-col gap-0 overflow-hidden p-0 sm:p-0">
<Dialog.Content
width="56rem"
className="flex max-h-[85vh] flex-col gap-0 overflow-hidden p-0 sm:p-0"
>
<Dialog.Header className="border-b border-border/70 px-4 py-3.5 pr-12 text-left sm:px-5 sm:pr-12">
<Dialog.Title className="text-base">
{t('workspace.usage.shareImage.dialogTitle')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function UpdateChangelogDialog({

return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Content style={{ width: PANEL_WIDTH }}>
<Dialog.Content width={PANEL_WIDTH}>
<Dialog.Header>
<Dialog.Title>
{t('updates.changelog.title', "What's new in {{version}}", { version })}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ type Story = StoryObj<typeof meta>;
// so the story exercises the panel exactly as users see it.
const wrap = (content: React.ReactNode) => (
<Dialog.Root open>
<Dialog.Content closeButton={false} className="w-[calc(100vw-2rem)] max-w-3xl">
<Dialog.Content closeButton={false} width="48rem">
{content}
</Dialog.Content>
</Dialog.Root>
Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { WindowDragStrip } from '@/ui/window-drag-region';
* - `WindowDragStrip` on the backdrop, so an Electron window stays draggable
* beside the dialog the same way it is beside the page.
*
* A non-default panel width goes on `Content`'s `width` prop (`@lody/ui`'s
* `ModalContentProps`), never a `max-w-*` class — a cap alone can only narrow
* the rung's fixed `dialog.width`, never widen it.
*
* `AlertDialog.Action`/`AlertDialog.Cancel` are `Close` rendered as styled
* buttons: an answer runs its `onClick` and then closes. An answer that must
* hold the dialog open while work is in flight is not a `Close` at all — it is
Expand Down
Loading
Loading