@santjc/react-pretext 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/README.md +347 -0
- package/dist/chunk-4TMIB6R6.js +51 -0
- package/dist/chunk-6P7OEVAC.js +51 -0
- package/dist/editorial.d.ts +163 -0
- package/dist/editorial.js +684 -0
- package/dist/index.d.ts +157 -0
- package/dist/index.js +393 -0
- package/dist/pretext.d.ts +1 -0
- package/dist/pretext.js +2 -0
- package/dist/usePreparedText-DmUr2kss.d.ts +20 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# @santjc/react-pretext
|
|
2
|
+
|
|
3
|
+
Simple React wrapper over [`@chenglou/pretext`](https://www.npmjs.com/package/@chenglou/pretext) for deterministic text measurement before paint, without DOM reads.
|
|
4
|
+
|
|
5
|
+
`@santjc/react-pretext` is intentionally a small React layer over `@chenglou/pretext`. It lets you predict text height and line count from text, typography, and width before the browser renders the final layout. The core use case is measurement-driven UI: accordions, cards, virtualized lists, previews, and any responsive layout where text height affects placement.
|
|
6
|
+
|
|
7
|
+
The package keeps the original `pretext` model intact and adds React-facing hooks, types, and semantic rendering helpers where React actually helps.
|
|
8
|
+
|
|
9
|
+
The intended adoption path is:
|
|
10
|
+
|
|
11
|
+
- define typography once
|
|
12
|
+
- measure text with `useMeasuredText()`
|
|
13
|
+
- render semantic DOM with `PText`
|
|
14
|
+
- use predicted heights in normal UI like accordions, cards, lists, and previews
|
|
15
|
+
- opt into editorial flow only when you need custom line routing
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @santjc/react-pretext react react-dom
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Peer dependencies: React 18 or 19.
|
|
24
|
+
|
|
25
|
+
## Why use it
|
|
26
|
+
|
|
27
|
+
- Predict text height before paint
|
|
28
|
+
- Avoid hidden measurement nodes and `scrollHeight` reads
|
|
29
|
+
- Truncate text to a known number of lines without DOM reads
|
|
30
|
+
- Keep measurement inputs and render styles aligned
|
|
31
|
+
- Re-layout from width changes without leaving the arithmetic path
|
|
32
|
+
- Drop down to lower-level hooks only when you need more control
|
|
33
|
+
|
|
34
|
+
## Start Here
|
|
35
|
+
|
|
36
|
+
### Measure text with one hook
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { createPretextTypography, useMeasuredText } from '@santjc/react-pretext'
|
|
40
|
+
|
|
41
|
+
function Example({ text }: { text: string }) {
|
|
42
|
+
const typography = createPretextTypography({
|
|
43
|
+
family: 'Inter, sans-serif',
|
|
44
|
+
size: 18,
|
|
45
|
+
weight: 400,
|
|
46
|
+
lineHeight: 28,
|
|
47
|
+
width: 320,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const { height, lineCount } = useMeasuredText({ text, typography })
|
|
51
|
+
|
|
52
|
+
return <div>{height}px / {lineCount} lines</div>
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use this for the common case where a component needs a known text height, line count, or both.
|
|
57
|
+
|
|
58
|
+
### Use shared typography with `PText`
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { PText, createPretextTypography } from '@santjc/react-pretext'
|
|
62
|
+
|
|
63
|
+
function Example() {
|
|
64
|
+
const body = createPretextTypography({
|
|
65
|
+
family: 'Inter, sans-serif',
|
|
66
|
+
size: 18,
|
|
67
|
+
weight: 400,
|
|
68
|
+
lineHeight: 28,
|
|
69
|
+
width: 320,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<PText as="p" typography={body}>
|
|
74
|
+
Semantic text with one source of truth for measurement and render output.
|
|
75
|
+
</PText>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`PText` is a semantic rendering helper for the shared typography object. It is useful when you want real DOM output to stay aligned with the same measurement inputs, but the main measurement story still starts with hooks.
|
|
81
|
+
|
|
82
|
+
### Let `PText` observe responsive width
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { PText, createPretextTypography } from '@santjc/react-pretext'
|
|
86
|
+
|
|
87
|
+
function Example() {
|
|
88
|
+
const body = createPretextTypography({
|
|
89
|
+
family: 'Inter, sans-serif',
|
|
90
|
+
size: 18,
|
|
91
|
+
weight: 400,
|
|
92
|
+
lineHeight: 28,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div style={{ width: 'min(100%, 36rem)' }}>
|
|
97
|
+
<PText as="p" typography={body}>
|
|
98
|
+
This paragraph does not receive an explicit width. PText observes the element width and remeasures as the container changes.
|
|
99
|
+
</PText>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Replace hidden measurement or `scrollHeight`
|
|
106
|
+
|
|
107
|
+
Before:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
111
|
+
|
|
112
|
+
useLayoutEffect(() => {
|
|
113
|
+
setHeight(ref.current?.scrollHeight ?? 0)
|
|
114
|
+
}, [text, width])
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
After:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { createPretextTypography, useMeasuredText, PText } from '@santjc/react-pretext'
|
|
121
|
+
|
|
122
|
+
function AccordionBody({ isOpen, text }: { isOpen: boolean; text: string }) {
|
|
123
|
+
const typography = createPretextTypography({
|
|
124
|
+
family: 'Inter, sans-serif',
|
|
125
|
+
size: 18,
|
|
126
|
+
weight: 400,
|
|
127
|
+
lineHeight: 28,
|
|
128
|
+
width: 360,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const { height } = useMeasuredText({ text, typography })
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div style={{ height: isOpen ? `${height}px` : '0px', overflow: 'hidden' }}>
|
|
135
|
+
<PText as="p" typography={typography}>
|
|
136
|
+
{text}
|
|
137
|
+
</PText>
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Predict measured card or list heights
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
import { createPretextTypography, useMeasuredText } from '@santjc/react-pretext'
|
|
147
|
+
|
|
148
|
+
function ResultCard({ text, width }: { text: string; width: number }) {
|
|
149
|
+
const typography = createPretextTypography({
|
|
150
|
+
family: 'Inter, sans-serif',
|
|
151
|
+
size: 16,
|
|
152
|
+
weight: 400,
|
|
153
|
+
lineHeight: 26,
|
|
154
|
+
width: width - 32,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const { height, lineCount } = useMeasuredText({ text, typography })
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div>
|
|
161
|
+
<div>{lineCount} lines</div>
|
|
162
|
+
<div>predicted body height: {height}px</div>
|
|
163
|
+
</div>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
This pattern works well for feeds, search results, CMS previews, issue lists, and any responsive grid where text height affects placement.
|
|
169
|
+
|
|
170
|
+
### Truncate text for previews and teasers
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
import { PText, createPretextTypography, useTruncatedText } from '@santjc/react-pretext'
|
|
174
|
+
|
|
175
|
+
function ResultPreview({ text }: { text: string }) {
|
|
176
|
+
const typography = createPretextTypography({
|
|
177
|
+
family: 'Inter, sans-serif',
|
|
178
|
+
size: 16,
|
|
179
|
+
lineHeight: 24,
|
|
180
|
+
width: 280,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const preview = useTruncatedText({
|
|
184
|
+
text,
|
|
185
|
+
typography,
|
|
186
|
+
maxLines: 3,
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<>
|
|
191
|
+
<PText as="p" typography={typography}>{preview.text}</PText>
|
|
192
|
+
{preview.didTruncate ? <button>Read more</button> : null}
|
|
193
|
+
</>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`useTruncatedText()` is meant for cards, snippets, collapsed previews, and compact list rows where the visible text itself needs to be deterministic before render.
|
|
199
|
+
|
|
200
|
+
## Typography input
|
|
201
|
+
|
|
202
|
+
`createPretextTypography()` accepts either a CSS font shorthand string or a structured typography object.
|
|
203
|
+
|
|
204
|
+
Structured input:
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
const typography = createPretextTypography({
|
|
208
|
+
family: 'Inter, sans-serif',
|
|
209
|
+
size: 18,
|
|
210
|
+
weight: 400,
|
|
211
|
+
lineHeight: 28,
|
|
212
|
+
width: 320,
|
|
213
|
+
})
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Font shorthand input:
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
const typography = createPretextTypography({
|
|
220
|
+
font: '400 18px Inter, sans-serif',
|
|
221
|
+
lineHeight: 28,
|
|
222
|
+
width: 320,
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The structured form is the recommended default because it is easier to read, easier to derive from design tokens, and less fragile in application code. Internally, both forms resolve to the same `font` string and matching render styles.
|
|
227
|
+
|
|
228
|
+
## Stable root API
|
|
229
|
+
|
|
230
|
+
The root package is the intentional React-facing surface.
|
|
231
|
+
|
|
232
|
+
- `createPretextTypography`
|
|
233
|
+
- `useElementWidth`
|
|
234
|
+
- `useMeasuredText`
|
|
235
|
+
- `usePreparedText`
|
|
236
|
+
- `usePreparedSegments`
|
|
237
|
+
- `usePretextLayout`
|
|
238
|
+
- `usePretextLines`
|
|
239
|
+
- `useTruncatedText`
|
|
240
|
+
- `PText`
|
|
241
|
+
|
|
242
|
+
Drop down to `usePreparedText()` and `usePretextLayout()` when you want to control the prepare and layout phases separately. Use `usePreparedSegments()` with `usePretextLines()` when you need actual line output.
|
|
243
|
+
|
|
244
|
+
## Low-level pretext API
|
|
245
|
+
|
|
246
|
+
Raw `@chenglou/pretext` exports live on a dedicated `pretext` subpath:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import {
|
|
250
|
+
prepare,
|
|
251
|
+
prepareWithSegments,
|
|
252
|
+
layout,
|
|
253
|
+
layoutWithLines,
|
|
254
|
+
layoutNextLine,
|
|
255
|
+
walkLineRanges,
|
|
256
|
+
} from '@santjc/react-pretext/pretext'
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Use this subpath when you want the original low-level pretext model without the React-first root entrypoint.
|
|
260
|
+
|
|
261
|
+
## Editorial API
|
|
262
|
+
|
|
263
|
+
Editorial helpers live on the advanced `editorial` subpath:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import {
|
|
267
|
+
FlowLines,
|
|
268
|
+
useTextFlow,
|
|
269
|
+
flowText,
|
|
270
|
+
carveLineSlots,
|
|
271
|
+
createLineSlotResolver,
|
|
272
|
+
getCircleBlockedLineRangeForRow,
|
|
273
|
+
pickWidestLineSlot,
|
|
274
|
+
EditorialColumns,
|
|
275
|
+
EditorialSurface,
|
|
276
|
+
type EditorialTrack,
|
|
277
|
+
type EditorialFigure,
|
|
278
|
+
} from '@santjc/react-pretext/editorial'
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
These APIs are public and tested, but they are not part of the default adoption path. Reach for them when you need custom line rendering, obstacle-aware flow, or multi-column continuation.
|
|
282
|
+
|
|
283
|
+
## SSR and runtime guidance
|
|
284
|
+
|
|
285
|
+
Measurement depends on canvas-backed text metrics, so the measurement hooks are a client-side feature.
|
|
286
|
+
|
|
287
|
+
- In Next.js and other SSR frameworks, call the hooks from client components.
|
|
288
|
+
- If you need a server-rendered fallback, render with a placeholder height and replace it after hydration.
|
|
289
|
+
- Keep measurement logic at the edge of the UI that actually needs it instead of pushing it into shared server code.
|
|
290
|
+
|
|
291
|
+
Example with a client component boundary:
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
'use client'
|
|
295
|
+
|
|
296
|
+
import { createPretextTypography, useMeasuredText } from '@santjc/react-pretext'
|
|
297
|
+
|
|
298
|
+
export function MeasuredPreview({ text }: { text: string }) {
|
|
299
|
+
const typography = createPretextTypography({
|
|
300
|
+
family: 'Inter, sans-serif',
|
|
301
|
+
size: 16,
|
|
302
|
+
lineHeight: 24,
|
|
303
|
+
width: 320,
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
const { height } = useMeasuredText({ text, typography })
|
|
307
|
+
|
|
308
|
+
return <div style={{ minHeight: `${height}px` }}>{text}</div>
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Webfont guidance
|
|
313
|
+
|
|
314
|
+
Measurement is only as accurate as the font you actually render.
|
|
315
|
+
|
|
316
|
+
- Keep the measurement typography aligned with the real DOM font.
|
|
317
|
+
- Expect differences until a webfont finishes loading.
|
|
318
|
+
- If first-render accuracy matters, wait for the font before measuring or remeasure once the font is ready.
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
document.fonts.ready.then(() => {
|
|
325
|
+
setFontsReady(true)
|
|
326
|
+
})
|
|
327
|
+
}, [])
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Caveats
|
|
331
|
+
|
|
332
|
+
- `createPretextTypography()` is the recommended way to keep measurement inputs and render styles aligned.
|
|
333
|
+
- `PText` currently supports `string` children only.
|
|
334
|
+
- `prepareOptions` currently map directly to pretext preparation options, such as `whiteSpace`.
|
|
335
|
+
- `useTextFlow` expects a reference-stable `getLineSlotAtY` callback. Memoize custom resolvers in React.
|
|
336
|
+
- Editorial `lineRenderMode="justify"` uses pretext-derived `word-spacing` and will skip justification for unsupported whitespace patterns instead of delegating wrapping back to the browser.
|
|
337
|
+
- `EditorialFigure` treats explicit `x` and `y` as overrides over `placement`, and clamps the result within available bounds.
|
|
338
|
+
|
|
339
|
+
## Source layout
|
|
340
|
+
|
|
341
|
+
The repository keeps package boundaries visible in the source tree:
|
|
342
|
+
|
|
343
|
+
- `src/core/*` backs the root package exports
|
|
344
|
+
- `src/editorial/*` backs `@santjc/react-pretext/editorial`
|
|
345
|
+
- `src/test/*` holds cross-entrypoint package tests
|
|
346
|
+
|
|
347
|
+
Playground helpers stay in `apps/playground/src/lib/*` unless they are intentionally promoted into the package with matching tests and docs.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/hooks/useElementWidth.ts
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
function useElementWidth() {
|
|
4
|
+
const [node, setNode] = useState(null);
|
|
5
|
+
const [width, setWidth] = useState(0);
|
|
6
|
+
const ref = useCallback((nextNode) => {
|
|
7
|
+
setWidth(nextNode?.getBoundingClientRect().width ?? 0);
|
|
8
|
+
setNode(nextNode);
|
|
9
|
+
}, []);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (node === null) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const observer = new ResizeObserver((entries) => {
|
|
15
|
+
const entry = entries[0];
|
|
16
|
+
if (entry === void 0) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
setWidth((currentWidth) => currentWidth === entry.contentRect.width ? currentWidth : entry.contentRect.width);
|
|
20
|
+
});
|
|
21
|
+
observer.observe(node);
|
|
22
|
+
return () => {
|
|
23
|
+
observer.disconnect();
|
|
24
|
+
};
|
|
25
|
+
}, [node]);
|
|
26
|
+
return { ref, width, node };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/hooks/usePreparedSegments.ts
|
|
30
|
+
import { prepareWithSegments } from "@chenglou/pretext";
|
|
31
|
+
import { useMemo } from "react";
|
|
32
|
+
function usePreparedSegments({ text, font, options, enabled = true }) {
|
|
33
|
+
const whiteSpace = options?.whiteSpace;
|
|
34
|
+
return useMemo(() => {
|
|
35
|
+
if (!enabled || text.length === 0 || font.length === 0) {
|
|
36
|
+
return {
|
|
37
|
+
prepared: null,
|
|
38
|
+
isReady: false
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
prepared: prepareWithSegments(text, font, whiteSpace === void 0 ? void 0 : { whiteSpace }),
|
|
43
|
+
isReady: true
|
|
44
|
+
};
|
|
45
|
+
}, [enabled, font, text, whiteSpace]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
useElementWidth,
|
|
50
|
+
usePreparedSegments
|
|
51
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/core/hooks/useElementWidth.ts
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
function useElementWidth() {
|
|
4
|
+
const [node, setNode] = useState(null);
|
|
5
|
+
const [width, setWidth] = useState(0);
|
|
6
|
+
const ref = useCallback((nextNode) => {
|
|
7
|
+
setWidth(nextNode?.getBoundingClientRect().width ?? 0);
|
|
8
|
+
setNode(nextNode);
|
|
9
|
+
}, []);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (node === null) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const observer = new ResizeObserver((entries) => {
|
|
15
|
+
const entry = entries[0];
|
|
16
|
+
if (entry === void 0) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
setWidth((currentWidth) => currentWidth === entry.contentRect.width ? currentWidth : entry.contentRect.width);
|
|
20
|
+
});
|
|
21
|
+
observer.observe(node);
|
|
22
|
+
return () => {
|
|
23
|
+
observer.disconnect();
|
|
24
|
+
};
|
|
25
|
+
}, [node]);
|
|
26
|
+
return { ref, width, node };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/core/hooks/usePreparedSegments.ts
|
|
30
|
+
import { prepareWithSegments } from "@chenglou/pretext";
|
|
31
|
+
import { useMemo } from "react";
|
|
32
|
+
function usePreparedSegments({ text, font, options, enabled = true }) {
|
|
33
|
+
const whiteSpace = options?.whiteSpace;
|
|
34
|
+
return useMemo(() => {
|
|
35
|
+
if (!enabled || text.length === 0 || font.length === 0) {
|
|
36
|
+
return {
|
|
37
|
+
prepared: null,
|
|
38
|
+
isReady: false
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
prepared: prepareWithSegments(text, font, whiteSpace === void 0 ? void 0 : { whiteSpace }),
|
|
43
|
+
isReady: true
|
|
44
|
+
};
|
|
45
|
+
}, [enabled, font, text, whiteSpace]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
useElementWidth,
|
|
50
|
+
usePreparedSegments
|
|
51
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
3
|
+
import { P as PrepareOptions } from './usePreparedText-DmUr2kss.js';
|
|
4
|
+
import { LayoutCursor, PreparedTextWithSegments } from '@chenglou/pretext';
|
|
5
|
+
|
|
6
|
+
type LineSlot = {
|
|
7
|
+
left: number;
|
|
8
|
+
right: number;
|
|
9
|
+
};
|
|
10
|
+
type BlockedLineRange = {
|
|
11
|
+
left: number;
|
|
12
|
+
right: number;
|
|
13
|
+
};
|
|
14
|
+
type GetBlockedLineRanges = (lineTop: number, lineBottom: number) => BlockedLineRange[];
|
|
15
|
+
type LineSlotResolver = (y: number) => LineSlot | null;
|
|
16
|
+
type CreateLineSlotResolverInput = {
|
|
17
|
+
baseLineSlot: LineSlot;
|
|
18
|
+
lineHeight: number;
|
|
19
|
+
minWidth?: number;
|
|
20
|
+
getBlockedLineRanges?: GetBlockedLineRanges;
|
|
21
|
+
};
|
|
22
|
+
type CircleLineRangeInput = {
|
|
23
|
+
cx: number;
|
|
24
|
+
cy: number;
|
|
25
|
+
radius: number;
|
|
26
|
+
lineTop: number;
|
|
27
|
+
lineBottom: number;
|
|
28
|
+
horizontalPadding?: number;
|
|
29
|
+
verticalPadding?: number;
|
|
30
|
+
};
|
|
31
|
+
declare function carveLineSlots(baseLineSlot: LineSlot, blockedLineRanges: BlockedLineRange[], minWidth?: number): LineSlot[];
|
|
32
|
+
declare function getCircleBlockedLineRangeForRow({ cx, cy, radius, lineTop, lineBottom, horizontalPadding, verticalPadding, }: CircleLineRangeInput): BlockedLineRange | null;
|
|
33
|
+
declare function pickWidestLineSlot(lineSlots: LineSlot[]): LineSlot | null;
|
|
34
|
+
declare function createLineSlotResolver({ baseLineSlot, lineHeight, minWidth, getBlockedLineRanges, }: CreateLineSlotResolverInput): LineSlotResolver;
|
|
35
|
+
|
|
36
|
+
type EditorialPlacement = 'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
37
|
+
type EditorialFigure = {
|
|
38
|
+
shape: 'circle' | 'rect';
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
placement?: EditorialPlacement;
|
|
42
|
+
x?: number;
|
|
43
|
+
y?: number;
|
|
44
|
+
linePadding?: number;
|
|
45
|
+
className?: string;
|
|
46
|
+
style?: CSSProperties;
|
|
47
|
+
content?: ReactNode;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type EditorialTrack = {
|
|
51
|
+
width?: number;
|
|
52
|
+
fr?: number;
|
|
53
|
+
minHeight?: number;
|
|
54
|
+
paddingInline?: number;
|
|
55
|
+
paddingBlock?: number;
|
|
56
|
+
className?: string;
|
|
57
|
+
style?: CSSProperties;
|
|
58
|
+
figures?: EditorialFigure[];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type EditorialColumnsProps = {
|
|
62
|
+
text: string;
|
|
63
|
+
font: string;
|
|
64
|
+
lineHeight: number;
|
|
65
|
+
gap?: number;
|
|
66
|
+
lineRenderMode?: 'natural' | 'justify';
|
|
67
|
+
prepareOptions?: PrepareOptions;
|
|
68
|
+
className?: string;
|
|
69
|
+
style?: CSSProperties;
|
|
70
|
+
tracks: EditorialTrack[];
|
|
71
|
+
};
|
|
72
|
+
declare function EditorialColumns({ text, font, lineHeight, gap, lineRenderMode, prepareOptions, className, style, tracks: trackDefs, }: EditorialColumnsProps): react_jsx_runtime.JSX.Element;
|
|
73
|
+
|
|
74
|
+
type EditorialSurfaceProps = {
|
|
75
|
+
text: string;
|
|
76
|
+
font: string;
|
|
77
|
+
lineHeight: number;
|
|
78
|
+
startY?: number;
|
|
79
|
+
maxY?: number;
|
|
80
|
+
minHeight?: number;
|
|
81
|
+
lineRenderMode?: 'natural' | 'justify';
|
|
82
|
+
prepareOptions?: PrepareOptions;
|
|
83
|
+
className?: string;
|
|
84
|
+
style?: CSSProperties;
|
|
85
|
+
figures?: EditorialFigure[];
|
|
86
|
+
};
|
|
87
|
+
declare function EditorialSurface({ text, font, lineHeight, startY, maxY, minHeight, lineRenderMode, prepareOptions, className, style, figures, }: EditorialSurfaceProps): react_jsx_runtime.JSX.Element;
|
|
88
|
+
|
|
89
|
+
type PositionedLine = {
|
|
90
|
+
text: string;
|
|
91
|
+
x: number;
|
|
92
|
+
y: number;
|
|
93
|
+
width: number;
|
|
94
|
+
slotLeft: number;
|
|
95
|
+
slotRight: number;
|
|
96
|
+
slotWidth: number;
|
|
97
|
+
start: LayoutCursor;
|
|
98
|
+
end: LayoutCursor;
|
|
99
|
+
};
|
|
100
|
+
type TextFlowInput = {
|
|
101
|
+
prepared: PreparedTextWithSegments;
|
|
102
|
+
lineHeight: number;
|
|
103
|
+
getLineSlotAtY: (y: number) => LineSlot | null;
|
|
104
|
+
startY?: number;
|
|
105
|
+
startCursor?: LayoutCursor;
|
|
106
|
+
maxLines?: number;
|
|
107
|
+
maxY?: number;
|
|
108
|
+
maxSteps?: number;
|
|
109
|
+
};
|
|
110
|
+
type TextFlowResult = {
|
|
111
|
+
lines: PositionedLine[];
|
|
112
|
+
height: number;
|
|
113
|
+
exhausted: boolean;
|
|
114
|
+
truncated: boolean;
|
|
115
|
+
endCursor: LayoutCursor;
|
|
116
|
+
};
|
|
117
|
+
declare function flowText({ prepared, lineHeight, getLineSlotAtY, startY, startCursor, maxLines, maxY, maxSteps, }: TextFlowInput): TextFlowResult;
|
|
118
|
+
|
|
119
|
+
type EditorialPositionedLine = PositionedLine & {
|
|
120
|
+
justifyWordSpacing: number | null;
|
|
121
|
+
isTerminal: boolean;
|
|
122
|
+
isParagraphTerminal: boolean;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
type FlowRenderableLine = PositionedLine & Partial<Pick<EditorialPositionedLine, 'justifyWordSpacing'>>;
|
|
126
|
+
type FlowLineRenderInput<TLine extends FlowRenderableLine = FlowRenderableLine> = {
|
|
127
|
+
key: string;
|
|
128
|
+
line: TLine;
|
|
129
|
+
text: string;
|
|
130
|
+
style: CSSProperties;
|
|
131
|
+
};
|
|
132
|
+
type FlowLinesProps<TLine extends FlowRenderableLine = FlowRenderableLine> = {
|
|
133
|
+
lines: TLine[];
|
|
134
|
+
font: string;
|
|
135
|
+
lineHeight: number;
|
|
136
|
+
lineClassName?: string;
|
|
137
|
+
lineRenderMode?: 'natural' | 'justify';
|
|
138
|
+
renderLine?: (input: FlowLineRenderInput<TLine>) => ReactNode;
|
|
139
|
+
};
|
|
140
|
+
declare function FlowLines<TLine extends FlowRenderableLine = FlowRenderableLine>({ lines, font, lineHeight, lineClassName, lineRenderMode, renderLine, }: FlowLinesProps<TLine>): react_jsx_runtime.JSX.Element;
|
|
141
|
+
|
|
142
|
+
type UseTextFlowInput = {
|
|
143
|
+
prepared: PreparedTextWithSegments | null;
|
|
144
|
+
lineHeight: number;
|
|
145
|
+
getLineSlotAtY: (y: number) => LineSlot | null;
|
|
146
|
+
startY?: number;
|
|
147
|
+
startCursor?: LayoutCursor;
|
|
148
|
+
maxLines?: number;
|
|
149
|
+
maxY?: number;
|
|
150
|
+
maxSteps?: number;
|
|
151
|
+
enabled?: boolean;
|
|
152
|
+
};
|
|
153
|
+
type UseTextFlowResult = {
|
|
154
|
+
lines: PositionedLine[];
|
|
155
|
+
height: number;
|
|
156
|
+
exhausted: boolean;
|
|
157
|
+
truncated: boolean;
|
|
158
|
+
isReady: boolean;
|
|
159
|
+
endCursor: LayoutCursor;
|
|
160
|
+
};
|
|
161
|
+
declare function useTextFlow({ prepared, lineHeight, getLineSlotAtY, startY, startCursor, maxLines, maxY, maxSteps, enabled, }: UseTextFlowInput): UseTextFlowResult;
|
|
162
|
+
|
|
163
|
+
export { EditorialColumns, type EditorialColumnsProps, type EditorialFigure, type EditorialPlacement, EditorialSurface, type EditorialSurfaceProps, type EditorialTrack, type FlowLineRenderInput, FlowLines, type FlowLinesProps, type FlowRenderableLine, type LineSlot, type PositionedLine, type TextFlowInput, type TextFlowResult, type UseTextFlowInput, type UseTextFlowResult, carveLineSlots, createLineSlotResolver, flowText, getCircleBlockedLineRangeForRow, pickWidestLineSlot, useTextFlow };
|