@colixsystems/widget-sdk 0.291.0 → 0.293.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 +10 -2
- package/dist/contract.cjs +86 -5
- package/dist/contract.js +86 -5
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/package.json +1 -1
- package/src/contract.js +86 -5
package/README.md
CHANGED
|
@@ -90,7 +90,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
90
90
|
|
|
91
91
|
## Status
|
|
92
92
|
|
|
93
|
-
`v0.
|
|
93
|
+
`v0.293.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**.
|
|
94
|
+
|
|
95
|
+
### What's new in 0.293.0 (contract 1.240.0)
|
|
96
|
+
|
|
97
|
+
**Status colours follow the app's look.** `theme.colors.success`, `danger`, `warning` and `info` used to be the same four colours in every app, so a warm editorial palette showed a saturated green "Completed" chip and a bright blue "Booked" one. Each tone now keeps its hue family (green, red, amber, blue) but takes the app's accent saturation and lightness, stepped until it reads as text on the surface — and a dark palette derives its own lighter set. `<Badge>`, `<Banner>`, `<Progress>`, `<Stat>` deltas and toasts pick this up with no change on your side, and a badge's soft fill and label stay WCAG AA. A colour the workspace sets in the Alerts section of Design still wins. The derivation is `deriveStatusColors(primary, surface, onSurface)`, a host-side helper like `deriveAccentTints`; the four default literals are that derivation on the default palette, so an untouched theme renders as before.
|
|
98
|
+
|
|
99
|
+
### What's new in 0.292.0 (contract 1.239.0)
|
|
100
|
+
|
|
101
|
+
**An Action a widget runs must be done 250 ms after the press.** The time counts from when the press reaches the server, so waiting for a free sandbox and setting the run up spend from it too. A run started by `useAction()` (or a Button's Run action) that is still going then is stopped, and `run()` rejects with `ACTION_TIMEOUT` (504). Its outcome is unknown: open requests and connector calls are cut and no new work starts, but a write that had already finished stays, and a notification already sent can't be recalled. So an Action the app waits on must do its writes as one call (a bulk write) or be safe to repeat, and never tell the user it did nothing. Move slow or multi-step work to an Action triggered by a record the widget writes. Scheduled, record-triggered and Run now runs keep their own limits.
|
|
94
102
|
|
|
95
103
|
### What's new in 0.291.0 (contract 1.238.0)
|
|
96
104
|
|
|
@@ -299,7 +307,7 @@ const { available, run, running, result, error } = useAction(props.actionId);
|
|
|
299
307
|
|
|
300
308
|
**Bind it with an `actionRef` property.** Declare `{ type: "actionRef" }` in your `propertySchema` and the Studio renders an Action picker that warns when the chosen Action lacks the app trigger. The value is a bare Action id, so Copy Workspace remaps it like `pageRef`.
|
|
301
309
|
|
|
302
|
-
When nothing is bound, or the host brokers no actions client, `available` is `false` and `run()` rejects `UNAVAILABLE`: render a visible "pick an action" state, never a blank box. A changed binding clears `result` and `error`, and a run that settles after the binding changed or the widget unmounted writes nothing. When runs overlap, `result` and `error` follow the newest one. Branch on `error.code`: `ACTION_NOT_APP_INVOKABLE` and `ACTION_NOT_FOUND` will not clear on a retry, `RATE_LIMITED` will, `INVALID_ACTION_INPUT` means the input was refused, `AUTH_REQUIRED`, `TOKEN_EXPIRED` and `TOKEN_INVALID` mean the app user must sign in,
|
|
310
|
+
When nothing is bound, or the host brokers no actions client, `available` is `false` and `run()` rejects `UNAVAILABLE`: render a visible "pick an action" state, never a blank box. A changed binding clears `result` and `error`, and a run that settles after the binding changed or the widget unmounted writes nothing. When runs overlap, `result` and `error` follow the newest one. Branch on `error.code`: `ACTION_NOT_APP_INVOKABLE` and `ACTION_NOT_FOUND` will not clear on a retry, `RATE_LIMITED` will, `INVALID_ACTION_INPUT` means the input was refused, `AUTH_REQUIRED`, `TOKEN_EXPIRED` and `TOKEN_INVALID` mean the app user must sign in, `ACTION_FAILED` means the Action ran and failed, and `ACTION_TIMEOUT` means it was still running 250 ms after the press and was stopped, so its outcome is unknown: some of its writes may have happened. Never show an app user the raw message. A text answer from a bot is often markdown, so render it with `<RichText>`.
|
|
303
311
|
|
|
304
312
|
The linter rejects `useAction()` unless your manifest declares `actions.run:*` in `requestedScopes`.
|
|
305
313
|
|
package/dist/contract.cjs
CHANGED
|
@@ -87,10 +87,13 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
|
|
|
87
87
|
// background — so a dark theme gets a visible spinner with no new maths.
|
|
88
88
|
loader: "#475569",
|
|
89
89
|
border: "#e2e8f0",
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
// The four status tones are re-derived per tenant by `deriveStatusColors`
|
|
91
|
+
// (their hue families on the accent's saturation and lightness); these
|
|
92
|
+
// literals are that derivation applied to the default palette.
|
|
93
|
+
danger: "#e90b0b",
|
|
94
|
+
success: "#067e56",
|
|
95
|
+
warning: "#9b6507",
|
|
96
|
+
info: "#0877af",
|
|
94
97
|
}),
|
|
95
98
|
elevation: ELEVATION,
|
|
96
99
|
interaction: INTERACTION,
|
|
@@ -2285,6 +2288,12 @@ const HOOKS = [
|
|
|
2285
2288
|
"INVALID_ACTION_INPUT (400) — the input was refused; " +
|
|
2286
2289
|
"ACTION_NOT_APP_INVOKABLE (403) and ACTION_NOT_FOUND (404) will NOT " +
|
|
2287
2290
|
"clear on a retry, so hide the affordance; RATE_LIMITED (429) will; " +
|
|
2291
|
+
"ACTION_TIMEOUT (504) — the run was still going 250 ms after the press " +
|
|
2292
|
+
"and was stopped, so its OUTCOME IS UNKNOWN: some of its writes may " +
|
|
2293
|
+
"have happened. Never retry it blindly and never tell the user it did " +
|
|
2294
|
+
"nothing; an Action a widget runs must do its writes as one call or be " +
|
|
2295
|
+
"safe to repeat, and slow or multi-step work belongs in a " +
|
|
2296
|
+
"record-triggered Action; " +
|
|
2288
2297
|
"ACTION_FAILED — the Action ran and failed, or no answer arrived. NEVER " +
|
|
2289
2298
|
"show an app user a raw error. A text answer from a bot is often " +
|
|
2290
2299
|
"MARKDOWN, so render it with <RichText value={result} />, not <Text>. " +
|
|
@@ -6802,10 +6811,17 @@ const CONTRACT = deepFreeze({
|
|
|
6802
6811
|
// PaymentError codes.
|
|
6803
6812
|
// 1.237.0: widening — `manifest.translations` may declare up to 500 keys
|
|
6804
6813
|
// (was 200).
|
|
6814
|
+
// 1.239.0: additive — `deriveStatusColors`; the four status tokens follow the
|
|
6815
|
+
// palette, and their default literals are that derivation on the default.
|
|
6805
6816
|
// 1.238.0: additive (sc-15659) — `useConnector(connectorRef)` + the
|
|
6806
6817
|
// `connectors` host client + the `connectorRef` propertySchema type + the
|
|
6807
6818
|
// `connectors.call:*` scope. A widget calls a REST connector's listed operations.
|
|
6808
|
-
|
|
6819
|
+
// 1.240.0: additive — `deriveStatusColors`; the four status tokens follow the
|
|
6820
|
+
// palette, and their default literals are that derivation on the default.
|
|
6821
|
+
// 1.239.0: additive (sc-15658) — an app-triggered run must finish within 250 ms
|
|
6822
|
+
// of the press; `useAction` errors carry the new code ACTION_TIMEOUT
|
|
6823
|
+
// (504), whose outcome is unknown.
|
|
6824
|
+
version: "1.240.0",
|
|
6809
6825
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6810
6826
|
hooks: HOOKS,
|
|
6811
6827
|
primitives: PRIMITIVES,
|
|
@@ -7042,6 +7058,70 @@ function deriveAccentTints(primary, surface, onSurface) {
|
|
|
7042
7058
|
};
|
|
7043
7059
|
}
|
|
7044
7060
|
|
|
7061
|
+
// The hue family each status tone keeps whatever the palette.
|
|
7062
|
+
const STATUS_HUES = Object.freeze({ success: 160, danger: 0, warning: 38, info: 200 });
|
|
7063
|
+
// Below this saturation a tone loses its hue, so a grey accent still reads as a colour.
|
|
7064
|
+
const STATUS_MIN_SATURATION = 0.35;
|
|
7065
|
+
// The lightness band a tone starts in, so a near-black or pastel accent keeps a visible hue.
|
|
7066
|
+
const STATUS_LIGHTNESS = Object.freeze({ min: 0.28, max: 0.62 });
|
|
7067
|
+
const STATUS_LIGHTNESS_STEP = 0.02;
|
|
7068
|
+
|
|
7069
|
+
/** `[hue 0-360, saturation 0-1, lightness 0-1]` of a hex colour. */
|
|
7070
|
+
function hexToHsl(hex) {
|
|
7071
|
+
const [r, g, b] = hexChannels(hex).map((v) => v / 255);
|
|
7072
|
+
const max = Math.max(r, g, b);
|
|
7073
|
+
const min = Math.min(r, g, b);
|
|
7074
|
+
const l = (max + min) / 2;
|
|
7075
|
+
if (max === min) return [0, 0, l];
|
|
7076
|
+
const d = max - min;
|
|
7077
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
7078
|
+
let h;
|
|
7079
|
+
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
|
|
7080
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
7081
|
+
else h = (r - g) / d + 4;
|
|
7082
|
+
return [h * 60, s, l];
|
|
7083
|
+
}
|
|
7084
|
+
|
|
7085
|
+
function hslToHex(h, s, l) {
|
|
7086
|
+
const k = (n) => (n + h / 30) % 12;
|
|
7087
|
+
const a = s * Math.min(l, 1 - l);
|
|
7088
|
+
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1));
|
|
7089
|
+
const hex = (v) => Math.round(v * 255).toString(16).padStart(2, "0");
|
|
7090
|
+
return `#${hex(f(0))}${hex(f(8))}${hex(f(4))}`;
|
|
7091
|
+
}
|
|
7092
|
+
|
|
7093
|
+
/**
|
|
7094
|
+
* Derive the four status colours from a resolved palette. Each tone keeps its
|
|
7095
|
+
* hue family (green, red, amber, blue) and takes the accent's saturation and
|
|
7096
|
+
* lightness, then is stepped toward `onSurface` until it clears WCAG AA against
|
|
7097
|
+
* the surface, so a status mark reads as text on every look, light or dark.
|
|
7098
|
+
* Returns `{ success, danger, warning, info }`, or `null` when the inputs
|
|
7099
|
+
* aren't usable hex, letting the caller keep the default tokens.
|
|
7100
|
+
*/
|
|
7101
|
+
function deriveStatusColors(primary, surface, onSurface) {
|
|
7102
|
+
if (!isHexColor(primary) || !isHexColor(surface) || !isHexColor(onSurface)) {
|
|
7103
|
+
return null;
|
|
7104
|
+
}
|
|
7105
|
+
const [, accentS, accentL] = hexToHsl(primary);
|
|
7106
|
+
const targetL = hexToHsl(onSurface)[2];
|
|
7107
|
+
const s = Math.max(accentS, STATUS_MIN_SATURATION);
|
|
7108
|
+
const out = {};
|
|
7109
|
+
for (const [tone, hue] of Object.entries(STATUS_HUES)) {
|
|
7110
|
+
let l = Math.min(STATUS_LIGHTNESS.max, Math.max(STATUS_LIGHTNESS.min, accentL));
|
|
7111
|
+
let color = hslToHex(hue, s, l);
|
|
7112
|
+
const step = targetL < l ? -STATUS_LIGHTNESS_STEP : STATUS_LIGHTNESS_STEP;
|
|
7113
|
+
while (
|
|
7114
|
+
contrastRatio(color, surface) < ACCENT_TEXT_MIN_CONTRAST &&
|
|
7115
|
+
Math.abs(targetL - l) > STATUS_LIGHTNESS_STEP
|
|
7116
|
+
) {
|
|
7117
|
+
l += step;
|
|
7118
|
+
color = hslToHex(hue, s, l);
|
|
7119
|
+
}
|
|
7120
|
+
out[tone] = color;
|
|
7121
|
+
}
|
|
7122
|
+
return out;
|
|
7123
|
+
}
|
|
7124
|
+
|
|
7045
7125
|
// REQ-THEME-SURFACE: how far a derived surface travels toward white, and the
|
|
7046
7126
|
// light-on-dark text pair a dark surface carries.
|
|
7047
7127
|
const SURFACE_LIFT = Object.freeze({ surface: 0.1, surfaceMuted: 0.05, border: 0.18 });
|
|
@@ -7239,6 +7319,7 @@ module.exports = {
|
|
|
7239
7319
|
contrastRatio,
|
|
7240
7320
|
readableTextColor,
|
|
7241
7321
|
deriveAccentTints,
|
|
7322
|
+
deriveStatusColors,
|
|
7242
7323
|
deriveSurfaceTokens,
|
|
7243
7324
|
inkForBackdrop,
|
|
7244
7325
|
INK_CONTRAST,
|
package/dist/contract.js
CHANGED
|
@@ -83,10 +83,13 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
|
|
|
83
83
|
// background — so a dark theme gets a visible spinner with no new maths.
|
|
84
84
|
loader: "#475569",
|
|
85
85
|
border: "#e2e8f0",
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
// The four status tones are re-derived per tenant by `deriveStatusColors`
|
|
87
|
+
// (their hue families on the accent's saturation and lightness); these
|
|
88
|
+
// literals are that derivation applied to the default palette.
|
|
89
|
+
danger: "#e90b0b",
|
|
90
|
+
success: "#067e56",
|
|
91
|
+
warning: "#9b6507",
|
|
92
|
+
info: "#0877af",
|
|
90
93
|
}),
|
|
91
94
|
elevation: ELEVATION,
|
|
92
95
|
interaction: INTERACTION,
|
|
@@ -2281,6 +2284,12 @@ const HOOKS = [
|
|
|
2281
2284
|
"INVALID_ACTION_INPUT (400) — the input was refused; " +
|
|
2282
2285
|
"ACTION_NOT_APP_INVOKABLE (403) and ACTION_NOT_FOUND (404) will NOT " +
|
|
2283
2286
|
"clear on a retry, so hide the affordance; RATE_LIMITED (429) will; " +
|
|
2287
|
+
"ACTION_TIMEOUT (504) — the run was still going 250 ms after the press " +
|
|
2288
|
+
"and was stopped, so its OUTCOME IS UNKNOWN: some of its writes may " +
|
|
2289
|
+
"have happened. Never retry it blindly and never tell the user it did " +
|
|
2290
|
+
"nothing; an Action a widget runs must do its writes as one call or be " +
|
|
2291
|
+
"safe to repeat, and slow or multi-step work belongs in a " +
|
|
2292
|
+
"record-triggered Action; " +
|
|
2284
2293
|
"ACTION_FAILED — the Action ran and failed, or no answer arrived. NEVER " +
|
|
2285
2294
|
"show an app user a raw error. A text answer from a bot is often " +
|
|
2286
2295
|
"MARKDOWN, so render it with <RichText value={result} />, not <Text>. " +
|
|
@@ -6798,10 +6807,17 @@ const CONTRACT = deepFreeze({
|
|
|
6798
6807
|
// PaymentError codes.
|
|
6799
6808
|
// 1.237.0: widening — `manifest.translations` may declare up to 500 keys
|
|
6800
6809
|
// (was 200).
|
|
6810
|
+
// 1.239.0: additive — `deriveStatusColors`; the four status tokens follow the
|
|
6811
|
+
// palette, and their default literals are that derivation on the default.
|
|
6801
6812
|
// 1.238.0: additive (sc-15659) — `useConnector(connectorRef)` + the
|
|
6802
6813
|
// `connectors` host client + the `connectorRef` propertySchema type + the
|
|
6803
6814
|
// `connectors.call:*` scope. A widget calls a REST connector's listed operations.
|
|
6804
|
-
|
|
6815
|
+
// 1.240.0: additive — `deriveStatusColors`; the four status tokens follow the
|
|
6816
|
+
// palette, and their default literals are that derivation on the default.
|
|
6817
|
+
// 1.239.0: additive (sc-15658) — an app-triggered run must finish within 250 ms
|
|
6818
|
+
// of the press; `useAction` errors carry the new code ACTION_TIMEOUT
|
|
6819
|
+
// (504), whose outcome is unknown.
|
|
6820
|
+
version: "1.240.0",
|
|
6805
6821
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6806
6822
|
hooks: HOOKS,
|
|
6807
6823
|
primitives: PRIMITIVES,
|
|
@@ -7038,6 +7054,70 @@ function deriveAccentTints(primary, surface, onSurface) {
|
|
|
7038
7054
|
};
|
|
7039
7055
|
}
|
|
7040
7056
|
|
|
7057
|
+
// The hue family each status tone keeps whatever the palette.
|
|
7058
|
+
const STATUS_HUES = Object.freeze({ success: 160, danger: 0, warning: 38, info: 200 });
|
|
7059
|
+
// Below this saturation a tone loses its hue, so a grey accent still reads as a colour.
|
|
7060
|
+
const STATUS_MIN_SATURATION = 0.35;
|
|
7061
|
+
// The lightness band a tone starts in, so a near-black or pastel accent keeps a visible hue.
|
|
7062
|
+
const STATUS_LIGHTNESS = Object.freeze({ min: 0.28, max: 0.62 });
|
|
7063
|
+
const STATUS_LIGHTNESS_STEP = 0.02;
|
|
7064
|
+
|
|
7065
|
+
/** `[hue 0-360, saturation 0-1, lightness 0-1]` of a hex colour. */
|
|
7066
|
+
function hexToHsl(hex) {
|
|
7067
|
+
const [r, g, b] = hexChannels(hex).map((v) => v / 255);
|
|
7068
|
+
const max = Math.max(r, g, b);
|
|
7069
|
+
const min = Math.min(r, g, b);
|
|
7070
|
+
const l = (max + min) / 2;
|
|
7071
|
+
if (max === min) return [0, 0, l];
|
|
7072
|
+
const d = max - min;
|
|
7073
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
7074
|
+
let h;
|
|
7075
|
+
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
|
|
7076
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
7077
|
+
else h = (r - g) / d + 4;
|
|
7078
|
+
return [h * 60, s, l];
|
|
7079
|
+
}
|
|
7080
|
+
|
|
7081
|
+
function hslToHex(h, s, l) {
|
|
7082
|
+
const k = (n) => (n + h / 30) % 12;
|
|
7083
|
+
const a = s * Math.min(l, 1 - l);
|
|
7084
|
+
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1));
|
|
7085
|
+
const hex = (v) => Math.round(v * 255).toString(16).padStart(2, "0");
|
|
7086
|
+
return `#${hex(f(0))}${hex(f(8))}${hex(f(4))}`;
|
|
7087
|
+
}
|
|
7088
|
+
|
|
7089
|
+
/**
|
|
7090
|
+
* Derive the four status colours from a resolved palette. Each tone keeps its
|
|
7091
|
+
* hue family (green, red, amber, blue) and takes the accent's saturation and
|
|
7092
|
+
* lightness, then is stepped toward `onSurface` until it clears WCAG AA against
|
|
7093
|
+
* the surface, so a status mark reads as text on every look, light or dark.
|
|
7094
|
+
* Returns `{ success, danger, warning, info }`, or `null` when the inputs
|
|
7095
|
+
* aren't usable hex, letting the caller keep the default tokens.
|
|
7096
|
+
*/
|
|
7097
|
+
function deriveStatusColors(primary, surface, onSurface) {
|
|
7098
|
+
if (!isHexColor(primary) || !isHexColor(surface) || !isHexColor(onSurface)) {
|
|
7099
|
+
return null;
|
|
7100
|
+
}
|
|
7101
|
+
const [, accentS, accentL] = hexToHsl(primary);
|
|
7102
|
+
const targetL = hexToHsl(onSurface)[2];
|
|
7103
|
+
const s = Math.max(accentS, STATUS_MIN_SATURATION);
|
|
7104
|
+
const out = {};
|
|
7105
|
+
for (const [tone, hue] of Object.entries(STATUS_HUES)) {
|
|
7106
|
+
let l = Math.min(STATUS_LIGHTNESS.max, Math.max(STATUS_LIGHTNESS.min, accentL));
|
|
7107
|
+
let color = hslToHex(hue, s, l);
|
|
7108
|
+
const step = targetL < l ? -STATUS_LIGHTNESS_STEP : STATUS_LIGHTNESS_STEP;
|
|
7109
|
+
while (
|
|
7110
|
+
contrastRatio(color, surface) < ACCENT_TEXT_MIN_CONTRAST &&
|
|
7111
|
+
Math.abs(targetL - l) > STATUS_LIGHTNESS_STEP
|
|
7112
|
+
) {
|
|
7113
|
+
l += step;
|
|
7114
|
+
color = hslToHex(hue, s, l);
|
|
7115
|
+
}
|
|
7116
|
+
out[tone] = color;
|
|
7117
|
+
}
|
|
7118
|
+
return out;
|
|
7119
|
+
}
|
|
7120
|
+
|
|
7041
7121
|
// REQ-THEME-SURFACE: how far a derived surface travels toward white, and the
|
|
7042
7122
|
// light-on-dark text pair a dark surface carries.
|
|
7043
7123
|
const SURFACE_LIFT = Object.freeze({ surface: 0.1, surfaceMuted: 0.05, border: 0.18 });
|
|
@@ -7235,6 +7315,7 @@ export {
|
|
|
7235
7315
|
contrastRatio,
|
|
7236
7316
|
readableTextColor,
|
|
7237
7317
|
deriveAccentTints,
|
|
7318
|
+
deriveStatusColors,
|
|
7238
7319
|
deriveSurfaceTokens,
|
|
7239
7320
|
inkForBackdrop,
|
|
7240
7321
|
INK_CONTRAST,
|
package/dist/index.d.ts
CHANGED
|
@@ -3513,6 +3513,7 @@ export class ActionError extends Error {
|
|
|
3513
3513
|
| "AUTH_REQUIRED"
|
|
3514
3514
|
| "TOKEN_EXPIRED"
|
|
3515
3515
|
| "TOKEN_INVALID"
|
|
3516
|
+
| "ACTION_TIMEOUT"
|
|
3516
3517
|
| "ACTION_FAILED"
|
|
3517
3518
|
| (string & {});
|
|
3518
3519
|
constructor(code: string, message: string, opts?: { cause?: unknown });
|
|
@@ -6066,6 +6067,23 @@ export function deriveAccentTints(
|
|
|
6066
6067
|
primaryStrong: string;
|
|
6067
6068
|
} | null;
|
|
6068
6069
|
|
|
6070
|
+
/**
|
|
6071
|
+
* Derive the four status colours from a resolved palette: each keeps its hue
|
|
6072
|
+
* family and takes the accent's saturation and lightness, stepped toward
|
|
6073
|
+
* `onSurface` until it clears WCAG AA against the surface. `null` when the
|
|
6074
|
+
* inputs aren't hex.
|
|
6075
|
+
*/
|
|
6076
|
+
export function deriveStatusColors(
|
|
6077
|
+
primary: string,
|
|
6078
|
+
surface: string,
|
|
6079
|
+
onSurface: string,
|
|
6080
|
+
): {
|
|
6081
|
+
success: string;
|
|
6082
|
+
danger: string;
|
|
6083
|
+
warning: string;
|
|
6084
|
+
info: string;
|
|
6085
|
+
} | null;
|
|
6086
|
+
|
|
6069
6087
|
/**
|
|
6070
6088
|
* Map a CSS gradient angle (0 = to top, 90 = to right) to the `{ start, end }`
|
|
6071
6089
|
* unit vectors expo-linear-gradient expects.
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.293.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
|
@@ -83,10 +83,13 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
|
|
|
83
83
|
// background — so a dark theme gets a visible spinner with no new maths.
|
|
84
84
|
loader: "#475569",
|
|
85
85
|
border: "#e2e8f0",
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
// The four status tones are re-derived per tenant by `deriveStatusColors`
|
|
87
|
+
// (their hue families on the accent's saturation and lightness); these
|
|
88
|
+
// literals are that derivation applied to the default palette.
|
|
89
|
+
danger: "#e90b0b",
|
|
90
|
+
success: "#067e56",
|
|
91
|
+
warning: "#9b6507",
|
|
92
|
+
info: "#0877af",
|
|
90
93
|
}),
|
|
91
94
|
elevation: ELEVATION,
|
|
92
95
|
interaction: INTERACTION,
|
|
@@ -2281,6 +2284,12 @@ const HOOKS = [
|
|
|
2281
2284
|
"INVALID_ACTION_INPUT (400) — the input was refused; " +
|
|
2282
2285
|
"ACTION_NOT_APP_INVOKABLE (403) and ACTION_NOT_FOUND (404) will NOT " +
|
|
2283
2286
|
"clear on a retry, so hide the affordance; RATE_LIMITED (429) will; " +
|
|
2287
|
+
"ACTION_TIMEOUT (504) — the run was still going 250 ms after the press " +
|
|
2288
|
+
"and was stopped, so its OUTCOME IS UNKNOWN: some of its writes may " +
|
|
2289
|
+
"have happened. Never retry it blindly and never tell the user it did " +
|
|
2290
|
+
"nothing; an Action a widget runs must do its writes as one call or be " +
|
|
2291
|
+
"safe to repeat, and slow or multi-step work belongs in a " +
|
|
2292
|
+
"record-triggered Action; " +
|
|
2284
2293
|
"ACTION_FAILED — the Action ran and failed, or no answer arrived. NEVER " +
|
|
2285
2294
|
"show an app user a raw error. A text answer from a bot is often " +
|
|
2286
2295
|
"MARKDOWN, so render it with <RichText value={result} />, not <Text>. " +
|
|
@@ -6798,10 +6807,17 @@ const CONTRACT = deepFreeze({
|
|
|
6798
6807
|
// PaymentError codes.
|
|
6799
6808
|
// 1.237.0: widening — `manifest.translations` may declare up to 500 keys
|
|
6800
6809
|
// (was 200).
|
|
6810
|
+
// 1.239.0: additive — `deriveStatusColors`; the four status tokens follow the
|
|
6811
|
+
// palette, and their default literals are that derivation on the default.
|
|
6801
6812
|
// 1.238.0: additive (sc-15659) — `useConnector(connectorRef)` + the
|
|
6802
6813
|
// `connectors` host client + the `connectorRef` propertySchema type + the
|
|
6803
6814
|
// `connectors.call:*` scope. A widget calls a REST connector's listed operations.
|
|
6804
|
-
|
|
6815
|
+
// 1.240.0: additive — `deriveStatusColors`; the four status tokens follow the
|
|
6816
|
+
// palette, and their default literals are that derivation on the default.
|
|
6817
|
+
// 1.239.0: additive (sc-15658) — an app-triggered run must finish within 250 ms
|
|
6818
|
+
// of the press; `useAction` errors carry the new code ACTION_TIMEOUT
|
|
6819
|
+
// (504), whose outcome is unknown.
|
|
6820
|
+
version: "1.240.0",
|
|
6805
6821
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6806
6822
|
hooks: HOOKS,
|
|
6807
6823
|
primitives: PRIMITIVES,
|
|
@@ -7038,6 +7054,70 @@ function deriveAccentTints(primary, surface, onSurface) {
|
|
|
7038
7054
|
};
|
|
7039
7055
|
}
|
|
7040
7056
|
|
|
7057
|
+
// The hue family each status tone keeps whatever the palette.
|
|
7058
|
+
const STATUS_HUES = Object.freeze({ success: 160, danger: 0, warning: 38, info: 200 });
|
|
7059
|
+
// Below this saturation a tone loses its hue, so a grey accent still reads as a colour.
|
|
7060
|
+
const STATUS_MIN_SATURATION = 0.35;
|
|
7061
|
+
// The lightness band a tone starts in, so a near-black or pastel accent keeps a visible hue.
|
|
7062
|
+
const STATUS_LIGHTNESS = Object.freeze({ min: 0.28, max: 0.62 });
|
|
7063
|
+
const STATUS_LIGHTNESS_STEP = 0.02;
|
|
7064
|
+
|
|
7065
|
+
/** `[hue 0-360, saturation 0-1, lightness 0-1]` of a hex colour. */
|
|
7066
|
+
function hexToHsl(hex) {
|
|
7067
|
+
const [r, g, b] = hexChannels(hex).map((v) => v / 255);
|
|
7068
|
+
const max = Math.max(r, g, b);
|
|
7069
|
+
const min = Math.min(r, g, b);
|
|
7070
|
+
const l = (max + min) / 2;
|
|
7071
|
+
if (max === min) return [0, 0, l];
|
|
7072
|
+
const d = max - min;
|
|
7073
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
7074
|
+
let h;
|
|
7075
|
+
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
|
|
7076
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
7077
|
+
else h = (r - g) / d + 4;
|
|
7078
|
+
return [h * 60, s, l];
|
|
7079
|
+
}
|
|
7080
|
+
|
|
7081
|
+
function hslToHex(h, s, l) {
|
|
7082
|
+
const k = (n) => (n + h / 30) % 12;
|
|
7083
|
+
const a = s * Math.min(l, 1 - l);
|
|
7084
|
+
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1));
|
|
7085
|
+
const hex = (v) => Math.round(v * 255).toString(16).padStart(2, "0");
|
|
7086
|
+
return `#${hex(f(0))}${hex(f(8))}${hex(f(4))}`;
|
|
7087
|
+
}
|
|
7088
|
+
|
|
7089
|
+
/**
|
|
7090
|
+
* Derive the four status colours from a resolved palette. Each tone keeps its
|
|
7091
|
+
* hue family (green, red, amber, blue) and takes the accent's saturation and
|
|
7092
|
+
* lightness, then is stepped toward `onSurface` until it clears WCAG AA against
|
|
7093
|
+
* the surface, so a status mark reads as text on every look, light or dark.
|
|
7094
|
+
* Returns `{ success, danger, warning, info }`, or `null` when the inputs
|
|
7095
|
+
* aren't usable hex, letting the caller keep the default tokens.
|
|
7096
|
+
*/
|
|
7097
|
+
function deriveStatusColors(primary, surface, onSurface) {
|
|
7098
|
+
if (!isHexColor(primary) || !isHexColor(surface) || !isHexColor(onSurface)) {
|
|
7099
|
+
return null;
|
|
7100
|
+
}
|
|
7101
|
+
const [, accentS, accentL] = hexToHsl(primary);
|
|
7102
|
+
const targetL = hexToHsl(onSurface)[2];
|
|
7103
|
+
const s = Math.max(accentS, STATUS_MIN_SATURATION);
|
|
7104
|
+
const out = {};
|
|
7105
|
+
for (const [tone, hue] of Object.entries(STATUS_HUES)) {
|
|
7106
|
+
let l = Math.min(STATUS_LIGHTNESS.max, Math.max(STATUS_LIGHTNESS.min, accentL));
|
|
7107
|
+
let color = hslToHex(hue, s, l);
|
|
7108
|
+
const step = targetL < l ? -STATUS_LIGHTNESS_STEP : STATUS_LIGHTNESS_STEP;
|
|
7109
|
+
while (
|
|
7110
|
+
contrastRatio(color, surface) < ACCENT_TEXT_MIN_CONTRAST &&
|
|
7111
|
+
Math.abs(targetL - l) > STATUS_LIGHTNESS_STEP
|
|
7112
|
+
) {
|
|
7113
|
+
l += step;
|
|
7114
|
+
color = hslToHex(hue, s, l);
|
|
7115
|
+
}
|
|
7116
|
+
out[tone] = color;
|
|
7117
|
+
}
|
|
7118
|
+
return out;
|
|
7119
|
+
}
|
|
7120
|
+
|
|
7041
7121
|
// REQ-THEME-SURFACE: how far a derived surface travels toward white, and the
|
|
7042
7122
|
// light-on-dark text pair a dark surface carries.
|
|
7043
7123
|
const SURFACE_LIFT = Object.freeze({ surface: 0.1, surfaceMuted: 0.05, border: 0.18 });
|
|
@@ -7235,6 +7315,7 @@ export {
|
|
|
7235
7315
|
contrastRatio,
|
|
7236
7316
|
readableTextColor,
|
|
7237
7317
|
deriveAccentTints,
|
|
7318
|
+
deriveStatusColors,
|
|
7238
7319
|
deriveSurfaceTokens,
|
|
7239
7320
|
inkForBackdrop,
|
|
7240
7321
|
INK_CONTRAST,
|