Skip to content

fix(plugin-kanban,plugin-calendar): stop spelling the house drawer width as a renderer default - #6581

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-6303-kanban-calendar-nav-width
Aug 26, 2026
Merged

fix(plugin-kanban,plugin-calendar): stop spelling the house drawer width as a renderer default#6581
os-support-ai merged 2 commits into
mainfrom
claude/issue-6303-kanban-calendar-nav-width

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#6303

Converges ObjectKanban and ObjectCalendar on the shape #6305 gave ObjectGantt. Only the decided width half; the size: 'lg' bucket question is untouched and stays open on #6303 for a human ruling.

The four sites

Each renderer carried two copies of the house default. Taking only the first would have left the old width alive by a different route — which is what the card was filed to prevent.

fileline on mainbeforeafter
plugin-kanban/src/ObjectKanban.tsx693(schema as any).navigation ?? { mode: 'drawer', width: 'min(960px, 60vw)' }(schema as any).navigation ?? { mode: 'drawer' }
plugin-kanban/src/ObjectKanban.tsx995width={(navigation.width as any) ?? 'min(960px, 60vw)'}width={navigation.width as any}
plugin-calendar/src/ObjectCalendar.tsx449(schema as any).navigation ?? { mode: 'drawer', width: 'min(960px, 60vw)' }(schema as any).navigation ?? { mode: 'drawer' }
plugin-calendar/src/ObjectCalendar.tsx763width={(navigation.width as any) ?? 'min(960px, 60vw)'}width={navigation.width as any}

All four line numbers were re-measured on origin/main @ b1a732b22 and matched the dispatch reading exactly. After this PR the literal survives in these two files only inside the explanatory comments; grep finds no remaining producer.

Why this is zero-pixel

