@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
@@ -18,6 +18,7 @@ import {
18
18
  Text as RNText,
19
19
  TextInput,
20
20
  View,
21
+ type LayoutChangeEvent,
21
22
  type NativeSyntheticEvent,
22
23
  type StyleProp,
23
24
  type TextInputKeyPressEventData,
@@ -29,8 +30,6 @@ import UntypedWebView, {
29
30
  } from "react-native-webview";
30
31
  import { useColorScheme } from "nativewind";
31
32
 
32
- import { EDITOR_HTML } from "./editorHtml.generated";
33
-
34
33
  import type {
35
34
  EditorCommand,
36
35
  EditorDocument,
@@ -45,6 +44,8 @@ import {
45
44
  INITIAL_EDITOR_STATE,
46
45
  getSlashCommand,
47
46
  normalizeDocument,
47
+ normalizeLinkInput,
48
+ sanitizeImageSrc,
48
49
  } from "@medway-ui/notes-core";
49
50
  import {
50
51
  ArrowUndoUpLeft,
@@ -66,6 +67,7 @@ import {
66
67
 
67
68
  import { Button } from "@/components/Button";
68
69
  import { Input } from "@/components/Input";
70
+ import { Text } from "@/components/Text";
69
71
  import {
70
72
  Dialog,
71
73
  DialogContainer,
@@ -74,20 +76,21 @@ import {
74
76
  DialogHeader,
75
77
  DialogTitle,
76
78
  } from "@/components/Dialog";
77
- import { DEFAULT_COLOR, useThemeColor } from "@/hooks/useThemeColor";
78
-
79
- function useEditorThemeColor(token: string, fallback: string): string {
80
- const value = useThemeColor(token);
81
- return value === DEFAULT_COLOR ? fallback : value;
82
- }
83
-
79
+ import { EDITOR_HTML } from "@/editor/editorHtml.generated";
84
80
  import {
85
81
  EDITOR_PROTOCOL_VERSION,
86
82
  EDITOR_RECEIVE_HANDLER,
87
83
  isEditorMessage,
88
84
  type EditorToNativeMessage,
89
85
  type NativeToEditorMessage,
90
- } from "./protocol";
86
+ } from "@/editor/protocol";
87
+ import { DEFAULT_COLOR, useThemeColor } from "@/hooks/useThemeColor";
88
+ import { getMinTouchHitSlop } from "@/lib/touch-target";
89
+
90
+ function useEditorThemeColor(token: string, fallback: string): string {
91
+ const value = useThemeColor(token);
92
+ return value === DEFAULT_COLOR ? fallback : value;
93
+ }
91
94
 
92
95
  /**
93
96
  * The ref surface of `react-native-webview`, kept local so consumers need not
@@ -114,6 +117,15 @@ const WebView = UntypedWebView as unknown as ComponentType<
114
117
  export type NoteEditorValue = {
115
118
  title: string;
116
119
  json: EditorDocument;
120
+ /**
121
+ * Images `onImageUpload` produced this session that `json` no longer has.
122
+ * Keep the list from the last save that succeeded (replace it on every
123
+ * save, never merge) and delete those files only when the editor unmounts
124
+ * (closing it, or switching notes with a new `key`): until then an undo can
125
+ * bring an image back. `setDocument` keeps the undo history, so it is not
126
+ * the moment to delete them.
127
+ */
128
+ removedImages: string[];
117
129
  };
118
130
 
119
131
  /**
@@ -132,12 +144,12 @@ export type NoteEditorHandle = {
132
144
  ) => void;
133
145
  };
134
146
 
135
- type NoteEditorProps = {
147
+ export type NoteEditorProps = {
136
148
  /**
137
149
  * Initial document: the note's JSON, or an HTML string (parsed bundle-side,
138
- * the only side with a DOM). Changing it replaces the content but keeps the
139
- * instance — pass a new React `key` for a fresh undo stack, as when
140
- * switching notes.
150
+ * the only side with a DOM). Read once, on mount: changing it later does
151
+ * nothing. To replace the content use the ref's `setDocument`; to open
152
+ * another note with a fresh undo stack, pass a new React `key`.
141
153
  */
142
154
  document?: EditorDocument | string;
143
155
  /** Initial title, shown above the content in its own always-visible area. */
@@ -154,15 +166,26 @@ type NoteEditorProps = {
154
166
  * picker was cancelled. **Different shape from the web prop**: there is no
155
167
  * file input here and this package carries no image-picker dependency, so
156
168
  * picking the file is the host's job. Providing it is what enables the
157
- * toolbar "Imagem" button and the `/Imagem` slash command.
169
+ * toolbar "Inserir imagem" button and the `/Imagem` slash command.
158
170
  */
159
171
  onImageUpload?: () => Promise<{ src: string; alt?: string | null } | null>;
160
172
  /**
161
- * Fires when an image inserted this session leaves the document and stays out
162
- * past the grace period. Tracked bundle-side (`editor-bundle/src/main.ts`).
173
+ * Fires right away for an uploaded file that never made it into the note:
174
+ * a `src` the editor refuses (anything but absolute `http(s):`/`data:`) or
175
+ * one that arrives after the editor stopped accepting it. Images removed
176
+ * while editing come from `getSaveData().removedImages` instead, so the
177
+ * host only deletes what a successful save left out.
178
+ *
179
+ * Neither covers images present when the document loaded, nor a file pasted
180
+ * into another note: this component cannot see other notes, so checking
181
+ * that no note still references a file before deleting it is the backend's
182
+ * job.
163
183
  */
164
184
  onImageRemoved?: (src: string) => void;
165
- /** Default 4000ms. */
185
+ /**
186
+ * @deprecated Ignored. Removed images come from
187
+ * `getSaveData().removedImages`, deleted by the host when the editor closes.
188
+ */
166
189
  imageDeleteGraceMs?: number;
167
190
  /**
168
191
  * Native-only. The WebView never navigates, so the host decides what to open
@@ -188,7 +211,45 @@ type NoteEditorProps = {
188
211
  /** The title is single-line; newlines can still arrive via paste. */
189
212
  const sanitizeTitle = (value: string): string => value.replace(/\r?\n/g, " ");
190
213
 
191
- const DEFAULT_IMAGE_DELETE_GRACE_MS = 4000;
214
+ /**
215
+ * Enter in the multiline title still commits a "\n" (`onKeyPress` cannot
216
+ * cancel it): one newline added to the previous title is that key, dropped
217
+ * instead of becoming a trailing space. Pasted newlines still become spaces.
218
+ */
219
+ const nextTitle = (previous: string, value: string): string =>
220
+ value.replace(/\r?\n/, "") === previous ? previous : sanitizeTitle(value);
221
+
222
+ const TOOLBAR_BUTTON_SIZE = 32;
223
+
224
+ const SLASH_MENU_WIDTH = 256;
225
+ const SLASH_MENU_GAP = 6;
226
+
227
+ /** Starts at the caret, pulled left so the menu never crosses the editor's right edge. */
228
+ const getSlashMenuLeft = (caretX: number, editorWidth: number): number =>
229
+ editorWidth > 0
230
+ ? Math.max(
231
+ SLASH_MENU_GAP,
232
+ Math.min(caretX, editorWidth - SLASH_MENU_WIDTH - SLASH_MENU_GAP)
233
+ )
234
+ : caretX;
235
+ const TOOLBAR_BUTTON_HIT_SLOP = getMinTouchHitSlop(TOOLBAR_BUTTON_SIZE);
236
+
237
+ const MAX_WEBVIEW_REMOUNTS = 3;
238
+ const WEBVIEW_REMOUNT_WINDOW_MS = 60_000;
239
+
240
+ const collectImageSrcs = (doc: EditorDocument): Set<string> => {
241
+ const srcs = new Set<string>();
242
+ const walk = (nodes: EditorDocument["content"]) => {
243
+ nodes.forEach((node) => {
244
+ if (node.type === "image" && typeof node.attrs?.src === "string") {
245
+ srcs.add(node.attrs.src);
246
+ }
247
+ if (node.content) walk(node.content);
248
+ });
249
+ };
250
+ walk(doc.content);
251
+ return srcs;
252
+ };
192
253
 
193
254
  let requestCounter = 0;
194
255
  /** Correlates a `getDocument` request with the `document` response that answers it. */
@@ -206,10 +267,11 @@ const useNoteEditorTheme = (): EditorTheme => {
206
267
  const { colorScheme } = useColorScheme();
207
268
  const background = useThemeColor("surface-card");
208
269
  const text = useThemeColor("content-paragraph");
209
- const heading = useThemeColor("content-title");
270
+ const heading = useThemeColor("gray-foreground");
210
271
  const muted = useThemeColor("gray-foreground-muted");
211
272
  const border = useThemeColor("gray-stroke-muted");
212
273
  const accent = useThemeColor("primary-foreground");
274
+ const link = useThemeColor("content-link");
213
275
  const codeBackground = useEditorThemeColor("gray-background-soft", "#F0F1F5");
214
276
 
215
277
  return useMemo<EditorTheme>(
@@ -222,6 +284,7 @@ const useNoteEditorTheme = (): EditorTheme => {
222
284
  placeholder: muted,
223
285
  border,
224
286
  accent,
287
+ link,
225
288
  caret: accent,
226
289
  selection: border,
227
290
  codeBackground,
@@ -233,7 +296,7 @@ const useNoteEditorTheme = (): EditorTheme => {
233
296
  paddingTop: 12,
234
297
  paddingBottom: 32,
235
298
  }),
236
- [colorScheme, background, text, heading, muted, border, accent, codeBackground]
299
+ [colorScheme, background, text, heading, muted, border, accent, link, codeBackground]
237
300
  );
238
301
  };
239
302
 
@@ -299,14 +362,13 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
299
362
  {
300
363
  document: initialDocument,
301
364
  initialTitle,
302
- titlePlaceholder = "Title",
365
+ titlePlaceholder = "Título",
303
366
  onChange,
304
367
  editable = true,
305
368
  autofocus = false,
306
369
  toolbar = false,
307
370
  onImageUpload,
308
371
  onImageRemoved,
309
- imageDeleteGraceMs = DEFAULT_IMAGE_DELETE_GRACE_MS,
310
372
  onLinkPress,
311
373
  height,
312
374
  backgroundColor,
@@ -333,12 +395,10 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
333
395
  { start: number; end: number } | undefined
334
396
  >(undefined);
335
397
 
336
- const autofocusTitleRef = useRef(autofocus === true && title.length === 0);
398
+ const autofocusTitleRef = useRef(Boolean(autofocus) && title.length === 0);
337
399
 
338
400
  useEffect(() => {
339
401
  if (autofocusTitleRef.current) titleInputRef.current?.focus();
340
- // Mount-only: the one-time "new note" autofocus decision.
341
- // eslint-disable-next-line react-hooks/exhaustive-deps
342
402
  }, []);
343
403
 
344
404
  const focusTitleAtEnd = useCallback(() => {
@@ -371,15 +431,60 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
371
431
  setSlashState(EMPTY_SLASH_STATE);
372
432
  }, []);
373
433
 
434
+ const [webViewKey, setWebViewKey] = useState(0);
435
+ const recentCrashesRef = useRef<number[]>([]);
436
+
437
+ // A WebView whose content/render process died can't be reused (Android
438
+ // tears it down after `onRenderProcessGone`), so a fresh one is mounted.
439
+ // Capped, so a document that crashes it on every load doesn't loop.
440
+ const handleWebViewCrash = useCallback(() => {
441
+ handleWebViewReset();
442
+
443
+ const now = Date.now();
444
+ const recent = recentCrashesRef.current.filter(
445
+ (crashedAt) => now - crashedAt < WEBVIEW_REMOUNT_WINDOW_MS
446
+ );
447
+ if (recent.length >= MAX_WEBVIEW_REMOUNTS) {
448
+ recentCrashesRef.current = recent;
449
+ console.error(
450
+ `@medway-ui/native NoteEditor: the editor WebView crashed ${MAX_WEBVIEW_REMOUNTS + 1} times within ${WEBVIEW_REMOUNT_WINDOW_MS / 1000}s; not remounting it again.`
451
+ );
452
+ return;
453
+ }
454
+
455
+ recentCrashesRef.current = [...recent, now];
456
+ setWebViewKey((key) => key + 1);
457
+ }, [handleWebViewReset]);
458
+
459
+ // What `setContent` sends on every `ready`, so a remount after a crash comes
460
+ // back with the latest content the host has seen rather than `initialDocument`.
461
+ const lastKnownContentRef = useRef<EditorDocument | string>(
462
+ initialDocument ?? EMPTY_DOCUMENT
463
+ );
464
+ const uploadedThisSessionRef = useRef(new Set<string>());
465
+ const isMountedRef = useRef(true);
466
+ const hasLoadedContentRef = useRef(false);
467
+ // Bumped by `setDocument`: a `document` answering a request made before it
468
+ // describes the replaced note.
469
+ const documentGenerationRef = useRef(0);
470
+
374
471
  const pendingDocumentRequests = useRef(
375
- new Map<string, { resolve: (doc: EditorDocument) => void; reject: (error: Error) => void }>()
472
+ new Map<
473
+ string,
474
+ {
475
+ generation: number;
476
+ resolve: (doc: EditorDocument) => void;
477
+ reject: (error: Error) => void;
478
+ }
479
+ >()
376
480
  );
377
481
 
378
482
  const requestDocument = useCallback((): Promise<EditorDocument> => {
379
483
  const requestId = nextRequestId();
484
+ const generation = documentGenerationRef.current;
380
485
 
381
486
  return new Promise<EditorDocument>((resolve, reject) => {
382
- pendingDocumentRequests.current.set(requestId, { resolve, reject });
487
+ pendingDocumentRequests.current.set(requestId, { generation, resolve, reject });
383
488
  send({ type: "getDocument", requestId });
384
489
 
385
490
  setTimeout(() => {
@@ -407,6 +512,13 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
407
512
  onLinkPressRef.current = onLinkPress;
408
513
  }, [onLinkPress]);
409
514
 
515
+ useEffect(() => {
516
+ isMountedRef.current = true;
517
+ return () => {
518
+ isMountedRef.current = false;
519
+ };
520
+ }, []);
521
+
410
522
  const insertImage = useCallback(async () => {
411
523
  const upload = onImageUploadRef.current;
412
524
  if (!upload || !editableRef.current) return;
@@ -414,17 +526,22 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
414
526
  try {
415
527
  const result = await upload();
416
528
  if (!result) return;
417
- if (!editableRef.current) {
529
+ if (!isMountedRef.current || !editableRef.current) {
418
530
  onImageRemovedRef.current?.(result.src);
419
531
  return;
420
532
  }
421
- sendMutation({
422
- type: "insertImage",
423
- src: result.src,
424
- alt: result.alt ?? null,
425
- });
533
+ const src = sanitizeImageSrc(result.src);
534
+ if (!src) {
535
+ console.error(
536
+ "@medway-ui/native NoteEditor: onImageUpload resolved with an unsupported URL (only absolute http(s): or data:)",
537
+ result.src
538
+ );
539
+ onImageRemovedRef.current?.(result.src);
540
+ return;
541
+ }
542
+ uploadedThisSessionRef.current.add(src);
543
+ sendMutation({ type: "insertImage", src, alt: result.alt ?? null });
426
544
  } catch (error) {
427
- // eslint-disable-next-line no-console
428
545
  console.error("@medway-ui/native NoteEditor: onImageUpload failed", error);
429
546
  }
430
547
  }, [sendMutation]);
@@ -446,7 +563,6 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
446
563
  // Refused loudly rather than letting commands quietly not match
447
564
  // what the bundle understands.
448
565
  if (editorMessage.protocol !== EDITOR_PROTOCOL_VERSION) {
449
- // eslint-disable-next-line no-console
450
566
  console.error(
451
567
  `@medway-ui/native NoteEditor: editor bundle reports protocol ${editorMessage.protocol}, expected ${EDITOR_PROTOCOL_VERSION}. Rebuild editor-bundle (its generated HTML is stale).`
452
568
  );
@@ -465,8 +581,12 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
465
581
  // round trip happens only when a host asked for `onChange`, which
466
582
  // keeps serialization per-settled-keystroke, not per-keystroke.
467
583
  if (onChangeRef.current) {
584
+ const generation = documentGenerationRef.current;
468
585
  requestDocument()
469
- .then((doc) => onChangeRef.current?.(doc))
586
+ .then((doc) => {
587
+ if (generation !== documentGenerationRef.current) return;
588
+ onChangeRef.current?.(doc);
589
+ })
470
590
  .catch(() => {});
471
591
  }
472
592
  return;
@@ -478,7 +598,13 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
478
598
  // whitelist/depth pass is what makes it an `EditorDocument`, and it
479
599
  // runs on the way out too, not just on `setContent` — the WebView
480
600
  // renders student content, so what it hands back is untrusted.
481
- pending.resolve(normalizeDocument(editorMessage.doc));
601
+ const doc = normalizeDocument(editorMessage.doc);
602
+ // Still the answer to what was asked, but no longer the content
603
+ // to restore after a crash.
604
+ if (pending.generation === documentGenerationRef.current) {
605
+ lastKnownContentRef.current = doc;
606
+ }
607
+ pending.resolve(doc);
482
608
  return;
483
609
  }
484
610
  case "resize":
@@ -490,11 +616,7 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
490
616
  case "backspaceAtDocStart":
491
617
  focusTitleAtEnd();
492
618
  return;
493
- case "imageRemoved":
494
- onImageRemovedRef.current?.(editorMessage.src);
495
- return;
496
619
  case "error":
497
- // eslint-disable-next-line no-console
498
620
  console.error("@medway-ui/native NoteEditor:", editorMessage.message);
499
621
  return;
500
622
  default:
@@ -504,24 +626,24 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
504
626
  [focusTitleAtEnd, isEmbedded, requestDocument]
505
627
  );
506
628
 
507
- // Fires once, when the bundle reports `ready`: the initial document, layout
508
- // mode and image-delete grace period are all read-once.
629
+ // Fires on every `ready`: the first load, and the remount after a crash.
630
+ // `editable`/`autofocus` stay out of deps on purpose: they seed the editor
631
+ // the bundle builds on load; `editable` is kept in sync by the next effect.
509
632
  useEffect(() => {
510
633
  if (!isReady) return;
511
634
 
635
+ const isFirstLoad = !hasLoadedContentRef.current;
636
+ hasLoadedContentRef.current = true;
637
+
512
638
  send({
513
639
  type: "setContent",
514
- doc: initialDocument ?? EMPTY_DOCUMENT,
640
+ doc: lastKnownContentRef.current,
515
641
  editable,
516
- focus: autofocusTitleRef.current ? false : Boolean(autofocus),
642
+ focus: isFirstLoad && !autofocusTitleRef.current && Boolean(autofocus),
517
643
  focusPosition: typeof autofocus === "string" ? autofocus : undefined,
518
- imageDeleteGraceMs,
519
644
  });
520
645
 
521
646
  if (isEmbedded) send({ type: "setLayout", embedded: true });
522
- // `editable`/`autofocus`/`imageDeleteGraceMs` are excluded from deps on
523
- // purpose: they seed the *initial* editor, like `document`.
524
- // eslint-disable-next-line react-hooks/exhaustive-deps
525
647
  }, [isReady, send]);
526
648
 
527
649
  // `editable` is the one prop that stays reactive after mount.
@@ -542,34 +664,64 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
542
664
  focus: (position) => send({ type: "focus", position: position ?? "end" }),
543
665
  blur: () => send({ type: "blur" }),
544
666
  getDocument: () => requestDocument(),
545
- getSaveData: async () => ({ title, json: await requestDocument() }),
546
- setDocument: (doc, options) =>
667
+ getSaveData: async () => {
668
+ const generation = documentGenerationRef.current;
669
+ const json = await requestDocument();
670
+ // After a `setDocument` the session uploads belong to the new note.
671
+ if (generation !== documentGenerationRef.current) {
672
+ return { title, json, removedImages: [] };
673
+ }
674
+ const kept = collectImageSrcs(json);
675
+ const removedImages = [...uploadedThisSessionRef.current].filter(
676
+ (src) => !kept.has(src)
677
+ );
678
+ return { title, json, removedImages };
679
+ },
680
+ setDocument: (doc, options) => {
681
+ documentGenerationRef.current += 1;
682
+ lastKnownContentRef.current = doc;
683
+ uploadedThisSessionRef.current.clear();
547
684
  send({
548
685
  type: "setContent",
549
686
  doc,
550
687
  editable,
551
688
  focus: Boolean(options?.focus),
552
689
  reset: false,
553
- }),
690
+ });
691
+ },
554
692
  }),
555
693
  [title, editable, requestDocument, send]
556
694
  );
557
695
 
558
696
  const [linkDialogOpen, setLinkDialogOpen] = useState(false);
559
697
  const [linkValue, setLinkValue] = useState("");
698
+ const [linkInvalid, setLinkInvalid] = useState(false);
560
699
 
561
700
  const openLinkDialog = useCallback(() => {
562
701
  setLinkValue(selectionState.linkHref ?? "");
702
+ setLinkInvalid(false);
563
703
  setLinkDialogOpen(true);
564
704
  }, [selectionState.linkHref]);
565
705
 
706
+ const handleLinkValueChange = useCallback((value: string) => {
707
+ setLinkValue(value);
708
+ setLinkInvalid(false);
709
+ }, []);
710
+
566
711
  const confirmLink = useCallback(() => {
567
- const href = linkValue.trim();
568
- sendMutation(
569
- href
570
- ? { type: "command", command: { name: "setLink", href } }
571
- : { type: "command", command: { name: "unsetLink" } }
572
- );
712
+ if (!linkValue.trim()) {
713
+ sendMutation({ type: "command", command: { name: "unsetLink" } });
714
+ setLinkDialogOpen(false);
715
+ return;
716
+ }
717
+
718
+ const href = normalizeLinkInput(linkValue);
719
+ if (!href) {
720
+ setLinkInvalid(true);
721
+ return;
722
+ }
723
+
724
+ sendMutation({ type: "command", command: { name: "setLink", href } });
573
725
  setLinkDialogOpen(false);
574
726
  }, [linkValue, sendMutation]);
575
727
 
@@ -593,6 +745,11 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
593
745
 
594
746
  const containerHeight = isEmbedded ? contentHeight : undefined;
595
747
 
748
+ const [editorWidth, setEditorWidth] = useState(0);
749
+ const handleEditorLayout = useCallback((event: LayoutChangeEvent) => {
750
+ setEditorWidth(event.nativeEvent.layout.width);
751
+ }, []);
752
+
596
753
  const resolvedBackground = backgroundColor ?? theme.background;
597
754
 
598
755
  return (
@@ -616,11 +773,10 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
616
773
  ref={titleInputRef}
617
774
  multiline
618
775
  value={title}
619
- onChangeText={(value) => setTitle(sanitizeTitle(value))}
776
+ onChangeText={(value) => setTitle((previous) => nextTitle(previous, value))}
620
777
  onKeyPress={(event: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
621
- // Enter moves focus into the content's first block, never inserts
622
- // a line break; `sanitizeTitle` is the backstop if the platform
623
- // commits the `\n` before this handler runs.
778
+ // Enter moves focus into the content's first block; the `\n` it
779
+ // still commits is dropped by `nextTitle`.
624
780
  if (event.nativeEvent.key !== "Enter") return;
625
781
  send({ type: "focus", position: "start" });
626
782
  }}
@@ -636,8 +792,12 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
636
792
  style={[styles.title, { color: titleColor }]}
637
793
  />
638
794
 
639
- <View style={{ flex: isEmbedded ? undefined : 1 }}>
795
+ <View
796
+ style={{ flex: isEmbedded ? undefined : 1 }}
797
+ onLayout={handleEditorLayout}
798
+ >
640
799
  <WebView
800
+ key={webViewKey}
641
801
  ref={webViewRef}
642
802
  testID={testID}
643
803
  containerStyle={
@@ -650,8 +810,8 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
650
810
  originWhitelist={["^about:blank$"]}
651
811
  onMessage={handleMessage}
652
812
  onLoadStart={handleWebViewReset}
653
- onContentProcessDidTerminate={handleWebViewReset}
654
- onRenderProcessGone={handleWebViewReset}
813
+ onContentProcessDidTerminate={handleWebViewCrash}
814
+ onRenderProcessGone={handleWebViewCrash}
655
815
  // Links already leave the editor via the `linkPress` message, so
656
816
  // nothing rendered here needs to navigate. Allowing only the
657
817
  // initial `about:blank` load closes off `data:`/script-driven
@@ -689,6 +849,7 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
689
849
  {slashState.active && slashState.caret ? (
690
850
  <NoteEditorSlashMenu
691
851
  slash={slashState}
852
+ editorWidth={editorWidth}
692
853
  canUploadImage={Boolean(onImageUpload)}
693
854
  onSelect={handleSlashSelect}
694
855
  />
@@ -703,13 +864,22 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
703
864
  <DialogContent>
704
865
  <Input
705
866
  value={linkValue}
706
- onChangeText={setLinkValue}
867
+ onChangeText={handleLinkValueChange}
868
+ invalid={linkInvalid}
707
869
  placeholder="https://"
708
870
  label="URL"
709
871
  autoCapitalize="none"
710
872
  autoCorrect={false}
711
873
  keyboardType="url"
712
874
  />
875
+ {linkInvalid ? (
876
+ <Text
877
+ accessibilityRole="alert"
878
+ className="mt-1 text-xs text-feedback-error-foreground"
879
+ >
880
+ Link inválido. Use um endereço como medway.com.br
881
+ </Text>
882
+ ) : null}
713
883
  </DialogContent>
714
884
  <DialogFooter>
715
885
  <Button onPress={confirmLink}>Confirmar</Button>
@@ -769,6 +939,7 @@ function NoteEditorToolbar({
769
939
  accessibilityLabel={item.label}
770
940
  accessibilityState={{ disabled, selected: active }}
771
941
  disabled={disabled}
942
+ hitSlop={TOOLBAR_BUTTON_HIT_SLOP}
772
943
  onPress={item.onPress ?? (() => item.command && onCommand(item.command))}
773
944
  style={buttonStyle}
774
945
  >
@@ -787,6 +958,7 @@ function NoteEditorToolbar({
787
958
  accessibilityLabel="Inserir imagem"
788
959
  accessibilityState={{ disabled: allDisabled }}
789
960
  disabled={allDisabled}
961
+ hitSlop={TOOLBAR_BUTTON_HIT_SLOP}
790
962
  onPress={onImagePress}
791
963
  style={[
792
964
  styles.toolbarButton,
@@ -810,10 +982,12 @@ function NoteEditorToolbar({
810
982
  */
811
983
  function NoteEditorSlashMenu({
812
984
  slash,
985
+ editorWidth,
813
986
  canUploadImage,
814
987
  onSelect,
815
988
  }: {
816
989
  slash: SlashMenuState;
990
+ editorWidth: number;
817
991
  canUploadImage: boolean;
818
992
  onSelect: (id: SlashCommandId) => void;
819
993
  }) {
@@ -835,8 +1009,8 @@ function NoteEditorSlashMenu({
835
1009
  style={[
836
1010
  styles.slashMenuShadow,
837
1011
  {
838
- left: slash.caret.x,
839
- top: slash.caret.y + slash.caret.height + 6,
1012
+ left: getSlashMenuLeft(slash.caret.x, editorWidth),
1013
+ top: slash.caret.y + slash.caret.height + SLASH_MENU_GAP,
840
1014
  },
841
1015
  ]}
842
1016
  >
@@ -921,8 +1095,8 @@ const styles = StyleSheet.create({
921
1095
  marginHorizontal: 4,
922
1096
  },
923
1097
  toolbarButton: {
924
- width: 32,
925
- height: 32,
1098
+ width: TOOLBAR_BUTTON_SIZE,
1099
+ height: TOOLBAR_BUTTON_SIZE,
926
1100
  alignItems: "center",
927
1101
  justifyContent: "center",
928
1102
  borderRadius: 6,
@@ -933,7 +1107,7 @@ const styles = StyleSheet.create({
933
1107
  slashMenuShadow: {
934
1108
  position: "absolute",
935
1109
  zIndex: 50,
936
- width: 256,
1110
+ width: SLASH_MENU_WIDTH,
937
1111
  shadowColor: "#000",
938
1112
  shadowOffset: { width: 0, height: 4 },
939
1113
  shadowOpacity: 0.14,