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