Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/site/app/components/InteractiveDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 2 additions & 0 deletions apps/site/app/components/LiveSplitDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions apps/site/app/components/SchemaThumbnail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {} };

Expand Down
37 changes: 37 additions & 0 deletions apps/site/app/components/registerLayoutBlocks.ts
Original file line number Diff line number Diff line change
@@ -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();
1 change: 1 addition & 0 deletions apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
5 changes: 3 additions & 2 deletions content/docs/layout/page-header.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions examples/schema-catalog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
116 changes: 116 additions & 0 deletions examples/schema-catalog/test/pageheader-with-actions.test.tsx
Original file line number Diff line number Diff line change
@@ -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 `<h1>` title, the subtitle, and BOTH schema children in the
* right-hand slot.
*
* (2) is worth a test rather than an eyeball because the registration for
* `page-header` (`packages/layout/src/index.ts`) does NOT declare
* `isContainer: true`, which reads like children could not reach the slot.
* They do: `isContainer` is registry metadata that the render path never
* consults (its consumers are `sdui-parser`'s `not-a-container` diagnostic, the
* Studio palette, and the react-page tag map). `SchemaRenderer` strips
* `children` from the React props but always passes the whole node as `schema`,
* and `PageHeader` re-introduces `schema.children` itself. That is a load-
* bearing coincidence of two files, so it gets a pin.
*
* Module-scope imports, not `beforeAll` (AGENTS.md §测试纪律): the child
* `button` node resolves through `@object-ui/components`' registration
* side-effects, and paying that cost at import time keeps it out of every
* test/hook timeout budget.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import { render, screen } from '@testing-library/react';
import '@object-ui/components';
import { SchemaRenderer } from '@object-ui/react';
import { registerLayout } from '@object-ui/layout';
import { getExample } from '../src/index.js';

const EXAMPLE_ID = 'layout-page-header/pageheader-with-actions';

/**
* Class strings that exist to style the header and live in `PageHeader.tsx`
* (`:210`, `:211`, `:231`, `:233`). Their presence in the example JSON is the
* signature of a hand-rolled copy — the defect, not a styling choice.
*/
const COMPONENT_OWNED_CLASSES = [
'text-2xl',
'tracking-tight',
'text-muted-foreground',
'pb-4',
];

beforeAll(() => {
registerLayout();
});

describe('the page-header docs demo uses the page-header component (#3787)', () => {
it('is rooted at a `page-header` node', () => {
const schema = getExample(EXAMPLE_ID).schema as { type?: string };
expect(schema.type).toBe('page-header');
});

it('declares title/subtitle on the component instead of restating its classes', () => {
const schema = getExample(EXAMPLE_ID).schema as Record<string, unknown>;
expect(schema.title).toBe('Users');
expect(schema.subtitle).toBe('Manage your team members and permissions');
// The third copy of the spacing/typography numbers (#3786) is gone, and
// stays gone: re-hand-rolling the header re-introduces these strings.
const json = JSON.stringify(schema);
for (const cls of COMPONENT_OWNED_CLASSES) {
expect(json, `example JSON should not restate \`${cls}\``).not.toContain(cls);
}
});

it('renders the real header: h1 title, subtitle, and both children in the action slot', () => {
const { container } = render(
<SchemaRenderer schema={getExample(EXAMPLE_ID).schema as never} />,
);

// The title is the document heading — the page's Accessibility section
// claims an `<h1>`, and the hand-rolled demo it replaces emitted a `span`.
const h1 = container.querySelector('h1');
expect(h1?.textContent).toBe('Users');
expect(screen.getByText('Manage your team members and permissions')).toBeTruthy();

// Both children reach the right-hand slot despite the registration not
// declaring `isContainer` — see the module header.
expect(screen.getByRole('button', { name: 'Export' })).toBeTruthy();
expect(screen.getByRole('button', { name: 'Add User' })).toBeTruthy();
});

it('renders the container spacing the docs Styling section states (#3786)', () => {
// Pins the CODE side of the three Container bullets in
// `content/docs/layout/page-header.mdx`: `pb-4` with no responsive
// variant, `gap-3` on the outer column, and a `border-b`. Changing any of
// them turns this red, which is the prompt to update that section — the
// doc drifted precisely because nothing was watching. (Mechanising the
// prose itself is the separate decision #3786 deferred.)
const { container } = render(
<SchemaRenderer schema={getExample(EXAMPLE_ID).schema as never} />,
);
const root = container.querySelector('[data-obj-type="page-header"]');
const cls = root?.className ?? '';

expect(cls).toContain('gap-3');
expect(cls).toContain('pb-4');
expect(cls).toContain('border-b');
// No responsive padding variant — the doc used to promise `pb-8` on desktop.
expect(cls).not.toMatch(/\b(sm|md|lg|xl):pb-/);
});
});
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading