@medway-ui/native 1.9.2 → 1.10.1

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 (297) hide show
  1. package/README.md +2 -2
  2. package/lib/commonjs/components/Alert.js +36 -25
  3. package/lib/commonjs/components/Alert.js.map +1 -1
  4. package/lib/commonjs/components/AlertDialog.js +22 -23
  5. package/lib/commonjs/components/AlertDialog.js.map +1 -1
  6. package/lib/commonjs/components/Avatar.js +4 -4
  7. package/lib/commonjs/components/Badge.js +22 -15
  8. package/lib/commonjs/components/Badge.js.map +1 -1
  9. package/lib/commonjs/components/Button.js +56 -42
  10. package/lib/commonjs/components/Button.js.map +1 -1
  11. package/lib/commonjs/components/CardNews.js +18 -13
  12. package/lib/commonjs/components/CardNews.js.map +1 -1
  13. package/lib/commonjs/components/Carousel.js +12 -8
  14. package/lib/commonjs/components/Carousel.js.map +1 -1
  15. package/lib/commonjs/components/Checkbox.js +5 -5
  16. package/lib/commonjs/components/Checkbox.js.map +1 -1
  17. package/lib/commonjs/components/Chip.js +52 -23
  18. package/lib/commonjs/components/Chip.js.map +1 -1
  19. package/lib/commonjs/components/Choice.js +26 -12
  20. package/lib/commonjs/components/Choice.js.map +1 -1
  21. package/lib/commonjs/components/Dialog.js +16 -8
  22. package/lib/commonjs/components/Dialog.js.map +1 -1
  23. package/lib/commonjs/components/DropdownMenu.js +35 -14
  24. package/lib/commonjs/components/DropdownMenu.js.map +1 -1
  25. package/lib/commonjs/components/EmptyState.js +5 -5
  26. package/lib/commonjs/components/EmptyState.js.map +1 -1
  27. package/lib/commonjs/components/FallbackImage.js +42 -35
  28. package/lib/commonjs/components/FallbackImage.js.map +1 -1
  29. package/lib/commonjs/components/Feed.js +6 -5
  30. package/lib/commonjs/components/Feed.js.map +1 -1
  31. package/lib/commonjs/components/FeedNotice.js +3 -3
  32. package/lib/commonjs/components/Feedback.js +2 -4
  33. package/lib/commonjs/components/Feedback.js.map +1 -1
  34. package/lib/commonjs/components/Filter.js +6 -9
  35. package/lib/commonjs/components/Filter.js.map +1 -1
  36. package/lib/commonjs/components/IndexLevel.js +4 -4
  37. package/lib/commonjs/components/Input.js +25 -20
  38. package/lib/commonjs/components/Input.js.map +1 -1
  39. package/lib/commonjs/components/InvitePopover.js +19 -8
  40. package/lib/commonjs/components/InvitePopover.js.map +1 -1
  41. package/lib/commonjs/components/Label.js +2 -2
  42. package/lib/commonjs/components/LeaderboardCard.js +3 -2
  43. package/lib/commonjs/components/LeaderboardCard.js.map +1 -1
  44. package/lib/commonjs/components/Loading.js +18 -15
  45. package/lib/commonjs/components/Loading.js.map +1 -1
  46. package/lib/commonjs/components/LoadingOverlay.js +36 -43
  47. package/lib/commonjs/components/LoadingOverlay.js.map +1 -1
  48. package/lib/commonjs/components/MemberRow.js +7 -6
  49. package/lib/commonjs/components/MemberRow.js.map +1 -1
  50. package/lib/commonjs/components/PageHeader.js +3 -2
  51. package/lib/commonjs/components/PageHeader.js.map +1 -1
  52. package/lib/commonjs/components/Popover.js +1 -1
  53. package/lib/commonjs/components/PostForm.js +38 -24
  54. package/lib/commonjs/components/PostForm.js.map +1 -1
  55. package/lib/commonjs/components/PrizeCollection.js +5 -5
  56. package/lib/commonjs/components/PrizeCollection.js.map +1 -1
  57. package/lib/commonjs/components/RadioGroup.js +65 -15
  58. package/lib/commonjs/components/RadioGroup.js.map +1 -1
  59. package/lib/commonjs/components/SectionHeader.js +3 -2
  60. package/lib/commonjs/components/SectionHeader.js.map +1 -1
  61. package/lib/commonjs/components/SegmentControl.js +4 -4
  62. package/lib/commonjs/components/Select.js +7 -6
  63. package/lib/commonjs/components/Select.js.map +1 -1
  64. package/lib/commonjs/components/Separator.js +2 -2
  65. package/lib/commonjs/components/Separator.js.map +1 -1
  66. package/lib/commonjs/components/Sheet.js +17 -11
  67. package/lib/commonjs/components/Sheet.js.map +1 -1
  68. package/lib/commonjs/components/Skeleton.js +2 -2
  69. package/lib/commonjs/components/Skeleton.js.map +1 -1
  70. package/lib/commonjs/components/Switch.js +3 -3
  71. package/lib/commonjs/components/Tabs.js +3 -2
  72. package/lib/commonjs/components/Tabs.js.map +1 -1
  73. package/lib/commonjs/components/Tag.js +52 -32
  74. package/lib/commonjs/components/Tag.js.map +1 -1
  75. package/lib/commonjs/components/Text.js +1 -1
  76. package/lib/commonjs/components/ThemedIcon.js +1 -1
  77. package/lib/commonjs/components/Toast.js +12 -12
  78. package/lib/commonjs/components/Toast.js.map +1 -1
  79. package/lib/commonjs/components/Tooltip.js +2 -2
  80. package/lib/commonjs/editor/NoteEditor.js +190 -67
  81. package/lib/commonjs/editor/NoteEditor.js.map +1 -1
  82. package/lib/commonjs/editor/editorHtml.generated.js +1 -1
  83. package/lib/commonjs/editor/editorHtml.generated.js.map +1 -1
  84. package/lib/commonjs/editor/index.js.map +1 -1
  85. package/lib/commonjs/editor/protocol.js +1 -2
  86. package/lib/commonjs/editor/protocol.js.map +1 -1
  87. package/lib/commonjs/hooks/useThemeColor.js +41 -19
  88. package/lib/commonjs/hooks/useThemeColor.js.map +1 -1
  89. package/lib/commonjs/index.js +46 -46
  90. package/lib/commonjs/lib/backdrop.js +18 -0
  91. package/lib/commonjs/lib/backdrop.js.map +1 -0
  92. package/lib/commonjs/lib/overlay-host.js +5 -3
  93. package/lib/commonjs/lib/overlay-host.js.map +1 -1
  94. package/lib/commonjs/lib/raised-surface.js +1 -1
  95. package/lib/module/components/Alert.js +36 -25
  96. package/lib/module/components/Alert.js.map +1 -1
  97. package/lib/module/components/AlertDialog.js +21 -22
  98. package/lib/module/components/AlertDialog.js.map +1 -1
  99. package/lib/module/components/Avatar.js +4 -4
  100. package/lib/module/components/Badge.js +22 -15
  101. package/lib/module/components/Badge.js.map +1 -1
  102. package/lib/module/components/Button.js +56 -42
  103. package/lib/module/components/Button.js.map +1 -1
  104. package/lib/module/components/CardNews.js +18 -13
  105. package/lib/module/components/CardNews.js.map +1 -1
  106. package/lib/module/components/Carousel.js +12 -8
  107. package/lib/module/components/Carousel.js.map +1 -1
  108. package/lib/module/components/Checkbox.js +5 -5
  109. package/lib/module/components/Checkbox.js.map +1 -1
  110. package/lib/module/components/Chip.js +52 -23
  111. package/lib/module/components/Chip.js.map +1 -1
  112. package/lib/module/components/Choice.js +26 -12
  113. package/lib/module/components/Choice.js.map +1 -1
  114. package/lib/module/components/Dialog.js +16 -8
  115. package/lib/module/components/Dialog.js.map +1 -1
  116. package/lib/module/components/DropdownMenu.js +36 -15
  117. package/lib/module/components/DropdownMenu.js.map +1 -1
  118. package/lib/module/components/EmptyState.js +5 -5
  119. package/lib/module/components/EmptyState.js.map +1 -1
  120. package/lib/module/components/FallbackImage.js +42 -35
  121. package/lib/module/components/FallbackImage.js.map +1 -1
  122. package/lib/module/components/Feed.js +6 -5
  123. package/lib/module/components/Feed.js.map +1 -1
  124. package/lib/module/components/FeedNotice.js +3 -3
  125. package/lib/module/components/Feedback.js +2 -4
  126. package/lib/module/components/Feedback.js.map +1 -1
  127. package/lib/module/components/Filter.js +6 -9
  128. package/lib/module/components/Filter.js.map +1 -1
  129. package/lib/module/components/IndexLevel.js +4 -4
  130. package/lib/module/components/Input.js +24 -18
  131. package/lib/module/components/Input.js.map +1 -1
  132. package/lib/module/components/InvitePopover.js +20 -9
  133. package/lib/module/components/InvitePopover.js.map +1 -1
  134. package/lib/module/components/Label.js +2 -2
  135. package/lib/module/components/LeaderboardCard.js +3 -2
  136. package/lib/module/components/LeaderboardCard.js.map +1 -1
  137. package/lib/module/components/Loading.js +18 -15
  138. package/lib/module/components/Loading.js.map +1 -1
  139. package/lib/module/components/LoadingOverlay.js +37 -44
  140. package/lib/module/components/LoadingOverlay.js.map +1 -1
  141. package/lib/module/components/MemberRow.js +7 -6
  142. package/lib/module/components/MemberRow.js.map +1 -1
  143. package/lib/module/components/PageHeader.js +3 -2
  144. package/lib/module/components/PageHeader.js.map +1 -1
  145. package/lib/module/components/Popover.js +1 -1
  146. package/lib/module/components/PostForm.js +38 -24
  147. package/lib/module/components/PostForm.js.map +1 -1
  148. package/lib/module/components/PrizeCollection.js +5 -5
  149. package/lib/module/components/PrizeCollection.js.map +1 -1
  150. package/lib/module/components/RadioGroup.js +65 -15
  151. package/lib/module/components/RadioGroup.js.map +1 -1
  152. package/lib/module/components/SectionHeader.js +3 -2
  153. package/lib/module/components/SectionHeader.js.map +1 -1
  154. package/lib/module/components/SegmentControl.js +4 -4
  155. package/lib/module/components/Select.js +6 -5
  156. package/lib/module/components/Select.js.map +1 -1
  157. package/lib/module/components/Separator.js +2 -2
  158. package/lib/module/components/Separator.js.map +1 -1
  159. package/lib/module/components/Sheet.js +17 -11
  160. package/lib/module/components/Sheet.js.map +1 -1
  161. package/lib/module/components/Skeleton.js +2 -2
  162. package/lib/module/components/Skeleton.js.map +1 -1
  163. package/lib/module/components/Switch.js +3 -3
  164. package/lib/module/components/Tabs.js +3 -2
  165. package/lib/module/components/Tabs.js.map +1 -1
  166. package/lib/module/components/Tag.js +52 -32
  167. package/lib/module/components/Tag.js.map +1 -1
  168. package/lib/module/components/Text.js +1 -1
  169. package/lib/module/components/ThemedIcon.js +1 -1
  170. package/lib/module/components/Toast.js +12 -12
  171. package/lib/module/components/Toast.js.map +1 -1
  172. package/lib/module/components/Tooltip.js +2 -2
  173. package/lib/module/editor/NoteEditor.js +193 -69
  174. package/lib/module/editor/NoteEditor.js.map +1 -1
  175. package/lib/module/editor/editorHtml.generated.js +1 -1
  176. package/lib/module/editor/editorHtml.generated.js.map +1 -1
  177. package/lib/module/editor/index.js.map +1 -1
  178. package/lib/module/editor/protocol.js +1 -2
  179. package/lib/module/editor/protocol.js.map +1 -1
  180. package/lib/module/hooks/useThemeColor.js +41 -19
  181. package/lib/module/hooks/useThemeColor.js.map +1 -1
  182. package/lib/module/index.js +46 -46
  183. package/lib/module/lib/backdrop.js +15 -0
  184. package/lib/module/lib/backdrop.js.map +1 -0
  185. package/lib/module/lib/overlay-host.js +6 -4
  186. package/lib/module/lib/overlay-host.js.map +1 -1
  187. package/lib/module/lib/raised-surface.js +1 -1
  188. package/lib/typescript/components/Alert.d.ts +1 -1
  189. package/lib/typescript/components/Alert.d.ts.map +1 -1
  190. package/lib/typescript/components/AlertDialog.d.ts +1 -1
  191. package/lib/typescript/components/AlertDialog.d.ts.map +1 -1
  192. package/lib/typescript/components/Badge.d.ts.map +1 -1
  193. package/lib/typescript/components/Button.d.ts +1 -0
  194. package/lib/typescript/components/Button.d.ts.map +1 -1
  195. package/lib/typescript/components/CardNews.d.ts +7 -2
  196. package/lib/typescript/components/CardNews.d.ts.map +1 -1
  197. package/lib/typescript/components/Checkbox.d.ts +3 -1
  198. package/lib/typescript/components/Checkbox.d.ts.map +1 -1
  199. package/lib/typescript/components/Chip.d.ts.map +1 -1
  200. package/lib/typescript/components/Choice.d.ts +4 -1
  201. package/lib/typescript/components/Choice.d.ts.map +1 -1
  202. package/lib/typescript/components/Dialog.d.ts +2 -1
  203. package/lib/typescript/components/Dialog.d.ts.map +1 -1
  204. package/lib/typescript/components/DropdownMenu.d.ts +3 -2
  205. package/lib/typescript/components/DropdownMenu.d.ts.map +1 -1
  206. package/lib/typescript/components/FallbackImage.d.ts.map +1 -1
  207. package/lib/typescript/components/Feed.d.ts.map +1 -1
  208. package/lib/typescript/components/Feedback.d.ts.map +1 -1
  209. package/lib/typescript/components/Filter.d.ts.map +1 -1
  210. package/lib/typescript/components/IndexLevel.d.ts +1 -1
  211. package/lib/typescript/components/Input.d.ts +5 -5
  212. package/lib/typescript/components/Input.d.ts.map +1 -1
  213. package/lib/typescript/components/InvitePopover.d.ts +1 -1
  214. package/lib/typescript/components/InvitePopover.d.ts.map +1 -1
  215. package/lib/typescript/components/LeaderboardCard.d.ts.map +1 -1
  216. package/lib/typescript/components/Loading.d.ts +1 -1
  217. package/lib/typescript/components/Loading.d.ts.map +1 -1
  218. package/lib/typescript/components/LoadingOverlay.d.ts +9 -9
  219. package/lib/typescript/components/LoadingOverlay.d.ts.map +1 -1
  220. package/lib/typescript/components/PageHeader.d.ts.map +1 -1
  221. package/lib/typescript/components/PostForm.d.ts.map +1 -1
  222. package/lib/typescript/components/RadioGroup.d.ts +4 -2
  223. package/lib/typescript/components/RadioGroup.d.ts.map +1 -1
  224. package/lib/typescript/components/SectionHeader.d.ts.map +1 -1
  225. package/lib/typescript/components/SegmentControl.d.ts +1 -1
  226. package/lib/typescript/components/Select.d.ts +2 -1
  227. package/lib/typescript/components/Select.d.ts.map +1 -1
  228. package/lib/typescript/components/Separator.d.ts.map +1 -1
  229. package/lib/typescript/components/Sheet.d.ts +2 -1
  230. package/lib/typescript/components/Sheet.d.ts.map +1 -1
  231. package/lib/typescript/components/Tabs.d.ts +4 -3
  232. package/lib/typescript/components/Tabs.d.ts.map +1 -1
  233. package/lib/typescript/components/Tag.d.ts +3 -1
  234. package/lib/typescript/components/Tag.d.ts.map +1 -1
  235. package/lib/typescript/components/ThemedIcon.d.ts +1 -1
  236. package/lib/typescript/components/Toast.d.ts +1 -1
  237. package/lib/typescript/components/Toast.d.ts.map +1 -1
  238. package/lib/typescript/editor/NoteEditor.d.ts +28 -8
  239. package/lib/typescript/editor/NoteEditor.d.ts.map +1 -1
  240. package/lib/typescript/editor/editorHtml.generated.d.ts.map +1 -1
  241. package/lib/typescript/editor/index.d.ts +1 -1
  242. package/lib/typescript/editor/index.d.ts.map +1 -1
  243. package/lib/typescript/editor/protocol.d.ts +4 -14
  244. package/lib/typescript/editor/protocol.d.ts.map +1 -1
  245. package/lib/typescript/hooks/use-toast.d.ts +1 -1
  246. package/lib/typescript/hooks/useThemeColor.d.ts.map +1 -1
  247. package/lib/typescript/index.d.ts +91 -91
  248. package/lib/typescript/index.d.ts.map +1 -1
  249. package/lib/typescript/lib/backdrop.d.ts +4 -0
  250. package/lib/typescript/lib/backdrop.d.ts.map +1 -0
  251. package/lib/typescript/lib/overlay-host.d.ts +1 -0
  252. package/lib/typescript/lib/overlay-host.d.ts.map +1 -1
  253. package/package.json +9 -14
  254. package/setup/tailwind.js +16 -8
  255. package/setup/tokens.native.css +181 -80
  256. package/src/components/Alert.tsx +37 -20
  257. package/src/components/AlertDialog.tsx +18 -19
  258. package/src/components/Badge.tsx +16 -15
  259. package/src/components/Button.tsx +64 -31
  260. package/src/components/CardNews.tsx +17 -6
  261. package/src/components/Carousel.tsx +5 -5
  262. package/src/components/Checkbox.tsx +17 -7
  263. package/src/components/Chip.tsx +49 -21
  264. package/src/components/Choice.tsx +34 -14
  265. package/src/components/Dialog.tsx +15 -5
  266. package/src/components/DropdownMenu.tsx +62 -14
  267. package/src/components/EmptyState.tsx +3 -3
  268. package/src/components/FallbackImage.tsx +32 -23
  269. package/src/components/Feed.tsx +2 -1
  270. package/src/components/Feedback.tsx +0 -1
  271. package/src/components/Filter.tsx +2 -3
  272. package/src/components/Input.tsx +83 -64
  273. package/src/components/InvitePopover.tsx +11 -3
  274. package/src/components/LeaderboardCard.tsx +4 -1
  275. package/src/components/Loading.tsx +17 -14
  276. package/src/components/LoadingOverlay.tsx +33 -50
  277. package/src/components/MemberRow.tsx +3 -3
  278. package/src/components/PageHeader.tsx +4 -1
  279. package/src/components/PostForm.tsx +28 -16
  280. package/src/components/PrizeCollection.tsx +3 -3
  281. package/src/components/RadioGroup.tsx +69 -12
  282. package/src/components/SectionHeader.tsx +4 -1
  283. package/src/components/Select.tsx +4 -3
  284. package/src/components/Separator.tsx +2 -2
  285. package/src/components/Sheet.tsx +15 -8
  286. package/src/components/Skeleton.tsx +1 -1
  287. package/src/components/Tabs.tsx +5 -4
  288. package/src/components/Tag.tsx +56 -35
  289. package/src/components/Toast.tsx +10 -6
  290. package/src/editor/NoteEditor.tsx +246 -72
  291. package/src/editor/editorHtml.generated.ts +1 -1
  292. package/src/editor/index.ts +8 -4
  293. package/src/editor/protocol.ts +4 -12
  294. package/src/hooks/useThemeColor.ts +41 -19
  295. package/src/index.ts +1 -1
  296. package/src/lib/backdrop.ts +16 -0
  297. package/src/lib/overlay-host.tsx +5 -5
