@colixsystems/widget-sdk 0.207.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 +44 -1
- package/dist/contract.cjs +37 -2
- package/dist/contract.js +37 -2
- package/dist/hooks.js +13 -0
- package/dist/host.d.ts +46 -0
- package/dist/host.js +4 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -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/surface-theme.js +108 -0
- package/dist/web-modal.js +24 -5
- package/package.json +1 -1
- package/src/contract.js +37 -2
package/README.md
CHANGED
|
@@ -83,7 +83,49 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
83
83
|
|
|
84
84
|
## Status
|
|
85
85
|
|
|
86
|
-
`v0.
|
|
86
|
+
`v0.210.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
87
|
+
|
|
88
|
+
### What's new in 0.210.0 (contract 1.169.0)
|
|
89
|
+
|
|
90
|
+
**New component `<Modal>` — the standard dialog.** Widgets that opened a dialog each built their own header, close button and action row inside a bare `<Overlay>`, so no two dialogs in an app matched and none could be restyled from the theme. `<Modal>` is that dialog, built once on `<Overlay>`:
|
|
91
|
+
|
|
92
|
+
```jsx
|
|
93
|
+
import { useState } from "react";
|
|
94
|
+
import { Modal, Button, TextField } from "@colixsystems/widget-sdk";
|
|
95
|
+
|
|
96
|
+
function AddNote({ onSave }) {
|
|
97
|
+
const [open, setOpen] = useState(false);
|
|
98
|
+
const [note, setNote] = useState("");
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
<Button title="Add note" onPress={() => setOpen(true)} />
|
|
102
|
+
<Modal
|
|
103
|
+
visible={open}
|
|
104
|
+
onRequestClose={() => setOpen(false)}
|
|
105
|
+
title="Add note"
|
|
106
|
+
footer={
|
|
107
|
+
<>
|
|
108
|
+
<Button variant="secondary" title="Cancel" onPress={() => setOpen(false)} />
|
|
109
|
+
<Button title="Save" onPress={() => onSave(note)} />
|
|
110
|
+
</>
|
|
111
|
+
}
|
|
112
|
+
>
|
|
113
|
+
<TextField value={note} onChangeText={setNote} placeholder="Note" accessibilityLabel="Note" />
|
|
114
|
+
</Modal>
|
|
115
|
+
</>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
It draws the title, a close button, a body that scrolls when it is long, and a footer action row laid out below it, and it renders outside the widget's layout box on both hosts exactly as `<Overlay>` does. Props: `visible`, `onRequestClose` (the close button, the backdrop press, Escape on web and the Android back button), `title`, `footer` (the action buttons, laid out as one right-aligned row that wraps; omit it for no footer), `presentation` (`dialog`, centred, the default | `sheet`, anchored to the bottom edge), `size` (`sm` | `md` | `lg` | `full`), `dismissOnBackdropPress`, `dismissible` (default true; false hides the close button and stops the backdrop, Escape and Android back from closing a dialog that must stay open, e.g. while it saves), `role` (`dialog` | `alertdialog`, web only), `initialFocusRef` (the control focused on open, web only), `closeAccessibilityLabel` (default `Close`), `accessibilityLabel` (names a dialog with no title) and `testID`. With no `onRequestClose` it draws no close button; with no title either, no header row. Use `dismissible={false}` only while that work runs and always restore it: with no footer button or in-body control, such a dialog cannot be closed. Do not wrap the body in your own `ScrollView`.
|
|
121
|
+
|
|
122
|
+
A dialog that paints its own background hands whatever is rendered inside it a theme re-inked for that background, so SDK components and any component of yours calling `useTheme()` inside the `<Modal>` stay readable; text you style with the theme read *outside* the dialog does not follow, so build the body as its own component. In the exported app the dialog pads a `sheet` by the bottom safe-area inset and a `full` dialog by the top and bottom ones, and wraps its panel in React Native's `KeyboardAvoidingView`. This is not yet verified on a device, and `KeyboardAvoidingView` inside a React Native `Modal` window is a known risk on Android: until it is verified, keep a form with text inputs in the default `dialog` presentation. A bare `<Overlay>` gets neither.
|
|
123
|
+
|
|
124
|
+
**New theme scope `modal`.** A dialog's `background`, `titleColor`, `scrim`, `radius`, `padding` and `shadow` are set app-wide under `themeConfig.components.modal` (Design > Components > Dialogs). `<Modal>` reads the scope itself, so the scope names it under `sdkComponents` and binds no `styleSchema` field. The `scrim` is always painted see-through: an opaque colour takes the default 55% alpha, and no scrim exceeds 85%. With no token set a `<Modal>` sits on exactly the panel and scrim `<Overlay>` has always drawn.
|
|
125
|
+
|
|
126
|
+
**`<Overlay>` gains `presentation`, `role`, `initialFocusRef`, `backdropAccessible` and `testID`, and a themed scrim.** Its `accessibilityLabel` now names the web dialog node itself (the panel still carries it in the exported app). Apart from the scrim, a bare `<Overlay>` renders in the exported app exactly as before. `presentation="sheet"` anchors the panel to the bottom edge; `dialog` (the default) is the centred panel it always was. Its scrim now follows the `modal` scope's `scrim` token, and is unchanged when the token is unset. Keep `<Overlay>` for what is not a dialog: a media or document preview, a lightbox.
|
|
127
|
+
|
|
128
|
+
The `<MarkdownInput>` link form now opens as a `<Modal>`. Hosts get `resolveModalTokens(theme, { size, presentation })`, `modalScopeDefaults(theme)` and `surfaceAwareTheme(theme, backgroundColor)` from `@colixsystems/widget-sdk/host`. Additive.
|
|
87
129
|
|
|
88
130
|
### What's new in 0.207.0 (contract 1.166.0)
|
|
89
131
|
|
|
@@ -2457,6 +2499,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
2457
2499
|
- `WidgetTree({ node })` — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
|
|
2458
2500
|
- `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`; `DateTimePicker` wraps `@react-native-community/datetimepicker` on native and renders `<input type="date|time|datetime-local">` directly on web because the RN library has no react-native-web mapping). The web build aliases `react-native` to `react-native-web` so the RN-re-exported primitives render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
|
|
2459
2501
|
- `Overlay` — the screen-level overlay (sc-6607). `<Overlay visible={!!preview} onRequestClose={() => setPreview(null)} size="full">…</Overlay>` renders its children OUTSIDE the widget's layout box on both hosts (web portals it to the document root, native uses the OS modal), so no clipping card, `ScrollView` or neighbouring widget can cut off a document/media preview, lightbox or confirm dialog — which absolute positioning inside the widget cannot achieve on either platform. Children stay in your own React tree, so state and hooks work normally. `onRequestClose` carries the backdrop press, Escape on web, and the Android back button; `size` is `sm` | `md` (default) | `lg` | `full`; the scrim, surface, radius, padding and elevation come from the workspace theme. An anchored dropdown still belongs inside your own root.
|
|
2502
|
+
- `Modal` — the standard dialog, built on `Overlay`. `<Modal visible={open} onRequestClose={() => setOpen(false)} title="Edit order" footer={<Button title="Save" onPress={save} />}>…</Modal>` draws a title, a close button, a body that scrolls when it is long and a footer action row laid out below it. Its background, corner radius, padding, scrim, shadow and title colour come from the theme's `modal` scope (Design > Components > Dialogs), so every dialog in an app matches. Props: `visible`, `onRequestClose`, `dismissible`, `title`, `footer`, `presentation` (`dialog` | `sheet`), `size` (`sm` | `md` | `lg` | `full`), `dismissOnBackdropPress`, `role`, `initialFocusRef`, `closeAccessibilityLabel`, `accessibilityLabel`, `testID`. Use it for every dialog instead of hand-building a header and an action row inside `Overlay`.
|
|
2460
2503
|
- `RichText` — the formatted-content renderer (sc-6970). `<RichText value={post.body} />` parses the markdown subset (`**bold**`, `*italic*` / `_italic_`, `` `code` ``, `#`/`##`/`###` headings, `-`/`*` bullets, `1.` ordered items, one-line `` images) and renders it with SDK primitives, so formatted content reads the same in the web Player and the exported Expo app. `value` is markdown, NOT HTML — widgets have no `dangerouslySetInnerHTML` on either host, so tags in a content string render as literal tag soup; stored text that still holds legacy HTML is stripped to plain text rather than shown as markup. Props: `value`, `renderImage` (`({ src, alt, size }) => node` — supply it when images are filestore ids, because resolving one needs the scopes your own widget holds; without it only absolute `http(s)` URLs render), `style`, `testID`. Colour, type scale and leading come from the workspace theme — never re-style them.
|
|
2461
2504
|
- `MarkdownInput` — the authoring half of `<RichText>` (sc-6970). `<MarkdownInput value={draft} onChange={setDraft} />` is a multi-line field, a toolbar whose buttons wrap the current SELECTION in markdown markers (bold, italic, code, H2, H3, bullets, numbered list, each toggling back off), and a live `<RichText>` preview of the result underneath — the way to let an app USER write formatted text, instead of a bare `TextInput` plus hand-rolled buttons that can only splice literal tags into the value. Props: `value`, `onChange(next)`, `placeholder`, `previewLabel` (default `Preview`), `showPreview` (default true), `renderImage` (passed through to the preview), `minHeight` (150) and `maxHeight` (280) so a long draft scrolls inside its box, `accessibilityLabel`, `style`, `testID`.
|
|
2462
2505
|
- `parseMarkdown`, `markdownToPlainText`, `parseMarkdownImage`, `formatMarkdownImage`, `stripHtmlToMarkdown`, `MARKDOWN_IMAGE_SIZES` — the same grammar as pure helpers, for content you inspect rather than render: the block list, a marker-free projection (list previews, search, a11y labels), the one-line image form read and written, a legacy-HTML row normalised to markdown on read, and the closed size list (`small` | `medium` | `large` | `full`).
|
package/dist/contract.cjs
CHANGED
|
@@ -723,6 +723,25 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
723
723
|
}),
|
|
724
724
|
targets: Object.freeze({}),
|
|
725
725
|
}),
|
|
726
|
+
// Dialogs: `<Modal>` reads this scope directly, so it names the SDK component
|
|
727
|
+
// it styles and binds no widget field. `<Overlay>` reads the scrim alone.
|
|
728
|
+
modal: Object.freeze({
|
|
729
|
+
label: "Dialogs",
|
|
730
|
+
sdkComponents: Object.freeze(["Modal"]),
|
|
731
|
+
tokens: Object.freeze({
|
|
732
|
+
background: Object.freeze({ type: "color", uiDefault: "colors.surface" }),
|
|
733
|
+
titleColor: Object.freeze({ type: "color" }),
|
|
734
|
+
scrim: Object.freeze({
|
|
735
|
+
type: "color",
|
|
736
|
+
translucent: true,
|
|
737
|
+
note: "also the backdrop behind every <Overlay>",
|
|
738
|
+
}),
|
|
739
|
+
radius: Object.freeze({ type: "size", min: 0, max: 48, uiDefault: "radii.lg" }),
|
|
740
|
+
padding: Object.freeze({ type: "size", min: 0, max: 64, uiDefault: "spacing.lg" }),
|
|
741
|
+
shadow: Object.freeze({ type: "shadow" }),
|
|
742
|
+
}),
|
|
743
|
+
targets: Object.freeze({}),
|
|
744
|
+
}),
|
|
726
745
|
});
|
|
727
746
|
|
|
728
747
|
// sc-4505 — the value types a widget event payload field may declare. Narrower
|
|
@@ -2886,7 +2905,15 @@ const PRIMITIVES = [
|
|
|
2886
2905
|
{
|
|
2887
2906
|
name: "Overlay",
|
|
2888
2907
|
description:
|
|
2889
|
-
'Screen-level overlay. `<Overlay visible={open} onRequestClose={() => setOpen(false)} size="md">…panel…</Overlay>`. THE way to open anything that must cover the SCREEN rather than the widget — a preview (PDF, image, video), a lightbox, a
|
|
2908
|
+
'Screen-level overlay. `<Overlay visible={open} onRequestClose={() => setOpen(false)} size="md">…panel…</Overlay>`. THE way to open anything that must cover the SCREEN rather than the widget and is not a dialog — a preview (PDF, image, video), a lightbox, a full-screen viewer. A dialog (a title, a close button, actions) is `<Modal>`, which is built on this. It renders OUTSIDE the widget layout box on both hosts (react-native-web portals it to the document root; native uses the OS modal), so no `overflow: hidden` card, scroll container or sibling widget can clip or cover it — which absolute positioning inside the widget cannot achieve on either host. Children stay in your own React tree, so state and hooks work normally. Props: `visible` (boolean, required to show), `onRequestClose` (fired by the backdrop press, the Escape key on web, and the Android back button — always wire it or the overlay cannot be closed), `size` ("sm" | "md" | "lg" | "full", default "md"; `full` runs edge to edge for media), `presentation` ("dialog", centred, the default | "sheet", anchored to the bottom edge), `dismissOnBackdropPress` (default true), `role` ("dialog" default | "alertdialog"; web only), `initialFocusRef` (web only), `accessibilityLabel` (names the dialog), `testID`, and `style` for extra panel styles. The scrim, surface, radius, padding and elevation come from the workspace theme (the scrim from Design > Components > Dialogs) — never re-style them. Keep it for screen-level surfaces: a dropdown anchored to its trigger still belongs inside the widget.',
|
|
2909
|
+
rnComponent: null,
|
|
2910
|
+
docsUrl: null,
|
|
2911
|
+
},
|
|
2912
|
+
// The standard dialog, built on `<Overlay>`, so widgets stop hand-building one.
|
|
2913
|
+
{
|
|
2914
|
+
name: "Modal",
|
|
2915
|
+
description:
|
|
2916
|
+
'Standard dialog. `<Modal visible={open} onRequestClose={() => setOpen(false)} title="Edit order" footer={<><Button variant="secondary" title="Cancel" onPress={() => setOpen(false)} /><Button title="Save" onPress={save} /></>}>…body…</Modal>`. THE way to open a dialog, a form in a pop-up, a detail panel or a bottom sheet: it draws the title, the close button, a body that scrolls when it is long, and a footer action row laid out below it, and its background, corner radius, padding, scrim, shadow and title colour come from the theme (Design > Components > Dialogs), so every dialog in the app matches. It is built on `<Overlay>`, so it renders OUTSIDE the widget layout box on both hosts and nothing can clip it. Never hand-build a header, a close button or an action row inside a bare `<Overlay>`; keep `<Overlay>` for surfaces that are not a dialog (a media or document preview, a lightbox). Props: `visible` (boolean), `onRequestClose` (fired by the close button, the backdrop press, the Escape key on web and the Android back button — always wire it), `dismissible` (default true; false hides the close button and stops the backdrop, Escape and Android back from closing it, for a dialog that must stay open while it saves — only while that work runs, and always restore it, or nobody can close the dialog), `title` (string), `footer` (the action buttons, laid out as one right-aligned row that wraps on a phone; omit for no footer), `presentation` ("dialog", centred, the default | "sheet", anchored to the bottom edge; keep a form with text inputs in "dialog" — in the exported app the keyboard handling of a sheet is not yet verified on a device), `size` ("sm" | "md" | "lg" | "full", default "md"), `dismissOnBackdropPress` (default true), `role` ("dialog" default | "alertdialog"; web only), `initialFocusRef` (a ref to the control focused on open; web only), `closeAccessibilityLabel` (default "Close" — pass your translated label), `accessibilityLabel` (names a dialog with no title) and `testID`. With no `onRequestClose` it draws no close button. Components rendered inside it read a theme re-inked for the background the dialog paints, so build the body as its own component. Do not wrap the body in your own `ScrollView` and do not re-style the panel.',
|
|
2890
2917
|
rnComponent: null,
|
|
2891
2918
|
docsUrl: null,
|
|
2892
2919
|
},
|
|
@@ -5554,7 +5581,15 @@ const CONTRACT = deepFreeze({
|
|
|
5554
5581
|
// 1.166.0: additive (sc-10927) — the `Banner` primitive (a themed inline
|
|
5555
5582
|
// error, warning, success or notice) and the `alert` component scope it
|
|
5556
5583
|
// reads.
|
|
5557
|
-
|
|
5584
|
+
// 1.169.0: additive (sc-10924) — the `Modal` primitive: the standard dialog
|
|
5585
|
+
// (title, close button, scrolling body, footer action row; `presentation`
|
|
5586
|
+
// "dialog" | "sheet") built on `Overlay`. New `themeComponents.modal`
|
|
5587
|
+
// scope (background, titleColor, scrim, radius, padding, shadow), read by
|
|
5588
|
+
// `Modal`; `Overlay` reads its `scrim` and gains `presentation`, `role`,
|
|
5589
|
+
// `initialFocusRef`, `backdropAccessible` and `testID`. A scope may now
|
|
5590
|
+
// name `sdkComponents` and bind no widget field; a colour token may be
|
|
5591
|
+
// `translucent` (always painted see-through) and carry a `note`.
|
|
5592
|
+
version: "1.169.0",
|
|
5558
5593
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5559
5594
|
hooks: HOOKS,
|
|
5560
5595
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -723,6 +723,25 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
723
723
|
}),
|
|
724
724
|
targets: Object.freeze({}),
|
|
725
725
|
}),
|
|
726
|
+
// Dialogs: `<Modal>` reads this scope directly, so it names the SDK component
|
|
727
|
+
// it styles and binds no widget field. `<Overlay>` reads the scrim alone.
|
|
728
|
+
modal: Object.freeze({
|
|
729
|
+
label: "Dialogs",
|
|
730
|
+
sdkComponents: Object.freeze(["Modal"]),
|
|
731
|
+
tokens: Object.freeze({
|
|
732
|
+
background: Object.freeze({ type: "color", uiDefault: "colors.surface" }),
|
|
733
|
+
titleColor: Object.freeze({ type: "color" }),
|
|
734
|
+
scrim: Object.freeze({
|
|
735
|
+
type: "color",
|
|
736
|
+
translucent: true,
|
|
737
|
+
note: "also the backdrop behind every <Overlay>",
|
|
738
|
+
}),
|
|
739
|
+
radius: Object.freeze({ type: "size", min: 0, max: 48, uiDefault: "radii.lg" }),
|
|
740
|
+
padding: Object.freeze({ type: "size", min: 0, max: 64, uiDefault: "spacing.lg" }),
|
|
741
|
+
shadow: Object.freeze({ type: "shadow" }),
|
|
742
|
+
}),
|
|
743
|
+
targets: Object.freeze({}),
|
|
744
|
+
}),
|
|
726
745
|
});
|
|
727
746
|
|
|
728
747
|
// sc-4505 — the value types a widget event payload field may declare. Narrower
|
|
@@ -2886,7 +2905,15 @@ const PRIMITIVES = [
|
|
|
2886
2905
|
{
|
|
2887
2906
|
name: "Overlay",
|
|
2888
2907
|
description:
|
|
2889
|
-
'Screen-level overlay. `<Overlay visible={open} onRequestClose={() => setOpen(false)} size="md">…panel…</Overlay>`. THE way to open anything that must cover the SCREEN rather than the widget — a preview (PDF, image, video), a lightbox, a
|
|
2908
|
+
'Screen-level overlay. `<Overlay visible={open} onRequestClose={() => setOpen(false)} size="md">…panel…</Overlay>`. THE way to open anything that must cover the SCREEN rather than the widget and is not a dialog — a preview (PDF, image, video), a lightbox, a full-screen viewer. A dialog (a title, a close button, actions) is `<Modal>`, which is built on this. It renders OUTSIDE the widget layout box on both hosts (react-native-web portals it to the document root; native uses the OS modal), so no `overflow: hidden` card, scroll container or sibling widget can clip or cover it — which absolute positioning inside the widget cannot achieve on either host. Children stay in your own React tree, so state and hooks work normally. Props: `visible` (boolean, required to show), `onRequestClose` (fired by the backdrop press, the Escape key on web, and the Android back button — always wire it or the overlay cannot be closed), `size` ("sm" | "md" | "lg" | "full", default "md"; `full` runs edge to edge for media), `presentation` ("dialog", centred, the default | "sheet", anchored to the bottom edge), `dismissOnBackdropPress` (default true), `role` ("dialog" default | "alertdialog"; web only), `initialFocusRef` (web only), `accessibilityLabel` (names the dialog), `testID`, and `style` for extra panel styles. The scrim, surface, radius, padding and elevation come from the workspace theme (the scrim from Design > Components > Dialogs) — never re-style them. Keep it for screen-level surfaces: a dropdown anchored to its trigger still belongs inside the widget.',
|
|
2909
|
+
rnComponent: null,
|
|
2910
|
+
docsUrl: null,
|
|
2911
|
+
},
|
|
2912
|
+
// The standard dialog, built on `<Overlay>`, so widgets stop hand-building one.
|
|
2913
|
+
{
|
|
2914
|
+
name: "Modal",
|
|
2915
|
+
description:
|
|
2916
|
+
'Standard dialog. `<Modal visible={open} onRequestClose={() => setOpen(false)} title="Edit order" footer={<><Button variant="secondary" title="Cancel" onPress={() => setOpen(false)} /><Button title="Save" onPress={save} /></>}>…body…</Modal>`. THE way to open a dialog, a form in a pop-up, a detail panel or a bottom sheet: it draws the title, the close button, a body that scrolls when it is long, and a footer action row laid out below it, and its background, corner radius, padding, scrim, shadow and title colour come from the theme (Design > Components > Dialogs), so every dialog in the app matches. It is built on `<Overlay>`, so it renders OUTSIDE the widget layout box on both hosts and nothing can clip it. Never hand-build a header, a close button or an action row inside a bare `<Overlay>`; keep `<Overlay>` for surfaces that are not a dialog (a media or document preview, a lightbox). Props: `visible` (boolean), `onRequestClose` (fired by the close button, the backdrop press, the Escape key on web and the Android back button — always wire it), `dismissible` (default true; false hides the close button and stops the backdrop, Escape and Android back from closing it, for a dialog that must stay open while it saves — only while that work runs, and always restore it, or nobody can close the dialog), `title` (string), `footer` (the action buttons, laid out as one right-aligned row that wraps on a phone; omit for no footer), `presentation` ("dialog", centred, the default | "sheet", anchored to the bottom edge; keep a form with text inputs in "dialog" — in the exported app the keyboard handling of a sheet is not yet verified on a device), `size` ("sm" | "md" | "lg" | "full", default "md"), `dismissOnBackdropPress` (default true), `role` ("dialog" default | "alertdialog"; web only), `initialFocusRef` (a ref to the control focused on open; web only), `closeAccessibilityLabel` (default "Close" — pass your translated label), `accessibilityLabel` (names a dialog with no title) and `testID`. With no `onRequestClose` it draws no close button. Components rendered inside it read a theme re-inked for the background the dialog paints, so build the body as its own component. Do not wrap the body in your own `ScrollView` and do not re-style the panel.',
|
|
2890
2917
|
rnComponent: null,
|
|
2891
2918
|
docsUrl: null,
|
|
2892
2919
|
},
|
|
@@ -5550,7 +5577,15 @@ const CONTRACT = deepFreeze({
|
|
|
5550
5577
|
// 1.166.0: additive (sc-10927) — the `Banner` primitive (a themed inline
|
|
5551
5578
|
// error, warning, success or notice) and the `alert` component scope it
|
|
5552
5579
|
// reads.
|
|
5553
|
-
|
|
5580
|
+
// 1.169.0: additive (sc-10924) — the `Modal` primitive: the standard dialog
|
|
5581
|
+
// (title, close button, scrolling body, footer action row; `presentation`
|
|
5582
|
+
// "dialog" | "sheet") built on `Overlay`. New `themeComponents.modal`
|
|
5583
|
+
// scope (background, titleColor, scrim, radius, padding, shadow), read by
|
|
5584
|
+
// `Modal`; `Overlay` reads its `scrim` and gains `presentation`, `role`,
|
|
5585
|
+
// `initialFocusRef`, `backdropAccessible` and `testID`. A scope may now
|
|
5586
|
+
// name `sdkComponents` and bind no widget field; a colour token may be
|
|
5587
|
+
// `translucent` (always painted see-through) and carry a `note`.
|
|
5588
|
+
version: "1.169.0",
|
|
5554
5589
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5555
5590
|
hooks: HOOKS,
|
|
5556
5591
|
primitives: PRIMITIVES,
|
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
|
@@ -45,6 +45,13 @@ export function normaliseThemeComponents(raw: unknown): ThemeComponents;
|
|
|
45
45
|
*/
|
|
46
46
|
export function bannerScopeDefaults(theme: unknown): Record<string, string | number>;
|
|
47
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
|
+
|
|
48
55
|
/** Minimum contrast: `text` for labels (WCAG 1.4.3), `boundary` for edges (1.4.11). */
|
|
49
56
|
export const INK_CONTRAST: Readonly<{ text: number; boundary: number }>;
|
|
50
57
|
|
|
@@ -152,6 +159,45 @@ export interface ToastControllerOptions {
|
|
|
152
159
|
clearTimer?: (handle: unknown) => void;
|
|
153
160
|
}
|
|
154
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
|
+
|
|
155
201
|
export const TOAST_DEFAULTS: { durationMs: number; maxVisible: number };
|
|
156
202
|
|
|
157
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,
|
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
|
|
package/dist/index.js
CHANGED
|
@@ -272,6 +272,8 @@ export { Tabs } from "./tabs.js";
|
|
|
272
272
|
export { Spinner, Skeleton } from "./loading.js";
|
|
273
273
|
// A themed inline message; follows the theme's `alert` scope.
|
|
274
274
|
export { Banner } from "./banner.js";
|
|
275
|
+
// The standard dialog, on `<Overlay>`; follows the theme's `modal` scope.
|
|
276
|
+
export { Modal } from "./modal.js";
|
|
275
277
|
// The semantic text tiers, shared with the built-in widgets.
|
|
276
278
|
export {
|
|
277
279
|
textRoleStyle,
|
package/dist/index.native.js
CHANGED
|
@@ -265,6 +265,8 @@ export { Tabs } from "./tabs.native.js";
|
|
|
265
265
|
export { Spinner, Skeleton } from "./loading.native.js";
|
|
266
266
|
// A themed inline message; follows the theme's `alert` scope.
|
|
267
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";
|
|
268
270
|
// The semantic text tiers, shared with the built-in widgets.
|
|
269
271
|
export {
|
|
270
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
|
);
|