@colixsystems/widget-sdk 0.206.0 → 0.210.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 +56 -1
- package/dist/banner-tokens.js +116 -0
- package/dist/banner-view.js +198 -0
- package/dist/banner.js +7 -0
- package/dist/banner.native.js +7 -0
- package/dist/contract.cjs +66 -2
- package/dist/contract.js +66 -2
- package/dist/hooks.js +13 -0
- package/dist/host.d.ts +52 -0
- package/dist/host.js +8 -0
- package/dist/index.d.ts +128 -0
- package/dist/index.js +4 -0
- package/dist/index.native.js +4 -0
- package/dist/markdown-input-view.js +2 -3
- package/dist/markdown-input.js +4 -2
- package/dist/markdown-input.native.js +4 -2
- package/dist/markdown-link-dialog.js +94 -108
- package/dist/modal-tokens.js +107 -0
- package/dist/modal-view.js +255 -0
- package/dist/modal.js +8 -0
- package/dist/modal.native.js +45 -0
- package/dist/overlay-tokens.js +69 -5
- package/dist/overlay-view.js +52 -16
- package/dist/overlay.native.js +8 -1
- package/dist/readable-ink.js +28 -0
- package/dist/surface-theme.js +108 -0
- package/dist/tabs-tokens.js +3 -9
- package/dist/toast-host.js +1 -1
- package/dist/web-modal.js +24 -5
- package/package.json +1 -1
- package/src/contract.js +66 -2
package/dist/hooks.js
CHANGED
|
@@ -89,6 +89,19 @@ export function BackdropScope({ basis, children }) {
|
|
|
89
89
|
return React.createElement(HostWidgetContext.Provider, { value }, children);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* @internal — hands a subtree `theme` in place of the host's (`<Modal>` uses
|
|
94
|
+
* it for the theme re-inked for its own background). No theme, no change.
|
|
95
|
+
*/
|
|
96
|
+
export function ThemeScope({ theme, children }) {
|
|
97
|
+
const ctx = useContext(HostWidgetContext);
|
|
98
|
+
const value = useMemo(
|
|
99
|
+
() => (theme && ctx ? { ...ctx, workspace: { ...ctx.workspace, theme } } : ctx),
|
|
100
|
+
[ctx, theme],
|
|
101
|
+
);
|
|
102
|
+
return React.createElement(HostWidgetContext.Provider, { value }, children);
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
// Exported for ./toast.js + future hook modules outside hooks.js. Internal
|
|
93
106
|
// utility — widget code should not import this directly.
|
|
94
107
|
export function useWidgetContextOrThrow(hookName) {
|
package/dist/host.d.ts
CHANGED
|
@@ -39,6 +39,19 @@ export type ThemeWidgetStyles = Record<string, ThemeComponentStyle>;
|
|
|
39
39
|
*/
|
|
40
40
|
export function normaliseThemeComponents(raw: unknown): ThemeComponents;
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* What each token of the `alert` component scope resolves to while unset,
|
|
44
|
+
* keyed by token name: the values `<Banner>` paints an unthemed app with.
|
|
45
|
+
*/
|
|
46
|
+
export function bannerScopeDefaults(theme: unknown): Record<string, string | number>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The theme a surface painting `backgroundColor` hands its subtree: surface
|
|
50
|
+
* tokens derived for that fill, and any type tier whose own ink cannot be read
|
|
51
|
+
* on it re-inked. Returns `theme` itself when the colour is unusable.
|
|
52
|
+
*/
|
|
53
|
+
export function surfaceAwareTheme<T>(theme: T, backgroundColor: unknown): T;
|
|
54
|
+
|
|
42
55
|
/** Minimum contrast: `text` for labels (WCAG 1.4.3), `boundary` for edges (1.4.11). */
|
|
43
56
|
export const INK_CONTRAST: Readonly<{ text: number; boundary: number }>;
|
|
44
57
|
|
|
@@ -146,6 +159,45 @@ export interface ToastControllerOptions {
|
|
|
146
159
|
clearTimer?: (handle: unknown) => void;
|
|
147
160
|
}
|
|
148
161
|
|
|
162
|
+
/** What one `<Modal>` paints with; see `resolveModalTokens`. */
|
|
163
|
+
export interface ModalTokens {
|
|
164
|
+
size: "sm" | "md" | "lg" | "full";
|
|
165
|
+
presentation: "dialog" | "sheet";
|
|
166
|
+
scrim: string;
|
|
167
|
+
/** The `background` token as set (it may be see-through), else the surface. */
|
|
168
|
+
background: string;
|
|
169
|
+
/** What the panel paints: `background` composited over the overlay surface. */
|
|
170
|
+
panelColor: string;
|
|
171
|
+
/** `panelColor` when a background is set, for re-inking the body; else null. */
|
|
172
|
+
bodyBackdrop: string | null;
|
|
173
|
+
radius: number;
|
|
174
|
+
padding: number;
|
|
175
|
+
/** The themed elevation as a style, or null for the overlay's own. */
|
|
176
|
+
shadow: Record<string, unknown> | null;
|
|
177
|
+
/** The elevation level in force: the themed one, else the overlay's. */
|
|
178
|
+
shadowLevel: "none" | "sm" | "md" | "lg";
|
|
179
|
+
titleColor: string;
|
|
180
|
+
closeColor: string;
|
|
181
|
+
sectionGap: number;
|
|
182
|
+
actionGap: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The look of the SDK `<Modal>` under a resolved theme: its `modal` component
|
|
187
|
+
* scope over the overlay's defaults. For a host previewing Theme Settings >
|
|
188
|
+
* Dialogs with the values the dialog itself paints.
|
|
189
|
+
*/
|
|
190
|
+
export function resolveModalTokens(
|
|
191
|
+
theme: unknown,
|
|
192
|
+
options?: { size?: string; presentation?: string },
|
|
193
|
+
): ModalTokens;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* What the `modal` scope's tokens without a `uiDefault` path resolve to while
|
|
197
|
+
* unset: the scrim, and the title colour on the background currently set.
|
|
198
|
+
*/
|
|
199
|
+
export function modalScopeDefaults(theme: unknown): { scrim: string; titleColor: string };
|
|
200
|
+
|
|
149
201
|
export const TOAST_DEFAULTS: { durationMs: number; maxVisible: number };
|
|
150
202
|
|
|
151
203
|
export function normalizeToastKind(kind: unknown): ToastKind;
|
package/dist/host.js
CHANGED
|
@@ -42,12 +42,16 @@ export {
|
|
|
42
42
|
// Host plumbing, deliberately off the author entry point: a widget reads the
|
|
43
43
|
// already-correct `useTheme().colors`, it never derives them itself.
|
|
44
44
|
export { deriveSurfaceTokens, inkForBackdrop, INK_CONTRAST } from "./contract.js";
|
|
45
|
+
export { surfaceAwareTheme } from "./surface-theme.js";
|
|
45
46
|
|
|
46
47
|
// REQ-WSDK-PLATFORM §6: the host half of `useToast()` — the queue, the
|
|
47
48
|
// auto-dismiss timing, and the themed values a toast is painted with. The
|
|
48
49
|
// widget-facing hook only forwards to `ctx.toast.showToast`; the host owns the
|
|
49
50
|
// surface, and sharing everything but the JSX is what keeps the web Player and
|
|
50
51
|
// the Expo export rendering the same notification (CLAUDE.md §8).
|
|
52
|
+
// The dialog's resolved look, for the Studio's Design > Components > Dialogs preview.
|
|
53
|
+
export { resolveModalTokens, modalScopeDefaults } from "./modal-tokens.js";
|
|
54
|
+
|
|
51
55
|
export {
|
|
52
56
|
createToastController,
|
|
53
57
|
resolveToastTokens,
|
|
@@ -55,6 +59,10 @@ export {
|
|
|
55
59
|
TOAST_DEFAULTS,
|
|
56
60
|
} from "./toast-host.js";
|
|
57
61
|
|
|
62
|
+
// What each `alert` scope token resolves to while unset, for the Studio's
|
|
63
|
+
// Design page to preview beside its controls.
|
|
64
|
+
export { bannerScopeDefaults } from "./banner-tokens.js";
|
|
65
|
+
|
|
58
66
|
// REQ-NAV-STRUCTURE: the navigation resolver both hosts draw their chrome from.
|
|
59
67
|
// `normaliseNavigation` turns a stored `theme_config.navigation` block into the
|
|
60
68
|
// resolved `{ menuType }` — always `sidebar` since sc-7352 retired the other
|
package/dist/index.d.ts
CHANGED
|
@@ -3257,13 +3257,107 @@ export const Overlay: (props: {
|
|
|
3257
3257
|
onRequestClose?: () => void;
|
|
3258
3258
|
/** Panel width tier. `full` runs edge to edge. Defaults to `md`. */
|
|
3259
3259
|
size?: "sm" | "md" | "lg" | "full";
|
|
3260
|
+
/** Centred (`dialog`, the default) or anchored to the bottom edge (`sheet`). */
|
|
3261
|
+
presentation?: OverlayPresentation;
|
|
3260
3262
|
/** Set false when only an explicit control may close the overlay. */
|
|
3261
3263
|
dismissOnBackdropPress?: boolean;
|
|
3264
|
+
/**
|
|
3265
|
+
* Set false when the content has its own close control: the backdrop stays
|
|
3266
|
+
* pressable but leaves the tab order and the accessibility tree.
|
|
3267
|
+
*/
|
|
3268
|
+
backdropAccessible?: boolean;
|
|
3269
|
+
/** The web dialog node's role. Defaults to "dialog". No native equivalent. */
|
|
3270
|
+
role?: "dialog" | "alertdialog";
|
|
3271
|
+
/**
|
|
3272
|
+
* Names the dialog: on the web the node that carries the role, in the
|
|
3273
|
+
* exported app the panel (React Native's Modal carries no name).
|
|
3274
|
+
*/
|
|
3262
3275
|
accessibilityLabel?: string;
|
|
3276
|
+
/** The id of the element that names the dialog; wins over the label. Web only. */
|
|
3277
|
+
accessibilityLabelledBy?: string;
|
|
3263
3278
|
/** Label for the backdrop's dismiss target. Defaults to "Close". */
|
|
3264
3279
|
closeAccessibilityLabel?: string;
|
|
3280
|
+
/** Focused on open instead of the dialog itself. Web only; a no-op on native. */
|
|
3281
|
+
initialFocusRef?: { current: any };
|
|
3265
3282
|
/** Extra styles merged onto the themed panel. */
|
|
3266
3283
|
style?: any;
|
|
3284
|
+
/** Set on the panel. */
|
|
3285
|
+
testID?: string;
|
|
3286
|
+
children?: ReactNode;
|
|
3287
|
+
}) => any;
|
|
3288
|
+
|
|
3289
|
+
export type OverlayPresentation = "dialog" | "sheet";
|
|
3290
|
+
|
|
3291
|
+
/**
|
|
3292
|
+
* The standard dialog, built on `<Overlay>`: a title, a close button, a body
|
|
3293
|
+
* that scrolls when it is long, and a footer action row laid out below it.
|
|
3294
|
+
* Its background, corner radius, padding, scrim, shadow and title colour come
|
|
3295
|
+
* from the theme's `modal` scope (Design > Components > Dialogs), so every dialog in
|
|
3296
|
+
* the app matches. Use it instead of hand-building a header and an action row
|
|
3297
|
+
* inside a bare `<Overlay>`.
|
|
3298
|
+
*
|
|
3299
|
+
* A dialog that paints its own background hands what is rendered inside it a
|
|
3300
|
+
* theme re-inked for that background, so SDK components and any component
|
|
3301
|
+
* calling `useTheme()` inside the Modal stay readable.
|
|
3302
|
+
*
|
|
3303
|
+
* In the exported app the dialog pads a `sheet` by the bottom safe-area inset
|
|
3304
|
+
* and a `full` dialog by the top and bottom ones, and wraps its panel in React
|
|
3305
|
+
* Native's `KeyboardAvoidingView`. This is not yet verified on a device, and
|
|
3306
|
+
* `KeyboardAvoidingView` inside a React Native `Modal` window is a known risk
|
|
3307
|
+
* on Android: until it is verified, keep a form with text inputs in the
|
|
3308
|
+
* default `dialog` presentation. A bare `<Overlay>` gets neither.
|
|
3309
|
+
*
|
|
3310
|
+
* @example
|
|
3311
|
+
* <Modal
|
|
3312
|
+
* visible={open}
|
|
3313
|
+
* onRequestClose={() => setOpen(false)}
|
|
3314
|
+
* title="Edit order"
|
|
3315
|
+
* footer={
|
|
3316
|
+
* <>
|
|
3317
|
+
* <Button variant="secondary" title="Cancel" onPress={() => setOpen(false)} />
|
|
3318
|
+
* <Button title="Save" onPress={save} />
|
|
3319
|
+
* </>
|
|
3320
|
+
* }
|
|
3321
|
+
* >
|
|
3322
|
+
* <TextField value={note} onChangeText={setNote} placeholder="Note" />
|
|
3323
|
+
* </Modal>
|
|
3324
|
+
*/
|
|
3325
|
+
export const Modal: (props: {
|
|
3326
|
+
/** Nothing renders (and nothing is mounted) while this is false. */
|
|
3327
|
+
visible?: boolean;
|
|
3328
|
+
/**
|
|
3329
|
+
* The close button, backdrop press, Escape (web) and Android back fire this.
|
|
3330
|
+
* Omitted, no close button is drawn and only your own controls close it.
|
|
3331
|
+
*/
|
|
3332
|
+
onRequestClose?: () => void;
|
|
3333
|
+
/**
|
|
3334
|
+
* Defaults to true. False while the dialog must stay open (a save in
|
|
3335
|
+
* flight): no close button, and the backdrop, Escape and Android back do
|
|
3336
|
+
* not fire `onRequestClose`. Only while that work runs, and always restore
|
|
3337
|
+
* it: with no footer button or in-body control, nobody can close the dialog.
|
|
3338
|
+
*/
|
|
3339
|
+
dismissible?: boolean;
|
|
3340
|
+
/** With no title and no close button the dialog has no header row. */
|
|
3341
|
+
title?: string;
|
|
3342
|
+
/** The action buttons: one right-aligned row that wraps. Omit for no footer. */
|
|
3343
|
+
footer?: ReactNode;
|
|
3344
|
+
/** Centred (`dialog`, the default) or anchored to the bottom edge (`sheet`). */
|
|
3345
|
+
presentation?: OverlayPresentation;
|
|
3346
|
+
/** Panel width tier. `full` runs edge to edge. Defaults to `md`. */
|
|
3347
|
+
size?: "sm" | "md" | "lg" | "full";
|
|
3348
|
+
/** Set false when only the close button or a footer action may close it. */
|
|
3349
|
+
dismissOnBackdropPress?: boolean;
|
|
3350
|
+
/** Defaults to "dialog". Web only; native has no equivalent. */
|
|
3351
|
+
role?: "dialog" | "alertdialog";
|
|
3352
|
+
/** Focused on open instead of the dialog itself. Web only; a no-op on native. */
|
|
3353
|
+
initialFocusRef?: { current: any };
|
|
3354
|
+
/** Label for the close button. Defaults to "Close". */
|
|
3355
|
+
closeAccessibilityLabel?: string;
|
|
3356
|
+
/** Names a dialog that has no title; the title names it otherwise. */
|
|
3357
|
+
accessibilityLabel?: string;
|
|
3358
|
+
/** On the panel; the parts take `-close`, `-body` and `-footer`. */
|
|
3359
|
+
testID?: string;
|
|
3360
|
+
/** The body. It scrolls on its own: do not wrap it in a ScrollView. */
|
|
3267
3361
|
children?: ReactNode;
|
|
3268
3362
|
}) => any;
|
|
3269
3363
|
|
|
@@ -3571,6 +3665,40 @@ export const Tabs: (props: {
|
|
|
3571
3665
|
testID?: string;
|
|
3572
3666
|
}) => any;
|
|
3573
3667
|
|
|
3668
|
+
export type BannerTone = "info" | "success" | "warning" | "error";
|
|
3669
|
+
|
|
3670
|
+
/**
|
|
3671
|
+
* Themed inline message: an error, warning, success or notice that stays in
|
|
3672
|
+
* place until it is resolved. Its fill, text colour, border and corners come
|
|
3673
|
+
* from the theme's `alert` scope, so every banner in the app matches. An
|
|
3674
|
+
* `error` is marked as an alert and the other tones as a polite status: the
|
|
3675
|
+
* web and Android announce them when they appear, iOS reads them in place.
|
|
3676
|
+
* For a brief confirmation that may disappear on its own, use `useToast()`.
|
|
3677
|
+
*
|
|
3678
|
+
* @example
|
|
3679
|
+
* <Banner tone="error" message={error.message} action={{ label: "Retry", onPress: refetch }} />
|
|
3680
|
+
*/
|
|
3681
|
+
export const Banner: (props: {
|
|
3682
|
+
/** Default "info". */
|
|
3683
|
+
tone?: BannerTone;
|
|
3684
|
+
/** Text is themed; an element renders as given. */
|
|
3685
|
+
title?: ReactNode;
|
|
3686
|
+
/** Text wraps onto as many lines as it needs; an element renders as given. */
|
|
3687
|
+
message?: ReactNode;
|
|
3688
|
+
/** A lucide name replacing the tone's glyph, or `false` for none. */
|
|
3689
|
+
icon?: string | false | null;
|
|
3690
|
+
action?: { label: string; onPress: () => void; accessibilityLabel?: string; testID?: string };
|
|
3691
|
+
/** Shows a close control; hide the banner in the handler. */
|
|
3692
|
+
onDismiss?: () => void;
|
|
3693
|
+
/** Default "Dismiss". */
|
|
3694
|
+
dismissAccessibilityLabel?: string;
|
|
3695
|
+
/** For hosts rendering outside a widget context; widgets omit it. */
|
|
3696
|
+
theme?: any;
|
|
3697
|
+
style?: any;
|
|
3698
|
+
/** The action and close control default to `<testID>-action` / `<testID>-dismiss`. */
|
|
3699
|
+
testID?: string;
|
|
3700
|
+
}) => any;
|
|
3701
|
+
|
|
3574
3702
|
/**
|
|
3575
3703
|
* Themed collapsible sections. The surface comes from the theme's `card`
|
|
3576
3704
|
* scope and the headers and dividers from its `list` scope, so every
|
package/dist/index.js
CHANGED
|
@@ -270,6 +270,10 @@ export { Accordion, AccordionItem } from "./accordion.js";
|
|
|
270
270
|
export { Tabs } from "./tabs.js";
|
|
271
271
|
// Themed loading states; follow the theme's `loading` scope.
|
|
272
272
|
export { Spinner, Skeleton } from "./loading.js";
|
|
273
|
+
// A themed inline message; follows the theme's `alert` scope.
|
|
274
|
+
export { Banner } from "./banner.js";
|
|
275
|
+
// The standard dialog, on `<Overlay>`; follows the theme's `modal` scope.
|
|
276
|
+
export { Modal } from "./modal.js";
|
|
273
277
|
// The semantic text tiers, shared with the built-in widgets.
|
|
274
278
|
export {
|
|
275
279
|
textRoleStyle,
|
package/dist/index.native.js
CHANGED
|
@@ -263,6 +263,10 @@ export { Accordion, AccordionItem } from "./accordion.native.js";
|
|
|
263
263
|
export { Tabs } from "./tabs.native.js";
|
|
264
264
|
// Themed loading states; follow the theme's `loading` scope.
|
|
265
265
|
export { Spinner, Skeleton } from "./loading.native.js";
|
|
266
|
+
// A themed inline message; follows the theme's `alert` scope.
|
|
267
|
+
export { Banner } from "./banner.native.js";
|
|
268
|
+
// The standard dialog, on `<Overlay>`; follows the theme's `modal` scope.
|
|
269
|
+
export { Modal } from "./modal.native.js";
|
|
266
270
|
// The semantic text tiers, shared with the built-in widgets.
|
|
267
271
|
export {
|
|
268
272
|
textRoleStyle,
|
|
@@ -34,9 +34,9 @@ const TOOLS = Object.freeze([
|
|
|
34
34
|
},
|
|
35
35
|
]);
|
|
36
36
|
|
|
37
|
-
export function makeMarkdownInput(rn, RichText,
|
|
37
|
+
export function makeMarkdownInput(rn, RichText, Modal, Button) {
|
|
38
38
|
const { Text, View, Pressable, TextInput } = rn;
|
|
39
|
-
const MarkdownLinkDialog = makeMarkdownLinkDialog(rn,
|
|
39
|
+
const MarkdownLinkDialog = makeMarkdownLinkDialog(rn, Modal, Button);
|
|
40
40
|
|
|
41
41
|
function MarkdownInput({
|
|
42
42
|
value,
|
|
@@ -210,7 +210,6 @@ export function makeMarkdownInput(rn, RichText, Overlay) {
|
|
|
210
210
|
visible: linkOpen,
|
|
211
211
|
onClose: () => setLinkOpen(false),
|
|
212
212
|
onSubmit: insertLink,
|
|
213
|
-
tokens,
|
|
214
213
|
pages,
|
|
215
214
|
initialLabel: selected,
|
|
216
215
|
testID,
|
package/dist/markdown-input.js
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
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 {
|
|
8
|
+
import { Modal } from "./modal.js";
|
|
9
|
+
import { Button } from "./button.js";
|
|
9
10
|
import { TextInput } from "./primitives.js";
|
|
10
11
|
|
|
11
12
|
export const MarkdownInput = makeMarkdownInput(
|
|
12
13
|
{ ...ReactNative, TextInput },
|
|
13
14
|
RichText,
|
|
14
|
-
|
|
15
|
+
Modal,
|
|
16
|
+
Button,
|
|
15
17
|
);
|
|
@@ -5,12 +5,14 @@
|
|
|
5
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 {
|
|
8
|
+
import { Modal } from "./modal.native.js";
|
|
9
|
+
import { Button } from "./button.native.js";
|
|
9
10
|
|
|
10
11
|
// sc-7349: ScrollView joins the set for the link form's page list, which must
|
|
11
12
|
// scroll rather than grow an app's every page past the panel.
|
|
12
13
|
export const MarkdownInput = makeMarkdownInput(
|
|
13
14
|
{ Text, View, Pressable, TextInput, ScrollView },
|
|
14
15
|
RichText,
|
|
15
|
-
|
|
16
|
+
Modal,
|
|
17
|
+
Button,
|
|
16
18
|
);
|
|
@@ -2,20 +2,22 @@
|
|
|
2
2
|
//
|
|
3
3
|
// A link needs a TARGET, and no selection can supply one: the author has to
|
|
4
4
|
// name either a page in this app or an external address. That is a form, and a
|
|
5
|
-
// form belongs in
|
|
6
|
-
// own clipping box on both hosts — not in a bespoke absolutely-
|
|
7
|
-
// the layout container would crop.
|
|
5
|
+
// form belongs in a `<Modal>` — the SDK's standard dialog, which escapes the
|
|
6
|
+
// widget's own clipping box on both hosts — not in a bespoke absolutely-
|
|
7
|
+
// positioned panel the layout container would crop.
|
|
8
8
|
//
|
|
9
9
|
// Split out of markdown-input-view.js so each file stays one component's worth
|
|
10
10
|
// of markup. Like every view module here it is bound per host by the caller
|
|
11
11
|
// (see markdown-input.js / .native.js), so the two hosts cannot drift.
|
|
12
12
|
|
|
13
13
|
import React from "react";
|
|
14
|
+
import { useHostTheme } from "./hooks.js";
|
|
15
|
+
import { resolveRichTextTokens } from "./richtext-tokens.js";
|
|
14
16
|
|
|
15
17
|
// The page list scrolls rather than growing an app's 40 pages past the panel.
|
|
16
18
|
const PAGE_LIST_MAX_HEIGHT = 180;
|
|
17
19
|
|
|
18
|
-
export function makeMarkdownLinkDialog(rn,
|
|
20
|
+
export function makeMarkdownLinkDialog(rn, Modal, Button) {
|
|
19
21
|
const { Text, View, Pressable, TextInput, ScrollView } = rn;
|
|
20
22
|
|
|
21
23
|
function Field({ label, tokens, children }) {
|
|
@@ -38,50 +40,12 @@ export function makeMarkdownLinkDialog(rn, Overlay) {
|
|
|
38
40
|
);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
};
|
|
43
|
+
// The form is its own component so its tokens resolve INSIDE the dialog: a
|
|
44
|
+
// dialog with its own background re-inks the theme its body reads.
|
|
45
|
+
function LinkForm({ form, pageList, canPickPage, testID }) {
|
|
46
|
+
const { label, setLabel, url, setUrl, pageId, setPageId, mode, setMode } = form;
|
|
47
|
+
const theme = useHostTheme();
|
|
48
|
+
const tokens = React.useMemo(() => resolveRichTextTokens(theme), [theme]);
|
|
85
49
|
|
|
86
50
|
const inputStyle = {
|
|
87
51
|
fontFamily: tokens.bodyFont,
|
|
@@ -130,49 +94,9 @@ export function makeMarkdownLinkDialog(rn, Overlay) {
|
|
|
130
94
|
),
|
|
131
95
|
);
|
|
132
96
|
|
|
133
|
-
const action = (key, text, { primary, disabled, onPress }) =>
|
|
134
|
-
React.createElement(
|
|
135
|
-
Pressable,
|
|
136
|
-
{
|
|
137
|
-
key,
|
|
138
|
-
onPress,
|
|
139
|
-
disabled,
|
|
140
|
-
accessibilityRole: "button",
|
|
141
|
-
testID: testID ? `${testID}-${key}` : undefined,
|
|
142
|
-
style: {
|
|
143
|
-
paddingVertical: tokens.markerGap,
|
|
144
|
-
paddingHorizontal: tokens.padding,
|
|
145
|
-
marginLeft: tokens.markerGap,
|
|
146
|
-
borderWidth: 1,
|
|
147
|
-
borderColor: primary ? tokens.accent : tokens.border,
|
|
148
|
-
borderRadius: tokens.radius,
|
|
149
|
-
opacity: disabled ? 0.5 : 1,
|
|
150
|
-
...(primary ? { backgroundColor: tokens.accent } : null),
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
React.createElement(
|
|
154
|
-
Text,
|
|
155
|
-
{
|
|
156
|
-
style: {
|
|
157
|
-
fontFamily: tokens.bodyFont,
|
|
158
|
-
fontSize: tokens.codeSize,
|
|
159
|
-
fontWeight: "500",
|
|
160
|
-
color: primary ? tokens.onAccent : tokens.color,
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
text,
|
|
164
|
-
),
|
|
165
|
-
);
|
|
166
|
-
|
|
167
97
|
return React.createElement(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
visible: !!visible,
|
|
171
|
-
onRequestClose: onClose,
|
|
172
|
-
size: "sm",
|
|
173
|
-
accessibilityLabel: "Insert link",
|
|
174
|
-
testID: testID ? `${testID}-dialog` : undefined,
|
|
175
|
-
},
|
|
98
|
+
React.Fragment,
|
|
99
|
+
null,
|
|
176
100
|
React.createElement(
|
|
177
101
|
Field,
|
|
178
102
|
{ label: "Link text", tokens },
|
|
@@ -209,6 +133,8 @@ export function makeMarkdownLinkDialog(rn, Overlay) {
|
|
|
209
133
|
ScrollView,
|
|
210
134
|
{
|
|
211
135
|
accessibilityRole: "radiogroup",
|
|
136
|
+
// Inside the dialog's scrolling body; Android needs the opt-in.
|
|
137
|
+
nestedScrollEnabled: true,
|
|
212
138
|
style: {
|
|
213
139
|
maxHeight: PAGE_LIST_MAX_HEIGHT,
|
|
214
140
|
borderWidth: 1,
|
|
@@ -264,25 +190,85 @@ export function makeMarkdownLinkDialog(rn, Overlay) {
|
|
|
264
190
|
testID: testID ? `${testID}-url` : undefined,
|
|
265
191
|
}),
|
|
266
192
|
),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
267
195
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
196
|
+
/**
|
|
197
|
+
* @param {object} props
|
|
198
|
+
* @param {boolean} props.visible
|
|
199
|
+
* @param {() => void} props.onClose
|
|
200
|
+
* @param {(result: {label: string, target: string}) => void} props.onSubmit
|
|
201
|
+
* @param {Array<{id: string, name: string}>} props.pages — pickable pages;
|
|
202
|
+
* empty means this host offered none, so only the URL mode is shown.
|
|
203
|
+
* @param {string} props.initialLabel — the author's current selection.
|
|
204
|
+
* @param {string} [props.testID]
|
|
205
|
+
*/
|
|
206
|
+
function MarkdownLinkDialog({
|
|
207
|
+
visible,
|
|
208
|
+
onClose,
|
|
209
|
+
onSubmit,
|
|
210
|
+
pages,
|
|
211
|
+
initialLabel,
|
|
212
|
+
testID,
|
|
213
|
+
}) {
|
|
214
|
+
const pageList = Array.isArray(pages) ? pages.filter((p) => p && p.id) : [];
|
|
215
|
+
const canPickPage = pageList.length > 0;
|
|
216
|
+
|
|
217
|
+
const [label, setLabel] = React.useState("");
|
|
218
|
+
const [url, setUrl] = React.useState("");
|
|
219
|
+
const [pageId, setPageId] = React.useState("");
|
|
220
|
+
const [mode, setMode] = React.useState("url");
|
|
221
|
+
|
|
222
|
+
// Re-seed on each open: the author's selection and the mode they should
|
|
223
|
+
// land in are both properties of THIS opening, not of the component.
|
|
224
|
+
React.useEffect(() => {
|
|
225
|
+
if (!visible) return;
|
|
226
|
+
setLabel(typeof initialLabel === "string" ? initialLabel : "");
|
|
227
|
+
setUrl("");
|
|
228
|
+
setPageId("");
|
|
229
|
+
setMode(canPickPage ? "page" : "url");
|
|
230
|
+
}, [visible, initialLabel, canPickPage]);
|
|
231
|
+
|
|
232
|
+
const target = mode === "page" ? pageId : url.trim();
|
|
233
|
+
|
|
234
|
+
const submit = () => {
|
|
235
|
+
if (!target) return;
|
|
236
|
+
if (typeof onSubmit === "function") onSubmit({ label, target });
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const action = (key, title, props) =>
|
|
240
|
+
React.createElement(Button, {
|
|
241
|
+
key,
|
|
242
|
+
title,
|
|
243
|
+
testID: testID ? `${testID}-${key}` : undefined,
|
|
244
|
+
...props,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const actions = React.createElement(
|
|
248
|
+
React.Fragment,
|
|
249
|
+
null,
|
|
250
|
+
action("cancel", "Cancel", { variant: "secondary", onPress: onClose }),
|
|
251
|
+
// Disabled until there is somewhere to go — an empty target would
|
|
252
|
+
// write `[text]()`, which reads as a link and goes nowhere.
|
|
253
|
+
action("insert", "Insert link", { disabled: !target, onPress: submit }),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return React.createElement(
|
|
257
|
+
Modal,
|
|
258
|
+
{
|
|
259
|
+
visible: !!visible,
|
|
260
|
+
onRequestClose: onClose,
|
|
261
|
+
size: "sm",
|
|
262
|
+
title: "Insert link",
|
|
263
|
+
footer: actions,
|
|
264
|
+
testID: testID ? `${testID}-dialog` : undefined,
|
|
265
|
+
},
|
|
266
|
+
React.createElement(LinkForm, {
|
|
267
|
+
form: { label, setLabel, url, setUrl, pageId, setPageId, mode, setMode },
|
|
268
|
+
pageList,
|
|
269
|
+
canPickPage,
|
|
270
|
+
testID,
|
|
271
|
+
}),
|
|
286
272
|
);
|
|
287
273
|
}
|
|
288
274
|
|