Uh oh!
There was an error while loading. Please reload this page.
feat(types): declare fitContent on TableColumn (#6424) - #6673
Conversation
`data-table` skips `fitContent` columns in the auto-width pass and renders them as a `width:1%` + nowrap cell with no overflow clamp, but the published declaration refused the key: a typed author got a compile error, and `TableColumnSchema.parse` silently stripped the flag, while an untyped producer's identical key worked. Declare it — interface + `TableColumnSchema` mirror (`z.boolean()`, typed rather than waved through) + the `StaticTableColumn` tombstone under #5474's lockstep rule — in the shape #6615 used for `headerIcon`. Retire was excluded by measurement: shipped source authors the key on the row-actions column. The two `(col as any).fitContent` casts drop with the declaration; they were redundant at compile time already (`col` is `any` via the file's own normalization), so that removal is bookkeeping, not the fix. Part of #6424 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
✅ Console Performance Budget
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
Size Limits
|
os-sales
commented
Aug 28, 2026
✅ ACCEPT — flipped ready, auto-merge onReviewer: Gate reading: all 29 check runs read, returned count compared against ⏱ This PR sat green for ~35 minutes before being flipped: this seat hit the GitHub API rate limit at 13:05Z and backed off rather than retrying. The delay is the reviewer's, not the change's. Measured, since it is now twice: the window is ~33 minutes (12:17Z→12:51Z, 13:05Z→13:38Z). ⭐ The best decision here is a deliberate departure from the precedent it was told to followThe dispatch said to use PR #6615's shape for
Copying the precedent wholesale would have re-introduced exactly the leniency this card family exists to remove: accepting the key while declining to say what it is. Precedent followed where it applies, departed from where its reason does not. The cast claim was proven, not taken on the reviewer's wordThe order asserted the two ⇒ Compile-time no-op, visible only at the lint layer ( The freshness caveat, observed rather than recitedAblation A deletes only the static tombstone and produces The check I did not think to ask forA published-type widening can silently move a derived tombstone band. Measured: Serial constraint: fenced, correctly, and the line number was correctedPR #6670 was verified open, The remainder — ruled and scheduled, not forgotten
Generated by Claude Code |
Part of #6424
Declares
fitContentonTableColumn— the second of this card's two keys, inthe exact shape PR #6615 used for
headerIcon. Maintainer ruling 2026-08-28(live director session, summon #2, batch #4 item 5), presented with
recommendation A; ruling verbatim: 「同意」.
Part of, notFixes: the card's second item (theObjectGridColumnHolds.headerIconhold) is measured and reported below, notacted on — removing a hold is an adjudication and the seat rules it. A
Fixeswould take that unsettled half into closure.
The declaration, in all three places (#6615's shape)
headerIcon(#6615)fitContent(here)TableColumninterface?: React.ReactNode?: booleanTableColumnSchemamirrorz.any().optional()z.boolean().optional()StaticTableColumntombstone?: never?: neverStaticTableColumnSchematombstonez.never().optional()z.never().optional()One deliberate divergence from the mirrored shape:
headerIconis a renderedReact node, so its mirror is
z.any()and its only job is to pass the nodethrough.
fitContentis serializable metadata, so the mirror types it —z.boolean(). Declaring it asz.any()would have bought acceptance withoutvalidation, the lenient face #5853 removed for
type. A non-boolean is now aloud refusal naming the key, pinned below.
The static side tombstones it under #5474's lockstep rule. Measured, not
assumed: the static renderer's every
col.KEYread isheader,accessorKey,className,cellClassName,width—fitContentscores 0there against a positive control of
accessorKey5 in the same query shape. Ithas no auto-width pass to opt out of and no per-cell overflow clamp to lift.
Lockstep counts move 5 live + 13 tombstones -> 5 live + 14.
The authoring surface — what actually changes
The renderer already honoured the key; the published declaration refused it. A
typed author writing
{ accessorKey: '_actions', fitContent: true }got acompile error for a key
data-tableimplements, andTableColumnSchema.parsesilently stripped it — while an untyped producer's identical key worked.
Both halves are pinned positively:
static-table-narrow-surface.test.ts) — rich accepts, static refuses.data-table-declared-column-keys.test.tsx) — the flag survivesparse(result.data.fitContent === true), not merely "parse succeeded".Acceptance cannot pin this: a non-strict
z.object()accepted this exactinput before the declaration, while the flag vanished and the row-actions
column fell back to the 80px floor that clipped its buttons.
The freshness caveat, handled
At a fresh object literal an undeclared key and a
?: nevertombstone areindistinguishable — both are tsc errors, by excess-property checking. So a
fresh-literal pin measures freshness and declaration together. Each side
therefore also carries a non-fresh route:
non-fresh value are structurally fine, so an undeclared key would be
accepted and the directive would go unused. Only a real tombstone refuses it.
column.fitContent), which compiles only ifTableColumndeclares the key, whatever the value's freshness.Equal< TableColumn['fitContent'], boolean | undefined >andEqual< StaticTableColumn['fitContent'], undefined >.Ablation A — delete the static tombstone only, rebuild,
tsc -p tsconfig.test.json(mutation proven on disk: anchored count 1 -> 0, blob hashd8c5f4db->a141c9d4):Line 381 is the non-fresh directive. Line 360 — the fresh-literal directive
— does not appear: it stayed "used", i.e. a fresh literal cannot tell a
tombstone from mere absence. That is the caveat, measured.
Ablation B — delete the rich declaration only (hash
d8c5f4db->911c554e): the freshness-free read fires,static-table-narrow-surface.test.ts(391,45): error TS2339: Property 'fitContent' does not exist on type 'TableColumn',alongside the key-list exhaustiveness pins at 142/147.
Both ablations restored under a
trap … EXIT INT TERMwith absolute paths andan explicit
git checkout HEAD -- ABSOLUTE_PATH; restoration proven byobservation — blob hash back to the HEAD blob and
git diff HEADempty.The two
(col as any).fitContentcasts — bookkeeping, NOT the fixDropped, per the ruling. But this PR does not claim them as the change:
Ablation C — remove the declaration from src and dist (dist proof:
fitContent?: boolean;inpackages/types/dist/data-display.d.ts= 0) whilekeeping the casts removed, then type-check
@object-ui/components:TC_EXIT=0, green. Socolis alreadyanyat both sites — widened by thefile's own
col: anynormalization — and the cast removal is a no-op atcompile time today, independent of the declaration. It becomes load-bearing
only once those render callbacks are typed, which is the standing instrument
gap and is not closed here.
Visible only at the lint layer:
as anyindata-table.tsx6 -> 4.family — the normalization is.
Serial constraint: FENCED (not folded)
The emit-side cast travels with this verdict and needs no new ruling, but it
needs the file free. PR #6670 (#6458) is still open — checked at
implementation time:
state: open,merged: false. Sopackages/plugin-grid/src/ObjectGrid.tsxis untouched here; this PR's diffcontains no plugin-grid file.
ObjectGrid.tsx:3418. Measured on this ref it is:3587—if ((col as any).fitContent) continue;. The card's number is stale, as itssibling's was.
Second item: the
ObjectGridColumnHolds.headerIconhold — MEASURED, NOT ACTED ONReading only; the hold is left exactly as it is.
The hold is redundant, not load-bearing. Three measured facts:
headerIconIS a member ofkeyof TableColumn(landed by DeclareheaderIcononTableColumn(interface + zod mirror + static tombstone); measure thefitContentauthor surface #6615), so bothObjectGridColumnDraft(viaOmit< TableColumn, 'type' >) andObjectGridColumn(viaTableColumn) already contributeheaderIcon?: React.ReactNode.headerIconis not aListColumnmember —ListColumndeclaresfield label width align hidden sortable resizable wrap type pinned summary(0 hits for
headerIcon, positive controlwidth1). So it was never inthe derived
RetiredListColumnKeyband, and the holds member was neverrescuing it from a tombstone.
ObjectGridColumnHolds.headerIcondeclares the identical type,React.ReactNode.⇒ The intersection is idempotent (
A & A = A): removing the member changesneither emit type.
No second road. The distinguishing check #5453 demonstrated, applied here:
ObjectGridwritesheaderIconat three sites (all underschema.showColumnTypeIcons,getTypeIcon(...)) anddata-tablereads it atone (
{col.headerIcon && …}into the header cell). Forwarded verbatim, neverre-expressed. Contrast
pinned, which stays load-bearing on both counts —it is undeclared by
TableColumn(so the holds member is the only thingdeclaring it on the emit type) and
ObjectGrid's own reorder pass consumesit first and re-expresses it as the sticky
className.silent-expiry shape #6425 recorded for
optionsand this seam's own docblockwarns about ("a pin enforced by a key's non-membership silently stops enforcing
the moment the key becomes a member").
Caveat, stated rather than papered over: this reading is a type-algebra
derivation from three mechanically measured facts, not a tsc ablation — the
ablation would require editing the fenced
ObjectGrid.tsx. The one-line probethat would settle it mechanically, for the round that owns the file:
Equal< ObjectGridColumn['headerIcon'], TableColumn['headerIcon'] >.Also false as written today, and untouched here: the docblock's general claim
that the held keys are "undeclared by
TableColumn". #6651 already correctedthe one sentence it was editing; the rest is the seat's to rule.
Gate verdicts (exit code captured before any pipe; each gate's own verdict line)
Heavy work serialized through the shared verify lock; verdicts read from the
lock's own
VERDICTline, never a bare$?.d431b5ddTest Files 90 passed (90) / Tests 887 passed (887)—VERDICT command-exit 0@object-ui/typesbuildVERDICT command-exit 0VERDICT command-exit 0(needed for the two population gates below)@object-ui/typestype-checktsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json—VERDICT command-exit 0Scope: 3 of 47 workspace projects, eachDone—VERDICT command-exit 0@object-ui/types+@object-ui/components)927 problems (0 errors, 927 warnings)—VERDICT command-exit 0changeset:checkcheck:control-bytescheck:spec-symbolscheck:phantom-depscheck:self-importcheck:esm-specifierscheck:shell-escape-residuecheck:doc-typescheck:published-dist✅ No published package's build output carries tooling material.check:readme-exports✅ check-readme-exports: OK (… 0 unbuilt …)check:readme-exportsfirst returned exit 1 with❌ the population COLLAPSED -- this run proves nothing / packagesRead: found 14, floor is 25.That is NOT MEASURED, neither green nor red — so the tree was built as the
gate names and it was re-run; the green above is from the real population (37
of 40 packages, 0 unbuilt, 378 self-imports judged vs 87 in the collapsed run).
@ts-expect-errordirectives are real enforcement here —packages/typestype-checks its tests via
tsconfig.test.json, proven with--listFiles: theedited test file appears in that compilation (1 hit, among 65 test files), so a
re-widening fails the build on an unused directive.
Declared narrowing
Repo-wide
turbo run lintand the full vitest farm were not run locally;CI runs the farm exactly once regardless. The lint narrowing is a measurement,
not a skip, and all three pieces of evidence are present:
lintisturbo run lint, i.e. per-packageeslint .units. Only the two editedpackages' units can move.
--format json:packages/types127 files,packages/components415 files — both units run in full, 542 files.(0 matches for
projectService/parserOptionsineslint.config.js), sothis diff cannot move the verdict on any file it did not touch.
Gates skipped with reason:
check:vi-mock-specifiers(novi.mockin diff),i18n / skills / designer / action / icon / doc-snippets / doc-fences gates (no
such surface in the diff — no
skills/**file is touched, so no skills-budgetreadings are owed).
Scope
Changeset:
@object-ui/typesminor (published-type widening, Clause-2 yes).Out of scope and untouched: #6004's other held keys
(
options/pinned/wrap/essential/size); thenamealias, keptdeliberately per #5120;
@objectstack/spec—TableColumnis objectui's owntype,
ListColumnis the spec's, and this PR touches only the former.Generated by Claude Code