Skip to content

fix(app-shell,plugin-view): relay a per-view rowColor to ListView (objectui#7218) - #7558

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-7218-rowcolor-host-relay
Sep 3, 2026
Merged

fix(app-shell,plugin-view): relay a per-view rowColor to ListView (objectui#7218)#7558
os-project-manager merged 1 commit into
mainfrom
claude/issue-7218-rowcolor-host-relay

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #7218

Scope of record: rowColor only. The card's TITLE names two keys and is out
of date; the authority is triage comment
5528535616
(R+122, 2026-09-03), which split the card and says so in terms: "Scope now:
rowColor only. fieldOrder moved to #7516 … This comment is the scope of
record."
fieldOrder is untouched here — it is #7516's, it is
needs-user-decision, and it got the opposite answer (no producer on any host,
and columns already expresses order), so it needs a ruling rather than a
relay. Nothing in this branch touches it.

Clause-②: no

What was wrong

rowColor is a declared member of ListViewSchema — imported by reference from
@objectstack/spec (RowColorConfigSchema, shape field plus an optional
colors map) — and ListView READS it: it seeds rowColorConfig state from
schema.rowColor and hands it to the grid, which colours whole rows from the
named field's value.

Neither object-view host relayed it:

host literal relayed keys before rowColor rung
app-shell ObjectView.renderListView fullSchema 47 named + the host spread none
plugin-view ObjectView the object-view HOST-COMPOSITION SURFACE fence 46 none
app-shell InterfaceListPage the interface route already shipped

So an authored per-view row colour was unreachable on the object route:
authored, validated, built and served correctly, then dropped at the relay.
Nothing errored, every authoring gate passed, and the only symptom was that the
rows were not coloured — an author cannot notice short of diffing the DOM. Same
"declared and inert" shape as #7199's description, whose PR is the shape this
one follows.

Why this is mechanical, and not an intent question

The card concluded rowColor was "authorable metadata with a live reader and
no delivery path"
. That conclusion does not hold: the triage measured a THIRD
host, and it already relays the key. packages/app-shell/src/views/InterfaceListPage.tsx
relays rowColor: view.rowColor next to grouping and pagination into a
schema typed ListViewSchema, and has since ADR-0047. There IS a delivery
path — two of three hosts simply did not use it.

Re-measured on the tree this branch cuts from (origin/main 045d20ba8, the
triage read 0e3b3be), and the precedent still stands at
InterfaceListPage.tsx:465.

One measurement of my own strengthens it: the LEGACY shorthand for this same
feature — bare color, which list-view-spec-parity records as "legacy
row/text coloring shorthand (spec-canonical: rowColor)"
— was already
relayed by both host literals. Only the spec-canonical spelling was missing. The
hosts were not withholding row colour; they were carrying only its deprecated
name.

Stop-condition: searched for, not found

The brief said to stop and report if the object-view route deliberately
excludes this key. Four independent readings, all negative:

  1. No marker, pin, ruling or comment names rowColor as excluded anywhere
    in the repo — a full-tree grep of the identifier returns the type
    declarations, the ListView/ObjectGrid readers, the InterfaceListPage
    relay, the i18n keys, and the userActions toggle. No fence.
  2. git log -S rowColor on both host files returns ZERO commits. The key
    was never relayed and never removed — it was simply never added. A deliberate
    exclusion would have left a deletion.
  3. The objectui#5097 ledger does not cover this. RECORD 1 governs the CAST
    reads off the object-view NODE — the 27 undeclared keys the 2026-08-18 ruling
    exempted. A view-sourced rung adds no cast read, and grouping is the
    in-fence precedent for exactly that shape.
  4. rowColor is published authoring surface elsewhere already — the
    object-grid registration declares it as an input, with the description
    "Rules that colour whole rows from a field value."

The change

Two rungs, one per host, both inside the existing region markers where markers
exist:

  • plugin-view/src/ObjectView.tsxrowColor: activeView?.rowColor, inside the
    fence, next to the legacy color rung. 46 keys becomes 47.
  • app-shell/src/views/ObjectView.tsxrowColor: viewDef.rowColor ?? listSchema.rowColor,
    in fullSchema, the same two-rung shape as the neighbouring description.
    47 named keys becomes 48.

Both rungs are view-sourced only, and that is load-bearing. Neither adds a
cast read off the object-view node. Such a read would put a 28th name on the
objectui#5097 HOST-COMPOSITION exemption whose count the 2026-08-18 maintainer
ruling fixed at 27 — a ruling, not a refactor. A pin in the new plugin-view
test asserts the node-level key stays unreachable, so the shortcut cannot be
taken later without a test going red and naming the ledger.

On the two traps carried from the triage

Trap 1 — the key count. ⚠️ One half of this did not survive re-measurement,
and the correction matters for anyone reading the triage later. Only
plugin-view's literal is fenced by object-view HOST-COMPOSITION SURFACE
markers; app-shell's fullSchema has NO region markers at all (grep for the
marker returns only plugin-view sources and its ledger test). And the two
literals do not carry the same count: plugin-view carries 46, matching the
triage, but app-shell carries 47 named keys plus the host spread, not 46.
Measured with a brace-depth scan of each literal, before and after.

No gate hand-copies either count. The one ledger that counts anything —
plugin-view/src/__tests__/objectViewHostSurface.test.tsx — derives its set from
the fence at test time and pins the CAST reads (27 exempt + 4 declared), not the
literal's key count, so a view-sourced rung leaves it untouched. It is green,
and is included in the runs below precisely because a red there would have been
the #7448 shape.

Trap 2 — #5435. Confirmed independently and NOT held behind it. That card
concerns userActions.rowColor, a boolean permission toggle that
normalizeListViewSchema folds from the legacy showColor flag. This card
concerns top-level rowColor, the row-colour configuration. "May the user open
the colour panel" versus "what the colours are" — same name, different nesting
level, different key. The last case in the new app-shell pin authors the
toggle OFF alongside a colour config and asserts both survive independently, so
a future crossed wire fails instead of passing.

Scheduling. The earlier reading that PR #7332 touches ListView.tsx was
re-checked and is moot either way: this branch does not touch ListView.tsx.

Verification

All runs from the repo root, at the final commit 79e31a597.

Measured in both directions. The pins were written and run BEFORE the fix
existed, so the before-state is a measurement rather than a claim:

  • plugin-view pin, before: 2 failed | 3 passed
    expected undefined to deeply equal { field: 'stage', colors: … }
  • app-shell pin, before: 4 failed | 2 passed, including
    expected { field: 'priority', … } to deeply equal { field: 'stage', … }
    the host-supplied value arriving where the view's own was expected, which is
    the defect stated as an assertion.

Reverse verification, on the committed tree, each rung deleted in turn:

leg mutation proven on disk result restore proven by state
remove plugin-view rung rung occurrences 1 to 0 2 failed | 17 passed blob equals HEAD blob, git diff HEAD empty
remove app-shell rung rung occurrences 1 to 0 4 failed | 2 passed blob equals HEAD blob, git diff HEAD empty

Each leg asserted the anchor was present exactly once before mutating and absent
after, and each restore was proven by comparing the worktree blob hash against
the HEAD blob hash AND requiring an empty git diff HEAD — never by an exit
code. No build or dist is involved: this repo's vitest config aliases every
package specifier to its src, so there is no stale-artifact leg to preflight.

Green at 79e31a597:

  • pnpm exec vitest run packages/plugin-view/Test Files 34 passed, Tests 297 passed
  • pnpm exec vitest run packages/app-shell/src/views/Test Files 370 passed, Tests 3535 passed | 1 skipped
  • pnpm --filter @object-ui/plugin-view type-check — clean; echoed tsc --noEmit && tsc -p tsconfig.test.json
  • pnpm --filter @object-ui/app-shell type-check — clean, same two-program script
  • both new pins confirmed INSIDE the typecheck program via tsc -p tsconfig.test.json --listFiles (1 hit each), so the green covers them rather than merely excluding them
  • eslint on the four changed source files: 0 errors (warnings are the
    pre-existing no-explicit-any population every sibling test carries)
  • pnpm --filter @object-ui/plugin-view --filter @object-ui/app-shell lint — 0 errors
  • gates implicated by these paths, each printing its own verdict line:
    check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit,
    check:phantom-deps, check:self-import, check:changeset-no-major — all OK
  • check:governed-queue-guard --test on the five changed paths: NOT GOVERNED

Narrowings, declared

  • Repo-wide pnpm lint (turbo run lint) was not run locally; the two
    affected packages were linted in full instead, which is the same unit turbo
    invokes for them. Safe to narrow here because this repo enables NO type-aware
    linting (no projectService and no parserOptions.project anywhere in
    eslint.config.js), so a change confined to two files cannot move the verdict
    on a file it does not touch, and no rule block is scoped to either changed
    path. What it could have caught and did not: a lint regression in a package
    this diff does not touch. CI runs the full farm regardless.
  • The remaining check:* farm was not enumerated locally. The aggregate
    pnpm check needs the CLI built; the gates whose inputs this diff actually
    moves are listed above and are green. What this could have caught: a gate
    keyed on a path class I did not recognise as implicated.
  • plugin-list / plugin-grid tests were not re-run. No file in either
    package changed, and vitest resolves package specifiers to src, so no
    rebuilt artifact can reach them. The consumer half — that a rowColor which
    ARRIVES then colours a row — is useRowColor's, already covered there, and is
    not re-pinned here; this PR pins the relay, exactly as A list view's description is served but never rendered — ObjectView's relay drops it on the way to ListView #7199 split the same
    two halves.
  • No browser/dogfood run. The claim is about what each host hands down, so
    both pins capture the schema at the boundary rather than rendering it.

Reviewer notes

⛔ Draft, per the dispatch: not marked ready, no auto-merge, no self-review.

#7516 is NOT addressed here and remains open — it is the other half of the split
card and is awaiting a ruling. #5435 also remains open and is neither a
duplicate of this nor a blocker on it, for the reason measured above.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…objectui#7218)

`rowColor` is a declared `ListViewSchema` member (by reference from
`@objectstack/spec`, `{ field, colors? }`) that `ListView` reads to seed its
`rowColorConfig` state. Neither object-view host relayed it, so an authored
per-view row colour was unreachable on that route: authored, validated, built
and served, then dropped at the relay, with no error and every authoring gate
green. Same "declared and inert" shape objectui#7199 fixed for `description`.

The interface route (`InterfaceListPage.tsx`) has shipped
`rowColor: view.rowColor` next to `grouping`/`pagination` since ADR-0047, with
no fence — so the delivery path already existed and two of three hosts simply
did not use it. The legacy shorthand for the same feature (bare `color`) already
had a rung in both literals; only the canonical spelling was missing.

Both rungs are view-sourced only. Neither adds a cast read off the object-view
node, which would have put a 28th name on the objectui#5097 HOST-COMPOSITION
exemption the 2026-08-18 ruling fixed at 27 — a ruling, not a refactor.
`grouping` is the in-fence precedent for a view-only rung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3182.7 KB 3191.4 KB
Main entry chunk (gzip) 143.2 KB 350 KB
Entry file index-CaY6pkcj.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 516.19KB 117.80KB
core (index.js) 6.12KB 2.42KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 180.00KB 50.20KB
fields (index.js) 242.40KB 61.26KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 48.15KB 13.35KB
plugin-charts (index.js) 70.92KB 19.75KB
plugin-chatbot (index.js) 196.19KB 46.43KB
plugin-dashboard (index.js) 132.89KB 34.68KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 250.93KB 64.09KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.87KB 32.66KB
plugin-gantt (index.js) 167.44KB 41.05KB
plugin-grid (index.js) 209.25KB 56.71KB
plugin-kanban (index.js) 52.71KB 14.55KB
plugin-list (index.js) 113.28KB 27.59KB
plugin-map (index.js) 20.55KB 6.80KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.57KB 11.96KB
plugin-timeline (index.js) 30.84KB 8.85KB
plugin-tree (index.js) 9.38KB 3.22KB
plugin-view (index.js) 85.24KB 20.94KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.58KB 2.23KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 21:24
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit 34ea56d Sep 3, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-7218-rowcolor-host-relay branch September 3, 2026 21:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two more declared list-view keys never reach ListView: fieldOrder and rowColor are read by the renderer but relayed by neither host

2 participants