@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 +5 -1
- package/dist/contract.cjs +77 -5
- package/dist/contract.js +77 -5
- package/dist/index.d.ts +17 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/package.json +1 -1
- package/src/contract.js +77 -5
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.
|
|
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
|
-
|
|
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,
|
|
@@ -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.
|
|
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
|
-
|
|
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,
|
|
@@ -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.
|
|
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
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,
|
|
@@ -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.
|
|
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,
|