mosage 0.1.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/README.md +16 -217
- package/bin.js +2 -0
- package/dist/build-C7NW_3Pk.js +14 -0
- package/dist/check-CP4873Wx.js +41 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +228 -0
- package/dist/config-DPm1BBAb.js +2619 -0
- package/dist/config-TlTe7Ona.d.ts +24 -0
- package/dist/context-BqsdSrAQ.js +1084 -0
- package/dist/dev-Biz42qlu.js +17 -0
- package/dist/diagram-xlVDekYk.js +763 -0
- package/dist/export-Bi6nuxjT.js +31 -0
- package/dist/import-D2jNB07F.js +25 -0
- package/dist/index.d.ts +455 -0
- package/dist/index.js +693 -0
- package/dist/init-Bbtj2pxF.js +262 -0
- package/dist/preview-CLm51aRt.js +19 -0
- package/dist/sdk-DjpX6mCv.js +51 -0
- package/dist/vite/index.d.ts +25 -0
- package/dist/vite/index.js +2 -0
- package/env.d.ts +83 -0
- package/package.json +84 -64
- package/skills/apply-comments/SKILL.md +43 -56
- package/skills/create-doc/SKILL.md +106 -0
- package/skills/create-theme/SKILL.md +184 -0
- package/skills/current-doc/SKILL.md +120 -0
- package/skills/doc-authoring/SKILL.md +434 -0
- package/skills/doc-authoring/references/assets.md +47 -0
- package/skills/doc-authoring/references/design-system.md +81 -0
- package/skills/doc-authoring/references/long-form.md +131 -0
- package/skills/doc-authoring/references/pagination.md +118 -0
- package/skills/doc-authoring/references/tables-and-charts.md +161 -0
- package/src/app/app.tsx +42 -0
- package/src/app/components/data-table.tsx +196 -0
- package/src/app/components/design-panel/design-panel.tsx +318 -0
- package/src/app/components/design-panel/design-provider.tsx +121 -0
- package/src/app/components/design-panel/use-design.ts +85 -0
- package/src/app/components/diagram.tsx +76 -0
- package/src/app/components/doc-assets.tsx +129 -0
- package/src/app/components/doc-search.tsx +248 -0
- package/src/app/components/doc-sidebar.tsx +162 -0
- package/src/app/components/flow-page.tsx +93 -0
- package/src/app/components/footnote.tsx +204 -0
- package/src/app/components/image-placeholder.tsx +50 -0
- package/src/app/components/inspector/inspector.tsx +518 -0
- package/src/app/components/numbering.tsx +224 -0
- package/src/app/components/page-frame.tsx +70 -0
- package/src/app/components/sidebar/folder-item.tsx +212 -0
- package/src/app/components/sidebar/icon-picker.tsx +99 -0
- package/src/app/components/sidebar/sidebar.tsx +252 -0
- package/src/app/components/table-of-contents.tsx +93 -0
- package/src/app/components/theme-toggle.tsx +50 -0
- package/src/app/components/themes/markdown.tsx +249 -0
- package/src/app/components/themes/theme-preview.tsx +74 -0
- package/src/app/components/ui/menu.tsx +143 -0
- package/src/app/index.html +12 -0
- package/src/app/lib/agent-bridge.ts +140 -0
- package/src/app/lib/assets.ts +151 -0
- package/src/app/lib/design-presets.ts +109 -0
- package/src/app/lib/design.ts +88 -0
- package/src/app/lib/diagnostics.ts +282 -0
- package/src/app/lib/doc-preview.tsx +29 -0
- package/src/app/lib/docs.ts +26 -0
- package/src/app/lib/docx/extract.ts +1623 -0
- package/src/app/lib/docx/fonts.test.ts +136 -0
- package/src/app/lib/docx/fonts.ts +166 -0
- package/src/app/lib/docx/media.ts +102 -0
- package/src/app/lib/docx/model.ts +206 -0
- package/src/app/lib/docx/paragraph.test.ts +92 -0
- package/src/app/lib/docx/paragraph.ts +107 -0
- package/src/app/lib/docx/props.ts +187 -0
- package/src/app/lib/docx/styles.ts +306 -0
- package/src/app/lib/docx/units.ts +35 -0
- package/src/app/lib/docx/write.test.ts +507 -0
- package/src/app/lib/docx/write.ts +581 -0
- package/src/app/lib/docx/xml.ts +39 -0
- package/src/app/lib/export-docx.ts +289 -0
- package/src/app/lib/export-dom.ts +318 -0
- package/src/app/lib/export-html.ts +156 -0
- package/src/app/lib/export-image.ts +70 -0
- package/src/app/lib/export-pdf.ts +165 -0
- package/src/app/lib/flow-measure.test.ts +31 -0
- package/src/app/lib/flow-measure.ts +183 -0
- package/src/app/lib/flow.test.ts +110 -0
- package/src/app/lib/flow.ts +136 -0
- package/src/app/lib/folders.ts +192 -0
- package/src/app/lib/footnotes.test.tsx +102 -0
- package/src/app/lib/footnotes.ts +94 -0
- package/src/app/lib/inspector/fiber.ts +99 -0
- package/src/app/lib/labels.test.ts +18 -0
- package/src/app/lib/labels.ts +181 -0
- package/src/app/lib/outline.ts +118 -0
- package/src/app/lib/page-context.tsx +43 -0
- package/src/app/lib/page-range.test.ts +95 -0
- package/src/app/lib/page-range.ts +90 -0
- package/src/app/lib/print-ready.ts +69 -0
- package/src/app/lib/rasterize.ts +173 -0
- package/src/app/lib/scan.ts +26 -0
- package/src/app/lib/sdk.test.ts +32 -0
- package/src/app/lib/sdk.ts +115 -0
- package/src/app/lib/themes.ts +31 -0
- package/src/app/lib/use-doc-module.ts +53 -0
- package/src/app/lib/use-doc-pages.ts +147 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/lib/view-mode.test.ts +91 -0
- package/src/app/lib/view-mode.ts +104 -0
- package/src/app/main.tsx +14 -0
- package/src/app/routes/assets.tsx +257 -0
- package/src/app/routes/doc.tsx +877 -0
- package/src/app/routes/home-shell.tsx +203 -0
- package/src/app/routes/home.tsx +269 -0
- package/src/app/routes/themes.tsx +121 -0
- package/src/app/styles.css +97 -0
- package/src/app/virtual.d.ts +30 -0
- package/template/AGENTS.md +27 -0
- package/template/README.md +39 -0
- package/template/docs/getting-started/index.tsx +230 -0
- package/template/mosage.config.ts +5 -0
- package/template/package.json +24 -0
- package/template/tsconfig.json +17 -0
- package/README.en.md +0 -57
- package/dist/cli.js +0 -4545
- package/dist/web/assets/index-Czg2WeHe.js +0 -182
- package/dist/web/assets/index-DKAyt92W.css +0 -1
- package/dist/web/index.html +0 -15
- package/skills/current-position/SKILL.md +0 -65
- package/skills/kickoff/SKILL.md +0 -105
- package/skills/mosage-reference/SKILL.md +0 -164
- package/skills/outline/SKILL.md +0 -79
- package/skills/review/SKILL.md +0 -64
- package/skills/write-chapter/SKILL.md +0 -58
- package/template/book/STYLE.md +0 -6
- package/template/book/assets/.gitkeep +0 -0
- package/template/book/book.yaml +0 -42
- package/template/book/brief.md +0 -6
- package/template/book/chapters/.gitkeep +0 -0
- package/template/book/notes/README.md +0 -7
- package/template/project/AGENTS.md +0 -82
- package/template/project/CLAUDE.md +0 -1
- package/template/project/README.md +0 -48
- package/template/project/books/.gitkeep +0 -0
- package/template/project/gitignore +0 -5
- package/template/project/mosage.yaml +0 -23
- package/template/project/notes/README.md +0 -8
- package/template/project/package.json +0 -14
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doc-authoring
|
|
3
|
+
description: Technical reference for writing or editing MoSage pages — file contract, the A4/B4/A3 page canvas, print type scale, the vertical budget that decides where a page breaks, tables, charts, table of contents, page numbers, running headers/footers, and assets. Consult this whenever you are about to write or modify any file under `docs/<id>/`, including from inside the `create-doc` workflow, or for any ad-hoc document edit. Triggers on phrases like "edit the report", "fix this page", "add a section", "change the margins", "add a table", "page numbers", "table of contents", "how do documents work here".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Authoring MoSage pages
|
|
7
|
+
|
|
8
|
+
This skill is the **technical reference** for everything inside `docs/<id>/index.tsx`. It owns no workflow:
|
|
9
|
+
|
|
10
|
+
- `create-doc` owns "draft a new document" — it asks the scoping questions, then delegates the *how* to this skill.
|
|
11
|
+
- Any ad-hoc edit (fix a table, retitle a section, adjust margins) should also read this first.
|
|
12
|
+
|
|
13
|
+
## Primitive references
|
|
14
|
+
|
|
15
|
+
Read the matching reference **before** using a primitive:
|
|
16
|
+
|
|
17
|
+
| Primitive | Read before | File |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `design` const + `var(--od-*)` tokens | writing any new document | `references/design-system.md` |
|
|
20
|
+
| `flow()` auto-pagination | any body content (the default) | `references/pagination.md` |
|
|
21
|
+
| Vertical budget (fixed pages) | laying out a cover or divider by hand | `references/pagination.md` |
|
|
22
|
+
| Tables, stat rows, inline charts | rendering data of any kind | `references/tables-and-charts.md` |
|
|
23
|
+
| Assets + `<ImagePlaceholder>` | importing images or leaving a placeholder | `references/assets.md` |
|
|
24
|
+
| Footnotes, `<Figure>`, `<Ref>`, `<DataTable>` | any note, numbered figure, cross-reference, or `.csv` | `references/long-form.md` |
|
|
25
|
+
|
|
26
|
+
## Themes
|
|
27
|
+
|
|
28
|
+
If `themes/<id>.md` exists at the project root and the document is meant to follow it, **the theme file overrides the defaults in this skill** — its palette, typography, page setup, and paste-ready components are authoritative. Read the theme file end-to-end before applying anything else here, and set `meta.theme: '<id>'` so the document back-links to it (chip on the document card, listing on `/themes/<id>`).
|
|
29
|
+
|
|
30
|
+
Themes are produced by the `create-theme` skill and are pure documentation: copy the `design` const and the Title / Footer / Table components straight into the document. `mode: dark` in a theme's frontmatter applies to its cover, not to body pages — anything that prints stays light.
|
|
31
|
+
|
|
32
|
+
## Hard rules
|
|
33
|
+
|
|
34
|
+
- Put the document under `docs/<kebab-case-id>/`.
|
|
35
|
+
- Entry is `docs/<id>/index.tsx`. Images/fonts go under `docs/<id>/assets/`.
|
|
36
|
+
- Do **not** touch `package.json`, `mosage.config.ts`, or other documents.
|
|
37
|
+
- Do not add dependencies. Only `react`, `mosage`, and standard web APIs are available.
|
|
38
|
+
- A document is **one `index.tsx` plus `assets/`** — nothing else. Helper components and constants live inside `index.tsx`; no sibling `.tsx` files, no `README.md`.
|
|
39
|
+
|
|
40
|
+
## File contract
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// docs/<id>/index.tsx
|
|
44
|
+
import type { DocMeta, DocPage } from 'mosage';
|
|
45
|
+
|
|
46
|
+
const Cover: DocPage = () => <div>…</div>;
|
|
47
|
+
const Body: DocPage = () => <div>…</div>;
|
|
48
|
+
|
|
49
|
+
export const meta: DocMeta = {
|
|
50
|
+
title: 'Q3 Infrastructure Review',
|
|
51
|
+
subtitle: 'Platform team',
|
|
52
|
+
author: 'Platform Engineering',
|
|
53
|
+
pageSize: 'A4',
|
|
54
|
+
orientation: 'portrait',
|
|
55
|
+
createdAt: '2026-08-15T13:44:40.268Z',
|
|
56
|
+
};
|
|
57
|
+
export default [Cover, Body] satisfies DocPage[];
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- `export default` is a **non-empty array of entries**. An entry is either a zero-prop React component (one fixed page) or a `flow(<>…</>)` section the framework paginates by measuring. Mix them freely — the usual shape is a fixed cover, a fixed contents page, then one flow section for the body.
|
|
61
|
+
- **Default to `flow()` for body content.** Hand-splitting prose into fixed pages produces documents where every heading starts a half-empty page. Read `references/pagination.md` before writing either kind.
|
|
62
|
+
- `meta.pageSize` is `'A4' | 'B4' | 'A3'` (default `'A4'`) and `meta.orientation` is `'portrait' | 'landscape'` (default portrait). **These six combinations are the only sheets there are** — there is no Letter, no A5, no custom size, and no way to set a page's dimensions by hand. The same value drives the on-screen page, the `@page` size when printing, and the HTML export.
|
|
63
|
+
- `meta.createdAt` is an **ISO 8601 string literal** set once when the doc is scaffolded — the home page sorts on it. **Immediately before writing the file, run `node -e "console.log(new Date().toISOString())"` and paste the exact output.** It must stay a plain string literal (no `new Date(...)`): the framework reads it with a regex at build time, it never evaluates the module.
|
|
64
|
+
|
|
65
|
+
## Two ways to fill pages
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { flow, type DocEntry } from 'mosage';
|
|
69
|
+
|
|
70
|
+
const Body = flow(
|
|
71
|
+
<>
|
|
72
|
+
<h1 style={h1}>1. Findings</h1>
|
|
73
|
+
<p style={p}>…</p>
|
|
74
|
+
<Table>…</Table>
|
|
75
|
+
</>,
|
|
76
|
+
{ footer: Footer },
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
export default [Cover, Contents, Body] satisfies DocEntry[];
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Each direct child of the fragment is one atomic block. Blocks never split across pages; headings glue to what follows them; a caption marked `data-od-keep-with-previous` stays with its figure. Everything else — where the breaks land, how many pages the section becomes, the running footer on each — is the framework's job.
|
|
83
|
+
|
|
84
|
+
Fixed `DocPage` components remain the right tool for the cover, a contents page, or a divider whose layout *is* the content. Those pages are subject to the vertical budget below.
|
|
85
|
+
|
|
86
|
+
## The page canvas
|
|
87
|
+
|
|
88
|
+
| Size | Portrait px (96dpi) | Text block at 76px margins |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| A4 (210 × 297mm) | 794 × 1123 | 642 × 971 |
|
|
91
|
+
| B4 (JIS, 257 × 364mm) | 971 × 1376 | 819 × 1224 |
|
|
92
|
+
| A3 (297 × 420mm) | 1123 × 1587 | 971 × 1435 |
|
|
93
|
+
|
|
94
|
+
Landscape swaps the two numbers. B4 and A3 are for wide tables, plans, and posters — a body of prose set the full width of an A3 sheet is unreadable, so give a large sheet columns or wider margins.
|
|
95
|
+
|
|
96
|
+
You design as if the viewport is literally the page in CSS pixels. The viewer only scales the whole sheet.
|
|
97
|
+
|
|
98
|
+
- Use **absolute pixel values** for `font-size`, padding, and positioning. No `rem`, no `vw`/`vh`, no `%` for type.
|
|
99
|
+
- Each page's root element must fill the sheet: `width: '100%'; height: '100%'`.
|
|
100
|
+
- Prefer inline `style={{ … }}`. Any CSS you load is global — scope classnames carefully.
|
|
101
|
+
- The viewer's CSS reset strips list markers. A `<ul>`/`<ol>` needs an explicit `listStyle: 'disc outside'` / `'decimal outside'` or it renders as unindented plain lines.
|
|
102
|
+
- **1pt ≈ 1.333px.** Body copy at 14px prints as ~10.5pt; anything under 12px (9pt) is uncomfortable in print, and under 10px (7.5pt) is unreadable.
|
|
103
|
+
|
|
104
|
+
### Print type scale (start here)
|
|
105
|
+
|
|
106
|
+
| Element | Size | Notes |
|
|
107
|
+
| --- | --- | --- |
|
|
108
|
+
| Cover title | 40–52px | Cover page only |
|
|
109
|
+
| H1 / section opener | 26–32px | One per section |
|
|
110
|
+
| H2 / subsection | 18–22px | |
|
|
111
|
+
| H3 / run-in heading | 15–17px | Often bold body size |
|
|
112
|
+
| Body | 13–15px | 1.5–1.65 line-height |
|
|
113
|
+
| Caption / table cell | 10–12px | Tables can go to 11px |
|
|
114
|
+
| Footnote / footer | 9–10px | |
|
|
115
|
+
|
|
116
|
+
### Margins
|
|
117
|
+
|
|
118
|
+
- Standard report: **72–96px** (19–25mm) on all four sides.
|
|
119
|
+
- Bound/printed double-sided: add ~24px to the inner edge.
|
|
120
|
+
- Running header/footer live **inside** the margin band, not in the text block.
|
|
121
|
+
|
|
122
|
+
## Vertical budget — for fixed pages only
|
|
123
|
+
|
|
124
|
+
A `DocPage` does **not** scroll or reflow: anything past the bottom edge is silently cropped, so do the math before writing JSX. (Inside a `flow()` section the framework measures for you — this arithmetic is exactly what it removes.)
|
|
125
|
+
|
|
126
|
+
**Usable height** = `page_height − 2 × margin` (A4 @ 76px margins → **971px**).
|
|
127
|
+
**Text height** = `font_size × line_height × line_count`. A paragraph that wraps to 4 lines counts as 4.
|
|
128
|
+
**Lines per page** ≈ `971 / (14 × 1.55)` ≈ **44 lines of body copy** on A4 — that is the whole budget for one page.
|
|
129
|
+
|
|
130
|
+
`references/pagination.md` has the worked example, the character-count estimate for how many lines a paragraph will wrap to, and the rules for where to split.
|
|
131
|
+
|
|
132
|
+
## Headings and the outline
|
|
133
|
+
|
|
134
|
+
The framework builds the document outline by scanning rendered pages for `h1`, `h2`, `h3` (or any element carrying `data-od-heading`). That outline powers the sidebar **and** `<TableOfContents />`.
|
|
135
|
+
|
|
136
|
+
- Use real `<h1>/<h2>/<h3>` elements for section titles, styled inline. Don't fake a heading with a `<div>` — it disappears from the outline and the TOC.
|
|
137
|
+
- Conversely, don't use heading tags for decorative text (a cover eyebrow, a stat label). Mark those `<div data-od-outline="skip">` if you must use a heading tag for styling reasons.
|
|
138
|
+
- **The cover title and the word "Contents" both carry `data-od-outline="skip"`.** A contents list that opens with the cover and lists itself reads as a bug.
|
|
139
|
+
- Override the listed text with `data-od-heading="Short title"` when the visible heading is long or contains markup.
|
|
140
|
+
|
|
141
|
+
## Footnotes, numbering, and data
|
|
142
|
+
|
|
143
|
+
Four primitives resolve themselves from the rendered pages, the same way the
|
|
144
|
+
contents list does. Read `references/long-form.md` before using any of them.
|
|
145
|
+
|
|
146
|
+
- **`<Footnote>`** — numbered by position, printed at the foot of the page its
|
|
147
|
+
marker landed on, and its height is taken out of that page's budget before the
|
|
148
|
+
packer breaks. On a fixed page, add `<Footnotes />` where they should print.
|
|
149
|
+
- **`<Figure caption id>`** — a numbered figure (or `kind="table"`), caption and
|
|
150
|
+
content in one unbreakable block. `<ListOfFigures />` / `<ListOfTables />`
|
|
151
|
+
build the lists.
|
|
152
|
+
- **`<Ref to="id" />`** — "Figure 3", plus the page when the target is elsewhere.
|
|
153
|
+
- **`<DataTable rows={…}>`** — a print-shaped table from an imported `.csv`.
|
|
154
|
+
- **`<Diagram chart={…} caption>`** — an architecture or flow drawing from an
|
|
155
|
+
imported `.mmd`. Given a caption it numbers as a figure, like `<Figure>`.
|
|
156
|
+
|
|
157
|
+
`meta.labels` sets what they are called (`圖`, `表`) — the numbering itself is
|
|
158
|
+
structural.
|
|
159
|
+
|
|
160
|
+
## Diagrams
|
|
161
|
+
|
|
162
|
+
Write the drawing as Mermaid-flavoured text in `docs/<id>/<name>.mmd`, import
|
|
163
|
+
it, and hand it to `<Diagram>`:
|
|
164
|
+
|
|
165
|
+
```mermaid
|
|
166
|
+
%% docs/my-doc/architecture.mmd
|
|
167
|
+
flowchart TD
|
|
168
|
+
Client[使用者] -->|HTTPS| Gate{驗證}
|
|
169
|
+
Gate -->|通過| App[應用伺服器]
|
|
170
|
+
Gate -.->|拒絕| Deny([401])
|
|
171
|
+
App --> DB[資料庫]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { Diagram } from 'mosage';
|
|
176
|
+
import architecture from './architecture.mmd';
|
|
177
|
+
|
|
178
|
+
<Diagram chart={architecture} caption="請求路徑" width={420} />
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
It is compiled to SVG at build time and drawn with the document's own theme
|
|
182
|
+
variables, so it prints with the same ink and faces as the prose around it.
|
|
183
|
+
Never reach for an image of a diagram when the diagram can be written.
|
|
184
|
+
|
|
185
|
+
Supported: `flowchart`/`graph` with `TD` or `LR`; nodes as `A[box]`, `A(round)`,
|
|
186
|
+
`A([stadium])`, `A{decision}`, `A((circle))`; links `-->`, `---`, `-.->`, `==>`
|
|
187
|
+
with optional `|labels|`; chains `A --> B --> C`; `%%` comments. Anything else
|
|
188
|
+
in Mermaid's syntax — subgraphs, class diagrams, sequence diagrams — is not
|
|
189
|
+
supported, and a bad diagram fails the build with the line to fix.
|
|
190
|
+
|
|
191
|
+
Keep `width` inside the text block: a drawing wider than the column is a layout
|
|
192
|
+
fault, and `mosage check` reports it as one.
|
|
193
|
+
|
|
194
|
+
## Table of contents
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import { TableOfContents } from 'mosage';
|
|
198
|
+
|
|
199
|
+
const Contents: DocPage = () => (
|
|
200
|
+
<div style={page}>
|
|
201
|
+
<h1 style={h1}>Contents</h1>
|
|
202
|
+
<TableOfContents maxLevel={2} />
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Page numbers come from the scan, so they are always correct — **never hand-write a contents list**. The outline fills in after the first render pass; that is expected and it is resolved before PDF/HTML export serializes the pages.
|
|
208
|
+
|
|
209
|
+
## Page numbers, headers, footers
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
import { useDocPageCount, useDocPageNumber } from 'mosage';
|
|
213
|
+
|
|
214
|
+
const Footer = () => {
|
|
215
|
+
const page = useDocPageNumber();
|
|
216
|
+
const total = useDocPageCount();
|
|
217
|
+
return (
|
|
218
|
+
<div style={{ position: 'absolute', left: 76, right: 76, bottom: 40, display: 'flex', justifyContent: 'space-between', fontSize: 10, color: 'var(--od-muted)' }}>
|
|
219
|
+
<span>Q3 Infrastructure Review</span>
|
|
220
|
+
<span style={{ fontVariantNumeric: 'tabular-nums' }}>{page} / {total}</span>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- **Never hardcode** `3 / 12`. Both hooks are 1-based and return `0` outside a page.
|
|
227
|
+
- Define the header/footer once as a local component and drop it into every page that needs it. Cover pages normally omit it.
|
|
228
|
+
- Footers are absolutely positioned inside the margin band; they do not consume the text block's vertical budget — but keep at least 24px of clearance between the last line of body copy and the footer.
|
|
229
|
+
|
|
230
|
+
## Starter template
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
import { type DesignSystem, type DocMeta, type DocPage, useDocPageCount, useDocPageNumber } from 'mosage';
|
|
234
|
+
|
|
235
|
+
export const design: DesignSystem = {
|
|
236
|
+
palette: {
|
|
237
|
+
bg: '#ffffff',
|
|
238
|
+
text: '#16181d',
|
|
239
|
+
muted: '#6b7280',
|
|
240
|
+
accent: '#1d4ed8',
|
|
241
|
+
rule: '#e5e7eb',
|
|
242
|
+
},
|
|
243
|
+
fonts: {
|
|
244
|
+
heading: '-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif',
|
|
245
|
+
body: '-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif',
|
|
246
|
+
mono: 'ui-monospace, "SF Mono", Menlo, monospace',
|
|
247
|
+
},
|
|
248
|
+
typeScale: { title: 44, h1: 28, h2: 20, h3: 16, body: 14, caption: 10 },
|
|
249
|
+
margin: 76,
|
|
250
|
+
leading: 1.55,
|
|
251
|
+
radius: 6,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const page = {
|
|
255
|
+
width: '100%',
|
|
256
|
+
height: '100%',
|
|
257
|
+
boxSizing: 'border-box' as const,
|
|
258
|
+
padding: 'var(--od-margin)',
|
|
259
|
+
background: 'var(--od-bg)',
|
|
260
|
+
color: 'var(--od-text)',
|
|
261
|
+
fontFamily: 'var(--od-font-body)',
|
|
262
|
+
fontSize: 'var(--od-size-body)',
|
|
263
|
+
lineHeight: 'var(--od-leading)',
|
|
264
|
+
position: 'relative' as const,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const h1 = {
|
|
268
|
+
fontFamily: 'var(--od-font-heading)',
|
|
269
|
+
fontSize: 'var(--od-size-h1)',
|
|
270
|
+
lineHeight: 1.2,
|
|
271
|
+
fontWeight: 650,
|
|
272
|
+
margin: '0 0 20px',
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const Footer = () => {
|
|
276
|
+
const n = useDocPageNumber();
|
|
277
|
+
const total = useDocPageCount();
|
|
278
|
+
return (
|
|
279
|
+
<div
|
|
280
|
+
style={{
|
|
281
|
+
position: 'absolute',
|
|
282
|
+
left: 'var(--od-margin)',
|
|
283
|
+
right: 'var(--od-margin)',
|
|
284
|
+
bottom: 40,
|
|
285
|
+
display: 'flex',
|
|
286
|
+
justifyContent: 'space-between',
|
|
287
|
+
fontSize: 'var(--od-size-caption)',
|
|
288
|
+
color: 'var(--od-muted)',
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
<span>Q3 Infrastructure Review</span>
|
|
292
|
+
<span style={{ fontVariantNumeric: 'tabular-nums' }}>
|
|
293
|
+
{n} / {total}
|
|
294
|
+
</span>
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const Cover: DocPage = () => (
|
|
300
|
+
<div style={{ ...page, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
|
|
301
|
+
<p style={{ fontSize: 12, letterSpacing: '0.16em', textTransform: 'uppercase', color: 'var(--od-accent)', margin: 0 }}>
|
|
302
|
+
Platform Engineering
|
|
303
|
+
</p>
|
|
304
|
+
<h1 style={{ ...h1, fontSize: 'var(--od-size-title)', margin: '16px 0 12px' }}>
|
|
305
|
+
Q3 Infrastructure Review
|
|
306
|
+
</h1>
|
|
307
|
+
<p style={{ color: 'var(--od-muted)', margin: 0 }}>August 2026</p>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const Section: DocPage = () => (
|
|
312
|
+
<div style={page}>
|
|
313
|
+
<h1 style={h1}>1. Summary</h1>
|
|
314
|
+
<p style={{ margin: '0 0 14px' }}>
|
|
315
|
+
One paragraph per idea. Keep the page inside its vertical budget.
|
|
316
|
+
</p>
|
|
317
|
+
<Footer />
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
export const meta: DocMeta = {
|
|
322
|
+
title: 'Q3 Infrastructure Review',
|
|
323
|
+
subtitle: 'Platform team',
|
|
324
|
+
pageSize: 'A4',
|
|
325
|
+
createdAt: '2026-08-15T13:44:40.268Z',
|
|
326
|
+
};
|
|
327
|
+
export default [Cover, Section] satisfies DocPage[];
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Editing an existing document
|
|
331
|
+
|
|
332
|
+
A finished report commonly runs 800–2000 lines. When you only need one page, **don't read the whole file** — locate it first:
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
grep -n ": DocPage = " docs/<id>/index.tsx
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Then `Read` with `offset` + `limit` (~150 lines covers a page plus its helpers). Read the whole file only for cross-page work (renumbering sections, palette audit, reordering).
|
|
339
|
+
|
|
340
|
+
**Renumbering is a real cost.** If sections are numbered ("3.2 Findings"), inserting a page means editing every downstream heading. Prefer appending, or use unnumbered headings when the document is still churning.
|
|
341
|
+
|
|
342
|
+
## Prose discipline
|
|
343
|
+
|
|
344
|
+
A document is not a slide deck. Long-form copy is the point — but it still has rules:
|
|
345
|
+
|
|
346
|
+
- One idea per paragraph, 2–5 sentences. A paragraph over ~8 lines should be split.
|
|
347
|
+
- Lead each section with its conclusion, then the evidence. Readers skim reports.
|
|
348
|
+
- Tables beat bullet lists for anything with two or more dimensions.
|
|
349
|
+
- Cite numbers with their source and date inline (`Q3 billing export, 2026-08-01`) — a report that can't be traced gets ignored.
|
|
350
|
+
- Don't invent data. If a number must come from the user, leave `<ImagePlaceholder>` for a chart or an explicit `TODO:` marker in the copy and tell them at hand-off.
|
|
351
|
+
|
|
352
|
+
## Runtime behavior you get for free
|
|
353
|
+
|
|
354
|
+
- Home page lists every folder under `docs/` with a live thumbnail of page 1.
|
|
355
|
+
- Document view: vertical scroll of real-size pages, a left rail that switches between page thumbnails, the outline, and the document's assets, zoom (actual size / fit width / fit page), page counter, and fullscreen reading (`F`).
|
|
356
|
+
- Export PDF (print pipeline, correct `@page` size) and export HTML (self-contained, printable).
|
|
357
|
+
- Hot reload: edit `index.tsx` and the pages update live.
|
|
358
|
+
- **Assets panel** (`/assets` in the dev UI): upload, rename, and delete files in the global `assets/` folder or any document's `assets/` folder, with an "unused" badge and a copy-ready import line. Files you reference in source are what it scans, so an import you write by hand shows up there immediately.
|
|
359
|
+
- **Inspect mode** (the "Inspect" button, dev only): click any element on a page to edit its text in place — the change is written straight back into `docs/<id>/index.tsx` — or leave a note for the agent, which is stored as a `@doc-comment` marker and processed by the `apply-comments` skill.
|
|
360
|
+
- **Download menu** — PDF (true page size), self-contained HTML, and DOCX for review in Word.
|
|
361
|
+
- **Word export** — DOCX reflows the text instead of copying the sheets, so write structure, not position: real `h1`–`h3` become Word headings, `<Footnote>` a Word footnote, `<TableOfContents />` a contents field, a flow `footer` a running footer with live page numbers, and tables, lists, and links their Word equivalents. Inside a `<Figure>`, anything that is not an image or a table — a chart drawn with divs — is exported as a picture of itself.
|
|
362
|
+
- **Headless render** — `mosage export <id> --format pdf|html|docx|png` produces the same output from a script, and `mosage check <id>` reports layout faults. Both drive the real viewer in a headless browser, so what they produce is what the Download menu produces.
|
|
363
|
+
- **Design panel** (the "Design" button in the document view, dev only): live-tweaks the `design` const — palette, fonts, type scale, margin, leading, radius — previewing on the real pages and writing the values back into `docs/<id>/index.tsx` on save.
|
|
364
|
+
|
|
365
|
+
### Writing for the inspector
|
|
366
|
+
|
|
367
|
+
Inspect mode edits the **literal text runs** of an element, and nothing else:
|
|
368
|
+
|
|
369
|
+
- A single run (`<p style={p}>copy</p>`) is editable in place.
|
|
370
|
+
- Mixed content (`<p>對外端點為 <code>/mcp</code>,另外自訂 …</p>`) is split into one field per run, with the markup shown as read-only chips. Each run is written back on its own, so the markup between them survives untouched.
|
|
371
|
+
- Text passed into a local helper (`<Td>FastMCP</Td>`) **is** editable: the inspector walks the React tree to the call site, which is where the words actually live. The helper's own definition stays untouched.
|
|
372
|
+
- Text produced by code (`{entry.text}`, a `map`, a hook) is refused — there is nothing literal to rewrite. Edit whatever feeds it.
|
|
373
|
+
- Every resolution is checked against the text on screen, and every write against the text the panel read. A mismatch is refused rather than guessed at, because the alternative is silently rewriting a different element.
|
|
374
|
+
- Comments anchor **inside** the clicked element, so self-closing elements (`<img />`, `<ImagePlaceholder />`) cannot host one; the user has to click the wrapper.
|
|
375
|
+
|
|
376
|
+
Practical consequence for authors: **put text in its own element**. `<Td>{value}</Td>` renders the same as `<Td>text</Td>` but only the latter can be edited from the page.
|
|
377
|
+
|
|
378
|
+
### Writing for the Design panel
|
|
379
|
+
|
|
380
|
+
The panel rewrites the `design` object in place through an AST edit, so keep its initializer in the shape it can read:
|
|
381
|
+
|
|
382
|
+
- `export const design: DesignSystem = { … }` (or `const design = …`) at module level, **object literal only** — string/number literals, nested objects. No spreads, no `satisfies` on the inner object, no computed keys, no values pulled from other constants.
|
|
383
|
+
- Values you want tweakable live *in* the const. A hex you inline into a style is invisible to the panel.
|
|
384
|
+
- If the document has no `design` const, saving from the panel creates one (and adds the `DesignSystem` type import). Anything the panel can't parse is reported in the panel instead of being silently overwritten.
|
|
385
|
+
|
|
386
|
+
## Check the layout before you call it done
|
|
387
|
+
|
|
388
|
+
You cannot see the pages you just wrote. The framework can:
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
mosage check <id> # every document if you omit the id; exits non-zero on errors
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
It renders each sheet at true page size and reports what a reader would call a
|
|
395
|
+
mistake — content clipped by the page edge, a blank sheet, a heading stranded at
|
|
396
|
+
the foot of a page, type too small to print, an image that never loaded — each
|
|
397
|
+
with the `line:column` in your source.
|
|
398
|
+
|
|
399
|
+
**Run it after writing a document and after any edit that changes how much text
|
|
400
|
+
is on a page.** The checklist below is what you reason about; `check` is what
|
|
401
|
+
confirms it.
|
|
402
|
+
|
|
403
|
+
## Self-review before finishing
|
|
404
|
+
|
|
405
|
+
- [ ] `mosage check <id>` reports no errors.
|
|
406
|
+
- [ ] `docs/<id>/index.tsx` `export default`s a non-empty `DocEntry[]`, with body content in a `flow()` section rather than hand-split pages.
|
|
407
|
+
- [ ] Every page's root fills `100% × 100%` and sets `boxSizing: 'border-box'` with the margin as padding.
|
|
408
|
+
- [ ] **For every fixed page, sum (font_size × line_height × lines) + gaps + 2×margin ≤ page height.** If close, split — or move the content into the flow section. No `overflow: auto` escape hatches.
|
|
409
|
+
- [ ] No block inside a `flow()` section is taller than one page (a long table has to be split by hand — the framework never splits a block).
|
|
410
|
+
- [ ] Body type ≥ 13px; nothing on the page under 9px.
|
|
411
|
+
- [ ] Section titles are real `h1`/`h2`/`h3` elements, so the outline and TOC pick them up.
|
|
412
|
+
- [ ] Contents page uses `<TableOfContents />`, not a hand-written list.
|
|
413
|
+
- [ ] Page numbers come from `useDocPageNumber()` / `useDocPageCount()`.
|
|
414
|
+
- [ ] Document declares a top-level `export const design: DesignSystem` and pages consume `var(--od-*)`.
|
|
415
|
+
- [ ] Tables have a header row, aligned numerals (`fontVariantNumeric: 'tabular-nums'`), and fit the text block width.
|
|
416
|
+
- [ ] Numbers that refer to other things — figures, tables, notes, pages — come from `<Ref>` / `<Figure>` / `<Footnote>`, never typed in.
|
|
417
|
+
- [ ] Any data that exists as a file is imported, not retyped into JSX.
|
|
418
|
+
- [ ] All imported assets exist on disk (`docs/<id>/assets/`, or root `assets/` via `@assets/...`).
|
|
419
|
+
- [ ] Every `<ImagePlaceholder>` marks a real image the user must supply — not decorative filler.
|
|
420
|
+
- [ ] Nothing outside `docs/<id>/` was edited.
|
|
421
|
+
|
|
422
|
+
## Anti-patterns
|
|
423
|
+
|
|
424
|
+
- ❌ Overflowing the page. Cropped content is invisible — split instead.
|
|
425
|
+
- ❌ `overflow: auto` / `scroll` / `hidden` to "fit" more. The sheet doesn't scroll; you've hidden the bug.
|
|
426
|
+
- ❌ Shrinking body type below 13px or margins below 60px to cram content in.
|
|
427
|
+
- ❌ Hand-written contents lists or hardcoded page numbers — they go stale the moment a page is added.
|
|
428
|
+
- ❌ "See Figure 3 on page 12" written by hand, or a figure caption numbered `Figure 3` in the copy. Use `<Ref>` and `<Figure>`.
|
|
429
|
+
- ❌ Retyping a CSV the user already has into a JSX table.
|
|
430
|
+
- ❌ Fake headings (`<div>` styled like a title) — they vanish from the outline.
|
|
431
|
+
- ❌ A slide-deck voice: 6-word bullets and 100px type. This is a document.
|
|
432
|
+
- ❌ Tables built from `%` widths that overflow the text block, or with more than ~7 columns on portrait A4.
|
|
433
|
+
- ❌ Installing packages, editing `package.json` / `mosage.config.ts` / other documents.
|
|
434
|
+
- ❌ Inventing data, sources, or citations.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Assets and image placeholders
|
|
2
|
+
|
|
3
|
+
## Where files live
|
|
4
|
+
|
|
5
|
+
| Scope | Path | Import |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| One document | `docs/<id>/assets/chart.png` | `import chart from './assets/chart.png'` |
|
|
8
|
+
| Shared across documents | `assets/logo.svg` (project root) | `import logo from '@assets/logo.svg'` |
|
|
9
|
+
|
|
10
|
+
Both resolve to a URL string at build time. For a pure-text document, don't create an `assets/` folder at all.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import logo from '@assets/logo.svg';
|
|
14
|
+
import diagram from './assets/architecture.png';
|
|
15
|
+
|
|
16
|
+
<img src={logo} alt="Acme" style={{ height: 28 }} />
|
|
17
|
+
<img src={diagram} alt="Service topology" style={{ width: 642, display: 'block' }} />
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Rules:
|
|
21
|
+
|
|
22
|
+
- Always size images in absolute px, never `%` — a percentage resolves against the page, and the printed result stops matching the screen.
|
|
23
|
+
- Set `display: 'block'` on figures so the line-box descender doesn't add a stray few pixels to your vertical budget.
|
|
24
|
+
- Prefer SVG for logos, diagrams, and anything with type in it. A raster diagram at page width needs ≥1280px of source to survive print.
|
|
25
|
+
- Photos: keep them under ~1600px wide. The PDF export embeds them at full resolution, and a 6000px photo makes a 40MB PDF.
|
|
26
|
+
- Every image needs an `alt`. Figures need a caption below at `--od-size-caption`, muted, on the same page as the image.
|
|
27
|
+
|
|
28
|
+
## `<ImagePlaceholder>`
|
|
29
|
+
|
|
30
|
+
When a page needs a real image **the user has to provide** — a chart from their data, a product screenshot, a signed diagram — leave a typed placeholder instead of inventing a stand-in:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { ImagePlaceholder } from 'mosage';
|
|
34
|
+
|
|
35
|
+
<ImagePlaceholder hint="Revenue by segment, Q1–Q3 2026 — export from the finance dashboard" height={200} />
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- The `hint` is what the user reads when replacing it. Name the exact artifact and where it comes from — "chart here" is useless.
|
|
39
|
+
- Size it to the space it will occupy so the page's vertical budget stays honest after the real image lands.
|
|
40
|
+
- **Do not** use placeholders for decoration or stock-photo filler. If type, a table, or an inline SVG can carry the page, do that instead.
|
|
41
|
+
- List every placeholder for the user at hand-off — those are the blockers between the draft and a sendable document.
|
|
42
|
+
|
|
43
|
+
## Export behavior
|
|
44
|
+
|
|
45
|
+
- **PDF**: images are embedded. The exporter waits for every `<img>` to finish loading before printing, so a slow asset delays the export rather than producing a blank frame.
|
|
46
|
+
- **HTML**: same-origin assets are collected and rewritten to a local `assets/` folder; the download becomes a `.zip` when the document references any. A document with no assets downloads as a single `.html`.
|
|
47
|
+
- Remote images (a URL on another origin) are left as-is — they render only while that host is reachable, and they may be missing from the PDF if the fetch is slow. Import files into the project instead.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Design system
|
|
2
|
+
|
|
3
|
+
Every document declares typed design tokens at the top of `index.tsx` and consumes them through CSS variables. The framework injects the variables at the page root, so both the on-screen page and the exported PDF/HTML read the same values.
|
|
4
|
+
|
|
5
|
+
## The shape
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import type { DesignSystem } from 'mosage';
|
|
9
|
+
|
|
10
|
+
export const design: DesignSystem = {
|
|
11
|
+
palette: {
|
|
12
|
+
bg: '#ffffff', // sheet background — keep near-white for print
|
|
13
|
+
text: '#16181d', // body copy
|
|
14
|
+
muted: '#6b7280', // captions, footers, secondary cells
|
|
15
|
+
accent: '#1d4ed8', // section numbers, rules, links, chart series
|
|
16
|
+
rule: '#e5e7eb', // hairlines, table borders, dot leaders
|
|
17
|
+
},
|
|
18
|
+
fonts: {
|
|
19
|
+
heading: '…',
|
|
20
|
+
body: '…',
|
|
21
|
+
mono: '…',
|
|
22
|
+
},
|
|
23
|
+
typeScale: { title: 44, h1: 28, h2: 20, h3: 16, body: 14, caption: 10 },
|
|
24
|
+
margin: 76, // px inset from the sheet edge
|
|
25
|
+
leading: 1.55, // body line-height multiplier
|
|
26
|
+
radius: 6,
|
|
27
|
+
};
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Tokens available in CSS
|
|
31
|
+
|
|
32
|
+
| Token | From |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `var(--od-bg)` `--od-text` `--od-muted` `--od-accent` `--od-rule` | `palette` |
|
|
35
|
+
| `var(--od-font-heading)` `--od-font-body` `--od-font-mono` | `fonts` |
|
|
36
|
+
| `var(--od-size-title)` `--od-size-h1` `--od-size-h2` `--od-size-h3` `--od-size-body` `--od-size-caption` | `typeScale` |
|
|
37
|
+
| `var(--od-margin)` `--od-leading` `--od-radius` | top-level |
|
|
38
|
+
|
|
39
|
+
Read the tokens through `var(--od-*)` in inline styles. Read `design.typeScale.body` directly only when you need a **number** for arithmetic (e.g. computing a fixed row height).
|
|
40
|
+
|
|
41
|
+
## Print-specific palette rules
|
|
42
|
+
|
|
43
|
+
- **Background must be white or near-white.** A dark report burns toner, and most print pipelines drop background colors by default — a dark-mode document silently prints as white-on-white. If the user insists on a dark cover, make the cover page dark and keep body pages light.
|
|
44
|
+
- **Body text at least `#333`-dark.** Grey body copy (#666) that looks refined on screen prints washed out.
|
|
45
|
+
- **One accent.** Section numbers, rules, chart series, links. Two accents means a color system nobody asked for.
|
|
46
|
+
- `rule` should be barely visible — `#e5e7eb` on white. Heavy table borders make a report look like a spreadsheet.
|
|
47
|
+
- Anything conveying meaning by color (status cells, chart series) needs a second cue — a label, a shape, a weight — because the report will be printed in mono somewhere.
|
|
48
|
+
|
|
49
|
+
## Colors outside the shape
|
|
50
|
+
|
|
51
|
+
Extra colors (status green/amber/red, chart series beyond the accent) stay as plain module constants:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const positive = '#15803d';
|
|
55
|
+
const warning = '#b45309';
|
|
56
|
+
const negative = '#b91c1c';
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Keep them next to the `design` const so a future edit finds the whole palette in one place.
|
|
60
|
+
|
|
61
|
+
## Fonts
|
|
62
|
+
|
|
63
|
+
The default is a system stack — prefer it. A webfont is warranted for a brand face, or for CJK/Thai/Arabic where system coverage differs across machines.
|
|
64
|
+
|
|
65
|
+
Load it **at module level**, once per document:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
const FONT_CSS = 'https://fonts.googleapis.com/css2?family=Noto+Serif+TC:wght@400;700&display=swap';
|
|
69
|
+
|
|
70
|
+
if (typeof document !== 'undefined' && !document.getElementById('od-font-noto-serif-tc')) {
|
|
71
|
+
const link = document.createElement('link');
|
|
72
|
+
link.id = 'od-font-noto-serif-tc';
|
|
73
|
+
link.rel = 'stylesheet';
|
|
74
|
+
link.href = FONT_CSS;
|
|
75
|
+
document.head.appendChild(link);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- Give the `<link>` a stable `id` and guard on it — every page component mounts separately, and during export all pages mount at once.
|
|
80
|
+
- The PDF export waits on `document.fonts.ready`, so a webfont is safe to use — but a CJK family is megabytes; subset it or accept a slow first export.
|
|
81
|
+
- Never rely on a font installed only on your machine. If it isn't loaded by the document, it won't be there in the export.
|