Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

plugin-gantt: between 640px and 1023px the task-list rows carry NO dates at all — the Start/End columns are gated off and the date sublabel is display:none from 640px up #7224

Closed

Description

@os-warren

Found while verifying a premise on #7204. Filed unassigned, not claiming. This is a different defect from #7204: that card is about names being squeezed at 1024px and wider; this one is about dates being absent entirely in a band #7204 never visits.

Measured on origin/main 3561bd2ca93b2cd3f7e63616f0c986328c6408ce, packages/plugin-gantt/src/GanttView.tsx.

Symptom

At any viewport from 640px up to 1023px, a gantt task-list row shows neither the Start/End date columns nor the 8/26 → 9/2 sublabel under the title. The row's dates exist in the DOM twice and are painted zero times. No drag and no configuration is needed — it is the default at those widths.

Mechanism — two gates on two different width sources, and they overlap wrong

Both gates are in GanttView.tsx.

Gate 1 — the Start/End columns. Container-driven, in JS:

taskListWidthForContainer(width)  ->  width under 640: 140 | under 1024: 220 | else 320
showStartEndColumns(taskListWidth) ->  taskListWidth >= 280

so at a container width of 640..1023 the default task-list width is 220, and showStartEndColumns(220) is false. The two cells are rendered with hidden plus an inline display: none.

Gate 2 — the date sublabel. Viewport-driven, in the component's own injected style element:

@media (min-width: 640px) {
  .gantt-sm-hidden { display: none; }
}

and the sublabel span carries gantt-sm-hidden. Above 640px it is display: none.

Gate 1 reads the container (a ResizeObserver on the gantt root: effectiveWidth = containerWidth || window.innerWidth). Gate 2 reads the viewport (a CSS media query). The two are the same number only when the gantt fills the window. The uncovered band is where gate 1 has already withdrawn the columns and gate 2 has already withdrawn the sublabel.

Measurements

A. Real Chromium 1194 (/opt/pw-browsers/chromium-1194), loading the component's own rendered markup and its own injected style element, reading getComputedStyle(sublabel).display:

viewport sublabel display sublabel client width
500 inline 72.9
639 inline 72.9
640 none 0
800 none 0
1024 none 0
1440 none 0
1920 none 0

B. jsdom render of GanttView at container width 800, reading the two Start/End cells of a data row:

w-16 gantt-sm-w20 text-right ... | hidden attr = true | inline display = none   (start, "8/26")
w-16 gantt-sm-w20 text-right ... | hidden attr = true | inline display = none   (end,  "9/2")

and the list header at that width renders only Task Name plus the open-details spacer — the Start and End header captions are gone too.

A and B are the same width, so at 800px both gates are shut simultaneously: no columns, no sublabel.

The band is wider than the default case

Gate 1 keys off the task-list width, not the viewport, and the user can drag the splitter down to TASK_LIST_MIN_W = 160. So the hole also opens at 1024px and above whenever the persisted layout holds a dragged width under 280 — and a dragged width is the only way taskListWidth is ever anything but the three defaults, since there is no prop and no GanttConfigSchema key for it.

There is a mirror-image hole at the other end: a viewport under 640px with the task list dragged wider than 280 satisfies showStartEndColumns, but the columns also carry Tailwind's hidden sm:block, so the viewport keeps them hidden while gantt-sm-hidden is inactive — that one lands the right way up (the sublabel shows).

What is left when both gates shut

Only the bar hover tooltip, which is itself conditional: the tooltip prints Aug 26 → Sep 2 only when task.fields is empty. Configure the documented tooltipFields key and the tooltip renders those fields instead, at which point a row's start and end dates are not readable anywhere in the view without opening the detail drawer.

Inline edit is caught by the same gate: the Start/End cells are also where the two type="date" inputs live, so at a task-list width under 280 an inline-editing user can edit the title but cannot see or change the dates.