@@ -4,25 +4,28 @@ import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo,
4
4
  import { Platform, Pressable, ScrollView, StyleSheet, Text as RNText, TextInput, View } from "react-native";
5
5
  import UntypedWebView from "react-native-webview";
6
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";
7
+ import { EMPTY_DOCUMENT, EMPTY_SLASH_STATE, INITIAL_EDITOR_STATE, getSlashCommand, normalizeDocument, normalizeLinkInput, sanitizeImageSrc } from "@medway-ui/notes-core";
9
8
  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 { Button } from "@/components/Button";
11
- import { Input } from "@/components/Input";
12
- import { Dialog, DialogContainer, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/Dialog";
13
- import { DEFAULT_COLOR, useThemeColor } from "@/hooks/useThemeColor";
9
+ import { Button } from "../components/Button";
10
+ import { Input } from "../components/Input";
11
+ import { Text } from "../components/Text";
12
+ import { Dialog, DialogContainer, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "../components/Dialog";
13
+ import { EDITOR_HTML } from "./editorHtml.generated";
14
+ import { EDITOR_PROTOCOL_VERSION, EDITOR_RECEIVE_HANDLER, isEditorMessage } from "./protocol";
15
+ import { DEFAULT_COLOR, useThemeColor } from "../hooks/useThemeColor";
16
+ import { getMinTouchHitSlop } from "../lib/touch-target";
17
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
18
  function useEditorThemeColor(token, fallback) {
15
19
  const value = useThemeColor(token);
16
20
  return value === DEFAULT_COLOR ? fallback : value;
17
21
  }
18
- import { EDITOR_PROTOCOL_VERSION, EDITOR_RECEIVE_HANDLER, isEditorMessage } from "./protocol";
19
22
 
20
23
  /**
21
24
  * The ref surface of `react-native-webview`, kept local so consumers need not
22
25
  * depend on internal WebView types. Internal only — `NoteEditorHandle` below is
23
26
  * the public ref shape.
24
27
  */
25
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
28
+
26
29
  /**
27
30
  * Retyped locally: the library's own `WebView` resolves to `Component<never>`
28
31
  * under `strict` and rejects every JSX prop.
@@ -43,7 +46,35 @@ const WebView = UntypedWebView;
43
46
 
44
47
  /** The title is single-line; newlines can still arrive via paste. */
45
48
  const sanitizeTitle = value => value.replace(/\r?\n/g, " ");
46
- const DEFAULT_IMAGE_DELETE_GRACE_MS = 4000;
49
+
50
+ /**
51
+ * Enter in the multiline title still commits a "\n" (`onKeyPress` cannot
52
+ * cancel it): one newline added to the previous title is that key, dropped
53
+ * instead of becoming a trailing space. Pasted newlines still become spaces.
54
+ */
55
+ const nextTitle = (previous, value) => value.replace(/\r?\n/, "") === previous ? previous : sanitizeTitle(value);
56
+ const TOOLBAR_BUTTON_SIZE = 32;
57
+ const SLASH_MENU_WIDTH = 256;
58
+ const SLASH_MENU_GAP = 6;
59
+
60
+ /** Starts at the caret, pulled left so the menu never crosses the editor's right edge. */
61
+ const getSlashMenuLeft = (caretX, editorWidth) => editorWidth > 0 ? Math.max(SLASH_MENU_GAP, Math.min(caretX, editorWidth - SLASH_MENU_WIDTH - SLASH_MENU_GAP)) : caretX;
62
+ const TOOLBAR_BUTTON_HIT_SLOP = getMinTouchHitSlop(TOOLBAR_BUTTON_SIZE);
63
+ const MAX_WEBVIEW_REMOUNTS = 3;
64
+ const WEBVIEW_REMOUNT_WINDOW_MS = 60_000;
65
+ const collectImageSrcs = doc => {
66
+ const srcs = new Set();
67
+ const walk = nodes => {
68
+ nodes.forEach(node => {
69
+ if (node.type === "image" && typeof node.attrs?.src === "string") {
70
+ srcs.add(node.attrs.src);
71
+ }
72
+ if (node.content) walk(node.content);
73
+ });
74
+ };
75
+ walk(doc.content);
76
+ return srcs;
77
+ };
47
78
  let requestCounter = 0;
48
79
  /** Correlates a `getDocument` request with the `document` response that answers it. */
49
80
  const nextRequestId = () => {
@@ -62,10 +93,11 @@ const useNoteEditorTheme = () => {
62
93
  } = useColorScheme();
63
94
  const background = useThemeColor("surface-card");
64
95
  const text = useThemeColor("content-paragraph");
65
- const heading = useThemeColor("content-title");
96
+ const heading = useThemeColor("gray-foreground");
66
97
  const muted = useThemeColor("gray-foreground-muted");
67
98
  const border = useThemeColor("gray-stroke-muted");
68
99
  const accent = useThemeColor("primary-foreground");
100
+ const link = useThemeColor("content-link");
69
101
  const codeBackground = useEditorThemeColor("gray-background-soft", "#F0F1F5");
70
102
  return useMemo(() => ({
71
103
  colorScheme: colorScheme === "dark" ? "dark" : "light",
@@ -76,6 +108,7 @@ const useNoteEditorTheme = () => {
76
108
  placeholder: muted,
77
109
  border,
78
110
  accent,
111
+ link,
79
112
  caret: accent,
80
113
  selection: border,
81
114
  codeBackground,
@@ -86,7 +119,7 @@ const useNoteEditorTheme = () => {
86
119
  paddingHorizontal: 16,
87
120
  paddingTop: 12,
88
121
  paddingBottom: 32
89
- }), [colorScheme, background, text, heading, muted, border, accent, codeBackground]);
122
+ }), [colorScheme, background, text, heading, muted, border, accent, link, codeBackground]);
90
123
  };
91
124
 
92
125
  /** Every icon exported from `@medway-ui/icons/native` shares this component shape. */
@@ -219,14 +252,13 @@ const visibleSlashCommandIds = (commandIds, canUploadImage) => canUploadImage ?
219
252
  const NoteEditor = /*#__PURE__*/forwardRef(({
220
253
  document: initialDocument,
221
254
  initialTitle,
222
- titlePlaceholder = "Title",
255
+ titlePlaceholder = "Título",
223
256
  onChange,
224
257
  editable = true,
225
258
  autofocus = false,
226
259
  toolbar = false,
227
260
  onImageUpload,
228
261
  onImageRemoved,
229
- imageDeleteGraceMs = DEFAULT_IMAGE_DELETE_GRACE_MS,
230
262
  onLinkPress,
231
263
  height,
232
264
  backgroundColor,
@@ -243,11 +275,9 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
243
275
  const [title, setTitle] = useState(() => sanitizeTitle(initialTitle ?? ""));
244
276
  const titleInputRef = useRef(null);
245
277
  const [titleSelection, setTitleSelection] = useState(undefined);
246
- const autofocusTitleRef = useRef(autofocus === true && title.length === 0);
278
+ const autofocusTitleRef = useRef(Boolean(autofocus) && title.length === 0);
247
279
  useEffect(() => {
248
280
  if (autofocusTitleRef.current) titleInputRef.current?.focus();
249
- // Mount-only: the one-time "new note" autofocus decision.
250
- // eslint-disable-next-line react-hooks/exhaustive-deps
251
281
  }, []);
252
282
  const focusTitleAtEnd = useCallback(() => {
253
283
  titleInputRef.current?.focus();
@@ -273,11 +303,41 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
273
303
  setIsReady(false);
274
304
  setSlashState(EMPTY_SLASH_STATE);
275
305
  }, []);
306
+ const [webViewKey, setWebViewKey] = useState(0);
307
+ const recentCrashesRef = useRef([]);
308
+
309
+ // A WebView whose content/render process died can't be reused (Android
310
+ // tears it down after `onRenderProcessGone`), so a fresh one is mounted.
311
+ // Capped, so a document that crashes it on every load doesn't loop.
312
+ const handleWebViewCrash = useCallback(() => {
313
+ handleWebViewReset();
314
+ const now = Date.now();
315
+ const recent = recentCrashesRef.current.filter(crashedAt => now - crashedAt < WEBVIEW_REMOUNT_WINDOW_MS);
316
+ if (recent.length >= MAX_WEBVIEW_REMOUNTS) {
317
+ recentCrashesRef.current = recent;
318
+ console.error(`@medway-ui/native NoteEditor: the editor WebView crashed ${MAX_WEBVIEW_REMOUNTS + 1} times within ${WEBVIEW_REMOUNT_WINDOW_MS / 1000}s; not remounting it again.`);
319
+ return;
320
+ }
321
+ recentCrashesRef.current = [...recent, now];
322
+ setWebViewKey(key => key + 1);
323
+ }, [handleWebViewReset]);
324
+
325
+ // What `setContent` sends on every `ready`, so a remount after a crash comes
326
+ // back with the latest content the host has seen rather than `initialDocument`.
327
+ const lastKnownContentRef = useRef(initialDocument ?? EMPTY_DOCUMENT);
328
+ const uploadedThisSessionRef = useRef(new Set());
329
+ const isMountedRef = useRef(true);
330
+ const hasLoadedContentRef = useRef(false);
331
+ // Bumped by `setDocument`: a `document` answering a request made before it
332
+ // describes the replaced note.
333
+ const documentGenerationRef = useRef(0);
276
334
  const pendingDocumentRequests = useRef(new Map());
277
335
  const requestDocument = useCallback(() => {
278
336
  const requestId = nextRequestId();
337
+ const generation = documentGenerationRef.current;
279
338
  return new Promise((resolve, reject) => {
280
339
  pendingDocumentRequests.current.set(requestId, {
340
+ generation,
281
341
  resolve,
282
342
  reject
283
343
  });
@@ -308,23 +368,35 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
308
368
  useEffect(() => {
309
369
  onLinkPressRef.current = onLinkPress;
310
370
  }, [onLinkPress]);
371
+ useEffect(() => {
372
+ isMountedRef.current = true;
373
+ return () => {
374
+ isMountedRef.current = false;
375
+ };
376
+ }, []);
311
377
  const insertImage = useCallback(async () => {
312
378
  const upload = onImageUploadRef.current;
313
379
  if (!upload || !editableRef.current) return;
314
380
  try {
315
381
  const result = await upload();
316
382
  if (!result) return;
317
- if (!editableRef.current) {
383
+ if (!isMountedRef.current || !editableRef.current) {
384
+ onImageRemovedRef.current?.(result.src);
385
+ return;
386
+ }
387
+ const src = sanitizeImageSrc(result.src);
388
+ if (!src) {
389
+ console.error("@medway-ui/native NoteEditor: onImageUpload resolved with an unsupported URL (only absolute http(s): or data:)", result.src);
318
390
  onImageRemovedRef.current?.(result.src);
319
391
  return;
320
392
  }
393
+ uploadedThisSessionRef.current.add(src);
321
394
  sendMutation({
322
395
  type: "insertImage",
323
- src: result.src,
396
+ src,
324
397
  alt: result.alt ?? null
325
398
  });
326
399
  } catch (error) {
327
- // eslint-disable-next-line no-console
328
400
  console.error("@medway-ui/native NoteEditor: onImageUpload failed", error);
329
401
  }
330
402
  }, [sendMutation]);
@@ -343,7 +415,6 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
343
415
  // Refused loudly rather than letting commands quietly not match
344
416
  // what the bundle understands.
345
417
  if (editorMessage.protocol !== EDITOR_PROTOCOL_VERSION) {
346
- // eslint-disable-next-line no-console
347
418
  console.error(`@medway-ui/native NoteEditor: editor bundle reports protocol ${editorMessage.protocol}, expected ${EDITOR_PROTOCOL_VERSION}. Rebuild editor-bundle (its generated HTML is stale).`);
348
419
  return;
349
420
  }
@@ -360,7 +431,11 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
360
431
  // round trip happens only when a host asked for `onChange`, which
361
432
  // keeps serialization per-settled-keystroke, not per-keystroke.
362
433
  if (onChangeRef.current) {
363
- requestDocument().then(doc => onChangeRef.current?.(doc)).catch(() => {});
434
+ const generation = documentGenerationRef.current;
435
+ requestDocument().then(doc => {
436
+ if (generation !== documentGenerationRef.current) return;
437
+ onChangeRef.current?.(doc);
438
+ }).catch(() => {});
364
439
  }
365
440
  return;
366
441
  case "document":
@@ -372,7 +447,13 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
372
447
  // whitelist/depth pass is what makes it an `EditorDocument`, and it
373
448
  // runs on the way out too, not just on `setContent` — the WebView
374
449
  // renders student content, so what it hands back is untrusted.
375
- pending.resolve(normalizeDocument(editorMessage.doc));
450
+ const doc = normalizeDocument(editorMessage.doc);
451
+ // Still the answer to what was asked, but no longer the content
452
+ // to restore after a crash.
453
+ if (pending.generation === documentGenerationRef.current) {
454
+ lastKnownContentRef.current = doc;
455
+ }
456
+ pending.resolve(doc);
376
457
  return;
377
458
  }
378
459
  case "resize":
@@ -384,11 +465,7 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
384
465
  case "backspaceAtDocStart":
385
466
  focusTitleAtEnd();
386
467
  return;
387
- case "imageRemoved":
388
- onImageRemovedRef.current?.(editorMessage.src);
389
- return;
390
468
  case "error":
391
- // eslint-disable-next-line no-console
392
469
  console.error("@medway-ui/native NoteEditor:", editorMessage.message);
393
470
  return;
394
471
  default:
@@ -396,25 +473,24 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
396
473
  }
397
474
  }, [focusTitleAtEnd, isEmbedded, requestDocument]);
398
475
 
399
- // Fires once, when the bundle reports `ready`: the initial document, layout
400
- // mode and image-delete grace period are all read-once.
476
+ // Fires on every `ready`: the first load, and the remount after a crash.
477
+ // `editable`/`autofocus` stay out of deps on purpose: they seed the editor
478
+ // the bundle builds on load; `editable` is kept in sync by the next effect.
401
479
  useEffect(() => {
402
480
  if (!isReady) return;
481
+ const isFirstLoad = !hasLoadedContentRef.current;
482
+ hasLoadedContentRef.current = true;
403
483
  send({
404
484
  type: "setContent",
405
- doc: initialDocument ?? EMPTY_DOCUMENT,
485
+ doc: lastKnownContentRef.current,
406
486
  editable,
407
- focus: autofocusTitleRef.current ? false : Boolean(autofocus),
408
- focusPosition: typeof autofocus === "string" ? autofocus : undefined,
409
- imageDeleteGraceMs
487
+ focus: isFirstLoad && !autofocusTitleRef.current && Boolean(autofocus),
488
+ focusPosition: typeof autofocus === "string" ? autofocus : undefined
410
489
  });
411
490
  if (isEmbedded) send({
412
491
  type: "setLayout",
413
492
  embedded: true
414
493
  });
415
- // `editable`/`autofocus`/`imageDeleteGraceMs` are excluded from deps on
416
- // purpose: they seed the *initial* editor, like `document`.
417
- // eslint-disable-next-line react-hooks/exhaustive-deps
418
494
  }, [isReady, send]);
419
495
 
420
496
  // `editable` is the one prop that stays reactive after mount.
@@ -442,37 +518,72 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
442
518
  type: "blur"
443
519
  }),
444
520
  getDocument: () => requestDocument(),
445
- getSaveData: async () => ({
446
- title,
447
- json: await requestDocument()
448
- }),
449
- setDocument: (doc, options) => send({
450
- type: "setContent",
451
- doc,
452
- editable,
453
- focus: Boolean(options?.focus),
454
- reset: false
455
- })
521
+ getSaveData: async () => {
522
+ const generation = documentGenerationRef.current;
523
+ const json = await requestDocument();
524
+ // After a `setDocument` the session uploads belong to the new note.
525
+ if (generation !== documentGenerationRef.current) {
526
+ return {
527
+ title,
528
+ json,
529
+ removedImages: []
530
+ };
531
+ }
532
+ const kept = collectImageSrcs(json);
533
+ const removedImages = [...uploadedThisSessionRef.current].filter(src => !kept.has(src));
534
+ return {
535
+ title,
536
+ json,
537
+ removedImages
538
+ };
539
+ },
540
+ setDocument: (doc, options) => {
541
+ documentGenerationRef.current += 1;
542
+ lastKnownContentRef.current = doc;
543
+ uploadedThisSessionRef.current.clear();
544
+ send({
545
+ type: "setContent",
546
+ doc,
547
+ editable,
548
+ focus: Boolean(options?.focus),
549
+ reset: false
550
+ });
551
+ }
456
552
  }), [title, editable, requestDocument, send]);
