@colixsystems/widget-sdk 0.195.0 → 0.197.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 +18 -1
- package/dist/card-style.js +15 -4
- package/dist/contract.cjs +55 -3
- package/dist/contract.js +55 -3
- package/dist/host.d.ts +15 -3
- package/dist/index.d.ts +12 -0
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/navigation.cjs +58 -25
- package/dist/navigation.js +58 -25
- 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,24 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
77
77
|
|
|
78
78
|
## Status
|
|
79
79
|
|
|
80
|
-
`v0.
|
|
80
|
+
`v0.197.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.197.0 (contract 1.157.0)
|
|
83
|
+
|
|
84
|
+
**Host navigation: a page's sidebar menu can be opened from any page.** `resolveSidebarUnfold({ entries, active, sections })` used to answer only the section being viewed, so a host could draw a section's menu only while the visitor was inside it. The result now also carries `trees`: one `{ under, items, standalone, inside }` for every sidebar entry that carries a local menu, in menu order. `inside` is true for the section being viewed, which a host draws open; the others it draws folded behind the entry's arrow. `under`, `items` and `standalone` still describe the viewed section, so existing callers keep working. Widgets are unaffected: this is the host surface (`@colixsystems/widget-sdk/host`), and the widget contract version does not change.
|
|
85
|
+
|
|
86
|
+
### What's new in 0.196.0 (contract 1.157.0)
|
|
87
|
+
|
|
88
|
+
**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`:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
{ from: "#0f172a", via: ["#7c3aed", "#db2777"], to: "#f59e0b", angle: 135 }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- **`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.
|
|
95
|
+
- **`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.
|
|
96
|
+
- **`<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.
|
|
97
|
+
- **Fully additive.** Every stored two-colour gradient renders byte-identically.
|
|
81
98
|
|
|
82
99
|
### What's new in 0.195.0 (contract 1.156.0)
|
|
83
100
|
|
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/host.d.ts
CHANGED
|
@@ -254,14 +254,26 @@ export interface SidebarUnfoldItem {
|
|
|
254
254
|
[field: string]: unknown;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/** One section's local menu and the sidebar entry it hangs beneath. `inside`
|
|
258
|
+
* marks the section the visitor is in — the one that starts unfolded. */
|
|
259
|
+
export interface SidebarUnfoldTree {
|
|
260
|
+
under: string;
|
|
261
|
+
items: Array<SidebarUnfoldItem & { current: boolean }>;
|
|
262
|
+
standalone: boolean;
|
|
263
|
+
inside: boolean;
|
|
264
|
+
}
|
|
265
|
+
|
|
257
266
|
/** Where a page's `SIDEBAR` local menu unfolds (REQ-NAV-LOCAL, sc-7696).
|
|
258
|
-
* `under` is the global menu entry the
|
|
259
|
-
*
|
|
260
|
-
* list the page, so the page itself is the entry.
|
|
267
|
+
* `under` is the global menu entry the visitor's own section hangs beneath,
|
|
268
|
+
* or null when they are in none. `standalone` marks the case where the global
|
|
269
|
+
* menu does not list the page, so the page itself is the entry. `trees` is
|
|
270
|
+
* every section an entry carries, in menu order, so an entry can offer its
|
|
271
|
+
* menu from any page; a `standalone` tree comes last. */
|
|
261
272
|
export interface SidebarUnfold {
|
|
262
273
|
under: string | null;
|
|
263
274
|
items: Array<SidebarUnfoldItem & { current: boolean }>;
|
|
264
275
|
standalone: boolean;
|
|
276
|
+
trees: SidebarUnfoldTree[];
|
|
265
277
|
}
|
|
266
278
|
|
|
267
279
|
/** Resolve where the viewed page's local menu unfolds in the sidebar and the
|
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/navigation.cjs
CHANGED
|
@@ -636,21 +636,25 @@ function resolveTopBarTokens(theme) {
|
|
|
636
636
|
* inside it, so the menu is never lost — a section off the menu is exactly
|
|
637
637
|
* the case that would otherwise render blank.
|
|
638
638
|
*
|
|
639
|
+
* sc-10693: `trees` answers EVERY section, not only the visitor's own, so a
|
|
640
|
+
* section's entry can offer its menu from any page. `under` / `items` /
|
|
641
|
+
* `standalone` still name the one the visitor is inside.
|
|
642
|
+
*
|
|
639
643
|
* @param {{ entries?: unknown, active?: unknown, sections?: unknown }} input
|
|
640
644
|
* `entries` — the global menu's keys, in menu order.
|
|
641
645
|
* `active` — the key of the page being viewed.
|
|
642
646
|
* `sections` — `{ [key]: { menu, items } }` for pages whose local nav is
|
|
643
647
|
* `SIDEBAR`; `menu` identifies the saved menu, `items` carry their own key.
|
|
644
|
-
* @returns {{ under: string|null, items: Array<object>, standalone: boolean }}
|
|
645
|
-
* `under` is null when
|
|
646
|
-
* section's own page, which the host draws as the entry. Each
|
|
647
|
-
* returned with a `current` flag rather than leaving each host to
|
|
648
|
-
* recompute it.
|
|
648
|
+
* @returns {{ under: string|null, items: Array<object>, standalone: boolean, trees: Array<object> }}
|
|
649
|
+
* `under` is null when the visitor is in no section; when `standalone` it
|
|
650
|
+
* names the section's own page, which the host draws as the entry. Each
|
|
651
|
+
* item is returned with a `current` flag rather than leaving each host to
|
|
652
|
+
* recompute it. `trees` lists `{ under, items, standalone, inside }` in menu
|
|
653
|
+
* order, a `standalone` one last; `inside` marks the visitor's own.
|
|
649
654
|
*/
|
|
650
655
|
function resolveSidebarUnfold(input) {
|
|
651
656
|
const { entries, active, sections } = isPlainObject(input) ? input : {};
|
|
652
|
-
const
|
|
653
|
-
if (typeof active !== "string" || !active) return nothing;
|
|
657
|
+
const viewed = typeof active === "string" && active ? active : null;
|
|
654
658
|
const bank = isPlainObject(sections) ? sections : {};
|
|
655
659
|
const order = Array.isArray(entries) ? entries : [];
|
|
656
660
|
const sectionAt = (key) => {
|
|
@@ -663,31 +667,60 @@ function resolveSidebarUnfold(input) {
|
|
|
663
667
|
return (
|
|
664
668
|
Boolean(candidate) &&
|
|
665
669
|
candidate.items.some(
|
|
666
|
-
(item) => isPlainObject(item) && item.key ===
|
|
670
|
+
(item) => isPlainObject(item) && item.key === viewed,
|
|
667
671
|
)
|
|
668
672
|
);
|
|
669
673
|
};
|
|
670
674
|
// The page's own section wins; otherwise the section whose menu lists it,
|
|
671
675
|
// preferring one a global entry carries so the tree hangs where it always
|
|
672
676
|
// does. Both are the same section from the visitor's side.
|
|
673
|
-
const section =
|
|
674
|
-
sectionAt(
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
677
|
+
const section = viewed
|
|
678
|
+
? sectionAt(viewed) ||
|
|
679
|
+
sectionAt(order.find(lists)) ||
|
|
680
|
+
sectionAt(Object.keys(bank).find(lists))
|
|
681
|
+
: null;
|
|
682
|
+
const mark = (items) =>
|
|
683
|
+
items.map((item) => ({
|
|
684
|
+
...(isPlainObject(item) ? item : {}),
|
|
685
|
+
current:
|
|
686
|
+
viewed !== null &&
|
|
687
|
+
isPlainObject(item) &&
|
|
688
|
+
typeof item.key === "string" &&
|
|
689
|
+
item.key === viewed,
|
|
690
|
+
}));
|
|
691
|
+
// A saved menu hangs under the FIRST global entry carrying it, so two
|
|
692
|
+
// entries sharing one menu never draw it twice.
|
|
693
|
+
const claimed = [];
|
|
694
|
+
const trees = [];
|
|
695
|
+
for (const key of order) {
|
|
696
|
+
if (!isPlainObject(bank[key])) continue;
|
|
697
|
+
const { menu } = bank[key];
|
|
698
|
+
if (claimed.includes(menu)) continue;
|
|
699
|
+
claimed.push(menu);
|
|
700
|
+
// An unusable key still owns its menu: nothing may hang it elsewhere.
|
|
701
|
+
if (typeof key !== "string" || !key) continue;
|
|
702
|
+
const inside = Boolean(section) && menu === section.menu;
|
|
703
|
+
const own = inside ? section : sectionAt(key);
|
|
704
|
+
if (!own) continue;
|
|
705
|
+
trees.push({ under: key, items: mark(own.items), standalone: false, inside });
|
|
706
|
+
}
|
|
707
|
+
if (!section) return { under: null, items: [], standalone: false, trees };
|
|
708
|
+
const owned = trees.find((tree) => tree.inside);
|
|
709
|
+
if (owned) {
|
|
710
|
+
return { under: owned.under, items: owned.items, standalone: false, trees };
|
|
689
711
|
}
|
|
690
|
-
|
|
712
|
+
const alone = {
|
|
713
|
+
under: section.key,
|
|
714
|
+
items: mark(section.items),
|
|
715
|
+
standalone: true,
|
|
716
|
+
inside: true,
|
|
717
|
+
};
|
|
718
|
+
return {
|
|
719
|
+
under: alone.under,
|
|
720
|
+
items: alone.items,
|
|
721
|
+
standalone: true,
|
|
722
|
+
trees: [...trees, alone],
|
|
723
|
+
};
|
|
691
724
|
}
|
|
692
725
|
|
|
693
726
|
/**
|
package/dist/navigation.js
CHANGED
|
@@ -627,21 +627,25 @@ export function resolveTopBarTokens(theme) {
|
|
|
627
627
|
* inside it, so the menu is never lost — a section off the menu is exactly
|
|
628
628
|
* the case that would otherwise render blank.
|
|
629
629
|
*
|
|
630
|
+
* sc-10693: `trees` answers EVERY section, not only the visitor's own, so a
|
|
631
|
+
* section's entry can offer its menu from any page. `under` / `items` /
|
|
632
|
+
* `standalone` still name the one the visitor is inside.
|
|
633
|
+
*
|
|
630
634
|
* @param {{ entries?: unknown, active?: unknown, sections?: unknown }} input
|
|
631
635
|
* `entries` — the global menu's keys, in menu order.
|
|
632
636
|
* `active` — the key of the page being viewed.
|
|
633
637
|
* `sections` — `{ [key]: { menu, items } }` for pages whose local nav is
|
|
634
638
|
* `SIDEBAR`; `menu` identifies the saved menu, `items` carry their own key.
|
|
635
|
-
* @returns {{ under: string|null, items: Array<object>, standalone: boolean }}
|
|
636
|
-
* `under` is null when
|
|
637
|
-
* section's own page, which the host draws as the entry. Each
|
|
638
|
-
* returned with a `current` flag rather than leaving each host to
|
|
639
|
-
* recompute it.
|
|
639
|
+
* @returns {{ under: string|null, items: Array<object>, standalone: boolean, trees: Array<object> }}
|
|
640
|
+
* `under` is null when the visitor is in no section; when `standalone` it
|
|
641
|
+
* names the section's own page, which the host draws as the entry. Each
|
|
642
|
+
* item is returned with a `current` flag rather than leaving each host to
|
|
643
|
+
* recompute it. `trees` lists `{ under, items, standalone, inside }` in menu
|
|
644
|
+
* order, a `standalone` one last; `inside` marks the visitor's own.
|
|
640
645
|
*/
|
|
641
646
|
export function resolveSidebarUnfold(input) {
|
|
642
647
|
const { entries, active, sections } = isPlainObject(input) ? input : {};
|
|
643
|
-
const
|
|
644
|
-
if (typeof active !== "string" || !active) return nothing;
|
|
648
|
+
const viewed = typeof active === "string" && active ? active : null;
|
|
645
649
|
const bank = isPlainObject(sections) ? sections : {};
|
|
646
650
|
const order = Array.isArray(entries) ? entries : [];
|
|
647
651
|
const sectionAt = (key) => {
|
|
@@ -654,31 +658,60 @@ export function resolveSidebarUnfold(input) {
|
|
|
654
658
|
return (
|
|
655
659
|
Boolean(candidate) &&
|
|
656
660
|
candidate.items.some(
|
|
657
|
-
(item) => isPlainObject(item) && item.key ===
|
|
661
|
+
(item) => isPlainObject(item) && item.key === viewed,
|
|
658
662
|
)
|
|
659
663
|
);
|
|
660
664
|
};
|
|
661
665
|
// The page's own section wins; otherwise the section whose menu lists it,
|
|
662
666
|
// preferring one a global entry carries so the tree hangs where it always
|
|
663
667
|
// does. Both are the same section from the visitor's side.
|
|
664
|
-
const section =
|
|
665
|
-
sectionAt(
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
668
|
+
const section = viewed
|
|
669
|
+
? sectionAt(viewed) ||
|
|
670
|
+
sectionAt(order.find(lists)) ||
|
|
671
|
+
sectionAt(Object.keys(bank).find(lists))
|
|
672
|
+
: null;
|
|
673
|
+
const mark = (items) =>
|
|
674
|
+
items.map((item) => ({
|
|
675
|
+
...(isPlainObject(item) ? item : {}),
|
|
676
|
+
current:
|
|
677
|
+
viewed !== null &&
|
|
678
|
+
isPlainObject(item) &&
|
|
679
|
+
typeof item.key === "string" &&
|
|
680
|
+
item.key === viewed,
|
|
681
|
+
}));
|
|
682
|
+
// A saved menu hangs under the FIRST global entry carrying it, so two
|
|
683
|
+
// entries sharing one menu never draw it twice.
|
|
684
|
+
const claimed = [];
|
|
685
|
+
const trees = [];
|
|
686
|
+
for (const key of order) {
|
|
687
|
+
if (!isPlainObject(bank[key])) continue;
|
|
688
|
+
const { menu } = bank[key];
|
|
689
|
+
if (claimed.includes(menu)) continue;
|
|
690
|
+
claimed.push(menu);
|
|
691
|
+
// An unusable key still owns its menu: nothing may hang it elsewhere.
|
|
692
|
+
if (typeof key !== "string" || !key) continue;
|
|
693
|
+
const inside = Boolean(section) && menu === section.menu;
|
|
694
|
+
const own = inside ? section : sectionAt(key);
|
|
695
|
+
if (!own) continue;
|
|
696
|
+
trees.push({ under: key, items: mark(own.items), standalone: false, inside });
|
|
697
|
+
}
|
|
698
|
+
if (!section) return { under: null, items: [], standalone: false, trees };
|
|
699
|
+
const owned = trees.find((tree) => tree.inside);
|
|
700
|
+
if (owned) {
|
|
701
|
+
return { under: owned.under, items: owned.items, standalone: false, trees };
|
|
680
702
|
}
|
|
681
|
-
|
|
703
|
+
const alone = {
|
|
704
|
+
under: section.key,
|
|
705
|
+
items: mark(section.items),
|
|
706
|
+
standalone: true,
|
|
707
|
+
inside: true,
|
|
708
|
+
};
|
|
709
|
+
return {
|
|
710
|
+
under: alone.under,
|
|
711
|
+
items: alone.items,
|
|
712
|
+
standalone: true,
|
|
713
|
+
trees: [...trees, alone],
|
|
714
|
+
};
|
|
682
715
|
}
|
|
683
716
|
|
|
684
717
|
/**
|
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.197.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,
|