|
1 | 1 | # Pages & Docs |
2 | 2 |
|
3 | 3 | - [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) |
5 | 5 | - [AI-authored source pages](#ai-authored-source-pages--kindhtml-and-kindreact-adr-00800081) · [Styling a page](#styling-a-page-adr-0065--responsivestyles-not-classname) |
6 | 6 | - [Docs — Package Documentation](#docs--package-documentation-adr-0046) · [Authoring rules](#authoring-rules-each-enforced-by-os-build) |
7 | 7 | - [Routing model](#routing-model--platform-level-viewer-opt-in-entry) · [Inline metadata views](#inline-metadata-views--the-metadata-fence-adr-0051) · [Example](#example) |
8 | 8 |
|
9 | 9 | ## Pages — Lightning-Style Page Layouts |
10 | 10 |
|
11 | 11 | 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**. |
14 | 13 |
|
15 | 14 | Register under `defineStack({ pages: [...] })`. |
16 | 15 |
|
@@ -47,83 +46,84 @@ which contain components. |
47 | 46 |
|
48 | 47 | | `type` | Use | |
49 | 48 | |:---------------------|:----| |
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 | |
51 | 50 | | `page:card` | Bordered/un-bordered card with `children: Component[]` (plus an optional `footer: Component[]` slot) | |
52 | 51 | | `flex` | Generic styleable box (`properties.children`) — the workhorse for custom layout; style via `responsiveStyles` (see Styling below) | |
53 | 52 | | `element:text` | Text node — `properties.content`; style via `responsiveStyles` | |
54 | 53 | | `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 }]` | |
57 | 56 | | `record:related_list` | Related-list (child records via lookup) | |
58 | 57 | | `nav:menu` | Quick-create / nav menu bound to current context | |
59 | 58 | | `object-metric` | Single KPI widget (count/sum/avg) | |
60 | 59 | | `object-chart` | Embedded chart | |
61 | 60 |
|
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; |
| 82 | +the page-break hints `repeatTableHeaders` (on by default) and |
| 83 | +`avoidBreakInside` (keep each block whole). |
| 84 | + |
| 85 | +Every block, at any depth, must draw all it declares, in full, the same at |
| 86 | +any width: the containers `page:section` / `page:card` / |
| 87 | +`page:footer`, the field blocks `record:details` / `record:highlights`, the |
| 88 | +all-lines child table `record:line_items`, `element:text` / `element:image` / |
| 89 | +`element:divider` / `element:definition-list` / `element:repeater`, and the |
| 90 | +single values `element:number` / `object-metric`. `os validate` / `os build` |
| 91 | +and the page save door refuse any other block (`print-page-block-unprintable`) |
| 92 | +with its reason: it **draws a window of its rows** (`record:related_list`, |
| 93 | +`object-grid` — use `record:line_items`), it **lays itself out to the screen** |
| 94 | +(`page:header`, sidebars, kanban / calendar / gantt / map), it has **nothing |
| 95 | +to print** (`record:path`, tabs, buttons, inputs, forms — values print through |
| 96 | +`record:details`), or it is **outside the vocabulary** (`flex`, `object-chart`, |
| 97 | +a plugin widget), so nothing answers for how it prints. |
| 98 | + |
| 99 | +Validated today; the console's print rendering that applies it follows, so |
| 100 | +`os validate` warns the layout is not yet applied. List export has no `'pdf'`; |
| 101 | +a list prints as shown via the view's `allowPrinting`. |
63 | 102 |
|
64 | 103 | <!-- os:check --> |
65 | 104 | ```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 | | -}); |
| 105 | +import { definePage } from '@objectstack/spec/ui'; |
73 | 106 |
|
74 | | -export const LeadDetailPage = definePage({ |
75 | | - name: 'lead_detail_page', |
76 | | - label: 'Lead Detail', |
77 | | - type: 'record', |
78 | | - object: 'lead', |
79 | | - template: 'three-column', |
| 107 | +export const InvoicePrintPage = definePage({ |
| 108 | + name: 'invoice_print', label: 'Invoice', type: 'record', object: 'invoice', |
| 109 | + print: { paperSize: 'A4', margins: { top: 15, right: 12, bottom: 15, left: 12 }, |
| 110 | + repeatHeader: true, repeatFooter: true, pageNumbers: true }, |
80 | 111 | 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… |
| 112 | + { name: 'header', components: [ |
| 113 | + { type: 'element:text', properties: { content: 'ACME Ltd' } }, |
| 114 | + ] }, |
| 115 | + { name: 'main', components: [ |
| 116 | + { type: 'record:details' }, |
| 117 | + { type: 'record:line_items', properties: { childObject: 'invoice_line', relationshipField: 'invoice', |
| 118 | + columns: [{ name: 'product' }, { name: 'quantity' }, { name: 'unit_price' }, { name: 'amount' }] } }, |
| 119 | + ] }, |
| 120 | + { name: 'footer', components: [ |
| 121 | + { type: 'element:text', properties: { content: 'Due within 30 days.' } }, |
| 122 | + ] }, |
111 | 123 | ], |
112 | 124 | }); |
113 | 125 | ``` |
114 | 126 |
|
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 | | -
|
127 | 127 | ### AI-authored *source* pages — `kind:'html'` and `kind:'react'` (ADR-0080/0081) |
128 | 128 |
|
129 | 129 | Besides the structured `regions` model above, a page's whole body can be written |
@@ -192,19 +192,13 @@ The source is real React executed at render by the runtime. The injected scope a |
192 | 192 | itself? Author the page as `type:'record'`, where the context exists |
193 | 193 | - `data` / `variables` / `page` |
194 | 194 |
|
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 | | - |
199 | 195 | > **Do not guess props — read the contract.** Each injected block's full prop set |
200 | 196 | > (name, type, `data`/`controlled`/`callback` kind, required, description) is the |
201 | 197 | > **[React-tier component contract](../references/react-blocks.md)**, generated from |
202 | 198 | > 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, |
| 199 | +> The `data` props are sourced from the platform's spec schemas (FormView, |
205 | 200 | > ListView, Chart, …) — the same protocol the server validates; |
206 | 201 | > `binding`/`controlled`/`callback` are the React overlay. |
207 | | -> (Maintainers: regenerate with `pnpm --filter @objectstack/spec gen:react-blocks`.) |
208 | 202 |
|
209 | 203 | Master/detail (click a row → edit it → save refreshes the list): |
210 | 204 |
|
@@ -294,8 +288,7 @@ Rules: |
294 | 288 | ``` |
295 | 289 |
|
296 | 290 | 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). |
| 291 | +see `node_modules/@objectstack/spec/src/ui/responsive.zod.ts`). |
299 | 292 |
|
300 | 293 | **In the source tiers (`kind:'html'` / `kind:'react'`) the same rule holds — no |
301 | 294 | Tailwind `className` — but the primitive differs:** |
@@ -328,12 +321,6 @@ compiles each `*.md` into a `doc` item that travels inside the package |
328 | 321 | artifact and renders in the console at `/docs/<name>`. Docs are also the |
329 | 322 | grounding the AI assistant reads about a package. |
330 | 323 |
|
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 | | - |
337 | 324 | ### Authoring rules (each enforced by `os build`) |
338 | 325 |
|
339 | 326 | 1. **Flat directory.** Every `.md` lives directly in `src/docs/`; |
@@ -382,15 +369,10 @@ navigation: [ |
382 | 369 | ] |
383 | 370 | ``` |
384 | 371 |
|
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 |
| 372 | +> **Live instances.** For a *live, interactive |
389 | 373 | > instance* — a dashboard, a report, a record table — **don't embed it**: |
390 | 374 | > 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). |
| 375 | +> stale copy). |
394 | 376 |
|
395 | 377 | ### Inline metadata views — the `metadata` fence (ADR-0051) |
396 | 378 |
|
@@ -424,8 +406,7 @@ package** — a dead same-package reference fails the build (same posture as |
424 | 406 | a broken link). At render time a missing or forbidden reference degrades to |
425 | 407 | a placeholder, never a crash. |
426 | 408 |
|
427 | | -Scope is deliberately narrow: **only** `state_machine`, `flow`, |
428 | | -`permission`. Embedding an `object` (data model) or an arbitrary SDUI |
| 409 | +Embedding an `object` (data model) or an arbitrary SDUI |
429 | 410 | component is **not** supported. **`permission` caveat:** the matrix is not |
430 | 411 | yet projected to the reader's own permissions (ADR-0051 P3) — do not place a |
431 | 412 | `permission` embed in a doc reachable by less-privileged or anonymous |
|
0 commit comments