react-native-richify 1.0.4 → 1.0.5

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 (74) hide show
  1. package/lib/commonjs/components/RenderedOutput.js +168 -0
  2. package/lib/commonjs/components/RenderedOutput.js.map +1 -0
  3. package/lib/commonjs/components/RichTextInput.js +134 -15
  4. package/lib/commonjs/components/RichTextInput.js.map +1 -1
  5. package/lib/commonjs/components/Toolbar.js +41 -2
  6. package/lib/commonjs/components/Toolbar.js.map +1 -1
  7. package/lib/commonjs/constants/defaultStyles.js +55 -1
  8. package/lib/commonjs/constants/defaultStyles.js.map +1 -1
  9. package/lib/commonjs/hooks/useFormatting.js +40 -2
  10. package/lib/commonjs/hooks/useFormatting.js.map +1 -1
  11. package/lib/commonjs/hooks/useRichText.js +75 -6
  12. package/lib/commonjs/hooks/useRichText.js.map +1 -1
  13. package/lib/commonjs/utils/formatter.js +48 -9
  14. package/lib/commonjs/utils/formatter.js.map +1 -1
  15. package/lib/commonjs/utils/parser.js +1 -1
  16. package/lib/commonjs/utils/parser.js.map +1 -1
  17. package/lib/commonjs/utils/serializer.js +102 -6
  18. package/lib/commonjs/utils/serializer.js.map +1 -1
  19. package/lib/commonjs/utils/styleMapper.js +11 -0
  20. package/lib/commonjs/utils/styleMapper.js.map +1 -1
  21. package/lib/module/components/RenderedOutput.js +163 -0
  22. package/lib/module/components/RenderedOutput.js.map +1 -0
  23. package/lib/module/components/RichTextInput.js +135 -16
  24. package/lib/module/components/RichTextInput.js.map +1 -1
  25. package/lib/module/components/Toolbar.js +41 -2
  26. package/lib/module/components/Toolbar.js.map +1 -1
  27. package/lib/module/constants/defaultStyles.js +55 -1
  28. package/lib/module/constants/defaultStyles.js.map +1 -1
  29. package/lib/module/hooks/useFormatting.js +41 -3
  30. package/lib/module/hooks/useFormatting.js.map +1 -1
  31. package/lib/module/hooks/useRichText.js +75 -6
  32. package/lib/module/hooks/useRichText.js.map +1 -1
  33. package/lib/module/utils/formatter.js +46 -9
  34. package/lib/module/utils/formatter.js.map +1 -1
  35. package/lib/module/utils/parser.js +1 -1
  36. package/lib/module/utils/parser.js.map +1 -1
  37. package/lib/module/utils/serializer.js +102 -6
  38. package/lib/module/utils/serializer.js.map +1 -1
  39. package/lib/module/utils/styleMapper.js +11 -0
  40. package/lib/module/utils/styleMapper.js.map +1 -1
  41. package/lib/typescript/src/components/RenderedOutput.d.ts +9 -0
  42. package/lib/typescript/src/components/RenderedOutput.d.ts.map +1 -0
  43. package/lib/typescript/src/components/RichTextInput.d.ts.map +1 -1
  44. package/lib/typescript/src/components/Toolbar.d.ts.map +1 -1
  45. package/lib/typescript/src/constants/defaultStyles.d.ts +1 -0
  46. package/lib/typescript/src/constants/defaultStyles.d.ts.map +1 -1
  47. package/lib/typescript/src/hooks/useFormatting.d.ts +4 -1
  48. package/lib/typescript/src/hooks/useFormatting.d.ts.map +1 -1
  49. package/lib/typescript/src/hooks/useRichText.d.ts.map +1 -1
  50. package/lib/typescript/src/index.d.ts +1 -1
  51. package/lib/typescript/src/index.d.ts.map +1 -1
  52. package/lib/typescript/src/types/index.d.ts +94 -1
  53. package/lib/typescript/src/types/index.d.ts.map +1 -1
  54. package/lib/typescript/src/utils/formatter.d.ts +9 -1
  55. package/lib/typescript/src/utils/formatter.d.ts.map +1 -1
  56. package/lib/typescript/src/utils/parser.d.ts.map +1 -1
  57. package/lib/typescript/src/utils/serializer.d.ts.map +1 -1
  58. package/lib/typescript/src/utils/styleMapper.d.ts.map +1 -1
  59. package/package.json +1 -1
  60. package/src/components/RenderedOutput.tsx +231 -0
  61. package/src/components/RichTextInput.tsx +197 -19
  62. package/src/components/Toolbar.tsx +54 -2
  63. package/src/constants/defaultStyles.d.ts +2 -1
  64. package/src/constants/defaultStyles.ts +21 -0
  65. package/src/hooks/useFormatting.ts +76 -2
  66. package/src/hooks/useRichText.ts +101 -5
  67. package/src/index.d.ts +1 -1
  68. package/src/index.ts +4 -0
  69. package/src/types/index.d.ts +94 -1
  70. package/src/types/index.ts +104 -1
  71. package/src/utils/formatter.ts +60 -6
  72. package/src/utils/parser.ts +6 -1
  73. package/src/utils/serializer.ts +150 -8
  74. package/src/utils/styleMapper.ts +21 -0
