@colixsystems/widget-sdk 0.275.2 → 0.276.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.
@@ -0,0 +1,70 @@
1
+ // sc-15080 — the page `<MarkdownInput>` runs its editor in on iOS and Android.
2
+ //
3
+ // React Native has no editable rich-text surface, so the native field hosts
4
+ // the SAME editor module the web field mounts, inside a WebView. This entry is
5
+ // bundled into ./markdown-editor-html.js by scripts/build-markdown-editor.mjs;
6
+ // nothing imports it at runtime.
7
+ //
8
+ // Messages are JSON strings. In: init, style, set, command, focus. Out:
9
+ // ready, change, height, focus, selection, state.
10
+
11
+ import { createMarkdownEditor } from "./markdown-editor-core.js";
12
+ import { markdownEditorCss } from "./markdown-editor-css.js";
13
+
14
+ const ROOT = "#root";
15
+
16
+ function post(message) {
17
+ if (window.ReactNativeWebView) window.ReactNativeWebView.postMessage(JSON.stringify(message));
18
+ }
19
+
20
+ let instance = null;
21
+ const style = document.createElement("style");
22
+ document.head.appendChild(style);
23
+
24
+ function paint({ tokens, minHeight }) {
25
+ if (tokens && typeof tokens === "object") style.textContent = markdownEditorCss(tokens, ROOT, { minHeight });
26
+ }
27
+
28
+ function init(message) {
29
+ if (instance) return;
30
+ const { markdown, placeholder, label } = message;
31
+ paint(message);
32
+
33
+ const root = document.querySelector(ROOT);
34
+ instance = createMarkdownEditor({
35
+ element: root,
36
+ markdown,
37
+ placeholder,
38
+ label,
39
+ onChange: (next) => post({ type: "change", markdown: next }),
40
+ onFocusChange: (focused) => post({ type: "focus", focused }),
41
+ onState: (state) => post({ type: "state", inTable: state.inTable === true }),
42
+ });
43
+ instance.editor.on("selectionUpdate", () => post({ type: "selection", text: instance.selectedText() }));
44
+
45
+ let lastHeight = 0;
46
+ new ResizeObserver(() => {
47
+ const height = Math.ceil(root.getBoundingClientRect().height);
48
+ if (height !== lastHeight) post({ type: "height", height: (lastHeight = height) });
49
+ }).observe(root);
50
+ }
51
+
52
+ window.mdEditor = {
53
+ receive(raw) {
54
+ let message;
55
+ try {
56
+ message = JSON.parse(raw);
57
+ } catch {
58
+ return;
59
+ }
60
+ if (!message || typeof message !== "object") return;
61
+ if (message.type === "init") init(message);
62
+ else if (message.type === "style") paint(message);
63
+ else if (!instance) return;
64
+ else if (message.type === "set") instance.setMarkdown(message.markdown);
65
+ else if (message.type === "command") instance.command(message.name, message.args);
66
+ else if (message.type === "focus") instance.focus();
67
+ },
68
+ };
69
+
70
+ post({ type: "ready" });
@@ -1,14 +1,14 @@
1
1
  // sc-6970 — the ONE implementation behind the `<MarkdownInput>` SDK primitive.
2
2
  //
3
- // The authoring half of `<RichText>`: a multi-line field whose toolbar wraps the
4
- // author's SELECTION in markdown markers. Widgets previously had only a bare
3
+ // The authoring half of `<RichText>`. Widgets previously had only a bare
5
4
  // `TextInput`, so a widget asked for a formatting toolbar had to splice literal
6
5
  // `<strong>`/`<br>` tags into the text — markup the author then read as tag soup.
7
6
  //