457
553
  const [linkDialogOpen, setLinkDialogOpen] = useState(false);
458
554
  const [linkValue, setLinkValue] = useState("");
555
+ const [linkInvalid, setLinkInvalid] = useState(false);
459
556
  const openLinkDialog = useCallback(() => {
460
557
  setLinkValue(selectionState.linkHref ?? "");
558
+ setLinkInvalid(false);
461
559
  setLinkDialogOpen(true);
462
560
  }, [selectionState.linkHref]);
561
+ const handleLinkValueChange = useCallback(value => {
562
+ setLinkValue(value);
563
+ setLinkInvalid(false);
564
+ }, []);
463
565
  const confirmLink = useCallback(() => {
464
- const href = linkValue.trim();
465
- sendMutation(href ? {
566
+ if (!linkValue.trim()) {
567
+ sendMutation({
568
+ type: "command",
569
+ command: {
570
+ name: "unsetLink"
571
+ }
572
+ });
573
+ setLinkDialogOpen(false);
574
+ return;
575
+ }
576
+ const href = normalizeLinkInput(linkValue);
577
+ if (!href) {
578
+ setLinkInvalid(true);
579
+ return;
580
+ }
581
+ sendMutation({
466
582
  type: "command",
467
583
  command: {
468
584
  name: "setLink",
469
585
  href
470
586
  }
471
- } : {
472
- type: "command",
473
- command: {
474
- name: "unsetLink"
475
- }
476
587
  });
477
588
  setLinkDialogOpen(false);
478
589
  }, [linkValue, sendMutation]);
@@ -490,6 +601,10 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
490
601
  if (id === "image") insertImage();
491
602
  }, [editable, send, insertImage]);
