@colixsystems/widget-sdk 0.292.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,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
90
90
 
91
91
  ## Status
92
92
 
93
- `v0.292.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.
94
98
 
95
99
  ### What's new in 0.292.0 (contract 1.239.0)
96
100
 
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,
@@ -6808,13 +6811,17 @@ const CONTRACT = deepFreeze({
6808
6811
  // PaymentError codes.
6809
6812
  // 1.237.0: widening — `manifest.translations` may declare up to 500 keys
6810
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.
6811
6816
  // 1.238.0: additive (sc-15659) — `useConnector(connectorRef)` + the
6812
6817
  // `connectors` host client + the `connectorRef` propertySchema type + the
6813
6818
  // `connectors.call:*` scope. A widget calls a REST connector's listed operations.
6819
+ // 1.240.0: additive — `deriveStatusColors`; the four status tokens follow the
6820
+ // palette, and their default literals are that derivation on the default.
6814
6821
  // 1.239.0: additive (sc-15658) — an app-triggered run must finish within 250 ms
6815
6822
  // of the press; `useAction` errors carry the new code ACTION_TIMEOUT
6816
6823
  // (504), whose outcome is unknown.
6817
- version: "1.239.0",
6824
+ version: "1.240.0",
6818
6825
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6819
6826
  hooks: HOOKS,
6820
6827
  primitives: PRIMITIVES,
@@ -7051,6 +7058,70 @@ function deriveAccentTints(primary, surface, onSurface) {
7051
7058
  };
7052
7059
  }
7053
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
+
7054
7125
  // REQ-THEME-SURFACE: how far a derived surface travels toward white, and the
7055
7126
  // light-on-dark text pair a dark surface carries.
7056
7127
  const SURFACE_LIFT = Object.freeze({ surface: 0.1, surfaceMuted: 0.05, border: 0.18 });
@@ -7248,6 +7319,7 @@ module.exports = {
7248
7319
  contrastRatio,
7249
7320
  readableTextColor,
7250
7321
  deriveAccentTints,
7322
+ deriveStatusColors,
7251
7323
  deriveSurfaceTokens,
7252
7324
  inkForBackdrop,
7253
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,
@@ -6804,13 +6807,17 @@ const CONTRACT = deepFreeze({
6804
6807
  // PaymentError codes.
6805
6808
  // 1.237.0: widening — `manifest.translations` may declare up to 500 keys
6806
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.
6807
6812
  // 1.238.0: additive (sc-15659) — `useConnector(connectorRef)` + the
6808
6813
  // `connectors` host client + the `connectorRef` propertySchema type + the
6809
6814
  // `connectors.call:*` scope. A widget calls a REST connector's listed operations.
6815
+ // 1.240.0: additive — `deriveStatusColors`; the four status tokens follow the
6816
+ // palette, and their default literals are that derivation on the default.
6810
6817
  // 1.239.0: additive (sc-15658) — an app-triggered run must finish within 250 ms
6811
6818
  // of the press; `useAction` errors carry the new code ACTION_TIMEOUT
6812
6819
  // (504), whose outcome is unknown.
6813
- version: "1.239.0",
6820
+ version: "1.240.0",
6814
6821
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6815
6822
  hooks: HOOKS,
6816
6823
  primitives: PRIMITIVES,
@@ -7047,6 +7054,70 @@ function deriveAccentTints(primary, surface, onSurface) {
7047
7054
  };
7048
7055
  }
7049
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
+
7050
7121
  // REQ-THEME-SURFACE: how far a derived surface travels toward white, and the
7051
7122
  // light-on-dark text pair a dark surface carries.
7052
7123
  const SURFACE_LIFT = Object.freeze({ surface: 0.1, surfaceMuted: 0.05, border: 0.18 });
@@ -7244,6 +7315,7 @@ export {
7244
7315
  contrastRatio,
7245
7316
  readableTextColor,
7246
7317
  deriveAccentTints,
7318
+ deriveStatusColors,
7247
7319
  deriveSurfaceTokens,
7248
7320
  inkForBackdrop,
7249
7321
  INK_CONTRAST,
package/dist/index.d.ts CHANGED
@@ -6067,6 +6067,23 @@ export function deriveAccentTints(
6067
6067
  primaryStrong: string;
6068
6068
  } | null;
6069
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
+
6070
6087
  /**
6071
6088
  * Map a CSS gradient angle (0 = to top, 90 = to right) to the `{ start, end }`
6072
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.292.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,
@@ -6804,13 +6807,17 @@ const CONTRACT = deepFreeze({
6804
6807
  // PaymentError codes.
6805
6808
  // 1.237.0: widening — `manifest.translations` may declare up to 500 keys
6806
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.
6807
6812
  // 1.238.0: additive (sc-15659) — `useConnector(connectorRef)` + the
6808
6813
  // `connectors` host client + the `connectorRef` propertySchema type + the
6809
6814
  // `connectors.call:*` scope. A widget calls a REST connector's listed operations.
6815
+ // 1.240.0: additive — `deriveStatusColors`; the four status tokens follow the
6816
+ // palette, and their default literals are that derivation on the default.
6810
6817
  // 1.239.0: additive (sc-15658) — an app-triggered run must finish within 250 ms
6811
6818
  // of the press; `useAction` errors carry the new code ACTION_TIMEOUT
6812
6819
  // (504), whose outcome is unknown.
6813
- version: "1.239.0",
6820
+ version: "1.240.0",
6814
6821
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6815
6822
  hooks: HOOKS,
6816
6823
  primitives: PRIMITIVES,
@@ -7047,6 +7054,70 @@ function deriveAccentTints(primary, surface, onSurface) {
7047
7054
  };
7048
7055
  }
7049
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
+
7050
7121
  // REQ-THEME-SURFACE: how far a derived surface travels toward white, and the
7051
7122
  // light-on-dark text pair a dark surface carries.
7052
7123
  const SURFACE_LIFT = Object.freeze({ surface: 0.1, surfaceMuted: 0.05, border: 0.18 });
@@ -7244,6 +7315,7 @@ export {
7244
7315
  contrastRatio,
7245
7316
  readableTextColor,
7246
7317
  deriveAccentTints,
7318
+ deriveStatusColors,
7247
7319
  deriveSurfaceTokens,
7248
7320
  inkForBackdrop,
7249
7321
  INK_CONTRAST,