@particle-academy/last-word-ui 0.0.0-stage → 0.1.1
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 +76 -0
- package/LICENSE +21 -0
- package/README.md +135 -2
- package/dist/index.cjs +1187 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +456 -0
- package/dist/index.d.ts +456 -0
- package/dist/index.js +1174 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +28 -0
- package/dist/styles.css.map +1 -0
- package/package.json +86 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
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.1] - 2026-11-01
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Test environment: `jsdom` moved from 26 to 30 (devDependency only). jsdom 26
|
|
17
|
+
pulled in `whatwg-encoding`, which is deprecated upstream and printed a
|
|
18
|
+
deprecation warning on every `npm ci`. Nothing in the published package
|
|
19
|
+
changes. **Consumer action: none.**
|
|
20
|
+
|
|
21
|
+
## [0.1.0] - 2026-11-01
|
|
22
|
+
|
|
23
|
+
First release. **What you must do:** install it beside `react` 19 and
|
|
24
|
+
`@particle-academy/react-fancy` 5, then render `<DocViewer doc={doc} />` with the
|
|
25
|
+
JSON a last-word reader returns. last-word itself is NOT a dependency: read the
|
|
26
|
+
file on the server, or with `@particle-academy/last-word`, and pass the result in.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`<DocViewer doc />`**, a read-only, paged view of a last-word `Doc`:
|
|
31
|
+
- every page at its paper size and orientation (letter, legal, a4);
|
|
32
|
+
- pages broken by CONTENT HEIGHT, as well as at explicit `pageBreak`s.
|
|
33
|
+
Paragraphs, headings, lists, quotes and code split between lines, with
|
|
34
|
+
Word's widow/orphan rule on paragraphs. Tables split between rows, repeat
|
|
35
|
+
their header rows on each continuation page, and never break inside a row
|
|
36
|
+
span. Headings keep with the next block;
|
|
37
|
+
- header and footer at their distances (`page.margins.header` / `.footer`,
|
|
38
|
+
36pt when absent), as a one-line left/centre/right band or as blocks (a
|
|
39
|
+
letterhead logo, address lines). The body moves down when a header is
|
|
40
|
+
taller than the top margin, as Word does;
|
|
41
|
+
- `{page}` / `{pages}` as live numbers on every page, in bands only;
|
|
42
|
+
- footnotes (the last-word 0.15.0 model: a run `{"text": "", "footnote":
|
|
43
|
+
{"blocks": [...]}}`), numbered 1, 2, 3 in document order and set at the
|
|
44
|
+
foot of the page holding the mark, under a separator rule. Their height
|
|
45
|
+
comes off that page's body;
|
|
46
|
+
- tables (widths, header rows and their default fill, cell shading, borders,
|
|
47
|
+
padding, vertical alignment, column and row spans), paragraph alignment,
|
|
48
|
+
shading, borders, indents and spacing, run styling, headings, lists,
|
|
49
|
+
quotes, code, images and rules, all as the reader returns them;
|
|
50
|
+
- scales whole pages to fit the panel (`fit`, `scale`, `maxScale`) and never
|
|
51
|
+
reflows;
|
|
52
|
+
- stable handles: `data-page`, `data-block` (`b0`, `b1`, ...),
|
|
53
|
+
`data-footnote`, `data-lw-band`; `onLayout` reports `{pages, pageWidth,
|
|
54
|
+
pageHeight, scale}` as JSON.
|
|
55
|
+
- **`paginate(items, options)`**, the pure page packer behind the viewer:
|
|
56
|
+
heights in, page assignments out, no DOM.
|
|
57
|
+
- **`pageGeometry(doc)`, `pageMargins(doc)`, `bodyBox()`**: the page model,
|
|
58
|
+
with the ONE place margin defaults are applied (72pt a side, 36pt to each
|
|
59
|
+
band), mirroring PHP last-word's `Schema::pageMargins()`.
|
|
60
|
+
- **`docFromReader(output)`**: accepts a reader's Doc, or its JSON string.
|
|
61
|
+
- `numberFootnotes(doc)` and `substituteFields(text, fields)`.
|
|
62
|
+
|
|
63
|
+
### Security
|
|
64
|
+
|
|
65
|
+
- A Doc is untrusted input. Links render only for `http(s)`, `mailto`, `tel`
|
|
66
|
+
and `#anchor` URLs, so a `javascript:` link never reaches an `href`. Images
|
|
67
|
+
render only from embedded `data:image/...` URLs, so a document cannot make
|
|
68
|
+
the reader's browser fetch a remote URL (a tracking pixel). Colours reach CSS
|
|
69
|
+
only as `#RRGGBB`, and font names are reduced to safe characters.
|
|
70
|
+
|
|
71
|
+
### Not yet
|
|
72
|
+
|
|
73
|
+
- Charts draw as a sized, labelled placeholder (pagination stays right).
|
|
74
|
+
- A table ROW taller than a page is not split; it is placed alone and flagged
|
|
75
|
+
`data-lw-overflow`. A footnote taller than the space left does not continue
|
|
76
|
+
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
|
-
#
|
|
1
|
+
# @particle-academy/last-word-ui
|
|
2
2
|
|
|
3
|
-
|
|
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
|