@medway-ui/native 1.2.0 → 1.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.
Files changed (33) hide show
  1. package/lib/commonjs/editor/NoteEditor.js +710 -0
  2. package/lib/commonjs/editor/NoteEditor.js.map +1 -0
  3. package/lib/commonjs/editor/editorHtml.generated.js +21 -0
  4. package/lib/commonjs/editor/editorHtml.generated.js.map +1 -0
  5. package/lib/commonjs/editor/index.js +33 -0
  6. package/lib/commonjs/editor/index.js.map +1 -0
  7. package/lib/commonjs/editor/protocol.js +88 -0
  8. package/lib/commonjs/editor/protocol.js.map +1 -0
  9. package/lib/module/editor/NoteEditor.js +705 -0
  10. package/lib/module/editor/NoteEditor.js.map +1 -0
  11. package/lib/module/editor/editorHtml.generated.js +17 -0
  12. package/lib/module/editor/editorHtml.generated.js.map +1 -0
  13. package/lib/module/editor/index.js +5 -0
  14. package/lib/module/editor/index.js.map +1 -0
  15. package/lib/module/editor/protocol.js +83 -0
  16. package/lib/module/editor/protocol.js.map +1 -0
  17. package/lib/module/index.js +6 -0
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/typescript/editor/NoteEditor.d.ts +82 -0
  20. package/lib/typescript/editor/NoteEditor.d.ts.map +1 -0
  21. package/lib/typescript/editor/editorHtml.generated.d.ts +13 -0
  22. package/lib/typescript/editor/editorHtml.generated.d.ts.map +1 -0
  23. package/lib/typescript/editor/index.d.ts +6 -0
  24. package/lib/typescript/editor/index.d.ts.map +1 -0
  25. package/lib/typescript/editor/protocol.d.ts +153 -0
  26. package/lib/typescript/editor/protocol.d.ts.map +1 -0
  27. package/package.json +24 -1
  28. package/setup/metro.js +27 -5
  29. package/src/editor/NoteEditor.tsx +802 -0
  30. package/src/editor/editorHtml.generated.ts +14 -0
  31. package/src/editor/index.ts +13 -0
  32. package/src/editor/protocol.ts +184 -0
  33. package/src/index.ts +6 -0
