@colixsystems/widget-sdk 0.195.0 → 0.196.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 +14 -1
- package/dist/card-style.js +15 -4
- package/dist/contract.cjs +55 -3
- package/dist/contract.js +55 -3
- package/dist/index.d.ts +12 -0
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/theme-token-refs.js +3 -1
- package/package.json +1 -1
- package/src/contract.js +55 -3
package/README.md
CHANGED
|
@@ -77,7 +77,20 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
77
77
|
|
|
78
78
|
## Status
|
|
79
79
|
|
|
80
|
-
`v0.
|
|
80
|
+
`v0.196.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**.
|
|
81
|
+
|
|
82
|
+
### What's new in 0.196.0 (contract 1.157.0)
|
|
83
|
+
|
|
84
|
+
**A gradient can carry more than two colours.** A component gradient was exactly `{ from, to, angle }`. It now takes an optional `via` list of up to three middle colours, painted in order and evenly spaced between `from` and `to`:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
{ from: "#0f172a", via: ["#7c3aed", "#db2777"], to: "#f59e0b", angle: 135 }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- **`normaliseComponentGradient`** returns `via` only when the value carries some, so a two-colour gradient normalises exactly as before. A malformed `via` (not a list, more than `CONTRACT.themeComponentGradient.maxViaStops` entries, or an entry that is not a hex colour) is dropped and the two ends still paint, so a stored value never loses its gradient over it. Pass `{ strict: true }` to get `null` for a malformed `via` instead, which is what a write boundary wants.
|
|
91
|
+
- **`componentGradientColors(gradient)`** gives a normalised gradient's colours in paint order, ready for `<Gradient colors={…}>`. **`componentGradientMidpoint(gradient)`** gives the colour halfway along it, the basis for choosing readable text.
|
|
92
|
+
- **`<Card>` and `<Button>` paint the middle colours** when their gradient carries them, and a layout container's wash and photo scrim do the same on the web Player and in the exported app. A middle colour of a container wash may bind to the theme (`"theme:colors.primary"`) like its two ends.
|
|
93
|
+
- **Fully additive.** Every stored two-colour gradient renders byte-identically.
|
|
81
94
|
|
|
82
95
|
### What's new in 0.195.0 (contract 1.156.0)
|
|
83
96
|
|
package/dist/card-style.js
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
// Every resolver emits ONLY the fields that are set, so a widget's own base
|
|
6
6
|
// style keeps whatever the author left alone.
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
CONTRACT,
|
|
10
|
+
isHexColor,
|
|
11
|
+
normaliseComponentGradient,
|
|
12
|
+
componentGradientColors,
|
|
13
|
+
componentGradientMidpoint,
|
|
14
|
+
} from "./contract.js";
|
|
9
15
|
import { normaliseCornerRadius, cornerRadiusStyle } from "./corner-radius.js";
|
|
10
16
|
import { normaliseSpacing, spacingStyle, isZeroSpacing } from "./spacing.js";
|
|
11
17
|
import { flattenStyle } from "./flatten-style.js";
|
|
@@ -117,7 +123,7 @@ export function cardLayoutStyle(style) {
|
|
|
117
123
|
export function gradientProps(value) {
|
|
118
124
|
const gradient = normaliseComponentGradient(value);
|
|
119
125
|
return gradient
|
|
120
|
-
? { colors:
|
|
126
|
+
? { colors: componentGradientColors(gradient), angle: gradient.angle }
|
|
121
127
|
: null;
|
|
122
128
|
}
|
|
123
129
|
|
|
@@ -131,8 +137,13 @@ export function gradientProps(value) {
|
|
|
131
137
|
*/
|
|
132
138
|
export function cardBackdrop(styles, gradient) {
|
|
133
139
|
if (gradient) {
|
|
134
|
-
const
|
|
135
|
-
|
|
140
|
+
const { colors } = gradient;
|
|
141
|
+
if (!colors.every(isOpaqueHex)) return null;
|
|
142
|
+
return componentGradientMidpoint({
|
|
143
|
+
from: colors[0],
|
|
144
|
+
via: colors.slice(1, -1),
|
|
145
|
+
to: colors[colors.length - 1],
|
|
146
|
+
});
|
|
136
147
|
}
|
|
137
148
|
const fill = flattenStyle(styles).backgroundColor;
|
|
138
149
|
return isOpaqueHex(fill) ? fill : null;
|
package/dist/contract.cjs
CHANGED
|
@@ -332,6 +332,8 @@ const THEME_COMPONENT_GRADIENT = Object.freeze({
|
|
|
332
332
|
angleMin: 0,
|
|
333
333
|
angleMax: 359,
|
|
334
334
|
defaultAngle: 180,
|
|
335
|
+
// sc-10685: middle colours allowed between the two ends (`via`).
|
|
336
|
+
maxViaStops: 3,
|
|
335
337
|
});
|
|
336
338
|
|
|
337
339
|
// REQ-THEME-LOOK: the app-wide SPACING MULTIPLIER. Layout spacing lives on the
|
|
@@ -5099,7 +5101,14 @@ const CONTRACT = deepFreeze({
|
|
|
5099
5101
|
// `update(userId, { name?, email? })`, gated on `users.write:*`; the
|
|
5100
5102
|
// linter requires that scope for a `.update(` call beside `useUsers(`.
|
|
5101
5103
|
// `DirectoryError` gains `reason`, the host's specific refusal code.
|
|
5102
|
-
|
|
5104
|
+
// 1.157.0: additive (sc-10685) — a component gradient takes an optional
|
|
5105
|
+
// `via` list of up to `themeComponentGradient.maxViaStops` middle colours
|
|
5106
|
+
// between `from` and `to`, evenly spaced. `normaliseComponentGradient`
|
|
5107
|
+
// returns it only when present, so a two-colour value normalises exactly
|
|
5108
|
+
// as before; a malformed `via` is dropped, or voids the gradient under the
|
|
5109
|
+
// new `{ strict: true }` option. New `componentGradientColors` / `componentGradientMidpoint`
|
|
5110
|
+
// read the paint list and the ink basis off a normalised gradient.
|
|
5111
|
+
version: "1.157.0",
|
|
5103
5112
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5104
5113
|
hooks: HOOKS,
|
|
5105
5114
|
primitives: PRIMITIVES,
|
|
@@ -5418,6 +5427,10 @@ const GRADIENT_HEX_RE = HEX_RE;
|
|
|
5418
5427
|
|
|
5419
5428
|
/**
|
|
5420
5429
|
* sc-3727 — normalise a component `gradient` to `{ from, to, angle }`, or `null`.
|
|
5430
|
+
* sc-10685: plus `via`, the middle colours, only when the value carries some.
|
|
5431
|
+
* A malformed `via` is dropped and the two ends still paint, so a stored value
|
|
5432
|
+
* never loses its wash over it; `{ strict: true }` voids the gradient instead,
|
|
5433
|
+
* for the boundaries that must refuse the write.
|
|
5421
5434
|
*
|
|
5422
5435
|
* ONE validator for a value arriving by two routes — the theme token and the
|
|
5423
5436
|
* author's never-coerced per-instance `props.style` — so `set_theme` and the
|
|
@@ -5426,7 +5439,7 @@ const GRADIENT_HEX_RE = HEX_RE;
|
|
|
5426
5439
|
* hex grammar is also the injection guard — these stops are interpolated into
|
|
5427
5440
|
* CSS and into generated export source.
|
|
5428
5441
|
*/
|
|
5429
|
-
function normaliseComponentGradient(raw) {
|
|
5442
|
+
function normaliseComponentGradient(raw, options) {
|
|
5430
5443
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
5431
5444
|
const from = typeof raw.from === "string" ? raw.from.trim() : "";
|
|
5432
5445
|
const to = typeof raw.to === "string" ? raw.to.trim() : "";
|
|
@@ -5436,7 +5449,44 @@ function normaliseComponentGradient(raw) {
|
|
|
5436
5449
|
// land NaN in a style.
|
|
5437
5450
|
const angle = raw.angle;
|
|
5438
5451
|
const deg = Number.isFinite(angle) ? Math.round(angle) : defaultAngle;
|
|
5439
|
-
|
|
5452
|
+
const via = normaliseGradientVia(raw.via);
|
|
5453
|
+
if (!via && options && options.strict) return null;
|
|
5454
|
+
const gradient = { from, to, angle: ((deg % 360) + 360) % 360 };
|
|
5455
|
+
if (via && via.length > 0) gradient.via = via;
|
|
5456
|
+
return gradient;
|
|
5457
|
+
}
|
|
5458
|
+
|
|
5459
|
+
// All or nothing: one bad middle colour and none of them paint. `null` = malformed.
|
|
5460
|
+
function normaliseGradientVia(raw) {
|
|
5461
|
+
if (raw === undefined || raw === null) return [];
|
|
5462
|
+
if (!Array.isArray(raw) || raw.length > THEME_COMPONENT_GRADIENT.maxViaStops) {
|
|
5463
|
+
return null;
|
|
5464
|
+
}
|
|
5465
|
+
const via = [];
|
|
5466
|
+
for (const item of raw) {
|
|
5467
|
+
const stop = typeof item === "string" ? item.trim() : "";
|
|
5468
|
+
if (!GRADIENT_HEX_RE.test(stop)) return null;
|
|
5469
|
+
via.push(stop);
|
|
5470
|
+
}
|
|
5471
|
+
return via;
|
|
5472
|
+
}
|
|
5473
|
+
|
|
5474
|
+
/**
|
|
5475
|
+
* sc-10685 — a NORMALISED gradient's colours in paint order, evenly spaced:
|
|
5476
|
+
* `[from, ...via, to]`. The one place both hosts read the list from.
|
|
5477
|
+
*/
|
|
5478
|
+
function componentGradientColors(gradient) {
|
|
5479
|
+
return [gradient.from, ...(gradient.via || []), gradient.to];
|
|
5480
|
+
}
|
|
5481
|
+
|
|
5482
|
+
/**
|
|
5483
|
+
* sc-10685 — the colour halfway along a NORMALISED gradient, which is what
|
|
5484
|
+
* text laid across it is contrasted against. Two colours: their mix, as before.
|
|
5485
|
+
*/
|
|
5486
|
+
function componentGradientMidpoint(gradient) {
|
|
5487
|
+
const colors = componentGradientColors(gradient);
|
|
5488
|
+
const middle = (colors.length - 1) / 2;
|
|
5489
|
+
return mixHex(colors[Math.floor(middle)], colors[Math.ceil(middle)], 0.5);
|
|
5440
5490
|
}
|
|
5441
5491
|
|
|
5442
5492
|
// REQ-THEME-LOOK: coerce a theme's spacing scale into the usable band. Shared
|
|
@@ -5479,6 +5529,8 @@ module.exports = {
|
|
|
5479
5529
|
INK_CONTRAST,
|
|
5480
5530
|
gradientAngleToVector,
|
|
5481
5531
|
normaliseComponentGradient,
|
|
5532
|
+
componentGradientColors,
|
|
5533
|
+
componentGradientMidpoint,
|
|
5482
5534
|
isThemeColorTokenRef,
|
|
5483
5535
|
parseThemeColorTokenRef,
|
|
5484
5536
|
formatThemeColorTokenRef,
|
package/dist/contract.js
CHANGED
|
@@ -332,6 +332,8 @@ const THEME_COMPONENT_GRADIENT = Object.freeze({
|
|
|
332
332
|
angleMin: 0,
|
|
333
333
|
angleMax: 359,
|
|
334
334
|
defaultAngle: 180,
|
|
335
|
+
// sc-10685: middle colours allowed between the two ends (`via`).
|
|
336
|
+
maxViaStops: 3,
|
|
335
337
|
});
|
|
336
338
|
|
|
337
339
|
// REQ-THEME-LOOK: the app-wide SPACING MULTIPLIER. Layout spacing lives on the
|
|
@@ -5095,7 +5097,14 @@ const CONTRACT = deepFreeze({
|
|
|
5095
5097
|
// `update(userId, { name?, email? })`, gated on `users.write:*`; the
|
|
5096
5098
|
// linter requires that scope for a `.update(` call beside `useUsers(`.
|
|
5097
5099
|
// `DirectoryError` gains `reason`, the host's specific refusal code.
|
|
5098
|
-
|
|
5100
|
+
// 1.157.0: additive (sc-10685) — a component gradient takes an optional
|
|
5101
|
+
// `via` list of up to `themeComponentGradient.maxViaStops` middle colours
|
|
5102
|
+
// between `from` and `to`, evenly spaced. `normaliseComponentGradient`
|
|
5103
|
+
// returns it only when present, so a two-colour value normalises exactly
|
|
5104
|
+
// as before; a malformed `via` is dropped, or voids the gradient under the
|
|
5105
|
+
// new `{ strict: true }` option. New `componentGradientColors` / `componentGradientMidpoint`
|
|
5106
|
+
// read the paint list and the ink basis off a normalised gradient.
|
|
5107
|
+
version: "1.157.0",
|
|
5099
5108
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5100
5109
|
hooks: HOOKS,
|
|
5101
5110
|
primitives: PRIMITIVES,
|
|
@@ -5414,6 +5423,10 @@ const GRADIENT_HEX_RE = HEX_RE;
|
|
|
5414
5423
|
|
|
5415
5424
|
/**
|
|
5416
5425
|
* sc-3727 — normalise a component `gradient` to `{ from, to, angle }`, or `null`.
|
|
5426
|
+
* sc-10685: plus `via`, the middle colours, only when the value carries some.
|
|
5427
|
+
* A malformed `via` is dropped and the two ends still paint, so a stored value
|
|
5428
|
+
* never loses its wash over it; `{ strict: true }` voids the gradient instead,
|
|
5429
|
+
* for the boundaries that must refuse the write.
|
|
5417
5430
|
*
|
|
5418
5431
|
* ONE validator for a value arriving by two routes — the theme token and the
|
|
5419
5432
|
* author's never-coerced per-instance `props.style` — so `set_theme` and the
|
|
@@ -5422,7 +5435,7 @@ const GRADIENT_HEX_RE = HEX_RE;
|
|
|
5422
5435
|
* hex grammar is also the injection guard — these stops are interpolated into
|
|
5423
5436
|
* CSS and into generated export source.
|
|
5424
5437
|
*/
|
|
5425
|
-
function normaliseComponentGradient(raw) {
|
|
5438
|
+
function normaliseComponentGradient(raw, options) {
|
|
5426
5439
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
5427
5440
|
const from = typeof raw.from === "string" ? raw.from.trim() : "";
|
|
5428
5441
|
const to = typeof raw.to === "string" ? raw.to.trim() : "";
|
|
@@ -5432,7 +5445,44 @@ function normaliseComponentGradient(raw) {
|
|
|
5432
5445
|
// land NaN in a style.
|
|
5433
5446
|
const angle = raw.angle;
|
|
5434
5447
|
const deg = Number.isFinite(angle) ? Math.round(angle) : defaultAngle;
|
|
5435
|
-
|
|
5448
|
+
const via = normaliseGradientVia(raw.via);
|
|
5449
|
+
if (!via && options && options.strict) return null;
|
|
5450
|
+
const gradient = { from, to, angle: ((deg % 360) + 360) % 360 };
|
|
5451
|
+
if (via && via.length > 0) gradient.via = via;
|
|
5452
|
+
return gradient;
|
|
5453
|
+
}
|
|
5454
|
+
|
|
5455
|
+
// All or nothing: one bad middle colour and none of them paint. `null` = malformed.
|
|
5456
|
+
function normaliseGradientVia(raw) {
|
|
5457
|
+
if (raw === undefined || raw === null) return [];
|
|
5458
|
+
if (!Array.isArray(raw) || raw.length > THEME_COMPONENT_GRADIENT.maxViaStops) {
|
|
5459
|
+
return null;
|
|
5460
|
+
}
|
|
5461
|
+
const via = [];
|
|
5462
|
+
for (const item of raw) {
|
|
5463
|
+
const stop = typeof item === "string" ? item.trim() : "";
|
|
5464
|
+
if (!GRADIENT_HEX_RE.test(stop)) return null;
|
|
5465
|
+
via.push(stop);
|
|
5466
|
+
}
|
|
5467
|
+
return via;
|
|
5468
|
+
}
|
|
5469
|
+
|
|
5470
|
+
/**
|
|
5471
|
+
* sc-10685 — a NORMALISED gradient's colours in paint order, evenly spaced:
|
|
5472
|
+
* `[from, ...via, to]`. The one place both hosts read the list from.
|
|
5473
|
+
*/
|
|
5474
|
+
function componentGradientColors(gradient) {
|
|
5475
|
+
return [gradient.from, ...(gradient.via || []), gradient.to];
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
/**
|
|
5479
|
+
* sc-10685 — the colour halfway along a NORMALISED gradient, which is what
|
|
5480
|
+
* text laid across it is contrasted against. Two colours: their mix, as before.
|
|
5481
|
+
*/
|
|
5482
|
+
function componentGradientMidpoint(gradient) {
|
|
5483
|
+
const colors = componentGradientColors(gradient);
|
|
5484
|
+
const middle = (colors.length - 1) / 2;
|
|
5485
|
+
return mixHex(colors[Math.floor(middle)], colors[Math.ceil(middle)], 0.5);
|
|
5436
5486
|
}
|
|
5437
5487
|
|
|
5438
5488
|
// REQ-THEME-LOOK: coerce a theme's spacing scale into the usable band. Shared
|
|
@@ -5475,6 +5525,8 @@ export {
|
|
|
5475
5525
|
INK_CONTRAST,
|
|
5476
5526
|
gradientAngleToVector,
|
|
5477
5527
|
normaliseComponentGradient,
|
|
5528
|
+
componentGradientColors,
|
|
5529
|
+
componentGradientMidpoint,
|
|
5478
5530
|
isThemeColorTokenRef,
|
|
5479
5531
|
parseThemeColorTokenRef,
|
|
5480
5532
|
formatThemeColorTokenRef,
|
package/dist/index.d.ts
CHANGED
|
@@ -3449,18 +3449,30 @@ export interface ComponentGradient {
|
|
|
3449
3449
|
from: string;
|
|
3450
3450
|
to: string;
|
|
3451
3451
|
angle: number;
|
|
3452
|
+
/** Middle colours between `from` and `to`. Present only when there are some. */
|
|
3453
|
+
via?: string[];
|
|
3452
3454
|
}
|
|
3453
3455
|
|
|
3454
3456
|
/**
|
|
3455
3457
|
* Normalise a component `gradient` style value to `{ from, to, angle }`, or null
|
|
3456
3458
|
* when it is unusable. Both stops are required; the angle wraps into 0-359.
|
|
3459
|
+
* An optional `via` list (at most `CONTRACT.themeComponentGradient.maxViaStops`
|
|
3460
|
+
* hex colours) is kept as given. A malformed one is dropped and the two ends
|
|
3461
|
+
* still paint; pass `{ strict: true }` to get null for it instead.
|
|
3457
3462
|
* Shared by the theme-token coercion and the widget render path, so the same
|
|
3458
3463
|
* value cannot resolve two ways.
|
|
3459
3464
|
*/
|
|
3460
3465
|
export function normaliseComponentGradient(
|
|
3461
3466
|
raw: unknown,
|
|
3467
|
+
options?: { strict?: boolean },
|
|
3462
3468
|
): ComponentGradient | null;
|
|
3463
3469
|
|
|
3470
|
+
/** A normalised gradient's colours in paint order: `[from, ...via, to]`. */
|
|
3471
|
+
export function componentGradientColors(gradient: ComponentGradient): string[];
|
|
3472
|
+
|
|
3473
|
+
/** The colour halfway along a normalised gradient: the basis for text ink. */
|
|
3474
|
+
export function componentGradientMidpoint(gradient: ComponentGradient): string;
|
|
3475
|
+
|
|
3464
3476
|
/** sc-8326 — the `theme:<path>` binding prefix, shared with the hosts' resolver. */
|
|
3465
3477
|
export const THEME_TOKEN_REF_PREFIX: string;
|
|
3466
3478
|
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/dist/theme-token-refs.js
CHANGED
|
@@ -148,7 +148,9 @@ function resolveAgainstSchema(schema, value, theme) {
|
|
|
148
148
|
// VALUES — colours, sizes, enums — so an allowlisted `theme:` string in it can
|
|
149
149
|
// only ever have been a binding. The depth cap keeps the walk bounded for the
|
|
150
150
|
// structured style values (a gradient's stops, per-corner radii).
|
|
151
|
-
|
|
151
|
+
// sc-10685: 4, so a widget gradient's `via` list (style > gradient > via >
|
|
152
|
+
// item) resolves like its two ends.
|
|
153
|
+
const MAX_STYLE_DEPTH = 4;
|
|
152
154
|
|
|
153
155
|
function resolveStyleBag(value, theme, depth) {
|
|
154
156
|
if (typeof value === "string") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.196.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
|
@@ -332,6 +332,8 @@ const THEME_COMPONENT_GRADIENT = Object.freeze({
|
|
|
332
332
|
angleMin: 0,
|
|
333
333
|
angleMax: 359,
|
|
334
334
|
defaultAngle: 180,
|
|
335
|
+
// sc-10685: middle colours allowed between the two ends (`via`).
|
|
336
|
+
maxViaStops: 3,
|
|
335
337
|
});
|
|
336
338
|
|
|
337
339
|
// REQ-THEME-LOOK: the app-wide SPACING MULTIPLIER. Layout spacing lives on the
|
|
@@ -5095,7 +5097,14 @@ const CONTRACT = deepFreeze({
|
|
|
5095
5097
|
// `update(userId, { name?, email? })`, gated on `users.write:*`; the
|
|
5096
5098
|
// linter requires that scope for a `.update(` call beside `useUsers(`.
|
|
5097
5099
|
// `DirectoryError` gains `reason`, the host's specific refusal code.
|
|
5098
|
-
|
|
5100
|
+
// 1.157.0: additive (sc-10685) — a component gradient takes an optional
|
|
5101
|
+
// `via` list of up to `themeComponentGradient.maxViaStops` middle colours
|
|
5102
|
+
// between `from` and `to`, evenly spaced. `normaliseComponentGradient`
|
|
5103
|
+
// returns it only when present, so a two-colour value normalises exactly
|
|
5104
|
+
// as before; a malformed `via` is dropped, or voids the gradient under the
|
|
5105
|
+
// new `{ strict: true }` option. New `componentGradientColors` / `componentGradientMidpoint`
|
|
5106
|
+
// read the paint list and the ink basis off a normalised gradient.
|
|
5107
|
+
version: "1.157.0",
|
|
5099
5108
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5100
5109
|
hooks: HOOKS,
|
|
5101
5110
|
primitives: PRIMITIVES,
|
|
@@ -5414,6 +5423,10 @@ const GRADIENT_HEX_RE = HEX_RE;
|
|
|
5414
5423
|
|
|
5415
5424
|
/**
|
|
5416
5425
|
* sc-3727 — normalise a component `gradient` to `{ from, to, angle }`, or `null`.
|
|
5426
|
+
* sc-10685: plus `via`, the middle colours, only when the value carries some.
|
|
5427
|
+
* A malformed `via` is dropped and the two ends still paint, so a stored value
|
|
5428
|
+
* never loses its wash over it; `{ strict: true }` voids the gradient instead,
|
|
5429
|
+
* for the boundaries that must refuse the write.
|
|
5417
5430
|
*
|
|
5418
5431
|
* ONE validator for a value arriving by two routes — the theme token and the
|
|
5419
5432
|
* author's never-coerced per-instance `props.style` — so `set_theme` and the
|
|
@@ -5422,7 +5435,7 @@ const GRADIENT_HEX_RE = HEX_RE;
|
|
|
5422
5435
|
* hex grammar is also the injection guard — these stops are interpolated into
|
|
5423
5436
|
* CSS and into generated export source.
|
|
5424
5437
|
*/
|
|
5425
|
-
function normaliseComponentGradient(raw) {
|
|
5438
|
+
function normaliseComponentGradient(raw, options) {
|
|
5426
5439
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
5427
5440
|
const from = typeof raw.from === "string" ? raw.from.trim() : "";
|
|
5428
5441
|
const to = typeof raw.to === "string" ? raw.to.trim() : "";
|
|
@@ -5432,7 +5445,44 @@ function normaliseComponentGradient(raw) {
|
|
|
5432
5445
|
// land NaN in a style.
|
|
5433
5446
|
const angle = raw.angle;
|
|
5434
5447
|
const deg = Number.isFinite(angle) ? Math.round(angle) : defaultAngle;
|
|
5435
|
-
|
|
5448
|
+
const via = normaliseGradientVia(raw.via);
|
|
5449
|
+
if (!via && options && options.strict) return null;
|
|
5450
|
+
const gradient = { from, to, angle: ((deg % 360) + 360) % 360 };
|
|
5451
|
+
if (via && via.length > 0) gradient.via = via;
|
|
5452
|
+
return gradient;
|
|
5453
|
+
}
|
|
5454
|
+
|
|
5455
|
+
// All or nothing: one bad middle colour and none of them paint. `null` = malformed.
|
|
5456
|
+
function normaliseGradientVia(raw) {
|
|
5457
|
+
if (raw === undefined || raw === null) return [];
|
|
5458
|
+
if (!Array.isArray(raw) || raw.length > THEME_COMPONENT_GRADIENT.maxViaStops) {
|
|
5459
|
+
return null;
|
|
5460
|
+
}
|
|
5461
|
+
const via = [];
|
|
5462
|
+
for (const item of raw) {
|
|
5463
|
+
const stop = typeof item === "string" ? item.trim() : "";
|
|
5464
|
+
if (!GRADIENT_HEX_RE.test(stop)) return null;
|
|
5465
|
+
via.push(stop);
|
|
5466
|
+
}
|
|
5467
|
+
return via;
|
|
5468
|
+
}
|
|
5469
|
+
|
|
5470
|
+
/**
|
|
5471
|
+
* sc-10685 — a NORMALISED gradient's colours in paint order, evenly spaced:
|
|
5472
|
+
* `[from, ...via, to]`. The one place both hosts read the list from.
|
|
5473
|
+
*/
|
|
5474
|
+
function componentGradientColors(gradient) {
|
|
5475
|
+
return [gradient.from, ...(gradient.via || []), gradient.to];
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
/**
|
|
5479
|
+
* sc-10685 — the colour halfway along a NORMALISED gradient, which is what
|
|
5480
|
+
* text laid across it is contrasted against. Two colours: their mix, as before.
|
|
5481
|
+
*/
|
|
5482
|
+
function componentGradientMidpoint(gradient) {
|
|
5483
|
+
const colors = componentGradientColors(gradient);
|
|
5484
|
+
const middle = (colors.length - 1) / 2;
|
|
5485
|
+
return mixHex(colors[Math.floor(middle)], colors[Math.ceil(middle)], 0.5);
|
|
5436
5486
|
}
|
|
5437
5487
|
|
|
5438
5488
|
// REQ-THEME-LOOK: coerce a theme's spacing scale into the usable band. Shared
|
|
@@ -5475,6 +5525,8 @@ export {
|
|
|
5475
5525
|
INK_CONTRAST,
|
|
5476
5526
|
gradientAngleToVector,
|
|
5477
5527
|
normaliseComponentGradient,
|
|
5528
|
+
componentGradientColors,
|
|
5529
|
+
componentGradientMidpoint,
|
|
5478
5530
|
isThemeColorTokenRef,
|
|
5479
5531
|
parseThemeColorTokenRef,
|
|
5480
5532
|
formatThemeColorTokenRef,
|