@bardway/trpg-notebook-native 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/dist/index.js ADDED
@@ -0,0 +1,1094 @@
1
+ // src/storage.ts
2
+ var nativePromise = null;
3
+ var native = () => {
4
+ if (!nativePromise) {
5
+ nativePromise = import("expo").then(
6
+ ({ requireNativeModule }) => requireNativeModule("NotebookStorage")
7
+ ).catch(() => {
8
+ nativePromise = null;
9
+ throw new Error("\u624B\u8BB0\u672C\u5730\u5B58\u50A8\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u65B0\u7F16\u8BD1 Android \u5E94\u7528");
10
+ });
11
+ }
12
+ return nativePromise;
13
+ };
14
+ var decode = (payload) => JSON.parse(payload);
15
+ var notebookNativeStorage = {
16
+ capabilities: async () => decode(await (await native()).capabilities()),
17
+ ensureWorkspace: async (accountId, sessionId) => decode(await (await native()).ensureWorkspace(JSON.stringify({ accountId, sessionId }))),
18
+ readDocument: async (context, documentId) => decode(await (await native()).readDocument(JSON.stringify({ context, documentId }))),
19
+ initializeMemoTemplate: async (context, documentId, template) => decode(
20
+ await (await native()).initializeMemoTemplate(JSON.stringify({ context, documentId, template }))
21
+ ),
22
+ importDocument: async (context, document) => decode(await (await native()).importDocument(JSON.stringify({ context, document }))),
23
+ commitOperation: async (context, documentId, operation) => decode(
24
+ await (await native()).commitOperation(JSON.stringify({ context, documentId, operation }))
25
+ ),
26
+ saveViewport: async (context, documentId, viewport, memoTemplateId) => {
27
+ await (await native()).saveViewport(
28
+ JSON.stringify({
29
+ context,
30
+ documentId,
31
+ viewport,
32
+ ...memoTemplateId ? { memoTemplateId } : {}
33
+ })
34
+ );
35
+ },
36
+ saveLayoutProfile: async (context, documentId, layout, viewport, memoTemplateId) => {
37
+ await (await native()).saveLayoutProfile(
38
+ JSON.stringify({
39
+ context,
40
+ documentId,
41
+ layout,
42
+ viewport,
43
+ ...memoTemplateId ? { memoTemplateId } : {}
44
+ })
45
+ );
46
+ },
47
+ setActiveDocument: async (context, documentId) => {
48
+ await (await native()).setActiveDocument(JSON.stringify({ context, documentId }));
49
+ },
50
+ deleteSession: async (accountId, sessionId) => {
51
+ await (await native()).deleteSession(JSON.stringify({ accountId, sessionId }));
52
+ },
53
+ flush: async () => {
54
+ if (nativePromise) await (await nativePromise).flush();
55
+ }
56
+ };
57
+ var readNotebookMarkdownImport = async (uri) => decode(await (await native()).readImport(JSON.stringify({ uri })));
58
+
59
+ // src/importPicker.ts
60
+ import * as DocumentPicker from "expo-document-picker";
61
+ import { File, Paths } from "expo-file-system";
62
+ var pickNotebookMarkdownFiles = async () => {
63
+ const result = await DocumentPicker.getDocumentAsync({
64
+ type: "*/*",
65
+ multiple: true,
66
+ copyToCacheDirectory: true
67
+ });
68
+ if (result.canceled) return null;
69
+ return result.assets.map(({ uri, name, size }) => ({ uri, name, size }));
70
+ };
71
+ var cleanupNotebookImportCopy = (uri) => {
72
+ const cache = Paths.cache.uri.endsWith("/") ? Paths.cache.uri : `${Paths.cache.uri}/`;
73
+ if (!uri.startsWith(cache) || uri.includes("/../") || uri.includes("%2e")) return;
74
+ const file = new File(uri);
75
+ if (file.exists) file.delete();
76
+ };
77
+
78
+ // src/reader/ReaderHost.tsx
79
+ import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
80
+ import {
81
+ Linking,
82
+ NativeModules,
83
+ Platform as Platform2,
84
+ Text,
85
+ UIManager,
86
+ View,
87
+ findNodeHandle
88
+ } from "react-native";
89
+ import WebView from "react-native-webview";
90
+ import { useTheme } from "@bardway/trpg-ui-native";
91
+ import { resolveTypographyTextStyle } from "@bardway/trpg-ui-native";
92
+ import { ThemedButton } from "@bardway/trpg-ui-native";
93
+ import { safeExternalLink } from "@bardway/trpg-markdown/protocol";
94
+ import { createReaderChunks, ReaderChunkAssembler } from "@bardway/trpg-markdown/protocol";
95
+ import {
96
+ READER_ORIGIN,
97
+ READER_PROTOCOL_VERSION,
98
+ READER_URL
99
+ } from "@bardway/trpg-markdown/protocol";
100
+
101
+ // src/reader/ReaderNativeConfig.ts
102
+ import { Platform, requireNativeComponent } from "react-native";
103
+ var ReaderNativeComponent = Platform.OS === "android" ? requireNativeComponent("TrpgMarkdownWebView") : void 0;
104
+ var READER_NATIVE_CONFIG = ReaderNativeComponent ? { component: ReaderNativeComponent } : void 0;
105
+
106
+ // src/reader/ReaderHost.tsx
107
+ import { jsx, jsxs } from "react/jsx-runtime";
108
+ var describeSafeFailure = (value) => ({
109
+ errorType: value instanceof Error ? value.name : "UnknownFailure"
110
+ });
111
+ var SOURCE = { uri: READER_URL };
112
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
113
+ var ReaderHost = React.memo(function ReaderHost2({
114
+ documentKey,
115
+ payload,
116
+ active = true,
117
+ viewport = false,
118
+ onEvent,
119
+ onError
120
+ }) {
121
+ const { theme, typography, logger } = useTheme();
122
+ const reactId = useId();
123
+ const instanceId = useMemo(() => `reader-${reactId}-${Date.now().toString(36)}`, [reactId]);
124
+ const nonce = useRef(`${Math.random().toString(36)}${Math.random().toString(36)}`).current;
125
+ const webview = useRef(null);
126
+ const container = useRef(null);
127
+ const [ready, setReady] = useState(false);
128
+ const [height, setHeight] = useState(1);
129
+ const [failure, setFailure] = useState(null);
130
+ const currentReady = useRef(ready);
131
+ currentReady.current = ready;
132
+ const errorText = resolveTypographyTextStyle(typography, {
133
+ role: "body",
134
+ fontSize: 13,
135
+ metric: "body"
136
+ });
137
+ const eventRoutes = useRef(/* @__PURE__ */ new Map());
138
+ eventRoutes.current.set(documentKey, onEvent);
139
+ if (eventRoutes.current.size > 24) {
140
+ const first = eventRoutes.current.keys().next().value;
141
+ if (first) eventRoutes.current.delete(first);
142
+ }
143
+ const currentKey = useRef(documentKey);
144
+ currentKey.current = documentKey;
145
+ const currentActive = useRef(active);
146
+ currentActive.current = active;
147
+ const errorCallback = useRef(onError);
148
+ errorCallback.current = onError;
149
+ const incoming = useRef(new ReaderChunkAssembler());
150
+ const sendForKey = useCallback(
151
+ (key, type, data) => {
152
+ webview.current?.postMessage(
153
+ JSON.stringify({
154
+ protocol: READER_PROTOCOL_VERSION,
155
+ instanceId,
156
+ nonce,
157
+ documentKey: key,
158
+ type,
159
+ payload: data
160
+ })
161
+ );
162
+ },
163
+ [instanceId, nonce]
164
+ );
165
+ const report = useCallback(
166
+ (message, stage, cause) => {
167
+ setFailure(message);
168
+ errorCallback.current?.(message);
169
+ logger.warn("[MarkdownReader] Reader failed", {
170
+ instanceId,
171
+ stage,
172
+ ...describeSafeFailure(message),
173
+ ...cause === void 0 ? {} : { cause: describeSafeFailure(cause) },
174
+ ready: currentReady.current,
175
+ active: currentActive.current
176
+ });
177
+ },
178
+ [instanceId, logger]
179
+ );
180
+ const serialized = useMemo(() => JSON.stringify(payload), [payload]);
181
+ useEffect(() => {
182
+ if (ready) return;
183
+ const timeout = setTimeout(
184
+ () => report("\u672C\u5730\u9605\u8BFB\u5668\u672A\u5C31\u7EEA\uFF0C\u8BF7\u68C0\u67E5 Android System WebView \u7248\u672C\u5E76\u91CD\u65B0\u6253\u5F00", "initialization"),
185
+ 15e3
186
+ );
187
+ return () => clearTimeout(timeout);
188
+ }, [ready, report]);
189
+ useEffect(() => {
190
+ if (!ready) return;
191
+ setFailure(null);
192
+ const id = `${documentKey}-${Date.now()}-${Math.random()}`;
193
+ try {
194
+ for (const chunk of createReaderChunks(serialized, id))
195
+ sendForKey(documentKey, "documentChunk", chunk);
196
+ } catch (cause) {
197
+ report("\u6587\u6863\u8D85\u8FC7\u9605\u8BFB\u5668\u6D88\u606F\u5BB9\u91CF\uFF0C\u539F\u59CB\u6570\u636E\u5DF2\u4FDD\u7559", "document-transfer", cause);
198
+ }
199
+ sendForKey(documentKey, "setActive", currentActive.current);
200
+ }, [ready, documentKey, serialized, sendForKey, report]);
201
+ useEffect(() => {
202
+ if (ready) sendForKey(documentKey, "setActive", active);
203
+ }, [ready, active, documentKey, sendForKey]);
204
+ const processMessage = useCallback(
205
+ function process(message) {
206
+ if (message.type === "readerReady") {
207
+ setReady(true);
208
+ return;
209
+ }
210
+ if (message.type === "bridgeChunk") {
211
+ const combined = incoming.current.accept(message.documentKey, message.payload);
212
+ if (combined !== null) {
213
+ const value = JSON.parse(combined);
214
+ if (isRecord(value) && typeof value.type === "string" && value.type !== "bridgeChunk")
215
+ process({ ...message, type: value.type, payload: value.payload });
216
+ }
217
+ return;
218
+ }
219
+ if (message.type === "readerError") {
220
+ if (message.documentKey !== currentKey.current) return;
221
+ report(
222
+ isRecord(message.payload) && typeof message.payload.message === "string" ? message.payload.message : "\u9605\u8BFB\u5668\u52A0\u8F7D\u5931\u8D25",
223
+ "renderer"
224
+ );
225
+ return;
226
+ }
227
+ if (message.type === "openExternalLink") {
228
+ const url = isRecord(message.payload) && typeof message.payload.url === "string" ? safeExternalLink(message.payload.url) : null;
229
+ if (url && message.documentKey === currentKey.current)
230
+ void Linking.openURL(url).catch(
231
+ (cause) => report("\u94FE\u63A5\u65E0\u6CD5\u6253\u5F00", "external-link", cause)
232
+ );
233
+ return;
234
+ }
235
+ if (message.type === "anchorRequested" && message.documentKey === currentKey.current && isRecord(message.payload)) {
236
+ const y = message.payload.y;
237
+ const bridge = NativeModules.TrpgMarkdownReaderBridge;
238
+ const tag = findNodeHandle(container.current);
239
+ if (tag !== null && typeof y === "number" && Number.isFinite(y) && y >= 0 && y <= 1e6 && isRecord(bridge) && typeof bridge.revealAnchor === "function")
240
+ bridge.revealAnchor(tag, y);
241
+ return;
242
+ }
243
+ if (message.type === "blockMetrics" && message.documentKey === currentKey.current && isRecord(message.payload)) {
244
+ const next = message.payload.height;
245
+ if (typeof next === "number" && Number.isFinite(next) && next > 0 && next <= 1e6)
246
+ setHeight((old) => Math.abs(old - next) < 1 ? old : Math.ceil(next));
247
+ }
248
+ eventRoutes.current.get(message.documentKey)?.(message.type, message.payload, {
249
+ send: (type, data) => sendForKey(message.documentKey, type, data)
250
+ });
251
+ },
252
+ [report, sendForKey]
253
+ );
254
+ const onMessage = useCallback(
255
+ (event) => {
256
+ if (!event.nativeEvent.url.startsWith(`${READER_ORIGIN}/reader/`) || event.nativeEvent.data.length > 1e5)
257
+ return;
258
+ try {
259
+ const value = JSON.parse(event.nativeEvent.data);
260
+ if (!isRecord(value) || value.protocol !== READER_PROTOCOL_VERSION || value.instanceId !== instanceId || value.nonce !== nonce || typeof value.documentKey !== "string" || value.documentKey.length > 512 || typeof value.type !== "string")
261
+ return;
262
+ processMessage({
263
+ protocol: READER_PROTOCOL_VERSION,
264
+ instanceId,
265
+ nonce,
266
+ documentKey: value.documentKey,
267
+ type: value.type,
268
+ payload: value.payload
269
+ });
270
+ } catch (cause) {
271
+ report("\u9605\u8BFB\u5668\u6D88\u606F\u65E0\u6548", "bridge-message", cause);
272
+ }
273
+ },
274
+ [instanceId, nonce, processMessage, report]
275
+ );
276
+ if (Platform2.OS !== "android" || !UIManager.getViewManagerConfig("TrpgMarkdownWebView")) {
277
+ return /* @__PURE__ */ jsx(
278
+ Text,
279
+ {
280
+ style: {
281
+ color: theme.colors.text.muted,
282
+ fontFamily: typography.body,
283
+ fontSize: 14,
284
+ lineHeight: 21,
285
+ includeFontPadding: false
286
+ },
287
+ children: "\u5F53\u524D\u5B89\u88C5\u5305\u7F3A\u5C11\u672C\u5730\u9605\u8BFB\u5668\uFF0C\u8BF7\u91CD\u65B0\u7F16\u8BD1 Android APK\u3002"
288
+ }
289
+ );
290
+ }
291
+ return /* @__PURE__ */ jsxs(
292
+ View,
293
+ {
294
+ ref: container,
295
+ collapsable: false,
296
+ style: viewport ? { flex: 1, minHeight: 1 } : { height: Math.max(1, height) },
297
+ children: [
298
+ /* @__PURE__ */ jsx(
299
+ WebView,
300
+ {
301
+ ref: webview,
302
+ nativeConfig: READER_NATIVE_CONFIG,
303
+ source: SOURCE,
304
+ onMessage,
305
+ style: { backgroundColor: "transparent" },
306
+ containerStyle: { backgroundColor: "transparent" },
307
+ onLoadEnd: () => {
308
+ setReady(false);
309
+ sendForKey(currentKey.current, "initialize");
310
+ },
311
+ onError: (event) => report("\u9605\u8BFB\u5668\u8D44\u6E90\u52A0\u8F7D\u5931\u8D25\uFF0C\u53EF\u91CD\u65B0\u6253\u5F00\u6587\u6863", "resource-load", {
312
+ code: event.nativeEvent.code
313
+ }),
314
+ onRenderProcessGone: () => {
315
+ setReady(false);
316
+ webview.current?.reload();
317
+ },
318
+ onShouldStartLoadWithRequest: (request) => request.url === READER_URL,
319
+ javaScriptEnabled: true,
320
+ domStorageEnabled: false,
321
+ allowFileAccess: false,
322
+ allowFileAccessFromFileURLs: false,
323
+ allowUniversalAccessFromFileURLs: false,
324
+ mixedContentMode: "never",
325
+ thirdPartyCookiesEnabled: false,
326
+ sharedCookiesEnabled: false,
327
+ setSupportMultipleWindows: false,
328
+ setBuiltInZoomControls: false,
329
+ scalesPageToFit: false,
330
+ scrollEnabled: viewport,
331
+ textZoom: 100,
332
+ androidLayerType: "hardware"
333
+ }
334
+ ),
335
+ failure ? /* @__PURE__ */ jsxs(
336
+ View,
337
+ {
338
+ accessibilityLiveRegion: "assertive",
339
+ style: {
340
+ position: "absolute",
341
+ bottom: 0,
342
+ left: 0,
343
+ right: 0,
344
+ backgroundColor: theme.colors.surface.actionDanger,
345
+ borderTopWidth: 1,
346
+ borderColor: theme.colors.status.error,
347
+ padding: theme.layout.contentPadding.compact,
348
+ flexDirection: "row",
349
+ alignItems: "center",
350
+ gap: theme.layout.density.spacing.groupY
351
+ },
352
+ children: [
353
+ /* @__PURE__ */ jsx(
354
+ Text,
355
+ {
356
+ accessibilityRole: "alert",
357
+ style: [errorText, { color: theme.colors.text.primary, flex: 1 }],
358
+ children: failure
359
+ }
360
+ ),
361
+ /* @__PURE__ */ jsx(
362
+ ThemedButton,
363
+ {
364
+ label: "\u91CD\u8BD5",
365
+ accessibilityLabel: "\u91CD\u65B0\u52A0\u8F7D\u9605\u8BFB\u5668",
366
+ variant: "danger",
367
+ style: {
368
+ minHeight: theme.layout.touch.minimumTarget,
369
+ minWidth: theme.layout.touch.minimumTarget
370
+ },
371
+ onPress: () => {
372
+ incoming.current = new ReaderChunkAssembler();
373
+ setFailure(null);
374
+ setReady(false);
375
+ webview.current?.reload();
376
+ }
377
+ }
378
+ )
379
+ ]
380
+ }
381
+ ) : null
382
+ ]
383
+ }
384
+ );
385
+ });
386
+
387
+ // src/reader/MarkdownView.tsx
388
+ import React2, { useCallback as useCallback2, useMemo as useMemo2 } from "react";
389
+ import { useTheme as useTheme2 } from "@bardway/trpg-ui-native";
390
+ import { jsx as jsx2 } from "react/jsx-runtime";
391
+ var useMarkdownTheme = (surface) => {
392
+ const { theme, typography } = useTheme2();
393
+ return useMemo2(
394
+ () => ({
395
+ body: surface === "paper" ? theme.colors.notebook.text : theme.colors.text.markdownBody,
396
+ heading: surface === "paper" ? theme.colors.notebook.text : theme.colors.text.accent,
397
+ muted: surface === "paper" ? theme.colors.notebook.muted : theme.colors.text.muted,
398
+ link: surface === "paper" ? theme.colors.notebook.link : theme.colors.text.accent,
399
+ border: surface === "paper" ? theme.colors.notebook.border : theme.colors.border.default,
400
+ codeBackground: surface === "paper" ? theme.colors.notebook.codeBackground : theme.colors.surface.card,
401
+ background: surface === "paper" ? theme.colors.notebook.paper : "transparent",
402
+ paperShade: surface === "paper" ? theme.colors.notebook.paperShade : void 0,
403
+ paperFiber: surface === "paper" ? theme.colors.notebook.paperFiber : void 0,
404
+ sizeScale: surface === "paper" ? 1 : typography.sizeScale,
405
+ narrativeFamily: typography.narrative,
406
+ titleFamily: typography.title,
407
+ uiFamily: typography.ui,
408
+ warning: theme.colors.functional.warning,
409
+ success: theme.colors.functional.success
410
+ }),
411
+ [surface, theme.colors, typography]
412
+ );
413
+ };
414
+ var MarkdownView = React2.memo(function MarkdownView2({
415
+ documentKey,
416
+ markdown,
417
+ blocks,
418
+ variant = "body",
419
+ surface = "app",
420
+ scrollMode = "intrinsic",
421
+ columns = 1,
422
+ onBlockMetrics
423
+ }) {
424
+ const theme = useMarkdownTheme(surface);
425
+ const payload = useMemo2(
426
+ () => ({
427
+ blocks: blocks ?? [{ id: "body", markdown: markdown ?? "", variant }],
428
+ theme,
429
+ scrollMode,
430
+ columns
431
+ }),
432
+ [blocks, markdown, variant, theme, scrollMode, columns]
433
+ );
434
+ const onEvent = useCallback2(
435
+ (type, data) => {
436
+ if (type !== "blockMetrics" || !isRecord(data) || !Array.isArray(data.metrics)) return;
437
+ const metrics = [];
438
+ for (const entry of data.metrics) {
439
+ if (!isRecord(entry) || typeof entry.id !== "string" || typeof entry.top !== "number" || typeof entry.height !== "number" || !Number.isFinite(entry.top) || !Number.isFinite(entry.height) || entry.height < 0)
440
+ return;
441
+ metrics.push({
442
+ id: entry.id,
443
+ top: entry.top,
444
+ height: entry.height,
445
+ left: typeof entry.left === "number" ? entry.left : 0,
446
+ width: typeof entry.width === "number" ? entry.width : void 0
447
+ });
448
+ }
449
+ onBlockMetrics?.(metrics);
450
+ },
451
+ [onBlockMetrics]
452
+ );
453
+ return /* @__PURE__ */ jsx2(
454
+ ReaderHost,
455
+ {
456
+ documentKey,
457
+ payload,
458
+ viewport: scrollMode === "viewport",
459
+ onEvent
460
+ }
461
+ );
462
+ });
463
+ var MarkdownBlocksView = MarkdownView;
464
+
465
+ // src/reader/MarkdownDocumentView.tsx
466
+ import React3, { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo3, useRef as useRef2 } from "react";
467
+ import { useTheme as useTheme3 } from "@bardway/trpg-ui-native";
468
+ import { resolveNotebookPaperTypography } from "@bardway/trpg-notebook";
469
+ import { NOTEBOOK_LIMITS } from "@bardway/trpg-notebook";
470
+ import { jsx as jsx3 } from "react/jsx-runtime";
471
+ var finite = (value) => typeof value === "number" && Number.isFinite(value);
472
+ var viewportGuard = (value) => isRecord(value) && finite(value.scale) && value.scale >= NOTEBOOK_LIMITS.minScale && value.scale <= NOTEBOOK_LIMITS.maxScale && finite(value.paperOffsetX) && Math.abs(value.paperOffsetX) <= 1e7 && finite(value.paperOffsetY) && Math.abs(value.paperOffsetY) <= 1e7;
473
+ var operationGuard = (value) => {
474
+ if (!isRecord(value) || typeof value.opId !== "string" || value.opId.length > 160) return false;
475
+ if (value.type === "eraseStrokes")
476
+ return Array.isArray(value.strokeIds) && value.strokeIds.length <= 1e5 && value.strokeIds.every((id) => typeof id === "string");
477
+ if (value.type !== "addStroke" || !isRecord(value.stroke)) return false;
478
+ const stroke = value.stroke;
479
+ return typeof stroke.id === "string" && typeof stroke.documentId === "string" && finite(stroke.width) && stroke.width > 0 && finite(stroke.layoutVersion) && typeof stroke.color === "string" && typeof stroke.createdAt === "string" && (stroke.coordinateSpace === void 0 || stroke.coordinateSpace === "paper") && Array.isArray(stroke.points) && stroke.points.length > 0 && stroke.points.length <= 1e5 && stroke.points.every(
480
+ (point) => isRecord(point) && finite(point.x) && finite(point.y) && (point.pressure === void 0 || finite(point.pressure))
481
+ );
482
+ };
483
+ var layoutGuard = (value) => isRecord(value) && ["rendererVersion", "fontAssetVersion", "styleProfileId", "fingerprint"].every(
484
+ (key) => typeof value[key] === "string"
485
+ ) && ["layoutVersion", "paperWidth", "paperHeight", "contentWidth"].every(
486
+ (key) => finite(value[key]) && Number(value[key]) > 0
487
+ ) && (value.memoTemplateId === void 0 || typeof value.memoTemplateId === "string");
488
+ var MarkdownDocumentView = React3.memo(function MarkdownDocumentView2(props) {
489
+ const { theme } = useTheme3();
490
+ const markdownTheme = useMarkdownTheme("paper");
491
+ const handle = useRef2(null);
492
+ const templateId = props.document.memoTemplate?.id;
493
+ const key = `${props.context.accountId}/${props.context.sessionId}/${props.context.generation}/${props.document.id}${templateId ? `/template/${templateId}` : ""}`;
494
+ const activeKey = useRef2(key);
495
+ activeKey.current = key;
496
+ const activeDocumentId = useRef2(props.document.id);
497
+ activeDocumentId.current = props.document.id;
498
+ const payload = useMemo3(() => {
499
+ const paperTypography = resolveNotebookPaperTypography(props.document);
500
+ return {
501
+ document: props.document,
502
+ context: props.context,
503
+ theme: { ...markdownTheme, sizeScale: paperTypography.sizeScale },
504
+ profile: {
505
+ rendererVersion: "1",
506
+ fontAssetVersion: "1",
507
+ styleProfileId: paperTypography.styleProfileId,
508
+ paperWidth: 1600,
509
+ inkColor: theme.colors.notebook.ink,
510
+ inkWidth: 3
511
+ }
512
+ };
513
+ }, [props.document, props.context, markdownTheme, theme.colors.notebook.ink]);
514
+ useEffect2(() => {
515
+ handle.current?.send("setTool", props.tool);
516
+ }, [props.tool]);
517
+ useEffect2(() => {
518
+ if (props.retryToken) handle.current?.send("retrySaves");
519
+ }, [props.retryToken]);
520
+ const onEvent = useCallback3(
521
+ (type, payloadValue, bridge) => {
522
+ if (key === activeKey.current) handle.current = bridge;
523
+ if (!isRecord(payloadValue)) return;
524
+ if (key !== activeKey.current && props.document.id === activeDocumentId.current || payloadValue.memoTemplateId !== templateId)
525
+ return;
526
+ if (type === "layoutReady" && layoutGuard(payloadValue.layout)) {
527
+ if (key !== activeKey.current || payloadValue.layout.memoTemplateId !== templateId) return;
528
+ if (payloadValue.viewport !== void 0 && !viewportGuard(payloadValue.viewport)) {
529
+ bridge.send("layoutAccepted", false);
530
+ props.onError("\u7EB8\u9762\u9605\u8BFB\u4F4D\u7F6E\u65E0\u6548\uFF0C\u5DF2\u4FDD\u7559\u539F\u59CB\u6570\u636E");
531
+ return;
532
+ }
533
+ void props.onLayoutProfile(payloadValue.layout, payloadValue.viewport).then(() => {
534
+ if (key !== activeKey.current) return;
535
+ bridge.send("layoutAccepted", true);
536
+ bridge.send("setTool", props.tool);
537
+ props.onReady?.();
538
+ }).catch((error) => {
539
+ if (key !== activeKey.current) return;
540
+ bridge.send("layoutAccepted", false);
541
+ props.onError(error instanceof Error ? error.message : "\u7EB8\u9762\u7248\u672C\u4E0D\u517C\u5BB9\uFF0C\u7B14\u8FF9\u5DF2\u4FDD\u7559");
542
+ });
543
+ } else if ((type === "strokeCommitted" || type === "strokesErased") && operationGuard(payloadValue.operation)) {
544
+ const operation = payloadValue.operation;
545
+ void props.onOperation(operation).then((ack) => bridge.send("saveAcknowledged", ack)).catch((error) => {
546
+ const message = error instanceof Error ? error.message : "\u4FDD\u5B58\u5931\u8D25\uFF0C\u53EF\u91CD\u8BD5";
547
+ bridge.send("saveFailed", { opId: operation.opId, message });
548
+ props.onError(message);
549
+ });
550
+ } else if (type === "viewportChanged" && isRecord(payloadValue.viewport)) {
551
+ const { scale, paperOffsetX, paperOffsetY } = payloadValue.viewport;
552
+ if (finite(scale) && finite(paperOffsetX) && finite(paperOffsetY) && scale >= 0.1 && scale <= 8)
553
+ void Promise.resolve(props.onViewport({ scale, paperOffsetX, paperOffsetY })).catch(
554
+ () => props.onError("\u9605\u8BFB\u4F4D\u7F6E\u4FDD\u5B58\u5931\u8D25")
555
+ );
556
+ } else if (type === "saveStateChanged") {
557
+ const state = payloadValue.state;
558
+ if (state === "saved" || state === "saving" || state === "failed")
559
+ props.onSaveStateChange?.(state);
560
+ }
561
+ },
562
+ [props, key, templateId]
563
+ );
564
+ return /* @__PURE__ */ jsx3(
565
+ ReaderHost,
566
+ {
567
+ documentKey: key,
568
+ payload,
569
+ active: props.isActive,
570
+ viewport: true,
571
+ onEvent,
572
+ onError: props.onError
573
+ }
574
+ );
575
+ });
576
+
577
+ // src/NotebookReaderFrame.tsx
578
+ import { useRef as useRef3 } from "react";
579
+ import { ActivityIndicator, StyleSheet, View as View2 } from "react-native";
580
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
581
+ function NotebookReaderFrame({
582
+ scopeKey,
583
+ readerProps,
584
+ loading,
585
+ active,
586
+ loadingColor,
587
+ loadingBackground,
588
+ children
589
+ }) {
590
+ const retained = useRef3(null);
591
+ if (retained.current?.scopeKey !== scopeKey) retained.current = null;
592
+ if (readerProps) retained.current = { scopeKey, props: readerProps };
593
+ const ready = retained.current?.props;
594
+ return /* @__PURE__ */ jsxs2(View2, { style: styles.root, children: [
595
+ ready ? /* @__PURE__ */ jsx4(
596
+ MarkdownDocumentView,
597
+ {
598
+ ...ready,
599
+ isActive: active && !loading && readerProps !== null
600
+ },
601
+ scopeKey
602
+ ) : !loading ? children : null,
603
+ loading ? /* @__PURE__ */ jsx4(
604
+ View2,
605
+ {
606
+ pointerEvents: "auto",
607
+ accessibilityLabel: "\u6B63\u5728\u8BFB\u53D6\u624B\u8BB0\u6B63\u6587",
608
+ style: [styles.loading, { backgroundColor: loadingBackground }],
609
+ children: /* @__PURE__ */ jsx4(ActivityIndicator, { color: loadingColor })
610
+ }
611
+ ) : null
612
+ ] });
613
+ }
614
+ var styles = StyleSheet.create({
615
+ root: { flex: 1, minHeight: 0 },
616
+ loading: { ...StyleSheet.absoluteFillObject, alignItems: "center", justifyContent: "center" }
617
+ });
618
+
619
+ // src/NotebookPage.tsx
620
+ import React5, { useCallback as useCallback4, useEffect as useEffect3, useRef as useRef4, useState as useState2 } from "react";
621
+ import {
622
+ ActivityIndicator as ActivityIndicator2,
623
+ AppState,
624
+ Pressable,
625
+ ScrollView,
626
+ StyleSheet as StyleSheet2,
627
+ Text as Text2,
628
+ View as View3
629
+ } from "react-native";
630
+ import { useTheme as useTheme4 } from "@bardway/trpg-ui-native";
631
+ import { resolveTypographyTextStyle as resolveTypographyTextStyle2 } from "@bardway/trpg-ui-native";
632
+ import { ThemedButton as ThemedButton2 } from "@bardway/trpg-ui-native";
633
+ import { useNotebookWorkspace } from "@bardway/trpg-notebook/react";
634
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
635
+ var NotebookPage = React5.memo(function NotebookPage2({
636
+ accountId,
637
+ sessionId,
638
+ isActive,
639
+ repository,
640
+ importMarkdown,
641
+ diagnostics,
642
+ memoTemplate
643
+ }) {
644
+ const { theme, typography } = useTheme4();
645
+ const state = useNotebookWorkspace({
646
+ accountId,
647
+ sessionId,
648
+ repository,
649
+ importMarkdown,
650
+ diagnostics,
651
+ memoTemplate
652
+ });
653
+ const documentTabs = [...state.workspace?.documents ?? []].sort(
654
+ (left, right) => Number(right.kind === "memo") - Number(left.kind === "memo")
655
+ );
656
+ const flushWorkspace = state.flush;
657
+ const [tool, setTool] = useState2("pen");
658
+ const [retryToken, setRetryToken] = useState2(0);
659
+ const [appActive, setAppActive] = useState2(
660
+ AppState.currentState !== "background" && AppState.currentState !== "inactive"
661
+ );
662
+ const tabScrollRef = useRef4(null);
663
+ const tabPositions = useRef4(/* @__PURE__ */ new Map());
664
+ const tabWidthRef = useRef4(0);
665
+ const uiText = resolveTypographyTextStyle2(typography, {
666
+ role: "ui",
667
+ fontSize: 13,
668
+ metric: "control",
669
+ centerVertically: true
670
+ });
671
+ const bodyText = resolveTypographyTextStyle2(typography, {
672
+ role: "body",
673
+ fontSize: 13,
674
+ metric: "body"
675
+ });
676
+ const revealTab = useCallback4((id) => {
677
+ const x = tabPositions.current.get(id);
678
+ if (x !== void 0)
679
+ tabScrollRef.current?.scrollTo({
680
+ x: Math.max(0, x - tabWidthRef.current / 3),
681
+ animated: false
682
+ });
683
+ }, []);
684
+ useEffect3(() => {
685
+ if (state.document) revealTab(state.document.id);
686
+ }, [state.document?.id, state.document, revealTab]);
687
+ useEffect3(() => {
688
+ setTool("pen");
689
+ }, [accountId, sessionId]);
690
+ useEffect3(() => {
691
+ const subscription = AppState.addEventListener("change", (next) => {
692
+ setAppActive(next === "active");
693
+ if (next !== "active") void flushWorkspace();
694
+ });
695
+ return () => subscription.remove();
696
+ }, [flushWorkspace]);
697
+ const retry = () => {
698
+ if (state.failedSelectionId) void state.selectDocument(state.failedSelectionId);
699
+ else if (!state.document) state.reload();
700
+ else {
701
+ state.prepareRetry();
702
+ void flushWorkspace();
703
+ setRetryToken((value) => value + 1);
704
+ }
705
+ };
706
+ const hasCurrentWorkspace = state.workspace?.context.accountId === accountId && state.workspace.context.sessionId === sessionId;
707
+ const readerLoading = state.loading || state.workspace !== null && !hasCurrentWorkspace;
708
+ const hasFailure = Boolean(state.error) || state.saveState === "failed";
709
+ const failureMessage = state.error || (state.saveState === "failed" ? "\u624B\u8BB0\u4FDD\u5B58\u5931\u8D25\uFF0C\u5DF2\u4FDD\u5B58\u5185\u5BB9\u4FDD\u7559\u3002\u8BF7\u91CD\u8BD5\u3002" : null);
710
+ const statusLabel = hasFailure ? null : readerLoading ? "\u8BFB\u53D6\u4E2D" : state.saveState === "saving" ? "\u4FDD\u5B58\u4E2D" : null;
711
+ return /* @__PURE__ */ jsxs3(View3, { style: styles2.root, children: [
712
+ /* @__PURE__ */ jsxs3(View3, { style: [styles2.toolbar, { minHeight: theme.layout.density.chrome.toolbarHeight }], children: [
713
+ /* @__PURE__ */ jsxs3(
714
+ ScrollView,
715
+ {
716
+ ref: tabScrollRef,
717
+ horizontal: true,
718
+ showsHorizontalScrollIndicator: false,
719
+ style: styles2.tabScroller,
720
+ contentContainerStyle: styles2.tabs,
721
+ onLayout: (event) => {
722
+ tabWidthRef.current = event.nativeEvent.layout.width;
723
+ if (state.document) revealTab(state.document.id);
724
+ },
725
+ accessibilityRole: "tablist",
726
+ children: [
727
+ documentTabs.map((item) => {
728
+ const selected = state.document?.id === item.id;
729
+ return /* @__PURE__ */ jsx5(
730
+ Pressable,
731
+ {
732
+ accessibilityRole: "tab",
733
+ accessibilityLabel: item.title,
734
+ accessibilityState: { selected },
735
+ onPress: () => {
736
+ void state.selectDocument(item.id);
737
+ },
738
+ onLayout: (event) => {
739
+ tabPositions.current.set(item.id, event.nativeEvent.layout.x);
740
+ if (selected) revealTab(item.id);
741
+ },
742
+ style: [
743
+ styles2.tab,
744
+ {
745
+ minHeight: theme.layout.touch.minimumTarget,
746
+ backgroundColor: selected ? theme.colors.surface.tabActive : theme.colors.surface.tileAlt,
747
+ borderColor: selected ? theme.colors.border.active : theme.colors.border.default
748
+ }
749
+ ],
750
+ children: /* @__PURE__ */ jsx5(
751
+ Text2,
752
+ {
753
+ numberOfLines: 1,
754
+ style: [
755
+ uiText,
756
+ { color: selected ? theme.colors.text.accent : theme.colors.text.primary }
757
+ ],
758
+ children: item.title
759
+ }
760
+ )
761
+ },
762
+ item.id
763
+ );
764
+ }),
765
+ /* @__PURE__ */ jsx5(
766
+ Pressable,
767
+ {
768
+ onPress: () => {
769
+ void state.importDocuments();
770
+ },
771
+ disabled: state.importing || !state.workspace,
772
+ accessibilityRole: "button",
773
+ accessibilityLabel: "\u5BFC\u5165 Markdown \u8865\u5145\u8D44\u6599",
774
+ accessibilityState: {
775
+ disabled: state.importing || !state.workspace,
776
+ busy: state.importing
777
+ },
778
+ style: [
779
+ styles2.add,
780
+ {
781
+ minHeight: theme.layout.touch.minimumTarget,
782
+ minWidth: theme.layout.touch.minimumTarget,
783
+ borderColor: theme.colors.border.default,
784
+ backgroundColor: theme.colors.surface.tileAlt
785
+ }
786
+ ],
787
+ children: state.importing ? /* @__PURE__ */ jsx5(ActivityIndicator2, { color: theme.colors.text.accent }) : /* @__PURE__ */ jsx5(Text2, { style: [uiText, { color: theme.colors.text.accent }], children: "+" })
788
+ }
789
+ )
790
+ ]
791
+ }
792
+ ),
793
+ /* @__PURE__ */ jsx5(View3, { style: styles2.toolGroup, accessibilityRole: "radiogroup", children: ["pen", "eraser"].map((value, index) => /* @__PURE__ */ jsx5(
794
+ Pressable,
795
+ {
796
+ accessibilityRole: "radio",
797
+ accessibilityLabel: value === "pen" ? "\u753B\u7B14" : "\u6574\u7B14\u6A61\u76AE",
798
+ accessibilityState: { checked: tool === value },
799
+ onPress: () => setTool((current) => value === "eraser" && current === "eraser" ? "pen" : value),
800
+ style: [
801
+ styles2.tool,
802
+ index === 0 ? styles2.toolFirst : styles2.toolLast,
803
+ {
804
+ minHeight: theme.layout.touch.minimumTarget,
805
+ borderColor: tool === value ? theme.colors.border.active : theme.colors.border.default,
806
+ backgroundColor: tool === value ? theme.colors.surface.tabActive : theme.colors.surface.tileAlt
807
+ }
808
+ ],
809
+ children: /* @__PURE__ */ jsx5(
810
+ Text2,
811
+ {
812
+ style: [
813
+ uiText,
814
+ { color: tool === value ? theme.colors.text.accent : theme.colors.text.primary }
815
+ ],
816
+ children: value === "pen" ? "\u753B\u7B14" : "\u6A61\u76AE"
817
+ }
818
+ )
819
+ },
820
+ value
821
+ )) })
822
+ ] }),
823
+ failureMessage ? /* @__PURE__ */ jsxs3(
824
+ View3,
825
+ {
826
+ accessibilityLiveRegion: "assertive",
827
+ style: [
828
+ styles2.failure,
829
+ {
830
+ backgroundColor: theme.colors.surface.actionDanger,
831
+ borderColor: theme.colors.status.error,
832
+ padding: theme.layout.contentPadding.compact,
833
+ gap: theme.layout.density.spacing.groupY
834
+ }
835
+ ],
836
+ children: [
837
+ /* @__PURE__ */ jsx5(
838
+ Text2,
839
+ {
840
+ accessibilityRole: "alert",
841
+ style: [bodyText, { color: theme.colors.text.primary, flex: 1 }],
842
+ children: failureMessage
843
+ }
844
+ ),
845
+ /* @__PURE__ */ jsx5(
846
+ ThemedButton2,
847
+ {
848
+ label: "\u91CD\u8BD5",
849
+ accessibilityLabel: "\u91CD\u8BD5\u8BFB\u53D6\u6216\u4FDD\u5B58\u624B\u8BB0",
850
+ onPress: retry,
851
+ variant: "danger",
852
+ style: {
853
+ minHeight: theme.layout.touch.minimumTarget,
854
+ minWidth: theme.layout.touch.minimumTarget
855
+ }
856
+ }
857
+ )
858
+ ]
859
+ }
860
+ ) : null,
861
+ state.notice ? /* @__PURE__ */ jsx5(Text2, { numberOfLines: 3, style: [bodyText, { color: theme.colors.text.secondary }], children: state.notice }) : null,
862
+ /* @__PURE__ */ jsxs3(View3, { style: styles2.reader, children: [
863
+ /* @__PURE__ */ jsx5(
864
+ NotebookReaderFrame,
865
+ {
866
+ scopeKey: `${accountId}/${sessionId}`,
867
+ active: isActive && appActive,
868
+ loading: readerLoading,
869
+ loadingColor: theme.colors.text.accent,
870
+ loadingBackground: theme.colors.surface.overlay,
871
+ readerProps: !readerLoading && hasCurrentWorkspace && state.document && state.workspace ? {
872
+ context: state.workspace.context,
873
+ document: state.document,
874
+ isActive: isActive && appActive,
875
+ tool,
876
+ retryToken,
877
+ onOperation: state.commitOperation,
878
+ onViewport: state.saveViewport,
879
+ onLayoutProfile: state.saveLayoutProfile,
880
+ onError: state.reportError,
881
+ onSaveStateChange: state.reportSaveState
882
+ } : null,
883
+ children: /* @__PURE__ */ jsxs3(View3, { style: styles2.empty, children: [
884
+ /* @__PURE__ */ jsx5(Text2, { style: [bodyText, { color: theme.colors.text.secondary }], children: "\u624B\u8BB0\u5C1A\u672A\u8BFB\u53D6\u3002\u6B63\u6587\u548C\u7B14\u8FF9\u4FDD\u7559\u5728\u672C\u673A\u3002" }),
885
+ /* @__PURE__ */ jsx5(ThemedButton2, { label: "\u91CD\u8BD5", onPress: state.reload, variant: "secondary" })
886
+ ] })
887
+ }
888
+ ),
889
+ statusLabel ? /* @__PURE__ */ jsx5(
890
+ View3,
891
+ {
892
+ pointerEvents: "none",
893
+ style: [
894
+ styles2.status,
895
+ {
896
+ minHeight: theme.layout.touch.minimumTarget,
897
+ backgroundColor: theme.colors.surface.panel
898
+ }
899
+ ],
900
+ accessibilityRole: "text",
901
+ accessibilityLabel: statusLabel,
902
+ children: /* @__PURE__ */ jsx5(Text2, { style: [uiText, { color: theme.colors.text.secondary }], children: statusLabel })
903
+ }
904
+ ) : null
905
+ ] })
906
+ ] });
907
+ });
908
+ var styles2 = StyleSheet2.create({
909
+ root: { flex: 1, minHeight: 0, gap: 6 },
910
+ toolbar: { flexDirection: "row", alignItems: "center", gap: 12, flexShrink: 0 },
911
+ tabScroller: { flex: 1, minWidth: 0 },
912
+ tabs: { flexDirection: "row", alignItems: "stretch", gap: 4 },
913
+ tab: {
914
+ minWidth: 88,
915
+ maxWidth: 260,
916
+ borderWidth: 1,
917
+ borderRadius: 5,
918
+ paddingHorizontal: 12,
919
+ justifyContent: "center"
920
+ },
921
+ add: { borderWidth: 1, borderRadius: 5, justifyContent: "center", alignItems: "center" },
922
+ toolGroup: { flexDirection: "row", flexShrink: 0 },
923
+ failure: { flexDirection: "row", alignItems: "center", borderWidth: 1, borderRadius: 5 },
924
+ status: {
925
+ position: "absolute",
926
+ top: 6,
927
+ left: 6,
928
+ zIndex: 1,
929
+ borderRadius: 5,
930
+ paddingHorizontal: 12,
931
+ justifyContent: "center"
932
+ },
933
+ tool: {
934
+ borderWidth: 1,
935
+ paddingHorizontal: 12,
936
+ minWidth: 64,
937
+ justifyContent: "center",
938
+ alignItems: "center"
939
+ },
940
+ toolFirst: { borderTopLeftRadius: 5, borderBottomLeftRadius: 5 },
941
+ toolLast: { borderTopRightRadius: 5, borderBottomRightRadius: 5, marginLeft: -1 },
942
+ reader: { flex: 1, minHeight: 0, borderRadius: 5, overflow: "hidden" },
943
+ empty: { flex: 1, gap: 12, justifyContent: "center", alignItems: "center" }
944
+ });
945
+
946
+ // src/AppMarkdown.tsx
947
+ import React6 from "react";
948
+ import { jsx as jsx6 } from "react/jsx-runtime";
949
+ var AppMarkdown = React6.memo(function AppMarkdown2({
950
+ children,
951
+ variant = "body",
952
+ documentKey
953
+ }) {
954
+ const instanceKey = React6.useId();
955
+ return /* @__PURE__ */ jsx6(
956
+ MarkdownView,
957
+ {
958
+ documentKey: documentKey ?? `app-markdown:${instanceKey}`,
959
+ markdown: children,
960
+ variant,
961
+ surface: "app",
962
+ scrollMode: "intrinsic"
963
+ }
964
+ );
965
+ });
966
+
967
+ // src/AppMarkdownSections.tsx
968
+ import React7, { useCallback as useCallback5, useMemo as useMemo4, useRef as useRef5, useState as useState3 } from "react";
969
+ import { View as View4 } from "react-native";
970
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
971
+ var EMPTY_HEADER_HEIGHTS = {};
972
+ var AppMarkdownSections = React7.memo(function AppMarkdownSections2({
973
+ documentKey,
974
+ sections,
975
+ variant = "body",
976
+ leadingWidth = 0,
977
+ columns = 1,
978
+ style
979
+ }) {
980
+ const activeDocumentKey = useRef5(documentKey);
981
+ activeDocumentKey.current = documentKey;
982
+ const [measurement, setMeasurement] = useState3(null);
983
+ const [headerMeasurement, setHeaderMeasurement] = useState3({ documentKey, heights: {} });
984
+ const headerHeights = headerMeasurement.documentKey === documentKey ? headerMeasurement.heights : EMPTY_HEADER_HEIGHTS;
985
+ const blocks = useMemo4(
986
+ () => sections.map((section) => ({
987
+ id: section.id,
988
+ markdown: section.markdown,
989
+ variant: section.variant ?? variant,
990
+ beforeSpace: Math.max(0, section.headerHeight ?? 0, headerHeights[section.id] ?? 0),
991
+ fragments: section.fragments
992
+ })),
993
+ [sections, variant, headerHeights]
994
+ );
995
+ const onBlockMetrics = useCallback5(
996
+ (metrics2) => {
997
+ if (activeDocumentKey.current !== documentKey) return;
998
+ setMeasurement((current) => {
999
+ if (current?.documentKey === documentKey && current.metrics.length === metrics2.length && current.metrics.every(
1000
+ (value, index) => value.id === metrics2[index].id && value.top === metrics2[index].top && value.height === metrics2[index].height && value.left === metrics2[index].left && value.width === metrics2[index].width
1001
+ )) {
1002
+ return current;
1003
+ }
1004
+ return { documentKey, metrics: metrics2 };
1005
+ });
1006
+ },
1007
+ [documentKey]
1008
+ );
1009
+ const metrics = measurement?.documentKey === documentKey ? measurement.metrics : [];
1010
+ const metricsById = new Map(metrics.map((metric) => [metric.id, metric]));
1011
+ return /* @__PURE__ */ jsxs4(View4, { style, children: [
1012
+ /* @__PURE__ */ jsx7(View4, { style: { marginLeft: leadingWidth }, children: /* @__PURE__ */ jsx7(
1013
+ MarkdownBlocksView,
1014
+ {
1015
+ documentKey,
1016
+ blocks,
1017
+ columns,
1018
+ variant,
1019
+ surface: "app",
1020
+ scrollMode: "intrinsic",
1021
+ onBlockMetrics
1022
+ }
1023
+ ) }),
1024
+ /* @__PURE__ */ jsx7(
1025
+ View4,
1026
+ {
1027
+ pointerEvents: "box-none",
1028
+ style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 },
1029
+ children: sections.map((section) => {
1030
+ const metric = metricsById.get(section.id);
1031
+ if (!metric) return null;
1032
+ return /* @__PURE__ */ jsxs4(React7.Fragment, { children: [
1033
+ section.leading && leadingWidth > 0 ? /* @__PURE__ */ jsx7(
1034
+ View4,
1035
+ {
1036
+ testID: `markdown-leading-${section.id}`,
1037
+ style: {
1038
+ position: "absolute",
1039
+ top: metric.top,
1040
+ left: 0,
1041
+ width: leadingWidth,
1042
+ height: metric.height,
1043
+ justifyContent: "center"
1044
+ },
1045
+ children: section.leading
1046
+ }
1047
+ ) : null,
1048
+ section.header ? /* @__PURE__ */ jsx7(
1049
+ View4,
1050
+ {
1051
+ testID: `markdown-header-${section.id}`,
1052
+ onLayout: (event) => {
1053
+ const height = Math.ceil(event.nativeEvent.layout.height);
1054
+ if (!Number.isFinite(height) || height < 0) return;
1055
+ setHeaderMeasurement((current) => {
1056
+ const heights = current.documentKey === documentKey ? current.heights : {};
1057
+ if (heights[section.id] === height) return current;
1058
+ return { documentKey, heights: { ...heights, [section.id]: height } };
1059
+ });
1060
+ },
1061
+ style: {
1062
+ position: "absolute",
1063
+ top: metric.top,
1064
+ left: leadingWidth + (metric.left ?? 0),
1065
+ ...metric.width === void 0 ? { right: 0 } : { width: metric.width },
1066
+ minHeight: section.headerHeight,
1067
+ justifyContent: "center"
1068
+ },
1069
+ children: section.header
1070
+ }
1071
+ ) : null
1072
+ ] }, section.id);
1073
+ })
1074
+ }
1075
+ )
1076
+ ] });
1077
+ });
1078
+ export {
1079
+ AppMarkdown,
1080
+ AppMarkdownSections,
1081
+ MarkdownBlocksView,
1082
+ MarkdownDocumentView,
1083
+ MarkdownView,
1084
+ NotebookPage,
1085
+ NotebookReaderFrame,
1086
+ ReaderHost,
1087
+ cleanupNotebookImportCopy,
1088
+ isRecord,
1089
+ notebookNativeStorage,
1090
+ pickNotebookMarkdownFiles,
1091
+ readNotebookMarkdownImport,
1092
+ useMarkdownTheme
1093
+ };
1094
+ //# sourceMappingURL=index.js.map