`formatDateTime` gains a named `'compact'` style — today's
`DateTimeCellRenderer` face, byte-identical — and the cell renders through it
instead of inlining its own pair of `Intl` option bags. The cell also reads
`field` (it destructured `value` only), so `field.format` selects a datetime
style the way it already did for `date`. `data-table`'s `formatCellValue`
calls the same function for its datetime branch instead of a third,
independently authored bag.
`formatDateTime`'s signature is now `(value, style?, options?)`, matching
`formatDate`; the `options` added in objectui#4272 moved to position three and
every call site moved with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Fixes #7443
Implements the maintainer ruling of 2026-09-04 (decision batch #29), option 1' — comment 5536193583. One home for the
datetimedisplay convention:formatDateTimegains a named'compact'style that is today's cell face byte-for-byte, the cell renders through it, and the third independently authored spelling converges onto it.The three items
formatDateTimegains'compact'. It is the dense grid face —7/4/2024 7:00 aminen-US— thatDateTimeCellRendererused to build from its own inlined pair ofIntloption bags. Thehour12note moved with it. The default style is untouched and still rendersJul 4, 2024, 07:00 AM.DateTimeCellRendererreadsfield. It destructuredvalueonly, so adatetimefield had no way to reach the style vocabulary adatefield has. It now selectsfield.formatand renders through the shared function, sofield.formatworks fordatetimethe way it already did fordate.formatCellValueindata-table.tsxcallsformatDateTime(default style) for its datetime branch, instead of a thirdIntl.DateTimeFormatbag written independently of the shared one.The data-table before/after — measured, and it did not move
The ruling required this measured either way, so here it is either way. Before is the option bag
formatCellValuebuilt for itself onorigin/main; after is what the table renders now.en-USJul 4, 2024, 07:00 AMJul 4, 2024, 07:00 AMzh2024年7月4日 07:002024年7月4日 07:00de-DE4. Juli 2024, 07:004. Juli 2024, 07:00No pixel changed, because the bag
formatCellValueused to build is character-for-character the bagformatDateTime's default branch builds.data-table-datetime-convention-7443.test.tsxis that measurement rather than a claim: it keeps a verbatim copy of the former bag and asserts the rendered cell equals it, so the two cannot silently diverge again either.formatCellValuealso handles ISO strings with noT.formatDateTimealways carries a time andformatDate's default drops the year inside the current year, so routing that branch through either would move a pixel (Jul 4, 2026becomesJul 4on every current-year date). #7443's subject is the datetime convention; the date-only divergence is filed separately as #7620 rather than folded in here.formatDateTime's signatureThe ruling specifies the call
formatDateTime(value, field?.format ?? 'compact', { locale, t }).formatDateTimedid not have a style slot — it was(value, options?), one parameter short offormatDate— so implementing the ruling's call shape means the signature is now(value, style?, options?), matchingformatDateexactly.The
optionsparameter added in objectui#4272 therefore moved from position two to position three.formatDateTime(v, { locale })becomesformatDateTime(v, undefined, { locale }). Every in-repo call site moved with it (2 runtime, 8 assertions across 3 existing pin files); a repo-widetype-check(81/81 tasks) confirms none was missed.For a published consumer: TypeScript rejects the old form at the call site, but a JavaScript caller that does not move the argument silently loses its locale — which is precisely the objectui#4272 defect. That is called out in the changeset. Marked
minorper this repo's fixed-group rule that a breaking change is described rather than majored.The symmetry is the point, and it is the answer to the "fourth author copies the nearest spelling" axis:
formatDateandformatDateTimeare now callable the same way.Two judgement calls, stated rather than buried
The compact cell keeps its two-tone DOM. The cell paints the date and the time in two spans, the time muted and offset by
ml-2. Rendering the joined string in one span would have kept the text identical while changing what every datetime cell in the product looks like — which is the one thing the ruling's byte-identity requirement exists to prevent. So the compact branch asks the shared module for the two halves (formatDateTimeCompactParts) andformatDateTime(value, 'compact')is defined as those halves joined by one space. There is still exactly one home: both come out of the same function, and a pin asserts the join.||, not??, for the style default. The ruling writesfield?.format ?? 'compact'.||is used instead, matching thedatecell one function up: with??, an authored empty-stringformatwould fall through to the verbose face and visibly change that cell.||keeps it on the compact face. A pin covers it.Verification
Run from the repo root, on
460232c3:pnpm exec vitest run packages/core/ packages/fields/ packages/plugin-gantt/ packages/plugin-dashboard/ packages/components/src/__tests__/data-table— 409 files, 5827 tests, all passing.pnpm exec turbo run type-check --concurrency=2— 81/81 successful, repo-wide; this is the net under the signature change.pnpm exec eslintover the 10 changed files — 0 errors (269 pre-existing warnings, allno-explicit-any/react-refreshon untouched lines;.github/workflows/lint.ymlsets no--max-warnings).check:control-bytes,check:self-import,check:phantom-deps,check-changeset-presence— all pass.check:readme-exportsis not measured locally: it needs every package'sdist/**on disk and only 16 of 40 were built, so its findings are all "runpnpm buildfirst" plus onepackagesReadfloor. Zero of its findings namecore,fieldsorcomponents. CI builds all.Ablation — both legs rebuilt nothing because
vitest.config.mtsaliases@object-ui/coretopackages/core/src, so the mutation is in the resolution path directly; each leg proved the edit landed on disk by counting the injected and the deleted text, and each restore is verified bygit hash-objectagainst theHEADblob.({ value }), no field accessfield.formatcases go red, the function-level compact cases stay green'compact'branch fromformatDateTimeen-USliteral, the join identity, and "the default is not the compact face"Pins added
packages/fields/src/__tests__/datetime-compact-style-7443.test.tsx—'compact'is byte-identical to the former inlined bags inen-US,zhandde-DE; theen-USliteral7/4/2024 7:00 amfrom the card; the default style unchanged; the cell's two-tone DOM unchanged;field.formatlive fordatetime; thedatecell path untouched.packages/components/src/__tests__/data-table-datetime-convention-7443.test.tsx— the before/after table above, asserted against a verbatim copy of the former bag; the date-only branch pinned unchanged.Expectations are computed from the former option bags rather than typed as literals, so an ICU or Node upgrade moves both sides together and the pins keep measuring drift from the former face instead of the age of the runner.
Housekeeping
no.@object-ui/fieldsgains an accepted style value and one new exported helper; no published contract key moves and no spec name moves. TheformatDateTimeparameter reordering is called out above — it is a source-compatibility break, not a contract key.plugin-list/src/ListView.tsxanddata-table.tsxas held by other PRs whendatetimehas two display conventions:DateTimeCellRendererinlines its own and never callsformatDateTime#7443 was filed. That is no longer true: all 6 open PRs were checked file-by-file (7608 skills docs, 7606check-governed-queue-guard.mjs, three dependabot bumps, one release PR) and none touches this surface.plugin-list/src/ListView.tsxis not touched by this PR either.formatDateTime, with different signatures and different faces —@object-ui/reactre-exports one,@object-ui/fieldsthe other #7619 (two published functions namedformatDateTime—@object-ui/reactre-exports i18n's,@object-ui/fieldsre-exports core's, and their faces differ by one leading zero) anddatehas the same two-convention splitdatetimejust had:data-table's date-only cell keeps a year thatformatDatedrops #7620 (thedateconvention has the same split, indata-table's date-only branch).🤖 Generated with Claude Code
Generated by Claude Code