@@ -2,10 +2,12 @@ import { useState, useCallback, useRef } from 'react';
2
2
  import type {
3
3
  StyledSegment,
4
4
  FormatStyle,
5
+ ListType,
5
6
  OutputFormat,
6
7
  SelectionRange,
7
8
  RichTextState,
8
9
  RichTextActions,
10
+ TextAlign,
9
11
  UseRichTextReturn,
10
12
  } from '../types';
11
13
  import { EMPTY_FORMAT_STYLE } from '../constants/defaultStyles';
@@ -91,10 +93,11 @@ export function useRichText(
91
93
  (newText: string) => {
92
94
  const currentSegments = segmentsRef.current;
93
95
  const currentSelection = selectionRef.current;
94
- const currentActiveStyles =
96
+ const currentActiveStyles = sanitizeTypingStyles(
95
97
  currentSelection.start === currentSelection.end
96
98
  ? activeStylesRef.current
97
- : getSelectionStyle(currentSegments, currentSelection);
99
+ : getSelectionStyle(currentSegments, currentSelection),
100
+ );
98
101
 
99
102
  const newSegments = reconcileTextChange(
100
103
  currentSegments,
@@ -135,7 +138,7 @@ export function useRichText(
135
138
  );
136
139
  if (segmentsRef.current.length > 0) {
137
140
  const seg = segmentsRef.current[pos.segmentIndex];
138
- setActiveStyles({ ...seg.styles });
141
+ setActiveStyles(sanitizeTypingStyles(seg.styles));
139
142
  }
140
143
  }
141
144
  },
@@ -164,15 +167,18 @@ export function useRichText(
164
167
  const safeSegments =
165
168
  newSegments.length > 0 ? newSegments : [createSegment('')];
166
169
  updateSegments(safeSegments);
170
+ handleSelectionChange({ start: 0, end: 0 });
171
+ setActiveStyles({ ...EMPTY_FORMAT_STYLE });
167
172
  },
168
- [updateSegments],
173
+ [handleSelectionChange, updateSegments],
169
174
  );
170
175
 
171
176
  const clear = useCallback(() => {
172
177
  updateSegments([createSegment('')]);
178
+ handleSelectionChange({ start: 0, end: 0 });
173
179
  setActiveStyles({ ...EMPTY_FORMAT_STYLE });
174
180
  preserveActiveStylesRef.current = false;
175
- }, [updateSegments]);
181
+ }, [handleSelectionChange, updateSegments]);
176
182
 
