Skip to content

fix(charts): ChartContainer 的 min-size 兜底改为显式合并,不再被调用方 style 整体覆盖 - #3956

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-7026-chart-min-size-style
Aug 9, 2026
Merged

fix(charts): ChartContainer 的 min-size 兜底改为显式合并,不再被调用方 style 整体覆盖#3956
yinlianghui merged 1 commit into
mainfrom
claude/issue-7026-chart-min-size-style

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixesobjectstack-ai/objectstack#7026

机理

ChartContainerImpl.tsxstyle 写成显式 JSX 属性,而 {...props} 展开在它下面一行;propsComponentProps< "div" > 解构后的 rest,只摘掉了 id/className/children/config/disableSettleRemount —— style 还在里面。JSX 里后写的同名属性整体覆盖先写的,所以只要调用方传了 style,整个 style 对象就被替换:minHeight: 280minWidth: 0 一起消失,而写在里面的 ...props.style 那次合并从未执行过一次。它是死代码,却让读代码的人以为兜底一直在。

这个兜底不是装饰:它是为了让 Recharts 的 ResponsiveContainer 永远有一个非零盒子可量。dashboard widget 覆盖掉容器的 h-[350px] 类、又把 chart 塞进没有显式子高度的 flex/grid 时,盒子停在 0,Recharts 量到 width/height = -1,图渲染不出来 —— 正是当年加这道兜底要防的场景。

改法

style 从 rest props 里显式解构出来、单独合并,于是"谁覆盖谁"写在代码里,而不是靠 JSX 属性顺序这种一眼看不出来的机制;{...props} 也再也碰不到 style

合并语义:作者的显式尺寸赢。 只是"把 {...props} 挪到前面、无条件合并"会把这个 bug 换成它的镜像 —— minHeight: 280 与作者写的 height: 100 并存时,CSS 里 min-height 赢,作者的 100 被静默压成 280。所以兜底的每一半都只在调用方 style 两个对应键都没声明时才注入:

  • 高度那一半:调用方 style 既无 height 也无 minHeight 时才注入 minHeight: 280;
  • 宽度那一半:既无 width 也无 minWidth 时才注入 minWidth: 0;
  • 键存在但值为 undefined/null 记作"未声明";
  • 调用方其余 style 键原样透传。

注释也改成与实现一致:说清何时兜底、何时让位于作者的显式尺寸,并记下原来的顺序依赖为什么是死代码。

行为变化面(刻意收窄)

  • 不传 style 的调用方:逐字节不变。
  • style 带高度的调用方 —— 今天树里唯一的形状,即 AdvancedChartImplcontainerProps 转发 ChartConfig.height —— 保留它声明的高度,同样不变,额外多拿回 minWidth: 0 那一半。
  • 真正变的是这单被开出来的那一格:style 不含尺寸键时(margin、padding、aspect-ratio,以及以后任何走同一条 containerProps 路径的容器级呈现键)现在保有 min-size 兜底,而不是把它一起静默带走。

钉子与反向验证

两方向都钉,因为单方向的钉子会被镜像改法一样满足:

钉子修前修后
不含尺寸的 style({ margin: 8 })仍带 min-height: 280 / min-width: 0,且 margin 透传🔴 红🟢 绿
className 覆盖高度类 + 非尺寸 style 时兜底仍在🔴 红🟢 绿
style: { height: 100 } 渲染成 100 且min-height🔴 红(仅 minWidth 那半条)🟢 绿
style: { minHeight: 100 } 低于兜底也赢🟢 绿🟢 绿
style: { width: 400 } 不注入 minWidth、高度那半仍兜底🔴 红(仅 minHeight 那半条)🟢 绿
无 style 时行为与现状一致🟢 绿🟢 绿
height: undefined 记作未声明,仍兜底🔴 红🟢 绿

反向验证做了两次,方向都是先预判后跑:

  1. 新钉子打在未改动的源码上 —— 预判 5 红 2 绿,实测逐条命中(红:1、2、3、5、7;绿:4、6)。
  2. 另外装了一次"无条件合并"的镜像改法作探针 —— 预判恰好是那两条"作者显式尺寸赢"的钉子转红,实测 2 failed | 5 passed,正是第 3、5 条。所以这两条钉子是承重的,不是陪衬。

范围

只动 ChartContainerImpl.tsx + 新测试 + changeset。⛔ 没动 AdvancedChartImplcontainerProps 生产侧;⛔ 没碰在飞的 chart block 外壳文件;⛔ 没碰 releases/。

packages/components/src/ui/chart.tsx 里那份上游 shadcn 的 ChartContainer 副本已核查:它根本没有 min-size style,也没有显式 style 属性,不存在同一 bug(且属 No-Touch 区)。


Generated by Claude Code

… consumer style can't revoke it
`style` was written as an explicit JSX attribute ABOVE `{...props}`, and `props`
still carried the consumer's `style` (only id/className/children/config/
disableSettleRemount are destructured out). A later same-named JSX attribute
replaces an earlier one, so any caller passing `style` replaced the whole object:
`minHeight: 280` and `minWidth: 0` both vanished and the `...props.style` merge
written inside it never ran once — dead code that read as a guarantee.
`style` is now destructured out of the rest props and merged explicitly, so
precedence is stated in code rather than decided by attribute order.
Precedence: the author's explicit size wins. Spreading `{...props}` first and
merging unconditionally would trade this bug for its mirror image (a 280 floor
overriding an authored `height: 100`), so the height half of the fallback applies
only when the consumer style declares neither `height` nor `minHeight`, and the
width half only when it declares neither `width` nor `minWidth`.
Pinned in both directions in ChartContainerImpl.styleMerge.test.tsx.
Fixesobjectstack-ai/objectstack#7026
Co-Authored-By: Claude <noreply@anthropic.com>
@vercel

vercelBot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 9, 2026 4:55pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-DebUKnx_.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)482.64KB106.39KB
core (index.js)3.00KB1.14KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)139.61KB35.99KB
fields (index.js)227.41KB56.29KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.17KB17.35KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.02KB30.47KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)236.63KB59.02KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)187.63KB49.66KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)109.73KB26.55KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)20.15KB6.72KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 验收(session_01GTRjn8xBqp75dk7kFupVRt):通过,转 ready 并挂 auto-merge。os#7026 落地。

核验记录(head 00e1ef5db,基 65bb513dc,实物核验 + CI 亲读):

  1. 实现与裁定语义一致:style 从 rest 显式解构,hasDeclaredHeight/hasDeclaredWidth 按对应键分别判(!= null 同时覆盖 undefined/null),兜底仅在未声明侧注入,{...props} 不再含 style —— JSX 属性顺序依赖消灭,原死代码合并被真实合并取代;注释整段重写与实现一致。
  2. 前提实测非推理:钉子打在未改源码上证明 margin: 8 时 minHeight 为空串 —— 死代码判定成立。
  3. 反向验证两次、双向承重:实验 1(修前 5 红 2 绿)逐条命中预判;dev 诚实指出方向②「显式尺寸赢」修前即绿(模板不适用),另做「无条件合并」镜像探针证明该钉子承重(2 红 5 绿,红恰为③⑤)—— 这是正确的处置,不为凑模板改写实情。
  4. 消费半径外扩:plugin-dashboard 两条直接断言容器 style.height 的 fixture 一并跑绿(29 files / 262 tests);与在飞 os#6953 的文件面已核对不相交(其面为 ObjectChart/index.tsx)。
  5. 门与规程:type-check 78/78;控制字节门过 + 自查盲区零命中;changeset patch 三门全绿;提交文案控制词 0;⛔ releases/ 未触碰;上游 shadcn 副本(No-Touch 区)已核查不含同缺陷、未动。
  6. CI 亲读终态:20/20 全 completed、0 失败(Test shard×4 至 17:01:32Z、Type Check 16:57:31Z、Lint 16:59:12Z;coverage/dependabot path-filter skipped 计绿)。

out-of-scope:#3339 上的范围扩大评论(ECONNREFUSED 噪音旁证)处置得当,未开孪生单。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 9, 2026 17:05
@yinlianghui
yinlianghui added this pull request to the merge queueAug 9, 2026
Merged via the queue into main with commit a7e39a8Aug 9, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-7026-chart-min-size-style branch August 9, 2026 17:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectui:ChartContainer 的 min-size 兜底被 {...props} 覆盖掉 —— ...props.style 那次合并是死代码

2 participants

@yinlianghui@claude