panelui-native 0.46.0 → 0.51.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 (87) hide show
  1. package/README.md +12 -1
  2. package/lib/module/components/accordion/index.js +32 -4
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/button/index.js +83 -15
  5. package/lib/module/components/button/index.js.map +1 -1
  6. package/lib/module/components/button-group/index.js +186 -0
  7. package/lib/module/components/button-group/index.js.map +1 -0
  8. package/lib/module/components/color-picker/index.js +110 -1
  9. package/lib/module/components/color-picker/index.js.map +1 -1
  10. package/lib/module/components/date-time-picker/index.js +23 -5
  11. package/lib/module/components/date-time-picker/index.js.map +1 -1
  12. package/lib/module/components/fab/index.js +514 -0
  13. package/lib/module/components/fab/index.js.map +1 -0
  14. package/lib/module/components/markdown-editor/index.js +406 -0
  15. package/lib/module/components/markdown-editor/index.js.map +1 -0
  16. package/lib/module/components/markdown-editor/markdown-transforms.js +243 -0
  17. package/lib/module/components/markdown-editor/markdown-transforms.js.map +1 -0
  18. package/lib/module/components/questionnaire/index.js +1312 -0
  19. package/lib/module/components/questionnaire/index.js.map +1 -0
  20. package/lib/module/components/sortable/index.js +806 -0
  21. package/lib/module/components/sortable/index.js.map +1 -0
  22. package/lib/module/components/swipe/index.js +140 -5
  23. package/lib/module/components/swipe/index.js.map +1 -1
  24. package/lib/module/components/tabs/index.js +94 -18
  25. package/lib/module/components/tabs/index.js.map +1 -1
  26. package/lib/module/components/time-picker/index.js +34 -6
  27. package/lib/module/components/time-picker/index.js.map +1 -1
  28. package/lib/module/components/tree/index.js +500 -0
  29. package/lib/module/components/tree/index.js.map +1 -0
  30. package/lib/module/icons/index.js +246 -0
  31. package/lib/module/icons/index.js.map +1 -1
  32. package/lib/module/index.js +8 -2
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/utils/haptics.js +19 -0
  35. package/lib/module/utils/haptics.js.map +1 -1
  36. package/lib/typescript/src/components/accordion/index.d.ts +21 -0
  37. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  38. package/lib/typescript/src/components/button/index.d.ts +21 -0
  39. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  40. package/lib/typescript/src/components/button-group/index.d.ts +212 -0
  41. package/lib/typescript/src/components/button-group/index.d.ts.map +1 -0
  42. package/lib/typescript/src/components/color-picker/index.d.ts +82 -1
  43. package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -1
  44. package/lib/typescript/src/components/date-time-picker/index.d.ts +7 -1
  45. package/lib/typescript/src/components/date-time-picker/index.d.ts.map +1 -1
  46. package/lib/typescript/src/components/fab/index.d.ts +285 -0
  47. package/lib/typescript/src/components/fab/index.d.ts.map +1 -0
  48. package/lib/typescript/src/components/markdown-editor/index.d.ts +102 -0
  49. package/lib/typescript/src/components/markdown-editor/index.d.ts.map +1 -0
  50. package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts +76 -0
  51. package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts.map +1 -0
  52. package/lib/typescript/src/components/questionnaire/index.d.ts +336 -0
  53. package/lib/typescript/src/components/questionnaire/index.d.ts.map +1 -0
  54. package/lib/typescript/src/components/sortable/index.d.ts +238 -0
  55. package/lib/typescript/src/components/sortable/index.d.ts.map +1 -0
  56. package/lib/typescript/src/components/swipe/index.d.ts +35 -0
  57. package/lib/typescript/src/components/swipe/index.d.ts.map +1 -1
  58. package/lib/typescript/src/components/tabs/index.d.ts +9 -0
  59. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  60. package/lib/typescript/src/components/time-picker/index.d.ts +19 -1
  61. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
  62. package/lib/typescript/src/components/tree/index.d.ts +125 -0
  63. package/lib/typescript/src/components/tree/index.d.ts.map +1 -0
  64. package/lib/typescript/src/icons/index.d.ts +30 -0
  65. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  66. package/lib/typescript/src/index.d.ts +10 -4
  67. package/lib/typescript/src/index.d.ts.map +1 -1
  68. package/lib/typescript/src/utils/haptics.d.ts +14 -0
  69. package/lib/typescript/src/utils/haptics.d.ts.map +1 -1
  70. package/package.json +1 -1
  71. package/src/components/accordion/index.tsx +48 -6
  72. package/src/components/button/index.tsx +97 -15
  73. package/src/components/button-group/index.tsx +199 -0
  74. package/src/components/color-picker/index.tsx +140 -3
  75. package/src/components/date-time-picker/index.tsx +39 -3
  76. package/src/components/fab/index.tsx +583 -0
  77. package/src/components/markdown-editor/index.tsx +526 -0
  78. package/src/components/markdown-editor/markdown-transforms.ts +228 -0
  79. package/src/components/questionnaire/index.tsx +1615 -0
  80. package/src/components/sortable/index.tsx +1080 -0
  81. package/src/components/swipe/index.tsx +165 -3
  82. package/src/components/tabs/index.tsx +100 -28
  83. package/src/components/time-picker/index.tsx +42 -6
  84. package/src/components/tree/index.tsx +564 -0
  85. package/src/icons/index.tsx +176 -0
  86. package/src/index.ts +89 -1
  87. package/src/utils/haptics.ts +21 -0