177
183
  const toggleFormat = useCallback<RichTextActions['toggleFormat']>(
178
184
  (format) => {
@@ -204,6 +210,36 @@ export function useRichText(
204
210
  [formatting],
205
211
  );
206
212
 
213
+ const setListType = useCallback<RichTextActions['setListType']>(
214
+ (type: ListType) => {
215
+ if (selectionRef.current.start === selectionRef.current.end) {
216
+ preserveActiveStylesRef.current = true;
217
+ }
218
+ formatting.setListType(type);
219
+ },
220
+ [formatting],
221
+ );
222
+
223
+ const setTextAlign = useCallback<RichTextActions['setTextAlign']>(
224
+ (align: TextAlign) => {
225
+ if (selectionRef.current.start === selectionRef.current.end) {
226
+ preserveActiveStylesRef.current = true;
227
+ }
228
+ formatting.setTextAlign(align);
229
+ },
230
+ [formatting],
231
+ );
232
+
233
+ const setLink = useCallback<RichTextActions['setLink']>(
234
+ (url) => {
235
+ if (selectionRef.current.start === selectionRef.current.end) {
236
+ preserveActiveStylesRef.current = true;
237
+ }
238
+ formatting.setLink(url);
239
+ },
240
+ [formatting],
241
+ );
242
+
207
243
  const setColor = useCallback<RichTextActions['setColor']>(
208
244
  (color) => {
209
245
  if (selectionRef.current.start === selectionRef.current.end) {
@@ -234,6 +270,41 @@ export function useRichText(
234
270
  [formatting],
235
271
  );
236
272
 
273
+ const insertImage = useCallback<RichTextActions['insertImage']>(
274
+ (source, options) => {
275
+ const currentSegments = segmentsRef.current;
276
+ const currentSelection = selectionRef.current;
277
+ const plainText = segmentsToPlainText(currentSegments);
278
+ const start = Math.min(currentSelection.start, currentSelection.end);
279
+ const end = Math.max(currentSelection.start, currentSelection.end);
280
+ const placeholder =
281
+ options?.placeholder ?? buildImagePlaceholder(source, options?.alt);
282
+ const nextText = `${plainText.slice(0, start)}${placeholder}${plainText.slice(end)}`;
283
+ const insertionStyles = sanitizeTypingStyles(
284
+ start === end
285
+ ? activeStylesRef.current
286
+ : getSelectionStyle(currentSegments, currentSelection),
287
+ );
288
+ const nextSegments = reconcileTextChange(
289
+ currentSegments,
290
+ nextText,
291
+ {
292
+ ...insertionStyles,
293
+ imageSrc: source,
294
+ imageAlt: options?.alt,
295
+ },
296
+ );
297
+
298
+ updateSegments(nextSegments);
299
+ handleSelectionChange({
300
+ start: start + placeholder.length,
301
+ end: start + placeholder.length,
302
+ });
303
+ preserveActiveStylesRef.current = false;
304
+ },
305
+ [handleSelectionChange, updateSegments],
306
+ );
307
+
237
308
  // ─── Build Return Value ──────────────────────────────────────────────────
238
309
 
239
310
  const state: RichTextState = {
@@ -246,6 +317,10 @@ export function useRichText(
246
317
  toggleFormat,
247
318
  setStyleProperty,
248
319
  setHeading,
320
+ setListType,
321
+ setTextAlign,
322
+ setLink,
323
+ insertImage,
249
324
  setColor,
250
325
  setBackgroundColor,
251
326
  setFontSize,
@@ -262,3 +337,24 @@ export function useRichText(
262
337
 
263
338
  return { state, actions };
264
339
  }
340
+
341
+ function sanitizeTypingStyles(style: FormatStyle): FormatStyle {
342
+ return {
343
+ ...style,
344
+ imageSrc: undefined,
345
+ imageAlt: undefined,
346
+ };
347
+ }
348
+
349
+ function buildImagePlaceholder(source: string, alt?: string): string {
350
+ if (alt && alt.trim().length > 0) {
351
+ return `[Image: ${alt.trim()}]`;
352
+ }
353
+
354
+ const fileName = source.split('/').pop()?.split('?')[0]?.trim();
355
+ if (fileName) {
356
+ return `[Image: ${fileName}]`;
357
+ }
358
+
359
+ return '[Image]';
360
+ }
package/src/index.d.ts CHANGED
@@ -13,4 +13,4 @@ export { toggleFormatOnSelection, setStyleOnSelection, setHeadingOnLine, isForma
13
13
  export { formatStyleToTextStyle, segmentToTextStyle, segmentsToTextStyles, } from './utils/styleMapper';
14
14
  export { serializeSegments, segmentsToMarkdown, segmentsToHTML, } from './utils/serializer';
15
15
  export { DEFAULT_COLORS, DEFAULT_THEME, DEFAULT_TOOLBAR_ITEMS, DEFAULT_BASE_TEXT_STYLE, HEADING_FONT_SIZES, EMPTY_FORMAT_STYLE, } from './constants/defaultStyles';
16
- export type { FormatType, HeadingLevel, ListType, OutputFormat, FormatStyle, StyledSegment, SelectionRange, RichTextState, RichTextActions, UseRichTextReturn, RichTextTheme, ToolbarItem, ToolbarButtonRenderProps, ToolbarRenderProps, OverlayTextProps, ToolbarButtonProps, ToolbarProps, RichTextInputProps, } from './types';
16
+ export type { FormatType, HeadingLevel, ListType, TextAlign, OutputFormat, OutputPreviewMode, FormatStyle, StyledSegment, SelectionRange, RichTextState, RichTextActions, UseRichTextReturn, RichTextTheme, ToolbarItem, ToolbarButtonRenderProps, ToolbarRenderProps, LinkRequestPayload, ImageRequestPayload, OverlayTextProps, ToolbarButtonProps, ToolbarProps, RichTextInputProps, } from './types';
package/src/index.ts CHANGED
@@ -58,7 +58,9 @@ export type {
58
58
  FormatType,
59
59
  HeadingLevel,
60
60
  ListType,
61
+ TextAlign,
61
62
  OutputFormat,
63
+ OutputPreviewMode,
62
64
  FormatStyle,
63
65
  StyledSegment,
64
66
  SelectionRange,
@@ -69,6 +71,8 @@ export type {
69
71
  ToolbarItem,
70
72
  ToolbarButtonRenderProps,
71
73
  ToolbarRenderProps,
74
+ LinkRequestPayload,
75
+ ImageRequestPayload,
72
76
  OverlayTextProps,
73
77
  ToolbarButtonProps,
74
78
  ToolbarProps,
@@ -11,10 +11,18 @@ export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'none';
11
11
  * List type for a line/paragraph.
12
12
  */
13
13
  export type ListType = 'bullet' | 'ordered' | 'none';
14
+ /**
15
+ * Paragraph alignment presets.
16
+ */
17
+ export type TextAlign = 'left' | 'center' | 'right';
14
18
  /**
15
19
  * Serialized output formats supported by the editor.
16
20
  */
17
21
  export type OutputFormat = 'markdown' | 'html';
22
+ /**
23
+ * Output preview modes supported by the editor.
24
+ */
25
+ export type OutputPreviewMode = 'literal' | 'rendered';
18
26
  /**
19
27
  * Inline formatting styles attached to a text segment.
20
28
  */
@@ -28,6 +36,11 @@ export interface FormatStyle {
28
36
  backgroundColor?: string;
29
37
  fontSize?: number;
30
38
  heading?: HeadingLevel;
39
+ listType?: ListType;
40
+ textAlign?: TextAlign;
41
+ link?: string;
42
+ imageSrc?: string;
43
+ imageAlt?: string;
31
44
  }
32
45
  /**
33
46
  * A segment of text with uniform formatting.
@@ -69,6 +82,17 @@ export interface RichTextActions {
69
82
  setStyleProperty: <K extends keyof FormatStyle>(key: K, value: FormatStyle[K]) => void;
70
83
  /** Apply a heading level to the current line. */
71
84
  setHeading: (level: HeadingLevel) => void;
85
+ /** Apply a list style to the current line. */
86
+ setListType: (type: ListType) => void;
87
+ /** Apply paragraph alignment to the current line. */
88
+ setTextAlign: (align: TextAlign) => void;
89
+ /** Apply or clear a hyperlink on the current selection. */
90
+ setLink: (url?: string) => void;
91
+ /** Insert an image placeholder into the document. */
92
+ insertImage: (source: string, options?: {
93
+ alt?: string;
94
+ placeholder?: string;
95
+ }) => void;
72
96
  /** Set the text color for the current selection. */
73
97
  setColor: (color: string) => void;
74
98
  /** Set the background color for the current selection. */
@@ -119,6 +143,8 @@ export interface RichTextTheme {
119
143
  outputLabelStyle?: TextStyle;
120
144
  /** Style for the serialized output text. */
121
145
  outputTextStyle?: TextStyle;
146
+ /** Style for the rendered output preview content. */
147
+ renderedOutputStyle?: ViewStyle;
122
148
  /** Style for the toolbar container. */
123
149
  toolbarStyle?: ViewStyle;
124
150
  /** Style for toolbar buttons. */
@@ -145,6 +171,8 @@ export interface RichTextTheme {
145
171
  toolbarBackground?: string;
146
172
  /** Toolbar border color. */
147
173
  toolbarBorder?: string;
174
+ /** Default link color. */
175
+ link?: string;
148
176
  /** Cursor / caret color. */
149
177
  cursor?: ColorValue;
150
178
  };
@@ -161,6 +189,16 @@ export interface ToolbarItem {
161
189
  format?: FormatType;
162
190
  /** The heading level this button sets. */
163
191
  heading?: HeadingLevel;
192
+ /** The list type this button sets. */
193
+ listType?: ListType;
194
+ /** The alignment this button sets. */
195
+ textAlign?: TextAlign;
196
+ /** The output format this button toggles to. */
197
+ outputFormat?: OutputFormat;
198
+ /** The output preview mode this button toggles to. */
199
+ outputPreviewMode?: OutputPreviewMode;
200
+ /** Special toolbar action. */
201
+ actionType?: 'link' | 'image';
164
202
  /** Custom action handler (overrides default behavior). */
165
203
  onPress?: () => void;
166
204
  /** Whether this item is currently active. */
@@ -183,6 +221,33 @@ export interface ToolbarRenderProps {
183
221
  items: ToolbarItem[];
184
222
  state: RichTextState;
185
223
  actions: RichTextActions;
224
+ outputFormat: OutputFormat;
225
+ outputPreviewMode: OutputPreviewMode;
226
+ onOutputFormatChange: (format: OutputFormat) => void;
227
+ onOutputPreviewModeChange: (mode: OutputPreviewMode) => void;
228
+ onRequestLink?: () => void;
229
+ onRequestImage?: () => void;
230
+ }
231
+ /**
232
+ * Payload passed when the built-in link button requests a URL.
233
+ */
234
+ export interface LinkRequestPayload {
235
+ /** Selected plain text at the time of the request. */
236
+ selectedText: string;
237
+ /** Existing URL on the selection, when present. */
238
+ currentUrl?: string;
239
+ /** Apply or clear the URL on the current selection. */
240
+ applyLink: (url?: string) => void;
241
+ }
242
+ /**
243
+ * Payload passed when the built-in image button requests an image source.
244
+ */
245
+ export interface ImageRequestPayload {
246
+ /** Insert an image placeholder into the document. */
247
+ insertImage: (source: string, options?: {
248
+ alt?: string;
249
+ placeholder?: string;
250
+ }) => void;
186
251
  }
187
252
  /**
188
253
  * Props for the OverlayText component.
@@ -224,6 +289,18 @@ export interface ToolbarProps {
224
289
  theme?: RichTextTheme;
225
290
  /** Whether to show the toolbar. */
226
291
  visible?: boolean;
292
+ /** Currently selected serialized output format. */
293
+ outputFormat?: OutputFormat;
294
+ /** Currently selected preview mode. */
295
+ outputPreviewMode?: OutputPreviewMode;
296
+ /** Called when the output format changes from the toolbar. */
297
+ onOutputFormatChange?: (format: OutputFormat) => void;
298
+ /** Called when the preview mode changes from the toolbar. */
299
+ onOutputPreviewModeChange?: (mode: OutputPreviewMode) => void;
300
+ /** Called when the link button is pressed. */
301
+ onRequestLink?: () => void;
302
+ /** Called when the image button is pressed. */
303
+ onRequestImage?: () => void;
227
304
  /** Custom render function for the entire toolbar. */
228
305
  renderToolbar?: (props: ToolbarRenderProps) => React.ReactElement | null;
229
306
  }
@@ -253,10 +330,26 @@ export interface RichTextInputProps {
253
330
  theme?: RichTextTheme;
254
331
  /** Whether to show the serialized output preview below the input. */
255
332
  showOutputPreview?: boolean;
256
- /** Format used for the serialized output preview. */
333
+ /** Controlled format used for the serialized output preview. */
257
334
  outputFormat?: OutputFormat;
335
+ /** Initial format used for the serialized output preview. */
336
+ defaultOutputFormat?: OutputFormat;
337
+ /** Controlled preview mode for the output panel. */
338
+ outputPreviewMode?: OutputPreviewMode;
339
+ /** Initial preview mode for the output panel. */
340
+ defaultOutputPreviewMode?: OutputPreviewMode;
341
+ /** Maximum height for the output preview panel. */
342
+ maxOutputHeight?: number;
258
343
  /** Callback when the serialized output changes. */
259
344
  onChangeOutput?: (output: string, format: OutputFormat) => void;
345
+ /** Callback when the output format changes. */
346
+ onChangeOutputFormat?: (format: OutputFormat) => void;
347
+ /** Callback when the output preview mode changes. */
348
+ onChangeOutputPreviewMode?: (mode: OutputPreviewMode) => void;
349
+ /** Invoked when the built-in link button needs a URL. */
350
+ onRequestLink?: (payload: LinkRequestPayload) => void;
351
+ /** Invoked when the built-in image button needs an image source. */
352
+ onRequestImage?: (payload: ImageRequestPayload) => void;
260
353
  /** Whether multiline input is enabled. */
261
354
  multiline?: boolean;
262
355
  /** Minimum height for the input area. */
@@ -22,11 +22,21 @@ export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'none';
22
22
  */
23
23
  export type ListType = 'bullet' | 'ordered' | 'none';
24
24
 
25
+ /**
26
+ * Paragraph alignment presets.
27
+ */
28
+ export type TextAlign = 'left' | 'center' | 'right';
29
+
25
30
  /**
26
31
  * Serialized output formats supported by the editor.
27
32
  */
28
33
  export type OutputFormat = 'markdown' | 'html';
29
34
 
35
+ /**
36
+ * Output preview modes supported by the editor.
37
+ */
38
+ export type OutputPreviewMode = 'literal' | 'rendered';
39
+
30
40
  // ─── Style Types ─────────────────────────────────────────────────────────────
31
41
 
32
42
  /**
@@ -42,6 +52,11 @@ export interface FormatStyle {
42
52
  backgroundColor?: string;
43
53
  fontSize?: number;
44
54
  heading?: HeadingLevel;
55
+ listType?: ListType;
56
+ textAlign?: TextAlign;
57
+ link?: string;
58
+ imageSrc?: string;
59
+ imageAlt?: string;
45
60
  }
46
61
 
47
62
  /**
@@ -96,6 +111,20 @@ export interface RichTextActions {
96
111
  ) => void;
97
112
  /** Apply a heading level to the current line. */
98
113
  setHeading: (level: HeadingLevel) => void;
114
+ /** Apply a list style to the current line. */
115
+ setListType: (type: ListType) => void;
116
+ /** Apply paragraph alignment to the current line. */
117
+ setTextAlign: (align: TextAlign) => void;
118
+ /** Apply or clear a hyperlink on the current selection. */
119
+ setLink: (url?: string) => void;
120
+ /** Insert an image placeholder into the document. */
121
+ insertImage: (
122
+ source: string,
123
+ options?: {
124
+ alt?: string;
125
+ placeholder?: string;
126
+ },
127
+ ) => void;
99
128
  /** Set the text color for the current selection. */
100
129
  setColor: (color: string) => void;
101
130
  /** Set the background color for the current selection. */
@@ -152,6 +181,8 @@ export interface RichTextTheme {
152
181
  outputLabelStyle?: TextStyle;
153
182
  /** Style for the serialized output text. */
154
183
  outputTextStyle?: TextStyle;
184
+ /** Style for the rendered output preview content. */
185
+ renderedOutputStyle?: ViewStyle;
155
186
  /** Style for the toolbar container. */
156
187
  toolbarStyle?: ViewStyle;
157
188
  /** Style for toolbar buttons. */
@@ -178,6 +209,8 @@ export interface RichTextTheme {
178
209
  toolbarBackground?: string;
179
210
  /** Toolbar border color. */
180
211
  toolbarBorder?: string;
212
+ /** Default link color. */
213
+ link?: string;
181
214
  /** Cursor / caret color. */
182
215
  cursor?: ColorValue;
183
216
  };
@@ -197,6 +230,16 @@ export interface ToolbarItem {
197
230
  format?: FormatType;
198
231
  /** The heading level this button sets. */
199
232
  heading?: HeadingLevel;
233
+ /** The list type this button sets. */
234
+ listType?: ListType;
235
+ /** The alignment this button sets. */
236
+ textAlign?: TextAlign;
237
+ /** The output format this button toggles to. */
238
+ outputFormat?: OutputFormat;
239
+ /** The output preview mode this button toggles to. */
240
+ outputPreviewMode?: OutputPreviewMode;
241
+ /** Special toolbar action. */
242
+ actionType?: 'link' | 'image';
200
243
  /** Custom action handler (overrides default behavior). */
201
244
  onPress?: () => void;
202
245
  /** Whether this item is currently active. */
@@ -221,6 +264,38 @@ export interface ToolbarRenderProps {
221
264
  items: ToolbarItem[];
222
265
  state: RichTextState;
223
266
  actions: RichTextActions;
267
+ outputFormat: OutputFormat;
268
+ outputPreviewMode: OutputPreviewMode;
269
+ onOutputFormatChange: (format: OutputFormat) => void;
270
+ onOutputPreviewModeChange: (mode: OutputPreviewMode) => void;
271
+ onRequestLink?: () => void;
272
+ onRequestImage?: () => void;
273
+ }
274
+
275
+ /**
276
+ * Payload passed when the built-in link button requests a URL.
277
+ */
278
+ export interface LinkRequestPayload {
279
+ /** Selected plain text at the time of the request. */
280
+ selectedText: string;
281
+ /** Existing URL on the selection, when present. */
282
+ currentUrl?: string;
283
+ /** Apply or clear the URL on the current selection. */
284
+ applyLink: (url?: string) => void;
285
+ }
286
+
287
+ /**
288
+ * Payload passed when the built-in image button requests an image source.
289
+ */
290
+ export interface ImageRequestPayload {
291
+ /** Insert an image placeholder into the document. */
292
+ insertImage: (
293
+ source: string,
294
+ options?: {
295
+ alt?: string;
296
+ placeholder?: string;
297
+ },
298
+ ) => void;
224
299
  }
225
300
 
226
301
  // ─── Component Props ─────────────────────────────────────────────────────────
@@ -267,6 +342,18 @@ export interface ToolbarProps {
267
342
  theme?: RichTextTheme;
268
343
  /** Whether to show the toolbar. */
269
344
  visible?: boolean;
345
+ /** Currently selected serialized output format. */
346
+ outputFormat?: OutputFormat;
347
+ /** Currently selected preview mode. */
348
+ outputPreviewMode?: OutputPreviewMode;
349
+ /** Called when the output format changes from the toolbar. */
350
+ onOutputFormatChange?: (format: OutputFormat) => void;
351
+ /** Called when the preview mode changes from the toolbar. */
352
+ onOutputPreviewModeChange?: (mode: OutputPreviewMode) => void;
353
+ /** Called when the link button is pressed. */
354
+ onRequestLink?: () => void;
355
+ /** Called when the image button is pressed. */
356
+ onRequestImage?: () => void;
270
357
  /** Custom render function for the entire toolbar. */
271
358
  renderToolbar?: (props: ToolbarRenderProps) => React.ReactElement | null;
272
359
  }
@@ -297,10 +384,26 @@ export interface RichTextInputProps {
297
384
  theme?: RichTextTheme;
298
385
  /** Whether to show the serialized output preview below the input. */
299
386
  showOutputPreview?: boolean;
300
- /** Format used for the serialized output preview. */
387
+ /** Controlled format used for the serialized output preview. */
301
388
  outputFormat?: OutputFormat;
389
+ /** Initial format used for the serialized output preview. */
390
+ defaultOutputFormat?: OutputFormat;
391
+ /** Controlled preview mode for the output panel. */
392
+ outputPreviewMode?: OutputPreviewMode;
393
+ /** Initial preview mode for the output panel. */
394
+ defaultOutputPreviewMode?: OutputPreviewMode;
395
+ /** Maximum height for the output preview panel. */
396
+ maxOutputHeight?: number;
302
397
  /** Callback when the serialized output changes. */
303
398
  onChangeOutput?: (output: string, format: OutputFormat) => void;
399
+ /** Callback when the output format changes. */
400
+ onChangeOutputFormat?: (format: OutputFormat) => void;
401
+ /** Callback when the output preview mode changes. */
402
+ onChangeOutputPreviewMode?: (mode: OutputPreviewMode) => void;
403
+ /** Invoked when the built-in link button needs a URL. */
404
+ onRequestLink?: (payload: LinkRequestPayload) => void;
405
+ /** Invoked when the built-in image button needs an image source. */
406
+ onRequestImage?: (payload: ImageRequestPayload) => void;
304
407
  /** Whether multiline input is enabled. */
305
408
  multiline?: boolean;
306
409
  /** Minimum height for the input area. */
@@ -3,7 +3,9 @@ import type {
3
3
  FormatType,
4
4
  FormatStyle,
5
5
  HeadingLevel,
6
+ ListType,
6
7
  SelectionRange,
8
+ TextAlign,
7
9
  } from '../types';
8
10
  import {
9
11
  createSegment,
@@ -67,14 +69,37 @@ export function setHeadingOnLine(
67
69
  selection: SelectionRange,
68
70
  level: HeadingLevel,
69
71
  ): StyledSegment[] {
70
- const plainText = segmentsToPlainText(segments);
71
- const { lineStart, lineEnd } = getLineRange(plainText, selection.start);
72
-
73
- const headingStyle: Partial<FormatStyle> = {
72
+ return setLineStyleOnSelection(segments, selection, {
74
73
  heading: level === 'none' ? undefined : level,
75
- };
74
+ listType: level === 'none' ? undefined : undefined,
75
+ });
76
+ }
76
77
 
77
- return applyStyleToRange(segments, lineStart, lineEnd, headingStyle);
78
+ /**
79
+ * Apply a list type to the lines containing the cursor/selection.
80
+ */
81
+ export function setListTypeOnLine(
82
+ segments: StyledSegment[],
83
+ selection: SelectionRange,
84
+ listType: ListType,
85
+ ): StyledSegment[] {
86
+ return setLineStyleOnSelection(segments, selection, {
87
+ listType: listType === 'none' ? undefined : listType,
88
+ heading: listType === 'none' ? undefined : undefined,
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Apply text alignment to the lines containing the cursor/selection.
94
+ */
95
+ export function setTextAlignOnLine(
96
+ segments: StyledSegment[],
97
+ selection: SelectionRange,
98
+ textAlign: TextAlign,
99
+ ): StyledSegment[] {
100
+ return setLineStyleOnSelection(segments, selection, {
101
+ textAlign,
102
+ });
78
103
  }
79
104
 
80
105
  /**
@@ -138,6 +163,11 @@ export function getSelectionStyle(
138
163
  result.backgroundColor = undefined;
139
164
  if (result.fontSize !== s.fontSize) result.fontSize = undefined;
140
165
  if (result.heading !== s.heading) result.heading = undefined;
166
+ if (result.listType !== s.listType) result.listType = undefined;
167
+ if (result.textAlign !== s.textAlign) result.textAlign = undefined;
168
+ if (result.link !== s.link) result.link = undefined;
169
+ if (result.imageSrc !== s.imageSrc) result.imageSrc = undefined;
170
+ if (result.imageAlt !== s.imageAlt) result.imageAlt = undefined;
141
171
  }
142
172
 
143
173
  return result;
@@ -248,6 +278,17 @@ function applyStyleToRange(
248
278
  return mergeAdjacentSegments(result);
249
279
  }
250
280
 
281
+ function setLineStyleOnSelection(
282
+ segments: StyledSegment[],
283
+ selection: SelectionRange,
284
+ styleDelta: Partial<FormatStyle>,
285
+ ): StyledSegment[] {
286
+ const plainText = segmentsToPlainText(segments);
287
+ const { lineStart, lineEnd } = getSelectionLineRange(plainText, selection);
288
+
289
+ return applyStyleToRange(segments, lineStart, lineEnd, styleDelta);
290
+ }
291
+
251
292
  /**
252
293
  * Get the line start and end positions for the line containing the given position.
253
294
  */
@@ -268,5 +309,18 @@ function getLineRange(
268
309
  return { lineStart, lineEnd };
269
310
  }
270
311
 
312
+ function getSelectionLineRange(
313
+ text: string,
314
+ selection: SelectionRange,
315
+ ): { lineStart: number; lineEnd: number } {
316
+ const normalized = normalizeSelection(selection);
317
+ const lineStart = getLineRange(text, normalized.start).lineStart;
318
+ const effectiveEnd =
319
+ normalized.end > normalized.start ? normalized.end - 1 : normalized.end;
320
+ const lineEnd = getLineRange(text, effectiveEnd).lineEnd;
321
+
322
+ return { lineStart, lineEnd };
323
+ }
324
+
271
325
  // Re-export for convenience
272
326
  export { createSegment } from '../utils/parser';
@@ -77,7 +77,12 @@ export function areStylesEqual(a: FormatStyle, b: FormatStyle): boolean {
77
77
  (a.color ?? undefined) === (b.color ?? undefined) &&
78
78
  (a.backgroundColor ?? undefined) === (b.backgroundColor ?? undefined) &&
79
79
  (a.fontSize ?? undefined) === (b.fontSize ?? undefined) &&
80
- (a.heading ?? undefined) === (b.heading ?? undefined)
80
+ (a.heading ?? undefined) === (b.heading ?? undefined) &&
81
+ (a.listType ?? undefined) === (b.listType ?? undefined) &&
82
+ (a.textAlign ?? undefined) === (b.textAlign ?? undefined) &&
83
+ (a.link ?? undefined) === (b.link ?? undefined) &&
84
+ (a.imageSrc ?? undefined) === (b.imageSrc ?? undefined) &&
85
+ (a.imageAlt ?? undefined) === (b.imageAlt ?? undefined)
81
86
  );
82
87
  }
83
88