Skip to content

Commit 808cc8b

Browse files
skills(ui): the page skill learns the print page — print, the printable block subset and its lint
A printable document is a page that declares `print`; teach the keys and their paper meaning, the three page types that may carry it, the printable subset by reason, the `print-page-block-unprintable` refusal and the two list facts (no `'pdf'` export; `allowPrinting`). Paid inside `pages.md` by deleting the Record Detail example (its lessons move into the component catalogue rows) and restated lines; one routing word in SKILL.md; one pointer sentence in actions.md paid by its restated best-practice bullets. Claude-Session: https://claude.ai/code/session_01CXydFDyiQwNbGFkmwrcRQq Co-authored-by: Claude <noreply@anthropic.com>
1 parent c8bb3c8 commit 808cc8b

3 files changed

Lines changed: 72 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: 67 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,84 @@ 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;
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`.
63102

64103
<!-- os:check -->
65104
```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';
73106

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 },
80111
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+
] },
111123
],
112124
});
113125
```
114126

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-
127127
### AI-authored *source* pages — `kind:'html'` and `kind:'react'` (ADR-0080/0081)
128128

129129
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
192192
itself? Author the page as `type:'record'`, where the context exists
193193
- `data` / `variables` / `page`
194194

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-
199195
> **Do not guess props — read the contract.** Each injected block's full prop set
200196
> (name, type, `data`/`controlled`/`callback` kind, required, description) is the
201197
> **[React-tier component contract](../references/react-blocks.md)**, generated from
202198
> 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,
205200
> ListView, Chart, …) — the same protocol the server validates;
206201
> `binding`/`controlled`/`callback` are the React overlay.
207-
> (Maintainers: regenerate with `pnpm --filter @objectstack/spec gen:react-blocks`.)
208202
209203
Master/detail (click a row → edit it → save refreshes the list):
210204

@@ -294,8 +288,7 @@ Rules:
294288
```
295289

296290
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`).
299292

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

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-
337324
### Authoring rules (each enforced by `os build`)
338325

339326
1. **Flat directory.** Every `.md` lives directly in `src/docs/`;
@@ -382,15 +369,10 @@ navigation: [
382369
]
383370
```
384371

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
389373
> instance* — a dashboard, a report, a record table — **don't embed it**:
390374
> 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).
394376
395377
### Inline metadata views — the `metadata` fence (ADR-0051)
396378

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

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
429410
component is **not** supported. **`permission` caveat:** the matrix is not
430411
yet projected to the reader's own permissions (ADR-0051 P3) — do not place a
431412
`permission` embed in a doc reachable by less-privileged or anonymous

0 commit comments

Comments
 (0)