@@ -0,0 +1,526 @@
1
+ /**
2
+ * MarkdownEditor — a field for writing markdown, and a way to see it rendered.
3
+ *
4
+ * ```tsx
5
+ * const [draft, setDraft] = useState('');
6
+ *
7
+ * <MarkdownEditor value={draft} onValueChange={setDraft} rows={12} />
8
+ * ```
9
+ *
10
+ * ## Writing and reading are two modes, not two panes
11
+ *
12
+ * Side by side is how this is done on a desktop, and it does not survive the
13
+ * trip to a phone: two columns of a phone's width are two columns too narrow to
14
+ * read, and the keyboard covers the bottom half of the screen exactly when the
15
+ * writer is using it. So there is one pane and a switch — write, or read what
16
+ * you wrote. The toolbar carries the switch, because the toolbar is the one
17
+ * thing on screen in both modes.
18
+ *
19
+ * ## The toolbar edits the selection
20
+ *
21
+ * Every button here is a function of the text and where the caret is in it, and
22
+ * the answer that matters is not which characters get inserted but where the
23
+ * caret lands afterwards. Bolding a selected phrase leaves it selected, so it
24
+ * can be italicised next; bolding nothing puts the caret between the new
25
+ * markers, where the writing is about to go; and pressing the same button again
26
+ * takes the markers off. Those rules live in `markdown-transforms.ts` as pure
27
+ * functions over `(text, selection)`, away from anything to do with a field.
28
+ *
29
+ * ## Why it renders through Response
30
+ *
31
+ * The preview is `Response`, the same reader that renders a model's answer, so
32
+ * markdown means the same thing everywhere in the library and there is one
33
+ * parser to be right rather than two to keep in step. It renders through
34
+ * `Typography`, `CodeBlock` and `Table`, which is to say through the app's own
35
+ * type and colours — a preview that looked like a document viewer would be
36
+ * showing the writer something they are not going to ship.
37
+ *
38
+ * Nothing here reaches for a platform markdown renderer. `Text` in SwiftUI can
39
+ * parse markdown and Jetpack Compose's cannot, so a platform-backed editor
40
+ * would render on one of the two and be a plain string on the other.
41
+ *
42
+ * Works controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
43
+ */
44
+ import {
45
+ createContext,
46
+ forwardRef,
47
+ useCallback,
48
+ useContext,
49
+ useMemo,
50
+ useRef,
51
+ useState,
52
+ type ReactNode,
53
+ } from 'react';
54
+ import {
55
+ View,
56
+ type NativeSyntheticEvent,
57
+ type TextInput,
58
+ type TextInputSelectionChangeEventData,
59
+ type ViewProps,
60
+ } from 'react-native';
61
+ import { tv } from 'tailwind-variants';
62
+ import {
63
+ BoldIcon,
64
+ CodeIcon,
65
+ EyeIcon,
66
+ HeadingIcon,
67
+ ItalicIcon,
68
+ LinkIcon,
69
+ ListIcon,
70
+ ListOrderedIcon,
71
+ PencilIcon,
72
+ QuoteIcon,
73
+ } from '../../icons';
74
+ import { Text } from '../../primitives/text';
75
+ import { cn } from '../../utils/cn';
76
+ import { Button } from '../button';
77
+ import { ButtonGroup } from '../button-group';
78
+ import { Response } from '../response';
79
+ import { Textarea, type TextareaProps } from '../textarea';
80
+ import {
81
+ insertLink,
82
+ toggleFence,
83
+ toggleLinePrefix,
84
+ toggleOrderedList,
85
+ toggleWrap,
86
+ type EditResult,
87
+ type EditorSelection,
88
+ } from './markdown-transforms';
89
+
90
+ export type { EditorSelection, EditResult } from './markdown-transforms';
91
+
92
+ /** Which pane the editor is showing. */
93
+ export type MarkdownEditorMode = 'write' | 'preview';
94
+
95
+ /** A formatting action the toolbar can offer. */
96
+ export type MarkdownEditorAction =
97
+ | 'bold'
98
+ | 'italic'
99
+ | 'heading'
100
+ | 'quote'
101
+ | 'code'
102
+ | 'bulletList'
103
+ | 'orderedList'
104
+ | 'link';
105
+
106
+ /** The actions a toolbar shows when it is not told which to show. */
107
+ const DEFAULT_ACTIONS: MarkdownEditorAction[] = [
108
+ 'bold',
109
+ 'italic',
110
+ 'heading',
111
+ 'bulletList',
112
+ 'orderedList',
113
+ 'quote',
114
+ 'code',
115
+ 'link',
116
+ ];
117
+
118
+ /**
119
+ * What each action does, what it is called, and what it looks like.
120
+ *
121
+ * One table rather than a switch in the toolbar and another in the context:
122
+ * adding an action should be adding a row here, and nowhere else.
123
+ */
124
+ const ACTIONS: Record<
125
+ MarkdownEditorAction,
126
+ {
127
+ label: string;
128
+ icon: (size: number) => ReactNode;
129
+ apply: (text: string, selection: EditorSelection) => EditResult;
130
+ }
131
+ > = {
132
+ bold: {
133
+ label: 'Bold',
134
+ icon: (size) => <BoldIcon size={size} />,
135
+ apply: (text, selection) => toggleWrap(text, selection, '**'),
136
+ },
137
+ italic: {
138
+ label: 'Italic',
139
+ icon: (size) => <ItalicIcon size={size} />,
140
+ apply: (text, selection) => toggleWrap(text, selection, '_'),
141
+ },
142
+ heading: {
143
+ label: 'Heading',
144
+ icon: (size) => <HeadingIcon size={size} />,
145
+ apply: (text, selection) => toggleLinePrefix(text, selection, '## '),
146
+ },
147
+ quote: {
148
+ label: 'Quote',
149
+ icon: (size) => <QuoteIcon size={size} />,
150
+ apply: (text, selection) => toggleLinePrefix(text, selection, '> '),
151
+ },
152
+ code: {
153
+ label: 'Code block',
154
+ icon: (size) => <CodeIcon size={size} />,
155
+ apply: toggleFence,
156
+ },
157
+ bulletList: {
158
+ label: 'Bulleted list',
159
+ icon: (size) => <ListIcon size={size} />,
160
+ apply: (text, selection) => toggleLinePrefix(text, selection, '- '),
161
+ },
162
+ orderedList: {
163
+ label: 'Numbered list',
164
+ icon: (size) => <ListOrderedIcon size={size} />,
165
+ apply: toggleOrderedList,
166
+ },
167
+ link: {
168
+ label: 'Link',
169
+ icon: (size) => <LinkIcon size={size} />,
170
+ apply: insertLink,
171
+ },
172
+ };
173
+
174
+ const markdownEditorVariants = tv({
175
+ slots: {
176
+ root: 'w-full gap-2',
177
+ toolbar: 'flex-row items-center justify-between gap-2',
178
+ // The preview stands in the field's place, so it takes the field's shape.
179
+ // A rendered draft that sat on the page with no edge around it would not
180
+ // read as the same object the writing was in.
181
+ preview: 'w-full rounded-xl border border-input bg-popover px-3 py-2.5',
182
+ empty: 'py-6 text-center',
183
+ },
184
+ });
185
+
186
+ interface MarkdownEditorContextValue {
187
+ value: string;
188
+ mode: MarkdownEditorMode;
189
+ setMode: (mode: MarkdownEditorMode) => void;
190
+ /** Runs a transform against the live text and selection. */
191
+ run: (action: MarkdownEditorAction) => void;
192
+ /** What the field should be showing as selected, once, after an edit. */
193
+ pending: EditorSelection | undefined;
194
+ setSelection: (selection: EditorSelection) => void;
195
+ clearPending: () => void;
196
+ onChangeText: (next: string) => void;
197
+ disabled: boolean;
198
+ }
199
+
200
+ const MarkdownEditorContext = createContext<MarkdownEditorContextValue | null>(null);
201
+
202
+ function useMarkdownEditor(part: string): MarkdownEditorContextValue {
203
+ const ctx = useContext(MarkdownEditorContext);
204
+ if (!ctx) throw new Error(`${part} must be used inside <MarkdownEditor>.`);
205
+ return ctx;
206
+ }
207
+
208
+ export interface MarkdownEditorProps extends Omit<ViewProps, 'children'> {
209
+ className?: string;
210
+ /** Controlled text. Leave unset and pass `defaultValue` to run uncontrolled. */
211
+ value?: string;
212
+ /** Starting text when uncontrolled. */
213
+ defaultValue?: string;
214
+ onValueChange?: (value: string) => void;
215
+ /** Controlled pane. */
216
+ mode?: MarkdownEditorMode;
217
+ /** Starting pane when uncontrolled. */
218
+ defaultMode?: MarkdownEditorMode;
219
+ onModeChange?: (mode: MarkdownEditorMode) => void;
220
+ /** Stop the field being edited and the toolbar being pressed. */
221
+ disabled?: boolean;
222
+ /**
223
+ * The parts, in the order they should stack. Left out, the editor draws its
224
+ * toolbar, its field and its preview in that order — which is the whole
225
+ * component, and the reason it usually needs no children.
226
+ */
227
+ children?: ReactNode;
228
+ /** Forwarded to the field when the editor draws its own. */
229
+ placeholder?: string;
230
+ /** Height of the field, in lines. Forwarded to the field the editor draws. */
231
+ rows?: number;
232
+ }
233
+
234
+ const MarkdownEditorRoot = forwardRef<View, MarkdownEditorProps>(
235
+ (
236
+ {
237
+ className,
238
+ value: valueProp,
239
+ defaultValue = '',
240
+ onValueChange,
241
+ mode: modeProp,
242
+ defaultMode = 'write',
243
+ onModeChange,
244
+ disabled = false,
245
+ children,
246
+ placeholder,
247
+ rows,
248
+ ...props
249
+ },
250
+ ref
251
+ ) => {
252
+ const slots = markdownEditorVariants();
253
+
254
+ const [internalValue, setInternalValue] = useState(defaultValue);
255
+ const value = valueProp ?? internalValue;
256
+
257
+ const [internalMode, setInternalMode] = useState<MarkdownEditorMode>(defaultMode);
258
+ const mode = modeProp ?? internalMode;
259
+
260
+ /*
261
+ * The live selection is a ref rather than state.
262
+ *
263
+ * It changes on every caret move — every keystroke, every tap in the text —
264
+ * and nothing on screen depends on it. Holding it in state would re-render
265
+ * the field, and therefore the preview under it, once per character typed.
266
+ */
267
+ const selectionRef = useRef<EditorSelection>({ start: 0, end: 0 });
268
+
269
+ /*
270
+ * What the field should show as selected after a toolbar edit, held for
271
+ * exactly one render.
272
+ *
273
+ * A `selection` prop that stays set is a field the writer cannot move the
274
+ * caret in — every tap would be pulled back to wherever the last edit put
275
+ * it. So it is set once, applied, and cleared by the very selection change
276
+ * it causes.
277
+ */
278
+ const [pending, setPending] = useState<EditorSelection | undefined>(undefined);
279
+
280
+ const valueRef = useRef(value);
281
+ valueRef.current = value;
282
+
283
+ const commit = useCallback(
284
+ (next: string) => {
285
+ if (valueProp === undefined) setInternalValue(next);
286
+ onValueChange?.(next);
287
+ },
288
+ [valueProp, onValueChange]
289
+ );
290
+
291
+ const setMode = useCallback(
292
+ (next: MarkdownEditorMode) => {
293
+ if (modeProp === undefined) setInternalMode(next);
294
+ onModeChange?.(next);
295
+ },
296
+ [modeProp, onModeChange]
297
+ );
298
+
299
+ const run = useCallback(
300
+ (action: MarkdownEditorAction) => {
301
+ const result = ACTIONS[action].apply(valueRef.current, selectionRef.current);
302
+ // Kept locally too: the parent may be slow to hand the new text back,
303
+ // and the next press must not be computed against the old string.
304
+ valueRef.current = result.text;
305
+ selectionRef.current = result.selection;
306
+ commit(result.text);
307
+ setPending(result.selection);
308
+ },
309
+ [commit]
310
+ );
311
+
312
+ const context = useMemo<MarkdownEditorContextValue>(
313
+ () => ({
314
+ value,
315
+ mode,
316
+ setMode,
317
+ run,
318
+ pending,
319
+ setSelection: (selection) => {
320
+ selectionRef.current = selection;
321
+ },
322
+ clearPending: () => setPending(undefined),
323
+ onChangeText: (next) => {
324
+ valueRef.current = next;
325
+ commit(next);
326
+ },
327
+ disabled,
328
+ }),
329
+ [value, mode, setMode, run, pending, commit, disabled]
330
+ );
331
+
332
+ return (
333
+ <MarkdownEditorContext.Provider value={context}>
334
+ <View ref={ref} className={slots.root({ className })} {...props}>
335
+ {children ?? (
336
+ <>
337
+ <MarkdownEditorToolbar />
338
+ <MarkdownEditorInput placeholder={placeholder} rows={rows} />
339
+ <MarkdownEditorPreview />
340
+ </>
341
+ )}
342
+ </View>
343
+ </MarkdownEditorContext.Provider>
344
+ );
345
+ }
346
+ );
347
+
348
+ MarkdownEditorRoot.displayName = 'MarkdownEditor';
349
+
350
+ /* -------------------------------------------------------------------------- *
351
+ * Toolbar
352
+ * -------------------------------------------------------------------------- */
353
+
354
+ export interface MarkdownEditorToolbarProps extends Omit<ViewProps, 'children'> {
355
+ className?: string;
356
+ /** Which formatting actions to offer, in the order given. */
357
+ actions?: MarkdownEditorAction[];
358
+ /**
359
+ * Show the write/preview switch. On by default — a preview nobody can reach
360
+ * is a pane that does not exist.
361
+ */
362
+ showModeSwitch?: boolean;
363
+ /** Anything else to put on the row: a word count, a save state, a submit. */
364
+ children?: ReactNode;
365
+ }
366
+
367
+ /**
368
+ * The formatting actions, and the switch between writing and reading.
369
+ *
370
+ * The formatting run is hidden rather than disabled while previewing. Its
371
+ * buttons act on a selection, and in the preview there is no selection for them
372
+ * to act on — a row of controls that are present but inert is a row that has to
373
+ * be tried before it can be understood.
374
+ */
375
+ const MarkdownEditorToolbar = forwardRef<View, MarkdownEditorToolbarProps>(
376
+ ({ className, actions = DEFAULT_ACTIONS, showModeSwitch = true, children, ...props }, ref) => {
377
+ const { mode, setMode, run, disabled } = useMarkdownEditor('MarkdownEditor.Toolbar');
378
+ const slots = markdownEditorVariants();
379
+
380
+ return (
381
+ <View ref={ref} className={slots.toolbar({ className })} {...props}>
382
+ {mode === 'write' ? (
383
+ <ButtonGroup variant="ghost" size="sm" className="shrink">
384
+ {actions.map((action) => (
385
+ <Button
386
+ key={action}
387
+ accessibilityLabel={ACTIONS[action].label}
388
+ disabled={disabled}
389
+ onPress={() => run(action)}
390
+ className="px-2"
391
+ >
392
+ {ACTIONS[action].icon(16)}
393
+ </Button>
394
+ ))}
395
+ </ButtonGroup>
396
+ ) : (
397
+ <View className="shrink" />
398
+ )}
399
+
400
+ <View className="flex-row items-center gap-2">
401
+ {children}
402
+ {showModeSwitch ? (
403
+ <ButtonGroup size="sm">
404
+ <Button
405
+ variant={mode === 'write' ? 'secondary' : 'ghost'}
406
+ startContent={<PencilIcon size={14} />}
407
+ onPress={() => setMode('write')}
408
+ >
409
+ Write
410
+ </Button>
411
+ <Button
412
+ variant={mode === 'preview' ? 'secondary' : 'ghost'}
413
+ startContent={<EyeIcon size={14} />}
414
+ onPress={() => setMode('preview')}
415
+ >
416
+ Preview
417
+ </Button>
418
+ </ButtonGroup>
419
+ ) : null}
420
+ </View>
421
+ </View>
422
+ );
423
+ }
424
+ );
425
+
426
+ MarkdownEditorToolbar.displayName = 'MarkdownEditor.Toolbar';
427
+
428
+ /* -------------------------------------------------------------------------- *
429
+ * Input
430
+ * -------------------------------------------------------------------------- */
431
+
432
+ export interface MarkdownEditorInputProps
433
+ extends Omit<TextareaProps, 'value' | 'onChangeText' | 'defaultValue'> {
434
+ className?: string;
435
+ }
436
+
437
+ /**
438
+ * The field the markdown is written in — a `Textarea`, tracking its selection.
439
+ *
440
+ * Renders nothing while the preview is up. Unmounted rather than hidden: a
441
+ * field kept alive behind the preview keeps the keyboard up with it.
442
+ */
443
+ const MarkdownEditorInput = forwardRef<TextInput, MarkdownEditorInputProps>(
444
+ ({ className, rows = 8, ...props }, ref) => {
445
+ const { value, mode, pending, setSelection, clearPending, onChangeText, disabled } =
446
+ useMarkdownEditor('MarkdownEditor.Input');
447
+
448
+ const handleSelectionChange = useCallback(
449
+ (event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
450
+ setSelection(event.nativeEvent.selection);
451
+ // The edit's own selection has now landed, so stop forcing it and give
452
+ // the caret back to whoever is holding the phone.
453
+ clearPending();
454
+ },
455
+ [setSelection, clearPending]
456
+ );
457
+
458
+ if (mode !== 'write') return null;
459
+
460
+ return (
461
+ <Textarea
462
+ ref={ref}
463
+ value={value}
464
+ onChangeText={onChangeText}
465
+ selection={pending}
466
+ onSelectionChange={handleSelectionChange}
467
+ disabled={disabled}
468
+ rows={rows}
469
+ autoCapitalize="none"
470
+ autoCorrect={false}
471
+ // A markdown source is code as much as it is prose: an editor that
472
+ // capitalises the word after a fence, or rewrites a dash into an
473
+ // en dash, is an editor that changes what the document renders as.
474
+ className={cn('font-mono', className)}
475
+ {...props}
476
+ />
477
+ );
478
+ }
479
+ );
480
+
481
+ MarkdownEditorInput.displayName = 'MarkdownEditor.Input';
482
+
483
+ /* -------------------------------------------------------------------------- *
484
+ * Preview
485
+ * -------------------------------------------------------------------------- */
486
+
487
+ export interface MarkdownEditorPreviewProps extends Omit<ViewProps, 'children'> {
488
+ className?: string;
489
+ /** What to show when there is nothing written yet. */
490
+ emptyText?: string;
491
+ }
492
+
493
+ /**
494
+ * The draft, rendered.
495
+ *
496
+ * Renders nothing while the field is up, which is what makes the two one pane
497
+ * rather than two.
498
+ */
499
+ const MarkdownEditorPreview = forwardRef<View, MarkdownEditorPreviewProps>(
500
+ ({ className, emptyText = 'Nothing to preview yet.', ...props }, ref) => {
501
+ const { value, mode } = useMarkdownEditor('MarkdownEditor.Preview');
502
+ const slots = markdownEditorVariants();
503
+
504
+ if (mode !== 'preview') return null;
505
+
506
+ return (
507
+ <View ref={ref} className={slots.preview({ className })} {...props}>
508
+ {value.trim() ? (
509
+ <Response>{value}</Response>
510
+ ) : (
511
+ <Text size="sm" muted className={slots.empty()}>
512
+ {emptyText}
513
+ </Text>
514
+ )}
515
+ </View>
516
+ );
517
+ }
518
+ );
519
+
520
+ MarkdownEditorPreview.displayName = 'MarkdownEditor.Preview';
521
+
522
+ export const MarkdownEditor = Object.assign(MarkdownEditorRoot, {
523
+ Toolbar: MarkdownEditorToolbar,
524
+ Input: MarkdownEditorInput,
525
+ Preview: MarkdownEditorPreview,
526
+ });