Suggested directions — maintainer's call, none proposed as decided

  • A. Make the sublabel the complement of the columns rather than a separate viewport rule: render it exactly when the Start/End columns are not rendered. Needs care, because "the columns are rendered" is currently the conjunction of a container test and a viewport test.
  • B. Move gate 2 off the viewport onto the same container width gate 1 uses, so the two decisions cannot disagree about how wide the gantt is.
  • C. Leave both gates and accept the hover tooltip as the fallback — cheapest, but it is hover-only and tooltipFields defeats it.

Related: #7204 (the same two functions, at 1024px and wider). Fixing this one likely settles the shape question #7204 is currently blocked on, so they probably want to be decided together.

Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 1, 2026
  2. os-project-manager commented on Sep 2, 2026

    @os-project-manager
    Collaborator

    Maintainer ruling recorded — Y, ruled together with #7204: one container-driven predicate closes this band

    Director seat (objectstack #12708), summon #8, session session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-02.

    Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-02, replying to decision batch #5 in which #7204 and this card were items 2 and 3, presented as one ruling with the recommendation Y. Verbatim reply: 「同意」.

    Ruled: Y — the full ruling text is on #7204 (comment posted in the same stroke). For this card the operative part is option B of this card's own list, folded into #7204's predicate: the sublabel's gate moves off the viewport media query onto the same container width the Start/End column gate reads, and the sublabel renders exactly when the columns are not painted. Option C (accept the hover tooltip as the fallback) is not taken — hover-only, and tooltipFields defeats it.

    Execution: one dispatch with #7204 in the domain:ui lane; this card's viewport table (500 / 639 / 640 / 800 / 1024 / 1440 / 1920, real browser) is the acceptance pin for this half.

    State transition, same stroke: needs-user-decision cleared; pm:queue stands as the single state (this card carried both labels at once since filing — the double state is repaired here). Ledger: objectstack director seat post #12708, summon #8.


    Generated by Claude Code

  3. os-project-manager commented on Sep 2, 2026

    @os-project-manager
    Collaborator

    Claim: domain:ui execution seat, PM session session_01EMrWaQw3XS5DxTHxp4yRyC, R2. Dispatched together with #7204 as one PR, on branch claude/issue-7204-gantt-tasklist-width — per the maintainer's ruling of 2026-09-02 (verbatim 「同意」, option Y), which ruled both cards as one because they are the same file and the same two functions.

    ⛔ Do not claim this card separately; the full dispatch record is on #7204 (comment above it in that thread).

    Why one PR is right rather than tidy: this card and #7204 are two symptoms of a single root — the Start/End columns are gated by a container measurement (showStartEndColumns, a ResizeObserver width) while the date sublabel is gated by a viewport media query (gantt-sm-hidden, @media (min-width: 640px)), and neither predicate is the complement of the other. Fixing either alone leaves the split standing and lets them drift apart again. Ruled part 1 collapses them to one container-driven predicate, which closes this card's 640–1023 band at the root, and also the dragged-narrow case at any width.

    This card supplies the acceptance table for that half: the sublabel must be visible exactly when the columns are not painted, pinned in a real browser at 500 / 639 / 640 / 800 / 1024 / 1440 / 1920. ⚠️ jsdom cannot decide it — it applies the media rule irrespective of innerWidth and returns display: none at every width, which is a false confirmation in the reassuring direction.


    Generated by Claude Code

  4. os-project-manager commented on Sep 3, 2026

    @os-project-manager
    Collaborator

    os-dev-report

    Same report as on #7204 — one dispatch covered both cards, one PR fixes both.

    {
      "issue": 7204,
      "also_fixes": 7224,
      "status": "done",
      "branch": "claude/issue-7204-gantt-tasklist-width",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7420",
      "premise_still_valid": true,
      "summary": "All three parts of the 2026-09-02 ruling (option Y) landed in one PR on packages/plugin-gantt/src/GanttView.tsx. (1) The date sublabel now renders on exactly the complement of the Start/End columns, both from the same container-derived taskListWidth: the '@media (min-width: 640px) { .gantt-sm-hidden { display: none } }' rule and the class it hid are gone, and the two date cells lose their 'hidden sm:block' so 'the columns are painted' is no longer a conjunction of a container test and a viewport test. (2) taskListWidthForContainer takes 3/8 of the container clamped to [320, 560] from 1024 up, replacing the flat 320 cap. (3) showStartEndColumns compares against a derived START_END_COLUMNS_MIN_W = 32 + 160 + 28 + 32 + 160 = 412, each term a named constant traced to the markup that spends it. ObjectGantt.tsx (held by PR #7391) was not touched; content/docs/releases was not touched; no gate was weakened. A side effect worth naming: the header captions and the data cells now agree, where before a sub-640px viewport with a wide task list printed Start/End captions over cells that were not painted.",
      "tests": "All readings at commit b5f659e72 (branch head). RED-THEN-GREEN: origin/main's GanttView.tsx was restored over the committed fix; mutation proved on disk by blob hash (on-disk 8e7b4d8bc16a129ef58a3c58aba0b641d09be46c == origin/main blob, != HEAD blob e0cca573472e532521294bed7b067e3f49e1fffc) plus two anchored controls (gantt-sm-hidden lines back = 2, START_END_COLUMNS_MIN_W lines = 0); ablated run 'Test Files 1 failed (1) / Tests 14 failed (14)', VERDICT command-exit 1. Restore proved by STATE, not exit code: 'git diff HEAD' empty and on-disk blob == HEAD blob, under trap ... EXIT INT TERM with absolute paths. GREEN: same file 'Test Files 1 passed (1) / Tests 14 passed (14)'; whole plugin-gantt suite 'Test Files 59 passed (59) / Tests 471 passed (471)', VERDICT command-exit 0. All vitest runs went through os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=dev-7204-gantt. type-check: 'pnpm --filter @object-ui/plugin-gantt run type-check' exit 0 (script name echoed, so not a zero-match run), after 'pnpm --workspace-concurrency=2 --filter @object-ui/plugin-gantt^... run build' built the dependency closure (before that build it failed with TS2307 on every @object-ui import). lint: package lint exit 0, 0 errors; GanttView.tsx 19 warnings before and 19 after with an identical rule multiset, measured by linting the restored pre-fix blob, so the change adds none; the new test file adds 0. Gate family derived from the touched paths, all exit 0 with the exit captured before any pipe: check:control-bytes, check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, check:vi-mock-specifiers, check:vi-mock-inherit, check:readme-exports, check:shell-escape-residue, check:sdui-registration-pins, check:element-data-source-declaration, check:eager-closure. NOTE on check:readme-exports: it first exited 1 with '5 self-import(s) could not be judged ... not on disk -- run pnpm build first' for @object-ui/cli and @object-ui/plugin-ai. That is an unbuilt-artifact reading, not a red gate; after building those two packages it exits 0. Reported rather than silently re-run. Control-byte self-scan over the diff and the PR body: no matches (grep exit 1), with a firing control on a file carrying a deliberate 0x01.",
      "browser_viewport_table": {
        "provenance": "REAL CHROMIUM, NOT jsdom. Chromium 1194 at /opt/pw-browsers/chromium (preinstalled; playwright install was never run), driven by Playwright against a Vite dev server rendering the real GanttView with the repo's own Tailwind build and the component's own injected style element. The measurement script was run from INSIDE the worktree (running it from outside gives ERR_MODULE_NOT_FOUND for 'playwright', which is unmeasured, not a red). Every viewport-dependent number below is a browser reading; nothing here was taken from jsdom, which applies the media rule irrespective of innerWidth and would have confirmed 'the sublabel is never visible'.",
        "legend": "pane = .gantt-task-list painted width; sub = date sublabel painted; cols = both Start/End cells painted",
        "container_equals_viewport": [
          { "viewport": 500,  "before": { "pane": 140, "sub": true,  "cols": false }, "after": { "pane": 140, "sub": true,  "cols": false } },
          { "viewport": 639,  "before": { "pane": 140, "sub": true,  "cols": false }, "after": { "pane": 140, "sub": true,  "cols": false } },
          { "viewport": 640,  "before": { "pane": 220, "sub": false, "cols": false }, "after": { "pane": 220, "sub": true,  "cols": false } },
          { "viewport": 800,  "before": { "pane": 220, "sub": false, "cols": false }, "after": { "pane": 220, "sub": true,  "cols": false } },
          { "viewport": 1024, "before": { "pane": 320, "sub": false, "cols": true  }, "after": { "pane": 384, "sub": true,  "cols": false } },
          { "viewport": 1280, "before": { "pane": 320, "sub": false, "cols": true  }, "after": { "pane": 480, "sub": false, "cols": true  } },
          { "viewport": 1440, "before": { "pane": 320, "sub": false, "cols": true  }, "after": { "pane": 540, "sub": false, "cols": true  } },
          { "viewport": 1920, "before": { "pane": 320, "sub": false, "cols": true  }, "after": { "pane": 560, "sub": false, "cols": true  } }
        ],
        "xor_violations": { "before": 2, "before_at": [640, 800], "after": 0 },
        "container_decoupled_from_viewport_after_fix": [
          { "viewport": 1440, "container": 800,  "pane": 220, "sub": true,  "cols": false },
          { "viewport": 1920, "container": 700,  "pane": 220, "sub": true,  "cols": false },
          { "viewport": 500,  "container": 700,  "pane": 220, "sub": true,  "cols": false },
          { "viewport": 800,  "container": 1440, "pane": 540, "sub": false, "cols": true  },
          { "viewport": 1024, "container": 1097, "pane": 411, "sub": true,  "cols": false },
          { "viewport": 1024, "container": 1098, "pane": 412, "sub": false, "cols": true  }
        ],
        "raw_computed_reads_before_fix": "getComputedStyle(sublabel).display was 'block' with width 91px at 500 and 639, and 'none' with width 0 at 640, 800, 1024, 1440 and 1920; window.matchMedia('(min-width: 640px)').matches flipped exactly at 640; each .gantt-sm-w20 cell measured 80.0px at every width from 640 up, which is where 80 + 80 = 160 comes from."
      },
      "part2_derivation": {
        "rule": "taskListWidthForContainer(w) = w < 640 ? 140 : w < 1024 ? 220 : round(clamp(w * 0.375, 320, 560))",
        "share": "3/8 = 0.375",
        "clamp": [320, 560],
        "why_share": "The title's space with the columns painted is pane - 1 (the pane's own border-r, inside its border-box width) - 32 (sm:px-4) - 160 (the two date cells) - 28 (open-details slot) - 32 (title furniture) = pane - 253. A 40-character title MEASURES 262px in the row's sm:text-sm font (Chromium), so 'legible at 1440' needs pane >= 515, i.e. a share of at least 515/1440 = 0.3576. 3/8 is the smallest clean fraction above it and puts 1440 at 540, leaving 287px.",
        "why_floor_320": "The ruling's floor and the previous fixed default, so no container gets a narrower pane than before.",
        "why_ceiling_560": "560 - 253 = 307px of title, which still clears the measured 262px after one level of the row's depth*14 indent (262 + 14 = 276). Past that, more pane width buys no legibility the ruling asked for and the timeline - the view's primary content - pays for it. The ceiling binds from a 1494px container up.",
        "measured_widths": [
          { "container": 1280, "pane": 480, "clears_412": true, "name_cell": 259, "title_space": 227, "fits_25_char_154px": true, "fits_40_char_262px": false },
          { "container": 1440, "pane": 540, "clears_412": true, "name_cell": 319, "title_space": 287, "fits_25_char_154px": true, "fits_40_char_262px": true },
          { "container": 1920, "pane": 560, "clears_412": true, "name_cell": 339, "title_space": 307, "fits_25_char_154px": true, "fits_40_char_262px": true }
        ],
        "412_is_cleared_at_all_three_desktop_widths": true,
        "legibility_floor": "Defined as the ruling's own lower bound, 25 characters, which measures 154px. Any pane that clears 412 leaves the title at least 412 - 253 = 159px, so the threshold itself guarantees the floor - the ruling's 160px minimum-title term is worth almost exactly 25 characters, measured.",
        "derivation_reproduces_the_cards": "At pane 320 the same arithmetic gives 67px for a flat leaf, 53px at depth 1 (67 - 14) and 63px for a summary row (its w-4 toggle costs 4 more than a leaf's w-3 spacer). Chromium read 67, 53 and 63. #7204 reported 53. No term is off by a pixel."
      },
      "byte_reading": {
        "gate": "check:eager-closure exit 0",
        "per_chunk": {
          "framework": "511.5 KB / 511.7 KB ceiling, headroom 0.2 KB",
          "vendor-objectstack": "926.1 KB / 944.3 KB ceiling, headroom 18.2 KB",
          "ui-components": "387.3 KB / 389.6 KB ceiling, headroom 2.3 KB",
          "aggregate": "3178.3 KB / 3191.4 KB budget, headroom 13.1 KB"
        },
        "measured_delta_vs_pre_fix_baseline": {
          "eagerGzipBytes": { "branch": 3254545, "baseline": 3254545, "delta": 0 },
          "eagerRawBytes": { "branch": 11175307, "baseline": 11175307, "delta": 0 },
          "eagerChunkCount": { "branch": 48, "baseline": 48, "delta": 0 },
          "per_chunk_gzip_deltas": "none - every one of the 48 eager chunks identical, no chunk on one side only"
        },
        "method": "The console was built twice through turbo (both builds exited 0 - a failed ablation build reads a stale artifact and reports a stripped input as free), once on the branch and once with GanttView.tsx restored to origin/main under a trap-guarded ablation with the mutation proved on disk by blob hash and the restore proved by git diff HEAD being empty. No ceiling was raised and no copy was trimmed.",
        "firing_control": "The zero is a reading, not a stale artifact: the entry chunk hash DIFFERS between the two builds (index-DUp49asL.js on the branch vs index-XRCMG0Ou.js on the baseline), so the two builds genuinely produced different bundles and the eager closure still weighed the same. No gantt chunk appears in the eager set at all."
      },
      "deviations": [
        "The ruling's 412 does not count the pane's own 1px border-r, which sits inside its inline border-box width, so the true break-even is 413. 412 is implemented AS RULED; the effect is that at exactly pane 412 the title gets 159px rather than the 160 the term names. Flagged, not silently corrected.",
        "New behaviour the ruling did not describe: between a 1024px and a 1097px container the Start/End columns are now OFF where they used to be on, and the dates ride the sublabel (0.375 * 1024 = 384 < 412). It is a net gain - the title goes 67px to 291px and the dates stay visible - but #7204's table never visits that band, so it is called out and pinned in the tests at 1097/1098.",
        "Below a 640px viewport the 412 terms are cheaper than 412 assumes: .gantt-sm-w20 (80px) and the open-details slot (hidden sm:block) only apply from 640px up, so the columns cost 128 and the slot 0. The threshold is conservative there, never permissive. Making those unconditional would be a narrow-layout change nobody asked for; left alone.",
        "depth * 14 is unbounded, so no single default keeps a deeply nested row legible. Stated as a known limit in the source comment, the test file and the PR, per the dispatch - not chased.",
        "The three sub-1024 stepped defaults (140 / 220) were left unchanged; the ruling only asked for the container-sized default at 1440 and wider, with 320 as the floor."
      ],
      "mcp_calls": "5 - issue_read(#7204), create_pull_request(#7420), issue_write(create #7421), and the two add_issue_comment calls carrying this report (#7204 and #7224). Every other GitHub read (the ruling comments, #7224 and its comments, the 290 open issues for the duplicate check, the PR body read-back, the label read-back on #7421) went through the container's unauthenticated REST channel, probed green at the start of the run.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #7421: `taskListWidth_LEGACY_REMOVED` in GanttView.tsx is a declared-and-never-read `const null` left behind by a finished refactor - one hit repo-wide, its own declaration, with a comment that reads like a live to-do. Unassigned, label `finding` only.",
        "NOT filed, already open as #7228: `columnWidthForContainer` branches on container width and returns 110 from all three arms. Found adjacent to my edit; the duplicate check hit it, so no second card was created.",
        "NOT filed, covered by the ruling: the `gantt`-block passthrough (an invented `gantt.taskListWidth` validates and is silently ignored, same silent-inert class as #7199) is recorded on #7204 and explicitly out of scope here."
      ]
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions