@medway-ui/native 1.11.0 → 1.13.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 (36) hide show
  1. package/README.md +1 -0
  2. package/lib/commonjs/components/TextMarker.js +485 -0
  3. package/lib/commonjs/components/TextMarker.js.map +1 -0
  4. package/lib/commonjs/editor/NoteEditor.js +29 -152
  5. package/lib/commonjs/editor/NoteEditor.js.map +1 -1
  6. package/lib/commonjs/editor/editorHtml.generated.js +1 -1
  7. package/lib/commonjs/editor/editorHtml.generated.js.map +1 -1
  8. package/lib/commonjs/editor/protocol.js +2 -3
  9. package/lib/commonjs/editor/protocol.js.map +1 -1
  10. package/lib/commonjs/index.js +19 -0
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/module/components/TextMarker.js +479 -0
  13. package/lib/module/components/TextMarker.js.map +1 -0
  14. package/lib/module/editor/NoteEditor.js +32 -155
  15. package/lib/module/editor/NoteEditor.js.map +1 -1
  16. package/lib/module/editor/editorHtml.generated.js +1 -1
  17. package/lib/module/editor/editorHtml.generated.js.map +1 -1
  18. package/lib/module/editor/protocol.js +2 -3
  19. package/lib/module/editor/protocol.js.map +1 -1
  20. package/lib/module/index.js +2 -0
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/typescript/components/TextMarker.d.ts +94 -0
  23. package/lib/typescript/components/TextMarker.d.ts.map +1 -0
  24. package/lib/typescript/editor/NoteEditor.d.ts +1 -1
  25. package/lib/typescript/editor/NoteEditor.d.ts.map +1 -1
  26. package/lib/typescript/editor/editorHtml.generated.d.ts.map +1 -1
  27. package/lib/typescript/editor/protocol.d.ts +2 -10
  28. package/lib/typescript/editor/protocol.d.ts.map +1 -1
  29. package/lib/typescript/index.d.ts +2 -0
  30. package/lib/typescript/index.d.ts.map +1 -1
  31. package/package.json +2 -2
  32. package/src/components/TextMarker.tsx +696 -0
  33. package/src/editor/NoteEditor.tsx +10 -192
  34. package/src/editor/editorHtml.generated.ts +1 -1
  35. package/src/editor/protocol.ts +2 -8
  36. package/src/index.ts +14 -0
@@ -13,12 +13,9 @@ import {
13
13
  import {
14
14
  Platform,
15
15
  Pressable,
16
- ScrollView,
17
16
  StyleSheet,
18
- Text as RNText,
19
17
  TextInput,
20
18
  View,
21
- type LayoutChangeEvent,
22
19
  type NativeSyntheticEvent,
23
20
  type StyleProp,
24
21
  type TextInputKeyPressEventData,
@@ -35,14 +32,10 @@ import type {
35
32
  EditorDocument,
36
33
  EditorSelectionState,
37
34
  EditorTheme,
38
- SlashCommandId,
39
- SlashMenuState,
40
35
  } from "@medway-ui/notes-core";
41
36
  import {
42
37
  EMPTY_DOCUMENT,
43
- EMPTY_SLASH_STATE,
44
38
  INITIAL_EDITOR_STATE,
45
- getSlashCommand,
46
39
  normalizeDocument,
47
40
  normalizeLinkInput,
48
41
  sanitizeImageSrc,
@@ -52,14 +45,17 @@ import {
52
45
  Bold,
53
46
  Code,
54
47
  DoubleQuotesL,
48
+ HeadingH1,
55
49
  HeadingH2,
56
50
  HeadingH3,
57
51
  Image as ImageIcon,
58
52
  Italic,
59
53
  Link as LinkIcon,
54
+ ListChecklist,
60
55
  ListOrdered,
61
56
  ListUnordered,
62
57
  Redo,
58
+ RemoveMinus,
63
59
  Strikethrough,
64
60
  Underline,
65
61
  WindowCodeBlock,
@@ -166,7 +162,7 @@ export type NoteEditorProps = {
166
162
  * picker was cancelled. **Different shape from the web prop**: there is no
167
163
  * file input here and this package carries no image-picker dependency, so
168
164
  * picking the file is the host's job. Providing it is what enables the
169
- * toolbar "Inserir imagem" button and the `/Imagem` slash command.
165
+ * toolbar "Inserir imagem" button.
170
166
  */
171
167
  onImageUpload?: () => Promise<{ src: string; alt?: string | null } | null>;
172
168
  /**
@@ -221,17 +217,6 @@ const nextTitle = (previous: string, value: string): string =>
221
217
 
222
218
  const TOOLBAR_BUTTON_SIZE = 32;
223
219
 
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
220
  const TOOLBAR_BUTTON_HIT_SLOP = getMinTouchHitSlop(TOOLBAR_BUTTON_SIZE);
236
221
 
237
222
  const MAX_WEBVIEW_REMOUNTS = 3;
@@ -314,8 +299,8 @@ type ToolbarItem = {
314
299
  };
315
300
 
316
301
  /**
317
- * Same commands, grouping and order as the web `NoteEditor`'s `TOOLBAR_GROUPS`.
318
- * `link` and `undo`/`redo` are filled in per-instance: they need the live
302
+ * The web `NoteEditor`'s `TOOLBAR_GROUPS`, plus Título 1, Checklist and Divisor:
303
+ * the web reaches those through the `/` menu, which mobile does not have. `link` and `undo`/`redo` are filled in per-instance: they need the live
319
304
  * `EditorSelectionState` and a dialog opener that only exist in the component.
320
305
  */
321
306
  const buildToolbarGroups = (
@@ -330,14 +315,17 @@ const buildToolbarGroups = (
330
315
  { id: "code", label: "Código", Icon: Code, command: { name: "toggleCode" }, activeKey: "code" },
331
316
  ],
332
317
  [
318
+ { id: "heading1", label: "Título 1", Icon: HeadingH1, command: { name: "toggleHeading", level: 1 }, activeKey: "heading1" },
333
319
  { id: "heading2", label: "Título 2", Icon: HeadingH2, command: { name: "toggleHeading", level: 2 }, activeKey: "heading2" },
334
320
  { id: "heading3", label: "Título 3", Icon: HeadingH3, command: { name: "toggleHeading", level: 3 }, activeKey: "heading3" },
335
321
  ],
336
322
  [
337
323
  { id: "bulletList", label: "Lista com marcadores", Icon: ListUnordered, command: { name: "toggleBulletList" }, activeKey: "bulletList" },
338
324
  { id: "orderedList", label: "Lista numerada", Icon: ListOrdered, command: { name: "toggleOrderedList" }, activeKey: "orderedList" },
325
+ { id: "taskList", label: "Checklist", Icon: ListChecklist, command: { name: "toggleTaskList" }, activeKey: "taskList" },
339
326
  { id: "blockquote", label: "Citação", Icon: DoubleQuotesL, command: { name: "toggleBlockquote" }, activeKey: "blockquote" },
340
327
  { id: "codeBlock", label: "Bloco de código", Icon: WindowCodeBlock, command: { name: "toggleCodeBlock" }, activeKey: "codeBlock" },
328
+ { id: "horizontalRule", label: "Divisor", Icon: RemoveMinus, command: { name: "setHorizontalRule" } },
341
329
  ],
342
330
  [{ id: "link", label: "Inserir link", Icon: LinkIcon, activeKey: "link", onPress: openLinkDialog }],
343
331
  [
@@ -346,17 +334,6 @@ const buildToolbarGroups = (
346
334
  ],
347
335
  ];
348
336
 
349
- /**
350
- * Unlike every other command — a pure ProseMirror op — "Imagem" needs a file
351
- * first, so it is only offered when the host gave us a way to resolve one.
352
- * Kept local because `notes-core` cannot know what a given host can do.
353
- */
354
- const visibleSlashCommandIds = (
355
- commandIds: SlashCommandId[],
356
- canUploadImage: boolean
357
- ): SlashCommandId[] =>
358
- canUploadImage ? commandIds : commandIds.filter((id) => id !== "image");
359
-
360
337
  const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
361
338
  (
362
339
  {
@@ -386,7 +363,6 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
386
363
  const [selectionState, setSelectionState] = useState<EditorSelectionState>(
387
364
  INITIAL_EDITOR_STATE
388
365
  );
389
- const [slashState, setSlashState] = useState<SlashMenuState>(EMPTY_SLASH_STATE);
390
366
  const [contentHeight, setContentHeight] = useState(height);
391
367
 
392
368
  const [title, setTitle] = useState(() => sanitizeTitle(initialTitle ?? ""));
@@ -428,7 +404,6 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
428
404
 
429
405
  const handleWebViewReset = useCallback(() => {
430
406
  setIsReady(false);
431
- setSlashState(EMPTY_SLASH_STATE);
432
407
  }, []);
433
408
 
434
409
  const [webViewKey, setWebViewKey] = useState(0);
@@ -573,9 +548,6 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
573
548
  case "state":
574
549
  setSelectionState(editorMessage.state);
575
550
  return;
576
- case "slash":
577
- setSlashState(editorMessage.slash);
578
- return;
579
551
  case "change":
580
552
  // Already debounced by the bundle (`CHANGE_DEBOUNCE_MS`). The extra
581
553
  // round trip happens only when a host asked for `onChange`, which
@@ -734,22 +706,8 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
734
706
  [selectionState, openLinkDialog]
735
707
  );
736
708
 
737
- const handleSlashSelect = useCallback(
738
- (id: SlashCommandId) => {
739
- if (!editable) return;
740
- send({ type: "runSlashCommand", commandId: id });
741
- if (id === "image") insertImage();
742
- },
743
- [editable, send, insertImage]
744
- );
745
-
746
709
  const containerHeight = isEmbedded ? contentHeight : undefined;
747
710
 
748
- const [editorWidth, setEditorWidth] = useState(0);
749
- const handleEditorLayout = useCallback((event: LayoutChangeEvent) => {
750
- setEditorWidth(event.nativeEvent.layout.width);
751
- }, []);
752
-
753
711
  const resolvedBackground = backgroundColor ?? theme.background;
754
712
 
755
713
  return (
@@ -792,10 +750,7 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
792
750
  style={[styles.title, { color: titleColor }]}
793
751
  />
794
752
 
795
- <View
796
- style={{ flex: isEmbedded ? undefined : 1 }}
797
- onLayout={handleEditorLayout}
798
- >
753
+ <View style={{ flex: isEmbedded ? undefined : 1 }}>
799
754
  <WebView
800
755
  key={webViewKey}
801
756
  ref={webViewRef}
@@ -845,15 +800,6 @@ const NoteEditor = forwardRef<NoteEditorHandle, NoteEditorProps>(
845
800
  { backgroundColor: resolvedBackground },
846
801
  ]}
847
802
  />
848
-
849
- {slashState.active && slashState.caret ? (
850
- <NoteEditorSlashMenu
851
- slash={slashState}
852
- editorWidth={editorWidth}
853
- canUploadImage={Boolean(onImageUpload)}
854
- onSelect={handleSlashSelect}
855
- />
856
- ) : null}
857
803
  </View>
858
804
 
859
805
  <Dialog open={linkDialogOpen} onOpenChange={setLinkDialogOpen}>
@@ -973,94 +919,6 @@ function NoteEditorToolbar({
973
919
  );
974
920
  }
975
921
 
976
- /**
977
- * Positioned from the `caret` rect the bundle reports. No arrow-key navigation:
978
- * a touch UI has no keyboard focus to drive one, so every item is tappable.
979
- *
980
- * `caret` is in CSS pixels inside the WebView viewport, treated here as `dp` —
981
- * valid as long as the WebView isn't scaled.
982
- */
983
- function NoteEditorSlashMenu({
984
- slash,
985
- editorWidth,
986
- canUploadImage,
987
- onSelect,
988
- }: {
989
- slash: SlashMenuState;
990
- editorWidth: number;
991
- canUploadImage: boolean;
992
- onSelect: (id: SlashCommandId) => void;
993
- }) {
994
- const surfaceColor = useEditorThemeColor("surface-elevated", "#FFFFFF");
995
- const borderColor = useThemeColor("gray-stroke-muted");
996
- const primaryText = useThemeColor("gray-foreground");
997
- const mutedText = useThemeColor("gray-foreground-muted");
998
- const activeBackground = useEditorThemeColor(
999
- "secondary-background-soft-emphasis",
1000
- "rgba(1, 208, 180, 0.08)"
1001
- );
1002
-
1003
- if (!slash.caret) return null;
1004
-
1005
- const commandIds = visibleSlashCommandIds(slash.commandIds, canUploadImage);
1006
-
1007
- return (
1008
- <View
1009
- style={[
1010
- styles.slashMenuShadow,
1011
- {
1012
- left: getSlashMenuLeft(slash.caret.x, editorWidth),
1013
- top: slash.caret.y + slash.caret.height + SLASH_MENU_GAP,
1014
- },
1015
- ]}
1016
- >
1017
- <View
1018
- style={[
1019
- styles.slashMenu,
1020
- { backgroundColor: surfaceColor, borderColor },
1021
- ]}
1022
- >
1023
- <ScrollView
1024
- style={styles.slashMenuScroll}
1025
- contentContainerStyle={styles.slashMenuContent}
1026
- keyboardShouldPersistTaps="handled"
1027
- showsVerticalScrollIndicator
1028
- >
1029
- {commandIds.length === 0 ? (
1030
- <RNText style={[styles.slashEmpty, { color: mutedText }]}>
1031
- Nenhum resultado
1032
- </RNText>
1033
- ) : (
1034
- commandIds.map((id) => {
1035
- const command = getSlashCommand(id);
1036
- if (!command) return null;
1037
-
1038
- return (
1039
- <Pressable
1040
- key={id}
1041
- accessibilityRole="button"
1042
- onPress={() => onSelect(id)}
1043
- style={({ pressed }) => [
1044
- styles.slashItem,
1045
- pressed ? { backgroundColor: activeBackground } : null,
1046
- ]}
1047
- >
1048
- <RNText style={[styles.slashItemLabel, { color: primaryText }]}>
1049
- {command.label}
1050
- </RNText>
1051
- <RNText style={[styles.slashItemDescription, { color: mutedText }]}>
1052
- {command.description}
1053
- </RNText>
1054
- </Pressable>
1055
- );
1056
- })
1057
- )}
1058
- </ScrollView>
1059
- </View>
1060
- </View>
1061
- );
1062
- }
1063
-
1064
922
  const styles = StyleSheet.create({
1065
923
  container: { flex: 1 },
1066
924
  containerEmbedded: { flex: 0 },
@@ -1104,46 +962,6 @@ const styles = StyleSheet.create({
1104
962
  toolbarButtonDisabled: {
1105
963
  opacity: 0.4,
1106
964
  },
1107
- slashMenuShadow: {
1108
- position: "absolute",
1109
- zIndex: 50,
1110
- width: SLASH_MENU_WIDTH,
1111
- shadowColor: "#000",
1112
- shadowOffset: { width: 0, height: 4 },
1113
- shadowOpacity: 0.14,
1114
- shadowRadius: 14,
1115
- elevation: 8,
1116
- },
1117
- slashMenu: {
1118
- borderRadius: 6,
1119
- borderWidth: StyleSheet.hairlineWidth,
1120
- overflow: "hidden",
1121
- },
1122
- slashMenuScroll: {
1123
- maxHeight: 288,
1124
- },
1125
- slashMenuContent: {
1126
- padding: 4,
1127
- gap: 12,
1128
- },
1129
- slashEmpty: {
1130
- paddingHorizontal: 12,
1131
- paddingVertical: 10,
1132
- fontSize: 13,
1133
- },
1134
- slashItem: {
1135
- paddingHorizontal: 12,
1136
- paddingVertical: 8,
1137
- borderRadius: 4,
1138
- },
1139
- slashItemLabel: {
1140
- fontSize: 14,
1141
- fontWeight: "500",
1142
- },
1143
- slashItemDescription: {
1144
- fontSize: 12,
1145
- marginTop: 2,
1146
- },
1147
965
  });
1148
966
 
1149
967
  export default memo(NoteEditor);