This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
Commit 2fb0f9a
fix(plugin-timeline): a gantt-variant timeline draws its headers and bars on one continuous axis (objectui#11079) (#11109)
Fixes #11079
Clause-②: no
## What was wrong
A `variant: 'gantt'` timeline built its header row and its bars from two
different constructions in `packages/plugin-timeline/src/renderer.tsx`:
- `generateTimeScaleHeaders` drew one equal-width `flex-1` column per
unit, from the minimum date to the maximum date inclusive;
- `calculateBarDimensions` placed each bar as a percentage of the span
from the minimum date to the maximum date, which ends at the START of
the last column.
So the bars sat on a scale one column narrower than the headers. On a
day axis over October 5th to 7th, the bar starting on the 6th was drawn
at `left: 50%` under a column that starts at one third.
## What this changes (triage `5888963850`: one continuous axis)
- **One axis function.** The new module-private `timelineAxis(scale,
minDate, maxDate, locale, t)` returns `{ start, end, units: [{ start,
end, label }] }`. The axis is `[start of the first unit, end of the last
unit)`: the first unit contains `minDate`, and the last unit contains
`maxDate`.
- **One placement function.** `placeOnAxis(axis, start, end)` returns `{
start, width }` as percentages of the axis span. Each header cell's
`width` and each bar's `left` / `width` come from this call. There is no
second construction.
- **Each header is as wide as its unit's real length.** `hour` steps a
fixed hour. `day`, `week`, `month`, `quarter` and `year` step with local
setters from a local midnight. So day and hour columns are equal, a
month is as wide as its days, and a DST day is 23 or 25 hours wide.
- **Units snap to their boundary.** The old walk stepped from `minDate`
itself. So a month axis from January 15th to April 10th had no April
column, and one from January 31st skipped February. `week` still counts
from the axis's first day, as its `Week N` labels always did.
- **The inverted-range refusal is the axis's own refusal.** The branch
used to run a separate greater-than test "deliberately the same test
`generateTimeScaleHeaders` makes". It now refuses when `timelineAxis`
builds no axis: the same read.
- **Dates are still read through `readGanttDate`**, the objectui#10866
slice-3 invariant. No date read changes.
- `generateTimeScaleHeaders` keeps its exported signature and now
returns the axis's labels. No export, prop, type or schema key is added
or removed.
- The header cells' `flex-1` becomes `shrink-0 min-w-0 overflow-hidden`
plus an inline percentage `width`. That keeps a long label from widening
its column off the bars.
## Pins
New:
`packages/plugin-timeline/src/__tests__/TimelineGantt.oneAxis-11079.test.tsx`,
with 25 cases.
- For each of the six spec scales, on a pinned three-unit range:
- a bar starting at the second unit's start has `left` equal, as a
string, to the first header's `width`;
- that bar is exactly as wide as the second header;
- a bar spanning the last unit ends at 100% (`left + width`, to 10
decimal places);
- the header widths match each unit's length.
- `month` across a 28/31-day boundary (January 2026, then February): the
widths are 31/90 and 28/90.
- The card's own computed day axis: the Oct 6 bar sits at one third, not
50%.
- The three unit-boundary cases above.
- The schema catalog's `gantt-style-timeline.json`, read from disk as
the named real producer: months at 31/121, 29/121, 31/121 and 30/121.
Implementation starts on the Feb header, and QA Phase on the Mar header.
`TimelineGantt.dateOnlyZone-10866.test.tsx` gets one new driven case,
run in Los Angeles: a day axis across the March 8th 2026 spring change.
Its columns are 24/71, 23/71 and 24/71, and a bar from March 8th starts
on its column. The zone-pin driver
`scripts/__tests__/date-only-zone-pins-10183.test.ts` already lists this
file, so its PINS list needed no change.
Re-derived from the new axis, each with a comment naming objectui#11079
and its day-count derivation. Zone-rule assertions are untouched:
- `TimelineGantt.dateOnlyZone-10866.test.tsx`: the computed-range and
pinned-mixed bars, in the suite zone and both driven zones. A
header-width read was added so the pinned day bar can be compared with
its column.
- `timeline-gantt-empty-items.test.tsx`: pin 4 (the catalog shape on 91
days) and pin 5b (see Acceptance notes).
- `timeline-gantt-unusable-date-range.test.tsx`: the degenerate pinned
range and the healthy-path pin.
- `timeline-gantt-null-date-6770.test.tsx` and
`timeline-gantt-date-type-rule-6781.test.tsx`: their bar-geometry pins.
The dispatch did not list these two files, but they read the same
geometry.
## Evidence (objectui HEAD `efee2d5bd`, base `88fbd793d`)
- `pnpm exec vitest run packages/plugin-timeline/`: `Test Files 37
passed (37)`, `Tests 389 passed | 32 skipped (421)`. The base read 36
files, 364 passed and 31 skipped. The 25 new cases are all counted as
passed; the new DST case is a driven case, so the normal run skips it.
- `pnpm --filter @object-ui/plugin-timeline type-check` exits 0.
`tsconfig.test.json` includes all six touched test files, counted with
`--listFiles`.
- In one run, four files pass (`Tests 660 passed (660)`): the zone
driver `scripts/__tests__/date-only-zone-pins-10183.test.ts` (27 passed;
the forked child ran every case of every pin, including the new DST
case), `examples/schema-catalog/test/catalog-gallery-render.test.tsx`
(587 passed), and the two `packages/types` tests that read
`plugin-timeline.mdx` (46 passed).
- eslint on the 7 changed `.ts`/`.tsx` files (`--format json`: 7 files):
0 errors and 34 warnings. The same 6 pre-existing files read 34 warnings
on the base, and the new file has 0. The narrowing is safe:
`eslint.config.js` enables no type-aware linting (no
`parserOptions.project` / `projectService`), and no custom rule under
`eslint-rules/` reads the filesystem. So this diff cannot move the
verdict of any untouched file. The full `pnpm lint` run is CI's.
- These gates exit 0: `check-changeset-presence`, `check-control-bytes`,
`check-new-cross-file-line-citations` (0 new), `check-changeset-claims`,
`check-pending-changeset-literals`, `check-test-path-roots`,
`check-i18n-call-site-keys`, `check-unreferenced-sources`,
`check-changeset-no-major` and `check-doc-fence-languages`.
`check-governed-queue-guard --test` reports NOT GOVERNED.
- **Reverse verification (one time, not kept).** With the fix committed,
`renderer.tsx` was put back to the base blob, and the mutation was
confirmed on disk (`function timelineAxis` 0 hits). The six touched test
files then read `Tests 38 failed | 63 passed | 14 skipped`, and 24 of
the 25 new cases went red; only the fixture-on-disk check stayed green.
The file was then restored with `git checkout HEAD --`. The restore is
proven by its blob hash matching `HEAD` and by an empty `git diff HEAD`.
The direction was red, as predicted.
- **Real layout engine.** The rendered gantt was laid out in Chromium at
1200px, using the Tailwind utilities it uses written out by hand, not
the app's built stylesheet:
- the card's Oct 6 bar starts at x 528.328, the Oct 6 header's x;
- in the catalog gantt, Implementation starts at 450.734, the Feb
header's x, and QA Phase at 691.828, the Mar header's x.
## Acceptance notes
- **Inline `width` on the header cells.** AGENTS.md §2 bans inline
styles outside the colour carve-out. `skills/objectui/rules/styling.md`
says a runtime geometry value "is a separate question this carve-out
does not answer". The header width follows `TimelineGanttBar`'s existing
inline `left` / `width`, which is the same kind of value. The Zone 2
guidance asked for inline percentage widths. A computed percentage
cannot be a static utility.
- **Pin 5b moved from a full-width bar to a zero-width one.** Before, a
plan whose only dates were one same-day task (`2024-05-01` to
`2024-05-01`) drew its bar at `left: 0%; width: 100%`. That value was
the `totalDuration === 0` special case. Now the axis always spans at
least one whole unit, so the guard had nothing left to catch and was
removed. The bar is `left: 0%; width: 0%`, as a zero-length task already
was on any plan with two distinct dates. The pinned `minDate ===
maxDate` case is now a one-unit axis, and the bar overhangs it the way
any bar outside a pinned range does. This is recorded in the changeset.
- **Out of scope, reported for the seat.** The catalog's producer writes
inclusive date-only ends (API Design `2024-01-01` to `2024-01-31`), but
`readGanttDate` reads an end as the instant it names (local midnight at
the start of that day). So each such bar now ends visibly one day short
of its month column, and a same-day task is invisible. Before this
change, the equal-width columns hid the one-day gap by coincidence. The
claim forbids changing how a date is read, so this is left for its own
card.
- **Also outside the claimed file surface:** one sentence in
`content/docs/plugins/plugin-timeline.mdx` ("Time Scales (Gantt)")
describes the shared axis, per AGENTS.md #2.
---
_Generated by [Claude
Code](https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent efb3021 commit 2fb0f9a
9 files changed
Lines changed: 791 additions & 236 deletions
File tree
- .changeset
- content/docs/plugins
- packages/plugin-timeline/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
342 | 342 | | |
343 | 343 | | |
344 | 344 | | |
| 345 | + | |
| 346 | + | |
345 | 347 | | |
346 | 348 | | |
347 | 349 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
| 90 | + | |
| 91 | + | |
90 | 92 | | |
91 | 93 | | |
92 | 94 | | |
| |||
107 | 109 | | |
108 | 110 | | |
109 | 111 | | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
110 | 115 | | |
111 | 116 | | |
112 | 117 | | |
| |||
188 | 193 | | |
189 | 194 | | |
190 | 195 | | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
191 | 199 | | |
192 | | - | |
193 | | - | |
| 200 | + | |
| 201 | + | |
194 | 202 | | |
195 | 203 | | |
196 | 204 | | |
| |||
238 | 246 | | |
239 | 247 | | |
240 | 248 | | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
241 | 252 | | |
242 | | - | |
243 | | - | |
| 253 | + | |
| 254 | + | |
244 | 255 | | |
245 | 256 | | |
246 | 257 | | |
247 | 258 | | |
248 | 259 | | |
249 | 260 | | |
250 | 261 | | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
251 | 265 | | |
252 | | - | |
253 | | - | |
| 266 | + | |
| 267 | + | |
254 | 268 | | |
| 269 | + | |
255 | 270 | | |
256 | 271 | | |
257 | 272 | | |
| |||
262 | 277 | | |
263 | 278 | | |
264 | 279 | | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
265 | 298 | | |
266 | 299 | | |
267 | 300 | | |
| |||
286 | 319 | | |
287 | 320 | | |
288 | 321 | | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
289 | 325 | | |
290 | | - | |
291 | | - | |
| 326 | + | |
| 327 | + | |
292 | 328 | | |
293 | 329 | | |
294 | 330 | | |
295 | 331 | | |
296 | 332 | | |
297 | 333 | | |
298 | 334 | | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
299 | 338 | | |
300 | | - | |
301 | | - | |
| 339 | + | |
| 340 | + | |
302 | 341 | | |
| 342 | + | |
303 | 343 | | |
304 | 344 | | |
305 | 345 | | |
| |||
0 commit comments