Repository navigation
Description
Four page-component types that @objectstack/spec declares as first-class, no-props members of PageComponentType have no renderer in the console: each one draws the literal scaffold text Component Placeholder with its own type name above it, on the page where an author placed it.
Measured in a real browser against a running app — not read off source. Downstream tracking card: objectstack-ai/hotcrm#734 (half 1).
Environment
@objectstack/*17.1.0 (every package in the app pinned to it; nothing bumped for the measurement)- App: HotCRM at
objectstack-ai/hotcrm@f685136, served byobjectstack dev -p 4001, seed data loaded on first boot - Browser: Chromium 1194 via Playwright, admin-authenticated as the seeded dev admin (
admin@objectos.ai), viewport 1600×1200 - Node 22.22.2, pnpm 10.33.0
Reproduction
Sign in to the console as an admin and open, in the published app:
/_console/apps/app.objectstack.hotcrm/page/sales_home_page
/_console/apps/app.objectstack.hotcrm/page/app_launcher_page
/_console/apps/app.objectstack.hotcrm/page/utility_bar_page
What renders
All five component instances across the three pages draw the placeholder. Each is a dashed-outline box containing a document-with-question-mark glyph, the component type in monospace, and the caption Component Placeholder:
| Page | Component | Renders |
|---|---|---|
sales_home_page |
nav:menu (inside the Quick Create card) |
⛔ nav:menu / Component Placeholder |
app_launcher_page |
global:search |
⛔ global:search / Component Placeholder |
app_launcher_page |
app:launcher |
⛔ app:launcher / Component Placeholder |
utility_bar_page |
global:notifications |
⛔ global:notifications / Component Placeholder |
utility_bar_page |
global:search |
⛔ global:search / Component Placeholder |
Occurrences of the literal string Component Placeholder in document.body.innerText: 1 on sales_home_page, 2 on app_launcher_page, 2 on utility_bar_page.
Verbatim innerText from the app_launcher_page main region:
Central hub for accessing all applications
global:search
Component Placeholder
App Launcher
Select an app to get started
app:launcher
Component Placeholder
and from utility_bar_page:
Utility Bar
Quick access utility bar with floating tools
global:notifications
Component Placeholder
Quick Notes
global:search
Component Placeholder
This is not a page-framework failure — the same pages render everything else correctly. On sales_home_page the four object-metric KPI cards pull real figures (Revenue (Won) 1,290,000.00 / Deals Won 8 / Pipeline Value 3,775,000.00 / Open Leads 21), the embedded list views return 21 / 10 / 6 real rows, and page:header draws. Only these four component types fall through to the placeholder.
Why this is a platform gap and not app metadata using a wrong spelling
All four are named members of the PageComponentType enum shipped in @objectstack/spec@17.1.0, and each has a deliberate no-props row in ComponentPropsMap:
dist/ui/index.d.ts:
readonly 'app:launcher': z.ZodObject<{}, z.core.$strict>;
readonly 'nav:menu': z.ZodObject<{}, z.core.$strict>;
readonly 'global:search': z.ZodObject<{}, z.core.$strict>;
readonly 'global:notifications': z.ZodObject<{}, z.core.$strict>;dist/ui/index.js:
"app:launcher": emptyProps("app:launcher"),
"nav:menu": emptyProps("nav:menu"),
"global:search": emptyProps("global:search"),
"global:notifications": emptyProps("global:notifications"),where emptyProps builds a strict, intentionally empty props object whose own history string is explicit that the emptiness is the point:
var emptyProps = (type) => strictObject(
{
surface: `this \`${type}\` component`,
history: `\`${type}\` declares no props at all. ${PROPS_HISTORY}`,
guidanceSets: COMPONENT_LEVEL_GUIDANCE
},
{}
);So the app is using the spec's sanctioned spelling, with the exact props shape the spec prescribes (properties: {}), and objectstack validate passes clean on all three pages. The metadata is right; the renderer is missing.
Impact
An end user who reaches any of these pages sees developer scaffolding. app_launcher_page and utility_bar_page are, between them, entirely placeholder plus one empty card — there is no non-scaffold content on either page. Any app that composes a launcher, a global search box, a notifications panel or a nav menu from the spec's own vocabulary gets the same result.
Ask
Either implement renderers for the four types, or — if they are not intended to be author-placeable on a page — make that explicit in the spec and in validate so the failure lands at author time rather than in front of a user. Shipping them as first-class enum members with prescribed props and no renderer is the state that produces this report.
Notes for whoever picks this up
- Screenshots of all five placeholder instances were captured during the measurement and are archived on the downstream card, [17.0-rc2验收] sales_home_page / app_launcher_page / utility_bar_page 多个组件渲染为「Component Placeholder」,首页欢迎语用户名插值为空 hotcrm#734.
⚠️ The downstream app metadata is deliberately being left untouched: these three pages are currently the only reproduction of this gap, so "fixing" them app-side would delete the evidence rather than the defect.- Adjacent but separate, and not part of this report:
page:cardcomponents declaring atitleand nobodyrender as titled empty boxes (Quick Notesonutility_bar_page,Recent Items/Today's Scheduleonsales_home_page). That is a container with nothing to draw, not a missing renderer. - Small spec-accuracy note while looking:
PageComponentSchema.typeis az.ZodUnion<[z.ZodEnum<{…}>, z.ZodString]>, so despite being an enumerated vocabulary it accepts arbitrary strings at parse time. That is why an unimplemented type reaches the renderer as a plain string and lands on the placeholder branch rather than being rejected. It does not weaken anything above — the four names are declared members with declared props — but it is likely relevant to any fix that wants author-time rejection.
Activity
os-steve commentedon Aug 26, 2026
Maintainer ruling recorded — A, phased; B for any member not planned within the horizon
Provenance: maintainer, 2026-08-26, live PM chat (decision-inbox batch 3, session
session_01JANH3y7qe3MD8aLaLXci8N), verbatim: 「其他接受你的建议」 — accepting the presented recommendation.app:launcherandnav:menu— the two purely metadata-driven members with no external data-source dependency. Landing is objectui's console; the spec seat decomposes contract-first (parent card + per-repo sub-issues withBlocked-by:) per the standing cross-repo split.global:search/global:notifications— read platform data-source readiness FIRST; a member with no data-source plan inside the horizon is retired per B (ADR-0087 channel, author-time loud rejection) rather than left as a declared-unrendered trap.PageComponentSchema.type's union accepts arbitrary strings at parse time, which is why an unimplemented type reaches the renderer as a plain string; the author-time rejection half gets handled alongside whichever phase touches the enum.State:
needs-user-decision→pm:queue(domain:specstands; spec seat owns the decomposition).Generated by Claude Code
claude commentedon Aug 30, 2026
State:
pm:queue→pm:blocked(domain:spec seat, sessionsession_01KX8wnyjStaZcuMyAMNsy3N).Blocked-by: objectstack-ai/objectui#6661
This card is the COORDINATION PARENT per the 2026-08-28 decomposition — never dispatched itself. Status of the two dispatch units: #12950 (Phase 2) is CLOSED ✅; objectui#6661 (Phase 1 renderers) remains the only open dependency. A queue label on a never-dispatchable parent misleads the dispatch scan; the machine-readable edge above is what the unlock scan should watch. Parent closes when #6661 closes (keep-decisions, if any, recorded here per the decomposition note).
Generated by Claude Code
os-zhuang commentedon Sep 2, 2026
Closing as completed — unlock scan by the
domain:specseat (session_01GDA48PuRFrHyRfdkBz8m21), release double-check per the state-machine rules: ① the most recentpm:blockedtransition (comment 5466679802) named exactly one condition — objectui#6661 closes — and it closed 2026-08-31T20:38Z as completed via merged objectui PR #7091 (app:launcher+nav:menuconsole renderers); ② no PR merged against this card after that comment (closed-by list empty). All three sub-issues are closed completed: objectui#6661 (Phase 1 renderers), #12950 (Phase 2 readiness read + thecomponent-type-unknownauthor-time gate, PR #13117), objectui#6757 (Phase 2 renderers forglobal:search/global:notifications, filed after the readiness read showed both data sources shipped).Keep-decisions recorded here per the 2026-08-28 decomposition note: the Phase-2 readiness read (PR #13117) evidenced both
global:searchandglobal:notificationsdata sources SHIPPED, so both members STAY declared — no B-retirement was exercised; the keep verdict is pinned spec-side (component-type-vocabulary.test.ts) and the arbitrary-string parse acceptance onPageComponentSchema.typewas closed by the same PR. The hotCRM repro pages (hotcrm#734) were kept untouched until the renderers landed, as the ruling required; updating them is that repo's follow-up, not this card's.pm:blockedstripped in the same stroke;domain:specstays as attribution. Related but separate: #14159 (user:profile, the one sibling the 2026-08-26 ruling never covered) remains in the lane queue.Generated by Claude Code
docs(lint): re-anchor the dead tracker citations in packages/lint/src…