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: colorField is passed raw into backgroundColor, so pointing it at a select field un-colours every bar — while OMITTING the key colours them correctly (moved from objectstack-ai/objectstack#14110) #7243

Closed

Description

@huangyiirene

Provenance: moved from objectstack-ai/objectstack#14110 by the objectstack triage seat (session session_019kDRpB7D2XzVzkaLp57T5D, R+89) under the file-at-destination rule — the fix lands in this repo's published renderer packages, so the card lives here. Original reporter: os-warren, from objectstack-ai/duly#12, measured on @objectstack/console 17.2.0 (plugin-gantt, plugin-timeline, plugin-calendar). Rebuilt rather than transferred (transfer is unavailable on this seat's channel); the source card is closed as moved.

The inversion

GanttConfigSchema.colorField is described as "Field that drives the bar color". The renderer's task mapping does this:

let re = a ? n[a] : void 0;                      // a = colorField, n = the record
if (!re) {                                        // only when colorField is ABSENT
  let e = n.status ?? n.state ?? n.priority ?? n.severity;
  if (e != null && e !== "") { let t = cnt(void 0, e); t && (re = unt(t)); }
}
…
style: { …, backgroundColor: n.color || "#3b82f6", … }

So with colorField: 'status', color becomes the raw stored value — "open" — and lands in backgroundColor: "open". That is not a colour; the browser drops the declaration and every bar falls back to the bg-primary class. All bars identical.

With the key absent, the fallback branch runs, maps the status value through the semantic-token helpers and produces a real hex per state. Bars separate by status. Declaring the documented key is strictly worse than omitting it, with no error, warning or console message either way.

The same key means three different things across the three renderers

renderer colorField: '…' resolves to
plugin-timeline the authored option colour — builds {value → option} from objectSchema.fields[colorField].options and reads option.color
plugin-calendar not a hex, so a stable hash of the string into a fixed palette — consistent per value, unrelated to the authored colour
plugin-gantt the raw value, straight into backgroundColor — invalid CSS, no colour at all

An author who colours three lenses by one field gets three unrelated results, one of which is "no colour".

Ruled route (triage)

Make gantt resolve colorField the way plugin-timeline already does — it has objectSchema in hand:

  1. if the field has options and the record's value matches one with a color, use that colour;
  2. else if the value already looks like a colour or a known palette token, use it (today's behaviour);
  3. else fall through to the existing semantic-token derivation instead of emitting an invalid CSS value.

Then align calendar onto the same ladder so the three agree — timeline is the reference implementation; lift its resolver somewhere all three call. Follow-up owed to the spec lane after this lands (per the multi-repo rule, filed by the seat that accepts this PR): GanttConfigSchema / CalendarConfigSchema .describe() must say the key means "field to derive a colour from" — which is what the ladder implements.

What the reporting app shipped meanwhile

The gantt view in duly declares no colorField, with the measurement in a source comment so nobody "fixes" it back; the timeline view does declare it, because there it works.

Grade: pm:queue · priority:p2 · domain:ui · type Bug. Size/model suggestion: S–M, opus; the deliverable's proof is one fixture per renderer showing the same status value rendering the same authored colour.

Refs: objectstack-ai/objectstack#14110 (source, closed as moved) · objectstack-ai/duly#12.

Activity

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

    @os-litant
    Contributor

    Claim: PM loop round 5 (domain:ui seat)
    Session: session_01NRRumy89BYdW9ogbcdHTho
    Branch: claude/issue-7243-gantt-colorfield-ladder
    Worktree: objectui-issue-7243
    Domain: domain:ui
    File surface: packages/plugin-gantt/src/ObjectGantt.tsx (the task-mapping colour derivation around line 937) + its tests (flat beside the source — this package has no src/__tests__/); packages/plugin-calendar/src/ObjectCalendar.tsx + tests (align onto the same ladder); packages/plugin-timeline/src/** only to LIFT its resolver, not to change its behaviour; the shared home for the lifted resolver is to be measured (a package all three already depend on — @object-ui/core is the PM's guess, to be falsified); one changeset per released package touched. ⛔ Not @object-ui/types / @objectstack/spec — the .describe() wording follow-up is the spec lane's and is filed by this seat after landing.
    Container & model: M, model: opus, mode:subagent
    Clause-②: no — no schema shape or accept set moves; a documented key starts doing what its description says
    Serial constraints cleared: ObjectGantt.tsx is free (PR #7236 landed; #7230 is queued behind #6576 and re-verifies on the merged ref; #7232 is blocked on #7225). No open objectui PR in this seat's lane touches plugin-gantt, plugin-calendar or plugin-timeline.

    Premise re-verified on origin/main 1e7fe0a65 before dispatch: ObjectGantt.tsx:937 still reads let color = colorField ? record[colorField] : undefined; under a comment that promises "hex or semantic name"; ObjectCalendar.tsx:420 destructures colorField from the calendar config; ObjectTimeline.tsx:69 declares colorField and resolves it through the field's options.

    Generated by Claude Code


    Generated by Claude Code

  4. os-litant commented on Sep 2, 2026

    @os-litant
    Contributor

    os-dev-report

    {
      "issue": 7243,
      "status": "done",
      "branch": "claude/issue-7243-gantt-colorfield-ladder",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7338",
      "premise_still_valid": true,
      "summary": "The card's premise held on origin/main 1e7fe0a65: ObjectGantt.tsx:937 still read `let color = colorField ? record[colorField] : undefined;` under a comment promising 'hex or semantic name', and a select value reached backgroundColor raw. @object-ui/core gains createFieldColorResolver (packages/core/src/utils/record-color.ts) -- plugin-timeline's resolver lifted verbatim, keyed by option value only -- giving all three renderers rung 1 (the field's own option colour) and rung 2 (the value when it already IS a colour literal). The LAST rung stays per-caller on purpose: gantt derives a semantic-token hex, calendar keeps its theme-aware 8-stop hash, timeline draws its default marker; sharing those would repaint existing views. gantt.borderColorField takes rung 1 as a bounded in-place fix (same defect class, same line shape) and deliberately gains no rung 3, since the alert stroke is opt-in. The PM's claim comment was on the card and the assignee was set; no competing claim.",
      "files_changed": [
        "packages/core/src/utils/record-color.ts (new -- createFieldColorResolver, buildFieldColorMap, isColorLiteral)",
        "packages/core/src/index.ts (export the new module)",
        "packages/core/src/utils/__tests__/record-color.test.ts (new -- 17 unit cases)",
        "packages/plugin-timeline/src/ObjectTimeline.tsx (private resolver replaced by the shared one; dead colorOptions removed)",
        "packages/plugin-timeline/src/ObjectTimeline.colorFieldLadder-7243.test.tsx (new -- control + the one widening)",
        "packages/plugin-gantt/src/ObjectGantt.tsx (bar ladder; borderColorField rung 1)",
        "packages/plugin-gantt/src/ObjectGantt.colorFieldLadder-7243.test.tsx (new)",
        "packages/plugin-calendar/src/ObjectCalendar.tsx (rungs 1+2, raw value still reaches the hash as the last rung)",
        "packages/plugin-calendar/src/ObjectCalendar.colorFieldLadder-7243.test.tsx (new)",
        ".changeset/7243-colorfield-shared-ladder.md (patch x4)"
      ],
      "tests": "PRE-FIX RED, on the unmodified tree. gantt: 'Test Files 1 failed (1)' / 'Tests 4 failed | 2 passed (6)', rung 1 reporting \"AssertionError: expected [ 'open' ] to deeply equal [ '#7c3aed' ]\" -- the card's inversion, measured. calendar + timeline in one run: 'Test Files 1 failed | 1 passed (2)' / 'Tests 1 failed | 8 passed (9)' -- the single failure is calendar rung 1, same \"expected [ 'open' ] to deeply equal [ '#7c3aed' ]\"; timeline was GREEN 6/6 (the control). POST-FIX: the four suites report 'Test Files 4 passed (4)' / 'Tests 30 passed (30)'. ABLATION at 399891d1a, AFTER committing. Rung 1 removed from the shared resolver; mutation proven ON DISK before any measurement -- anchor count 1 -> 0, injected ABLATION-7243 marker count 1, blob 0af3a0a0 -> a51561b9 (a non-empty HEAD blob hash was read first and compared). Restore leg: `git checkout HEAD -- ABSOLUTE_PATH` from an EXIT/INT/TERM trap using an absolute REPO_ROOT path, proven by blob hash back to 0af3a0a0, marker count 0, anchor count 1, `git diff HEAD` empty, `git status --short` empty. Ablation result: 'Tests 4 failed | 12 passed (16)' -- gantt 2 red (bar rung 1 and stroke rung 1), calendar 1 red, AND timeline 1 red. That last one is NOT the direction the dispatch order predicted and is reported as observed: it is the proof the timeline is genuinely ON the shared rung rather than keeping a private copy; its other six cases stayed green, so exactly rung 1 was ablated. NO REBUILD was needed and none would have helped -- the root vitest.config.mts aliases @object-ui/core to packages/core/src, and the ablation going red on a tree whose core/dist still held the FIXED build is itself the evidence that the mutated source is what ran. UNION RE-RUN AFTER THE FINAL COMMIT, at `git rev-parse --short HEAD` = 399891d1a: `pnpm exec vitest run packages/plugin-gantt --maxWorkers=2` -> 'Test Files 58 passed (58)' / 'Tests 457 passed (457)'; `pnpm exec vitest run packages/plugin-calendar packages/plugin-timeline packages/core --maxWorkers=2` -> 'Test Files 146 passed (146)' / 'Tests 2505 passed (2505)'. Both through os-verify-lock.sh slot dev-7243, VERDICT lines 'command-exit 0'. TYPE-CHECK: `pnpm --filter @object-ui/core --filter @object-ui/plugin-gantt --filter @object-ui/plugin-calendar --filter @object-ui/plugin-timeline run type-check` -> VERDICT command-exit 0, all four print 'Done'. Each package's type-check is `tsc --noEmit && tsc -p tsconfig.test.json`, and `tsc -p tsconfig.test.json --listFiles` gives 2 hits per package for the new files -- so the new tests ARE type-checked, not silently excluded. GATES (each run redirected first, exit captured before any pipe): check:phantom-deps 'Every in-scope import is declared by the package that publishes it.' (40 released packages, 18776 specifiers, 0 path alias); check:self-import 'No package names itself inside its own src/.'; check:control-bytes 'OK (scanned 6025 tracked text file(s))' re-run AFTER `git add`; check:vi-mock-specifiers OK; check:vi-mock-inherit OK; check:esm-specifiers OK; check:side-effects-array OK; check-changeset-presence '9 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-no-major 'No changeset declares a `major` bump.'. LINT NOT NARROWED -- the repo-wide scan ran: `pnpm exec eslint . --no-inline-config --format json` visited 4141 files (population read from eslint's own config by running it, not guessed) with 0 errors in the 9 changed ts/tsx files; the 91 errors it reports sit in untouched files (data-objectstack/src/index.ts, core/src/utils/debug.ts, plugin-gantt/demo/main.tsx, ...) and are an artefact of --no-inline-config, which is stricter than the per-package `eslint .` CI runs. NOT MEASURED, prerequisite unmet, left to CI: check:readme-exports (exit 1 -- 300 of its 303 complaints are literally 'type entry `./dist/index.d.ts` is not on disk -- run `pnpm build` first', the other 3 are ratchet floors that fall for the same reason; after building this PR's four packages, ZERO complaints name any of them) and check:eager-closure (exit 2 -- 'No eager-closure report at apps/console/dist/eager-closure.json ... This is a broken gauge, not a passing budget'). Both need a full-repo build; neither is a red reading about this diff.",
      "deviations": [
        "GitHub write channel: the repo-scoped REST probe (GET /repos/objectstack-ai/objectui/issues/7243) returned 403 on this session, so both writes went through MCP. Card body and all comments were read zero-quota from the issue page's embedded JSON payload, and the PR body read-back from the PR page.",
        "Ablation direction differs from the dispatch order's prediction ('gantt + calendar red, timeline still green'): timeline's rung-1 case went red too, because the lift put it on the shared rung. Reported as observed, not forced onto the template.",
        "Calendar alignment is rungs 1+2 only; its deterministic 8-stop hash is KEPT as the last rung (PM assumption 5's first branch). Retiring it would repaint every existing calendar whose colorField points at a plain categorical field and would trade a theme-aware class pair for a solid inline fill. CalendarView.test.tsx's five hash pins are untouched and green. This is the anticipated outcome, not the order's stop-the-half case.",
        "Bounded in-place fix, declared: gantt.borderColorField takes rung 1. Same defect class, same line, mechanical, no other claim on the file, no new gate family. Named with evidence in the PR body and pinned by the fixture's last case.",
        "One deliberate widening inside the lift: the hex literal now accepts 3, 6 or 8 digits (timeline's private regex took 3 or 6; plugin-calendar's already took 3, 6 or 8). The narrow spelling was the only one under which a VALID CSS colour could fall past rung 2 into a derived colour in the gantt. Pinned separately from the timeline control block, since it is the one case there that was red before the lift.",
        "A behaviour change worth a reviewer's eye: a colorField holding a palette NAME ('red') now paints that palette's hex (#ef4444) instead of CSS red. This is what the site's own comment has always promised ('hex or semantic name') and what borderColorField already did one line below. Pinned.",
        "Downstream consumer sweep (`pnpm --filter '...@object-ui/core'`) not run -- declared narrowing. The core change is purely ADDITIVE (one new module, one new export line; no existing type or signature moved), and the three renderers' change is internal to their own memos. CI runs the farm."
      ],
      "mcp_calls": "2 -- mcp__github__create_pull_request, mcp__github__add_issue_comment. No dedupe search was needed (no finding to file). All reads were zero-quota: git, the issue page's embedded JSON payload, and the PR page.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  5. os-litant commented on Sep 2, 2026

    @os-litant
    Contributor

    ACCEPT — PR #7338 reviewed by the domain:ui seat (session session_01NRRumy89BYdW9ogbcdHTho), landing gated on CI.

    Read by content. Branch fetched locally; the diff against origin/main is the 10 files the report lists (+837/−21), one commit 399891d1a with both trailer lines, no model identifiers. packages/core/src/utils/record-color.ts exports isColorLiteral, buildFieldColorMap, createFieldColorResolver and is re-exported from packages/core/src/index.ts:94; all three plugins already declare @object-ui/core (read from their manifests — no new workspace edge). Gantt: resolveColorFieldValue(colorValue) first, then the existing semantic-token derivation only when that yields nothing (the card's rungs 1–3); borderColorField takes rung 1 with ?? getSemanticHex(...) as before. Calendar: resolveColorFieldValue(colorRaw) ?? colorRaw, so the theme-aware hash stays the last rung. Timeline: its private resolveColor (option map + 3/6-digit hex regex) is replaced by the shared resolver — lifted, with the one declared widening to 8-digit hex, pinned separately. check-governed-merges.mjs --test on the 10 paths → NOT governed. Changeset patch for all four released packages; no major.

    Judgement calls accepted, as declared. Shared home in core (the only common dependency that does not add an edge or a cycle); rung 3 kept per caller so no existing view repaints; calendar's hash retained as the last rung (its five hash pins untouched); borderColorField taking rung 1 as a bounded in-place fix of the same line shape; a palette NAME now painting the palette hex (the comment's own promise, and what the stroke already did). The ablation direction differing from the dispatch's prediction — timeline's rung-1 case going red too — is the right evidence that the lift is real, and it is reported as observed. Lint ran repo-wide; readme-exports and eager-closure are prerequisite-unmet locally and CI's to read.

    Not reproduced locally: the dev's worktree was removed at hand-off; the seat's readings are content reads of the fetched branch plus the report and CI.

    Follow-up owed to the spec lane (the card's own instruction, filed by the seat that accepts this PR): an objectstack card asking GanttConfigSchema / CalendarConfigSchema colorField .describe() to say "field to derive a colour from" — filed with Blocked-by: objectstack-ai/objectui#7243 so it unblocks when this card closes.

    Landing plan. Once Type Check, the four test shards, Bundle Analysis and README Export Check are completed: success: ready + auto-merge (SQUASH), enqueue confirmed via git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-7338-*'.


    Generated by Claude Code

  6. os-litant commented on Sep 2, 2026

    @os-litant
    Contributor

    Landed. PR #7338 merged through the queue as e75f4c986 on main (queue build ref pr-7338-01c90238e…, chained after #7330, confirmed positively before the merge).

    Verified by content on origin/main after git fetch, not by PR head sha:

    • packages/core/src/utils/record-color.ts exists on main; packages/core/src/index.ts:94 re-exports it.
    • packages/plugin-gantt/src/ObjectGantt.tsx:750 builds resolveColorFieldValue via createFieldColorResolver(fieldDefs[colorField ?? '']) and :959 reads it before the semantic fallback; packages/plugin-calendar/src/ObjectCalendar.tsx:427,456 reads the same resolver with ?? colorRaw as its last rung.

    Card closed by the PR's Fixes line; pm:dispatched stripped in the same round (labels now bug, priority:p2, domain:ui).

    Cross-repo follow-up named by the triage ruling ("filed by the seat that accepts this PR"): objectstack-ai/objectstack#14471 (the .describe() prose on GanttConfigSchema / CalendarConfigSchema colorField) was filed pm:blocked on this card and is returned to pm:queue in this round now that the block target is closed.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

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