width is @deprecated [#2578 -> size] and resolveOverlayWidth gives an explicit width priority oversize, so spelling it kept the deprecated branch load-bearing on the path most boards and calendars take (no declared navigation) and made the size buckets unreachable there. With both copies omitted, resolveOverlayWidth returns undefined and RecordDetailDrawer's own width default supplies the identical min(960px, 60vw).

The escalation clause was checked rather than assumed. Both renderers reach the drawer by the same path the gantt does — import { RecordDetailDrawer } from '@object-ui/plugin-detail', rendered directly with width={…} — and resolveOverlayWidth returns undefined, never null, so the drawer's default parameter does apply. navConfig.width and navigation.width have no other consumer in either file. Nothing contradicts the zero-pixel finding.

RecordDetailDrawer.tsx is not touched: it is the source the other three now delegate to, and :76-78 carries the only written rationale for the value.

Pin tests, both halves each

ObjectKanban.navWidthDefault.test.tsx and ObjectCalendar.navWidthDefault.test.tsx mirror ObjectGantt.navWidthDefault.test.tsx's posture — half 1 (the renderer injects no width of its own) and half 2 (the width the real drawer then resolves is still the pinned value), because the equivalence depends on both. A third case pins the other direction: an authored navigation.width still reaches the drawer unchanged, so the deprecated key stays accepted as an authored value.

Reverse-verification — direction predicted first, then measured

Prediction: restoring the pre-fix expressions turns only half 1 red in each file. Half 2 and the authored-width control cannot see a re-added ?? fallback — its value is the same string the drawer default produces — and that blind spot is exactly why half 1 exists.

Leg A — both files restored to b1a732b22 (mutation proved on disk by grep counts of both the injected and the deleted text, not by an exit code):

Test Files 2 failed (2)
Tests 2 failed | 4 passed (6)

Both failures are half 1; both half 2 and both authored-width controls stayed green. Matches the prediction exactly.

Leg B — half 2's own load-bearing check: RecordDetailDrawer's default temporarily moved to min(900px, 55vw):

Test Files 2 failed (2)
Tests 2 failed | 4 passed (6)

Both failures are half 2 this time; both half 1 and the controls stayed green — so half 2 really measures the drawer's default rather than restating half 1.

Both legs ran under a trap … EXIT INT TERM with absolute paths and were restored with git checkout HEAD -- <path>. Restoration is proved by byte identity, not by an exit code: git diff HEAD empty, and each file's git hash-object back to the HEAD blob recorded before the mutation (ace66b96…, 2592c8aa…, da15724a…).

Gates — each gate's own printed verdict, exit code captured before any pipe

Measured at d8814f129. The commit after the source commit adds .changeset/*.md and nothing else (git diff 087514d61 d8814f129 --stat = 1 file), so the suite and lint runs measured a byte-identical packages/**.

gateexitits own verdict line
vitest run packages/plugin-kanban/ packages/plugin-calendar/0Test Files 34 passed (34) · Tests 210 passed (210)
the two new pin tests, at d8814f1290Test Files 2 passed (2) · Tests 6 passed (6)
turbo run lint (whole repo, not narrowed)0Tasks: 47 successful, 47 total
type-check (both packages)0packages/plugin-{kanban,calendar} type-check: Done
turbo run build (dependency closure)0Tasks: 15 successful, 15 total
check-changeset-presence0✅ … declares 1 changeset(s)
check-changeset-no-major0✅ No changeset declares a major bump.
check-changeset-fixed0✅ All workspace packages are in the changeset fixed group.
check-changeset-overwrite0✅ No pre-existing changeset was modified or deleted.
check-control-bytes0✅ OK (scanned 5435 tracked text file(s); skipped 85 binary).
check-vi-mock-specifiers0✅ OK (… 694 bare (out of scope) …)
check-lint-coverage0✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
check-type-check-coverage0✅ type-check coverage: 45/46 via type-check …
check-package-self-import0✅ No package names itself inside its own src/.
check-phantom-dependencies0✅ Every in-scope import is declared by the package that publishes it.

The type-check green is proved to cover the new tests rather than merely coexist with them: tsc -p tsconfig.test.json --listFiles finds each new file as a program input (1 hit each). The two new files add 3 @typescript-eslint/no-explicit-anywarnings each — the same rule the gantt pin test triggers; lint.yml:20 deliberately sets no --max-warnings, and the error count is 0.

Changeset

.changeset/kanban-calendar-nav-width-converge.md, empty frontmatter, matching what #6305 used for the gantt half. check-changeset-presence.mjs reports that as "declared as releasing nothing, which is the explicit exemption and a complete answer to this gate". The body says the change is zero-pixel rather than inventing a behaviour claim.

Out of scope, deliberately

One check the card did not ask for: a repo-wide sweep for a fifth divergent renderer found none. All three navConfig defaults now read { mode: 'drawer' }, and plugin-view/src/ObjectView.tsx (:1901, :1958) already passes navigationConfig?.width with no literal fallback, so it delegates too. plugin-tree and plugin-map use useNavigationOverlay but spell no width. No new issues filed.


Generated by Claude Code

…dth as a renderer default
`min(960px, 60vw)` was a house default duplicated across the record-navigation
drawers of three renderers. #6305 converged ObjectGantt only; kanban and
calendar still carried the old shape at two sites each — the `navConfig`
default and a render-site `?? 'min(960px, 60vw)'` fallback, the second of which
would have kept the old width alive had only the first been taken.
`width` is `@deprecated [#2578 -> size]` and `resolveOverlayWidth` gives an
explicit `width` priority OVER `size`, so spelling it kept the deprecated
branch load-bearing on the path most boards and calendars take (no declared
`navigation`) and made the size buckets unreachable there. Omitting both leaves
`resolveOverlayWidth` returning `undefined`, which is what RecordDetailDrawer's
own `width` default is for; that default is the identical `min(960px, 60vw)`,
so this is a zero-pixel change on every viewport.
Not converged on `size: 'lg'`: that bucket is `min(92vw, 960px)`, up to 53%
wider below a 1600px viewport. That move is a real behaviour change and stays
open for a human ruling.
Pinned by ObjectKanban.navWidthDefault.test.tsx and
ObjectCalendar.navWidthDefault.test.tsx, both halves each, because the
equivalence now depends on the drawer's own default too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
…s releasing nothing
Empty frontmatter, matching the precedent the gantt half of this convergence
set: the resolved overlay width is unchanged on every viewport, so there is no
published behaviour to describe. `check-changeset-presence.mjs` names the empty
frontmatter as the explicit exemption and a complete answer to the gate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3235.1 KB3266.6 KB
Main entry chunk (gzip)157.0 KB350 KB
Entry fileindex-B01-da7x.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.71KB4.46KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.58KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.74KB27.50KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.85KB20.79KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)63.21KB21.05KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@os-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT on the diff. ⏸ Landing held until every check is green — 20 success / 3 skipped / 6 still running (4 test shards, Type Check, Lint), zero failures at d8814f129fdb. Enqueue qualification here is every check green on the reviewed head, not the required subset, so the ready-toggle waits.

The two ⛔ exclusions, verified from the file list rather than from the report

exclusionverified
packages/plugin-detail/src/RecordDetailDrawer.tsx untouchednot in the PR's file list at all
no move to size: 'lg'absent from the diff, and both new comments say so in terms: "Deliberately NOT converged on size: 'lg' either … That move is a real behaviour change and stays open on #6303 for a human ruling."

That second one is better than compliance: it carries the open half forward in the code, naming the card that owns it, which is the same convention ObjectGantt.tsx uses. The next person to read either file learns that the absent width is deliberate and where the remaining decision lives.

All four sites converged, both copies in each file — kanban navConfig and render site, calendar navConfig and render site. Taking only the first of each pair would have changed nothing, since the render-site ?? reproduces the same string by another route; the PR takes both and the test headers say why.

Why the test design is the strongest part

  • The mock delegates to the REAL drawer (return <Real {...props} />) rather than stubbing it, so half 2 measures the width the drawer actually resolves instead of a stub's idea of it.
  • The two halves fail for different reasons, and the reverse verification proves it — direction predicted before running. Leg A (restore the pre-fix expressions) reds only half 1 in each file, because half 2 cannot see a re-added ?? fallback: its value is the same string the drawer default produces. Leg B (move the drawer's own default to min(900px, 55vw)) reds only half 2. Two legs, disjoint failure sets, controls green throughout — that is what makes "both halves" a real claim rather than a restatement.
  • A masking trap was found and closed: the drawer prefers a drag-resized width persisted in localStorage, keyed by objectName, which would have masked half 2 from a value left by an earlier case in the same file. beforeEach clears it, with the reason written down.
  • The instrument is described honestly: happy-dom's CSS parser drops the width longhand for a min() expression but keeps max-width, so the assertion reads max-width — and the comment states that this is the same resolved string, not a proxy for it.
  • The header pins the standard directly: "All three assert the resolved width VALUE — never a className, never 'it renders', either of which passes in both worlds."

Restoration after both ablation legs was proved by byte identitygit diff HEAD empty andgit hash-object back to the HEAD blobs recorded before each mutation — not by an editor's exit code, under a trap on EXIT INT TERM with absolute paths.

Independently checked by this seat

The escalation clause

It was checked rather than assumed, and correctly did not fire: both renderers import RecordDetailDrawer and render it directly with a width prop exactly as the gantt does; resolveOverlayWidth returns undefined and never null, so the drawer's default parameter does apply; and navigation.width has no other consumer in either file. Zero-pixel established rather than asserted.

Every line number in the dispatch matched main exactly, so nothing in the order needed correcting — worth recording, since the last three orders this seat wrote all did.


Generated by Claude Code

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.

min(960px, 60vw) is a four-site house default, not one renderer's choice — the width/size question on #6259 decides all of them

2 participants

@os-support-ai@claude