@@ -0,0 +1,710 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = require("react");
8
+ var _reactNative = require("react-native");
9
+ var _reactNativeWebview = _interopRequireDefault(require("react-native-webview"));
10
+ var _nativewind = require("nativewind");
11
+ var _editorHtmlGenerated = require("./editorHtml.generated.js");
12
+ var _notesCore = require("@medway-ui/notes-core");
13
+ var _native = require("@medway-ui/icons/native");
14
+ var _Text = require("@/components/Text");
15
+ var _Button = require("@/components/Button");
16
+ var _Input = require("@/components/Input");
17
+ var _Dialog = require("@/components/Dialog");
18
+ var _useThemeColor = require("@/hooks/useThemeColor");
19
+ var _utils = require("@/lib/utils");
20
+ var _protocol = require("./protocol.js");
21
+ var _jsxRuntime = require("react/jsx-runtime");
22
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
23
+ /**
24
+ * The ref surface of `react-native-webview`, kept local so consumers need not
25
+ * depend on internal WebView types. Internal only — `NoteEditorHandle` below is
26
+ * the public ref shape.
27
+ */
28
+
29
+ /**
30
+ * Retyped locally: the library's own `WebView` resolves to `Component<never>`
31
+ * under `strict` and rejects every JSX prop.
32
+ */
33
+ const WebView = _reactNativeWebview.default;
34
+
35
+ /**
36
+ * What `getSaveData` returns. No `html` field, unlike web: generating HTML needs
37
+ * a DOM, which never exists outside the WebView here. Persist `json` and render
38
+ * HTML backend-side if needed.
39
+ */
40
+
41
+ /**
42
+ * Same shape as the web `NoteEditor`'s handle, minus `editor` (no in-process
43
+ * Tiptap instance to hand out) and with the document getters async, since
44
+ * reading the document is a bridge round trip here.
45
+ */
46
+
47
+ /** The title is single-line; newlines can still arrive via paste. */
48
+ const sanitizeTitle = value => value.replace(/\r?\n/g, " ");
49
+ const DEFAULT_IMAGE_DELETE_GRACE_MS = 4000;
50
+ let requestCounter = 0;
51
+ /** Correlates a `getDocument` request with the `document` response that answers it. */
52
+ const nextRequestId = () => {
53
+ requestCounter += 1;
54
+ return `req_${requestCounter}_${Date.now()}`;
55
+ };
56
+
57
+ /**
58
+ * Resolves semantic design-system colors into a raw `EditorTheme`. Sent to the
59
+ * WebView on mount and on every color-scheme change, so a host never has to
60
+ * manage editor theming itself.
61
+ */
62
+ const useNoteEditorTheme = () => {
63
+ const {
64
+ colorScheme
65
+ } = (0, _nativewind.useColorScheme)();
66
+ const background = (0, _useThemeColor.useThemeColor)("surface-card");
67
+ const text = (0, _useThemeColor.useThemeColor)("content-paragraph");
68
+ const heading = (0, _useThemeColor.useThemeColor)("content-title");
69
+ const muted = (0, _useThemeColor.useThemeColor)("gray-foreground-muted");
70
+ const border = (0, _useThemeColor.useThemeColor)("gray-stroke-muted");
71
+ const accent = (0, _useThemeColor.useThemeColor)("primary-foreground");
72
+ const codeBackground = (0, _useThemeColor.useThemeColor)("gray-background-soft");
73
+ return (0, _react.useMemo)(() => ({
74
+ colorScheme: colorScheme === "dark" ? "dark" : "light",
75
+ background,
76
+ text,
77
+ heading,
78
+ muted,
79
+ placeholder: muted,
80
+ border,
81
+ accent,
82
+ caret: accent,
83
+ selection: border,
84
+ codeBackground,
85
+ codeText: text,
86
+ // Approximates the web surface's `text-md`/`leading-md`; not pixel-exact.
87
+ fontSize: 15,
88
+ lineHeight: 22,
89
+ paddingHorizontal: 16,
90
+ paddingTop: 12,
91
+ paddingBottom: 32
92
+ }), [colorScheme, background, text, heading, muted, border, accent, codeBackground]);
93
+ };
94
+
95
+ /** Every icon exported from `@medway-ui/icons/native` shares this component shape. */
96
+
97
+ /**
98
+ * Same commands, grouping and order as the web `NoteEditor`'s `TOOLBAR_GROUPS`.
99
+ * `link` and `undo`/`redo` are filled in per-instance: they need the live
100
+ * `EditorSelectionState` and a dialog opener that only exist in the component.
101
+ */
102
+ const buildToolbarGroups = (state, openLinkDialog) => [[{
103
+ id: "bold",
104
+ label: "Negrito",
105
+ Icon: _native.Bold,
106
+ command: {
107
+ name: "toggleBold"
108
+ },
109
+ activeKey: "bold"
110
+ }, {
111
+ id: "italic",
112
+ label: "Itálico",
113
+ Icon: _native.Italic,
114
+ command: {
115
+ name: "toggleItalic"
116
+ },
117
+ activeKey: "italic"
118
+ }, {
119
+ id: "underline",
120
+ label: "Sublinhado",
121
+ Icon: _native.Underline,
122
+ command: {
123
+ name: "toggleUnderline"
124
+ },
125
+ activeKey: "underline"
126
+ }, {
127
+ id: "strike",
128
+ label: "Tachado",
129
+ Icon: _native.Strikethrough,
130
+ command: {
131
+ name: "toggleStrike"
132
+ },
133
+ activeKey: "strike"
134
+ }, {
135
+ id: "code",
136
+ label: "Código",
137
+ Icon: _native.Code,
138
+ command: {
139
+ name: "toggleCode"
140
+ },
141
+ activeKey: "code"
142
+ }], [{
143
+ id: "heading2",
144
+ label: "Título 2",
145
+ Icon: _native.HeadingH2,
146
+ command: {
147
+ name: "toggleHeading",
148
+ level: 2
149
+ },
150
+ activeKey: "heading2"
151
+ }, {
152
+ id: "heading3",
153
+ label: "Título 3",
154
+ Icon: _native.HeadingH3,
155
+ command: {
156
+ name: "toggleHeading",
157
+ level: 3
158
+ },
159
+ activeKey: "heading3"
160
+ }], [{
161
+ id: "bulletList",
162
+ label: "Lista com marcadores",
163
+ Icon: _native.ListUnordered,
164
+ command: {
165
+ name: "toggleBulletList"
166
+ },
167
+ activeKey: "bulletList"
168
+ }, {
169
+ id: "orderedList",
170
+ label: "Lista numerada",
171
+ Icon: _native.ListOrdered,
172
+ command: {
173
+ name: "toggleOrderedList"
174
+ },
175
+ activeKey: "orderedList"
176
+ }, {
177
+ id: "blockquote",
178
+ label: "Citação",
179
+ Icon: _native.DoubleQuotesL,
180
+ command: {
181
+ name: "toggleBlockquote"
182
+ },
183
+ activeKey: "blockquote"
184
+ }, {
185
+ id: "codeBlock",
186
+ label: "Bloco de código",
187
+ Icon: _native.WindowCodeBlock,
188
+ command: {
189
+ name: "toggleCodeBlock"
190
+ },
191
+ activeKey: "codeBlock"
192
+ }], [{
193
+ id: "link",
194
+ label: "Inserir link",
195
+ Icon: _native.Link,
196
+ activeKey: "link",
197
+ onPress: openLinkDialog
198
+ }], [{
199
+ id: "undo",
200
+ label: "Desfazer",
201
+ Icon: _native.ArrowUndoUpLeft,
202
+ command: {
203
+ name: "undo"
204
+ },
205
+ disabled: s => !s.canUndo
206
+ }, {
207
+ id: "redo",
208
+ label: "Refazer",
209
+ Icon: _native.Redo,
210
+ command: {
211
+ name: "redo"
212
+ },
213
+ disabled: s => !s.canRedo
214
+ }]];
215
+
216
+ /**
217
+ * Unlike every other command — a pure ProseMirror op — "Imagem" needs a file
218
+ * first, so it is only offered when the host gave us a way to resolve one.
219
+ * Kept local because `notes-core` cannot know what a given host can do.
220
+ */
221
+ const visibleSlashCommandIds = (commandIds, canUploadImage) => canUploadImage ? commandIds : commandIds.filter(id => id !== "image");
222
+ const NoteEditor = /*#__PURE__*/(0, _react.forwardRef)(({
223
+ document: initialDocument,
224
+ initialTitle,
225
+ titlePlaceholder = "Title",
226
+ onChange,
227
+ editable = true,
228
+ autofocus = false,
229
+ toolbar = false,
230
+ onImageUpload,
231
+ onImageRemoved,
232
+ imageDeleteGraceMs = DEFAULT_IMAGE_DELETE_GRACE_MS,
233
+ onLinkPress,
234
+ height,
235
+ backgroundColor,
236
+ testID
237
+ }, ref) => {
238
+ const webViewRef = (0, _react.useRef)(null);
239
+ const isEmbedded = height !== undefined;
240
+ const titlePlaceholderColor = (0, _useThemeColor.useThemeColor)("gray-foreground-muted");
241
+ const [isReady, setIsReady] = (0, _react.useState)(false);
242
+ const [selectionState, setSelectionState] = (0, _react.useState)(_notesCore.INITIAL_EDITOR_STATE);
243
+ const [slashState, setSlashState] = (0, _react.useState)(_notesCore.EMPTY_SLASH_STATE);
244
+ const [contentHeight, setContentHeight] = (0, _react.useState)(height);
245
+ const [title, setTitle] = (0, _react.useState)(() => sanitizeTitle(initialTitle ?? ""));
246
+ const titleInputRef = (0, _react.useRef)(null);
247
+ const [titleSelection, setTitleSelection] = (0, _react.useState)(undefined);
248
+ const autofocusTitleRef = (0, _react.useRef)(Boolean(autofocus) && title.length === 0);
249
+ (0, _react.useEffect)(() => {
250
+ if (autofocusTitleRef.current) titleInputRef.current?.focus();
251
+ // Mount-only: the one-time "new note" autofocus decision.
252
+ // eslint-disable-next-line react-hooks/exhaustive-deps
253
+ }, []);
254
+ const focusTitleAtEnd = (0, _react.useCallback)(() => {
255
+ titleInputRef.current?.focus();
256
+ setTitleSelection({
257
+ start: title.length,
258
+ end: title.length
259
+ });
260
+ }, [title.length]);
261
+
262
+ // Internal from here on: a host never sends or reads a bridge message
263
+ // directly. See `./protocol.ts`.
264
+ const send = (0, _react.useCallback)(message => {
265
+ webViewRef.current?.injectJavaScript(`window.${_protocol.EDITOR_RECEIVE_HANDLER}(${JSON.stringify(JSON.stringify(message))}); true;`);
266
+ }, []);
267
+ const pendingDocumentRequests = (0, _react.useRef)(new Map());
268
+ const requestDocument = (0, _react.useCallback)(() => {
269
+ const requestId = nextRequestId();
270
+ return new Promise((resolve, reject) => {
271
+ pendingDocumentRequests.current.set(requestId, {
272
+ resolve,
273
+ reject
274
+ });
275
+ send({
276
+ type: "getDocument",
277
+ requestId
278
+ });
279
+ setTimeout(() => {
280
+ if (!pendingDocumentRequests.current.has(requestId)) return;
281
+ pendingDocumentRequests.current.delete(requestId);
282
+ reject(new Error("NoteEditor: timed out waiting for the WebView bundle to respond"));
283
+ }, 5000);
284
+ });
285
+ }, [send]);
286
+ const onImageUploadRef = (0, _react.useRef)(onImageUpload);
287
+ const onImageRemovedRef = (0, _react.useRef)(onImageRemoved);
288
+ const onChangeRef = (0, _react.useRef)(onChange);
289
+ const onLinkPressRef = (0, _react.useRef)(onLinkPress);
290
+ (0, _react.useEffect)(() => {
291
+ onImageUploadRef.current = onImageUpload;
292
+ }, [onImageUpload]);
293
+ (0, _react.useEffect)(() => {
294
+ onImageRemovedRef.current = onImageRemoved;
295
+ }, [onImageRemoved]);
296
+ (0, _react.useEffect)(() => {
297
+ onChangeRef.current = onChange;
298
+ }, [onChange]);
299
+ (0, _react.useEffect)(() => {
300
+ onLinkPressRef.current = onLinkPress;
301
+ }, [onLinkPress]);
302
+ const insertImage = (0, _react.useCallback)(async () => {
303
+ const upload = onImageUploadRef.current;
304
+ if (!upload) return;
305
+ try {
306
+ const result = await upload();
307
+ if (!result) return;
308
+ send({
309
+ type: "insertImage",
310
+ src: result.src,
311
+ alt: result.alt ?? null
312
+ });
313
+ } catch (error) {
314
+ // eslint-disable-next-line no-console
315
+ console.error("@medway-ui/native NoteEditor: onImageUpload failed", error);
316
+ }
317
+ }, [send]);
318
+ const handleMessage = (0, _react.useCallback)(event => {
319
+ let message;
320
+ try {
321
+ message = JSON.parse(event.nativeEvent.data);
322
+ } catch {
323
+ return;
324
+ }
325
+ if (!(0, _protocol.isEditorMessage)(message)) return;
326
+ const editorMessage = message;
327
+ switch (editorMessage.type) {
328
+ case "ready":
329
+ // A protocol mismatch means a stale `editorHtml.generated.ts`.
330
+ // Refused loudly rather than letting commands quietly not match
331
+ // what the bundle understands.
332
+ if (editorMessage.protocol !== _protocol.EDITOR_PROTOCOL_VERSION) {
333
+ // eslint-disable-next-line no-console
334
+ console.error(`@medway-ui/native NoteEditor: editor bundle reports protocol ${editorMessage.protocol}, expected ${_protocol.EDITOR_PROTOCOL_VERSION}. Rebuild editor-bundle (its generated HTML is stale).`);
335
+ return;
336
+ }
337
+ setIsReady(true);
338
+ return;
339
+ case "state":
340
+ setSelectionState(editorMessage.state);
341
+ return;
342
+ case "slash":
343
+ setSlashState(editorMessage.slash);
344
+ return;
345
+ case "change":
346
+ // Already debounced by the bundle (`CHANGE_DEBOUNCE_MS`). The extra
347
+ // round trip happens only when a host asked for `onChange`, which
348
+ // keeps serialization per-settled-keystroke, not per-keystroke.
349
+ if (onChangeRef.current) {
350
+ requestDocument().then(doc => onChangeRef.current?.(doc)).catch(() => {});
351
+ }
352
+ return;
353
+ case "document":
354
+ {
355
+ const pending = pendingDocumentRequests.current.get(editorMessage.requestId);
356
+ if (!pending) return;
357
+ pendingDocumentRequests.current.delete(editorMessage.requestId);
358
+ // `isEditorMessage` only established that `doc` is an object. The
359
+ // whitelist/depth pass is what makes it an `EditorDocument`, and it
360
+ // runs on the way out too, not just on `setContent` — the WebView
361
+ // renders student content, so what it hands back is untrusted.
362
+ pending.resolve((0, _notesCore.normalizeDocument)(editorMessage.doc));
363
+ return;
364
+ }
365
+ case "resize":
366
+ if (isEmbedded) setContentHeight(editorMessage.height);
367
+ return;
368
+ case "linkPress":
369
+ onLinkPressRef.current?.(editorMessage.href);
370
+ return;
371
+ case "backspaceAtDocStart":
372
+ focusTitleAtEnd();
373
+ return;
374
+ case "imageRemoved":
375
+ onImageRemovedRef.current?.(editorMessage.src);
376
+ return;
377
+ case "error":
378
+ // eslint-disable-next-line no-console
379
+ console.error("@medway-ui/native NoteEditor:", editorMessage.message);
380
+ return;
381
+ default:
382
+ return;
383
+ }
384
+ }, [focusTitleAtEnd, isEmbedded, requestDocument]);
385
+
386
+ // Fires once, when the bundle reports `ready`: the initial document, layout
387
+ // mode and image-delete grace period are all read-once.
388
+ (0, _react.useEffect)(() => {
389
+ if (!isReady) return;
390
+ send({
391
+ type: "setContent",
392
+ doc: initialDocument ?? _notesCore.EMPTY_DOCUMENT,
393
+ editable,
394
+ focus: autofocusTitleRef.current ? false : Boolean(autofocus),
395
+ focusPosition: typeof autofocus === "string" ? autofocus : undefined,
396
+ imageDeleteGraceMs
397
+ });
398
+ if (isEmbedded) send({
399
+ type: "setLayout",
400
+ embedded: true
401
+ });
402
+ // `editable`/`autofocus`/`imageDeleteGraceMs` are excluded from deps on
403
+ // purpose: they seed the *initial* editor, like `document`.
404
+ // eslint-disable-next-line react-hooks/exhaustive-deps
405
+ }, [isReady, send]);
406
+
407
+ // `editable` is the one prop that stays reactive after mount.
408
+ (0, _react.useEffect)(() => {
409
+ if (!isReady) return;
410
+ send({
411
+ type: "setEditable",
412
+ editable
413
+ });
414
+ }, [isReady, editable, send]);
415
+ const theme = useNoteEditorTheme();
416
+ (0, _react.useEffect)(() => {
417
+ if (!isReady) return;
418
+ send({
419
+ type: "setTheme",
420
+ theme
421
+ });
422
+ }, [isReady, theme, send]);
423
+ (0, _react.useImperativeHandle)(ref, () => ({
424
+ focus: position => send({
425
+ type: "focus",
426
+ position: position ?? "end"
427
+ }),
428
+ blur: () => send({
429
+ type: "blur"
430
+ }),
431
+ getDocument: () => requestDocument(),
432
+ getSaveData: async () => ({
433
+ title,
434
+ json: await requestDocument()
435
+ }),
436
+ setDocument: (doc, options) => send({
437
+ type: "setContent",
438
+ doc,
439
+ editable,
440
+ focus: Boolean(options?.focus),
441
+ reset: false
442
+ })
443
+ }), [title, editable, requestDocument, send]);
444
+ const [linkDialogOpen, setLinkDialogOpen] = (0, _react.useState)(false);
445
+ const [linkValue, setLinkValue] = (0, _react.useState)("");
446
+ const openLinkDialog = (0, _react.useCallback)(() => {
447
+ setLinkValue(selectionState.linkHref ?? "");
448
+ setLinkDialogOpen(true);
449
+ }, [selectionState.linkHref]);
450
+ const confirmLink = (0, _react.useCallback)(() => {
451
+ const href = linkValue.trim();
452
+ send(href ? {
453
+ type: "command",
454
+ command: {
455
+ name: "setLink",
456
+ href
457
+ }
458
+ } : {
459
+ type: "command",
460
+ command: {
461
+ name: "unsetLink"
462
+ }
463
+ });
464
+ setLinkDialogOpen(false);
465
+ }, [linkValue, send]);
466
+ const runToolbarCommand = (0, _react.useCallback)(command => send({
467
+ type: "command",
468
+ command
469
+ }), [send]);
470
+ const toolbarGroups = (0, _react.useMemo)(() => buildToolbarGroups(selectionState, openLinkDialog), [selectionState, openLinkDialog]);
471
+ const handleSlashSelect = (0, _react.useCallback)(id => {
472
+ send({
473
+ type: "runSlashCommand",
474
+ commandId: id
475
+ });
476
+ if (id === "image") insertImage();
477
+ }, [send, insertImage]);
478
+ const containerHeight = isEmbedded ? contentHeight : undefined;
479
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
480
+ style: [isEmbedded ? styles.containerEmbedded : styles.container, isEmbedded ? {
481
+ height: containerHeight
482
+ } : null, backgroundColor ? {
483
+ backgroundColor
484
+ } : null],
485
+ children: [toolbar && /*#__PURE__*/(0, _jsxRuntime.jsx)(NoteEditorToolbar, {
486
+ groups: toolbarGroups,
487
+ state: selectionState,
488
+ onCommand: runToolbarCommand,
489
+ onImagePress: onImageUpload ? insertImage : undefined
490
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
491
+ ref: titleInputRef,
492
+ multiline: true,
493
+ value: title,
494
+ onChangeText: value => setTitle(sanitizeTitle(value)),
495
+ onKeyPress: event => {
496
+ // Enter moves focus into the content's first block, never inserts
497
+ // a line break; `sanitizeTitle` is the backstop if the platform
498
+ // commits the `\n` before this handler runs.
499
+ if (event.nativeEvent.key !== "Enter") return;
500
+ send({
501
+ type: "focus",
502
+ position: "start"
503
+ });
504
+ },
505
+ selection: titleSelection,
506
+ onSelectionChange: () => {
507
+ if (titleSelection) setTitleSelection(undefined);
508
+ },
509
+ placeholder: titlePlaceholder,
510
+ placeholderTextColor: titlePlaceholderColor,
511
+ editable: editable,
512
+ testID: testID ? `${testID}-title` : undefined,
513
+ accessibilityLabel: "T\xEDtulo da anota\xE7\xE3o",
514
+ className: "mb-1 p-0 text-title-lg font-bold leading-title-lg text-content-title"
515
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
516
+ style: {
517
+ flex: isEmbedded ? undefined : 1
518
+ },
519
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(WebView, {
520
+ ref: webViewRef,
521
+ testID: testID,
522
+ source: {
523
+ html: _editorHtmlGenerated.EDITOR_HTML
524
+ }
525
+ // Anchored: `react-native-webview` treats each entry as an
526
+ // unanchored pattern, so a bare "about:blank" also admits
527
+ // "about:blankse.evil.com".
528
+ ,
529
+ originWhitelist: ["^about:blank$"],
530
+ onMessage: handleMessage
531
+ // Links already leave the editor via the `linkPress` message, so
532
+ // nothing rendered here needs to navigate. Allowing only the
533
+ // initial `about:blank` load closes off `data:`/script-driven
534
+ // navigation, which would otherwise bypass the `<a>` click handler
535
+ // in `editor-bundle/src/main.ts`.
536
+ ,
537
+ onShouldStartLoadWithRequest: ({
538
+ url
539
+ }) => !url || url === "about:blank",
540
+ javaScriptEnabled: true,
541
+ domStorageEnabled: false,
542
+ allowFileAccess: false,
543
+ allowFileAccessFromFileURLs: false,
544
+ allowUniversalAccessFromFileURLs: false,
545
+ allowsLinkPreview: false,
546
+ setSupportMultipleWindows: false,
547
+ mixedContentMode: "never",
548
+ cacheEnabled: false,
549
+ hideKeyboardAccessoryView: true,
550
+ keyboardDisplayRequiresUserAction: false,
551
+ automaticallyAdjustContentInsets: false,
552
+ contentInsetAdjustmentBehavior: "never",
553
+ textInteractionEnabled: true
554
+ // Embedded, the native list owns the scroll: two nested scrollers
555
+ // put the caret in a scroll view ProseMirror was never told about.
556
+ ,
557
+ scrollEnabled: !isEmbedded,
558
+ nestedScrollEnabled: false,
559
+ androidLayerType: _reactNative.Platform.OS === "android" ? "hardware" : undefined,
560
+ style: [styles.webView, isEmbedded ? {
561
+ flex: undefined,
562
+ height: containerHeight
563
+ } : null, backgroundColor ? {
564
+ backgroundColor
565
+ } : null]
566
+ }), slashState.active && slashState.caret ? /*#__PURE__*/(0, _jsxRuntime.jsx)(NoteEditorSlashMenu, {
567
+ slash: slashState,
568
+ canUploadImage: Boolean(onImageUpload),
569
+ onSelect: handleSlashSelect
570
+ }) : null]
571
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Dialog.Dialog, {
572
+ open: linkDialogOpen,
573
+ onOpenChange: setLinkDialogOpen,
574
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_Dialog.DialogContainer, {
575
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_Dialog.DialogHeader, {
576
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Dialog.DialogTitle, {
577
+ children: "Inserir link"
578
+ })
579
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Dialog.DialogContent, {
580
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Input.Input, {
581
+ value: linkValue,
582
+ onChangeText: setLinkValue,
583
+ placeholder: "https://",
584
+ label: "URL",
585
+ autoCapitalize: "none",
586
+ autoCorrect: false,
587
+ keyboardType: "url"
588
+ })
589
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Dialog.DialogFooter, {
590
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Button.Button, {
591
+ onPress: confirmLink,
592
+ children: "Confirmar"
593
+ })
594
+ })]
595
+ })
596
+ })]
597
+ });
598
+ });
599
+ NoteEditor.displayName = "NoteEditor";
600
+ function NoteEditorToolbar({
601
+ groups,
602
+ state,
603
+ onCommand,
604
+ onImagePress
605
+ }) {
606
+ const iconColor = (0, _useThemeColor.useThemeColor)("gray-foreground");
607
+ const activeIconColor = (0, _useThemeColor.useThemeColor)("primary-foreground");
608
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
609
+ className: "mb-2 flex-row flex-wrap items-center gap-0.5 border-b border-gray-stroke-muted pb-2",
610
+ children: [groups.map((group, groupIndex) => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
611
+ className: "flex-row items-center gap-0.5",
612
+ children: [groupIndex > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
613
+ className: "mx-1 h-5 w-px bg-gray-stroke-muted"
614
+ }) : null, group.map(item => {
615
+ const active = item.activeKey ? state.active[item.activeKey] : false;
616
+ const disabled = item.disabled?.(state) ?? false;
617
+ const Icon = item.Icon;
618
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
619
+ accessibilityRole: "button",
620
+ accessibilityLabel: item.label,
621
+ accessibilityState: {
622
+ disabled,
623
+ selected: active
624
+ },
625
+ disabled: disabled,
626
+ onPress: item.onPress ?? (() => item.command && onCommand(item.command)),
627
+ className: (0, _utils.cn)("h-8 w-8 items-center justify-center rounded-md", disabled && "opacity-40", active && "bg-primary-background-soft"),
628
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(Icon, {
629
+ width: 16,
630
+ height: 16,
631
+ color: active ? activeIconColor : iconColor
632
+ })
633
+ }, item.id);
634
+ })]
635
+ }, groupIndex)), onImagePress ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
636
+ className: "flex-row items-center gap-0.5",
637
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
638
+ className: "mx-1 h-5 w-px bg-gray-stroke-muted"
639
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
640
+ accessibilityRole: "button",
641
+ accessibilityLabel: "Inserir imagem",
642
+ onPress: onImagePress,
643
+ className: "h-8 w-8 items-center justify-center rounded-md",
644
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_native.Image, {
645
+ width: 16,
646
+ height: 16,
647
+ color: iconColor
648
+ })
649
+ })]
650
+ }) : null]
651
+ });
652
+ }
653
+
654
+ /**
655
+ * Positioned from the `caret` rect the bundle reports. No arrow-key navigation:
656
+ * a touch UI has no keyboard focus to drive one, so every item is tappable.
657
+ *
658
+ * `caret` is in CSS pixels inside the WebView viewport, treated here as `dp` —
659
+ * valid as long as the WebView isn't scaled.
660
+ */
661
+ function NoteEditorSlashMenu({
662
+ slash,
663
+ canUploadImage,
664
+ onSelect
665
+ }) {
666
+ if (!slash.caret) return null;
667
+ const commandIds = visibleSlashCommandIds(slash.commandIds, canUploadImage);
668
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
669
+ className: "absolute z-50 max-h-72 w-64 overflow-hidden rounded-md border border-gray-stroke-muted bg-surface-elevated p-1 shadow-lg",
670
+ style: {
671
+ left: slash.caret.x,
672
+ top: slash.caret.y + slash.caret.height + 6
673
+ },
674
+ children: commandIds.length === 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Text.Text, {
675
+ className: "px-3 py-2 text-gray-foreground-muted",
676
+ children: "Nenhum resultado"
677
+ }) : commandIds.map(id => {
678
+ const command = (0, _notesCore.getSlashCommand)(id);
679
+ if (!command) return null;
680
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
681
+ accessibilityRole: "button",
682
+ onPress: () => onSelect(id),
683
+ className: "rounded-sm px-3 py-1.5 active:bg-secondary-background-soft-emphasis",
684
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_Text.Text, {
685
+ className: "font-medium text-gray-foreground",
686
+ children: command.label
687
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Text.Text, {
688
+ className: "text-xs text-gray-foreground-muted",
689
+ children: command.description
690
+ })]
691
+ }, id);
692
+ })
693
+ });
694
+ }
695
+ const styles = _reactNative.StyleSheet.create({
696
+ container: {
697
+ flex: 1
698
+ },
699
+ containerEmbedded: {
700
+ flex: 0
701
+ },
702
+ // `opacity` below 1 forces the WebView onto its own layer, which stops the
703
+ // white flash Android paints before the theme reaches the document.
704
+ webView: {
705
+ flex: 1,
706
+ opacity: 0.99
707
+ }
708
+ });
709
+ var _default = exports.default = /*#__PURE__*/(0, _react.memo)(NoteEditor);
710
+ //# sourceMappingURL=NoteEditor.js.map