Skip to content

fix(app-shell): draw real canvas chrome for renderable-but-unoffered block types - #6072

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5837-canvas-icon-for-unoffered-types
Aug 24, 2026
Merged

fix(app-shell): draw real canvas chrome for renderable-but-unoffered block types#6072
yinlianghui merged 1 commit into
mainfrom
claude/issue-5837-canvas-icon-for-unoffered-types

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5837

The gap

PageBlockCanvas drew every node's icon and colour tone from BLOCK_TYPE_META:

constmeta=BLOCK_TYPE_META[typeStrasBlockTypeId];constIcon=meta?.Icon??UnknownBlockIcon;consttone=resolveBlockTone(typeStr);// -> BLOCK_CATEGORY_TONE['misc'] on a miss

BLOCK_TYPE_META is the palette's offer list — it answers "what may an author drag in". The canvas is asking a different question — "what may an author already have in this page" — and one list was answering both.

For most palette exclusions the two answers coincide and the generic box is the honest chrome. They diverge for an alias pair: record:discussion and record:chatter are one renderer under two names, so the palette advertises exactly one of them to keep one entry per renderer, and the other spelling — which renders perfectly — got UnknownBlockIcon and the neutral misc grey where its twin one line above gets MessageSquare and the blue record tone.

The change

block-types.ts gains BLOCK_RENDERER_ALIAS_GROUPS — block types that are one renderer under several spellings — and resolveBlockDisplayMeta(type), which returns the palette entry when there is one and otherwise borrows an alias sibling's. resolveBlockTone routes through it; the canvas reads it instead of BLOCK_TYPE_META.

Three properties this shape was chosen for:

  • Keyed on renderer identity, not on "is excluded." A group may only list spellings the runtime registry resolves to the very same component, and canvas-display-meta.test.tsx asserts that against the real ComponentRegistry (identity, not "both defined"). A fix keyed on exclusion would hand element:text_input a friendly icon it has not earned.
  • No orientation is hard-coded. Which spelling the palette advertises already flipped once (The page-editor palette offers the legacy record:chatter and (as of the 17.1.0 pin) explicitly excludes the canonical record:discussion — the pair is advertised under the alias #5495 moved it from the legacy alias to the canonical name). Every assertion derives the offered/unoffered split from BLOCK_TYPE_META, and one test flips the catalogue outright to prove the resolver follows — a future flip cannot re-open this.
  • The offer list does not move. Nothing became draggable; this is why the fix is not a second BLOCK_TYPE_META entry, which would break block-config.test.ts's "no type is both offered and excluded".

BlockDisplayMeta deliberately carries only Icon + category, no label: blockLabel() never consulted the catalogue and author-visible naming is a separate decision. The gap is icon + tone only.

Verification — all on 7114898f0

Direction predicted before running: a page carrying the unoffered spelling must draw the same icon and tone as its offered twin, and reverting must turn those pins red.

GateVerdict
pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json)VERDICT command-exit 0
pnpm exec vitest run on the four affected suitesTest Files 4 passed (4) / Tests 53 passed (53)
eslint . in packages/app-shell (whole package, plain, no --no-inline-config)exit 0 — 948 files, 0 errors, 2659 pre-existing warnings; the three changed files contribute 0 errors
node scripts/check-changeset-presence.mjsexit 0 — 1 changeset declared

Counter-probe (a) — the hard constraint, run explicitly and quoted:

✓ |unit| .../__tests__/block-config.test.ts > page palette ↔ spec PageComponentType coverage > no type is both offered and excluded 0ms

Counter-probe (b) — a genuinely non-page-content exclusion keeps the generic box: an exclusion with no renderer twin keeps the generic box covers element:text_input, element:record_picker, element:form and ai:chat_window (all → resolveBlockDisplayMeta undefined, tone misc), and the DOM probe asserts element:text_input's badge keeps the zinc misc classes in the same render as the fixed pair.

Counter-probe (c) — the offer list is unchanged: the drag-in list is exactly the palette catalogue, nothing more derives TYPES_BY_CATEGORY from BLOCK_TYPE_META and asserts no PALETTE_EXCLUSIONS key is draggable; no alias-group member became offerable asserts it for the group members specifically.

Reverse verification. Ablating the alias fallback out of resolveBlockDisplayMeta (mutation proved on disk by grepping the injected text and, separately, the removed text; restore under trap … EXIT INT TERM with a cwd-independent git -C … checkout HEAD --; git diff HEAD --stat empty afterwards) turned exactly the four fix pins red and left both counter-probes green:

× the unadvertised spelling borrows the icon and tone of its twin
× follows the pair when the palette flips which spelling it advertises
× PageBlockCanvas … gets the tone of its twin on the type badge
× PageBlockCanvas … gets the icon of its twin
✓ no type is both offered and excluded
✓ the drag-in list is exactly the palette catalogue, nothing more
Tests 4 failed | 37 passed (41)

No rebuild was needed for the ablation: the suite imports ../block-types by relative path, so no dist/ sits between the mutation and the run.

Measured deltas from the card

Follow-up

This takes the card's smaller shape. The honest larger shape — splitting the catalogue into a display meta table covering every type the canvas can encounter and a palette offer list subset — is more than an S card here: BLOCK_TYPE_META is an exhaustive Record keyed by the whole BlockTypeId union, and it is read by the Add-block picker, the config-panel coverage tests and the spec-derived palette tests, so the split moves that entire surface. The alias-group resolver closes the measured gap and gives that refactor a seam to grow from.


Generated by Claude Code

…block types
`PageBlockCanvas` read each node's icon and colour tone from `BLOCK_TYPE_META`,
which is the palette's OFFER list ("what may an author drag in"). The canvas is
answering a different question ("what may an author already have in this page").
The two diverge for an alias pair: `record:discussion` and `record:chatter` are
one renderer under two names, so the palette advertises exactly one of them and
the other spelling — which renders perfectly — fell through to the unknown-block
box and the neutral `misc` grey.
`block-types.ts` now declares `BLOCK_RENDERER_ALIAS_GROUPS` and resolves display
chrome through `resolveBlockDisplayMeta()`, which falls back to an alias
sibling's entry. Keyed on renderer IDENTITY, not on "is excluded": the guard
test asserts every group member resolves through `ComponentRegistry` to the same
component, so exclusions that genuinely are not page blocks keep the generic
box. The palette offer list is unchanged — nothing became draggable.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3221.0 KB3990.2 KB
Main entry chunk (gzip)153.7 KB350 KB
Entry fileindex-CEAQRapj.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.15KB114.53KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)166.86KB46.08KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.44KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.12KB61.87KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.57KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Page-block canvas draws the generic unknown-block icon for types that render fine but are excluded from the palette (record:chatter)

2 participants

@yinlianghui@claude