@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/README.md
CHANGED
|
@@ -83,7 +83,60 @@ 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.
|
|
129
|
+
|
|
130
|
+
### What's new in 0.207.0 (contract 1.166.0)
|
|
131
|
+
|
|
132
|
+
**A themed inline message: `Banner`, and the `alert` theme scope.** `<Banner tone="error" title="Could not save" message={err.message} action={{ label: "Retry", onPress: save }} onDismiss={() => setErr(null)} />` draws the box a widget shows when something needs saying in place: a list that failed to load, a form that cannot be submitted yet, a read-only notice. `tone` is `info` (default), `success`, `warning` or `error`; `title`, `icon`, `action` and `onDismiss` are all optional, and a long `message` wraps. `title` and `message` take text, which the banner themes, or an element you built, which it renders as given. An `error` is marked as an alert, and the other tones as a polite status: the web and Android announce them when they appear, while iOS reads the banner in place. Its fill, text colour, border and corner radius come from **Design > Components > Alerts** (`themeConfig.components.alert`: `radius`, `borderWidth`, and `<tone>Background` / `<tone>TextColor` for each tone). Left unset, each tone is a soft tint of the theme's own status colour (`danger`, `success`, `warning`, `info`) with text that clears 4.5:1 on it, the same colours a toast of that kind is accented with, so the two agree. A background set without a text colour takes whichever of dark or light text has the higher contrast on it, measured as the fill appears over the surface when it is translucent.
|
|
133
|
+
|
|
134
|
+
Use a banner for a message that belongs to a place and stays until it is resolved, and `useToast()` for a brief confirmation of something the user just did. The built-in Notifications and User Management widgets now render their error boxes through it.
|
|
135
|
+
|
|
136
|
+
Like the `loading` scope, `alert` styles an SDK component rather than a widget's style field: it declares `sdkComponents: ["Banner"]` and an empty `targets`. `bannerScopeDefaults(theme)` on the `/host` entry returns what each token resolves to while unset.
|
|
137
|
+
|
|
138
|
+
- **`CONTRACT.version` → `1.166.0`** (additive: the `Banner` primitive and the `alert` scope).
|
|
139
|
+
- **Changed:** a pill tab's automatic active-label colour (a custom `activeColor` with no `activeTextColor`) is now chosen by contrast between a fixed dark ink (`#111827`) and white, measured over the surface when the accent is translucent. It was chosen between the theme's own text colour and white, which left the label unreadable on a dark theme. Apps whose theme text colour is not the default will see that label in the fixed ink instead of their text colour; an explicit `activeTextColor` still wins.
|
|
87
140
|
|
|
88
141
|
### What's new in 0.206.0 (contract 1.165.0)
|
|
89
142
|
|
|
@@ -2446,6 +2499,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
2446
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.
|
|
2447
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.
|
|
2448
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`.
|
|
2449
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.
|
|
2450
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`.
|
|
2451
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`).
|
|
@@ -2453,6 +2507,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
2453
2507
|
- `List`, `ListRow`, `ListHeader`, `EmptyState` — the themed list building blocks. `<ListRow title subtitle leading trailing onPress selected divider>` draws one record row from the theme's `list` scope (padding, height, gap, radius, background, divider); `<List>` spaces its rows and hands overrides down; `<ListHeader>` labels a table's columns on the rows' own inset; `<EmptyState icon title message>` is the "nothing here" panel. Never hand-roll a row or a header row from `View` + `Pressable`.
|
|
2454
2508
|
- `Badge`, `Avatar`, `Progress`, `Stat`, `Divider` — the themed status pill, person picture, completion bar or ring, headline figure and rule the built-in widgets of the same names render through. Reach for them inside a record row instead of hand-rolling a pill or a circle from `View`.
|
|
2455
2509
|
- `Card`, `Button`, `TextField` — the themed card surface, button and text field the built-in widgets render through. With no props each takes the app's look from Theme Settings (Cards, Buttons, Muted); never hand-roll one from `View` / `Pressable` / a bare `TextInput`.
|
|
2510
|
+
- `Banner` — the themed inline message. `<Banner tone message>` shows an error, warning, success or notice in place, styled by the theme's `alert` scope; `action` adds one button and `onDismiss` a close control. Use `useToast()` for a brief confirmation instead. Never hand-roll a tinted error box.
|
|
2456
2511
|
- `Accordion`, `AccordionItem` — themed collapsible sections. `<Accordion><AccordionItem title subtitle trailing>…</AccordionItem></Accordion>` draws its surface from the theme's `card` scope and its headers and dividers from the `list` scope; one section open at a time, or `multiple`. Never hand-roll a collapsible from `Pressable` + a chevron.
|
|
2457
2512
|
- `Spinner`, `Skeleton` — the themed loading states. `<Spinner size label>` for a pending action; `<Skeleton shape="text" | "block" | "circle" | "listRow">` (or a group of bones) for content whose layout you know. The spinner wears `colors.loader`, the skeleton follows the theme's `loading` scope; both stop under reduced motion and mark one busy region. Never use `ActivityIndicator` or hand-build placeholder bars.
|
|
2458
2513
|
- `Tabs` — the themed tab strip. `<Tabs items value onChange>` draws a segmented control, pills or an underline as the theme's `tabs` scope says; render the selected panel yourself. Never hand-roll tabs from `Pressable` chips.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// The values `<Banner>` paints with, resolved from the theme's `alert`
|
|
2
|
+
// component scope. Platform-free, like tabs-tokens.js, so both hosts draw one
|
|
3
|
+
// banner from the same numbers and tests can pin them without a renderer.
|
|
4
|
+
|
|
5
|
+
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
6
|
+
import { CONTRACT, deriveAccentTints, mixHex } from "./contract.js";
|
|
7
|
+
import { compositeOver, readableInk } from "./readable-ink.js";
|
|
8
|
+
import { normaliseThemeComponents } from "./theme-components.js";
|
|
9
|
+
import { KIND_COLOR_ROLE } from "./toast-host.js";
|
|
10
|
+
|
|
11
|
+
export const BANNER_TONES = Object.freeze(["info", "success", "warning", "error"]);
|
|
12
|
+
export const DEFAULT_BANNER_TONE = "info";
|
|
13
|
+
export const DEFAULT_BANNER_BORDER_WIDTH = 1;
|
|
14
|
+
|
|
15
|
+
// The glyph a tone carries when the caller names none.
|
|
16
|
+
export const BANNER_TONE_ICONS = Object.freeze({
|
|
17
|
+
info: "info",
|
|
18
|
+
success: "circle-check",
|
|
19
|
+
warning: "triangle-alert",
|
|
20
|
+
error: "circle-alert",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// How far the outline sits from the text colour toward the fill.
|
|
24
|
+
const BORDER_FILL_WEIGHT = 0.7;
|
|
25
|
+
|
|
26
|
+
export function normaliseBannerTone(tone) {
|
|
27
|
+
return BANNER_TONES.includes(tone) ? tone : DEFAULT_BANNER_TONE;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The `alert` scope's token names for one tone's fill and text. */
|
|
31
|
+
export function bannerToneTokenNames(tone) {
|
|
32
|
+
return { background: `${tone}Background`, textColor: `${tone}TextColor` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The soft fill + readable text of a tone, from the status colour a toast of
|
|
36
|
+
// the same kind is accented with.
|
|
37
|
+
function defaultTonePair(colors, tone) {
|
|
38
|
+
const fallback = DEFAULT_THEME_TOKENS.colors;
|
|
39
|
+
const role = KIND_COLOR_ROLE[tone];
|
|
40
|
+
const tints = deriveAccentTints(
|
|
41
|
+
colors[role] || fallback[role],
|
|
42
|
+
colors.surface || fallback.surface,
|
|
43
|
+
colors.onSurface || fallback.onSurface,
|
|
44
|
+
);
|
|
45
|
+
return tints
|
|
46
|
+
? { background: tints.primarySoft, textColor: tints.onPrimarySoft }
|
|
47
|
+
: { background: colors.surfaceMuted, textColor: colors.onSurface };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve one banner: the theme's `alert` scope, then the tone's status
|
|
52
|
+
* colour. A themed fill with no themed text takes whichever of dark or light
|
|
53
|
+
* text has the higher contrast on it, so the message stays legible.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} theme — the resolved workspace theme (`useTheme()` shape).
|
|
56
|
+
* @param {string} tone — `info` | `success` | `warning` | `error`.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveBannerTokens(theme, tone) {
|
|
59
|
+
const base = theme && typeof theme === "object" ? theme : DEFAULT_THEME_TOKENS;
|
|
60
|
+
const fallback = DEFAULT_THEME_TOKENS;
|
|
61
|
+
const colors = base.colors || fallback.colors;
|
|
62
|
+
const radii = base.radii || fallback.radii;
|
|
63
|
+
const safeTone = normaliseBannerTone(tone);
|
|
64
|
+
const scope = normaliseThemeComponents(base.components).alert || {};
|
|
65
|
+
const names = bannerToneTokenNames(safeTone);
|
|
66
|
+
const derived = defaultTonePair(colors, safeTone);
|
|
67
|
+
|
|
68
|
+
const background = scope[names.background] ?? derived.background;
|
|
69
|
+
const textColor =
|
|
70
|
+
scope[names.textColor] ??
|
|
71
|
+
(scope[names.background] === undefined
|
|
72
|
+
? derived.textColor
|
|
73
|
+
: readableInk(background, colors.surface));
|
|
74
|
+
return {
|
|
75
|
+
tone: safeTone,
|
|
76
|
+
background,
|
|
77
|
+
textColor,
|
|
78
|
+
borderColor: mixHex(
|
|
79
|
+
textColor,
|
|
80
|
+
compositeOver(background, colors.surface),
|
|
81
|
+
BORDER_FILL_WEIGHT,
|
|
82
|
+
),
|
|
83
|
+
borderWidth: scope.borderWidth ?? DEFAULT_BANNER_BORDER_WIDTH,
|
|
84
|
+
radius: scope.radius ?? radii.md,
|
|
85
|
+
icon: BANNER_TONE_ICONS[safeTone],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The tone a token belongs to and the resolved value it sets.
|
|
90
|
+
function tokenReading(token) {
|
|
91
|
+
for (const tone of BANNER_TONES) {
|
|
92
|
+
const names = bannerToneTokenNames(tone);
|
|
93
|
+
if (token === names.background) return { tone, value: "background" };
|
|
94
|
+
if (token === names.textColor) return { tone, value: "textColor" };
|
|
95
|
+
}
|
|
96
|
+
return { tone: DEFAULT_BANNER_TONE, value: token };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* What each `alert` token resolves to once THAT token is unset, keyed by token
|
|
101
|
+
* name, so the Studio previews the value each control would replace. The rest
|
|
102
|
+
* of the scope stays: a tone's default text depends on its themed fill.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} theme — the resolved workspace theme (`useTheme()` shape).
|
|
105
|
+
*/
|
|
106
|
+
export function bannerScopeDefaults(theme) {
|
|
107
|
+
const base = theme && typeof theme === "object" ? theme : DEFAULT_THEME_TOKENS;
|
|
108
|
+
const scope = (base.components && base.components.alert) || {};
|
|
109
|
+
const out = {};
|
|
110
|
+
for (const token of Object.keys(CONTRACT.themeComponents.alert.tokens)) {
|
|
111
|
+
const { tone, value } = tokenReading(token);
|
|
112
|
+
const without = { ...base, components: { ...base.components, alert: { ...scope, [token]: undefined } } };
|
|
113
|
+
out[token] = resolveBannerTokens(without, tone)[value];
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// The ONE implementation behind the `<Banner>` SDK primitive. The platform
|
|
2
|
+
// bindings (`banner.js` / `banner.native.js`) differ only in where the
|
|
3
|
+
// primitives come from, so the hosts cannot drift.
|
|
4
|
+
//
|
|
5
|
+
// Widgets each drew their own error box, so the same kind of message looked
|
|
6
|
+
// different in every widget. This one reads the theme's `alert` scope.
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { useHostTheme } from "./hooks.js";
|
|
10
|
+
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
11
|
+
import { Icon } from "./icon.js";
|
|
12
|
+
import { resolveBannerTokens } from "./banner-tokens.js";
|
|
13
|
+
|
|
14
|
+
const ICON_SIZE = 18;
|
|
15
|
+
const DISMISS_ICON_SIZE = 16;
|
|
16
|
+
const LINE_MIN_HEIGHT = 24;
|
|
17
|
+
// A real 36px target: the web host ignores hitSlop, so size carries it.
|
|
18
|
+
const CONTROL_MIN_SIZE = 36;
|
|
19
|
+
const CONTROL_HIT_SLOP = 4;
|
|
20
|
+
|
|
21
|
+
const h = React.createElement;
|
|
22
|
+
|
|
23
|
+
function isText(value) {
|
|
24
|
+
return typeof value === "string" || typeof value === "number";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isShown(value) {
|
|
28
|
+
return value !== undefined && value !== null && value !== false && value !== "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// An error is an alert on both hosts. The other tones are a polite live
|
|
32
|
+
// region on the web and Android; iOS has no equivalent and reads them in place.
|
|
33
|
+
function announcement(tone) {
|
|
34
|
+
return tone === "error"
|
|
35
|
+
? { accessibilityRole: "alert", accessibilityLiveRegion: "assertive" }
|
|
36
|
+
: { role: "status", accessibilityLiveRegion: "polite" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Bind the shared banner to one platform's primitives.
|
|
41
|
+
*
|
|
42
|
+
* @param {{View: any, Text: any, Pressable: any}} rn
|
|
43
|
+
*/
|
|
44
|
+
export function makeBanner(rn) {
|
|
45
|
+
const { View, Text, Pressable } = rn;
|
|
46
|
+
|
|
47
|
+
function BannerAction({ action, ink, typography, fallbackTestID }) {
|
|
48
|
+
return h(
|
|
49
|
+
Pressable,
|
|
50
|
+
{
|
|
51
|
+
onPress: action.onPress,
|
|
52
|
+
accessibilityRole: "button",
|
|
53
|
+
accessibilityLabel: action.accessibilityLabel || action.label,
|
|
54
|
+
hitSlop: CONTROL_HIT_SLOP,
|
|
55
|
+
testID: action.testID || fallbackTestID,
|
|
56
|
+
style: ({ pressed }) => [
|
|
57
|
+
{
|
|
58
|
+
minWidth: CONTROL_MIN_SIZE,
|
|
59
|
+
minHeight: CONTROL_MIN_SIZE,
|
|
60
|
+
paddingHorizontal: 4,
|
|
61
|
+
alignItems: "center",
|
|
62
|
+
justifyContent: "center",
|
|
63
|
+
flexShrink: 0,
|
|
64
|
+
},
|
|
65
|
+
pressed ? { opacity: 0.6 } : null,
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
h(
|
|
69
|
+
Text,
|
|
70
|
+
{
|
|
71
|
+
style: {
|
|
72
|
+
color: ink,
|
|
73
|
+
fontFamily: typography.fontFamily,
|
|
74
|
+
fontSize: typography.sizes.sm,
|
|
75
|
+
fontWeight: "600",
|
|
76
|
+
textDecorationLine: "underline",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
action.label,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function BannerDismiss({ onDismiss, label, ink, testID }) {
|
|
85
|
+
return h(
|
|
86
|
+
Pressable,
|
|
87
|
+
{
|
|
88
|
+
onPress: onDismiss,
|
|
89
|
+
accessibilityRole: "button",
|
|
90
|
+
accessibilityLabel: label,
|
|
91
|
+
hitSlop: CONTROL_HIT_SLOP,
|
|
92
|
+
testID,
|
|
93
|
+
style: ({ pressed }) => [
|
|
94
|
+
{
|
|
95
|
+
minWidth: CONTROL_MIN_SIZE,
|
|
96
|
+
minHeight: CONTROL_MIN_SIZE,
|
|
97
|
+
alignItems: "center",
|
|
98
|
+
justifyContent: "center",
|
|
99
|
+
flexShrink: 0,
|
|
100
|
+
},
|
|
101
|
+
pressed ? { opacity: 0.6 } : null,
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
h(Icon, { name: "x", size: DISMISS_ICON_SIZE, color: ink }),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function Banner({
|
|
109
|
+
tone,
|
|
110
|
+
title,
|
|
111
|
+
message,
|
|
112
|
+
icon,
|
|
113
|
+
action,
|
|
114
|
+
onDismiss,
|
|
115
|
+
dismissAccessibilityLabel = "Dismiss",
|
|
116
|
+
theme: explicitTheme,
|
|
117
|
+
style,
|
|
118
|
+
testID,
|
|
119
|
+
}) {
|
|
120
|
+
const hostTheme = useHostTheme();
|
|
121
|
+
// A Studio preview renders outside a widget context, so it passes its own.
|
|
122
|
+
const theme = explicitTheme || hostTheme || DEFAULT_THEME_TOKENS;
|
|
123
|
+
const t = resolveBannerTokens(theme, tone);
|
|
124
|
+
const spacing = theme.spacing || DEFAULT_THEME_TOKENS.spacing;
|
|
125
|
+
const typography = theme.typography || DEFAULT_THEME_TOKENS.typography;
|
|
126
|
+
const glyph = icon === false || icon === null ? null : icon || t.icon;
|
|
127
|
+
const hasAction =
|
|
128
|
+
action && isShown(action.label) && typeof action.onPress === "function";
|
|
129
|
+
const hasDismiss = typeof onDismiss === "function";
|
|
130
|
+
// Text sits level with a control when the row has one.
|
|
131
|
+
const lineMinHeight = hasAction || hasDismiss ? CONTROL_MIN_SIZE : LINE_MIN_HEIGHT;
|
|
132
|
+
const text = {
|
|
133
|
+
color: t.textColor,
|
|
134
|
+
fontFamily: typography.fontFamily,
|
|
135
|
+
fontSize: typography.sizes.sm,
|
|
136
|
+
};
|
|
137
|
+
// Text is wrapped and themed; an element the caller built renders as given.
|
|
138
|
+
const slot = (value, style) => {
|
|
139
|
+
if (!isShown(value)) return null;
|
|
140
|
+
return isText(value) ? h(Text, { style }, value) : value;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return h(
|
|
144
|
+
View,
|
|
145
|
+
{
|
|
146
|
+
...announcement(t.tone),
|
|
147
|
+
testID,
|
|
148
|
+
style: [
|
|
149
|
+
{
|
|
150
|
+
flexDirection: "row",
|
|
151
|
+
alignItems: "flex-start",
|
|
152
|
+
gap: spacing.sm,
|
|
153
|
+
paddingVertical: spacing.sm,
|
|
154
|
+
paddingHorizontal: spacing.md,
|
|
155
|
+
borderRadius: t.radius,
|
|
156
|
+
borderWidth: t.borderWidth,
|
|
157
|
+
borderColor: t.borderColor,
|
|
158
|
+
backgroundColor: t.background,
|
|
159
|
+
},
|
|
160
|
+
style,
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
glyph
|
|
164
|
+
? h(
|
|
165
|
+
View,
|
|
166
|
+
{ style: { minHeight: lineMinHeight, justifyContent: "center" } },
|
|
167
|
+
h(Icon, { name: glyph, size: ICON_SIZE, color: t.textColor }),
|
|
168
|
+
)
|
|
169
|
+
: null,
|
|
170
|
+
h(
|
|
171
|
+
View,
|
|
172
|
+
// minWidth 0 lets a long message wrap instead of pushing the row wide.
|
|
173
|
+
{ style: { flex: 1, minWidth: 0, minHeight: lineMinHeight, justifyContent: "center", gap: 2 } },
|
|
174
|
+
slot(title, { ...text, fontWeight: "600" }),
|
|
175
|
+
slot(message, text),
|
|
176
|
+
),
|
|
177
|
+
hasAction
|
|
178
|
+
? h(BannerAction, {
|
|
179
|
+
action,
|
|
180
|
+
ink: t.textColor,
|
|
181
|
+
typography,
|
|
182
|
+
fallbackTestID: testID ? `${testID}-action` : undefined,
|
|
183
|
+
})
|
|
184
|
+
: null,
|
|
185
|
+
hasDismiss
|
|
186
|
+
? h(BannerDismiss, {
|
|
187
|
+
onDismiss,
|
|
188
|
+
label: dismissAccessibilityLabel,
|
|
189
|
+
ink: t.textColor,
|
|
190
|
+
testID: testID ? `${testID}-dismiss` : undefined,
|
|
191
|
+
})
|
|
192
|
+
: null,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
Banner.displayName = "Banner";
|
|
197
|
+
return Banner;
|
|
198
|
+
}
|
package/dist/banner.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// `<Banner>` (web). Binds ./banner-view.js to react-native-web; see
|
|
2
|
+
// ./primitives.js for why not the bare specifier.
|
|
3
|
+
|
|
4
|
+
import * as ReactNative from "react-native-web";
|
|
5
|
+
import { makeBanner } from "./banner-view.js";
|
|
6
|
+
|
|
7
|
+
export const Banner = makeBanner(ReactNative);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// `<Banner>` (native). Binds ./banner-view.js to React Native's own
|
|
2
|
+
// primitives; only the import differs from ./banner.js.
|
|
3
|
+
|
|
4
|
+
import { View, Text, Pressable } from "react-native";
|
|
5
|
+
import { makeBanner } from "./banner-view.js";
|
|
6
|
+
|
|
7
|
+
export const Banner = makeBanner({ View, Text, Pressable });
|
package/dist/contract.cjs
CHANGED
|
@@ -704,6 +704,44 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
704
704
|
}),
|
|
705
705
|
targets: Object.freeze({}),
|
|
706
706
|
}),
|
|
707
|
+
// Inline messages: `<Banner>` reads this scope directly, so it names the
|
|
708
|
+
// SDK component it styles and binds no widget field.
|
|
709
|
+
alert: Object.freeze({
|
|
710
|
+
label: "Alerts",
|
|
711
|
+
sdkComponents: Object.freeze(["Banner"]),
|
|
712
|
+
tokens: Object.freeze({
|
|
713
|
+
radius: Object.freeze({ type: "size", min: 0, max: 32, uiDefault: "radii.md" }),
|
|
714
|
+
borderWidth: Object.freeze({ type: "size", min: 0, max: 4 }),
|
|
715
|
+
infoBackground: Object.freeze({ type: "color" }),
|
|
716
|
+
infoTextColor: Object.freeze({ type: "color" }),
|
|
717
|
+
successBackground: Object.freeze({ type: "color" }),
|
|
718
|
+
successTextColor: Object.freeze({ type: "color" }),
|
|
719
|
+
warningBackground: Object.freeze({ type: "color" }),
|
|
720
|
+
warningTextColor: Object.freeze({ type: "color" }),
|
|
721
|
+
errorBackground: Object.freeze({ type: "color" }),
|
|
722
|
+
errorTextColor: Object.freeze({ type: "color" }),
|
|
723
|
+
}),
|
|
724
|
+
targets: Object.freeze({}),
|
|
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
|
+
}),
|
|
707
745
|
});
|
|
708
746
|
|
|
709
747
|
// sc-4505 — the value types a widget event payload field may declare. Narrower
|
|
@@ -2867,7 +2905,15 @@ const PRIMITIVES = [
|
|
|
2867
2905
|
{
|
|
2868
2906
|
name: "Overlay",
|
|
2869
2907
|
description:
|
|
2870
|
-
'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.',
|
|
2871
2917
|
rnComponent: null,
|
|
2872
2918
|
docsUrl: null,
|
|
2873
2919
|
},
|
|
@@ -2962,6 +3008,13 @@ const PRIMITIVES = [
|
|
|
2962
3008
|
rnComponent: null,
|
|
2963
3009
|
docsUrl: null,
|
|
2964
3010
|
},
|
|
3011
|
+
{
|
|
3012
|
+
name: "Banner",
|
|
3013
|
+
description:
|
|
3014
|
+
'Themed inline message. `<Banner tone="error" title="Could not save" message={err.message} action={{ label: "Retry", onPress: save }} onDismiss={() => setErr(null)} />`. THE way to show an error, warning, success or notice that belongs to a place in the widget and stays until it is resolved: a list that failed to load, a form the user cannot submit yet, a "read-only" or "offline" notice. Render it where the problem is — above the list or form it concerns. Use `useToast()` instead for a brief confirmation of something the user just did ("Saved", "Deleted") that needs no decision and may disappear on its own; never both for one event. Its fill, text colour, border and corner radius come from the app\'s theme per tone (Design > Components > Alerts), so never hand-roll a tinted box from `View` + a border + a hex. Props: `tone` ("info" default | "success" | "warning" | "error"), `title` (optional, one short line) and `message` (wraps — never truncate it): text, or an element you built when part of it needs its own styling, `icon` (a lucide name replacing the tone\'s own glyph, or `false` for none), `action` (`{ label, onPress, accessibilityLabel?, testID? }`, ONE action such as Retry), `onDismiss` (pass it to show a close control; you hide the banner in the handler), `dismissAccessibilityLabel` (default "Dismiss" — pass a translated one), `style` (spacing around it), `testID`. An `error` is marked as an alert, and the other tones as a polite status: the web and Android announce them when they appear, while iOS reads the banner in place.',
|
|
3015
|
+
rnComponent: null,
|
|
3016
|
+
docsUrl: null,
|
|
3017
|
+
},
|
|
2965
3018
|
{
|
|
2966
3019
|
name: "Accordion",
|
|
2967
3020
|
description:
|
|
@@ -5525,7 +5578,18 @@ const CONTRACT = deepFreeze({
|
|
|
5525
5578
|
// and `useOnlineVisibility()` read who of a record's members is online
|
|
5526
5579
|
// and the viewer's own switch, through the new `ctx.directory.presence`.
|
|
5527
5580
|
// A `datastoreTemplate` table may declare `presence: true`.
|
|
5528
|
-
|
|
5581
|
+
// 1.166.0: additive (sc-10927) — the `Banner` primitive (a themed inline
|
|
5582
|
+
// error, warning, success or notice) and the `alert` component scope it
|
|
5583
|
+
// reads.
|
|
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",
|
|
5529
5593
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5530
5594
|
hooks: HOOKS,
|
|
5531
5595
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -704,6 +704,44 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
704
704
|
}),
|
|
705
705
|
targets: Object.freeze({}),
|
|
706
706
|
}),
|
|
707
|
+
// Inline messages: `<Banner>` reads this scope directly, so it names the
|
|
708
|
+
// SDK component it styles and binds no widget field.
|
|
709
|
+
alert: Object.freeze({
|
|
710
|
+
label: "Alerts",
|
|
711
|
+
sdkComponents: Object.freeze(["Banner"]),
|
|
712
|
+
tokens: Object.freeze({
|
|
713
|
+
radius: Object.freeze({ type: "size", min: 0, max: 32, uiDefault: "radii.md" }),
|
|
714
|
+
borderWidth: Object.freeze({ type: "size", min: 0, max: 4 }),
|
|
715
|
+
infoBackground: Object.freeze({ type: "color" }),
|
|
716
|
+
infoTextColor: Object.freeze({ type: "color" }),
|
|
717
|
+
successBackground: Object.freeze({ type: "color" }),
|
|
718
|
+
successTextColor: Object.freeze({ type: "color" }),
|
|
719
|
+
warningBackground: Object.freeze({ type: "color" }),
|
|
720
|
+
warningTextColor: Object.freeze({ type: "color" }),
|
|
721
|
+
errorBackground: Object.freeze({ type: "color" }),
|
|
722
|
+
errorTextColor: Object.freeze({ type: "color" }),
|
|
723
|
+
}),
|
|
724
|
+
targets: Object.freeze({}),
|
|
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
|
+
}),
|
|
707
745
|
});
|
|
708
746
|
|
|
709
747
|
// sc-4505 — the value types a widget event payload field may declare. Narrower
|
|
@@ -2867,7 +2905,15 @@ const PRIMITIVES = [
|
|
|
2867
2905
|
{
|
|
2868
2906
|
name: "Overlay",
|
|
2869
2907
|
description:
|
|
2870
|
-
'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.',
|
|
2871
2917
|
rnComponent: null,
|
|
2872
2918
|
docsUrl: null,
|
|
2873
2919
|
},
|
|
@@ -2962,6 +3008,13 @@ const PRIMITIVES = [
|
|
|
2962
3008
|
rnComponent: null,
|
|
2963
3009
|
docsUrl: null,
|
|
2964
3010
|
},
|
|
3011
|
+
{
|
|
3012
|
+
name: "Banner",
|
|
3013
|
+
description:
|
|
3014
|
+
'Themed inline message. `<Banner tone="error" title="Could not save" message={err.message} action={{ label: "Retry", onPress: save }} onDismiss={() => setErr(null)} />`. THE way to show an error, warning, success or notice that belongs to a place in the widget and stays until it is resolved: a list that failed to load, a form the user cannot submit yet, a "read-only" or "offline" notice. Render it where the problem is — above the list or form it concerns. Use `useToast()` instead for a brief confirmation of something the user just did ("Saved", "Deleted") that needs no decision and may disappear on its own; never both for one event. Its fill, text colour, border and corner radius come from the app\'s theme per tone (Design > Components > Alerts), so never hand-roll a tinted box from `View` + a border + a hex. Props: `tone` ("info" default | "success" | "warning" | "error"), `title` (optional, one short line) and `message` (wraps — never truncate it): text, or an element you built when part of it needs its own styling, `icon` (a lucide name replacing the tone\'s own glyph, or `false` for none), `action` (`{ label, onPress, accessibilityLabel?, testID? }`, ONE action such as Retry), `onDismiss` (pass it to show a close control; you hide the banner in the handler), `dismissAccessibilityLabel` (default "Dismiss" — pass a translated one), `style` (spacing around it), `testID`. An `error` is marked as an alert, and the other tones as a polite status: the web and Android announce them when they appear, while iOS reads the banner in place.',
|
|
3015
|
+
rnComponent: null,
|
|
3016
|
+
docsUrl: null,
|
|
3017
|
+
},
|
|
2965
3018
|
{
|
|
2966
3019
|
name: "Accordion",
|
|
2967
3020
|
description:
|
|
@@ -5521,7 +5574,18 @@ const CONTRACT = deepFreeze({
|
|
|
5521
5574
|
// and `useOnlineVisibility()` read who of a record's members is online
|
|
5522
5575
|
// and the viewer's own switch, through the new `ctx.directory.presence`.
|
|
5523
5576
|
// A `datastoreTemplate` table may declare `presence: true`.
|
|
5524
|
-
|
|
5577
|
+
// 1.166.0: additive (sc-10927) — the `Banner` primitive (a themed inline
|
|
5578
|
+
// error, warning, success or notice) and the `alert` component scope it
|
|
5579
|
+
// reads.
|
|
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",
|
|
5525
5589
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5526
5590
|
hooks: HOOKS,
|
|
5527
5591
|
primitives: PRIMITIVES,
|