diff --git a/apps/site/app/components/InteractiveDemo.tsx b/apps/site/app/components/InteractiveDemo.tsx index 024ce84f76..e59adf9b5c 100644 --- a/apps/site/app/components/InteractiveDemo.tsx +++ b/apps/site/app/components/InteractiveDemo.tsx @@ -3,6 +3,8 @@ import React, { useMemo } from 'react'; import { SchemaRenderer, SchemaRendererContext } from '@object-ui/react'; import { SidebarProvider } from '@object-ui/components'; +// Registers `page-header` & friends — see the module header (objectui#3787). +import './registerLayoutBlocks'; import type { SchemaNode } from '@object-ui/core'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock'; diff --git a/apps/site/app/components/LiveSplitDemo.tsx b/apps/site/app/components/LiveSplitDemo.tsx index 7d876cbeb0..f14ae5e90c 100644 --- a/apps/site/app/components/LiveSplitDemo.tsx +++ b/apps/site/app/components/LiveSplitDemo.tsx @@ -24,6 +24,8 @@ import { getExample, type Example, } from '@object-ui/example-schema-catalog'; +// Registers `page-header` & friends — see the module header (objectui#3787). +import './registerLayoutBlocks'; const PRESET_IDS = [ 'auth/login-simple', diff --git a/apps/site/app/components/SchemaThumbnail.tsx b/apps/site/app/components/SchemaThumbnail.tsx index 3a4a5fb798..7c24380a31 100644 --- a/apps/site/app/components/SchemaThumbnail.tsx +++ b/apps/site/app/components/SchemaThumbnail.tsx @@ -19,6 +19,8 @@ import React, { import { SchemaRenderer, SchemaRendererContext } from '@object-ui/react'; import { SidebarProvider } from '@object-ui/components'; import type { SchemaNode } from '@object-ui/core'; +// Registers `page-header` & friends — see the module header (objectui#3787). +import './registerLayoutBlocks'; const defaultCtx = { dataSource: {} }; diff --git a/apps/site/app/components/registerLayoutBlocks.ts b/apps/site/app/components/registerLayoutBlocks.ts new file mode 100644 index 0000000000..7b3472eb0b --- /dev/null +++ b/apps/site/app/components/registerLayoutBlocks.ts @@ -0,0 +1,37 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + */ + +/** + * Registers `@object-ui/layout`'s blocks (`page-header`, `app-shell`, + * `sidebar-nav`, …) for the docs site's schema renderers. + * + * Why this exists (objectui#3787): the site renders catalog examples through + * `SchemaRenderer`, and `ComponentRegistry` only knows a type once the package + * owning it has been loaded. `@object-ui/components` registers its own blocks + * as an import side-effect and every renderer host already imports it, so + * `div`/`text`/`button` resolved — but nothing pulled in `@object-ui/layout`, + * so `page-header` resolved to nothing and rendered the red + * "Unknown component type" panel (OBJUI-001). It was not noticed because no + * example used the component: the one demo on the PageHeader docs page + * hand-rolled the header out of `div`s instead, which is the defect #3787 is + * about. + * + * Imported for effect by EVERY host that renders a catalog example + * (`InteractiveDemo`, `SchemaThumbnail`, `LiveSplitDemo`) rather than wired + * into one docs page: `SchemaThumbnail` renders the whole catalog on + * `/docs/guide/schema-catalog`, so a page-local loader would fix the component + * page and leave that index showing the error panel for the same example. + * + * `registerLayout()` is called EXPLICITLY even though the package body also + * calls it on load: `@object-ui/layout` declares `"sideEffects": false` + * (`packages/layout/package.json`), which permits a bundler to drop a module + * imported only for its side-effects. A named import that is actually invoked + * cannot be dropped. Registration is idempotent, so the double call is safe, + * and doing it at module scope (not in an effect) means it has already happened + * for the server render — the demos stay in the prerendered HTML. + */ +import { registerLayout } from '@object-ui/layout'; + +registerLayout(); diff --git a/apps/site/package.json b/apps/site/package.json index 84b7e66eba..2b8fef56d9 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -17,6 +17,7 @@ "@object-ui/core": "workspace:*", "@object-ui/example-schema-catalog": "workspace:*", "@object-ui/fields": "workspace:*", + "@object-ui/layout": "workspace:*", "@object-ui/plugin-calendar": "workspace:*", "@object-ui/plugin-charts": "workspace:*", "@object-ui/plugin-chatbot": "workspace:*", diff --git a/content/docs/layout/page-header.mdx b/content/docs/layout/page-header.mdx index 38c11e463b..e6f90888ec 100644 --- a/content/docs/layout/page-header.mdx +++ b/content/docs/layout/page-header.mdx @@ -150,8 +150,9 @@ The PageHeader uses a flex layout: ### Container -- Padding bottom: `pb-4` on mobile, `pb-8` on desktop -- Gap: `gap-4` between elements +- Padding bottom: `pb-4` at every breakpoint — there is no responsive variant +- Border: `border-b` along the bottom edge +- Gap: `gap-3` on the outer column; the title row itself uses `gap-x-4 gap-y-2` ## Usage with Page Component diff --git a/examples/schema-catalog/package.json b/examples/schema-catalog/package.json index efed5ffcac..9533fde03e 100644 --- a/examples/schema-catalog/package.json +++ b/examples/schema-catalog/package.json @@ -29,7 +29,9 @@ "@object-ui/types": "workspace:*" }, "devDependencies": { + "@object-ui/components": "workspace:*", "@object-ui/core": "workspace:*", + "@object-ui/layout": "workspace:*", "@object-ui/react": "workspace:*", "typescript": "^6.0.3" } diff --git a/examples/schema-catalog/src/schemas/layout-page-header/pageheader-with-actions.json b/examples/schema-catalog/src/schemas/layout-page-header/pageheader-with-actions.json index 063ea366e9..2231cb9114 100644 --- a/examples/schema-catalog/src/schemas/layout-page-header/pageheader-with-actions.json +++ b/examples/schema-catalog/src/schemas/layout-page-header/pageheader-with-actions.json @@ -1,49 +1,17 @@ { - "type": "div", - "className": "space-y-6", + "type": "page-header", + "title": "Users", + "subtitle": "Manage your team members and permissions", + "icon": "users", "children": [ { - "type": "div", - "className": "flex flex-col gap-4 pb-4", - "children": [ - { - "type": "div", - "className": "flex items-center justify-between gap-4", - "children": [ - { - "type": "div", - "className": "flex flex-col gap-1", - "children": [ - { - "type": "text", - "content": "Users", - "className": "text-2xl font-bold tracking-tight" - }, - { - "type": "text", - "content": "Manage your team members and permissions", - "className": "text-sm text-muted-foreground" - } - ] - }, - { - "type": "div", - "className": "flex items-center gap-2", - "children": [ - { - "type": "button", - "label": "Export", - "variant": "outline" - }, - { - "type": "button", - "label": "Add User" - } - ] - } - ] - } - ] + "type": "button", + "label": "Export", + "variant": "outline" + }, + { + "type": "button", + "label": "Add User" } ] } diff --git a/examples/schema-catalog/test/pageheader-with-actions.test.tsx b/examples/schema-catalog/test/pageheader-with-actions.test.tsx new file mode 100644 index 0000000000..c0b5ac3d27 --- /dev/null +++ b/examples/schema-catalog/test/pageheader-with-actions.test.tsx @@ -0,0 +1,116 @@ +/** + * The PageHeader docs demo renders the COMPONENT, not a hand-rolled header + * (objectui#3787). + * + * `layout-page-header/pageheader-with-actions` is the only runnable example on + * `content/docs/layout/page-header.mdx`. It used to be a `div`/`text`/`button` + * tree carrying Tailwind classes copied out of `PageHeader.tsx` — so the page + * documenting the component shipped a copy-paste reference that told authors + * (AI authors included) to bypass it, it exercised none of `page-header`'s + * rendering, and it held a third, already-drifted copy of the component's + * spacing numbers (objectui#3786 fixed the second copy, in the prose). + * + * Two things are pinned here, and they are different facts: + * + * 1. SHAPE — the example's root node is a `page-header`, and it contains none + * of the class strings that only exist inside `PageHeader.tsx`. This is the + * regression that would fire if anyone hand-rolls the header again. + * 2. RENDER — driven through the real `SchemaRenderer`, the node produces the + * header: an `