@colixsystems/widget-sdk 0.157.0 → 0.159.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 +7 -1
- package/dist/host.d.ts +12 -1
- package/dist/navigation.cjs +60 -17
- package/dist/navigation.js +60 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,7 +119,7 @@ Behaviour: a bare date given to a `mode="datetime"` picker now shows local midni
|
|
|
119
119
|
|
|
120
120
|
**The app header's row is arrangeable, and `./host` publishes the resolver both hosts read (sc-8023).** The row runs across the top of the content outside the side panel, and is drawn three times — as the phone header where the panel is a drawer, as the desktop strip, and as the full-width bar when one is switched on. Each surface hard-coded its own copy of it, so an author could colour the row but never move anything in it.
|
|
121
121
|
|
|
122
|
-
- **`resolveTopStripeLayout(theme)`** reads `theme.sidebar.stripe` and returns the arrangement fully resolved: `slots` (already filtered and sequenced, so no host re-derives it), `order`, the box (`height`, `paddingX`, `contentOffsetY`), the resolved `backgroundColor` (joined in 0.155.0 by `tintColor` and `titleColor`), and `menuSide` — which edge the drawer must arrive from, since the brand grows and a burger ordered after it sits at the trailing edge. The web Player picks the drawer's side with it; the export makes it `drawerPosition`.
|
|
122
|
+
- **`resolveTopStripeLayout(theme)`** reads `theme.sidebar.stripe` and returns the arrangement fully resolved: `slots` (already filtered and sequenced, so no host re-derives it), `order`, the box (`height`, `paddingX`, `contentOffsetY`, and `barHeight` — the height once the nudge is held, since pushing the contents down grows the bar rather than letting them fall out of it), the resolved `backgroundColor` (joined in 0.155.0 by `tintColor` and `titleColor`), and `menuSide` — which edge the drawer must arrive from, since the brand grows and a burger ordered after it sits at the trailing edge. The web Player picks the drawer's side with it; the export makes it `drawerPosition`.
|
|
123
123
|
- **`TOP_STRIPE_SLOTS`** is the vocabulary (`back`, `menu`, `brand`, `notifications`) and **`TOP_STRIPE_ALWAYS_SHOWN_SLOTS`** the slots that move but never hide. The burger is the only member: it is drawn only where the panel is a drawer, and there it is the sole route into the menu, so hiding it would strand the visitor. It keeps its place in `order` regardless.
|
|
124
124
|
- **`topStripeShowKey(slot)`** derives the `theme_config` key that shows or hides one slot, in one place, so a control, the write path and the resolver cannot spell it differently.
|
|
125
125
|
- **The bounds travel with the resolver** — `MIN`/`MAX_TOP_STRIPE_HEIGHT_PX`, `MIN`/`MAX_TOP_STRIPE_PADDING_X_PX`, `MAX_TOP_STRIPE_OFFSET_Y_PX` — so a control offers exactly the range the resolver clamps to. `DEFAULT_TOP_STRIPE_HEIGHT_PX` / `DEFAULT_TOP_STRIPE_PADDING_X_PX` are what the DESKTOP row measures with nothing stored, published so a control can show the number an author is departing from; the resolver still answers `null`, because each surface keeps its own box until one is named.
|
|
@@ -128,6 +128,12 @@ Additive — nothing in the existing `./host` surface changed, and the contract
|
|
|
128
128
|
|
|
129
129
|
### What's new in 0.151.0 (contract 1.118.0 — unchanged)
|
|
130
130
|
|
|
131
|
+
**`resolveSidebarTokens` answers `activeInk` — the rail's active colour made readable as TEXT (sc-8655, REQ-NAV-LOCAL / REQ-THEME-14).** `activeColor` is authored as a SURFACE: the filled style paints it and forces a white label, so nothing in the system ever pressured it to be legible as ink. Every place that paints it as text inherited that — the accent style's active label, and now the entry a folded sidebar section hands its marking to — and the brand default reads 3.5:1 on the rail, below the 4.5:1 AA asks of normal text and worse than the unmarked rows around it.
|
|
132
|
+
|
|
133
|
+
- `activeInk` is the same hue stepped toward the rail's own polarity (darker on a light rail, lighter on a dark one) only as far as AA needs, and is `activeColor` unchanged whenever that already reads. Derived, never authored: it moves with whatever colour the workspace sets, so there is no second control to keep in step.
|
|
134
|
+
- **Additive.** `activeColor` still names the surface the filled style paints and the colour a non-text mark takes — a dot or an accent edge answers to 3:1, not 4.5:1, so those keep the author's actual colour. `CONTRACT` is untouched (`version` stays `1.122.0`): a host resolver, not a widget-facing surface.
|
|
135
|
+
- Both hosts read it, so the Player, the exported drawer and the Design page's chrome preview cannot paint three different labels off one theme.
|
|
136
|
+
|
|
131
137
|
**`resolveSidebarUnfold` joins the host navigation surface: where a page's local menu unfolds inside the app's own sidebar (sc-7696, REQ-NAV-LOCAL).** A page's local navigation could only be a bar, above or below its content. `local_nav` now also takes `SIDEBAR`, which takes no strip of the page: the saved menu unfolds in the rail and the drawer, under the global menu entry it belongs to.
|
|
132
138
|
|
|
133
139
|
- `resolveSidebarUnfold({ entries, active, sections })` answers `{ under, items, standalone }` from host-neutral keys — page slugs on web, screen names in the export — so it is the ONE derivation both chromes read. Web calls it per render; the compiler asks it once per screen at build time, because the only runtime input is which screen is showing.
|
package/dist/host.d.ts
CHANGED
|
@@ -208,7 +208,14 @@ export function resolveFooterTokens(theme: unknown): FooterTokens;
|
|
|
208
208
|
export interface SidebarTokens {
|
|
209
209
|
backgroundColor: string;
|
|
210
210
|
textColor: string;
|
|
211
|
+
/** The active colour as a SURFACE — what `filled` paints, what a non-text
|
|
212
|
+
* mark (an accent edge, a folded section's dot) is drawn in. */
|
|
211
213
|
activeColor: string;
|
|
214
|
+
/** sc-8655: the same colour as INK. `activeColor` is authored to sit under a
|
|
215
|
+
* white label, so it can fall below AA as text on the rail; this is it
|
|
216
|
+
* stepped toward the rail's own polarity until it clears 4.5:1, and equal to
|
|
217
|
+
* `activeColor` whenever that already reads. Derived, never authored. */
|
|
218
|
+
activeInk: string;
|
|
212
219
|
activeStyle: "filled" | "accent";
|
|
213
220
|
borderColor: string | null;
|
|
214
221
|
borderWidth: number | null;
|
|
@@ -332,8 +339,12 @@ export interface TopStripeLayout {
|
|
|
332
339
|
* bar's 64px, the phone header's content-sized row. */
|
|
333
340
|
height: number | null;
|
|
334
341
|
paddingX: number | null;
|
|
335
|
-
/** How far the row
|
|
342
|
+
/** How far the row's contents are pushed down, 0-60px. */
|
|
336
343
|
contentOffsetY: number;
|
|
344
|
+
/** The bar's total height once the nudge is held — its box plus
|
|
345
|
+
* `contentOffsetY`, so contents never fall out of the bottom. Null only
|
|
346
|
+
* while both are unset, where each surface keeps its own box. */
|
|
347
|
+
barHeight: number | null;
|
|
337
348
|
/** Every slot in the author's sequence, hidden ones included. */
|
|
338
349
|
order: TopStripeSlot[];
|
|
339
350
|
/** What the bar actually SHOWS, already filtered and sequenced. A surface
|
package/dist/navigation.cjs
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// the SAME literal, so what a planner may persist and what a host draws cannot
|
|
26
26
|
// diverge.
|
|
27
27
|
|
|
28
|
-
const { CONTRACT, isHexColor } = require("./contract.cjs");
|
|
28
|
+
const { CONTRACT, isHexColor, contrastRatio, mixHex, readableTextColor } = require("./contract.cjs");
|
|
29
29
|
|
|
30
30
|
// Absent or unknown resolves here, so every app authored before menu types
|
|
31
31
|
// existed renders and compiles byte-identically.
|
|
@@ -267,14 +267,46 @@ function brandPrimary(config) {
|
|
|
267
267
|
* @returns {{ backgroundColor: string, textColor: string, activeColor: string,
|
|
268
268
|
* activeStyle: string, borderColor: string|null, borderWidth: number|null }}
|
|
269
269
|
*/
|
|
270
|
+
// sc-8655: the sidebar's active colour is authored to sit UNDER a white label
|
|
271
|
+
// (the filled style), so nothing ever pressured it to read as INK — the brand
|
|
272
|
+
// default lands at 3.5:1 on the rail, below the 4.5:1 AA asks of normal text,
|
|
273
|
+
// and worse than the inactive rows beside it. Every surface that paints the
|
|
274
|
+
// active colour as TEXT reads the derived ink instead: the same hue, stepped
|
|
275
|
+
// toward the rail's own polarity only as far as legibility needs.
|
|
276
|
+
const AA_TEXT_CONTRAST = 4.5;
|
|
277
|
+
const INK_STEP = 0.05;
|
|
278
|
+
const INK_MAX_SHIFT = 0.7;
|
|
279
|
+
|
|
280
|
+
function readableInk(color, background) {
|
|
281
|
+
if (!isHexColor(color) || !isHexColor(background)) return color;
|
|
282
|
+
if (contrastRatio(color, background) >= AA_TEXT_CONTRAST) return color;
|
|
283
|
+
// Darken on a light rail, lighten on a dark one — the direction that keeps
|
|
284
|
+
// the hue rather than washing it toward grey.
|
|
285
|
+
const toward = readableTextColor(background, "#000000", "#ffffff");
|
|
286
|
+
let ink = color;
|
|
287
|
+
for (let t = INK_STEP; t <= INK_MAX_SHIFT + 1e-9; t += INK_STEP) {
|
|
288
|
+
ink = mixHex(color, toward, t);
|
|
289
|
+
if (contrastRatio(ink, background) >= AA_TEXT_CONTRAST) break;
|
|
290
|
+
}
|
|
291
|
+
// A colour that cannot reach AA within the shift keeps the closest it got:
|
|
292
|
+
// the most readable answer available beats reverting to the unreadable one.
|
|
293
|
+
return ink;
|
|
294
|
+
}
|
|
295
|
+
|
|
270
296
|
function resolveSidebarTokens(theme) {
|
|
271
297
|
const config = isPlainObject(theme) ? theme : {};
|
|
272
298
|
const sidebar = isPlainObject(config.sidebar) ? config.sidebar : {};
|
|
273
299
|
const borderColor = hexOrNull(sidebar.borderColor);
|
|
300
|
+
const backgroundColor = hexOr(sidebar.backgroundColor, chromeSurface(config));
|
|
301
|
+
const activeColor = hexOr(sidebar.activeColor, brandPrimary(config));
|
|
274
302
|
return {
|
|
275
|
-
backgroundColor
|
|
303
|
+
backgroundColor,
|
|
276
304
|
textColor: hexOr(sidebar.textColor, DEFAULT_CHROME_TEXT),
|
|
277
|
-
activeColor
|
|
305
|
+
activeColor,
|
|
306
|
+
// sc-8655: `activeColor` where it is a SURFACE, `activeInk` where it is
|
|
307
|
+
// text on the rail. Both hosts read the second for an accent-marked label
|
|
308
|
+
// and for a folded section's entry.
|
|
309
|
+
activeInk: readableInk(activeColor, backgroundColor),
|
|
278
310
|
activeStyle: sidebar.activeStyle === "accent" ? "accent" : "filled",
|
|
279
311
|
// REQ-THEME-LOOK: the colour is the switch — no colour, no line.
|
|
280
312
|
borderColor,
|
|
@@ -405,6 +437,7 @@ function topStripeOrder(value) {
|
|
|
405
437
|
* @param {unknown} theme — the whole `theme_config`.
|
|
406
438
|
* @returns {{ backgroundColor: string, tintColor: string, titleColor: string,
|
|
407
439
|
* height: number|null, paddingX: number|null, contentOffsetY: number,
|
|
440
|
+
* barHeight: number|null,
|
|
408
441
|
* order: string[], slots: string[], menuSide: "start"|"end" }}
|
|
409
442
|
*/
|
|
410
443
|
function resolveTopStripeLayout(theme) {
|
|
@@ -422,30 +455,40 @@ function resolveTopStripeLayout(theme) {
|
|
|
422
455
|
: TOP_STRIPE_SLOT_SHOWN_BY_DEFAULT[slot];
|
|
423
456
|
};
|
|
424
457
|
const slots = order.filter(shown);
|
|
458
|
+
// Unset defers to each surface's own box — the desktop bar's fixed 64px, the
|
|
459
|
+
// phone header's content-sized row.
|
|
460
|
+
const height = topStripeClampedOr(
|
|
461
|
+
stripe.height,
|
|
462
|
+
MIN_TOP_STRIPE_HEIGHT_PX,
|
|
463
|
+
MAX_TOP_STRIPE_HEIGHT_PX,
|
|
464
|
+
null,
|
|
465
|
+
);
|
|
466
|
+
const contentOffsetY = topStripeClampedOr(
|
|
467
|
+
stripe.contentOffsetY,
|
|
468
|
+
0,
|
|
469
|
+
MAX_TOP_STRIPE_OFFSET_Y_PX,
|
|
470
|
+
0,
|
|
471
|
+
);
|
|
425
472
|
return {
|
|
426
473
|
// There is no alignment to resolve: the brand slot GROWS, so the row has no
|
|
427
474
|
// free space for `justify-content` to distribute and every value looked
|
|
428
475
|
// identical. The row spreads because the brand takes the slack.
|
|
429
|
-
|
|
430
|
-
// the phone header's content-sized row.
|
|
431
|
-
height: topStripeClampedOr(
|
|
432
|
-
stripe.height,
|
|
433
|
-
MIN_TOP_STRIPE_HEIGHT_PX,
|
|
434
|
-
MAX_TOP_STRIPE_HEIGHT_PX,
|
|
435
|
-
null,
|
|
436
|
-
),
|
|
476
|
+
height,
|
|
437
477
|
paddingX: topStripeClampedOr(
|
|
438
478
|
stripe.paddingX,
|
|
439
479
|
MIN_TOP_STRIPE_PADDING_X_PX,
|
|
440
480
|
MAX_TOP_STRIPE_PADDING_X_PX,
|
|
441
481
|
null,
|
|
442
482
|
),
|
|
443
|
-
contentOffsetY
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
483
|
+
contentOffsetY,
|
|
484
|
+
// sc-8552: the bar GROWS by the nudge rather than letting its contents fall
|
|
485
|
+
// out of the bottom — the box the author sized, plus what they pushed into
|
|
486
|
+
// it. For a surface that must state a height (the desktop bar, the native
|
|
487
|
+
// header); the phone row is content-sized and grows on its padding alone.
|
|
488
|
+
barHeight:
|
|
489
|
+
height === null && contentOffsetY === 0
|
|
490
|
+
? null
|
|
491
|
+
: (height ?? DEFAULT_TOP_STRIPE_HEIGHT_PX) + contentOffsetY,
|
|
449
492
|
// sc-8023: the stripe's OWN surface, inheriting the app-header colour when
|
|
450
493
|
// unset — so an untouched app is unmoved, and an author who dresses a
|
|
451
494
|
// page's top navigation bar no longer repaints the stripe above the panel by
|
package/dist/navigation.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// the SAME literal, so what a planner may persist and what a host draws cannot
|
|
17
17
|
// diverge.
|
|
18
18
|
|
|
19
|
-
import { CONTRACT, isHexColor } from "./contract.js";
|
|
19
|
+
import { CONTRACT, isHexColor, contrastRatio, mixHex, readableTextColor } from "./contract.js";
|
|
20
20
|
|
|
21
21
|
// Absent or unknown resolves here, so every app authored before menu types
|
|
22
22
|
// existed renders and compiles byte-identically.
|
|
@@ -258,14 +258,46 @@ function brandPrimary(config) {
|
|
|
258
258
|
* @returns {{ backgroundColor: string, textColor: string, activeColor: string,
|
|
259
259
|
* activeStyle: string, borderColor: string|null, borderWidth: number|null }}
|
|
260
260
|
*/
|
|
261
|
+
// sc-8655: the sidebar's active colour is authored to sit UNDER a white label
|
|
262
|
+
// (the filled style), so nothing ever pressured it to read as INK — the brand
|
|
263
|
+
// default lands at 3.5:1 on the rail, below the 4.5:1 AA asks of normal text,
|
|
264
|
+
// and worse than the inactive rows beside it. Every surface that paints the
|
|
265
|
+
// active colour as TEXT reads the derived ink instead: the same hue, stepped
|
|
266
|
+
// toward the rail's own polarity only as far as legibility needs.
|
|
267
|
+
const AA_TEXT_CONTRAST = 4.5;
|
|
268
|
+
const INK_STEP = 0.05;
|
|
269
|
+
const INK_MAX_SHIFT = 0.7;
|
|
270
|
+
|
|
271
|
+
function readableInk(color, background) {
|
|
272
|
+
if (!isHexColor(color) || !isHexColor(background)) return color;
|
|
273
|
+
if (contrastRatio(color, background) >= AA_TEXT_CONTRAST) return color;
|
|
274
|
+
// Darken on a light rail, lighten on a dark one — the direction that keeps
|
|
275
|
+
// the hue rather than washing it toward grey.
|
|
276
|
+
const toward = readableTextColor(background, "#000000", "#ffffff");
|
|
277
|
+
let ink = color;
|
|
278
|
+
for (let t = INK_STEP; t <= INK_MAX_SHIFT + 1e-9; t += INK_STEP) {
|
|
279
|
+
ink = mixHex(color, toward, t);
|
|
280
|
+
if (contrastRatio(ink, background) >= AA_TEXT_CONTRAST) break;
|
|
281
|
+
}
|
|
282
|
+
// A colour that cannot reach AA within the shift keeps the closest it got:
|
|
283
|
+
// the most readable answer available beats reverting to the unreadable one.
|
|
284
|
+
return ink;
|
|
285
|
+
}
|
|
286
|
+
|
|
261
287
|
export function resolveSidebarTokens(theme) {
|
|
262
288
|
const config = isPlainObject(theme) ? theme : {};
|
|
263
289
|
const sidebar = isPlainObject(config.sidebar) ? config.sidebar : {};
|
|
264
290
|
const borderColor = hexOrNull(sidebar.borderColor);
|
|
291
|
+
const backgroundColor = hexOr(sidebar.backgroundColor, chromeSurface(config));
|
|
292
|
+
const activeColor = hexOr(sidebar.activeColor, brandPrimary(config));
|
|
265
293
|
return {
|
|
266
|
-
backgroundColor
|
|
294
|
+
backgroundColor,
|
|
267
295
|
textColor: hexOr(sidebar.textColor, DEFAULT_CHROME_TEXT),
|
|
268
|
-
activeColor
|
|
296
|
+
activeColor,
|
|
297
|
+
// sc-8655: `activeColor` where it is a SURFACE, `activeInk` where it is
|
|
298
|
+
// text on the rail. Both hosts read the second for an accent-marked label
|
|
299
|
+
// and for a folded section's entry.
|
|
300
|
+
activeInk: readableInk(activeColor, backgroundColor),
|
|
269
301
|
activeStyle: sidebar.activeStyle === "accent" ? "accent" : "filled",
|
|
270
302
|
// REQ-THEME-LOOK: the colour is the switch — no colour, no line.
|
|
271
303
|
borderColor,
|
|
@@ -396,6 +428,7 @@ function topStripeOrder(value) {
|
|
|
396
428
|
* @param {unknown} theme — the whole `theme_config`.
|
|
397
429
|
* @returns {{ backgroundColor: string, tintColor: string, titleColor: string,
|
|
398
430
|
* height: number|null, paddingX: number|null, contentOffsetY: number,
|
|
431
|
+
* barHeight: number|null,
|
|
399
432
|
* order: string[], slots: string[], menuSide: "start"|"end" }}
|
|
400
433
|
*/
|
|
401
434
|
export function resolveTopStripeLayout(theme) {
|
|
@@ -413,30 +446,40 @@ export function resolveTopStripeLayout(theme) {
|
|
|
413
446
|
: TOP_STRIPE_SLOT_SHOWN_BY_DEFAULT[slot];
|
|
414
447
|
};
|
|
415
448
|
const slots = order.filter(shown);
|
|
449
|
+
// Unset defers to each surface's own box — the desktop bar's fixed 64px, the
|
|
450
|
+
// phone header's content-sized row.
|
|
451
|
+
const height = topStripeClampedOr(
|
|
452
|
+
stripe.height,
|
|
453
|
+
MIN_TOP_STRIPE_HEIGHT_PX,
|
|
454
|
+
MAX_TOP_STRIPE_HEIGHT_PX,
|
|
455
|
+
null,
|
|
456
|
+
);
|
|
457
|
+
const contentOffsetY = topStripeClampedOr(
|
|
458
|
+
stripe.contentOffsetY,
|
|
459
|
+
0,
|
|
460
|
+
MAX_TOP_STRIPE_OFFSET_Y_PX,
|
|
461
|
+
0,
|
|
462
|
+
);
|
|
416
463
|
return {
|
|
417
464
|
// There is no alignment to resolve: the brand slot GROWS, so the row has no
|
|
418
465
|
// free space for `justify-content` to distribute and every value looked
|
|
419
466
|
// identical. The row spreads because the brand takes the slack.
|
|
420
|
-
|
|
421
|
-
// the phone header's content-sized row.
|
|
422
|
-
height: topStripeClampedOr(
|
|
423
|
-
stripe.height,
|
|
424
|
-
MIN_TOP_STRIPE_HEIGHT_PX,
|
|
425
|
-
MAX_TOP_STRIPE_HEIGHT_PX,
|
|
426
|
-
null,
|
|
427
|
-
),
|
|
467
|
+
height,
|
|
428
468
|
paddingX: topStripeClampedOr(
|
|
429
469
|
stripe.paddingX,
|
|
430
470
|
MIN_TOP_STRIPE_PADDING_X_PX,
|
|
431
471
|
MAX_TOP_STRIPE_PADDING_X_PX,
|
|
432
472
|
null,
|
|
433
473
|
),
|
|
434
|
-
contentOffsetY
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
474
|
+
contentOffsetY,
|
|
475
|
+
// sc-8552: the bar GROWS by the nudge rather than letting its contents fall
|
|
476
|
+
// out of the bottom — the box the author sized, plus what they pushed into
|
|
477
|
+
// it. For a surface that must state a height (the desktop bar, the native
|
|
478
|
+
// header); the phone row is content-sized and grows on its padding alone.
|
|
479
|
+
barHeight:
|
|
480
|
+
height === null && contentOffsetY === 0
|
|
481
|
+
? null
|
|
482
|
+
: (height ?? DEFAULT_TOP_STRIPE_HEIGHT_PX) + contentOffsetY,
|
|
440
483
|
// sc-8023: the stripe's OWN surface, inheriting the app-header colour when
|
|
441
484
|
// unset — so an untouched app is unmoved, and an author who dresses a
|
|
442
485
|
// page's top navigation bar no longer repaints the stripe above the panel by
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.159.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",
|