8
- // sc-14974: the field itself is live-styled, so the author edits the formatted
9
- // text directly. Each host binding supplies its own `LiveField` (a TextInput
10
- // with styled children natively, a contentEditable on web); the rendered
11
- // `<RichText>` preview is opt-in for content the field shows only as markers.
7
+ // sc-15080: the author edits the formatted result directly — headings, lists,
8
+ // links and real tables — while the stored value stays markdown. Each host
9
+ // binding supplies its own `EditorField` around the one shared editor (mounted
10
+ // in the page on web, in a WebView natively), so `showPreview` has nothing
11
+ // left to show and is accepted as a no-op.
12
12
  //
13
13
  // The two platform bindings differ ONLY in where the primitives come from; the
14
14
  // component is defined once here so the hosts cannot drift (CLAUDE.md §3, §8).
@@ -16,7 +16,6 @@
16
16
  import React from "react";
17
17
  import { useHostTheme } from "./hooks.js";
18
18
  import { resolveRichTextTokens } from "./richtext-tokens.js";
19
- import { markdownEditOps } from "./markdown-edit.js";
20
19
  import { makeMarkdownLinkDialog } from "./markdown-link-dialog.js";
21
20
 
22
21
  const TOOLS = Object.freeze([
@@ -28,8 +27,7 @@ const TOOLS = Object.freeze([
28
27
  { key: "bullet", label: "•", accessibilityLabel: "Bulleted list" },
29
28
  { key: "ordered", label: "1.", accessibilityLabel: "Numbered list" },
30
29
  { key: "table", label: "Table", accessibilityLabel: "Insert table" },
31
- // sc-7349: the one tool a selection cannot complete — a link needs a target,
32
- // so this button opens the form instead of editing the string directly.
30
+ // sc-7349: a link needs a target no selection supplies, so this opens a form.
33
31
  {
34
32
  key: "link",
35
33
  label: "Link",
@@ -38,17 +36,26 @@ const TOOLS = Object.freeze([
38
36
  },
39
37
  ]);
40
38
 
41
- export function makeMarkdownInput(rn, RichText, Modal, Button, LiveField) {
39
+ // Shown while the caret is in a table: a phone keyboard has no Tab key to
40
+ // grow one, and a table must be removable without selecting around it.
41
+ const TABLE_TOOLS = Object.freeze([
42
+ { key: "addRow", label: "+ Row", accessibilityLabel: "Add row" },
43
+ { key: "addColumn", label: "+ Column", accessibilityLabel: "Add column" },
44
+ { key: "removeRow", label: "− Row", accessibilityLabel: "Remove row" },
45
+ { key: "removeColumn", label: "− Column", accessibilityLabel: "Remove column" },
46
+ { key: "removeTable", label: "Delete table", accessibilityLabel: "Delete table" },
47
+ ]);
48
+
49
+ export function makeMarkdownInput(rn, Modal, Button, EditorField) {
42
50
  const { Text, View, Pressable } = rn;
43
51
  const MarkdownLinkDialog = makeMarkdownLinkDialog(rn, Modal, Button);
44
52
 
53
+ // `previewLabel`, `showPreview` and `renderImage` are accepted for widgets
54
+ // written before the field showed the formatted result itself.
45
55
  function MarkdownInput({
46
56
  value,
47
57
  onChange,
48
58
  placeholder,
49
- previewLabel = "Preview",
50
- showPreview = false,
51
- renderImage,
52
59
  pages,
53
60
  minHeight = 150,
54
61
  maxHeight = 280,
@@ -58,72 +65,34 @@ export function makeMarkdownInput(rn, RichText, Modal, Button, LiveField) {
58
65
  }) {
59
66
  const theme = useHostTheme();
60
67
  const tokens = React.useMemo(() => resolveRichTextTokens(theme), [theme]);
61
- const [selection, setSelection] = React.useState({ start: 0, end: 0 });
62
68
  const fieldRef = React.useRef(null);
63
- const [linkOpen, setLinkOpen] = React.useState(false);
64
-
65
- const text = typeof value === "string" ? value : "";
69
+ const [linkLabel, setLinkLabel] = React.useState(null);
70
+ const [inTable, setInTable] = React.useState(false);
71
+ const onStateChange = React.useCallback((state) => setInTable(state.inTable), []);
66
72
 
67
- const onSelectionChange = React.useCallback((event) => {
68
- const next = event && event.nativeEvent && event.nativeEvent.selection;
69
- if (next) setSelection({ start: next.start, end: next.end });
73
+ const run = React.useCallback((name, args) => {
74
+ const field = fieldRef.current;
75
+ if (field) field.command(name, args);
70
76
  }, []);
71
77
 
72
- const applyTool = React.useCallback(
73
- (toolKey, options) => {
74
- if (typeof onChange !== "function") return;
75
- const edit = markdownEditOps[toolKey];
76
- if (!edit) return;
77
- const next = edit(text, selection, options);
78
- const field = fieldRef.current;
79
- if (field && typeof field.recordEdit === "function") field.recordEdit();
80
- onChange(next.value);
81
- setSelection(next.selection);
82
- },
83
- [onChange, text, selection],
84
- );
85
-
86
78
  const pressTool = React.useCallback(
87
79
  (tool) => {
88
- if (tool.opensDialog) {
89
- setLinkOpen(true);
90
- return;
91
- }
92
- applyTool(tool.key);
80
+ // The selection seeds the form's Link text, so a highlighted phrase
81
+ // stays the label.
82
+ if (tool.opensDialog) setLinkLabel(fieldRef.current ? fieldRef.current.selectedText() : "");
83
+ else run(tool.key);
93
84
  },
94
- [applyTool],
85
+ [run],
95
86
  );
96
87
 
97
88
  const insertLink = React.useCallback(
98
89
  (result) => {
99
- setLinkOpen(false);
100
- applyTool("link", result);
90
+ setLinkLabel(null);
91
+ run("link", result);
101
92
  },
102
- [applyTool],
103
- );
104
-
105
- // The selection seeds the dialog's Link text, so highlighting a phrase and
106
- // pressing Link keeps that phrase as the label.
107
- const selected = text.slice(
108
- Math.min(selection.start, selection.end),
109
- Math.max(selection.start, selection.end),
93
+ [run],
110
94
  );
111
95
 
112
- const fieldStyle = {
113
- fontFamily: tokens.bodyFont,
114
- fontSize: tokens.bodySize,
115
- lineHeight: tokens.lineHeight,
116
- color: tokens.color,
117
- backgroundColor: tokens.surface,
118
- borderWidth: 1,
119
- borderColor: tokens.border,
120
- borderRadius: tokens.radius,
121
- padding: tokens.padding,
122
- minHeight,
123
- maxHeight,
124
- textAlignVertical: "top",
125
- };
126
-
127
96
  return React.createElement(
128
97
  View,
129
98
  { style, testID },
@@ -137,7 +106,7 @@ export function makeMarkdownInput(rn, RichText, Modal, Button, LiveField) {
137
106
  },
138
107
  testID: testID ? `${testID}-toolbar` : undefined,
139
108
  },
140
- TOOLS.map((tool) =>
109
+ (inTable ? [...TOOLS, ...TABLE_TOOLS] : TOOLS).map((tool) =>
141
110
  React.createElement(
142
111
  Pressable,
143
112
  {
@@ -174,51 +143,24 @@ export function makeMarkdownInput(rn, RichText, Modal, Button, LiveField) {
174
143
  ),
175
144
  ),
176
145
  ),
177
- React.createElement(LiveField, {
146
+ React.createElement(EditorField, {
178
147
  ref: fieldRef,
179
- value: text,
148
+ value: typeof value === "string" ? value : "",
180
149
  onChangeText: onChange,
181
- onSelectionChange,
182
- selection,
150
+ onStateChange,
183
151
  placeholder,
184
152
  accessibilityLabel,
185
153
  tokens,
186
- style: fieldStyle,
154
+ minHeight,
155
+ maxHeight,
187
156
  testID: testID ? `${testID}-field` : undefined,
188
157
  }),
189
- showPreview && text.trim()
190
- ? React.createElement(
191
- View,
192
- { style: { marginTop: tokens.padding } },
193
- React.createElement(
194
- Text,
195
- {
196
- style: {
197
- fontFamily: tokens.bodyFont,
198
- fontSize: tokens.codeSize,
199
- color: tokens.mutedColor,
200
- marginBottom: tokens.blockGap,
201
- },
202
- },
203
- previewLabel,
204
- ),
205
- React.createElement(RichText, {
206
- value: text,
207
- renderImage,
208
- // sc-7349: the preview shows the reader's document, but a tap
209
- // must NOT leave the page — that would throw away the draft the
210
- // author is still writing.
211
- followLinks: false,
212
- testID: testID ? `${testID}-preview` : undefined,
213
- }),
214
- )
215
- : null,
216
158
  React.createElement(MarkdownLinkDialog, {
217
- visible: linkOpen,
218
- onClose: () => setLinkOpen(false),
159
+ visible: linkLabel !== null,
160
+ onClose: () => setLinkLabel(null),
219
161
  onSubmit: insertLink,
220
162
  pages,
221
- initialLabel: selected,
163
+ initialLabel: linkLabel || "",
222
164
  testID,
223
165
  }),
224
166
  );
@@ -1,11 +1,10 @@
1
1
  // sc-6970 — `<MarkdownInput>` (web). Binds the shared implementation in
2
- // ./markdown-input-view.js to react-native-web's primitives, the web
3
- // `<RichText>` it renders its preview with, and the web live field.
2
+ // ./markdown-input-view.js to react-native-web's primitives and the web
3
+ // editor field.
4
4
 
5
5
  import { ReactNative } from "./text-scale.js";
6
6
  import { makeMarkdownInput } from "./markdown-input-view.js";
7
- import { MarkdownLiveField } from "./markdown-live-field.js";
8
- import { RichText } from "./richtext.js";
7
+ import { MarkdownEditorField } from "./markdown-editor-field.js";
9
8
  import { Modal } from "./modal.js";
10
9
  import { Button } from "./button.js";
11
10
  import { TextInput } from "./primitives.js";
@@ -13,8 +12,7 @@ import { TextInput } from "./primitives.js";
13
12
  // TextInput stays for the link form, which is built from the same `rn`.
14
13
  export const MarkdownInput = makeMarkdownInput(
15
14
  { ...ReactNative, TextInput },
16
- RichText,
17
15
  Modal,
18
16
  Button,
19
- MarkdownLiveField,
17
+ MarkdownEditorField,
20
18
  );
@@ -1,12 +1,11 @@
1
1
  // sc-6970 — `<MarkdownInput>` (native). Binds the shared implementation in
2
2
  // ./markdown-input-view.js to React Native's own primitives and the native
3
- // `<RichText>`. Only the imports differ from the web mirror.
3
+ // editor field. Only the imports differ from the web mirror.
4
4
 
5
5
  import { View, Pressable, ScrollView } from "react-native";
6
6
  import { Text, TextInput } from "./bounded-text.native.js";
7
7
  import { makeMarkdownInput } from "./markdown-input-view.js";
8
- import { MarkdownLiveField } from "./markdown-live-field.native.js";
9
- import { RichText } from "./richtext.native.js";
8
+ import { MarkdownEditorField } from "./markdown-editor-field.native.js";
10
9
  import { Modal } from "./modal.native.js";
11
10
  import { Button } from "./button.native.js";
12
11
 
@@ -14,8 +13,7 @@ import { Button } from "./button.native.js";
14
13
  // scroll rather than grow an app's every page past the panel.
15
14
  export const MarkdownInput = makeMarkdownInput(
16
15
  { Text, View, Pressable, TextInput, ScrollView },
17
- RichText,
18
16
  Modal,
19
17
  Button,
20
- MarkdownLiveField,
18
+ MarkdownEditorField,
21
19
  );
package/dist/markdown.js CHANGED
@@ -196,8 +196,7 @@ const PLAIN_PATTERN =
196
196
  * Splits one line into positioned tokens: `{ type: "text", start, end }`, or
197
197
  * `{ type: "bold"|"italic"|"code", start, end, contentStart, contentEnd }`, or
198
198
  * `{ type: "link", start, end, contentStart, contentEnd, href }` whose content
199
- * is the RAW label. `parseInline` and the live editor both read these, so the
200
- * editor cannot style a construct the renderer would not draw.
199
+ * is the RAW label.
201
200
  */
202
201
  function scanInline(source) {
203
202
  const tokens = [];
@@ -532,116 +531,6 @@ export function parseMarkdown(text, options) {
532
531
  return blocks;
533
532
  }
534
533
 
535
- const LIVE_STYLE_KEYS = ["marker", "bold", "italic", "code", "href", "heading"];
536
-
537
- function sameLiveStyle(a, b) {
538
- return LIVE_STYLE_KEYS.every((key) => (a[key] || false) === (b[key] || false));
539
- }
540
-
541
- /**
542
- * sc-14974 — `text` cut into styled runs for the live editor. Joining every
543
- * run's `text` gives back `text` byte for byte: nothing is trimmed or joined,
544
- * and markdown markers stay in place flagged `marker` so the editor can dim
545
- * them. Each run is `{ text, marker?, bold?, italic?, code?, href?, heading? }`
546
- * with `heading` the level. Lines are classified in `parseMarkdown`'s order.
547
- */
548
- export function markdownLiveSegments(text) {
549
- const source = typeof text === "string" ? text : "";
550
- const runs = [];
551
- const push = (value, style) => {
552
- if (!value) return;
553
- const last = runs[runs.length - 1];
554
- if (last && sameLiveStyle(last, style)) last.text += value;
555
- else runs.push({ text: value, ...style });
556
- };
557
- const inline = (value, base) => {
558
- for (const token of scanInline(value)) {
559
- if (token.type === "text") {
560
- push(value.slice(token.start, token.end), base);
561
- continue;
562
- }
563
- const content = value.slice(token.contentStart, token.contentEnd);
564
- push(value.slice(token.start, token.contentStart), { ...base, marker: true });
565
- if (token.type === "link") inline(content, { ...base, href: token.href });
566
- else push(content, { ...base, [token.type]: true });
567
- push(value.slice(token.contentEnd, token.end), { ...base, marker: true });
568
- }
569
- };
570
- const tableRow = (line, base) => {
571
- let cellStart = 0;
572
- for (let k = 0; k < line.length; k += 1) {
573
- if (line.charAt(k) === "\\" && line.charAt(k + 1) === "|") k += 1;
574
- else if (line.charAt(k) === "|") {
575
- inline(line.slice(cellStart, k), base);
576
- push("|", { marker: true });
577
- cellStart = k + 1;
578
- }
579
- }
580
- inline(line.slice(cellStart), base);
581
- };
582
-
583
- const lines = source.split("\n");
584
- let inFence = false;
585
- let tableStart = -1;
586
- let tableEnd = -1;
587
- for (let i = 0; i < lines.length; i += 1) {
588
- if (i > 0) push("\n", {});
589
- const line = lines[i];
590
- const trimmed = line.trimEnd();
591
- const tail = line.slice(trimmed.length);
592
-
593
- if (inFence) {
594
- if (FENCE_CLOSE_RE.test(line)) inFence = false;
595
- push(line, inFence ? { code: true } : { marker: true });
596
- continue;
597
- }
598
- if (FENCE_OPEN_RE.test(trimmed)) {
599
- inFence = true;
600
- push(line, { marker: true });
601
- continue;
602
- }
603
-
604
- if (i > tableEnd && TABLE_ROW_RE.test(line) && TABLE_DIVIDER_RE.test(lines[i + 1] || "")) {
605
- tableStart = i;
606
- tableEnd = i + 1;
607
- while (tableEnd + 1 < lines.length && TABLE_ROW_RE.test(lines[tableEnd + 1])) tableEnd += 1;
608
- }
609
- if (i <= tableEnd) {
610
- if (i === tableStart + 1) push(line, { marker: true });
611
- else tableRow(line, i === tableStart ? { bold: true } : {});
612
- continue;
613
- }
614
-
615
- const lead = trimmed.length - trimmed.trimStart().length;
616
- const image = IMAGE_RE.exec(trimmed.trimStart());
617
- if (image) {
618
- const altStart = lead + 2;
619
- const altEnd = altStart + image[1].length;
620
- push(line.slice(0, altStart), { marker: true });
621
- push(line.slice(altStart, altEnd), {});
622
- push(line.slice(altEnd, trimmed.length), { marker: true });
623
- push(tail, {});
624
- continue;
625
- }
626
-
627
- const heading = HEADING_RE.exec(trimmed);
628
- const listItem = heading ? null : BULLET_RE.exec(trimmed) || ORDERED_RE.exec(trimmed);
629
- if (heading || listItem) {
630
- const content = heading ? heading[2] : listItem[listItem.length - 1];
631
- const base = heading ? { heading: heading[1].length } : {};
632
- const contentStart = trimmed.length - content.length;
633
- push(trimmed.slice(0, contentStart), { ...base, marker: true });
634
- inline(content, base);
635
- push(tail, base);
636
- continue;
637
- }
638
-
639
- inline(trimmed, {});
640
- push(tail, {});
641
- }
642
- return runs;
643
- }
644
-
645
534
  function spansToText(spans) {
646
535
  return spans.map((span) => span.text).join("");
647
536
  }
@@ -58,7 +58,7 @@ export function resolveRichTextTokens(theme) {
58
58
 
59
59
  export { IMAGE_WIDTH as RICH_TEXT_IMAGE_WIDTH };
60
60
 
61
- /** The style one inline span is drawn with, by `<RichText>` and the editor. */
61
+ /** The style one inline span is drawn with by `<RichText>`. */
62
62
  export function richTextSpanStyle(span, tokens) {
63
63
  return {
64
64
  ...(span.bold ? { fontWeight: "700" } : null),
@@ -71,27 +71,3 @@ export function richTextSpanStyle(span, tokens) {
71
71
  ...(span.href ? { color: tokens.accent, textDecorationLine: "underline" } : null),
72
72
  };
73
73
  }
74
-
75
- /**
76
- * sc-14974 — the style one `markdownLiveSegments` run is drawn with in
77
- * `<MarkdownInput>`: the span's `<RichText>` look, sized as its heading, with
78
- * markers dimmed so the text reads as the result while staying editable.
79
- */
80
- export function markdownLiveRunStyle(run, tokens) {
81
- const headingSize = run.heading
82
- ? tokens.headingSizes[run.heading] || tokens.headingSizes[3]
83
- : null;
84
- return {
85
- ...(headingSize
86
- ? {
87
- fontFamily: tokens.headingFont,
88
- fontWeight: "700",
89
- fontSize: headingSize,
90
- // The field's body leading would overlap a larger heading line.
91
- lineHeight: Math.round(headingSize * 1.3),
92
- }
93
- : null),
94
- ...richTextSpanStyle(run, tokens),
95
- ...(run.marker ? { color: tokens.mutedColor, textDecorationLine: "none" } : null),
96
- };
97
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.275.2",
3
+ "version": "0.276.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",
@@ -75,7 +75,22 @@
75
75
  "react": ">=18.0.0",
76
76
  "react-native": "*",
77
77
  "react-dom": ">=18.0.0",
78
- "react-native-web": ">=0.19.0"
78
+ "react-native-web": ">=0.19.0",
79
+ "@tiptap/core": "^3.32.2",
80
+ "@tiptap/extension-bold": "^3.32.2",
81
+ "@tiptap/extension-code": "^3.32.2",
82
+ "@tiptap/extension-code-block": "^3.32.2",
83
+ "@tiptap/extension-document": "^3.32.2",
84
+ "@tiptap/extension-heading": "^3.32.2",
85
+ "@tiptap/extension-image": "^3.32.2",
86
+ "@tiptap/extension-italic": "^3.32.2",
87
+ "@tiptap/extension-link": "^3.32.2",
88
+ "@tiptap/extension-list": "^3.32.2",
89
+ "@tiptap/extension-paragraph": "^3.32.2",
90
+ "@tiptap/extension-table": "^3.32.2",
91
+ "@tiptap/extension-text": "^3.32.2",
92
+ "@tiptap/extensions": "^3.32.2",
93
+ "react-native-webview": ">=13.7.0"
79
94
  },
80
95
  "peerDependenciesMeta": {
81
96
  "react": {
@@ -89,6 +104,51 @@
89
104
  },
90
105
  "react-dom": {
91
106
  "optional": true
107
+ },
108
+ "@tiptap/core": {
109
+ "optional": true
110
+ },
111
+ "@tiptap/extension-bold": {
112
+ "optional": true
113
+ },
114
+ "@tiptap/extension-code": {
115
+ "optional": true
116
+ },
117
+ "@tiptap/extension-code-block": {
118
+ "optional": true
119
+ },
120
+ "@tiptap/extension-document": {
121
+ "optional": true
122
+ },
123
+ "@tiptap/extension-heading": {
124
+ "optional": true
125
+ },
126
+ "@tiptap/extension-image": {
127
+ "optional": true
128
+ },
129
+ "@tiptap/extension-italic": {
130
+ "optional": true
131
+ },
132
+ "@tiptap/extension-link": {
133
+ "optional": true
134
+ },
135
+ "@tiptap/extension-list": {
136
+ "optional": true
137
+ },
138
+ "@tiptap/extension-paragraph": {
139
+ "optional": true
140
+ },
141
+ "@tiptap/extension-table": {
142
+ "optional": true
143
+ },
144
+ "@tiptap/extension-text": {
145
+ "optional": true
146
+ },
147
+ "@tiptap/extensions": {
148
+ "optional": true
149
+ },
150
+ "react-native-webview": {
151
+ "optional": true
92
152
  }
93
153
  },
94
154
  "optionalDependencies": {
package/src/contract.js CHANGED
@@ -3464,13 +3464,13 @@ const PRIMITIVES = [
3464
3464
  rnComponent: null,
3465
3465
  docsUrl: null,
3466
3466
  },
3467
- // sc-6970 — formatted content, authored. The authoring half of `<RichText>`:
3468
- // a real selection-aware toolbar plus a live preview, so a generated widget
3469
- // never has to splice HTML tags into a plain TextInput again.
3467
+ // sc-6970 — formatted content, authored. The authoring half of `<RichText>`,
3468
+ // so a generated widget never has to splice HTML tags into a plain TextInput
3469
+ // again. sc-15080: the field edits the formatted result directly.
3470
3470
  {
3471
3471
  name: "MarkdownInput",
3472
3472
  description:
3473
- 'Formatted-content editor. `<MarkdownInput value={draft} onChange={setDraft} />`. The default way to let an app USER write formatted text: a multi-line field that styles the markdown live as the user types (headings sized, bold, italic, code and links drawn in place, the markers kept in the text but dimmed), so the user reads and edits the result in one place; a toolbar whose buttons wrap the current SELECTION in markdown markers (bold, italic, code, H2, H3, bullets, numbered list); a Table button that inserts a pipe-table skeleton with the first header cell selected; and a Link button that opens a form for the link text plus its target. Use it rather than a bare `TextInput` plus formatting buttons of your own; an editor you do build (one that only bolds, a checklist editor) still stores markdown markers and previews with `<RichText>` — buttons that splice `<strong>`/`<br>`/`<table>` tags into the string leave the author reading tag soup with no preview of the real output. The stored value is markdown text, exactly as typed. Props: `value`, `onChange(next)`, `placeholder`, `showPreview` (default false; true also draws a rendered `<RichText>` copy underneath, for content the field can only show as markers: images and tables), `previewLabel` (default "Preview", the heading above that copy), `renderImage` (passed through to the preview), `pages` (optional `[{ id, name }]` — supply it and the Link form offers those pages to link to by name, the way `renderImage` is supplied for filestore images; without it the form offers only an external address, because a widget cannot know the app\'s pages on its own), `minHeight` (default 150) and `maxHeight` (default 280) so the field scrolls internally instead of growing without bound, `accessibilityLabel`, `style`, `testID`. Links in the preview are deliberately inert, so a tap while editing cannot navigate away from the draft.',
3473
+ "Formatted-content editor. `<MarkdownInput value={draft} onChange={setDraft} />`. The default way to let an app USER write formatted text: a rich-text field where the user edits the FORMATTED RESULT directly — headings, bold, italic, code, links, bulleted and numbered lists and real tables are shown as they will read, with no markdown markers in view — plus a toolbar (bold, italic, code, H2, H3, bullets, numbered list), a Table button that inserts a real table, with buttons to add or remove rows and columns or delete the table while the caret is in one, and a Link button that opens a form for the link text plus its target. Typing `## `, `- `, `1. `, `**bold**` or `*italic*` formats as you type. Use it rather than a bare `TextInput` plus formatting buttons of your own, and do NOT add a `<RichText>` preview under it: the field already shows the result; an editor you do build (one that only bolds, a checklist editor) still stores markdown markers and previews with `<RichText>` — buttons that splice `<strong>`/`<br>`/`<table>` tags into the string leave the author reading tag soup. The stored value is markdown text in the `<RichText>` grammar; loading a value never rewrites it, and an edit writes each construct in its canonical spelling. On iOS and Android the field runs in a WebView bundled into the SDK (no network). Props: `value`, `onChange(next)`, `placeholder`, `pages` (optional `[{ id, name }]` — supply it and the Link form offers those pages to link to by name; without it the form offers only an external address, because a widget cannot know the app's pages on its own), `minHeight` (default 150) and `maxHeight` (default 280) — the field grows with its content between them, then scrolls — `accessibilityLabel`, `style`, `testID`. `showPreview`, `previewLabel` and `renderImage` are still accepted and ignored. Images show inside the field when their source is an http(s) URL and as a labelled placeholder when it is a filestore id. Links in the field are inert, so a tap while editing cannot navigate away from the draft.",
3474
3474
  rnComponent: null,
3475
3475
  docsUrl: null,
3476
3476
  },
@@ -6570,7 +6570,10 @@ const CONTRACT = deepFreeze({
6570
6570
  // ships a hand-written `sv` value beside `en`.
6571
6571
  // 1.225.0: behaviour (sc-14974) — `<MarkdownInput>` styles its own field
6572
6572
  // live; `showPreview` now defaults to false.
6573
- version: "1.225.0",
6573
+ // 1.226.0: behaviour (sc-15080) — `<MarkdownInput>` is a rich-text editor
6574
+ // showing the formatted result; `showPreview`, `previewLabel` and
6575
+ // `renderImage` are accepted and ignored.
6576
+ version: "1.226.0",
6574
6577
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6575
6578
  hooks: HOOKS,
6576
6579
  primitives: PRIMITIVES,