…lRenderer read the field width through resolveFieldScale (objectui#11254)
The gantt tooltip's number / integer / float / decimal row handed
formatNumber an undefined width, so its two-place parameter default
decided for every field. The row now resolves the width with
resolveFieldScale and renders through formatDisplayNumber: a resolved
width fixes both bounds; undefined (no protocol row for these types) is
the value's natural precision, the same two arms NumberCellRenderer
spells. Grouping is unchanged on this row.
NumberCellRenderer read scale with typeof === 'number', which let a
malformed 1.5 through for Intl to floor and -1 through for Intl to
throw on. It now reads resolveFieldScale, whose door is the record
validator's own; well-formed and absent declarations render as before,
and the resolved value feeds the grouping policy the same way.
Two existing gantt pins rode on the retired two-place constant:
ObjectGantt.test's undeclared effort moves to `12`, and the
numberLocale fixture declares scale 2 so its locale expectations keep
their bytes.
Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37
Co-authored-by: Claude <noreply@anthropic.com>
Fixes #11254
Clause-②: no
The two same-family carriers the director's ruling (card comment 5933283974) left on this card now read the field width through
resolveFieldScalefrom@objectstack/spec/data(ruling A′, objectstack-ai/objectstack#19628): the gantt tooltip's numeric row andNumberCellRenderer. Out, as ruled:formatPercent'sprecision = 0default,progress,GroupRow'stoFixed(2), and the tile'ssum/avgover an undeclarednumber(ruling A, deferred to its trigger). None of them is touched.What changes
packages/plugin-gantt/src/ObjectGantt.tsx,formatFieldValue, thenumber/integer/float/decimalcase. It calledformatNumber(Number(value), undefined, displayLocale). TheundefinedtookformatNumber's two-place parameter default, so every numeric row read two decimals whatever the field declared. The row now resolvesresolveFieldScale({ type: def?.type, scale: def?.scale })and renders throughformatDisplayNumber(from@object-ui/i18n, already a dependency) in the two armsNumberCellRendereruses. A resolved width fixes both bounds.undefinedmeans no fixed width: minimum 0, maximum 20, the value's natural precision.formatNumberis no longer imported here. Width only: noscalereaches the grouping policy on this row, so it groups exactly as before (see Acceptance notes).packages/fields/src/index.tsx,NumberCellRenderer.typeof numField.scale === 'number' ? numField.scale : undefinedbecomesresolveFieldScale({ type: numField.type, scale: numField.scale }). TheminimumFractionDigits: scale ?? 0/maximumFractionDigits: scale ?? 20pair becomes the same explicit two-arm spread, so no?? Nspelling sits beside the resolver. A well-formed or absent declaration renders byte-identically. The resolved value feedsformatDisplayNumber'sscale(the grouping-policy input) exactly as the raw one did.ObjectGantt.test.tsx,resolves and formats tooltipFields per record:effortdeclares noscale, so its expectation moves fromEffort=12.00toEffort=12. A comment beside it names the ruling.ObjectGantt.numberLocale.test.tsx: that file measures the locale, not the width. Itsqtyfixture now declaresscale: 2, so every locale expectation (1.234,50,1,234.50) keeps its bytes through the declared path. The width itself is pinned in the new file.packages/plugin-gantt/src/ObjectGantt.numberResolvedWidth-11254.test.tsx(6) andpackages/fields/src/__tests__/NumberCellRenderer.resolvedWidth-11254.test.tsx(6).patch):.changeset/11254-gantt-number-resolved-width.md(@object-ui/plugin-gantt) and.changeset/11254-number-cell-resolved-width.md(@object-ui/fields). No manifest moves. Both packages already declare@objectstack/spec^17.5.0and already importedresolveFieldScaleonmain.Premises measured first (the dispatch's Zone 2)
Gantt row: confirmed. At base
7728c67c86the case returnsformatNumber(Number(value), undefined, displayLocale).formatNumber(value, decimals = 2, locale)setsminimumFractionDigitsandmaximumFractionDigitsboth todecimals, soundefinedpads to two places. It cannot express "no fixed width": every input fixes both bounds to one number. The existing helper that can isformatDisplayNumber, whichformatNumberandNumberCellRendererboth call. No new helper and no new export.The resolver, read in the installed
@objectstack/spec17.5.0 (dist/data/index.mjs,resolveFieldScaleandABSENT_SCALE_BY_TYPE). A declaredscaleis returned whenNumber.isInteger(scale)and it is at least 0. Otherwise the type's row applies, and the table has one row,percent: 0. Sonumber,integer,floatanddecimalanswer the declared width when well-formed andundefinedotherwise. A malformedscale: "2",1.5or-1answersundefined. There is no clamp, so101comes back verbatim;FieldSchemarefuses it at authoring. Theintegeranswer isundefined, so the ganttintegerrow changes too: a stored7read7.00and now reads7.NumberCellRenderer: confirmed, with a correction to the example. It differs from the resolver only for a malformed declaration that is still a JS number. The string"2"is not such a case:typeof "2"is not'number', so both reads answerundefined, and it is pinned as a control. The cases that differ are:scale: 1.5:Intlfloors it, so3.14159read3.1;scale: -1:IntlthrowsRangeError: minimumFractionDigits value is out of range.and takes the row down.NaNbehaves the same way.Grouping: every malformed value is not
0, so the old read grouped, and the resolver'sundefinedgroups too. A declared0stays0and renders ungrouped. Both halves are pinned as controls.Pins: red first, then green
Red at
cee8642(pins only, base source):pnpm exec vitest run --maxWorkers=2over the two new files gaveTests 7 failed | 5 passed (12), VERDICT command-exit 1, and every direction matched the one predicted in the file headers:scale: 3:Expected "1.500" Received "1.50";scale: 0:Expected "2" Received "1.50";Expected "1.5" Received "1.50";integer:Expected "7" Received "7.00";"2":Expected "1.5" Received "1.50";scale: 1.5:Expected "3.14159" Received "3.1";scale: -1:RangeError: minimumFractionDigits value is out of range.The green ones were the gantt
scale: 2row (the old default equals the declaration) and the cell controls ("2",2, absent, and thescale: 0/ absent grouping pair).Green at
b4f818f: the two new files plusObjectGantt.test.tsx,ObjectGantt.numberLocale.test.tsxandObjectGantt.percentResolvedWidth-11254.test.tsxgaveTest Files 5 passed (5),Tests 55 passed (55), VERDICT command-exit 0.The gantt rows also assert agreement with the real
NumberCellRendererfor the same value and field, beside absolute bytes. The vitest config aliases@object-ui/*tosrc, and neither package had adistat the time, so both legs measured source. The red leg is the before-state of the ablation: the same pins, with no line of the fix present.Gates, all at head
b4f818fpnpm --workspace-concurrency=2 --filter '@object-ui/plugin-gantt^...' --filter '@object-ui/fields^...' buildbuiltScope: 13 of 47 workspace projects, VERDICT command-exit 0. Then--filter @object-ui/plugin-gantt --filter @object-ui/fields build, VERDICT command-exit 0.type-check:--filter @object-ui/plugin-gantt --filter @object-ui/fields type-checkechoedpackages/fields type-check: Doneandpackages/plugin-gantt type-check: Done, VERDICT command-exit 0.tsc -p tsconfig.test.json --listFilesOnlylists both new pins and both moved files.pnpm exec vitest run --maxWorkers=2 packages/plugin-gantt/ packages/fields/gaveTest Files 325 passed | 2 skipped (327)andTests 4291 passed | 121 skipped (4412), VERDICT command-exit 0 (shared box).check-spec-range-floors.mjsanalyze, scoped to the two packages over freshdist, gavefindings 0. Control: the same run with both floors at^17.0.0gavefloor-too-lownamingresolveFieldScalefor both packages (3 findings; fields addsInlineGridColumn).--format json) gave 0 errors. Warnings on the four edited files equal BASE through--stdin:fields/src/index.tsx138/138,ObjectGantt.tsx81/81,ObjectGantt.test.tsx30/30,ObjectGantt.numberLocale.test.tsx5/5. The new pins carry onlyno-explicit-any(2 and 6). Narrowing proof: (i) the population is the 6 changed lintable files, and none is ignored byeslint.config.js; (ii) the JSON output holds 6 results; (iii)eslint.config.jssets noparserOptions.projectorprojectService, so type-aware linting is off and this diff cannot move an untouched file's verdict.check-changeset-presence(6 source file(s) of 2 released package(s) changed, and this change declares 2 changeset(s)),changeset:check,check:control-bytes,check:new-line-citations(VERDICT new-cross-file-line-citations: 0 new citation(s)),check:pending-changeset-literals,check:phantom-deps,check:unused-deps,check:spec-symbols,check:unreferenced-sources,check:test-path-roots,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape, andcheck:changeset-claims(report-only; see Acceptance notes).pnpm check:spec-floors(reason: it needs every package'sdist, and only the two changed packages and their closure were built; the scoped run and its control are above), and the full lint and test farm (reason: CI owns it).Acceptance notes
scale: 0, objectui#4033, and the authoreduseGrouping, objectui#11026). Ascale: 0year reads2,026in the tooltip and2026in the list cell. That was already so before this PR (it read2,026.00). It is a different class from this card's width, so it is left as is and recorded here. No first-partytooltipFieldsproducer over anumberfield was found. Carrier: none..changeset/10071-number-scale-ceiling.mdsays "In-range, negative and non-integer widths are unchanged". That is true of the change it describes (the retired clamp). It ships in the same release as this PR's changeset, which says those widths now count as no declaration in the number cell. The two read together without contradiction, but the seat may add a dated note there if it judges the release text ambiguous. Not edited here.scaleabove 100 path is not changed: the resolver returns it verbatim, andFieldSchemarefuses it at authoring.v22.22.0, andjsdom@30.0.1declares^22.22.2. The root.npmrcsetsengine-strict=true, so this worktree was installed withpnpm install --config.engine-strict=false. That is a one-off override on the command line; no tracked file changed.Implemented by the
domain:uiseat 1 dev, branchclaude/issue-11254-number-faces-resolver, sessionhttps://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37.Generated by Claude Code