@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 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.291.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**.
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, and `ACTION_FAILED` means the Action ran and failed. Never show an app user the raw message. A text answer from a bot is often markdown, so render it with `<RichText>`.
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
- danger: "#dc2626",
91
- success: "#059669",
92
- warning: "#d97706",
93
- info: "#0284c7",
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
- version: "1.238.0",
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
- danger: "#dc2626",
87
- success: "#059669",
88
- warning: "#d97706",
89
- info: "#0284c7",
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
- version: "1.238.0",
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
@@ -215,6 +215,7 @@ export {
215
215
  contrastRatio,
216
216
  readableTextColor,
217
217
  deriveAccentTints,
218
+ deriveStatusColors,
218
219
  gradientAngleToVector,
219
220
  normaliseComponentGradient,
220
221
  componentGradientColors,
@@ -208,6 +208,7 @@ export {
208
208
  contrastRatio,
209
209
  readableTextColor,
210
210
  deriveAccentTints,
211
+ deriveStatusColors,
211
212
  gradientAngleToVector,
212
213
  normaliseComponentGradient,
213
214
  componentGradientColors,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.291.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
- danger: "#dc2626",
87
- success: "#059669",
88
- warning: "#d97706",
89
- info: "#0284c7",
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
- version: "1.238.0",
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,