Skip to content

Commit 238222d

Browse files
skills(ui): the page skill learns the print page — print, the printable block subset and its lint (#22271) (#22287)
Fixes #22271 Clause-②: no The Studio author's page skill (`skills/objectstack-ui/rules/pages.md`) learns the print page that card ① of the ruling of record on #8346 (comment 6051470224, letter B′, maintainer 「8346 B′」) landed as #22158 through PR #22193: a printable document is a page that declares `print`; the eight `print` keys and what each means on paper; the three page types that may carry it; the printable block subset taught by reason, not as a copied list; the `print-page-block-unprintable` refusal; and the two list facts (list export has no `'pdf'`; a list prints through the view's `allowPrinting`). #8346, #22158, #22193 and #22184 are references — none of them is addressed here. Every sentence is true of `origin/main` at `c8bb3c8d9c` (this branch's base): the declaration is parsed and refused by `@objectstack/spec` (`PagePrintSchema`, `checkPagePrintComposition`) and by `@objectstack/lint` (`validatePrintPageBlocks`, rule `print-page-block-unprintable`, on `os validate` / `os build` / `os lint` and the page save door). What renders it is the console's print rendering — card ② of the same ruling, not on this tree — so the skill says in one clause that the rendering follows and that `os validate` warns the author today (the liveness row `print` is `planned` with `authorWarn`, `packages/spec/liveness/page.json`). No rendering is invented. ## What changes (three markdown files, nothing else) - `skills/objectstack-ui/rules/pages.md` — a new `### Print pages` section under Pages: what a print page is and the refused page kinds/types (`list` → `interfaceConfig.allowPrinting`, `utility`, `slotted` / `html` / `react`, a `full` page with no regions); the keys `paperSize`, `orientation`, `margins` (millimetres), `repeatHeader` / `repeatFooter` (the page's own `header` / `footer` regions; refused without that region), `pageNumbers`, `repeatTableHeaders` (on by default), `avoidBreakInside`; the printable subset stated as the property every member has (draws all it declares, in full, the same at any width) with the 13 members grouped by that property; the four refusal reasons with the blocks an author reaches for first (`record:related_list` / `object-grid` → `record:line_items`; `page:header`, sidebars, boards; `record:path`, tabs, buttons, inputs, forms → `record:details`; `flex` / `object-chart` / plugin widgets are outside the vocabulary); the rendering status in one clause; the two list facts; and one `os:check` invoice example (`type: 'record'`, `print` with margins / running header and footer / page numbers, `element:text` letterhead, `record:details`, `record:line_items`, a footer region). - `skills/objectstack-ui/SKILL.md` — one word in the Detailed Rules routing row for pages.md ("print pages"), so an author routing from the entry file finds the section. - `skills/objectstack-ui/rules/actions.md` — one sentence beside the `OpenInvoicePdfAction` example: a printable document is a page that declares `print` (link to the section), validated today, rendered once the console's print rendering ships. The example itself is untouched (with only card ① landed, a per-project endpoint is still the only working route to a PDF today). Paid by deleting the three "Best practices" bullets the file already states elsewhere: `confirmText` for destructive actions (line 20), `visible` to hide an action (line 18), `input.selectedIds` for bulk actions (the `list_toolbar` row and the `AddToCampaignAction` example); the `refreshAfter` bullet, which had no other home, stays. - `skills/objectstack-ui/evals/*.json` — untouched: no eval quotes a changed line. Eval 4's `expected_output` names the record-page lessons (`page:header` with `properties.actions`, `record:path` with `statusField` / `stages`) that now live in the component catalogue rows. ## Ratchet route and readings (`scripts/check-skills-token-ratchet.mjs`, convention ceil(utf8 bytes / 4)) **Why the section lives inside `pages.md` and not in a new file.** The ratchet admits no new file on an author's own: a discovered authored file with no ceiling is RED, and its remedy carries the maintainer-only marker — "adding a CEILINGS row prices new text into the bundle that ships to every customer project, which is the maintainer ruling this ratchet implements — not a step an author takes while landing the file." So the section is paid inside `pages.md` by deletion, with no re-wrap counted and no ceiling moved. | file | lines before → after | tokens before → after | ceiling | headroom after | | --- | --- | --- | --- | --- | | `skills/objectstack-ui/rules/pages.md` | 453 → 434 (−19) | 5679 → 5691 (+12) | 5692 | 1 | | `skills/objectstack-ui/SKILL.md` | 308 → 308 (0) | 3844 → 3847 (+3) | 3856 | 9 | | `skills/objectstack-ui/rules/actions.md` | 207 → 206 (−1) | 2104 → 2101 (−3) | 2108 | 7 | | whole package, all `skills/*/SKILL.md` (10 files) | 4408 → 4408 (0) | 52572 → 52575 (+3) | — | — | | whole `skills/objectstack-ui` authored bundle (9 priced files) | 2051 → 2031 (−20) | 26096 → 26108 (+12) | — | — | Measured on `c8bb3c8d9c` (before) and on this PR's head (after); `node scripts/check-skills-token-ratchet.mjs` at the head: `pages.md is 5691 tokens (ceiling 5692; headroom 1)`, `SKILL.md … 3847 (ceiling 3856; headroom 9)`, `actions.md … 2101 (ceiling 2108; headroom 7)`, exit 0. **Every deletion in `pages.md`, and where its content survives:** | deleted | survives in | | --- | --- | | "Pages let designers assemble record details, home pages, app launchers, and utility bars without writing React." | the Page Types table (`record` / `home` / `app` / `utility` rows) | | the whole `### Example — Record Detail Page` `os:check` block (52 lines) | the `definePage` + `regions` shape → the new invoice `os:check` block; `page:header` `title` / `subtitle` / `actions` → its catalogue row (extended); `record:highlights` `properties.fields` → its catalogue row (extended); `record:path` `statusField` / `stages` → its catalogue row (extended); the inlined `ConvertLeadAction` → `rules/actions.md` (same action, same skill); `template: 'three-column'` → Templates & Regions | | the relative-date placeholder list and the `DATE_MACRO_TOKENS` pointer in the Variable substitution note | `SKILL.md` → Date Macros — Filter Placeholders (the pointer stays, shortened) | | the "Actions in header" note | the `page:header` catalogue row ("ids of the bound object's actions, never a sibling action node") | | react tier: "Compose layout with inline style (real CSS); use the injected blocks for data. Do NOT use Tailwind className — see Styling a page below" | Styling a page → the `kind:'react'` bullet (inline style object) and the section's no-`className` rule | | react-blocks note: "It is the authoritative answer to what props does ObjectForm / ListView take — author against it, not from memory." | the same note's bold lead "Do not guess props — read the contract." | | react-blocks note: "(Maintainers: regenerate with pnpm --filter @objectstack/spec gen:react-blocks.)" | a maintainer process note, not customer teaching — AGENTS.md's regeneration table and `packages/spec/package.json` | | "See ADR-0065 (SDUI styling model)." | the section heading "Styling a page (ADR-0065)" | | the `src/docs/` tree diagram | Authoring rule 2 (filename stem = doc name) and Routing model (`/docs/` + name) | | "A platform-level Documentation portal … is a later, additive concern — author-side, nothing to model now." | the one deletion that is not a restatement: a roadmap remark; no authoring instruction is lost (there is no portal key to author) — flagged here for the reviewer | | "Live instances vs. structural views", second half (structural metadata → a `metadata` fence) | Inline metadata views — the `metadata` fence (the three-kinds table and its lint) | | "Scope is deliberately narrow: only state_machine, flow, permission." | the same section's "Three view kinds" table and the lint line ("`type` must be one of the three") | ## Controls (against the built `@objectstack/spec` dist at the head, `scripts/pm/os-verify-lock.sh` VERDICT command-exit 0) - Positive: the invoice example exactly as `pages.md` carries it → `PageSchema.safeParse` success = true, `print` parsed as `{"paperSize":"A4","margins":{"top":15,"right":12,"bottom":15,"left":12},"repeatHeader":true,"repeatFooter":true,"pageNumbers":true}`; `validatePrintPageBlocks({ pages: [example] })` → `[]`. - Refused by the lint rule (the same page holding unprintable blocks): rule `print-page-block-unprintable`, severity `error`, each with its reason — "`page:header` cannot be placed in a print page … it lays its action bar out to the screen width"; "`record:related_list` … draws only the first `limit` related records (5 unless set) … a window of the rows"; "`flex` … is not in the printable block subset, and nothing answers for how it prints"; "`record:path` … is an interactive stage control; the current stage is a field value, which `record:details` prints". - Refused at the parse: `print` on a `type: 'list'` page ("… set `interfaceConfig.allowPrinting: true`; to print a document, declare `print` on a page of `type: 'record'`, `'home'` or `'app'` with its blocks in `regions`"); `repeatHeader: true` with no `header` region ("Add a `{ name: 'header', components: [...] }` region holding the letterhead blocks, or delete `repeatHeader`"); `paperSize: 'A3'` ("expected one of A4 | A5 | Letter | Legal"); `print` on a `utility` page; `print` on a `kind: 'react'` page. - `pnpm --filter @objectstack/spec run check:skill-examples` at the head: "262 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them", exit 0 (the new invoice block is one of the 228 skills + docs blocks). ## Gates (derived on this tree by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 24 families; every exit code captured before any pipe; `--ran` reconciliation: 24 accounted, 0 NOT-MEASURED) | command | exit | | --- | --- | | `node scripts/check-skills-token-ratchet.mjs` (+ `--self-test`) | 0 / 0 | | `pnpm --filter @objectstack/spec run check:skill-docs` | 0 | | `pnpm check:skill-identifier-liveness` · `check:skill-compatibility` · `check:skill-frame-sync` | 0 · 0 · 0 | | `pnpm check:doc-authoring` · `check:corpus-claim-drift` · `check:nul-bytes` | 0 · 0 · 0 | | `node scripts/check-doc-route-spelling.mjs --advisory` (+ `--self-test`) | 0 / 0 | | `pnpm --filter @objectstack/lint run check:doc-formula-expressions` | 3 before the lint/formula build (prerequisite, not a verdict) → 0 after `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --filter=@objectstack/client-react --filter=@objectstack/client` (34/34 tasks) | | `node scripts/check-ci-filter-parity.mjs` · `check-closing-keyword-parity.mjs` (+ `--self-test`) · `check-comment-mask-corpus.mjs` | 0 · 0 / 0 · 0 | | `pnpm check:agent-test-spelling` · `check:cross-package-test-inputs` · `check:driver-memory-census` · `check:gitlink-declared` · `check:pm-governed-merges` · `check:refd-timer-probe` · `check:role-word` · `check:watch-hint-literal` | all 0 | Not run locally, CI's own: the type-check lanes and `pnpm lint` (no TypeScript or source file moves). No changeset: the diff publishes nothing from a released package (`skills/**` is not in any package's `files`), so `skip-changeset` is the declaration. ## Acceptance notes (not filed; carrier named) - `packages/spec/src/ui/app.zod.ts:108` declares a `doc` navigation-item type (`{ type: 'doc', book }` / `{ type: 'doc', doc }`) and `SKILL.md`'s nav table lists it, but `pages.md` → Routing model still says "There is no dedicated `doc` nav-item type yet, so use a `url` item", and eval 4's `expected_output` expects a `type: 'url'` nav item. A stale line in the published skill; a different defect class from this card, so not touched here. Carrier: the `domain:skills` seat, reported in this card's os-dev-report for filing. - The print-page example omits `template` (spec default `'default'`); whether a print page constrains `template` is card ②'s call (PR #22193's own note). The skill does not speak about it. - Rendering: the skill's one clause ("the console's print rendering that applies it follows") is the only forward-looking sentence; the day card ② lands at a `.objectui-sha` pin, that clause and the `os validate` warning sentence are the two lines to revise. ## 维护者速读(草稿) **改了什么** — 给 Studio 作者技能的页面规则(`skills/objectstack-ui/rules/pages.md`)新增一节「Print pages」:可打印文档(发票、送货单、信函、报表)就是声明了 `print` 的普通页面,没有模板类型;八个 `print` 键各自对应纸面上的什么;只有带 `regions` 的 `record` / `home` / `app` 页面可以声明;打印页里只允许「整页画全、任意宽度布局不变」的块,其余块被 `print-page-block-unprintable` 拒绝并给出原因(只画一窗行、按屏幕布局、无可打印内容、词汇表之外);列表导出没有 `'pdf'`,列表靠视图的 `allowPrinting`。附一个会被 `check:skill-examples` 类型检查的发票页示例。SKILL.md 路由行加「print pages」一词;actions.md 的「打开发票 PDF」示例旁加一句指向打印页。 **为什么改** — #8346 裁决 B′ 原话:「the Studio author's skill learns the print page when ① lands」;① 已随 PR #22193 落地。spec 与 lint 会响亮拒绝写错的打印页,但没学过这个声明的 AI 作者根本不会去写它,只会继续给每个项目手搓一个 PDF 接口;技能文本是 `npx skills add` 直接发到每个客户项目的那份。 **风险与代价(含回滚)** — 发布技能包有 token 棘轮:`pages.md` 只剩 13 token 余量,新节靠删除同文件里重复的内容付账(记录详情页示例整块删除、其教学点并入组件目录表格;另 11 处重复句,逐条列在上文表格里,唯一一条非重复的是「文档门户是后续事项」这句路线图备注),净 +12 token,落在 5691/5692;SKILL.md +3、actions.md −3,三个上限都没动。风险一:AI 作者看到打印页示例,把 `print` 加到普通页面 —— 文本已写明只有文档才声明,spec 也会拒绝不合格的页面;风险二:今天没有渲染器读 `print`(裁决的卡 ② objectui 未落地),技能用一句话如实说明「已校验、渲染随后」,`os validate` 也会提示作者。回滚:整个 PR 只动三个 markdown 文件,revert 即可,无代码、无 changeset。 **席位意见** — **你要做的** — 核对「Print pages」一节的措辞与上文的删除清单;认可则 APPROVE 这个 draft PR,由你或授权审批落地(`skills/**` 为 Tier H)。 --- _Generated by [Claude Code](https://claude.ai/code/session_01CXydFDyiQwNbGFkmwrcRQq)_ --------- Co-authored-by: objectstack-fleet[bot] <objectstack-fleet[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent ee6aa01 commit 238222d

3 files changed

Lines changed: 71 additions & 92 deletions

File tree

‎skills/objectstack-ui/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ authoring non-trivial CEL.
146146
- [List, Kanban & Gantt Views](./rules/list-views.md) — `defineView`, `data`, columns, filtering, `userFilters`, toolbar search, sorting, kanban, gantt.
147147
- [Navigation & Run Modes](./rules/navigation.md) — `App.create`, the three run modes, record presentation.
148148
- [Dashboards, Reports & Cubes](./rules/dashboards.md) — widgets, dataset binding, filters, `compareTo`, bucketing, `options`, drilldown, report config, cubes.
149-
- [Pages & Docs](./rules/pages.md) — page types, regions, components, the html/react source tiers, styling, package docs.
149+
- [Pages & Docs](./rules/pages.md) — page types, regions, components, print pages, the html/react source tiers, styling, package docs.
150150
- [Actions](./rules/actions.md) — `locations`, visibility, examples, `ctx`, new tab, params.
151151

152152
---

‎skills/objectstack-ui/rules/actions.md‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ export const OpenInvoicePdfAction = defineAction({
184184
});
185185
```
186186

187+
> A printable document (an invoice) is a page that declares `print` — [Print pages](./pages.md#print-pages):
188+
> validated today, rendered once the console's print rendering ships.
189+
187190
> ⚠️ **Never express new-tab behavior via `params`.** `params` is exclusively
188191
> `ActionParam[]` for collecting **user input**. Writing an object form like
189192
> `params: { newTab: true }` fails the zod build outright; the array form
@@ -200,8 +203,4 @@ and widget mapping from object metadata. Use `objectOverride` to reference a
200203
field from a different object. Set `defaultFromRow: true` to pre-fill from
201204
the selected row in `list_item` contexts.
202205

203-
> **Best practices:**
204-
> - Always add `confirmText` for destructive actions.
205-
> - Use `visible` (CEL) so buttons appear only when actionable.
206-
> - Set `refreshAfter: true` whenever the action mutates the current record.
207-
> - For bulk actions, read `input.selectedIds` inside `body.source`.
206+
> **Best practice:** set `refreshAfter: true` whenever the action mutates the current record.

‎skills/objectstack-ui/rules/pages.md‎

Lines changed: 66 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
# Pages & Docs
22

33
- [Pages — Lightning-Style Page Layouts](#pages--lightning-style-page-layouts) · [Page Types](#page-types) · [Templates & Regions](#templates--regions)
4-
- [Component Catalogue](#component-catalogue-selection) · [Example — Record Detail Page](#example--record-detail-page)
4+
- [Component Catalogue](#component-catalogue-selection) · [Print pages](#print-pages)
55
- [AI-authored source pages](#ai-authored-source-pages--kindhtml-and-kindreact-adr-00800081) · [Styling a page](#styling-a-page-adr-0065--responsivestyles-not-classname)
66
- [Docs — Package Documentation](#docs--package-documentation-adr-0046) · [Authoring rules](#authoring-rules-each-enforced-by-os-build)
77
- [Routing model](#routing-model--platform-level-viewer-opt-in-entry) · [Inline metadata views](#inline-metadata-views--the-metadata-fence-adr-0051) · [Example](#example)
88

99
## Pages — Lightning-Style Page Layouts
1010

1111
A **Page** is a Salesforce-Lightning-style layout composed of **regions**
12-
populated with **components**. Pages let designers assemble record details,
13-
home pages, app launchers, and utility bars without writing React.
12+
populated with **components**.
1413

1514
Register under `defineStack({ pages: [...] })`.
1615

@@ -47,83 +46,83 @@ which contain components.
4746

4847
| `type` | Use |
4948
|:---------------------|:----|
50-
| `page:header` | Title + subtitle + `actions: string[]` (action ids) |
49+
| `page:header` | `title` / `subtitle` templates + `actions: string[]` — ids of the bound object's actions, never a sibling action node |
5150
| `page:card` | Bordered/un-bordered card with `children: Component[]` (plus an optional `footer: Component[]` slot) |
5251
| `flex` | Generic styleable box (`properties.children`) — the workhorse for custom layout; style via `responsiveStyles` (see Styling below) |
5352
| `element:text` | Text node — `properties.content`; style via `responsiveStyles` |
5453
| `element:button` | Button — `properties.label` + `variant`/`size` + optional `action` |
55-
| `record:highlights` | Salesforce highlights panel — strip of key fields |
56-
| `record:path` | Stage progress bar driven by a status field |
54+
| `record:highlights` | Salesforce highlights panel — strip of key fields (`properties.fields`) |
55+
| `record:path` | Stage progress bar — `statusField` + `stages: [{ value, label }]` |
5756
| `record:related_list` | Related-list (child records via lookup) |
5857
| `nav:menu` | Quick-create / nav menu bound to current context |
5958
| `object-metric` | Single KPI widget (count/sum/avg) |
6059
| `object-chart` | Embedded chart |
6160

62-
### Example — Record Detail Page
61+
> **Variable substitution** — `{first_name}`, `{current_user.first_name}`,
62+
> `{current_quarter_start}` etc. resolve from the page's `variables` block,
63+
> the bound record, and the runtime context. Declare `variables: [...]` at
64+
> the page root for any non-record value; relative-date placeholders:
65+
> [Date Macros](../SKILL.md#date-macros--filter-placeholders).
66+
67+
### Print pages
68+
69+
An invoice, a delivery order, a letter or a report is an ordinary page with a
70+
**`print`** declaration — there is no template type; the page's own blocks are
71+
the document. Only a `kind: 'full'` page of `type: 'record'`, `'home'` or
72+
`'app'` with its blocks in `regions` may declare it: the parse refuses `print`
73+
on a `list` page (its switch is `interfaceConfig.allowPrinting`), a `utility`
74+
panel, a `slotted` / `html` / `react` page and a `full` page with no regions.
75+
76+
`print` keys, each mapped to print CSS: `paperSize` (`'A4'` default, `'A5'`,
77+
`'Letter'`, `'Legal'`) and `orientation` (`'portrait'` default, `'landscape'`);
78+
`margins: { top, right, bottom, left }` in **millimetres**; `repeatHeader` /
79+
`repeatFooter` repeat the page's own `header` / `footer` region on every sheet
80+
(refused without that region; off = printed once); `pageNumbers` prints sheet
81+
number and count; the page-break hints `repeatTableHeaders` (on by default)
82+
and `avoidBreakInside` (keep each block whole).
83+
84+
Every block, at any depth, must draw all it declares, in full, the same at
85+
any width: the containers `page:section` / `page:card` /
86+
`page:footer`, the field blocks `record:details` / `record:highlights`, the
87+
all-lines child table `record:line_items`, `element:text` / `element:image` /
88+
`element:divider` / `element:definition-list` / `element:repeater`, and the
89+
single values `element:number` / `object-metric`. `os validate` / `os build`
90+
and the page save door refuse any other block (`print-page-block-unprintable`)
91+
with its reason: it **draws a window of its rows** (`record:related_list`,
92+
`object-grid` — use `record:line_items`), it **lays itself out to the screen**
93+
(`page:header`, sidebars, kanban / calendar / gantt / map), it has **nothing
94+
to print** (`record:path`, tabs, buttons, inputs, forms — values print through
95+
`record:details`), or it is **outside the vocabulary** (`flex`, `object-chart`,
96+
a plugin widget), so nothing answers for how it prints.
97+
98+
Validated today; the console's print rendering that applies it follows, so
99+
`os validate` warns the layout is not yet applied. List export has no `'pdf'`;
100+
a list prints as shown via the view's `allowPrinting`.
63101

64102
<!-- os:check -->
65103
```typescript
66-
import { defineAction, definePage } from '@objectstack/spec/ui';
67-
68-
// Normally lives in its own `*.action.ts`; inlined so this block stands alone.
69-
const ConvertLeadAction = defineAction({
70-
name: 'convert_lead', label: 'Convert Lead', objectName: 'lead',
71-
type: 'flow', target: 'lead_conversion', locations: ['record_header'],
72-
});
104+
import { definePage } from '@objectstack/spec/ui';
73105

74-
export const LeadDetailPage = definePage({
75-
name: 'lead_detail_page',
76-
label: 'Lead Detail',
77-
type: 'record',
78-
object: 'lead',
79-
template: 'three-column',
106+
export const InvoicePrintPage = definePage({
107+
name: 'invoice_print', label: 'Invoice', type: 'record', object: 'invoice',
108+
print: { paperSize: 'A4', margins: { top: 15, right: 12, bottom: 15, left: 12 },
109+
repeatHeader: true, repeatFooter: true, pageNumbers: true },
80110
regions: [
81-
{
82-
name: 'header', width: 'full',
83-
components: [
84-
{
85-
type: 'page:header', id: 'lead_header', label: 'Lead Information',
86-
properties: {
87-
title: '{first_name} {last_name}',
88-
subtitle: '{company}',
89-
actions: ['convert_lead'], // ids of `lead`'s actions
90-
},
91-
},
92-
{
93-
type: 'record:highlights', id: 'lead_highlights',
94-
properties: { fields: ['status', 'rating', 'lead_source', 'owner', 'email', 'phone'] },
95-
},
96-
{
97-
type: 'record:path', id: 'lead_path',
98-
properties: {
99-
statusField: 'status',
100-
stages: [
101-
{ value: 'new', label: 'New' },
102-
{ value: 'contacted', label: 'Contacted' },
103-
{ value: 'qualified', label: 'Qualified' },
104-
{ value: 'unqualified', label: 'Unqualified' },
105-
],
106-
},
107-
},
108-
],
109-
},
110-
// left_sidebar / main / right_sidebar regions follow…
111+
{ name: 'header', components: [
112+
{ type: 'element:text', properties: { content: 'ACME Ltd' } },
113+
] },
114+
{ name: 'main', components: [
115+
{ type: 'record:details' },
116+
{ type: 'record:line_items', properties: { childObject: 'invoice_line', relationshipField: 'invoice',
117+
columns: [{ name: 'product' }, { name: 'quantity' }, { name: 'unit_price' }, { name: 'amount' }] } },
118+
] },
119+
{ name: 'footer', components: [
120+
{ type: 'element:text', properties: { content: 'Due within 30 days.' } },
121+
] },
111122
],
112123
});
113124
```
114125

115-
> **Variable substitution** — `{first_name}`, `{current_user.first_name}`,
116-
> `{current_quarter_start}` etc. resolve from the page's `variables` block,
117-
> the bound record, and the runtime context. Declare `variables: [...]` at
118-
> the page root for any non-record value. For relative-date placeholders
119-
> (`{today}`, `{30_days_ago}`, `{N_<unit>_(ago|from_now)}` …) see the
120-
> [Date Macros](../SKILL.md#date-macros--filter-placeholders) reference below — the
121-
> full token list is published as `DATE_MACRO_TOKENS` in `@objectstack/spec/data`.
122-
123-
> **Actions in header** — `properties.actions` takes the **ids** of actions
124-
> declared on the bound object (`'convert_lead'`; no built-in id registry);
125-
> do **not** create a sibling action node.
126-
127126
### AI-authored *source* pages — `kind:'html'` and `kind:'react'` (ADR-0080/0081)
128127

129128
Besides the structured `regions` model above, a page's whole body can be written
@@ -192,19 +191,13 @@ The source is real React executed at render by the runtime. The injected scope a
192191
itself? Author the page as `type:'record'`, where the context exists
193192
- `data` / `variables` / `page`
194193

195-
Compose **layout with inline `style={{…}}`** (real CSS); use the injected blocks
196-
for data. **Do NOT use Tailwind `className`** — see *Styling a page* below for
197-
why it silently does nothing.
198-
199194
> **Do not guess props — read the contract.** Each injected block's full prop set
200195
> (name, type, `data`/`controlled`/`callback` kind, required, description) is the
201196
> **[React-tier component contract](../references/react-blocks.md)**, generated from
202197
> the block→schema index in `@objectstack/spec`.
203-
> It is the authoritative answer to "what props does `<ObjectForm>`/`<ListView>`/…
204-
> take?" — author against it, not from memory. The `data` props are sourced from the platform's spec schemas (FormView,
198+
> The `data` props are sourced from the platform's spec schemas (FormView,
205199
> ListView, Chart, …) — the same protocol the server validates;
206200
> `binding`/`controlled`/`callback` are the React overlay.
207-
> (Maintainers: regenerate with `pnpm --filter @objectstack/spec gen:react-blocks`.)
208201
209202
Master/detail (click a row → edit it → save refreshes the list):
210203

@@ -294,8 +287,7 @@ Rules:
294287
```
295288

296289
The spec field is `PageComponentSchema.responsiveStyles` (`ResponsiveStylesSchema` —
297-
see `node_modules/@objectstack/spec/src/ui/responsive.zod.ts`). See ADR-0065
298-
(SDUI styling model).
290+
see `node_modules/@objectstack/spec/src/ui/responsive.zod.ts`).
299291

300292
**In the source tiers (`kind:'html'` / `kind:'react'`) the same rule holds — no
301293
Tailwind `className` — but the primitive differs:**
@@ -328,12 +320,6 @@ compiles each `*.md` into a `doc` item that travels inside the package
328320
artifact and renders in the console at `/docs/<name>`. Docs are also the
329321
grounding the AI assistant reads about a package.
330322

331-
```
332-
src/docs/
333-
crm_index.md → doc "crm_index" → /docs/crm_index
334-
crm_user_guide.md → doc "crm_user_guide" → /docs/crm_user_guide
335-
```
336-
337323
### Authoring rules (each enforced by `os build`)
338324

339325
1. **Flat directory.** Every `.md` lives directly in `src/docs/`;
@@ -382,15 +368,10 @@ navigation: [
382368
]
383369
```
384370

385-
A platform-level "Documentation" portal (browse/search all docs by
386-
package) is a later, additive concern — author-side, nothing to model now.
387-
388-
> **Live instances vs. structural views.** For a *live, interactive
371+
> **Live instances.** For a *live, interactive
389372
> instance* — a dashboard, a report, a record table — **don't embed it**:
390373
> link to it by URL and let the platform render it (one source, never a
391-
> stale copy). But for *structural metadata that no single screen shows as
392-
> one picture* — a state machine, a flow, a permission matrix — embed a
393-
> read-only view inline with a `metadata` fence (below).
374+
> stale copy).
394375
395376
### Inline metadata views — the `metadata` fence (ADR-0051)
396377

@@ -424,8 +405,7 @@ package** — a dead same-package reference fails the build (same posture as
424405
a broken link). At render time a missing or forbidden reference degrades to
425406
a placeholder, never a crash.
426407

427-
Scope is deliberately narrow: **only** `state_machine`, `flow`,
428-
`permission`. Embedding an `object` (data model) or an arbitrary SDUI
408+
Embedding an `object` (data model) or an arbitrary SDUI
429409
component is **not** supported. **`permission` caveat:** the matrix is not
430410
yet projected to the reader's own permissions (ADR-0051 P3) — do not place a
431411
`permission` embed in a doc reachable by less-privileged or anonymous

0 commit comments

Comments
 (0)