Skip to content

Commit abba2c4

Browse files
docs(sdk): fold objectui.org into objectstack docs, K1 — move the Object UI SDK guide and 57 renderer-type pages into content/docs/sdk (#27168)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoPtdjqtKpSx1fGxRsHcdHte)_ Fixes #27140 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 新开入口「Custom front-end」(`content/docs/frontend/`,侧栏在 API & SDK 之后)。入口页先分三条路:在 Console 里加自定义页面去 React pages,自己的前端读写数据去 Client SDK,自己的 React 应用里渲染元数据才是 ObjectUI SDK。objectui.org 的 95 页按你定的方案 B 精简后放进来:SDK 专属的 16 页指南、UI 插件总览和 15 个插件页保留,57 个渲染器类型页合成一页清单,重复的 schema 总览、schema 参考、报表插件页删掉。 - **为什么改:** objectui.org 要撤掉;你指出 `sdk` 放最外层有歧义、`ui/` 只该放 UI 元数据,并选了「新入口 + 方案 B」(评估文档在项目线程里)。 - **风险与回滚:** 只动文档。新增 34 页,另有四个现有页面各加了几节:`data-modeling/field-types`、`ui/pages`、`ui/forms`、`ui/reports`,共 +495 行,内容取自被删的 SDK 页。`ui/content/docs` 没动,`apps/docs` 不加依赖。回滚就是 revert 本 PR。 - **先看:** `content/docs/frontend/index.mdx`(三条路的入口页)、`content/docs/frontend/renderers.mdx`(57 个类型合成的清单)、`content/docs/ui/pages.mdx` 的 Element data sources 一节(并入的最大一块,+302 行)。 - **两分钟验证:** 本地 `pnpm docs:dev`,打开 `/docs/frontend`,侧栏应出现「Custom Front-end」,入口页顶部是三行选择表。 - **需要你做的:** 本 PR 要在 K3 #27179 之后合并:插件页链到 K3 生成的 `/docs/ui/components/plugins#…`,K3 没合之前 `check:doc-anchors` 会报这 15 个链接。K3 合并后我合一次 main 转绿,超过 3000 行,由你合并。 ## What changes - **New entry `content/docs/frontend/` ("Custom front-end")**, listed in `content/docs/meta.json` after `api`. Its landing page is a three-row chooser: a custom page inside the Console → [React pages](/docs/ui/react-pages) (metadata, stays in `ui/`); data access from your own front-end → [Client SDK](/docs/api/client-sdk); rendering ObjectStack views and forms in your own React app → the ObjectUI SDK, which is the rest of the section. `ui/` keeps metadata only. - **ObjectUI SDK pages kept (flattened):** quick-start, building-crud-app, architecture-overview (now links `concepts/architecture` for the protocol layers), schema-rendering, component-registry, fields, data-source, layout, app-shell, sidebar-nav, theming, plugin-development (now says it is the front-end kind and links `plugins/development`), cli, create-plugin, data-objectstack, troubleshooting, and `plugins/` (the UI plugins overview plus 15 plugin pages). - **Removed as duplicates:** `guide/schema-overview` (the landing page covers it) and `api/schema-reference` (1443 lines; the generated component reference and `references/ui` cover it, and it carried stale shapes such as a detail view holding `tabs` beside `columns`). - **Merged:** `core/schema-renderer` into `schema-rendering` (its one unique section, the `data-obj-id` / `data-obj-type` attributes, checked in `SchemaRenderer.tsx`); the old plugin gallery `plugins/index` into the UI plugins overview, which gained `plugin-list` and `plugin-detail` entries and a line separating it from runtime plugins; `plugin-report` into `ui/reports`. - **57 renderer-type pages → `renderers.mdx`.** None of the 57 types is in `sdui.manifest.json`, so an ObjectStack page cannot use them; the page says so in a callout and lists each type with its description and the source file holding its `ComponentRegistry.register` call (52 in `@object-ui/components`, 3 in `@object-ui/plugin-view`, each path checked). - **Plugin pages trimmed** (calendar, charts, dashboard, form, gantt, grid, kanban, list, map, markdown, timeline): the `Schema API` section, the catalog `Example schemas` gallery and the `Examples` section are removed (plus calendar/gantt/map `Configuration` and dashboard's filter, widget-options and drill sections, which `ui/dashboards` already has). Each page now links its types' generated entries in `/docs/ui/components/plugins` (K3) and keeps installation, React usage, component props, events, data providers and TypeScript. Chatbot, editor, view and detail document types outside the manifest or React component APIs and keep their schema sections. - **Metadata the site lacked, moved to the metadata pages** (text moved as written, headings re-levelled): - `data-modeling/field-types`, under "How the Console renders a field": polymorphic pointer pairs (`referenceVia`, `referenceDeletedWhen`), naming a registry item by name (`position-ref`, `permission-ref`), a widget's face on the record heading. The number-input section was already there from K2. - `ui/pages`, under "Element data sources": how a saved `view` combines with the binding, and the per-block table of which `dataSource` keys each block honours (with `parentDataSource` and `{record_id}`). `frontend/data-source` keeps the SDK side (`useElementDataSource`, the host's `apiFetch`, `ElementApiOpenScope`) and links here. - `ui/forms`: "What an edit save writes". - `ui/reports`: where the numbers come from, stored pre-9.0 documents (the lossy bridge, confirmed in `ReportRenderer.tsx`), and the filter-time CEL limitation. - **Live demos → static code blocks** (unchanged from the first revision): every `SchemaExample` became a fenced `json` block; no `@object-ui/*` dependency in `apps/docs`. - **Gate bookkeeping:** `scripts/docs-audit/handwritten-docs.json` regenerated with `--write`; `scripts/role-word-baseline.json` is a pure relocation of the rows the maintainer approved (5 `sdk/` rows, 29 occurrences, out; 3 `frontend/` rows, 26, in; the 3 others left with the removed pages). ⛔ Nothing under `ui/` is edited or deleted; `ui/apps/site` is untouched. K4 does both. ## Old path → new path (for K4's redirect map) Every row is `/docs/PATH` on www.objectui.org → its target on objectstack.ai. 95 rows: 19 moved, 14 moved and trimmed, 60 merged into another page (57 renderer pages land on their group's heading in `renderers`), 2 removed and pointed at the page that covers them. <details><summary>95 rows</summary> | # | Source file (`ui/content/docs/`) | Old URL (www.objectui.org) | New URL (objectstack.ai) | Change | |---|---|---|---|---| | 1 | `index.md` | `/docs` | `/docs/frontend` | moved | | 2 | `guide/quick-start.md` | `/docs/guide/quick-start` | `/docs/frontend/quick-start` | moved | | 3 | `guide/building-crud-app.md` | `/docs/guide/building-crud-app` | `/docs/frontend/building-crud-app` | moved | | 4 | `guide/architecture-overview.md` | `/docs/guide/architecture-overview` | `/docs/frontend/architecture-overview` | moved | | 5 | `guide/schema-overview.md` | `/docs/guide/schema-overview` | `/docs/frontend` | removed | | 6 | `guide/schema-rendering.md` | `/docs/guide/schema-rendering` | `/docs/frontend/schema-rendering` | moved | | 7 | `guide/component-registry.md` | `/docs/guide/component-registry` | `/docs/frontend/component-registry` | moved | | 8 | `guide/fields.md` | `/docs/guide/fields` | `/docs/frontend/fields` | trimmed | | 9 | `guide/plugins.md` | `/docs/guide/plugins` | `/docs/frontend/plugins` | moved | | 10 | `guide/plugin-development.md` | `/docs/guide/plugin-development` | `/docs/frontend/plugin-development` | moved | | 11 | `guide/data-source.md` | `/docs/guide/data-source` | `/docs/frontend/data-source` | trimmed | | 12 | `guide/layout.md` | `/docs/guide/layout` | `/docs/frontend/layout` | trimmed | | 13 | `guide/theming.md` | `/docs/guide/theming` | `/docs/frontend/theming` | moved | | 14 | `guide/troubleshooting.md` | `/docs/guide/troubleshooting` | `/docs/frontend/troubleshooting` | moved | | 15 | `core/schema-renderer.mdx` | `/docs/core/schema-renderer` | `/docs/frontend/schema-rendering` | merged | | 16 | `layout/app-shell.mdx` | `/docs/layout/app-shell` | `/docs/frontend/app-shell` | moved | | 17 | `layout/sidebar-nav.mdx` | `/docs/layout/sidebar-nav` | `/docs/frontend/sidebar-nav` | moved | | 18 | `api/schema-reference.md` | `/docs/api/schema-reference` | `/docs/ui/components` | removed | | 19 | `utilities/cli.mdx` | `/docs/utilities/cli` | `/docs/frontend/cli` | moved | | 20 | `utilities/create-plugin.mdx` | `/docs/utilities/create-plugin` | `/docs/frontend/create-plugin` | moved | | 21 | `utilities/data-objectstack.mdx` | `/docs/utilities/data-objectstack` | `/docs/frontend/data-objectstack` | moved | | 22 | `plugins/index.md` | `/docs/plugins` | `/docs/frontend/plugins` | merged | | 23 | `plugins/plugin-calendar.mdx` | `/docs/plugins/plugin-calendar` | `/docs/frontend/plugins/plugin-calendar` | trimmed | | 24 | `plugins/plugin-charts.mdx` | `/docs/plugins/plugin-charts` | `/docs/frontend/plugins/plugin-charts` | trimmed | | 25 | `plugins/plugin-chatbot.mdx` | `/docs/plugins/plugin-chatbot` | `/docs/frontend/plugins/plugin-chatbot` | moved | | 26 | `plugins/plugin-dashboard.mdx` | `/docs/plugins/plugin-dashboard` | `/docs/frontend/plugins/plugin-dashboard` | trimmed | | 27 | `plugins/plugin-detail.mdx` | `/docs/plugins/plugin-detail` | `/docs/frontend/plugins/plugin-detail` | moved | | 28 | `plugins/plugin-editor.mdx` | `/docs/plugins/plugin-editor` | `/docs/frontend/plugins/plugin-editor` | moved | | 29 | `plugins/plugin-form.mdx` | `/docs/plugins/plugin-form` | `/docs/frontend/plugins/plugin-form` | trimmed | | 30 | `plugins/plugin-gantt.mdx` | `/docs/plugins/plugin-gantt` | `/docs/frontend/plugins/plugin-gantt` | trimmed | | 31 | `plugins/plugin-grid.mdx` | `/docs/plugins/plugin-grid` | `/docs/frontend/plugins/plugin-grid` | trimmed | | 32 | `plugins/plugin-kanban.mdx` | `/docs/plugins/plugin-kanban` | `/docs/frontend/plugins/plugin-kanban` | trimmed | | 33 | `plugins/plugin-list.mdx` | `/docs/plugins/plugin-list` | `/docs/frontend/plugins/plugin-list` | trimmed | | 34 | `plugins/plugin-map.mdx` | `/docs/plugins/plugin-map` | `/docs/frontend/plugins/plugin-map` | trimmed | | 35 | `plugins/plugin-markdown.mdx` | `/docs/plugins/plugin-markdown` | `/docs/frontend/plugins/plugin-markdown` | trimmed | | 36 | `plugins/plugin-report.mdx` | `/docs/plugins/plugin-report` | `/docs/ui/reports` | merged | | 37 | `plugins/plugin-timeline.mdx` | `/docs/plugins/plugin-timeline` | `/docs/frontend/plugins/plugin-timeline` | trimmed | | 38 | `plugins/plugin-view.mdx` | `/docs/plugins/plugin-view` | `/docs/frontend/plugins/plugin-view` | moved | | 39 | `components/basic/button-group.mdx` | `/docs/components/basic/button-group` | `/docs/frontend/renderers#basic` | merged | | 40 | `components/basic/div.mdx` | `/docs/components/basic/div` | `/docs/frontend/renderers#basic` | merged | | 41 | `components/basic/navigation-menu.mdx` | `/docs/components/basic/navigation-menu` | `/docs/frontend/renderers#basic` | merged | | 42 | `components/basic/pagination.mdx` | `/docs/components/basic/pagination` | `/docs/frontend/renderers#basic` | merged | | 43 | `components/basic/separator.mdx` | `/docs/components/basic/separator` | `/docs/frontend/renderers#basic` | merged | | 44 | `components/basic/sidebar.mdx` | `/docs/components/basic/sidebar` | `/docs/frontend/renderers#basic` | merged | | 45 | `components/complex/carousel.mdx` | `/docs/components/complex/carousel` | `/docs/frontend/renderers#complex` | merged | | 46 | `components/complex/data-table.mdx` | `/docs/components/complex/data-table` | `/docs/frontend/renderers#complex` | merged | | 47 | `components/complex/filter-builder.mdx` | `/docs/components/complex/filter-builder` | `/docs/frontend/renderers#complex` | merged | | 48 | `components/complex/filter-ui.mdx` | `/docs/components/complex/filter-ui` | `/docs/frontend/renderers#complex` | merged | | 49 | `components/complex/resizable.mdx` | `/docs/components/complex/resizable` | `/docs/frontend/renderers#complex` | merged | | 50 | `components/complex/scroll-area.mdx` | `/docs/components/complex/scroll-area` | `/docs/frontend/renderers#complex` | merged | | 51 | `components/complex/sort-ui.mdx` | `/docs/components/complex/sort-ui` | `/docs/frontend/renderers#complex` | merged | | 52 | `components/complex/view-switcher.mdx` | `/docs/components/complex/view-switcher` | `/docs/frontend/renderers#complex` | merged | | 53 | `components/data-display/avatar.mdx` | `/docs/components/data-display/avatar` | `/docs/frontend/renderers#data-display` | merged | | 54 | `components/data-display/breadcrumb.mdx` | `/docs/components/data-display/breadcrumb` | `/docs/frontend/renderers#data-display` | merged | | 55 | `components/data-display/kbd.mdx` | `/docs/components/data-display/kbd` | `/docs/frontend/renderers#data-display` | merged | | 56 | `components/data-display/list.mdx` | `/docs/components/data-display/list` | `/docs/frontend/renderers#data-display` | merged | | 57 | `components/data-display/statistic.mdx` | `/docs/components/data-display/statistic` | `/docs/frontend/renderers#data-display` | merged | | 58 | `components/data-display/tree-view.mdx` | `/docs/components/data-display/tree-view` | `/docs/frontend/renderers#data-display` | merged | | 59 | `components/disclosure/collapsible.mdx` | `/docs/components/disclosure/collapsible` | `/docs/frontend/renderers#disclosure` | merged | | 60 | `components/disclosure/toggle-group.mdx` | `/docs/components/disclosure/toggle-group` | `/docs/frontend/renderers#disclosure` | merged | | 61 | `components/feedback/empty.mdx` | `/docs/components/feedback/empty` | `/docs/frontend/renderers#feedback` | merged | | 62 | `components/feedback/loading.mdx` | `/docs/components/feedback/loading` | `/docs/frontend/renderers#feedback` | merged | | 63 | `components/feedback/progress.mdx` | `/docs/components/feedback/progress` | `/docs/frontend/renderers#feedback` | merged | | 64 | `components/feedback/skeleton.mdx` | `/docs/components/feedback/skeleton` | `/docs/frontend/renderers#feedback` | merged | | 65 | `components/feedback/sonner.mdx` | `/docs/components/feedback/sonner` | `/docs/frontend/renderers#feedback` | merged | | 66 | `components/feedback/spinner.mdx` | `/docs/components/feedback/spinner` | `/docs/frontend/renderers#feedback` | merged | | 67 | `components/feedback/toast.mdx` | `/docs/components/feedback/toast` | `/docs/frontend/renderers#feedback` | merged | | 68 | `components/form/calendar.mdx` | `/docs/components/form/calendar` | `/docs/frontend/renderers#form` | merged | | 69 | `components/form/checkbox.mdx` | `/docs/components/form/checkbox` | `/docs/frontend/renderers#form` | merged | | 70 | `components/form/combobox.mdx` | `/docs/components/form/combobox` | `/docs/frontend/renderers#form` | merged | | 71 | `components/form/command.mdx` | `/docs/components/form/command` | `/docs/frontend/renderers#form` | merged | | 72 | `components/form/date-picker.mdx` | `/docs/components/form/date-picker` | `/docs/frontend/renderers#form` | merged | | 73 | `components/form/file-upload.mdx` | `/docs/components/form/file-upload` | `/docs/frontend/renderers#form` | merged | | 74 | `components/form/form.mdx` | `/docs/components/form/form` | `/docs/frontend/renderers#form` | merged | | 75 | `components/form/input-otp.mdx` | `/docs/components/form/input-otp` | `/docs/frontend/renderers#form` | merged | | 76 | `components/form/input.mdx` | `/docs/components/form/input` | `/docs/frontend/renderers#form` | merged | | 77 | `components/form/radio-group.mdx` | `/docs/components/form/radio-group` | `/docs/frontend/renderers#form` | merged | | 78 | `components/form/select.mdx` | `/docs/components/form/select` | `/docs/frontend/renderers#form` | merged | | 79 | `components/form/slider.mdx` | `/docs/components/form/slider` | `/docs/frontend/renderers#form` | merged | | 80 | `components/form/switch.mdx` | `/docs/components/form/switch` | `/docs/frontend/renderers#form` | merged | | 81 | `components/form/textarea.mdx` | `/docs/components/form/textarea` | `/docs/frontend/renderers#form` | merged | | 82 | `components/form/toggle.mdx` | `/docs/components/form/toggle` | `/docs/frontend/renderers#form` | merged | | 83 | `components/layout/aspect-ratio.mdx` | `/docs/components/layout/aspect-ratio` | `/docs/frontend/renderers#layout` | merged | | 84 | `components/layout/page.mdx` | `/docs/components/layout/page` | `/docs/frontend/renderers#layout` | merged | | 85 | `components/navigation/header-bar.mdx` | `/docs/components/navigation/header-bar` | `/docs/frontend/renderers#navigation` | merged | | 86 | `components/overlay/alert-dialog.mdx` | `/docs/components/overlay/alert-dialog` | `/docs/frontend/renderers#overlay` | merged | | 87 | `components/overlay/context-menu.mdx` | `/docs/components/overlay/context-menu` | `/docs/frontend/renderers#overlay` | merged | | 88 | `components/overlay/dialog.mdx` | `/docs/components/overlay/dialog` | `/docs/frontend/renderers#overlay` | merged | | 89 | `components/overlay/drawer.mdx` | `/docs/components/overlay/drawer` | `/docs/frontend/renderers#overlay` | merged | | 90 | `components/overlay/dropdown-menu.mdx` | `/docs/components/overlay/dropdown-menu` | `/docs/frontend/renderers#overlay` | merged | | 91 | `components/overlay/hover-card.mdx` | `/docs/components/overlay/hover-card` | `/docs/frontend/renderers#overlay` | merged | | 92 | `components/overlay/menubar.mdx` | `/docs/components/overlay/menubar` | `/docs/frontend/renderers#overlay` | merged | | 93 | `components/overlay/popover.mdx` | `/docs/components/overlay/popover` | `/docs/frontend/renderers#overlay` | merged | | 94 | `components/overlay/sheet.mdx` | `/docs/components/overlay/sheet` | `/docs/frontend/renderers#overlay` | merged | | 95 | `components/overlay/tooltip.mdx` | `/docs/components/overlay/tooltip` | `/docs/frontend/renderers#overlay` | merged | </details> ## Tests Head `8275dff1` merges `main` at `1287b2fa`, after K3 #27179 landed, and renames the "Views & Apps" pointer in `frontend/index.mdx` to "User Interface". `check:doc-anchors` is now green, with 788 fragment links resolving, the 15 `plugins#TYPE` links among them. Every CI check-run on `8275dff1` concluded success or skipped, and `mergeable_state` is clean. Head `12b1047a` renames "Object UI" to "ObjectUI" in `content/docs/frontend` (25 occurrences plus the sidebar separator), on Tesla's question in the card thread. It is a wording-only commit on top of `773055de`; the docs gates were re-run green, and `check:doc-anchors` still fails only on the same 15 K3 links. The readings below are for `773055de`. Docs-only diff; no package source changes, so no package test or typecheck applies. - `pnpm --filter @objectstack/docs build` (the command production runs) at head `773055de`: exit 0 under the verify lock. It emits 34 pages under `/en/docs/frontend` (the landing page, 16 SDK pages, `renderers`, the UI plugins overview and 15 plugin pages). - `git grep -n "InteractiveDemo\|ComponentDemo\|LiveSplitDemo\|SchemaExample" -- content/docs` prints nothing (card acceptance). - Source checks on the kept pages: every named `import { … } from '@object-ui/PKG'` resolves to a name the package's `src/index.ts` exports; every `ComponentRegistry.*` method the pages call exists in `ui/packages/core/src/registry/Registry.ts`; every `objectui SUBCOMMAND` the pages name is a `.command()` in `ui/packages/cli/src`; every `@object-ui/*` package named exists, except `@object-ui/plugin-calendar-view`, which the calendar page names only in its migration note (that package merged into `plugin-calendar`). - `apps/docs/package.json`: unchanged. ## Verification Local gates at head `773055de`: `node scripts/pm/dispatch-gates.mjs --commands` derives 54 commands, all run, exit codes captured before any pipe. - 52 of 54 exit 0. - `pnpm --filter @objectstack/spec run check:skill-examples` exited 3 in the sweep (`packages/spec/dist` declarations predated the sources after merging main). After `env -u OS_SKIP_DTS pnpm --filter '@objectstack/client-react...' build`: exit 0, "270 prose examples type-check across 3 surface(s)". - `pnpm check:doc-anchors` exits 1 on exactly 15 links, all `/docs/ui/components/plugins#TYPE` from the trimmed plugin pages. That page is K3's (#27179) and is not on `main` yet; this PR lands after K3, and the gate goes green when `main` is merged in. ## Acceptance notes - **Landing order: K3 #27179 first.** The plugin pages point at K3's generated entries instead of carrying their own copy of each type's props; that is the duplication this revision removes. - **Scope grew past the card on the maintainer's ruling.** The card asked for a one-to-one move; the maintainer asked for a duplication and fit review, then chose a new entry plus the trimmed set. The review is in the project thread. Four existing pages gained moved sections (+495 lines, text as written in the removed SDK pages); nothing existing was rewritten. - `renderers` replaces 57 pages of per-type examples with one list. An SDK reader who needs a type's inputs follows its source link to the `ComponentRegistry.register` call, which declares them. - Reserved-word ratchet: the maintainer approved baselining the SDK's own `role` keys (the `ai` SDK chat message key, the ARIA DOM key). This revision only relocates those rows: 29 occurrences in 5 `sdk/` rows become 26 in 3 `frontend/` rows; the other 3 left with `api/schema-reference` and the trimmed page-header section of `layout`. No other row changes. - `guide/theming` keeps `export const brand = { … } satisfies Theme;` (the SDK's `@object-ui/types` runtime object, not spec metadata), as in the first revision. - The README's "guides are published at objectui.org" line and `ui/content/docs/` links outside this section are K4's sweep, untouched here. --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibzngYyRjmvNxjkfVyd)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0d45c0e commit abba2c4

43 files changed

Lines changed: 13590 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎content/docs/data-modeling/field-types.mdx‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,83 @@ what the box accepts, and only one of the two outcomes is announced:
10581058
10591059
"No warning" therefore does not mean "the value is right".
10601060
1061+
### Polymorphic pointer pairs (`referenceVia`)
1062+
1063+
A `text` field that declares `referenceVia` holds a record id of the object its
1064+
sibling column names, row by row: `sys_approval_request.record_id` points at the
1065+
record `object_name` names, and `sys_activity.source_id` at the one
1066+
`source_object` names. The record details grid resolves the pair from the row
1067+
with `resolveRecordPointer(field, row)` and draws it with the renderer
1068+
registered under `RECORD_POINTER_CARD_TYPE`. The fields package's default draws
1069+
the record id as text; the console registers a record preview card there, so a
1070+
pointer field on a record page shows the record it points at. When the card
1071+
could read that record, its title links to the record's page in the current
1072+
app, built by the same record-link builder the page's lookup values use; a
1073+
record the reader cannot read is drawn with no link, and the card never says
1074+
why. Nothing is authored for this: the `referenceVia` declaration on the field
1075+
is the whole contract. List cells are not a call site yet.
1076+
1077+
When the platform deletes the record a pointer names, it can say so on the row.
1078+
A pointer field that also declares `referenceDeletedWhen: { field, value }`
1079+
names a sibling `select` field and the option the platform writes there, as
1080+
`sys_approval_request.record_id` declares
1081+
`{ field: 'cancel_reason', value: 'record_deleted' }`. On a row holding that
1082+
option the details grid draws a tombstone in the option's own label (localized
1083+
when the console's bundle localizes it), reads nothing and links nothing. A
1084+
record the card cannot read on any other row still gets the cause-free text:
1085+
only the declaration means deleted.
1086+
1087+
### Naming a registry item by name (`position-ref`, `permission-ref`)
1088+
1089+
A `text` field that stores the **machine name** of one item of a metadata
1090+
registry — a position, a permission set — declares the matching widget, and the
1091+
record form lists that registry instead of asking for a typed name:
1092+
1093+
```json
1094+
{
1095+
"position": {
1096+
"type": "text",
1097+
"label": "Position",
1098+
"required": true,
1099+
"maxLength": 100,
1100+
"widget": "position-ref"
1101+
}
1102+
}
1103+
```
1104+
1105+
| Widget | Lists | Stores |
1106+
|---|---|---|
1107+
| `position-ref` | the `position` registry (`GET /api/v1/meta/position`) | the position's `name` |
1108+
| `permission-ref` | the `permission` registry (`GET /api/v1/meta/permission`) | the permission set's `name` |
1109+
1110+
Like every `widget` value, the name is looked up as `field:<widget>`. Each item
1111+
reads as its label beside its name, and the value written is the name alone: no
1112+
label, no id. A stored name the registry does not list stays on screen, flagged
1113+
*not found*, so a renamed or removed item is visible rather than dropped.
1114+
`position-ref` leaves out the audience anchors, the spec's
1115+
`AUDIENCE_ANCHOR_POSITIONS` (`everyone`, `guest`), because the server refuses an
1116+
anchor as a grant target; a stored anchor still shows as its own option. The
1117+
list comes from the data source's `listMetadataItems(type)`. When the adapter
1118+
has none, the list is empty, or the load fails, the field is a text box that
1119+
takes the typed name; a failure is shown with its cause, and the box stays
1120+
editable.
1121+
1122+
### A widget's face on the record heading
1123+
1124+
A `text` field that declares `widget: 'recipient-picker'` or `widget: 'object-ref'` is drawn
1125+
in a list column through the read cell registered under that widget's key, which names what
1126+
the stored value references. When that field is also the object's `nameField`, the record
1127+
page heading draws the value through the same cell: a record share heads its page with the
1128+
recipient's name, not the user id it stores. Nothing is authored for this; the `widget`
1129+
declaration on the field is the whole contract.
1130+
1131+
The heading lives in `@object-ui/components`, which this package depends on and so cannot be
1132+
imported from here the other way round. `@object-ui/components` therefore owns a slot,
1133+
`setFieldReadFaceResolver` / `resolveFieldReadFace`, and this package fills it when it loads with the
1134+
lookup the list cells make. The widget's key draws the stored text until a host registers a
1135+
face under it (the console registers its own), and while it does, the heading prints the
1136+
stored value as before. So does a host that does not load `@object-ui/fields`.
1137+
10611138
## Choosing a Field Type
10621139
10631140
Not sure which type fits your data? Follow the [Field Type Decision Tree](/docs/data-modeling/field-type-decision-tree) — a flowchart, quick-reference tables, and a use-case mapping for every type in this gallery.
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
---
2+
title: "AppShell"
3+
description: "Main application shell with sidebar and header layout"
4+
---
5+
6+
The AppShell component provides the main application layout structure with an integrated sidebar, header, and content area. It's the foundation for building enterprise applications with ObjectUI.
7+
8+
## Basic Usage
9+
10+
```tsx
11+
import { AppShell, SidebarNav, type NavItem } from '@object-ui/layout';
12+
import { Home, Settings, Users } from 'lucide-react';
13+
14+
const navItems: NavItem[] = [
15+
{ title: 'Dashboard', href: '/', icon: Home },
16+
{ title: 'Users', href: '/users', icon: Users },
17+
{ title: 'Settings', href: '/settings', icon: Settings },
18+
];
19+
20+
<AppShell
21+
sidebar={<SidebarNav items={navItems} />}
22+
navbar={
23+
<div className="flex items-center gap-4">
24+
<span className="font-semibold">My App</span>
25+
</div>
26+
}
27+
>
28+
{/* Your page content */}
29+
<div>Main content area</div>
30+
</AppShell>
31+
```
32+
33+
## Component Props
34+
35+
```ts
36+
import type { AppShellBranding } from '@object-ui/layout';
37+
38+
interface AppShellProps {
39+
sidebar?: React.ReactNode; // Sidebar content (usually SidebarNav)
40+
navbar?: React.ReactNode; // Top navbar content (logo, search, user menu)
41+
children: React.ReactNode; // Main content area
42+
className?: string; // Additional CSS classes for content area
43+
defaultOpen?: boolean; // Sidebar default state (default: true)
44+
branding?: AppShellBranding; // App branding — four theme fields, see Branding
45+
rightRail?: React.ReactNode; // Optional right rail beside the content, not
46+
// over it (ADR-0057 P3a)
47+
}
48+
```
49+
50+
## Features
51+
52+
### Responsive Sidebar
53+
54+
The AppShell does not build a sidebar of its own: it renders the node you pass in
55+
`sidebar`, wrapped in a `SidebarProvider`. Pass a `Sidebar`-based node (`SidebarNav`, or
56+
your own) and you get a sidebar that:
57+
- Collapses on mobile devices
58+
- Can be toggled with the `Cmd/Ctrl + B` keyboard shortcut, or by a toggle button you
59+
render yourself — see Collapsible Sidebar below
60+
- Persists state across page navigations
61+
- Supports smooth animations
62+
63+
### Header Bar
64+
65+
The header renders your `navbar` content and nothing else — the AppShell adds no controls
66+
of its own to it, in particular **no sidebar toggle**. If you want one, render it inside
67+
`navbar` (see Collapsible Sidebar below). The header itself provides:
68+
- Custom navbar content area
69+
- Fixed height: `h-12` (3rem / 48px) at every breakpoint — there is no responsive height variant
70+
- Border bottom separator
71+
- Background matching app theme
72+
73+
### Content Area
74+
75+
The main content area features:
76+
- Responsive padding in three steps: `p-3` on mobile, `sm:p-4` from the `sm` breakpoint, `md:p-6` from `md` up
77+
- A taller bottom padding on mobile only: `pb-20` (5rem), which keeps content clear of the mobile bottom bar (that bar is itself `sm:hidden`); from `sm` up the bottom edge follows `sm:pb-4` / `md:pb-6` like the other sides
78+
- Automatic scrolling
79+
- Flexible height (fills viewport)
80+
- Custom className support
81+
82+
### Branding
83+
84+
`branding` is theming, not chrome — the shell hands it to `useAppShellBranding`, and all
85+
four of the fields it declares are read:
86+
87+
- `primaryColor` / `accentColor` — a six-digit hex (`#3B82F6`; the leading `#` is
88+
optional, three-digit shorthand is not accepted), translated into the Shadcn theme
89+
tokens listed under Styling below. A value that does not parse that way is ignored
90+
silently.
91+
- `favicon` — overwrites the `href` of the icon link the page already has (`#favicon`,
92+
otherwise `link[rel="icon"]`). It never creates one, so a document with no icon link is
93+
left unchanged.
94+
- `title` — assigned to `document.title` as given. It is the whole title, not a suffix:
95+
the caller composes the string it wants (the console passes
96+
`"App label — Product name"`).
97+
98+
All of it acts on the document rather than on the shell's subtree, and the colour
99+
properties are removed again when the shell unmounts.
100+
101+
### Right Rail
102+
103+
`rightRail` renders your node as a flex sibling of the content area, so the rail sits
104+
**beside** the content and reflows it rather than overlaying it (ADR-0057 P3a). The shell
105+
adds no wrapper and no width around it — the node you pass owns its width, border and
106+
scrolling; omit it and the layout is the unchanged single pane.
107+
108+
## Layout Structure
109+
110+
```
111+
┌─────────────────────────────────────┐
112+
│ Header (your navbar content) │
113+
├──────────┬──────────────────────────┤
114+
│ │ │
115+
│ Sidebar │ Main Content Area │
116+
│ │ │
117+
│ │ │
118+
│ │ │
119+
└──────────┴──────────────────────────┘
120+
```
121+
122+
## Sidebar Configuration
123+
124+
### Default Open State
125+
126+
Control whether sidebar is open by default:
127+
128+
```plaintext
129+
<AppShell defaultOpen={false}>
130+
{/* Sidebar starts collapsed */}
131+
</AppShell>
132+
```
133+
134+
### Collapsible Sidebar
135+
136+
The AppShell renders no toggle button of its own. What it does give you is the
137+
`SidebarProvider` wrapped around both slots, so the sidebar you supply can be toggled via:
138+
- The `Cmd/Ctrl + B` keyboard shortcut, provided by `SidebarProvider`
139+
- Programmatic control — `useSidebar().toggleSidebar()` from any component inside the shell
140+
- A toggle button you render yourself; it belongs in the `navbar` slot:
141+
142+
```tsx
143+
import { SidebarTrigger } from '@object-ui/components';
144+
import { AppShell, SidebarNav, type NavItem } from '@object-ui/layout';
145+
import { Home, Settings } from 'lucide-react';
146+
147+
const navItems: NavItem[] = [
148+
{ title: 'Dashboard', href: '/', icon: Home },
149+
{ title: 'Settings', href: '/settings', icon: Settings },
150+
];
151+
152+
<AppShell
153+
navbar={<SidebarTrigger />}
154+
sidebar={<SidebarNav items={navItems} />}
155+
>
156+
<div>Your page content</div>
157+
</AppShell>
158+
```
159+
160+
## Navbar Content
161+
162+
The navbar area is flexible and can contain:
163+
164+
### Logo and Title
165+
166+
```plaintext
167+
navbar={
168+
<div className="flex items-center gap-2">
169+
<img src="/logo.svg" alt="Logo" className="h-8 w-8" />
170+
<span className="font-semibold text-lg">My Application</span>
171+
</div>
172+
}
173+
```
174+
175+
### Search Bar
176+
177+
```plaintext
178+
navbar={
179+
<div className="flex-1 max-w-md">
180+
<Input placeholder="Search..." />
181+
</div>
182+
}
183+
```
184+
185+
### User Menu
186+
187+
```plaintext
188+
navbar={
189+
<div className="flex items-center gap-4 ml-auto">
190+
<NotificationsButton />
191+
<UserDropdown />
192+
</div>
193+
}
194+
```
195+
196+
## Complete Example
197+
198+
```tsx
199+
import { AppShell, SidebarNav, type NavItem } from '@object-ui/layout';
200+
import { Avatar, AvatarFallback, Button, Input } from '@object-ui/components';
201+
import { Bell, Folder, LayoutDashboard, Settings, Users } from 'lucide-react';
202+
203+
const navItems: NavItem[] = [
204+
{ title: 'Dashboard', href: '/', icon: LayoutDashboard, badge: '12' },
205+
{
206+
title: 'Projects',
207+
href: '/projects',
208+
icon: Folder,
209+
children: [
210+
{ title: 'Active', href: '/projects/active' },
211+
{ title: 'Archived', href: '/projects/archived' },
212+
],
213+
},
214+
{ title: 'Team', href: '/team', icon: Users },
215+
{ title: 'Settings', href: '/settings', icon: Settings },
216+
];
217+
218+
function App() {
219+
return (
220+
<AppShell
221+
defaultOpen={true}
222+
sidebar={<SidebarNav title="ObjectUI" items={navItems} />}
223+
navbar={
224+
<div className="flex items-center justify-between w-full">
225+
{/* Search */}
226+
<div className="flex-1 max-w-md">
227+
<Input
228+
placeholder="Search..."
229+
className="w-full"
230+
/>
231+
</div>
232+
233+
{/* Right side items */}
234+
<div className="flex items-center gap-4">
235+
<Button variant="ghost" size="icon">
236+
<Bell className="h-5 w-5" />
237+
</Button>
238+
<Avatar>
239+
<AvatarFallback>JD</AvatarFallback>
240+
</Avatar>
241+
</div>
242+
</div>
243+
}
244+
className="bg-gray-50"
245+
>
246+
{/* Main content */}
247+
<div className="max-w-7xl">
248+
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
249+
{/* Page content */}
250+
</div>
251+
</AppShell>
252+
);
253+
}
254+
```
255+
256+
`SidebarNav` has no logo slot and no footer slot — `title` is its only branding prop, the
257+
section label printed above a flat `NavItem[]` (it is ignored when `items` is a
258+
`NavGroup[]`, since each group prints its own `label`). Put a logo in the `navbar` slot
259+
instead, as the Logo and Title example under Navbar Content above does.
260+
261+
## With Page Component
262+
263+
Combine AppShell with the Page component for complete layouts:
264+
265+
```plaintext
266+
<AppShell sidebar={<SidebarNav items={navItems} />}>
267+
<Page
268+
title="Dashboard"
269+
description="Welcome to your dashboard"
270+
body={[
271+
{ type: 'text', content: 'Dashboard content' }
272+
]}
273+
/>
274+
</AppShell>
275+
```
276+
277+
## Styling
278+
279+
The AppShell uses Tailwind CSS and Shadcn UI components:
280+
281+
- `SidebarProvider`: Manages sidebar state
282+
- `SidebarInset`: Content area wrapper
283+
284+
Brand colours arrive as Shadcn theme tokens rather than as per-component styles: each hex
285+
from `branding` is converted to the `H S% L%` triple those tokens carry and set as a CSS
286+
custom property on `document.documentElement`. `primaryColor` writes `--primary`,
287+
`--primary-foreground`, `--ring`, `--sidebar-primary` and `--sidebar-ring`; `accentColor`
288+
writes `--accent` and `--accent-foreground`. Tailwind's utilities resolve through the same
289+
tokens (`--color-primary: hsl(var(--primary))`), so every `bg-primary` / `text-primary` /
290+
`ring-primary` / `bg-accent` consumer picks the brand colour up with no per-component
291+
wiring. In dark mode a lighter variant of the same hue is used instead, and the shell
292+
re-applies the values whenever the `dark` class on the `html` element changes.
293+
294+
## Accessibility
295+
296+
The AppShell includes:
297+
- A keyboard shortcut for toggling the sidebar (`Cmd/Ctrl + B`), from `SidebarProvider`
298+
- Keyboard navigation support
299+
- Focus management
300+
- Screen reader announcements
301+
302+
## Related
303+
304+
- [SidebarNav](/docs/frontend/sidebar-nav) - Sidebar navigation component
305+
- [PageHeader](/docs/ui/pages) - Page header component
306+
- [Page](/docs/frontend/renderers) - Page layout component

0 commit comments

Comments
 (0)