Repository navigation
feat(components): the record chrome draws the record's picture from the object's imageField (objectui#11383) - #11619
Merged
objectstack-fleet[bot] merged 4 commits intoOct 4, 2026
Conversation
…he object's `imageField` (objectui#11383)
`page:header`'s record chip now draws the record's picture beside the title,
from the field the object names in its object-level `imageField`
(`@objectstack/spec` 17.6.0, objectstack#21182; ruling A on hotcrm#1199).
The value is the served row's: the expanded `{ url }`, a bare `sys_file` id
(drawn from the stable download path), a URL string, or the first drawable
entry of a list. No declaration, an empty value or a masked (absent) field
draws nothing, with no initials or placeholder; `recordChrome: false` keeps
the bare header. No `page:header` prop and no conventional field name is read.
The URL resolution is a private copy of the forms `readFileValue` in
`@object-ui/fields` resolves: `@object-ui/fields` depends on this package, so
importing it would close a cycle, and moving it into `@object-ui/core` would
add a public export this change does not make.
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
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
Size Limits
|
…lues`, and document the picture (objectui#11383) A test-only parity pin in `@object-ui/fields` renders `page:header` through the real `SchemaRenderer` and registry over every read form the header admits, and asserts the drawn `src` is the URL the image cell draws for the same value (the first `readFileValues` view with a `url`), or that neither draws. The one deliberate divergence, an id-only object, is asserted as a divergence. No export and no source change in `@object-ui/fields`. `content/docs/layout/page-header.mdx`: the record chip's description names the record's picture, shown when the object names an `imageField`. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
… that keeps it with `readFileValue` (objectui#11383) Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
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
Size Limits
|
Contributor
✅ Console Performance Budget
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
Size Limits
|
objectstack-fleet
Bot
deleted the
claude/issue-11383-record-chrome-image
branch
October 4, 2026 17:29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #11383
Clause-②: no
The record chrome (
page:header's record chip) now draws the record's picture beside the title, from the field the object names in its object-levelimageField(@objectstack/spec17.6.0, objectstack-ai/objectstack#21182). This is the renderer half of ruling A on objectstack-ai/hotcrm#1199.Session:
https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL(dispatched os-dev run for thedomain:uiseat 1 claim on this card).What changed
packages/components/src/renderers/layout/containers.tsx, the record branch ofPageHeaderRenderer: readsobjectSchema.imageField, takes the served row's value for that field, and passes the picture to the existingiconslot ofRecordTitleChip. Nothing else in the header moves.Avatar+AvatarImagewith noAvatarFallback. It renders only once the image has loaded, so there is no initials or placeholder avatar, and a URL that fails to load leaves no broken-image icon.avatarfield is drawn round and cropped (rounded-full,object-cover). Animagefield, such as a logo, is drawn whole in a rounded square (rounded-md,object-contain). Both are 32px.imageField;null,'',[], or an object with nourl);recordChrome: falsekeeps the bare header, picture or not. The record branch is skipped, as it was before.page:headerprop is read, and no field is read by a conventional name (logo,avatar,image, …)..changeset/11383-record-chrome-image.md,@object-ui/componentsminor (a new user-visible capability).No
@object-ui/*export, prop, type member or i18n key is added. Theiconslot ofRecordTitleChipalready exists, and the object schema is read untyped, as the title rungs already read it.Measured premises (the dispatch's mechanism assumptions)
imageFieldat parse. Measured on the installed@objectstack/spec17.6.0data.ObjectSchema.safeParse. It acceptsimageField: 'logo'naming animagefield and naming anavatarfield. It refuses an undeclared name (declares no field) and atextorfilefield (which is not a picture). A bogus sibling keyimageFieldBogusis refused as unrecognized, which is the control. So the renderer does not check the field's type again.ctx.objectSchemaandctx.dataare both in hand where the title is resolved fromnameField. The console's record page (RecordDetailView) fetches the row withfindOneand no$select, with$expandon relations only, so the picture field arrives with the row.image/avatarisvalueSchemaFor(def, 'expanded'): a baresys_fileid orFileValueSchema({ url, name?, size?, mimeType?, alt? }, whereurlis required), and an array whenmultiple. The server'sresolveFileReferences(objectql engine) hydrates a stored id into{ id, name, size, mimeType, url }. It passes a URL string or an inline blob through unchanged, and leaves a bare id when thesys_filelookup fails or the id matches no committed row.readFileValue/readFileValuesin@object-ui/fields(its file-value module), whichImageCellRendererdraws.@object-ui/fieldsdepends on@object-ui/components, so importing it from components closes a package cycle. Moving it down into@object-ui/coreadds core exports, which theClause-②: nofence forbids.recordPictureUrl, for exactly the spec's read forms. An expanded object gives itsurl. A bare id (judged by the spec's ownisFileIdToken, imported from@objectstack/spec/data) gives the stable download path/api/v1/storage/files/ID. A URL string is drawn as it is. For a list, the first entry that resolves is drawn.readFileValue's id-only-object arm: an object withouturlis not a read form the spec admits.packages/fields/src/widgets/file-value.recordChromeParity-11383.test.tsxkeeps the two together (seat ruling5981856796on the card, option C); see Tests.object.imageFieldflips at objectstack's.objectui-shapin bump.One deviation from the suggested route. The picture's accessible name is not the record title. The chip's
iconslot isaria-hidden, and the H1 directly beside the picture already names the record, so the picture is decorative and itsaltis empty. Naming it with the title would read the same name twice.Tests
New pin:
packages/components/src/__tests__/page-header-record-picture-11383.test.tsx, 13 cases, through the realSchemaRendererand registry inside aRecordContext:{ url }value is drawn in the title row, ahead of the H1, decorative (alt="", insidearia-hidden).sys_fileid is drawn from the stable download path.multiplelist, the first drawable entry is drawn, and only one image.imagefield is drawn as a rounded square withobject-contain; anavatarfield is drawn round withobject-cover.imageField, a row carryinglogo/avatar/imagedraws nothing.null,'',[], or an object with nourl. Each case's header markup equals the undeclared header's markup.img. This case uses the realwindow.Image; the other cases use theLoadedImagestand-in from the@object-ui/fieldsavatar tests.recordChrome: falsegives a header whose markup equals the undeclared one.Ablations, each run on the committed fix through objectstack's
scripts/ablation-replace.mjs. Each mutation was proven on disk (anchor count 1 → 0, blob changed). Each restore was proven by blob == HEAD and an emptygit diff HEAD.icon={recordPicture}: 6 failed | 7 passed. Every case that draws a picture went red. The 7 that stayed green are the "nothing is drawn" controls, as expected.objSchema?.imageField ?? 'logo': 1 failed. The "logo / avatar / image keys" control went red.imageFieldis declared (pictureFieldName ? (in place ofpictureUrl ? (): 5 failed. All five empty-value controls went red, so "nothing" cannot hide an empty wrapper.Parity pin (patch round 1,
e2df897):packages/fields/src/widgets/file-value.recordChromeParity-11383.test.tsx, 19 cases (11 drawn, 7 empty, 1 divergence). It renderspage:headerthrough the realSchemaRendererover every read form the header admits and asserts that the drawnsrcequals the URL the image cell draws, or that neither side draws. The id-only object is asserted as an explicit divergence: fields resolves it, the header draws nothing. Ablation on committed code,RECORD_PICTURE_FILE_PATH/api/v1/…→/api/v2/…: 3 failed | 16 passed (exactly the three bare-id cases); restored blob == HEAD. Patch round 2 (1a13128) is comment-only: therecordPictureUrldocblock now names this pin.Gates (local; CI runs the full farm)
pnpm exec vitest run packages/components/(one run, 358 passed + 1 skipped files, 3643 tests passed)711adb9(pre-merge)--shard=1/2+--shard=2/2(180 + 179 files, 1754 + 1889 tests passed)4a34ea4pnpm --filter "@object-ui/components^..." buildthenpnpm --filter @object-ui/components type-check(bothtscpasses;--listFilesconfirms both the new test andcontainers.tsxare compiled)4a34ea4pnpm --filter @object-ui/components lint(0 errors; no warning on a changed line)4a34ea4check:new-line-citations·check:control-bytes·check:phantom-deps711adb9check:changeset-claims·check:pending-changeset-literals·check:unreferenced-sources·check:test-path-roots·check-changeset-no-major·check-changeset-presence4a34ea4The package's script is spelled
type-check(with a hyphen); atypecheckfilter matches no script.NOT MEASURED locally:
check:spec-floorsandcheck:eager-closure. The first reads every released package's builtdist/, and the second reads the console's production build. This run built neither. The fact the floor gate asks about was read directly instead:isFileIdTokenis exported from@objectstack/spec/dataat17.0.0(bothdist/data/index.d.tsandindex.mjsof the npm tarball), which is@object-ui/components' declared floor.@object-ui/corealready imports@objectstack/spec/dataat runtime, so that module was already in the package's closure.Acceptance notes
content/docs/layout/page-header.mdx: the record-chip bullet now names the record's picture, shown beside the heading when the object names animageFieldand the record's value for it is not empty.RecordDetailPanel(@object-ui/plugin-detail) draws its ownDetailViewheading, notpage:header, so it shows no picture. The card scopes the change topage:header; this is noted, not changed.imageFielddescribe in@objectstack/specstill reads "Pending renderer: the record chrome does not draw it yet." That text belongs to objectstack, and it moves with the liveness row at the pin bump.Generated by Claude Code