492
603
  const containerHeight = isEmbedded ? contentHeight : undefined;
604
+ const [editorWidth, setEditorWidth] = useState(0);
605
+ const handleEditorLayout = useCallback(event => {
606
+ setEditorWidth(event.nativeEvent.layout.width);
607
+ }, []);
493
608
  const resolvedBackground = backgroundColor ?? theme.background;
494
609
  return /*#__PURE__*/_jsxs(View, {
495
610
  style: [isEmbedded ? styles.containerEmbedded : styles.container, {
@@ -505,11 +620,10 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
505
620
  ref: titleInputRef,
506
621
  multiline: true,
507
622
  value: title,
508
- onChangeText: value => setTitle(sanitizeTitle(value)),
623
+ onChangeText: value => setTitle(previous => nextTitle(previous, value)),
509
624
  onKeyPress: event => {
510
- // Enter moves focus into the content's first block, never inserts
511
- // a line break; `sanitizeTitle` is the backstop if the platform
512
- // commits the `\n` before this handler runs.
625
+ // Enter moves focus into the content's first block; the `\n` it
626
+ // still commits is dropped by `nextTitle`.
513
627
  if (event.nativeEvent.key !== "Enter") return;
514
628
  send({
515
629
  type: "focus",
@@ -532,6 +646,7 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
532
646
  style: {
533
647
  flex: isEmbedded ? undefined : 1
534
648
  },
649
+ onLayout: handleEditorLayout,
535
650
  children: [/*#__PURE__*/_jsx(WebView, {
536
651
  ref: webViewRef,
537
652
  testID: testID,
@@ -549,8 +664,8 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
549
664
  originWhitelist: ["^about:blank$"],
550
665
  onMessage: handleMessage,
551
666
  onLoadStart: handleWebViewReset,
552
- onContentProcessDidTerminate: handleWebViewReset,
553
- onRenderProcessGone: handleWebViewReset
667
+ onContentProcessDidTerminate: handleWebViewCrash,
668
+ onRenderProcessGone: handleWebViewCrash
554
669
  // Links already leave the editor via the `linkPress` message, so
555
670
  // nothing rendered here needs to navigate. Allowing only the
556
671
  // initial `about:blank` load closes off `data:`/script-driven
@@ -586,8 +701,9 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
586
701
  } : null, {
587
702
  backgroundColor: resolvedBackground
588
703
  }]
589
- }), slashState.active && slashState.caret ? /*#__PURE__*/_jsx(NoteEditorSlashMenu, {
704
+ }, webViewKey), slashState.active && slashState.caret ? /*#__PURE__*/_jsx(NoteEditorSlashMenu, {
590
705
  slash: slashState,
706
+ editorWidth: editorWidth,
591
707
  canUploadImage: Boolean(onImageUpload),
592
708
  onSelect: handleSlashSelect
593
709
  }) : null]
@@ -599,16 +715,21 @@ const NoteEditor = /*#__PURE__*/forwardRef(({
599
715
  children: /*#__PURE__*/_jsx(DialogTitle, {
600
716
  children: "Inserir link"
601
717
  })
602
- }), /*#__PURE__*/_jsx(DialogContent, {
603
- children: /*#__PURE__*/_jsx(Input, {
718
+ }), /*#__PURE__*/_jsxs(DialogContent, {
719
+ children: [/*#__PURE__*/_jsx(Input, {
604
720
  value: linkValue,
605
- onChangeText: setLinkValue,
721
+ onChangeText: handleLinkValueChange,
722
+ invalid: linkInvalid,
606
723
  placeholder: "https://",
607
724
  label: "URL",
608
725
  autoCapitalize: "none",
609
726
  autoCorrect: false,
610
727
  keyboardType: "url"
611
- })
728
+ }), linkInvalid ? /*#__PURE__*/_jsx(Text, {
729
+ accessibilityRole: "alert",
730
+ className: "mt-1 text-xs text-feedback-error-foreground",
731
+ children: "Link inv\xE1lido. Use um endere\xE7o como medway.com.br"
732
+ }) : null]
612
733
  }), /*#__PURE__*/_jsx(DialogFooter, {
613
734
  children: /*#__PURE__*/_jsx(Button, {
614
735
  onPress: confirmLink,
@@ -656,6 +777,7 @@ function NoteEditorToolbar({
656
777
  selected: active
657
778
  },
658
779
  disabled: disabled,
780
+ hitSlop: TOOLBAR_BUTTON_HIT_SLOP,
659
781
  onPress: item.onPress ?? (() => item.command && onCommand(item.command)),
660
782
  style: buttonStyle,
661
783
  children: /*#__PURE__*/_jsx(Icon, {
@@ -678,6 +800,7 @@ function NoteEditorToolbar({
678
800
  disabled: allDisabled
679
801
  },
680
802
  disabled: allDisabled,
803
+ hitSlop: TOOLBAR_BUTTON_HIT_SLOP,
681
804
  onPress: onImagePress,
682
805
  style: [styles.toolbarButton, allDisabled ? styles.toolbarButtonDisabled : null],
683
806
  children: /*#__PURE__*/_jsx(ImageIcon, {
@@ -699,6 +822,7 @@ function NoteEditorToolbar({
699
822
  */
700
823
  function NoteEditorSlashMenu({
701
824
  slash,
825
+ editorWidth,
702
826
  canUploadImage,
703
827
  onSelect
704
828
  }) {
@@ -711,8 +835,8 @@ function NoteEditorSlashMenu({
711
835
  const commandIds = visibleSlashCommandIds(slash.commandIds, canUploadImage);
712
836
  return /*#__PURE__*/_jsx(View, {
713
837
  style: [styles.slashMenuShadow, {
714
- left: slash.caret.x,
715
- top: slash.caret.y + slash.caret.height + 6
838
+ left: getSlashMenuLeft(slash.caret.x, editorWidth),
839
+ top: slash.caret.y + slash.caret.height + SLASH_MENU_GAP
716
840
  }],
717
841
  children: /*#__PURE__*/_jsx(View, {
718
842
  style: [styles.slashMenu, {
@@ -798,8 +922,8 @@ const styles = StyleSheet.create({
798
922
  marginHorizontal: 4
799
923
  },
800
924
  toolbarButton: {
801
- width: 32,
802
- height: 32,
925
+ width: TOOLBAR_BUTTON_SIZE,
926
+ height: TOOLBAR_BUTTON_SIZE,
803
927
  alignItems: "center",
804
928
  justifyContent: "center",
805
929
  borderRadius: 6
@@ -810,7 +934,7 @@ const styles = StyleSheet.create({
810
934
  slashMenuShadow: {
811
935
  position: "absolute",
812
936
  zIndex: 50,
813
- width: 256,
937
+ width: SLASH_MENU_WIDTH,
814
938
  shadowColor: "#000",
815
939
  shadowOffset: {
816
940
  width: 0,