Repository navigation
feat(print): render a page that declares print — print CSS, printable blocks whole, record print action (objectui#11958) - #12356
Conversation
…le blocks whole, record print action (objectui#11958) PageRenderer mounts a print stylesheet built from `page.print` (@page size, orientation, margins and page numbers; running header and footer regions; repeated table heads; break-inside) and publishes the declaration through PrintPageContext. record:line_items names its row cap on a print page instead of cutting lines in silence; a collapsible record:details section draws open. The record detail view offers a Print action when its page declares `print`. The leftover "PDF export declined" sentences now point at print pages. Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB Co-authored-by: Claude <noreply@anthropic.com>
…ecord print action; docs and changeset (objectui#11958) Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB Co-authored-by: Claude <noreply@anthropic.com>
…play: contents on screen (objectui#11958) Measured in Chromium 141: a div given display: table-header-group printed once, on the first sheet; a real thead / tfoot repeated on every sheet. The running regions now sit in a table whose boxes are display: contents on screen, so the layout is unchanged there, and the print sheet turns it into a table on paper. Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB Co-authored-by: Claude <noreply@anthropic.com>
…e; test literal typed (objectui#11958) Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs read: card #11958 (body, restart ① Derived judgmentsGate verdicts on the head. 31 check-runs Public-surface changes the diff implies, package by package:
Accept-set changes the diff implies: none on the authoring side. The renderer reads "The page renders on screen exactly as before" (card item 1), judged on the diff:
Card items 2–4. (2) The printable subset is read from ② Semver levelThe changeset
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #11958
Clause-②: yes
Card ② of the ruling of record
6051470224on objectstack#8346 (letter B′): a page that declaresprintnow prints as a document, the printable blocks reach paper whole, and the record page offers a Print action (M1, zero server).What changed
packages/components/src/renderers/layout/page-print.ts(new),page.tsxPageRendererstampsdata-print-pageon a page whoseprintis an object, mounts a stylesheet built from the declaration, and publishes the declaration throughPrintPageProvider. Every rule is inside@media print.page.tsx(RunningRegions)repeatHeader/repeatFooter, the page's ownheader/footerregion goes in a realthead/tfoot. On screen the table, its sections, rows and cells aredisplay: contents, so the regions stay the layout's flex children.packages/react/src/context/PrintPageContext.tsx(new)PrintPageProvider,usePrintPage()andPrintPageContext.usePrintPage()returns the page'sprintdeclaration, ornulloutside a print page.record:line_itemspackages/plugin-form/src/LineItemsPanel.tsx$count: true. When the probe comes back, the panel draws the cap's rows and the sharedNonGridRowCeilingNoteunder them. The note names both numbers when the server reports a total. Off a print page the wire and the panel are unchanged.record:detailspackages/plugin-detail/src/DetailSection.tsxpackages/app-shell/src/views/RecordDetailView.tsxprint, a Print button (record-print-button) callswindow.print(). Other record pages get no button.types/src/objectql.ts,ListView.tsx(two comments),ReportViewer.tsx, app-shellstyles.css, app-shellDashboardView.tsxLIST_VIEW_EXPORT_PDF_RETIRED: a printable document is a page that declaresprint.DashboardView.tsxis a fifth site in app-shell that card ①'s measurement did not list.content/docs/guide/print-pages.md(new, plusmeta.json),packages/react/README.mdLanding point outside the claimed surface: the page renderer is
PageRendererin@object-ui/components. It renders every page's regions, for bothPageViewandRecordDetailView, so it owns the print stylesheet. The context had to live in@object-ui/reactso thatplugin-formandplugin-detailcan read it. The line-items and detail-section edits are in their own packages because those packages own the two block renderers.Per key: read now? (ledger rows in
@objectstack/spec/liveness/page.json, allplannednaming this card)print(the declaration)paperSize@page { size: SIZE ORIENTATION }, A4 when omittedorientationmargins.top/.right/.bottom/.left@pagemargin-SIDE: Nmmper declared side. A side left out keeps the console's 12mm default fromstyles.css.repeatHeaderheaderregion in atheadrepeatFooterfooterregion in atfootpageNumbers@bottom-right { content: counter(page) " / " counter(pages) }repeatTableHeadersthead { display: table-header-group }(on when omitted).falsegivestable-row-group.avoidBreakInsidebreak-inside: avoidon each block of a regionThe ledger flip (
plannedtolive, droppingauthorWarn/authorHint) is an objectstack edit at the.objectui-shapin that carries this PR. This PR does not make that edit.Real-render measurement (Chromium 141.0.7390.37, one-off, not a repo file)
A replica of the
RecordDetailView/PageRendererDOM was printed to PDF. It had the shell'sh-svh/overflow-hiddenchain, the record view's own chrome and fallback panel, and a 120-line table inmain. The stylesheet was the onebuildPagePrintStylesheetemits for{ paperSize: 'A5', repeatHeader: true, repeatFooter: true, pageNumbers: true, margins: { top: 12, bottom: 12, left: 10, right: 10 } }. Per-sheet text came frompdftotext:420 x 594.96 pts (A5). The letterhead, the footer and the table'stheadappear on all 5 sheets. The page numbers read1/5…5/5. All 120 lines are present, and no console chrome string appears.display: table-header-groupon the regiondivs. In Chromium that printed the letterhead on sheet 1 only and the footer on the last sheet only. A realthead/tfootrepeats on every sheet; a CSS-display table does not, and isolated experiments showed the same. That result is why the regions sit in a real table that isdisplay: contentson screen.display: contentstable, the bounding boxes of the header, main and footer regions are identical (identical true).Tests (head
8817cba)New pins:
packages/components/src/__tests__/page-print-stylesheet-11958.test.tsx(15). One case per key, the injection boundary, the real render throughSchemaRenderer, the running-region table, and controls. A print page with no running regions is DOM-identical to the same page withoutprint, apart from the attribute and the style element.packages/plugin-form/src/__tests__/lineItemsPrintRowCap-11958.test.tsx(6). The probe row and$count, the note with and without a reported total, and three controls.packages/plugin-detail/src/__tests__/detailSectionPrintPage-11958.test.tsx(2).packages/app-shell/src/views/RecordDetailView.printAction-11958.test.tsx(2). The action calls the print preview, and the page is a print page. Control: noprintmeans no action and no print page.packages/app-shell/src/__tests__/printable-blocks-registered-11958.test.ts. Every type in the spec'sPRINTABLE_PAGE_COMPONENT_TYPES(read from@objectstack/spec/ui, with no second list kept) has a registered renderer, plus a control.Runs, all from the repo root under the shared verify lock:
LineItemsPanel/record:line_items:Test Files 23 passed (23),Tests 286 passed (286).DetailSection/detail-section/record:details, plus components files that mention the page renderer or regions:Test Files 75 passed | 1 skipped (76),Tests 830 passed | 8 skipped (838).RecordDetailView.*.test.tsx, thePageViewtests,page-kind-writing-end-9718,print-stylesheet-4462, the product-neutral ratchet, the list / report print-button semantics,objectql.exportOptionsandiconOnlyButtonCensus-12143:Test Files 72 passed (72),Tests 495 passed (495).type-checkexit codes (tsc --noEmitplus the test config):types 0,react 0,components 0,plugin-form 0,plugin-detail 0,plugin-list 0,plugin-report 0,app-shell 0. Components, plugin-form and app-shell were re-run after the last test edits.Gates on
8817cba, each exit 0:check:phantom-deps,check:self-import,check:unreferenced-sources,check:doc-example-readers,check:handler-key-reads,check:side-effects-array,check:element-data-source-declaration,check:spec-symbols,check:action-forward-parity,check:designer-field-key-parity,check:icon-record-names,check:i18n-keys,check:i18n-drift,check:i18n-designer-parity,check:doc-json,check:doc-fences,check:doc-example-ids,check:doc-types,check:docs-route-closure,check:new-line-citations,check-changeset-fixed,check-changeset-presence,check-changeset-no-major.check:doc-jsonfirst caught a real finding in the new guide:record:detailscolumnsmust be a string enum. The example now carries none, and the gate judges 242 documents accepted and 0 refused.check:doc-examples/check:doc-snippetsstopped at PRECONDITION NOT MET (exit 2) because seven packages outside this diff are unbuilt; the new guide has no TS fence.check:readme-exportsexited 1 for the same unbuilt-package reason: 94 self-imports could not be judged. It reported 0 wrong-path and 0 fabricated, and the new@object-ui/reactREADME snippet resolves against the rebuilt dist.ESLint on the 16 changed TS/TSX files (
--format json): 16 files, 0 errors, exit 0. This narrowing is complete because noparserOptions.project/projectServiceappears in any eslint config, so linting is not type-aware and this diff cannot move another file's verdict. The warnings on added lines are the movedRecordofstringtoanycast inLineItemsPaneland thereact-refresh/only-export-componentspair on the new context file, which has the same shape asPreviewModeContext.Clause ② — measured on the built declarations
@object-ui/reactwidens.dist/context/PrintPageContext.d.tsis re-exported bydist/context/index.d.ts, soPrintPageContext,PrintPageProviderandusePrintPageare new public exports. Control:usePreviewModeresolves indist/context/PreviewModeContext.d.ts. At baseb9814b3,git grep usePrintPage -- packages/react/srcexits 1, and the controlusePreviewModeexits 0. The changeset isminor.@object-ui/typesdoes not widen in this PR.PageNodeSchemaalready carriedprintby reference (Omitof the specPageoverPAGE_SPEC_EXCLUDED) since objectui#12030's@objectstack/spec18.0.0-next.0 bump; type-check confirmsschema.printis typed as the spec enum. This PR's diff underpackages/types/srcis comment lines only, so the mirror the claim anticipated was already in place.@object-ui/componentsemitsdist/renderers/layout/page-print.d.ts, but no.d.tsreachable from the package's only export.references it, so its public surface is unchanged. The behaviour change isminor.Acceptance notes (not filed)
usePageAssignmentalready picks for the object, which is the firsttype: 'record'page. Binding a SEPARATE print page next to a normal record page would need a selection rule this repo does not have. Today a second record page for the same object never renders. That needs a decision and is not done here.objectstack#1301"declined" sentences outside this card's surface:plugin-grid/src/ObjectGrid.tsx(export-format comment), and the headers ofprint-stylesheet-4462.test.ts,ListView.printButtonSemantics.test.tsx,ReportViewer.printButtonSemantics.test.tsx,exportGate.test.tsxandobjectql.exportOptions.test.ts. They are comments, and no carrier is named.record:highlightsvalues usetruncate(an ellipsis on a long value), so a very long highlight value is cut on paper as it is on screen. Not changed here.record:line_itemspanel on a print page is still the editable grid. On paper it prints its Save button and inputs as drawn.Implemented by the dispatched
os-devrun in sessionhttps://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB(seatdomain:ui#2).Generated by Claude Code