@colixsystems/widget-sdk 0.133.0 → 0.134.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.
- package/README.md +36 -14
- package/dist/contract.cjs +68 -23
- package/dist/contract.js +68 -23
- package/dist/hooks.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/markdown-edit.js +67 -0
- package/dist/markdown-input-view.js +56 -4
- package/dist/markdown-input.js +2 -1
- package/dist/markdown-input.native.js +6 -2
- package/dist/markdown-link-dialog.js +284 -0
- package/dist/markdown.js +153 -19
- package/dist/richtext-view.js +118 -6
- package/package.json +1 -1
package/dist/hooks.js
CHANGED
|
@@ -188,6 +188,17 @@ export function useHostTheme() {
|
|
|
188
188
|
return ctx && ctx.workspace ? ctx.workspace.theme : undefined;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
// @internal — the navigation counterpart of useHostTheme: reads the host's
|
|
192
|
+
// navigation slice WITHOUT throwing when no provider is mounted, unlike
|
|
193
|
+
// useNavigation() which asserts a widget context. `<RichText>` reads it so a
|
|
194
|
+
// link in formatted content is followable inside a widget yet the component
|
|
195
|
+
// stays renderable anywhere (a Studio preview, a test harness). Not part of the
|
|
196
|
+
// public widget hook surface — not re-exported from index.js.
|
|
197
|
+
export function useHostNavigation() {
|
|
198
|
+
const ctx = useContext(HostWidgetContext);
|
|
199
|
+
return ctx ? ctx.navigation : undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
191
202
|
/**
|
|
192
203
|
* REQ-THEME-13 — returns the author-set per-widget style values: the object the
|
|
193
204
|
* host delivers under `props.style`, keyed by the style-field names the widget
|
package/dist/index.d.ts
CHANGED
|
@@ -2600,6 +2600,8 @@ export function lintSource(
|
|
|
2600
2600
|
export interface ContractHookEntry {
|
|
2601
2601
|
name: string;
|
|
2602
2602
|
signature: string;
|
|
2603
|
+
/** Arguments and option keys in `signature` that may be omitted (sc-6946). */
|
|
2604
|
+
optionalArgs?: string[];
|
|
2603
2605
|
returnShape: Record<string, string>;
|
|
2604
2606
|
requiredContextSlice: string[];
|
|
2605
2607
|
scopes: string[] | null;
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/dist/markdown-edit.js
CHANGED
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
// author's SELECTION rather than appending at the end, and that is a thing
|
|
6
6
|
// tests can pin without a renderer.
|
|
7
7
|
|
|
8
|
+
import { escapeMarkdownLabel, formatMarkdownTable } from "./markdown.js";
|
|
9
|
+
|
|
8
10
|
const LINE_RE = /^(\s*)((?:#{1,3}\s)|(?:[-*]\s)|(?:\d+[.)]\s))?([\s\S]*)$/;
|
|
9
11
|
|
|
12
|
+
// The literal the inserted table skeleton opens with; the caret lands on it.
|
|
13
|
+
const FIRST_HEADING = "Column 1";
|
|
14
|
+
|
|
10
15
|
const LINE_KINDS = Object.freeze({
|
|
11
16
|
heading2: { marker: "## ", test: /^##\s/ },
|
|
12
17
|
heading3: { marker: "### ", test: /^###\s/ },
|
|
@@ -86,6 +91,66 @@ function linePrefix(kind) {
|
|
|
86
91
|
};
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
// sc-7349 — a link needs a TARGET, which no selection can supply, so this is
|
|
95
|
+
// the one op that takes a third argument. The target is stored verbatim: the
|
|
96
|
+
// host's `navigation.openLink` is what decides page / external / refuse, so
|
|
97
|
+
// this must not pre-judge it (see markdown.js).
|
|
98
|
+
function linkOp() {
|
|
99
|
+
return (text, selection, options) => {
|
|
100
|
+
const { src, start, end } = readRange(text, selection);
|
|
101
|
+
// A bare string is accepted as the target, so the common case stays terse.
|
|
102
|
+
const opts =
|
|
103
|
+
options && typeof options === "object" ? options : { target: options };
|
|
104
|
+
const href = typeof opts.target === "string" ? opts.target.trim() : "";
|
|
105
|
+
// Nothing to point at — leave the text exactly as the author left it.
|
|
106
|
+
if (!href) return { value: src, selection: { start, end } };
|
|
107
|
+
|
|
108
|
+
// An explicit label wins, so a dialog can name the link when the author
|
|
109
|
+
// had nothing selected; otherwise the selection IS the label.
|
|
110
|
+
const chosen =
|
|
111
|
+
typeof opts.label === "string" && opts.label.trim()
|
|
112
|
+
? opts.label
|
|
113
|
+
: src.slice(start, end);
|
|
114
|
+
const label = escapeMarkdownLabel(chosen) || href;
|
|
115
|
+
const markup = `[${label}](${href})`;
|
|
116
|
+
return {
|
|
117
|
+
value: src.slice(0, start) + markup + src.slice(end),
|
|
118
|
+
// Leaves the LABEL selected, so typing replaces the link text rather
|
|
119
|
+
// than the markup around it.
|
|
120
|
+
selection: { start: start + 1, end: start + 1 + label.length },
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// A table is a block, so it needs its own lines: a blank line before keeps it
|
|
126
|
+
// from fusing with the paragraph the caret was in.
|
|
127
|
+
function tableOp() {
|
|
128
|
+
return (text, selection) => {
|
|
129
|
+
const { src, start, end } = readRange(text, selection);
|
|
130
|
+
const before = src.slice(0, start);
|
|
131
|
+
const after = src.slice(end);
|
|
132
|
+
const lead =
|
|
133
|
+
before.length === 0 || before.endsWith("\n\n")
|
|
134
|
+
? ""
|
|
135
|
+
: before.endsWith("\n")
|
|
136
|
+
? "\n"
|
|
137
|
+
: "\n\n";
|
|
138
|
+
const trail = after.startsWith("\n") ? "" : "\n";
|
|
139
|
+
|
|
140
|
+
const skeleton = formatMarkdownTable();
|
|
141
|
+
const offset = before.length + lead.length;
|
|
142
|
+
return {
|
|
143
|
+
value: before + lead + skeleton + trail + after,
|
|
144
|
+
// Selects the first header cell, so the author types over "Column 1"
|
|
145
|
+
// instead of hunting for the caret inside the pipes.
|
|
146
|
+
selection: {
|
|
147
|
+
start: offset + 2,
|
|
148
|
+
end: offset + 2 + FIRST_HEADING.length,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
89
154
|
export const markdownEditOps = Object.freeze({
|
|
90
155
|
bold: wrap("**"),
|
|
91
156
|
italic: wrap("*"),
|
|
@@ -94,4 +159,6 @@ export const markdownEditOps = Object.freeze({
|
|
|
94
159
|
heading3: linePrefix("heading3"),
|
|
95
160
|
bullet: linePrefix("bullet"),
|
|
96
161
|
ordered: linePrefix("ordered"),
|
|
162
|
+
link: linkOp(),
|
|
163
|
+
table: tableOp(),
|
|
97
164
|
});
|
|
@@ -13,6 +13,7 @@ import React from "react";
|
|
|
13
13
|
import { useHostTheme } from "./hooks.js";
|
|
14
14
|
import { resolveRichTextTokens } from "./richtext-tokens.js";
|
|
15
15
|
import { markdownEditOps } from "./markdown-edit.js";
|
|
16
|
+
import { makeMarkdownLinkDialog } from "./markdown-link-dialog.js";
|
|
16
17
|
|
|
17
18
|
const TOOLS = Object.freeze([
|
|
18
19
|
{ key: "bold", label: "B", accessibilityLabel: "Bold", weight: "700" },
|
|
@@ -22,10 +23,20 @@ const TOOLS = Object.freeze([
|
|
|
22
23
|
{ key: "heading3", label: "H3", accessibilityLabel: "Subheading" },
|
|
23
24
|
{ key: "bullet", label: "•", accessibilityLabel: "Bulleted list" },
|
|
24
25
|
{ key: "ordered", label: "1.", accessibilityLabel: "Numbered list" },
|
|
26
|
+
{ key: "table", label: "Table", accessibilityLabel: "Insert table" },
|
|
27
|
+
// sc-7349: the one tool a selection cannot complete — a link needs a target,
|
|
28
|
+
// so this button opens the form instead of editing the string directly.
|
|
29
|
+
{
|
|
30
|
+
key: "link",
|
|
31
|
+
label: "Link",
|
|
32
|
+
accessibilityLabel: "Insert link",
|
|
33
|
+
opensDialog: true,
|
|
34
|
+
},
|
|
25
35
|
]);
|
|
26
36
|
|
|
27
|
-
export function makeMarkdownInput(rn, RichText) {
|
|
37
|
+
export function makeMarkdownInput(rn, RichText, Overlay) {
|
|
28
38
|
const { Text, View, Pressable, TextInput } = rn;
|
|
39
|
+
const MarkdownLinkDialog = makeMarkdownLinkDialog(rn, Overlay);
|
|
29
40
|
|
|
30
41
|
function MarkdownInput({
|
|
31
42
|
value,
|
|
@@ -34,6 +45,7 @@ export function makeMarkdownInput(rn, RichText) {
|
|
|
34
45
|
previewLabel = "Preview",
|
|
35
46
|
showPreview = true,
|
|
36
47
|
renderImage,
|
|
48
|
+
pages,
|
|
37
49
|
minHeight = 150,
|
|
38
50
|
maxHeight = 280,
|
|
39
51
|
accessibilityLabel,
|
|
@@ -43,6 +55,7 @@ export function makeMarkdownInput(rn, RichText) {
|
|
|
43
55
|
const theme = useHostTheme();
|
|
44
56
|
const tokens = React.useMemo(() => resolveRichTextTokens(theme), [theme]);
|
|
45
57
|
const [selection, setSelection] = React.useState({ start: 0, end: 0 });
|
|
58
|
+
const [linkOpen, setLinkOpen] = React.useState(false);
|
|
46
59
|
|
|
47
60
|
const text = typeof value === "string" ? value : "";
|
|
48
61
|
|
|
@@ -52,17 +65,43 @@ export function makeMarkdownInput(rn, RichText) {
|
|
|
52
65
|
}, []);
|
|
53
66
|
|
|
54
67
|
const applyTool = React.useCallback(
|
|
55
|
-
(toolKey) => {
|
|
68
|
+
(toolKey, options) => {
|
|
56
69
|
if (typeof onChange !== "function") return;
|
|
57
70
|
const edit = markdownEditOps[toolKey];
|
|
58
71
|
if (!edit) return;
|
|
59
|
-
const next = edit(text, selection);
|
|
72
|
+
const next = edit(text, selection, options);
|
|
60
73
|
onChange(next.value);
|
|
61
74
|
setSelection(next.selection);
|
|
62
75
|
},
|
|
63
76
|
[onChange, text, selection],
|
|
64
77
|
);
|
|
65
78
|
|
|
79
|
+
const pressTool = React.useCallback(
|
|
80
|
+
(tool) => {
|
|
81
|
+
if (tool.opensDialog) {
|
|
82
|
+
setLinkOpen(true);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
applyTool(tool.key);
|
|
86
|
+
},
|
|
87
|
+
[applyTool],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const insertLink = React.useCallback(
|
|
91
|
+
(result) => {
|
|
92
|
+
setLinkOpen(false);
|
|
93
|
+
applyTool("link", result);
|
|
94
|
+
},
|
|
95
|
+
[applyTool],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// The selection seeds the dialog's Link text, so highlighting a phrase and
|
|
99
|
+
// pressing Link keeps that phrase as the label.
|
|
100
|
+
const selected = text.slice(
|
|
101
|
+
Math.min(selection.start, selection.end),
|
|
102
|
+
Math.max(selection.start, selection.end),
|
|
103
|
+
);
|
|
104
|
+
|
|
66
105
|
const fieldStyle = {
|
|
67
106
|
fontFamily: tokens.bodyFont,
|
|
68
107
|
fontSize: tokens.bodySize,
|
|
@@ -96,7 +135,7 @@ export function makeMarkdownInput(rn, RichText) {
|
|
|
96
135
|
Pressable,
|
|
97
136
|
{
|
|
98
137
|
key: tool.key,
|
|
99
|
-
onPress: () =>
|
|
138
|
+
onPress: () => pressTool(tool),
|
|
100
139
|
accessibilityRole: "button",
|
|
101
140
|
accessibilityLabel: tool.accessibilityLabel,
|
|
102
141
|
testID: testID ? `${testID}-${tool.key}` : undefined,
|
|
@@ -159,10 +198,23 @@ export function makeMarkdownInput(rn, RichText) {
|
|
|
159
198
|
React.createElement(RichText, {
|
|
160
199
|
value: text,
|
|
161
200
|
renderImage,
|
|
201
|
+
// sc-7349: the preview shows the reader's document, but a tap
|
|
202
|
+
// must NOT leave the page — that would throw away the draft the
|
|
203
|
+
// author is still writing.
|
|
204
|
+
followLinks: false,
|
|
162
205
|
testID: testID ? `${testID}-preview` : undefined,
|
|
163
206
|
}),
|
|
164
207
|
)
|
|
165
208
|
: null,
|
|
209
|
+
React.createElement(MarkdownLinkDialog, {
|
|
210
|
+
visible: linkOpen,
|
|
211
|
+
onClose: () => setLinkOpen(false),
|
|
212
|
+
onSubmit: insertLink,
|
|
213
|
+
tokens,
|
|
214
|
+
pages,
|
|
215
|
+
initialLabel: selected,
|
|
216
|
+
testID,
|
|
217
|
+
}),
|
|
166
218
|
);
|
|
167
219
|
}
|
|
168
220
|
|
package/dist/markdown-input.js
CHANGED
|
@@ -5,5 +5,6 @@
|
|
|
5
5
|
import * as ReactNative from "react-native-web";
|
|
6
6
|
import { makeMarkdownInput } from "./markdown-input-view.js";
|
|
7
7
|
import { RichText } from "./richtext.js";
|
|
8
|
+
import { Overlay } from "./overlay.js";
|
|
8
9
|
|
|
9
|
-
export const MarkdownInput = makeMarkdownInput(ReactNative, RichText);
|
|
10
|
+
export const MarkdownInput = makeMarkdownInput(ReactNative, RichText, Overlay);
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
// ./markdown-input-view.js to React Native's own primitives and the native
|
|
3
3
|
// `<RichText>`. Only the imports differ from the web mirror.
|
|
4
4
|
|
|
5
|
-
import { Text, View, Pressable, TextInput } from "react-native";
|
|
5
|
+
import { Text, View, Pressable, TextInput, ScrollView } from "react-native";
|
|
6
6
|
import { makeMarkdownInput } from "./markdown-input-view.js";
|
|
7
7
|
import { RichText } from "./richtext.native.js";
|
|
8
|
+
import { Overlay } from "./overlay.native.js";
|
|
8
9
|
|
|
10
|
+
// sc-7349: ScrollView joins the set for the link form's page list, which must
|
|
11
|
+
// scroll rather than grow an app's every page past the panel.
|
|
9
12
|
export const MarkdownInput = makeMarkdownInput(
|
|
10
|
-
{ Text, View, Pressable, TextInput },
|
|
13
|
+
{ Text, View, Pressable, TextInput, ScrollView },
|
|
11
14
|
RichText,
|
|
15
|
+
Overlay,
|
|
12
16
|
);
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// sc-7349 — the link form `<MarkdownInput>`'s Link button opens.
|
|
2
|
+
//
|
|
3
|
+
// A link needs a TARGET, and no selection can supply one: the author has to
|
|
4
|
+
// name either a page in this app or an external address. That is a form, and a
|
|
5
|
+
// form belongs in an `<Overlay>` — the SDK primitive that escapes the widget's
|
|
6
|
+
// own clipping box on both hosts — not in a bespoke absolutely-positioned panel
|
|
7
|
+
// the layout container would crop.
|
|
8
|
+
//
|
|
9
|
+
// Split out of markdown-input-view.js so each file stays one component's worth
|
|
10
|
+
// of markup. Like every view module here it is bound per host by the caller
|
|
11
|
+
// (see markdown-input.js / .native.js), so the two hosts cannot drift.
|
|
12
|
+
|
|
13
|
+
import React from "react";
|
|
14
|
+
|
|
15
|
+
// The page list scrolls rather than growing an app's 40 pages past the panel.
|
|
16
|
+
const PAGE_LIST_MAX_HEIGHT = 180;
|
|
17
|
+
|
|
18
|
+
export function makeMarkdownLinkDialog(rn, Overlay) {
|
|
19
|
+
const { Text, View, Pressable, TextInput, ScrollView } = rn;
|
|
20
|
+
|
|
21
|
+
function Field({ label, tokens, children }) {
|
|
22
|
+
return React.createElement(
|
|
23
|
+
View,
|
|
24
|
+
{ style: { marginBottom: tokens.blockGap } },
|
|
25
|
+
React.createElement(
|
|
26
|
+
Text,
|
|
27
|
+
{
|
|
28
|
+
style: {
|
|
29
|
+
fontFamily: tokens.bodyFont,
|
|
30
|
+
fontSize: tokens.codeSize,
|
|
31
|
+
color: tokens.mutedColor,
|
|
32
|
+
marginBottom: tokens.markerGap,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
label,
|
|
36
|
+
),
|
|
37
|
+
children,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {object} props
|
|
43
|
+
* @param {boolean} props.visible
|
|
44
|
+
* @param {() => void} props.onClose
|
|
45
|
+
* @param {(result: {label: string, target: string}) => void} props.onSubmit
|
|
46
|
+
* @param {object} props.tokens — resolveRichTextTokens output.
|
|
47
|
+
* @param {Array<{id: string, name: string}>} props.pages — pickable pages;
|
|
48
|
+
* empty means this host offered none, so only the URL mode is shown.
|
|
49
|
+
* @param {string} props.initialLabel — the author's current selection.
|
|
50
|
+
* @param {string} [props.testID]
|
|
51
|
+
*/
|
|
52
|
+
function MarkdownLinkDialog({
|
|
53
|
+
visible,
|
|
54
|
+
onClose,
|
|
55
|
+
onSubmit,
|
|
56
|
+
tokens,
|
|
57
|
+
pages,
|
|
58
|
+
initialLabel,
|
|
59
|
+
testID,
|
|
60
|
+
}) {
|
|
61
|
+
const pageList = Array.isArray(pages) ? pages.filter((p) => p && p.id) : [];
|
|
62
|
+
const canPickPage = pageList.length > 0;
|
|
63
|
+
|
|
64
|
+
const [label, setLabel] = React.useState("");
|
|
65
|
+
const [url, setUrl] = React.useState("");
|
|
66
|
+
const [pageId, setPageId] = React.useState("");
|
|
67
|
+
const [mode, setMode] = React.useState("url");
|
|
68
|
+
|
|
69
|
+
// Re-seed on each open: the author's selection and the mode they should
|
|
70
|
+
// land in are both properties of THIS opening, not of the component.
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
if (!visible) return;
|
|
73
|
+
setLabel(typeof initialLabel === "string" ? initialLabel : "");
|
|
74
|
+
setUrl("");
|
|
75
|
+
setPageId("");
|
|
76
|
+
setMode(canPickPage ? "page" : "url");
|
|
77
|
+
}, [visible, initialLabel, canPickPage]);
|
|
78
|
+
|
|
79
|
+
const target = mode === "page" ? pageId : url.trim();
|
|
80
|
+
|
|
81
|
+
const submit = () => {
|
|
82
|
+
if (!target) return;
|
|
83
|
+
if (typeof onSubmit === "function") onSubmit({ label, target });
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const inputStyle = {
|
|
87
|
+
fontFamily: tokens.bodyFont,
|
|
88
|
+
fontSize: tokens.bodySize,
|
|
89
|
+
color: tokens.color,
|
|
90
|
+
backgroundColor: tokens.surface,
|
|
91
|
+
borderWidth: 1,
|
|
92
|
+
borderColor: tokens.border,
|
|
93
|
+
borderRadius: tokens.radius,
|
|
94
|
+
padding: tokens.markerGap,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const tab = (key, text) =>
|
|
98
|
+
React.createElement(
|
|
99
|
+
Pressable,
|
|
100
|
+
{
|
|
101
|
+
key,
|
|
102
|
+
onPress: () => setMode(key),
|
|
103
|
+
accessibilityRole: "button",
|
|
104
|
+
accessibilityState: { selected: mode === key },
|
|
105
|
+
testID: testID ? `${testID}-mode-${key}` : undefined,
|
|
106
|
+
style: {
|
|
107
|
+
paddingVertical: tokens.markerGap,
|
|
108
|
+
paddingHorizontal: tokens.padding,
|
|
109
|
+
marginRight: tokens.markerGap,
|
|
110
|
+
borderWidth: 1,
|
|
111
|
+
borderColor: mode === key ? tokens.accent : tokens.border,
|
|
112
|
+
borderRadius: tokens.radius,
|
|
113
|
+
...(mode === key ? { backgroundColor: tokens.accent } : null),
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
React.createElement(
|
|
117
|
+
Text,
|
|
118
|
+
{
|
|
119
|
+
style: {
|
|
120
|
+
fontFamily: tokens.bodyFont,
|
|
121
|
+
fontSize: tokens.codeSize,
|
|
122
|
+
fontWeight: "500",
|
|
123
|
+
color: mode === key ? tokens.onAccent : tokens.color,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
text,
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const action = (key, text, { primary, disabled, onPress }) =>
|
|
131
|
+
React.createElement(
|
|
132
|
+
Pressable,
|
|
133
|
+
{
|
|
134
|
+
key,
|
|
135
|
+
onPress,
|
|
136
|
+
disabled,
|
|
137
|
+
accessibilityRole: "button",
|
|
138
|
+
testID: testID ? `${testID}-${key}` : undefined,
|
|
139
|
+
style: {
|
|
140
|
+
paddingVertical: tokens.markerGap,
|
|
141
|
+
paddingHorizontal: tokens.padding,
|
|
142
|
+
marginLeft: tokens.markerGap,
|
|
143
|
+
borderWidth: 1,
|
|
144
|
+
borderColor: primary ? tokens.accent : tokens.border,
|
|
145
|
+
borderRadius: tokens.radius,
|
|
146
|
+
opacity: disabled ? 0.5 : 1,
|
|
147
|
+
...(primary ? { backgroundColor: tokens.accent } : null),
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
React.createElement(
|
|
151
|
+
Text,
|
|
152
|
+
{
|
|
153
|
+
style: {
|
|
154
|
+
fontFamily: tokens.bodyFont,
|
|
155
|
+
fontSize: tokens.codeSize,
|
|
156
|
+
fontWeight: "500",
|
|
157
|
+
color: primary ? tokens.onAccent : tokens.color,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
text,
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return React.createElement(
|
|
165
|
+
Overlay,
|
|
166
|
+
{
|
|
167
|
+
visible: !!visible,
|
|
168
|
+
onRequestClose: onClose,
|
|
169
|
+
size: "sm",
|
|
170
|
+
accessibilityLabel: "Insert link",
|
|
171
|
+
testID: testID ? `${testID}-dialog` : undefined,
|
|
172
|
+
},
|
|
173
|
+
React.createElement(
|
|
174
|
+
Field,
|
|
175
|
+
{ label: "Link text", tokens },
|
|
176
|
+
React.createElement(TextInput, {
|
|
177
|
+
value: label,
|
|
178
|
+
onChangeText: setLabel,
|
|
179
|
+
placeholder: "Text the reader sees",
|
|
180
|
+
placeholderTextColor: tokens.mutedColor,
|
|
181
|
+
accessibilityLabel: "Link text",
|
|
182
|
+
style: inputStyle,
|
|
183
|
+
testID: testID ? `${testID}-label` : undefined,
|
|
184
|
+
}),
|
|
185
|
+
),
|
|
186
|
+
|
|
187
|
+
// With no pages to offer there is nothing to choose between, so the
|
|
188
|
+
// switcher is hidden rather than shown with one dead option.
|
|
189
|
+
canPickPage
|
|
190
|
+
? React.createElement(
|
|
191
|
+
View,
|
|
192
|
+
{ style: { flexDirection: "row", marginBottom: tokens.blockGap } },
|
|
193
|
+
tab("page", "A page in this app"),
|
|
194
|
+
tab("url", "An external URL"),
|
|
195
|
+
)
|
|
196
|
+
: null,
|
|
197
|
+
|
|
198
|
+
mode === "page" && canPickPage
|
|
199
|
+
? React.createElement(
|
|
200
|
+
Field,
|
|
201
|
+
{ label: "Page", tokens },
|
|
202
|
+
React.createElement(
|
|
203
|
+
ScrollView,
|
|
204
|
+
{
|
|
205
|
+
style: {
|
|
206
|
+
maxHeight: PAGE_LIST_MAX_HEIGHT,
|
|
207
|
+
borderWidth: 1,
|
|
208
|
+
borderColor: tokens.border,
|
|
209
|
+
borderRadius: tokens.radius,
|
|
210
|
+
},
|
|
211
|
+
testID: testID ? `${testID}-pages` : undefined,
|
|
212
|
+
},
|
|
213
|
+
pageList.map((page) =>
|
|
214
|
+
React.createElement(
|
|
215
|
+
Pressable,
|
|
216
|
+
{
|
|
217
|
+
key: page.id,
|
|
218
|
+
onPress: () => setPageId(page.id),
|
|
219
|
+
accessibilityRole: "button",
|
|
220
|
+
accessibilityState: { selected: pageId === page.id },
|
|
221
|
+
style: {
|
|
222
|
+
padding: tokens.markerGap,
|
|
223
|
+
...(pageId === page.id
|
|
224
|
+
? { backgroundColor: tokens.surface }
|
|
225
|
+
: null),
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
React.createElement(
|
|
229
|
+
Text,
|
|
230
|
+
{
|
|
231
|
+
style: {
|
|
232
|
+
fontFamily: tokens.bodyFont,
|
|
233
|
+
fontSize: tokens.bodySize,
|
|
234
|
+
color: pageId === page.id ? tokens.accent : tokens.color,
|
|
235
|
+
fontWeight: pageId === page.id ? "700" : "400",
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
page.name || page.id,
|
|
239
|
+
),
|
|
240
|
+
),
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
: React.createElement(
|
|
245
|
+
Field,
|
|
246
|
+
{ label: "Address", tokens },
|
|
247
|
+
React.createElement(TextInput, {
|
|
248
|
+
value: url,
|
|
249
|
+
onChangeText: setUrl,
|
|
250
|
+
placeholder: "https://example.com",
|
|
251
|
+
placeholderTextColor: tokens.mutedColor,
|
|
252
|
+
accessibilityLabel: "Link address",
|
|
253
|
+
autoCapitalize: "none",
|
|
254
|
+
autoCorrect: false,
|
|
255
|
+
keyboardType: "url",
|
|
256
|
+
style: inputStyle,
|
|
257
|
+
testID: testID ? `${testID}-url` : undefined,
|
|
258
|
+
}),
|
|
259
|
+
),
|
|
260
|
+
|
|
261
|
+
React.createElement(
|
|
262
|
+
View,
|
|
263
|
+
{
|
|
264
|
+
style: {
|
|
265
|
+
flexDirection: "row",
|
|
266
|
+
justifyContent: "flex-end",
|
|
267
|
+
marginTop: tokens.markerGap,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
action("cancel", "Cancel", { onPress: onClose }),
|
|
271
|
+
// Disabled until there is somewhere to go — an empty target would
|
|
272
|
+
// write `[text]()`, which reads as a link and goes nowhere.
|
|
273
|
+
action("insert", "Insert link", {
|
|
274
|
+
primary: true,
|
|
275
|
+
disabled: !target,
|
|
276
|
+
onPress: submit,
|
|
277
|
+
}),
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
MarkdownLinkDialog.displayName = "MarkdownLinkDialog";
|
|
283
|
+
return MarkdownLinkDialog;
|
|
284
|
+
}
|