@particle-academy/last-word-ui 0.0.0-stage → 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,68 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ > **Pre-1.0:** breaking changes land in MINOR releases. Until 1.0 the minor
9
+ > number is not a compatibility promise. Read the entry, not the version.
10
+
11
+ ## [Unreleased]
12
+
13
+ ## [0.1.0] - 2026-11-01
14
+
15
+ First release. **What you must do:** install it beside `react` 19 and
16
+ `@particle-academy/react-fancy` 5, then render `<DocViewer doc={doc} />` with the
17
+ JSON a last-word reader returns. last-word itself is NOT a dependency: read the
18
+ file on the server, or with `@particle-academy/last-word`, and pass the result in.
19
+
20
+ ### Added
21
+
22
+ - **`<DocViewer doc />`**, a read-only, paged view of a last-word `Doc`:
23
+ - every page at its paper size and orientation (letter, legal, a4);
24
+ - pages broken by CONTENT HEIGHT, as well as at explicit `pageBreak`s.
25
+ Paragraphs, headings, lists, quotes and code split between lines, with
26
+ Word's widow/orphan rule on paragraphs. Tables split between rows, repeat
27
+ their header rows on each continuation page, and never break inside a row
28
+ span. Headings keep with the next block;
29
+ - header and footer at their distances (`page.margins.header` / `.footer`,
30
+ 36pt when absent), as a one-line left/centre/right band or as blocks (a
31
+ letterhead logo, address lines). The body moves down when a header is
32
+ taller than the top margin, as Word does;
33
+ - `{page}` / `{pages}` as live numbers on every page, in bands only;
34
+ - footnotes (the last-word 0.15.0 model: a run `{"text": "", "footnote":
35
+ {"blocks": [...]}}`), numbered 1, 2, 3 in document order and set at the
36
+ foot of the page holding the mark, under a separator rule. Their height
37
+ comes off that page's body;
38
+ - tables (widths, header rows and their default fill, cell shading, borders,
39
+ padding, vertical alignment, column and row spans), paragraph alignment,
40
+ shading, borders, indents and spacing, run styling, headings, lists,
41
+ quotes, code, images and rules, all as the reader returns them;
42
+ - scales whole pages to fit the panel (`fit`, `scale`, `maxScale`) and never
43
+ reflows;
44
+ - stable handles: `data-page`, `data-block` (`b0`, `b1`, ...),
45
+ `data-footnote`, `data-lw-band`; `onLayout` reports `{pages, pageWidth,
46
+ pageHeight, scale}` as JSON.
47
+ - **`paginate(items, options)`**, the pure page packer behind the viewer:
48
+ heights in, page assignments out, no DOM.
49
+ - **`pageGeometry(doc)`, `pageMargins(doc)`, `bodyBox()`**: the page model,
50
+ with the ONE place margin defaults are applied (72pt a side, 36pt to each
51
+ band), mirroring PHP last-word's `Schema::pageMargins()`.
52
+ - **`docFromReader(output)`**: accepts a reader's Doc, or its JSON string.
53
+ - `numberFootnotes(doc)` and `substituteFields(text, fields)`.
54
+
55
+ ### Security
56
+
57
+ - A Doc is untrusted input. Links render only for `http(s)`, `mailto`, `tel`
58
+ and `#anchor` URLs, so a `javascript:` link never reaches an `href`. Images
59
+ render only from embedded `data:image/...` URLs, so a document cannot make
60
+ the reader's browser fetch a remote URL (a tracking pixel). Colours reach CSS
61
+ only as `#RRGGBB`, and font names are reduced to safe characters.
62
+
63
+ ### Not yet
64
+
65
+ - Charts draw as a sized, labelled placeholder (pagination stays right).
66
+ - A table ROW taller than a page is not split; it is placed alone and flagged
67
+ `data-lw-overflow`. A footnote taller than the space left does not continue
68
+ onto the next page. Page `background` and `border` are not drawn yet.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Particle Academy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,136 @@
1
- # Temporary Holding Version
1
+ # @particle-academy/last-word-ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A read-only, **paged** React viewer for a [last-word](https://github.com/Particle-Academy/last-word-js)
4
+ `Doc`, drawn as the pages it prints on. Each page sits at its paper size, breaks
5
+ fall where the content runs out, the header and footer sit at Word's distances
6
+ with live page numbers, and footnotes sit at the foot of the page that holds
7
+ their mark. In a narrow preview panel it scales the whole page down and never
8
+ reflows the text.
9
+
10
+ > **Pre-1.0:** breaking changes land in MINOR releases. Read the
11
+ > [CHANGELOG](./CHANGELOG.md) entry before taking one.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @particle-academy/last-word-ui @particle-academy/react-fancy react react-dom
17
+ ```
18
+
19
+ ## Use
20
+
21
+ ```tsx
22
+ import { DocViewer } from "@particle-academy/last-word-ui";
23
+
24
+ <DocViewer doc={doc} />;
25
+ ```
26
+
27
+ `doc` is last-word JSON: what `new DocxReader().read(bytes)` returns in
28
+ `@particle-academy/last-word`, or the same JSON from the PHP or Python
29
+ packages, sent over the wire. This package does not read `.docx` bytes itself,
30
+ so last-word is not a dependency. Read on the server, or with last-word-js:
31
+
32
+ ```tsx
33
+ import { DocxReader } from "@particle-academy/last-word";
34
+ import { DocViewer, docFromReader } from "@particle-academy/last-word-ui";
35
+
36
+ const doc = docFromReader(new DocxReader().read(bytes)); // or a JSON string
37
+ <DocViewer doc={doc} onLayout={({ pages }) => setPageCount(pages)} />;
38
+ ```
39
+
40
+ Render the object you were given. Footnote numbering is keyed by run identity,
41
+ so a deep clone between numbering and render would lose the marks.
42
+
43
+ ### Props
44
+
45
+ | prop | type | default | |
46
+ |---|---|---|---|
47
+ | `doc` | `Doc` | (required) | last-word JSON |
48
+ | `fit` | `"width" \| "none"` | `"width"` | scale pages down to the viewer's width |
49
+ | `scale` | `number` | | a fixed scale; overrides `fit` |
50
+ | `maxScale` | `number` | `1` | the most `fit` will enlarge a page |
51
+ | `gap` | `number` | `16` | px between pages |
52
+ | `onLayout` | `(l) => void` | | `{ pages, pageWidth, pageHeight, scale }`, JSON |
53
+ | `injectStyles` | `boolean` | `true` | see *Styles* |
54
+ | `id`, `className`, `style` | | | on the root |
55
+
56
+ ### Stable handles
57
+
58
+ The root carries `data-lw-viewer`. Each page carries `data-page` (1-based), each
59
+ block or fragment of a block carries `data-block` (`b0`, `b1`, ... in document
60
+ order), each footnote carries `data-footnote`, and the bands carry
61
+ `data-lw-band="header" | "footer"`. Table fragments carry `data-lw-rows`, plus
62
+ `data-lw-repeated-header` on continuation pages.
63
+
64
+ ### Styles
65
+
66
+ The viewer renders its few CSS rules (react-fancy's `<table>` and `<hr>` take no
67
+ style prop) as a React 19 hoisted `<style>`. Under a CSP without
68
+ `'unsafe-inline'` styles, pass `injectStyles={false}` and import the same
69
+ rules instead:
70
+
71
+ ```ts
72
+ import "@particle-academy/last-word-ui/styles.css";
73
+ ```
74
+
75
+ ## What it renders
76
+
77
+ - **Pages:** letter, legal and a4, portrait or landscape, with margins as the
78
+ reader returns them. A missing side is the default of 72pt (the reader drops
79
+ 1-inch sides).
80
+ - **Breaks:** by content height, and at every `pageBreak`. Paragraphs,
81
+ headings, lists, quotes and code split between lines, and paragraphs follow
82
+ Word's widow and orphan rule. Tables split between rows, repeat their header
83
+ rows on each continuation page, and never split inside a row span. Headings
84
+ keep with the next block.
85
+ - **Header and footer:** at `page.margins.header` / `.footer` from the page
86
+ edge (36pt when absent). They are either a left / centre / right band or
87
+ blocks (a letterhead logo, address lines). A tall header pushes the body down,
88
+ as in Word.
89
+ - **Page numbers:** `{page}` and `{pages}` become live numbers on each page,
90
+ in bands only. In the body they stay literal text, as in the file.
91
+ - **Footnotes:** numbered 1, 2, 3 in document order and set at the foot of the
92
+ mark's page, under a short rule. Their height comes off that page's body.
93
+ - **Content:** tables (widths, header rows, cell shading, borders, padding,
94
+ vertical alignment, spans), paragraph alignment, shading, borders, indents
95
+ and spacing, run styling (bold, italic, underline, strike, colour, highlight,
96
+ size, font, small caps, letter spacing, inline code, links), headings,
97
+ lists, quotes, code, images and rules.
98
+
99
+ ### Not yet (v0)
100
+
101
+ - Charts draw as a sized, labelled placeholder, so pagination stays right.
102
+ - A table **row** taller than a page is not split. It is placed alone and
103
+ flagged with `data-lw-overflow`.
104
+ - A block with no line breaks that is taller than a page (a huge image) is cut
105
+ at the page height, with `data-lw-forced`.
106
+ - A footnote taller than the room left does not continue onto the next page.
107
+ - Page `background` and the page `border` frame are not drawn.
108
+
109
+ ## Security
110
+
111
+ A Doc is untrusted input, and this viewer renders it in your origin:
112
+
113
+ - Links render only for `http(s)`, `mailto`, `tel` and `#anchor` URLs.
114
+ - Images render only from embedded `data:image/...` URLs. A remote `src` is
115
+ never requested.
116
+ - Colours reach CSS only as `#RRGGBB`.
117
+
118
+ ## Lower-level API
119
+
120
+ `paginate(items, { pageHeight, footnoteSeparator })` is the pure page packer.
121
+ It takes heights in and returns page assignments, with no DOM. Use it with
122
+ `pageGeometry(doc)`, `pageMargins(doc)` (the one place margin defaults are
123
+ applied), `bodyBox()`, `numberFootnotes(doc)` and `substituteFields()` to lay
124
+ pages out elsewhere.
125
+
126
+ ## Develop
127
+
128
+ ```bash
129
+ npm test # vitest + jsdom
130
+ npm run lint # tsc + eslint
131
+ npm run demo # vite demo: a multi-page memo, wide and narrow
132
+ ```
133
+
134
+ ## License
135
+
136
+ MIT