@colixsystems/widget-sdk 0.206.0 → 0.207.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 +13 -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 +30 -1
- package/dist/contract.js +30 -1
- package/dist/host.d.ts +6 -0
- package/dist/host.js +4 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/readable-ink.js +28 -0
- package/dist/tabs-tokens.js +3 -9
- package/dist/toast-host.js +1 -1
- package/package.json +1 -1
- package/src/contract.js +30 -1
package/README.md
CHANGED
|
@@ -83,7 +83,18 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
83
83
|
|
|
84
84
|
## Status
|
|
85
85
|
|
|
86
|
-
`v0.
|
|
86
|
+
`v0.207.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.207.0 (contract 1.166.0)
|
|
89
|
+
|
|
90
|
+
**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.
|
|
91
|
+
|
|
92
|
+
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.
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
- **`CONTRACT.version` → `1.166.0`** (additive: the `Banner` primitive and the `alert` scope).
|
|
97
|
+
- **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
98
|
|
|
88
99
|
### What's new in 0.206.0 (contract 1.165.0)
|
|
89
100
|
|
|
@@ -2453,6 +2464,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
2453
2464
|
- `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
2465
|
- `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
2466
|
- `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`.
|
|
2467
|
+
- `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
2468
|
- `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
2469
|
- `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
2470
|
- `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,25 @@ 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
|
+
}),
|
|
707
726
|
});
|
|
708
727
|
|
|
709
728
|
// sc-4505 — the value types a widget event payload field may declare. Narrower
|
|
@@ -2962,6 +2981,13 @@ const PRIMITIVES = [
|
|
|
2962
2981
|
rnComponent: null,
|
|
2963
2982
|
docsUrl: null,
|
|
2964
2983
|
},
|
|
2984
|
+
{
|
|
2985
|
+
name: "Banner",
|
|
2986
|
+
description:
|
|
2987
|
+
'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.',
|
|
2988
|
+
rnComponent: null,
|
|
2989
|
+
docsUrl: null,
|
|
2990
|
+
},
|
|
2965
2991
|
{
|
|
2966
2992
|
name: "Accordion",
|
|
2967
2993
|
description:
|
|
@@ -5525,7 +5551,10 @@ const CONTRACT = deepFreeze({
|
|
|
5525
5551
|
// and `useOnlineVisibility()` read who of a record's members is online
|
|
5526
5552
|
// and the viewer's own switch, through the new `ctx.directory.presence`.
|
|
5527
5553
|
// A `datastoreTemplate` table may declare `presence: true`.
|
|
5528
|
-
|
|
5554
|
+
// 1.166.0: additive (sc-10927) — the `Banner` primitive (a themed inline
|
|
5555
|
+
// error, warning, success or notice) and the `alert` component scope it
|
|
5556
|
+
// reads.
|
|
5557
|
+
version: "1.166.0",
|
|
5529
5558
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5530
5559
|
hooks: HOOKS,
|
|
5531
5560
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -704,6 +704,25 @@ 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
|
+
}),
|
|
707
726
|
});
|
|
708
727
|
|
|
709
728
|
// sc-4505 — the value types a widget event payload field may declare. Narrower
|
|
@@ -2962,6 +2981,13 @@ const PRIMITIVES = [
|
|
|
2962
2981
|
rnComponent: null,
|
|
2963
2982
|
docsUrl: null,
|
|
2964
2983
|
},
|
|
2984
|
+
{
|
|
2985
|
+
name: "Banner",
|
|
2986
|
+
description:
|
|
2987
|
+
'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.',
|
|
2988
|
+
rnComponent: null,
|
|
2989
|
+
docsUrl: null,
|
|
2990
|
+
},
|
|
2965
2991
|
{
|
|
2966
2992
|
name: "Accordion",
|
|
2967
2993
|
description:
|
|
@@ -5521,7 +5547,10 @@ const CONTRACT = deepFreeze({
|
|
|
5521
5547
|
// and `useOnlineVisibility()` read who of a record's members is online
|
|
5522
5548
|
// and the viewer's own switch, through the new `ctx.directory.presence`.
|
|
5523
5549
|
// A `datastoreTemplate` table may declare `presence: true`.
|
|
5524
|
-
|
|
5550
|
+
// 1.166.0: additive (sc-10927) — the `Banner` primitive (a themed inline
|
|
5551
|
+
// error, warning, success or notice) and the `alert` component scope it
|
|
5552
|
+
// reads.
|
|
5553
|
+
version: "1.166.0",
|
|
5525
5554
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5526
5555
|
hooks: HOOKS,
|
|
5527
5556
|
primitives: PRIMITIVES,
|
package/dist/host.d.ts
CHANGED
|
@@ -39,6 +39,12 @@ 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
|
+
|
|
42
48
|
/** Minimum contrast: `text` for labels (WCAG 1.4.3), `boundary` for edges (1.4.11). */
|
|
43
49
|
export const INK_CONTRAST: Readonly<{ text: number; boundary: number }>;
|
|
44
50
|
|
package/dist/host.js
CHANGED
|
@@ -55,6 +55,10 @@ export {
|
|
|
55
55
|
TOAST_DEFAULTS,
|
|
56
56
|
} from "./toast-host.js";
|
|
57
57
|
|
|
58
|
+
// What each `alert` scope token resolves to while unset, for the Studio's
|
|
59
|
+
// Design page to preview beside its controls.
|
|
60
|
+
export { bannerScopeDefaults } from "./banner-tokens.js";
|
|
61
|
+
|
|
58
62
|
// REQ-NAV-STRUCTURE: the navigation resolver both hosts draw their chrome from.
|
|
59
63
|
// `normaliseNavigation` turns a stored `theme_config.navigation` block into the
|
|
60
64
|
// resolved `{ menuType }` — always `sidebar` since sc-7352 retired the other
|
package/dist/index.d.ts
CHANGED
|
@@ -3571,6 +3571,40 @@ export const Tabs: (props: {
|
|
|
3571
3571
|
testID?: string;
|
|
3572
3572
|
}) => any;
|
|
3573
3573
|
|
|
3574
|
+
export type BannerTone = "info" | "success" | "warning" | "error";
|
|
3575
|
+
|
|
3576
|
+
/**
|
|
3577
|
+
* Themed inline message: an error, warning, success or notice that stays in
|
|
3578
|
+
* place until it is resolved. Its fill, text colour, border and corners come
|
|
3579
|
+
* from the theme's `alert` scope, so every banner in the app matches. An
|
|
3580
|
+
* `error` is marked as an alert and the other tones as a polite status: the
|
|
3581
|
+
* web and Android announce them when they appear, iOS reads them in place.
|
|
3582
|
+
* For a brief confirmation that may disappear on its own, use `useToast()`.
|
|
3583
|
+
*
|
|
3584
|
+
* @example
|
|
3585
|
+
* <Banner tone="error" message={error.message} action={{ label: "Retry", onPress: refetch }} />
|
|
3586
|
+
*/
|
|
3587
|
+
export const Banner: (props: {
|
|
3588
|
+
/** Default "info". */
|
|
3589
|
+
tone?: BannerTone;
|
|
3590
|
+
/** Text is themed; an element renders as given. */
|
|
3591
|
+
title?: ReactNode;
|
|
3592
|
+
/** Text wraps onto as many lines as it needs; an element renders as given. */
|
|
3593
|
+
message?: ReactNode;
|
|
3594
|
+
/** A lucide name replacing the tone's glyph, or `false` for none. */
|
|
3595
|
+
icon?: string | false | null;
|
|
3596
|
+
action?: { label: string; onPress: () => void; accessibilityLabel?: string; testID?: string };
|
|
3597
|
+
/** Shows a close control; hide the banner in the handler. */
|
|
3598
|
+
onDismiss?: () => void;
|
|
3599
|
+
/** Default "Dismiss". */
|
|
3600
|
+
dismissAccessibilityLabel?: string;
|
|
3601
|
+
/** For hosts rendering outside a widget context; widgets omit it. */
|
|
3602
|
+
theme?: any;
|
|
3603
|
+
style?: any;
|
|
3604
|
+
/** The action and close control default to `<testID>-action` / `<testID>-dismiss`. */
|
|
3605
|
+
testID?: string;
|
|
3606
|
+
}) => any;
|
|
3607
|
+
|
|
3574
3608
|
/**
|
|
3575
3609
|
* Themed collapsible sections. The surface comes from the theme's `card`
|
|
3576
3610
|
* scope and the headers and dividers from its `list` scope, so every
|
package/dist/index.js
CHANGED
|
@@ -270,6 +270,8 @@ 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";
|
|
273
275
|
// The semantic text tiers, shared with the built-in widgets.
|
|
274
276
|
export {
|
|
275
277
|
textRoleStyle,
|
package/dist/index.native.js
CHANGED
|
@@ -263,6 +263,8 @@ 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";
|
|
266
268
|
// The semantic text tiers, shared with the built-in widgets.
|
|
267
269
|
export {
|
|
268
270
|
textRoleStyle,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The label colour for a fill an author chose. Picked by contrast between a
|
|
2
|
+
// FIXED dark and a fixed light: the theme's own text colour is light on a
|
|
3
|
+
// dark theme, so it cannot stand in for "the dark option".
|
|
4
|
+
|
|
5
|
+
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
6
|
+
import { contrastRatio, hexAlphaPercent, isHexColor, mixHex } from "./contract.js";
|
|
7
|
+
|
|
8
|
+
export const INK_DARK = DEFAULT_THEME_TOKENS.colors.onSurface;
|
|
9
|
+
export const INK_LIGHT = "#ffffff";
|
|
10
|
+
|
|
11
|
+
/** A fill as the opaque colour it shows once painted over `surface`. */
|
|
12
|
+
export function compositeOver(fill, surface) {
|
|
13
|
+
if (!isHexColor(fill)) return fill;
|
|
14
|
+
const backdrop = isHexColor(surface) ? surface : DEFAULT_THEME_TOKENS.colors.surface;
|
|
15
|
+
return mixHex(backdrop, fill, hexAlphaPercent(fill) / 100);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whichever of the dark and light ink has the higher contrast on `fill`. A
|
|
20
|
+
* translucent fill is measured as it appears over `surface`.
|
|
21
|
+
*/
|
|
22
|
+
export function readableInk(fill, surface) {
|
|
23
|
+
if (!isHexColor(fill)) return INK_DARK;
|
|
24
|
+
const painted = compositeOver(fill, surface);
|
|
25
|
+
return contrastRatio(painted, INK_DARK) >= contrastRatio(painted, INK_LIGHT)
|
|
26
|
+
? INK_DARK
|
|
27
|
+
: INK_LIGHT;
|
|
28
|
+
}
|
package/dist/tabs-tokens.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// the same numbers and tests can pin them without a renderer.
|
|
4
4
|
|
|
5
5
|
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
6
|
-
import { CONTRACT
|
|
6
|
+
import { CONTRACT } from "./contract.js";
|
|
7
|
+
import { readableInk } from "./readable-ink.js";
|
|
7
8
|
|
|
8
9
|
export const TAB_STYLES = CONTRACT.themeComponentTabStyles;
|
|
9
10
|
|
|
@@ -56,7 +57,7 @@ export function resolveTabsTokens(theme, overrides) {
|
|
|
56
57
|
pick(overrides, scope, "activeTextColor") ??
|
|
57
58
|
(style === "pill"
|
|
58
59
|
? ownAccent
|
|
59
|
-
?
|
|
60
|
+
? readableInk(ownAccent, colors.surface)
|
|
60
61
|
: colors.onPrimary
|
|
61
62
|
: activeColor),
|
|
62
63
|
textColor: pick(overrides, scope, "textColor") ?? colors.onSurfaceMuted,
|
|
@@ -66,10 +67,3 @@ export function resolveTabsTokens(theme, overrides) {
|
|
|
66
67
|
rule: colors.border,
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
// A chosen accent carries the label that reads best on it, dark or light.
|
|
71
|
-
function readableOn(fill, colors) {
|
|
72
|
-
const dark = colors.onSurface;
|
|
73
|
-
const light = "#ffffff";
|
|
74
|
-
return contrastRatio(fill, dark) >= contrastRatio(fill, light) ? dark : light;
|
|
75
|
-
}
|
package/dist/toast-host.js
CHANGED
|
@@ -18,7 +18,7 @@ export const TOAST_DEFAULTS = Object.freeze({
|
|
|
18
18
|
const KINDS = Object.freeze(["success", "error", "warning", "info"]);
|
|
19
19
|
|
|
20
20
|
// `error` is the widget-facing kind; `danger` is the theme's colour role.
|
|
21
|
-
const KIND_COLOR_ROLE = Object.freeze({
|
|
21
|
+
export const KIND_COLOR_ROLE = Object.freeze({
|
|
22
22
|
success: "success",
|
|
23
23
|
error: "danger",
|
|
24
24
|
warning: "warning",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.207.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|
package/src/contract.js
CHANGED
|
@@ -704,6 +704,25 @@ 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
|
+
}),
|
|
707
726
|
});
|
|
708
727
|
|
|
709
728
|
// sc-4505 — the value types a widget event payload field may declare. Narrower
|
|
@@ -2962,6 +2981,13 @@ const PRIMITIVES = [
|
|
|
2962
2981
|
rnComponent: null,
|
|
2963
2982
|
docsUrl: null,
|
|
2964
2983
|
},
|
|
2984
|
+
{
|
|
2985
|
+
name: "Banner",
|
|
2986
|
+
description:
|
|
2987
|
+
'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.',
|
|
2988
|
+
rnComponent: null,
|
|
2989
|
+
docsUrl: null,
|
|
2990
|
+
},
|
|
2965
2991
|
{
|
|
2966
2992
|
name: "Accordion",
|
|
2967
2993
|
description:
|
|
@@ -5521,7 +5547,10 @@ const CONTRACT = deepFreeze({
|
|
|
5521
5547
|
// and `useOnlineVisibility()` read who of a record's members is online
|
|
5522
5548
|
// and the viewer's own switch, through the new `ctx.directory.presence`.
|
|
5523
5549
|
// A `datastoreTemplate` table may declare `presence: true`.
|
|
5524
|
-
|
|
5550
|
+
// 1.166.0: additive (sc-10927) — the `Banner` primitive (a themed inline
|
|
5551
|
+
// error, warning, success or notice) and the `alert` component scope it
|
|
5552
|
+
// reads.
|
|
5553
|
+
version: "1.166.0",
|
|
5525
5554
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5526
5555
|
hooks: HOOKS,
|
|
5527
5556
|
primitives: PRIMITIVES,
|