@libraz/mejiro-react 0.1.0 → 0.3.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/README.md +90 -0
- package/dist/index.d.ts +87 -2
- package/dist/index.js +160 -2
- package/package.json +4 -3
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @libraz/mejiro-react
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@libraz/mejiro-react)
|
|
4
|
+
[](https://github.com/libraz/mejiro/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
React components and hooks for [mejiro](https://www.npmjs.com/package/@libraz/mejiro) vertical text rendering.
|
|
7
|
+
|
|
8
|
+
> **Experimental** — API may change in future releases.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @libraz/mejiro @libraz/mejiro-react
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Peer dependency: `react >= 18`
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { DEFAULT_HEADING_STYLES, MejiroBook } from '@libraz/mejiro/book';
|
|
22
|
+
import { parseEpub } from '@libraz/mejiro/epub';
|
|
23
|
+
import { MejiroPageView } from '@libraz/mejiro-react';
|
|
24
|
+
import '@libraz/mejiro/render/mejiro.css';
|
|
25
|
+
|
|
26
|
+
const book = new MejiroBook({
|
|
27
|
+
fontFamily: '"Noto Serif JP"',
|
|
28
|
+
fontSize: 16,
|
|
29
|
+
lineSpacing: 1.8,
|
|
30
|
+
headingStyles: DEFAULT_HEADING_STYLES,
|
|
31
|
+
});
|
|
32
|
+
book.computePageSize(containerEl);
|
|
33
|
+
|
|
34
|
+
const epub = await parseEpub(buffer);
|
|
35
|
+
const layout = await book.layoutChapter(epub.chapters[0]);
|
|
36
|
+
const spread = layout.getSpread(0);
|
|
37
|
+
|
|
38
|
+
<MejiroPageView result={spread.right} fontFamily='"Noto Serif JP"' lineSpacing={1.8} />
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Components
|
|
42
|
+
|
|
43
|
+
### `MejiroPageView` (Recommended)
|
|
44
|
+
|
|
45
|
+
Renders a `PageResult` from `ChapterLayout`. Automatically switches between CSS `vertical-rl` and slot-based rendering when images are present.
|
|
46
|
+
|
|
47
|
+
| Prop | Type | Description |
|
|
48
|
+
|------|------|-------------|
|
|
49
|
+
| `result` | `PageResult` | Required. Page result from `ChapterLayout`. |
|
|
50
|
+
| `fontFamily` | `string` | CSS font family (used in slot-based mode). |
|
|
51
|
+
| `lineSpacing` | `number` | Line spacing multiplier (used in slot-based mode). |
|
|
52
|
+
| `slotMode` | `boolean` | Force slot-based rendering (set when layout has images). |
|
|
53
|
+
| `className` | `string` | Additional CSS class name. |
|
|
54
|
+
| `style` | `CSSProperties` | Additional inline styles. |
|
|
55
|
+
|
|
56
|
+
### `MejiroPage` (Low-Level)
|
|
57
|
+
|
|
58
|
+
Renders a `RenderPage` using CSS `writing-mode: vertical-rl`. For use with the lower-level `buildRenderPage()` API.
|
|
59
|
+
|
|
60
|
+
## Hooks
|
|
61
|
+
|
|
62
|
+
### `useImageOverlay`
|
|
63
|
+
|
|
64
|
+
Manages a draggable/resizable image overlay with automatic text reflow.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { useImageOverlay } from '@libraz/mejiro-react';
|
|
68
|
+
|
|
69
|
+
const { imageRect, hasImage, toggleImage, onOverlayPointerDown, onResizePointerDown } =
|
|
70
|
+
useImageOverlay(layout, spreadIdx, (spread) => setSpread(spread));
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
| Parameter | Type | Description |
|
|
74
|
+
|-----------|------|-------------|
|
|
75
|
+
| `layout` | `ChapterLayout \| null` | Current chapter layout. |
|
|
76
|
+
| `spreadIdx` | `number` | Current spread index. |
|
|
77
|
+
| `onUpdate` | `(spread: SpreadResult) => void` | Called after every reflow. |
|
|
78
|
+
| `options?` | `UseImageOverlayOptions` | Default dimensions, position, and margin. |
|
|
79
|
+
|
|
80
|
+
Returns `{ imageRect, hasImage, toggleImage, onOverlayPointerDown, onResizePointerDown }`.
|
|
81
|
+
|
|
82
|
+
## CSS
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import '@libraz/mejiro/render/mejiro.css';
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
[Apache License 2.0](https://github.com/libraz/mejiro/blob/main/LICENSE)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RenderPage } from '@libraz/mejiro/render';
|
|
2
|
-
import { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import { CSSProperties, ReactNode, PointerEvent } from 'react';
|
|
3
|
+
import { PageResult, ChapterLayout, SpreadResult } from '@libraz/mejiro/book';
|
|
3
4
|
|
|
4
5
|
/** Props for the MejiroPage component. */
|
|
5
6
|
interface MejiroPageProps {
|
|
@@ -18,4 +19,88 @@ interface MejiroPageProps {
|
|
|
18
19
|
*/
|
|
19
20
|
declare function MejiroPage({ page, className, style }: MejiroPageProps): ReactNode;
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
/** Props for the MejiroPageView component. */
|
|
23
|
+
interface MejiroPageViewProps {
|
|
24
|
+
/** Page result from {@link ChapterLayout.getSpread} or {@link ChapterLayout.getPage}. */
|
|
25
|
+
result: PageResult;
|
|
26
|
+
/** CSS font family for slot-based rendering (used when images are present). */
|
|
27
|
+
fontFamily?: string;
|
|
28
|
+
/** Line spacing multiplier for slot-based rendering (used when images are present). */
|
|
29
|
+
lineSpacing?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Force slot-based rendering even when `result.hasImages` is false.
|
|
32
|
+
* Set to `true` when the layout has images on any spread, so that
|
|
33
|
+
* all pages use consistent slot-based rendering.
|
|
34
|
+
*/
|
|
35
|
+
slotMode?: boolean;
|
|
36
|
+
/** Additional CSS class name for the root element. */
|
|
37
|
+
className?: string;
|
|
38
|
+
/** Additional inline styles for the root element. */
|
|
39
|
+
style?: CSSProperties;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Renders a page from a {@link PageResult}.
|
|
43
|
+
*
|
|
44
|
+
* Automatically selects the rendering strategy:
|
|
45
|
+
* - **Normal mode** (no images): Uses CSS `writing-mode: vertical-rl` via `<MejiroPage>`.
|
|
46
|
+
* - **Slot mode** (images present): Uses absolute-positioned columns with per-line sizing.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* const spread = layout.getSpread(0);
|
|
51
|
+
* <MejiroPageView result={spread.right} fontFamily="serif" lineSpacing={1.8} />
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
declare function MejiroPageView({ result, fontFamily, lineSpacing, slotMode, className, style, }: MejiroPageViewProps): ReactNode;
|
|
55
|
+
|
|
56
|
+
/** Rectangle describing an image overlay position and size. */
|
|
57
|
+
interface ImageRect {
|
|
58
|
+
x: number;
|
|
59
|
+
y: number;
|
|
60
|
+
w: number;
|
|
61
|
+
h: number;
|
|
62
|
+
}
|
|
63
|
+
/** Options for {@link useImageOverlay}. */
|
|
64
|
+
interface UseImageOverlayOptions {
|
|
65
|
+
/** Default image width in pixels. @defaultValue 120 */
|
|
66
|
+
defaultWidth?: number;
|
|
67
|
+
/** Default image height in pixels. @defaultValue 160 */
|
|
68
|
+
defaultHeight?: number;
|
|
69
|
+
/** Default x position. @defaultValue 80 */
|
|
70
|
+
defaultX?: number;
|
|
71
|
+
/** Default y position. @defaultValue 100 */
|
|
72
|
+
defaultY?: number;
|
|
73
|
+
/** Margin around the image in pixels. Defaults to the book's font size. */
|
|
74
|
+
margin?: number;
|
|
75
|
+
}
|
|
76
|
+
/** Return value of {@link useImageOverlay}. */
|
|
77
|
+
interface UseImageOverlayReturn {
|
|
78
|
+
/** Current image rectangle, or `null` if no overlay is active. */
|
|
79
|
+
imageRect: ImageRect | null;
|
|
80
|
+
/** Whether an image overlay is currently active. */
|
|
81
|
+
hasImage: boolean;
|
|
82
|
+
/** Toggle the image overlay on/off. */
|
|
83
|
+
toggleImage: () => void;
|
|
84
|
+
/** Pointer-down handler for the overlay body (initiates drag). */
|
|
85
|
+
onOverlayPointerDown: (e: PointerEvent) => void;
|
|
86
|
+
/** Pointer-down handler for the resize handle. */
|
|
87
|
+
onResizePointerDown: (e: PointerEvent) => void;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* React hook that manages a draggable/resizable image overlay
|
|
91
|
+
* with automatic text reflow via {@link ChapterLayout.syncImages}.
|
|
92
|
+
*
|
|
93
|
+
* @param layout - Current chapter layout (or `null` if not yet loaded).
|
|
94
|
+
* @param spreadIdx - Current spread index.
|
|
95
|
+
* @param onUpdate - Called with the updated {@link SpreadResult} after every reflow.
|
|
96
|
+
* @param options - Default dimensions and position for the overlay.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```tsx
|
|
100
|
+
* const { imageRect, hasImage, toggleImage, onOverlayPointerDown, onResizePointerDown } =
|
|
101
|
+
* useImageOverlay(layout, spreadIdx, setSpread);
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
declare function useImageOverlay(layout: ChapterLayout | null, spreadIdx: number, onUpdate: (spread: SpreadResult) => void, options?: UseImageOverlayOptions): UseImageOverlayReturn;
|
|
105
|
+
|
|
106
|
+
export { type ImageRect, MejiroPage, type MejiroPageProps, MejiroPageView, type MejiroPageViewProps, type UseImageOverlayOptions, type UseImageOverlayReturn, useImageOverlay };
|
package/dist/index.js
CHANGED
|
@@ -22,13 +22,171 @@ function renderLine(line, lineIndex) {
|
|
|
22
22
|
function MejiroPage({ page, className, style }) {
|
|
23
23
|
const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
|
|
24
24
|
return /* @__PURE__ */ jsx("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
|
|
25
|
-
|
|
25
|
+
let paraClass = "mejiro-paragraph";
|
|
26
|
+
if (paragraph.headingLevel != null) {
|
|
27
|
+
paraClass += ` mejiro-paragraph--h${paragraph.headingLevel}`;
|
|
28
|
+
} else if (paragraph.isHeading) {
|
|
29
|
+
paraClass += " mejiro-paragraph--heading";
|
|
30
|
+
}
|
|
26
31
|
return (
|
|
27
32
|
// biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
|
|
28
33
|
/* @__PURE__ */ jsx("div", { className: paraClass, children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
|
|
29
34
|
);
|
|
30
35
|
}) });
|
|
31
36
|
}
|
|
37
|
+
|
|
38
|
+
// src/MejiroPageView.tsx
|
|
39
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
40
|
+
function renderSlotSegment(segment, key) {
|
|
41
|
+
if (segment.type === "text") return segment.text;
|
|
42
|
+
return /* @__PURE__ */ jsxs2("ruby", { children: [
|
|
43
|
+
segment.base,
|
|
44
|
+
/* @__PURE__ */ jsx2("rt", { children: segment.rubyText })
|
|
45
|
+
] }, key);
|
|
46
|
+
}
|
|
47
|
+
function MejiroPageView({
|
|
48
|
+
result,
|
|
49
|
+
fontFamily,
|
|
50
|
+
lineSpacing,
|
|
51
|
+
slotMode,
|
|
52
|
+
className,
|
|
53
|
+
style
|
|
54
|
+
}) {
|
|
55
|
+
if (result.hasImages || slotMode) {
|
|
56
|
+
const rootClass = className ? `mejiro-page-slots ${className}` : "mejiro-page-slots";
|
|
57
|
+
return /* @__PURE__ */ jsx2("div", { className: rootClass, style: { position: "relative", ...style }, children: result.lines.map((line, i) => {
|
|
58
|
+
const slot = result.slots[i];
|
|
59
|
+
if (!slot || slot.height <= 0) return null;
|
|
60
|
+
const colStyle = {
|
|
61
|
+
position: "absolute",
|
|
62
|
+
writingMode: "vertical-rl",
|
|
63
|
+
whiteSpace: "nowrap",
|
|
64
|
+
overflow: "hidden",
|
|
65
|
+
right: slot.xPos,
|
|
66
|
+
top: slot.yStart,
|
|
67
|
+
height: slot.height,
|
|
68
|
+
fontSize: line.fontSize,
|
|
69
|
+
fontFamily,
|
|
70
|
+
lineHeight: lineSpacing,
|
|
71
|
+
fontWeight: line.headingLevel != null ? 700 : void 0
|
|
72
|
+
};
|
|
73
|
+
return (
|
|
74
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: lines have no stable ID
|
|
75
|
+
/* @__PURE__ */ jsx2("div", { style: colStyle, children: line.segments.map((seg, si) => renderSlotSegment(seg, si)) }, i)
|
|
76
|
+
);
|
|
77
|
+
}) });
|
|
78
|
+
}
|
|
79
|
+
return /* @__PURE__ */ jsx2(MejiroPage, { page: result.page, className, style });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/useImageOverlay.ts
|
|
83
|
+
import { useCallback, useRef, useState } from "react";
|
|
84
|
+
function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
85
|
+
const defW = options?.defaultWidth ?? 120;
|
|
86
|
+
const defH = options?.defaultHeight ?? 160;
|
|
87
|
+
const defX = options?.defaultX ?? 80;
|
|
88
|
+
const defY = options?.defaultY ?? 100;
|
|
89
|
+
const margin = options?.margin;
|
|
90
|
+
const [imageRect, setImageRect] = useState(null);
|
|
91
|
+
const rectRef = useRef(imageRect);
|
|
92
|
+
rectRef.current = imageRect;
|
|
93
|
+
const layoutRef = useRef(layout);
|
|
94
|
+
layoutRef.current = layout;
|
|
95
|
+
const spreadRef = useRef(spreadIdx);
|
|
96
|
+
spreadRef.current = spreadIdx;
|
|
97
|
+
const onUpdateRef = useRef(onUpdate);
|
|
98
|
+
onUpdateRef.current = onUpdate;
|
|
99
|
+
const syncToLayout = useCallback(
|
|
100
|
+
(rect) => {
|
|
101
|
+
const lo = layoutRef.current;
|
|
102
|
+
if (!lo) return;
|
|
103
|
+
const images = rect ? [{ x: rect.x, y: rect.y, w: rect.w, h: rect.h, margin }] : void 0;
|
|
104
|
+
const spread = lo.syncImages(spreadRef.current, images);
|
|
105
|
+
onUpdateRef.current(spread);
|
|
106
|
+
},
|
|
107
|
+
[margin]
|
|
108
|
+
);
|
|
109
|
+
const toggleImage = useCallback(() => {
|
|
110
|
+
if (rectRef.current) {
|
|
111
|
+
setImageRect(null);
|
|
112
|
+
syncToLayout(null);
|
|
113
|
+
} else {
|
|
114
|
+
const r = { x: defX, y: defY, w: defW, h: defH };
|
|
115
|
+
setImageRect(r);
|
|
116
|
+
syncToLayout(r);
|
|
117
|
+
}
|
|
118
|
+
}, [defX, defY, defW, defH, syncToLayout]);
|
|
119
|
+
const onOverlayPointerDown = useCallback(
|
|
120
|
+
(e) => {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
const startX = e.clientX;
|
|
123
|
+
const startY = e.clientY;
|
|
124
|
+
const start = { ...rectRef.current };
|
|
125
|
+
const target = e.currentTarget;
|
|
126
|
+
target.setPointerCapture(e.nativeEvent.pointerId);
|
|
127
|
+
target.classList.add("dragging");
|
|
128
|
+
let rafId = 0;
|
|
129
|
+
const onMove = (me) => {
|
|
130
|
+
const dx = me.clientX - startX;
|
|
131
|
+
const dy = me.clientY - startY;
|
|
132
|
+
const r = { ...start, x: start.x + dx, y: start.y + dy };
|
|
133
|
+
cancelAnimationFrame(rafId);
|
|
134
|
+
rafId = requestAnimationFrame(() => {
|
|
135
|
+
setImageRect(r);
|
|
136
|
+
syncToLayout(r);
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
const onUp = () => {
|
|
140
|
+
target.classList.remove("dragging");
|
|
141
|
+
document.removeEventListener("pointermove", onMove);
|
|
142
|
+
document.removeEventListener("pointerup", onUp);
|
|
143
|
+
};
|
|
144
|
+
document.addEventListener("pointermove", onMove);
|
|
145
|
+
document.addEventListener("pointerup", onUp);
|
|
146
|
+
},
|
|
147
|
+
[syncToLayout]
|
|
148
|
+
);
|
|
149
|
+
const onResizePointerDown = useCallback(
|
|
150
|
+
(e) => {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
e.stopPropagation();
|
|
153
|
+
const startX = e.clientX;
|
|
154
|
+
const startY = e.clientY;
|
|
155
|
+
const start = { ...rectRef.current };
|
|
156
|
+
const target = e.currentTarget;
|
|
157
|
+
target.setPointerCapture(e.nativeEvent.pointerId);
|
|
158
|
+
target.parentElement?.classList.add("dragging");
|
|
159
|
+
let rafId = 0;
|
|
160
|
+
const onMove = (me) => {
|
|
161
|
+
const dx = me.clientX - startX;
|
|
162
|
+
const dy = me.clientY - startY;
|
|
163
|
+
const r = { ...start, w: Math.max(40, start.w + dx), h: Math.max(40, start.h + dy) };
|
|
164
|
+
cancelAnimationFrame(rafId);
|
|
165
|
+
rafId = requestAnimationFrame(() => {
|
|
166
|
+
setImageRect(r);
|
|
167
|
+
syncToLayout(r);
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
const onUp = () => {
|
|
171
|
+
target.parentElement?.classList.remove("dragging");
|
|
172
|
+
document.removeEventListener("pointermove", onMove);
|
|
173
|
+
document.removeEventListener("pointerup", onUp);
|
|
174
|
+
};
|
|
175
|
+
document.addEventListener("pointermove", onMove);
|
|
176
|
+
document.addEventListener("pointerup", onUp);
|
|
177
|
+
},
|
|
178
|
+
[syncToLayout]
|
|
179
|
+
);
|
|
180
|
+
return {
|
|
181
|
+
imageRect,
|
|
182
|
+
hasImage: imageRect !== null,
|
|
183
|
+
toggleImage,
|
|
184
|
+
onOverlayPointerDown,
|
|
185
|
+
onResizePointerDown
|
|
186
|
+
};
|
|
187
|
+
}
|
|
32
188
|
export {
|
|
33
|
-
MejiroPage
|
|
189
|
+
MejiroPage,
|
|
190
|
+
MejiroPageView,
|
|
191
|
+
useImageOverlay
|
|
34
192
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@libraz/mejiro-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "React components for mejiro vertical text rendering",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
|
-
"LICENSE"
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"README.md"
|
|
22
23
|
],
|
|
23
24
|
"sideEffects": false,
|
|
24
25
|
"engines": {
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
"build": "tsup"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
|
-
"@libraz/mejiro": "^0.
|
|
32
|
+
"@libraz/mejiro": "^0.3.0"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"react": ">=18"
|