@colixsystems/widget-sdk 0.151.0 → 0.153.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 +34 -1
- package/dist/contract.cjs +27 -1
- package/dist/contract.js +27 -1
- package/dist/datetimepicker-format.js +24 -5
- package/dist/datetimepicker.js +7 -13
- package/dist/host.d.ts +81 -0
- package/dist/host.js +27 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/navigation.cjs +170 -1
- package/dist/navigation.js +169 -0
- package/dist/theme-token-refs.js +32 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,28 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
72
72
|
|
|
73
73
|
## Status
|
|
74
74
|
|
|
75
|
-
`v0.
|
|
75
|
+
`v0.153.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**.
|
|
76
|
+
|
|
77
|
+
### What's new in 0.153.0 (contract 1.118.0 — unchanged)
|
|
78
|
+
|
|
79
|
+
**`./host` publishes the local-clock DATETIME conversion `<DateTimePicker>` uses (sc-8448).** The Studio's Data Manager sliced the stored UTC string and saved the input's value without a zone, so it showed and wrote UTC labelled as local. It now reads the same conversion the picker does, so a stored instant shows the same wall clock in the Studio and in the Player.
|
|
80
|
+
|
|
81
|
+
- **`isoToDateTimeLocalInput(value)`** — a stored instant as `<input type="datetime-local">`'s `YYYY-MM-DDTHH:mm` on the viewer's clock. A bare `YYYY-MM-DD` is local midnight.
|
|
82
|
+
- **`dateTimeLocalInputToIso(raw)`** — the inverse: a local `YYYY-MM-DDTHH:mm` as a full UTC ISO instant.
|
|
83
|
+
- **`formatDateTimeLabel(value, mode)`** — the picker's trigger label (`YYYY-MM-DD HH:mm` local for `"datetime"`).
|
|
84
|
+
|
|
85
|
+
Behaviour: a bare date given to a `mode="datetime"` picker now shows local midnight instead of UTC midnight (which read as the previous evening west of Greenwich), and a year before 1000 is zero-padded so typing a year no longer clears the web input. Otherwise additive.
|
|
86
|
+
|
|
87
|
+
### What's new in 0.152.0 (contract 1.118.0 — unchanged)
|
|
88
|
+
|
|
89
|
+
**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.
|
|
90
|
+
|
|
91
|
+
- **`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`, 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`.
|
|
92
|
+
- **`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.
|
|
93
|
+
- **`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.
|
|
94
|
+
- **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.
|
|
95
|
+
|
|
96
|
+
Additive — nothing in the existing `./host` surface changed, and the contract is untouched.
|
|
76
97
|
|
|
77
98
|
### What's new in 0.151.0 (contract 1.118.0 — unchanged)
|
|
78
99
|
|
|
@@ -241,6 +262,18 @@ All three are `warning`, so `appstudio-widget lint` reports them and still exits
|
|
|
241
262
|
|
|
242
263
|
`lintEditability(manifest, files)` is bundle-level like `lintStyleWiring`, and needs `--manifest` for the same reason. It counts rendered elements PER FILE, though: joining `widget.web.jsx` and `widget.native.jsx` would double every tag and call a single-`<Text>` split-impl widget multi-element.
|
|
243
264
|
|
|
265
|
+
### What's new in 0.151.0 (contract 1.119.0)
|
|
266
|
+
|
|
267
|
+
**A container's gradient wash can bind its stops to the theme (sc-8326).** sc-8197 gave a layout container's flat fill a theme-following `surface` role, but a two-stop `backgroundGradient` had no role form — so a generated colour-band opener could only freeze two literals out of whichever palette was active when the app was built, and it stopped following the app's own look from then on.
|
|
268
|
+
|
|
269
|
+
Each stop of a `backgroundGradient` may now be a binding — `"theme:colors.primary"` or `"theme:colors.secondary"` — as well as a hex.
|
|
270
|
+
|
|
271
|
+
- **`isThemeColorTokenRef(value)` and `THEME_TOKEN_REF_PREFIX`** are exported from the contract: the one grammar predicate the Studio's writer, both hosts' resolvers and Mason's plan validator read, so what may be written and what will resolve cannot drift.
|
|
272
|
+
- **`resolveGradientTokenStops(gradient, theme)`** is the host-integration call behind it, exported from `@colixsystems/widget-sdk/host`. A layout container's style never reaches `resolveThemeTokenRefs` — containers are painted by the page renderer, not by WidgetHost — so this is its own boundary call. Widget authors never call it.
|
|
273
|
+
- **The hex grammar is still the injection guard.** Stops resolve *before* `normaliseComponentGradient`, so what reaches CSS and generated export source is unchanged, and an unresolvable binding fails that guard and drops the wash rather than painting blank.
|
|
274
|
+
- **Nothing changes for a widget author.** A `styleSchema` gradient field already resolved bindings through `resolveThemeTokenRefs`; this closes the container half.
|
|
275
|
+
- **Fully additive.** Every stored hex gradient is untouched and renders byte-identically.
|
|
276
|
+
|
|
244
277
|
### What's new in 0.139.0 (contract 1.108.0)
|
|
245
278
|
|
|
246
279
|
**An author can BIND a colour field to the workspace brand instead of freezing a hex (sc-7800).** A `type: "color"` field tracked the theme only while it was *unset*, through its `themeDefault` placeholder. The moment the author picked a colour it became a literal that outranked the theme forever, so "make this our brand colour" was inexpressible — the author had to type today's hex, and it went stale silently at the next rebrand.
|
package/dist/contract.cjs
CHANGED
|
@@ -192,6 +192,20 @@ const THEME_COLOR_TOKENS = Object.freeze([
|
|
|
192
192
|
Object.freeze({ path: "colors.secondary", label: "Secondary" }),
|
|
193
193
|
]);
|
|
194
194
|
|
|
195
|
+
// sc-8326 — the `theme:<path>` value grammar, as a PREDICATE over a STORED
|
|
196
|
+
// value and declared beside the allowlist it reads. Three readers have to agree
|
|
197
|
+
// on what a binding IS: the Studio's colour picker writes one, both hosts
|
|
198
|
+
// resolve one, and Mason's plan validator must accept one as a gradient stop.
|
|
199
|
+
// One definition, so they cannot drift (CLAUDE.md §3).
|
|
200
|
+
const THEME_TOKEN_REF_PREFIX = "theme:";
|
|
201
|
+
|
|
202
|
+
function isThemeColorTokenRef(value) {
|
|
203
|
+
if (typeof value !== "string") return false;
|
|
204
|
+
if (!value.startsWith(THEME_TOKEN_REF_PREFIX)) return false;
|
|
205
|
+
const path = value.slice(THEME_TOKEN_REF_PREFIX.length);
|
|
206
|
+
return THEME_COLOR_TOKENS.some((token) => token.path === path);
|
|
207
|
+
}
|
|
208
|
+
|
|
195
209
|
|
|
196
210
|
// REQ-THEME-15 (sc-1497) — per-component style tokens. The global palette is a
|
|
197
211
|
// single lever: remapping `primaryColor` to recolour "the buttons" recolours
|
|
@@ -4264,7 +4278,17 @@ const CONTRACT = deepFreeze({
|
|
|
4264
4278
|
// because Newsfeed and Chat both render per-emoji pills over a Reactions
|
|
4265
4279
|
// table, and the legacy slug mapping (`like` -> emoji) must not be
|
|
4266
4280
|
// re-derived per widget.
|
|
4267
|
-
|
|
4281
|
+
// 1.119.0: additive (sc-8326) — a component gradient's `from` / `to` stops
|
|
4282
|
+
// accept a `theme:<path>` binding as well as a hex, with
|
|
4283
|
+
// `isThemeColorTokenRef` as the shared grammar predicate. `surface` gave a
|
|
4284
|
+
// container's FLAT fill a theme-following role in sc-8197, but a two-stop
|
|
4285
|
+
// wash had no role form, so Mason's colour-band opener could only freeze
|
|
4286
|
+
// two literals out of the creation-time palette and the band stopped
|
|
4287
|
+
// following the app's own theme. Additive: a stored hex gradient is
|
|
4288
|
+
// untouched and renders byte-identically, and an unresolvable binding
|
|
4289
|
+
// fails the existing hex guard, dropping the wash rather than painting it
|
|
4290
|
+
// blank.
|
|
4291
|
+
version: "1.119.0",
|
|
4268
4292
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4269
4293
|
hooks: HOOKS,
|
|
4270
4294
|
primitives: PRIMITIVES,
|
|
@@ -4567,6 +4591,8 @@ module.exports = {
|
|
|
4567
4591
|
deriveSurfaceTokens,
|
|
4568
4592
|
gradientAngleToVector,
|
|
4569
4593
|
normaliseComponentGradient,
|
|
4594
|
+
isThemeColorTokenRef,
|
|
4595
|
+
THEME_TOKEN_REF_PREFIX,
|
|
4570
4596
|
formatMoneyIn,
|
|
4571
4597
|
normaliseCurrencyCode,
|
|
4572
4598
|
sameWidgetRouteValue,
|
package/dist/contract.js
CHANGED
|
@@ -192,6 +192,20 @@ const THEME_COLOR_TOKENS = Object.freeze([
|
|
|
192
192
|
Object.freeze({ path: "colors.secondary", label: "Secondary" }),
|
|
193
193
|
]);
|
|
194
194
|
|
|
195
|
+
// sc-8326 — the `theme:<path>` value grammar, as a PREDICATE over a STORED
|
|
196
|
+
// value and declared beside the allowlist it reads. Three readers have to agree
|
|
197
|
+
// on what a binding IS: the Studio's colour picker writes one, both hosts
|
|
198
|
+
// resolve one, and Mason's plan validator must accept one as a gradient stop.
|
|
199
|
+
// One definition, so they cannot drift (CLAUDE.md §3).
|
|
200
|
+
const THEME_TOKEN_REF_PREFIX = "theme:";
|
|
201
|
+
|
|
202
|
+
function isThemeColorTokenRef(value) {
|
|
203
|
+
if (typeof value !== "string") return false;
|
|
204
|
+
if (!value.startsWith(THEME_TOKEN_REF_PREFIX)) return false;
|
|
205
|
+
const path = value.slice(THEME_TOKEN_REF_PREFIX.length);
|
|
206
|
+
return THEME_COLOR_TOKENS.some((token) => token.path === path);
|
|
207
|
+
}
|
|
208
|
+
|
|
195
209
|
|
|
196
210
|
// REQ-THEME-15 (sc-1497) — per-component style tokens. The global palette is a
|
|
197
211
|
// single lever: remapping `primaryColor` to recolour "the buttons" recolours
|
|
@@ -4260,7 +4274,17 @@ const CONTRACT = deepFreeze({
|
|
|
4260
4274
|
// because Newsfeed and Chat both render per-emoji pills over a Reactions
|
|
4261
4275
|
// table, and the legacy slug mapping (`like` -> emoji) must not be
|
|
4262
4276
|
// re-derived per widget.
|
|
4263
|
-
|
|
4277
|
+
// 1.119.0: additive (sc-8326) — a component gradient's `from` / `to` stops
|
|
4278
|
+
// accept a `theme:<path>` binding as well as a hex, with
|
|
4279
|
+
// `isThemeColorTokenRef` as the shared grammar predicate. `surface` gave a
|
|
4280
|
+
// container's FLAT fill a theme-following role in sc-8197, but a two-stop
|
|
4281
|
+
// wash had no role form, so Mason's colour-band opener could only freeze
|
|
4282
|
+
// two literals out of the creation-time palette and the band stopped
|
|
4283
|
+
// following the app's own theme. Additive: a stored hex gradient is
|
|
4284
|
+
// untouched and renders byte-identically, and an unresolvable binding
|
|
4285
|
+
// fails the existing hex guard, dropping the wash rather than painting it
|
|
4286
|
+
// blank.
|
|
4287
|
+
version: "1.119.0",
|
|
4264
4288
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4265
4289
|
hooks: HOOKS,
|
|
4266
4290
|
primitives: PRIMITIVES,
|
|
@@ -4563,6 +4587,8 @@ export {
|
|
|
4563
4587
|
deriveSurfaceTokens,
|
|
4564
4588
|
gradientAngleToVector,
|
|
4565
4589
|
normaliseComponentGradient,
|
|
4590
|
+
isThemeColorTokenRef,
|
|
4591
|
+
THEME_TOKEN_REF_PREFIX,
|
|
4566
4592
|
formatMoneyIn,
|
|
4567
4593
|
normaliseCurrencyCode,
|
|
4568
4594
|
sameWidgetRouteValue,
|
|
@@ -15,12 +15,33 @@ export function parseToDate(value) {
|
|
|
15
15
|
return new Date();
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
19
|
+
|
|
20
|
+
// A stored DATETIME instant → `<input type="datetime-local">`'s
|
|
21
|
+
// `YYYY-MM-DDTHH:mm` on the viewer's local clock. A bare calendar date is
|
|
22
|
+
// local midnight, not UTC midnight, so it never slips a day west of Greenwich.
|
|
23
|
+
export function isoToDateTimeLocalInput(value) {
|
|
24
|
+
if (value == null || value === "") return "";
|
|
25
|
+
if (typeof value === "string" && DATE_ONLY.test(value)) return `${value}T00:00`;
|
|
26
|
+
const d = new Date(value);
|
|
27
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
28
|
+
return `${formatToIso(d, "date")}T${formatToIso(d, "time")}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The inverse: a naive local `YYYY-MM-DDTHH:mm` → a full UTC ISO instant.
|
|
32
|
+
export function dateTimeLocalInputToIso(raw) {
|
|
33
|
+
if (raw == null || raw === "") return "";
|
|
34
|
+
const d = new Date(raw);
|
|
35
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
36
|
+
return d.toISOString();
|
|
37
|
+
}
|
|
38
|
+
|
|
18
39
|
export function formatToIso(date, mode) {
|
|
19
40
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;
|
|
20
41
|
if (mode === "date") {
|
|
21
42
|
// Local-date ISO (yyyy-mm-dd) — calendar dates are timezone-free so a
|
|
22
43
|
// "May 28" picked in Stockholm doesn't read as "May 27" in NYC.
|
|
23
|
-
const y = date.getFullYear();
|
|
44
|
+
const y = String(date.getFullYear()).padStart(4, "0");
|
|
24
45
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
25
46
|
const d = String(date.getDate()).padStart(2, "0");
|
|
26
47
|
return `${y}-${m}-${d}`;
|
|
@@ -46,10 +67,8 @@ export function formatDisplayLabel(value, mode) {
|
|
|
46
67
|
}
|
|
47
68
|
if (effectiveMode === "date") return String(value).slice(0, 10);
|
|
48
69
|
if (effectiveMode === "time") return String(value).slice(0, 5);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (Number.isNaN(d.getTime())) return String(value);
|
|
52
|
-
return `${formatToIso(d, "date")} ${formatToIso(d, "time")}`;
|
|
70
|
+
const local = isoToDateTimeLocalInput(value);
|
|
71
|
+
return local ? local.replace("T", " ") : String(value);
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
// Perceived-luminance test for a hex colour (`#rgb` or `#rrggbb`). The web
|
package/dist/datetimepicker.js
CHANGED
|
@@ -26,7 +26,11 @@
|
|
|
26
26
|
|
|
27
27
|
import React from "react";
|
|
28
28
|
import { useHostTheme } from "./hooks.js";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
isDarkColor,
|
|
31
|
+
isoToDateTimeLocalInput,
|
|
32
|
+
dateTimeLocalInputToIso,
|
|
33
|
+
} from "./datetimepicker-format.js";
|
|
30
34
|
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
31
35
|
|
|
32
36
|
const MODE_TO_INPUT_TYPE = {
|
|
@@ -51,15 +55,7 @@ function _isoToInputValue(value, mode) {
|
|
|
51
55
|
if (mode === "time") {
|
|
52
56
|
return String(value).slice(0, 5);
|
|
53
57
|
}
|
|
54
|
-
|
|
55
|
-
const d = new Date(value);
|
|
56
|
-
if (Number.isNaN(d.getTime())) return "";
|
|
57
|
-
const y = d.getFullYear();
|
|
58
|
-
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
59
|
-
const day = String(d.getDate()).padStart(2, "0");
|
|
60
|
-
const hh = String(d.getHours()).padStart(2, "0");
|
|
61
|
-
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
62
|
-
return `${y}-${m}-${day}T${hh}:${mm}`;
|
|
58
|
+
return isoToDateTimeLocalInput(value);
|
|
63
59
|
}
|
|
64
60
|
|
|
65
61
|
// Translate the input's typed string back to the ISO format the wire wants.
|
|
@@ -69,9 +65,7 @@ function _isoToInputValue(value, mode) {
|
|
|
69
65
|
function _inputValueToIso(raw, mode) {
|
|
70
66
|
if (raw == null || raw === "") return "";
|
|
71
67
|
if (mode === "date" || mode === "time") return raw;
|
|
72
|
-
|
|
73
|
-
if (Number.isNaN(d.getTime())) return "";
|
|
74
|
-
return d.toISOString();
|
|
68
|
+
return dateTimeLocalInputToIso(raw);
|
|
75
69
|
}
|
|
76
70
|
|
|
77
71
|
export function DateTimePicker({
|
package/dist/host.d.ts
CHANGED
|
@@ -300,6 +300,57 @@ export interface TopBarTokens {
|
|
|
300
300
|
*/
|
|
301
301
|
export function resolveTopBarTokens(theme: unknown): TopBarTokens;
|
|
302
302
|
|
|
303
|
+
/** sc-8023 — the arrangeable contents of the sidebar layout's TOP STRIPE: the
|
|
304
|
+
* row across the top of the content, outside the panel and above it. */
|
|
305
|
+
export type TopStripeSlot = "back" | "menu" | "brand" | "notifications";
|
|
306
|
+
|
|
307
|
+
export const TOP_STRIPE_SLOTS: TopStripeSlot[];
|
|
308
|
+
|
|
309
|
+
/** The slots that move but never hide. The burger is drawn only where the panel
|
|
310
|
+
* is a drawer, and there it is the only route into the menu. */
|
|
311
|
+
export const TOP_STRIPE_ALWAYS_SHOWN_SLOTS: TopStripeSlot[];
|
|
312
|
+
|
|
313
|
+
export const MIN_TOP_STRIPE_HEIGHT_PX: number;
|
|
314
|
+
export const MAX_TOP_STRIPE_HEIGHT_PX: number;
|
|
315
|
+
export const MIN_TOP_STRIPE_PADDING_X_PX: number;
|
|
316
|
+
export const MAX_TOP_STRIPE_PADDING_X_PX: number;
|
|
317
|
+
export const MAX_TOP_STRIPE_OFFSET_Y_PX: number;
|
|
318
|
+
|
|
319
|
+
/** What the DESKTOP stripe measures with nothing stored, so a control can
|
|
320
|
+
* show the number an author is departing from rather than an empty box. */
|
|
321
|
+
export const DEFAULT_TOP_STRIPE_HEIGHT_PX: number;
|
|
322
|
+
export const DEFAULT_TOP_STRIPE_PADDING_X_PX: number;
|
|
323
|
+
|
|
324
|
+
/** The `theme_config` key that shows or hides one slot (`back` → `showBack`). */
|
|
325
|
+
export function topStripeShowKey(slot: TopStripeSlot): string;
|
|
326
|
+
|
|
327
|
+
export interface TopStripeLayout {
|
|
328
|
+
/** The stripe's own surface, falling back to the app-header colour so an
|
|
329
|
+
* untouched app is unmoved and a page's top navigation bar cannot repaint it. */
|
|
330
|
+
backgroundColor: string;
|
|
331
|
+
/** The row's box in px, or null to keep each surface's own — the desktop
|
|
332
|
+
* bar's 64px, the phone header's content-sized row. */
|
|
333
|
+
height: number | null;
|
|
334
|
+
paddingX: number | null;
|
|
335
|
+
/** How far the row is pushed down, 0-60px. */
|
|
336
|
+
contentOffsetY: number;
|
|
337
|
+
/** Every slot in the author's sequence, hidden ones included. */
|
|
338
|
+
order: TopStripeSlot[];
|
|
339
|
+
/** What the bar actually SHOWS, already filtered and sequenced. A surface
|
|
340
|
+
* still drops what it cannot draw, and a bar left empty is not drawn. */
|
|
341
|
+
slots: TopStripeSlot[];
|
|
342
|
+
/** Which edge the drawer arrives from: the brand grows, so a burger ordered
|
|
343
|
+
* after it sits at the trailing edge and must open a panel from there.
|
|
344
|
+
* Web picks the drawer's side with it, native its `drawerPosition`. */
|
|
345
|
+
menuSide: "start" | "end";
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Resolves the top stripe's arrangement from a whole `theme_config`, reading
|
|
350
|
+
* `sidebar.stripe`. Both hosts draw the row from this one answer (§8).
|
|
351
|
+
*/
|
|
352
|
+
export function resolveTopStripeLayout(theme: unknown): TopStripeLayout;
|
|
353
|
+
|
|
303
354
|
/** A value a widget may persist: a scalar, or a flat array of them. */
|
|
304
355
|
export type WidgetRouteScalar = string | number | boolean;
|
|
305
356
|
export type WidgetRouteValue = WidgetRouteScalar | WidgetRouteScalar[];
|
|
@@ -390,3 +441,33 @@ export function resolveThemeTokenRefs<T>(
|
|
|
390
441
|
theme: unknown,
|
|
391
442
|
schemas?: { propertySchema?: unknown; styleSchema?: unknown },
|
|
392
443
|
): T;
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* sc-8326 — resolve the `theme:` stops of a layout container's gradient wash.
|
|
447
|
+
* A container's style never reaches `resolveThemeTokenRefs`, so this is its own
|
|
448
|
+
* boundary call; the result still goes through `normaliseComponentGradient`,
|
|
449
|
+
* which stays the hex injection guard.
|
|
450
|
+
*/
|
|
451
|
+
export function resolveGradientTokenStops(
|
|
452
|
+
gradient: unknown,
|
|
453
|
+
theme: unknown,
|
|
454
|
+
): unknown;
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* sc-8448 — a stored DATETIME instant as `<input type="datetime-local">`'s
|
|
458
|
+
* `YYYY-MM-DDTHH:mm` on the viewer's local clock. A bare `YYYY-MM-DD` is local
|
|
459
|
+
* midnight. Empty or unparseable input returns `""`.
|
|
460
|
+
*/
|
|
461
|
+
export function isoToDateTimeLocalInput(value: unknown): string;
|
|
462
|
+
|
|
463
|
+
/** sc-8448 — a local `YYYY-MM-DDTHH:mm` as a full UTC ISO instant, or `""`. */
|
|
464
|
+
export function dateTimeLocalInputToIso(raw: unknown): string;
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* sc-8448 — the `<DateTimePicker>` trigger label: for `"datetime"` the stored
|
|
468
|
+
* instant as `YYYY-MM-DD HH:mm` local time, falling back to the raw string.
|
|
469
|
+
*/
|
|
470
|
+
export function formatDateTimeLabel(
|
|
471
|
+
value: unknown,
|
|
472
|
+
mode: "date" | "time" | "datetime",
|
|
473
|
+
): string;
|
package/dist/host.js
CHANGED
|
@@ -28,10 +28,13 @@ export {
|
|
|
28
28
|
// at the SAME render boundary on both hosts — so a rebrand repaints the Player,
|
|
29
29
|
// the Canvas and the exported app alike, and the widget still receives a plain
|
|
30
30
|
// colour string.
|
|
31
|
+
// sc-8326 extends the same grammar to a layout container's gradient stops, the
|
|
32
|
+
// one themed value that never passes through WidgetHost.
|
|
31
33
|
export {
|
|
32
34
|
isThemeTokenRef,
|
|
33
35
|
resolveThemeTokenRef,
|
|
34
36
|
resolveThemeTokenRefs,
|
|
37
|
+
resolveGradientTokenStops,
|
|
35
38
|
} from "./theme-token-refs.js";
|
|
36
39
|
|
|
37
40
|
// REQ-THEME-SURFACE: the surface-token derivation both hosts apply per painted
|
|
@@ -63,6 +66,10 @@ export {
|
|
|
63
66
|
// REQ-THEME-14: `resolveSidebarTokens` and `resolveTopBarTokens` are the same
|
|
64
67
|
// idea for the rail and the app bar, whose defaults used to live inline in each
|
|
65
68
|
// host and had already drifted apart.
|
|
69
|
+
// sc-8023: `resolveTopStripeLayout` is the app header bar's ARRANGEMENT — what the
|
|
70
|
+
// row above the side panel holds, in what order, and the box it sits in. The
|
|
71
|
+
// same bar is drawn by the desktop top bar, the phone header and the exported
|
|
72
|
+
// app's screen header, and each hard-coded its own row before this.
|
|
66
73
|
// REQ-NAV-LOCAL (sc-7696): `resolveSidebarUnfold` answers where a page's
|
|
67
74
|
// SIDEBAR local menu hangs in the rail and the drawer. The compiler asks it
|
|
68
75
|
// once per screen at build time and the Player once per render, so the two
|
|
@@ -72,7 +79,20 @@ export {
|
|
|
72
79
|
menuItemCap,
|
|
73
80
|
resolveFooterTokens,
|
|
74
81
|
resolveSidebarTokens,
|
|
82
|
+
resolveTopStripeLayout,
|
|
75
83
|
resolveTopBarTokens,
|
|
84
|
+
topStripeShowKey,
|
|
85
|
+
TOP_STRIPE_ALWAYS_SHOWN_SLOTS,
|
|
86
|
+
TOP_STRIPE_SLOTS,
|
|
87
|
+
// The bar's BOUNDS travel with it: the Studio's controls offer exactly the
|
|
88
|
+
// range the resolver clamps to, rather than restating the numbers.
|
|
89
|
+
MIN_TOP_STRIPE_HEIGHT_PX,
|
|
90
|
+
MAX_TOP_STRIPE_HEIGHT_PX,
|
|
91
|
+
MIN_TOP_STRIPE_PADDING_X_PX,
|
|
92
|
+
MAX_TOP_STRIPE_PADDING_X_PX,
|
|
93
|
+
MAX_TOP_STRIPE_OFFSET_Y_PX,
|
|
94
|
+
DEFAULT_TOP_STRIPE_HEIGHT_PX,
|
|
95
|
+
DEFAULT_TOP_STRIPE_PADDING_X_PX,
|
|
76
96
|
resolveSidebarUnfold,
|
|
77
97
|
} from "./navigation.js";
|
|
78
98
|
|
|
@@ -104,3 +124,10 @@ export {
|
|
|
104
124
|
normaliseThemeGradient,
|
|
105
125
|
normaliseLoginDesign,
|
|
106
126
|
} from "./theme-background.js";
|
|
127
|
+
|
|
128
|
+
// sc-8448: one DATETIME local-clock conversion for the picker and the Studio.
|
|
129
|
+
export {
|
|
130
|
+
isoToDateTimeLocalInput,
|
|
131
|
+
dateTimeLocalInputToIso,
|
|
132
|
+
formatDisplayLabel as formatDateTimeLabel,
|
|
133
|
+
} from "./datetimepicker-format.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -2674,6 +2674,16 @@ export function normaliseComponentGradient(
|
|
|
2674
2674
|
raw: unknown,
|
|
2675
2675
|
): ComponentGradient | null;
|
|
2676
2676
|
|
|
2677
|
+
/** sc-8326 — the `theme:<path>` binding prefix, shared with the hosts' resolver. */
|
|
2678
|
+
export const THEME_TOKEN_REF_PREFIX: string;
|
|
2679
|
+
|
|
2680
|
+
/**
|
|
2681
|
+
* sc-8326 — true when a stored value is a `theme:<path>` binding naming a token
|
|
2682
|
+
* in `CONTRACT.themeColorTokens`. The one grammar predicate the Studio's writer,
|
|
2683
|
+
* the hosts' resolver and Mason's plan validator all read.
|
|
2684
|
+
*/
|
|
2685
|
+
export function isThemeColorTokenRef(value: unknown): boolean;
|
|
2686
|
+
|
|
2677
2687
|
// Linter
|
|
2678
2688
|
export interface LintFinding {
|
|
2679
2689
|
rule: string;
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/dist/navigation.cjs
CHANGED
|
@@ -301,6 +301,175 @@ function resolveSidebarTokens(theme) {
|
|
|
301
301
|
* resolves to the page's, as an `attached` tab's already did (sc-6596).
|
|
302
302
|
* @returns {{ backgroundColor: string, tintColor: string, titleColor: string }}
|
|
303
303
|
*/
|
|
304
|
+
// sc-8023 — the TOP STRIPE's arrangement: the row that runs across the top of
|
|
305
|
+
// the content in the sidebar layout, outside the panel and above it.
|
|
306
|
+
//
|
|
307
|
+
// It is NOT the optional full-width top bar, which keeps its own fixed row.
|
|
308
|
+
// The stripe is what an app with a side panel actually shows, and it is drawn
|
|
309
|
+
// twice — across the content on desktop, and as the phone header where the panel
|
|
310
|
+
// is a drawer — each hard-coding its own arrangement, so the author could colour
|
|
311
|
+
// the row but never move anything in it.
|
|
312
|
+
//
|
|
313
|
+
// It lives on the SIDEBAR block because it belongs to that layout: `topBar` is
|
|
314
|
+
// the other bar. Its colours still come from the app-header tokens
|
|
315
|
+
// (`resolveTopBarTokens`), which is what paints all of this chrome today.
|
|
316
|
+
// The identity is deliberately NOT here. On a phone the panel's own foot owns
|
|
317
|
+
// the account and sign-out (sc-5907 — a 120px name pill left the brand nothing
|
|
318
|
+
// to render in), and on desktop they sit in the content strip the rail does not
|
|
319
|
+
// reach. Offering them as arrangeable slots meant two checkboxes that could
|
|
320
|
+
// never move anything, which is worse than not offering them.
|
|
321
|
+
const TOP_STRIPE_SLOTS = ["back", "menu", "brand", "notifications"];
|
|
322
|
+
|
|
323
|
+
// Everything the bar draws today, so an app that never opens the panel is
|
|
324
|
+
// unmoved. Unlike the slots a surface adds, none of these is new — they are the
|
|
325
|
+
// controls already in the row, now nameable.
|
|
326
|
+
const TOP_STRIPE_SLOT_SHOWN_BY_DEFAULT = {
|
|
327
|
+
back: true,
|
|
328
|
+
menu: true,
|
|
329
|
+
brand: true,
|
|
330
|
+
notifications: true,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// The burger MOVES but never hides. It is drawn only where the panel is a
|
|
334
|
+
// drawer — a phone — and there it is the sole way in: the REQ-NAV-14 floating
|
|
335
|
+
// button belongs to site mode, so an app page that dropped it would strand the
|
|
336
|
+
// visitor with no route to the menu. Its position still matters (it decides
|
|
337
|
+
// which edge the drawer arrives from), so it stays in `order` and simply
|
|
338
|
+
// offers no visibility of its own, the way the identity offers no slot.
|
|
339
|
+
const TOP_STRIPE_ALWAYS_SHOWN_SLOTS = ["menu"];
|
|
340
|
+
|
|
341
|
+
// The bar's box. Neither measurement has a shared default: the desktop bar is a
|
|
342
|
+
// fixed `h-16` with `px-4`, the phone header is content-sized with `px-2`.
|
|
343
|
+
// `null` keeps each of those, so nothing moves until an author names one — and
|
|
344
|
+
// then both surfaces take the same, which is the convergence (§3).
|
|
345
|
+
const MIN_TOP_STRIPE_HEIGHT_PX = 40;
|
|
346
|
+
const MAX_TOP_STRIPE_HEIGHT_PX = 200;
|
|
347
|
+
const MIN_TOP_STRIPE_PADDING_X_PX = 0;
|
|
348
|
+
const MAX_TOP_STRIPE_PADDING_X_PX = 64;
|
|
349
|
+
const MAX_TOP_STRIPE_OFFSET_Y_PX = 60;
|
|
350
|
+
|
|
351
|
+
// What the DESKTOP stripe measures when nothing is stored — the row's shipped
|
|
352
|
+
// `h-16` and `px-4`. Published so a control can show the author the number
|
|
353
|
+
// they are departing from instead of an empty box; the resolver still answers
|
|
354
|
+
// `null`, because the phone header's box is its own and neither surface moves
|
|
355
|
+
// until an author names a value.
|
|
356
|
+
const DEFAULT_TOP_STRIPE_HEIGHT_PX = 64;
|
|
357
|
+
const DEFAULT_TOP_STRIPE_PADDING_X_PX = 16;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* The `theme_config` key that shows or hides one slot. Derived in ONE place so
|
|
361
|
+
* the Studio's control, the write-path coercion and the resolver cannot each
|
|
362
|
+
* spell it slightly differently.
|
|
363
|
+
*
|
|
364
|
+
* @param {string} slot — a member of `TOP_STRIPE_SLOTS`.
|
|
365
|
+
* @returns {string}
|
|
366
|
+
*/
|
|
367
|
+
function topStripeShowKey(slot) {
|
|
368
|
+
return `show${slot.charAt(0).toUpperCase()}${slot.slice(1)}`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Clamped rather than dropped: a value past the end expresses a real intent and
|
|
372
|
+
// lands on the end. A non-number is a different case and takes the default.
|
|
373
|
+
function topStripeClampedOr(value, min, max, fallback) {
|
|
374
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
375
|
+
return Math.min(Math.max(Math.round(value), min), max);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// An `order` is at most a permutation; anything longer is not an ordering, and
|
|
379
|
+
// `theme_config` is stored close to verbatim, so it is refused rather than
|
|
380
|
+
// walked. Both hosts resolve on every render.
|
|
381
|
+
const MAX_TOP_STRIPE_ORDER_ENTRIES = TOP_STRIPE_SLOTS.length * 2;
|
|
382
|
+
|
|
383
|
+
function topStripeOrder(value) {
|
|
384
|
+
if (!Array.isArray(value) || value.length > MAX_TOP_STRIPE_ORDER_ENTRIES) {
|
|
385
|
+
return TOP_STRIPE_SLOTS.slice();
|
|
386
|
+
}
|
|
387
|
+
const named = [];
|
|
388
|
+
for (const slot of value) {
|
|
389
|
+
if (TOP_STRIPE_SLOTS.includes(slot) && !named.includes(slot)) named.push(slot);
|
|
390
|
+
}
|
|
391
|
+
// An authored order names what it cares about; everything it leaves out keeps
|
|
392
|
+
// its default place. Dropping the unnamed slots would make "reorder two" and
|
|
393
|
+
// "hide the rest" the same gesture, and one of those is an accident.
|
|
394
|
+
return named.concat(TOP_STRIPE_SLOTS.filter((s) => !named.includes(s)));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* The app header bar's arrangement, resolved for every host.
|
|
399
|
+
*
|
|
400
|
+
* `slots` is the ordered list of what the bar SHOWS — already filtered and
|
|
401
|
+
* sequenced, so no surface re-derives it. A surface still drops what it cannot
|
|
402
|
+
* draw (a desktop bar has no burger; a root page has no back control), and a
|
|
403
|
+
* bar left holding nothing is not drawn at all.
|
|
404
|
+
*
|
|
405
|
+
* @param {unknown} theme — the whole `theme_config`.
|
|
406
|
+
* @returns {{ backgroundColor: string, height: number|null,
|
|
407
|
+
* paddingX: number|null, contentOffsetY: number, order: string[],
|
|
408
|
+
* slots: string[], menuSide: "start"|"end" }}
|
|
409
|
+
*/
|
|
410
|
+
function resolveTopStripeLayout(theme) {
|
|
411
|
+
const config = isPlainObject(theme) ? theme : {};
|
|
412
|
+
const sidebar = isPlainObject(config.sidebar) ? config.sidebar : {};
|
|
413
|
+
const stripe = isPlainObject(sidebar.stripe) ? sidebar.stripe : {};
|
|
414
|
+
const order = topStripeOrder(stripe.order);
|
|
415
|
+
const shown = (slot) => {
|
|
416
|
+
if (TOP_STRIPE_ALWAYS_SHOWN_SLOTS.includes(slot)) return true;
|
|
417
|
+
const value = stripe[topStripeShowKey(slot)];
|
|
418
|
+
return typeof value === "boolean"
|
|
419
|
+
? value
|
|
420
|
+
: TOP_STRIPE_SLOT_SHOWN_BY_DEFAULT[slot];
|
|
421
|
+
};
|
|
422
|
+
const slots = order.filter(shown);
|
|
423
|
+
return {
|
|
424
|
+
// There is no alignment to resolve: the brand slot GROWS, so the row has no
|
|
425
|
+
// free space for `justify-content` to distribute and every value looked
|
|
426
|
+
// identical. The row spreads because the brand takes the slack.
|
|
427
|
+
// Unset defers to each surface's own box — the desktop bar's fixed 64px,
|
|
428
|
+
// the phone header's content-sized row.
|
|
429
|
+
height: topStripeClampedOr(
|
|
430
|
+
stripe.height,
|
|
431
|
+
MIN_TOP_STRIPE_HEIGHT_PX,
|
|
432
|
+
MAX_TOP_STRIPE_HEIGHT_PX,
|
|
433
|
+
null,
|
|
434
|
+
),
|
|
435
|
+
paddingX: topStripeClampedOr(
|
|
436
|
+
stripe.paddingX,
|
|
437
|
+
MIN_TOP_STRIPE_PADDING_X_PX,
|
|
438
|
+
MAX_TOP_STRIPE_PADDING_X_PX,
|
|
439
|
+
null,
|
|
440
|
+
),
|
|
441
|
+
contentOffsetY: topStripeClampedOr(
|
|
442
|
+
stripe.contentOffsetY,
|
|
443
|
+
0,
|
|
444
|
+
MAX_TOP_STRIPE_OFFSET_Y_PX,
|
|
445
|
+
0,
|
|
446
|
+
),
|
|
447
|
+
// sc-8023: the stripe's OWN surface, inheriting the app-header colour when
|
|
448
|
+
// unset — so an untouched app is unmoved, and an author who dresses a
|
|
449
|
+
// page's top navigation bar no longer repaints the stripe above the panel by
|
|
450
|
+
// accident. Fully resolved here, not raw-or-null, for the reason sc-8033
|
|
451
|
+
// resolved the footer's: a default that lives in each host drifts.
|
|
452
|
+
backgroundColor: hexOr(
|
|
453
|
+
stripe.backgroundColor,
|
|
454
|
+
resolveTopBarTokens(theme).backgroundColor,
|
|
455
|
+
),
|
|
456
|
+
order,
|
|
457
|
+
slots,
|
|
458
|
+
// sc-8023: which edge the panel the burger opens should come in from. The
|
|
459
|
+
// brand GROWS, so anything ordered after it is pushed to the trailing edge
|
|
460
|
+
// — a burger over there opening a panel from the opposite side is a panel
|
|
461
|
+
// that flies past the button that summoned it. With no brand drawn nothing
|
|
462
|
+
// grows and the whole row packs to the start, so the burger is leading
|
|
463
|
+
// whatever its index. Both hosts read this: web picks the drawer's side,
|
|
464
|
+
// native its `drawerPosition`.
|
|
465
|
+
menuSide:
|
|
466
|
+
slots.includes("brand") &&
|
|
467
|
+
slots.indexOf("menu") > slots.indexOf("brand")
|
|
468
|
+
? "end"
|
|
469
|
+
: "start",
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
304
473
|
function resolveTopBarTokens(theme) {
|
|
305
474
|
const config = isPlainObject(theme) ? theme : {};
|
|
306
475
|
const topBar = isPlainObject(config.topBar) ? config.topBar : {};
|
|
@@ -477,4 +646,4 @@ function borderWidthOr(...values) {
|
|
|
477
646
|
return BORDER_WIDTH_MIN;
|
|
478
647
|
}
|
|
479
648
|
|
|
480
|
-
module.exports = { normaliseNavigation, menuItemCap, resolveFooterTokens, resolveSidebarTokens, resolveTopBarTokens, resolveSidebarUnfold };
|
|
649
|
+
module.exports = { normaliseNavigation, menuItemCap, resolveFooterTokens, resolveSidebarTokens, resolveTopStripeLayout, resolveTopBarTokens, topStripeShowKey, TOP_STRIPE_ALWAYS_SHOWN_SLOTS, TOP_STRIPE_SLOTS, MIN_TOP_STRIPE_HEIGHT_PX, MAX_TOP_STRIPE_HEIGHT_PX, MIN_TOP_STRIPE_PADDING_X_PX, MAX_TOP_STRIPE_PADDING_X_PX, MAX_TOP_STRIPE_OFFSET_Y_PX, DEFAULT_TOP_STRIPE_HEIGHT_PX, DEFAULT_TOP_STRIPE_PADDING_X_PX, resolveSidebarUnfold };
|
package/dist/navigation.js
CHANGED
|
@@ -292,6 +292,175 @@ export function resolveSidebarTokens(theme) {
|
|
|
292
292
|
* resolves to the page's, as an `attached` tab's already did (sc-6596).
|
|
293
293
|
* @returns {{ backgroundColor: string, tintColor: string, titleColor: string }}
|
|
294
294
|
*/
|
|
295
|
+
// sc-8023 — the TOP STRIPE's arrangement: the row that runs across the top of
|
|
296
|
+
// the content in the sidebar layout, outside the panel and above it.
|
|
297
|
+
//
|
|
298
|
+
// It is NOT the optional full-width top bar, which keeps its own fixed row.
|
|
299
|
+
// The stripe is what an app with a side panel actually shows, and it is drawn
|
|
300
|
+
// twice — across the content on desktop, and as the phone header where the panel
|
|
301
|
+
// is a drawer — each hard-coding its own arrangement, so the author could colour
|
|
302
|
+
// the row but never move anything in it.
|
|
303
|
+
//
|
|
304
|
+
// It lives on the SIDEBAR block because it belongs to that layout: `topBar` is
|
|
305
|
+
// the other bar. Its colours still come from the app-header tokens
|
|
306
|
+
// (`resolveTopBarTokens`), which is what paints all of this chrome today.
|
|
307
|
+
// The identity is deliberately NOT here. On a phone the panel's own foot owns
|
|
308
|
+
// the account and sign-out (sc-5907 — a 120px name pill left the brand nothing
|
|
309
|
+
// to render in), and on desktop they sit in the content strip the rail does not
|
|
310
|
+
// reach. Offering them as arrangeable slots meant two checkboxes that could
|
|
311
|
+
// never move anything, which is worse than not offering them.
|
|
312
|
+
export const TOP_STRIPE_SLOTS = ["back", "menu", "brand", "notifications"];
|
|
313
|
+
|
|
314
|
+
// Everything the bar draws today, so an app that never opens the panel is
|
|
315
|
+
// unmoved. Unlike the slots a surface adds, none of these is new — they are the
|
|
316
|
+
// controls already in the row, now nameable.
|
|
317
|
+
const TOP_STRIPE_SLOT_SHOWN_BY_DEFAULT = {
|
|
318
|
+
back: true,
|
|
319
|
+
menu: true,
|
|
320
|
+
brand: true,
|
|
321
|
+
notifications: true,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// The burger MOVES but never hides. It is drawn only where the panel is a
|
|
325
|
+
// drawer — a phone — and there it is the sole way in: the REQ-NAV-14 floating
|
|
326
|
+
// button belongs to site mode, so an app page that dropped it would strand the
|
|
327
|
+
// visitor with no route to the menu. Its position still matters (it decides
|
|
328
|
+
// which edge the drawer arrives from), so it stays in `order` and simply
|
|
329
|
+
// offers no visibility of its own, the way the identity offers no slot.
|
|
330
|
+
export const TOP_STRIPE_ALWAYS_SHOWN_SLOTS = ["menu"];
|
|
331
|
+
|
|
332
|
+
// The bar's box. Neither measurement has a shared default: the desktop bar is a
|
|
333
|
+
// fixed `h-16` with `px-4`, the phone header is content-sized with `px-2`.
|
|
334
|
+
// `null` keeps each of those, so nothing moves until an author names one — and
|
|
335
|
+
// then both surfaces take the same, which is the convergence (§3).
|
|
336
|
+
export const MIN_TOP_STRIPE_HEIGHT_PX = 40;
|
|
337
|
+
export const MAX_TOP_STRIPE_HEIGHT_PX = 200;
|
|
338
|
+
export const MIN_TOP_STRIPE_PADDING_X_PX = 0;
|
|
339
|
+
export const MAX_TOP_STRIPE_PADDING_X_PX = 64;
|
|
340
|
+
export const MAX_TOP_STRIPE_OFFSET_Y_PX = 60;
|
|
341
|
+
|
|
342
|
+
// What the DESKTOP stripe measures when nothing is stored — the row's shipped
|
|
343
|
+
// `h-16` and `px-4`. Published so a control can show the author the number
|
|
344
|
+
// they are departing from instead of an empty box; the resolver still answers
|
|
345
|
+
// `null`, because the phone header's box is its own and neither surface moves
|
|
346
|
+
// until an author names a value.
|
|
347
|
+
export const DEFAULT_TOP_STRIPE_HEIGHT_PX = 64;
|
|
348
|
+
export const DEFAULT_TOP_STRIPE_PADDING_X_PX = 16;
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The `theme_config` key that shows or hides one slot. Derived in ONE place so
|
|
352
|
+
* the Studio's control, the write-path coercion and the resolver cannot each
|
|
353
|
+
* spell it slightly differently.
|
|
354
|
+
*
|
|
355
|
+
* @param {string} slot — a member of `TOP_STRIPE_SLOTS`.
|
|
356
|
+
* @returns {string}
|
|
357
|
+
*/
|
|
358
|
+
export function topStripeShowKey(slot) {
|
|
359
|
+
return `show${slot.charAt(0).toUpperCase()}${slot.slice(1)}`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Clamped rather than dropped: a value past the end expresses a real intent and
|
|
363
|
+
// lands on the end. A non-number is a different case and takes the default.
|
|
364
|
+
function topStripeClampedOr(value, min, max, fallback) {
|
|
365
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
366
|
+
return Math.min(Math.max(Math.round(value), min), max);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// An `order` is at most a permutation; anything longer is not an ordering, and
|
|
370
|
+
// `theme_config` is stored close to verbatim, so it is refused rather than
|
|
371
|
+
// walked. Both hosts resolve on every render.
|
|
372
|
+
const MAX_TOP_STRIPE_ORDER_ENTRIES = TOP_STRIPE_SLOTS.length * 2;
|
|
373
|
+
|
|
374
|
+
function topStripeOrder(value) {
|
|
375
|
+
if (!Array.isArray(value) || value.length > MAX_TOP_STRIPE_ORDER_ENTRIES) {
|
|
376
|
+
return TOP_STRIPE_SLOTS.slice();
|
|
377
|
+
}
|
|
378
|
+
const named = [];
|
|
379
|
+
for (const slot of value) {
|
|
380
|
+
if (TOP_STRIPE_SLOTS.includes(slot) && !named.includes(slot)) named.push(slot);
|
|
381
|
+
}
|
|
382
|
+
// An authored order names what it cares about; everything it leaves out keeps
|
|
383
|
+
// its default place. Dropping the unnamed slots would make "reorder two" and
|
|
384
|
+
// "hide the rest" the same gesture, and one of those is an accident.
|
|
385
|
+
return named.concat(TOP_STRIPE_SLOTS.filter((s) => !named.includes(s)));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* The app header bar's arrangement, resolved for every host.
|
|
390
|
+
*
|
|
391
|
+
* `slots` is the ordered list of what the bar SHOWS — already filtered and
|
|
392
|
+
* sequenced, so no surface re-derives it. A surface still drops what it cannot
|
|
393
|
+
* draw (a desktop bar has no burger; a root page has no back control), and a
|
|
394
|
+
* bar left holding nothing is not drawn at all.
|
|
395
|
+
*
|
|
396
|
+
* @param {unknown} theme — the whole `theme_config`.
|
|
397
|
+
* @returns {{ backgroundColor: string, height: number|null,
|
|
398
|
+
* paddingX: number|null, contentOffsetY: number, order: string[],
|
|
399
|
+
* slots: string[], menuSide: "start"|"end" }}
|
|
400
|
+
*/
|
|
401
|
+
export function resolveTopStripeLayout(theme) {
|
|
402
|
+
const config = isPlainObject(theme) ? theme : {};
|
|
403
|
+
const sidebar = isPlainObject(config.sidebar) ? config.sidebar : {};
|
|
404
|
+
const stripe = isPlainObject(sidebar.stripe) ? sidebar.stripe : {};
|
|
405
|
+
const order = topStripeOrder(stripe.order);
|
|
406
|
+
const shown = (slot) => {
|
|
407
|
+
if (TOP_STRIPE_ALWAYS_SHOWN_SLOTS.includes(slot)) return true;
|
|
408
|
+
const value = stripe[topStripeShowKey(slot)];
|
|
409
|
+
return typeof value === "boolean"
|
|
410
|
+
? value
|
|
411
|
+
: TOP_STRIPE_SLOT_SHOWN_BY_DEFAULT[slot];
|
|
412
|
+
};
|
|
413
|
+
const slots = order.filter(shown);
|
|
414
|
+
return {
|
|
415
|
+
// There is no alignment to resolve: the brand slot GROWS, so the row has no
|
|
416
|
+
// free space for `justify-content` to distribute and every value looked
|
|
417
|
+
// identical. The row spreads because the brand takes the slack.
|
|
418
|
+
// Unset defers to each surface's own box — the desktop bar's fixed 64px,
|
|
419
|
+
// the phone header's content-sized row.
|
|
420
|
+
height: topStripeClampedOr(
|
|
421
|
+
stripe.height,
|
|
422
|
+
MIN_TOP_STRIPE_HEIGHT_PX,
|
|
423
|
+
MAX_TOP_STRIPE_HEIGHT_PX,
|
|
424
|
+
null,
|
|
425
|
+
),
|
|
426
|
+
paddingX: topStripeClampedOr(
|
|
427
|
+
stripe.paddingX,
|
|
428
|
+
MIN_TOP_STRIPE_PADDING_X_PX,
|
|
429
|
+
MAX_TOP_STRIPE_PADDING_X_PX,
|
|
430
|
+
null,
|
|
431
|
+
),
|
|
432
|
+
contentOffsetY: topStripeClampedOr(
|
|
433
|
+
stripe.contentOffsetY,
|
|
434
|
+
0,
|
|
435
|
+
MAX_TOP_STRIPE_OFFSET_Y_PX,
|
|
436
|
+
0,
|
|
437
|
+
),
|
|
438
|
+
// sc-8023: the stripe's OWN surface, inheriting the app-header colour when
|
|
439
|
+
// unset — so an untouched app is unmoved, and an author who dresses a
|
|
440
|
+
// page's top navigation bar no longer repaints the stripe above the panel by
|
|
441
|
+
// accident. Fully resolved here, not raw-or-null, for the reason sc-8033
|
|
442
|
+
// resolved the footer's: a default that lives in each host drifts.
|
|
443
|
+
backgroundColor: hexOr(
|
|
444
|
+
stripe.backgroundColor,
|
|
445
|
+
resolveTopBarTokens(theme).backgroundColor,
|
|
446
|
+
),
|
|
447
|
+
order,
|
|
448
|
+
slots,
|
|
449
|
+
// sc-8023: which edge the panel the burger opens should come in from. The
|
|
450
|
+
// brand GROWS, so anything ordered after it is pushed to the trailing edge
|
|
451
|
+
// — a burger over there opening a panel from the opposite side is a panel
|
|
452
|
+
// that flies past the button that summoned it. With no brand drawn nothing
|
|
453
|
+
// grows and the whole row packs to the start, so the burger is leading
|
|
454
|
+
// whatever its index. Both hosts read this: web picks the drawer's side,
|
|
455
|
+
// native its `drawerPosition`.
|
|
456
|
+
menuSide:
|
|
457
|
+
slots.includes("brand") &&
|
|
458
|
+
slots.indexOf("menu") > slots.indexOf("brand")
|
|
459
|
+
? "end"
|
|
460
|
+
: "start",
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
295
464
|
export function resolveTopBarTokens(theme) {
|
|
296
465
|
const config = isPlainObject(theme) ? theme : {};
|
|
297
466
|
const topBar = isPlainObject(config.topBar) ? config.topBar : {};
|
package/dist/theme-token-refs.js
CHANGED
|
@@ -18,11 +18,13 @@
|
|
|
18
18
|
// type today's hex, which went stale silently. A bound field stores the TOKEN,
|
|
19
19
|
// so the intent survives the rebrand.
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { isThemeColorTokenRef, THEME_TOKEN_REF_PREFIX } from "./contract.js";
|
|
22
22
|
|
|
23
23
|
// A value is a binding, not a colour, when it carries this prefix. No CSS
|
|
24
24
|
// colour can begin with it, so a stored literal can never be misread as one.
|
|
25
|
-
|
|
25
|
+
// Sourced from the contract (sc-8326): Mason's plan validator reads the same
|
|
26
|
+
// grammar from there, and two spellings of it would drift.
|
|
27
|
+
const PREFIX = THEME_TOKEN_REF_PREFIX;
|
|
26
28
|
|
|
27
29
|
/** True when `value` is a theme-token binding rather than a literal colour. */
|
|
28
30
|
export function isThemeTokenRef(value) {
|
|
@@ -38,13 +40,11 @@ export function isThemeTokenRef(value) {
|
|
|
38
40
|
* un-bound field does. A malformed binding must never paint blank.
|
|
39
41
|
*/
|
|
40
42
|
export function resolveThemeTokenRef(value, theme) {
|
|
41
|
-
if (!isThemeTokenRef(value)) return undefined;
|
|
42
|
-
const path = value.slice(PREFIX.length);
|
|
43
43
|
// The allowlist is the contract's, so what the Studio may WRITE and what a
|
|
44
|
-
// host will RESOLVE cannot drift apart.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
// host will RESOLVE cannot drift apart. sc-8326 moved the test itself there
|
|
45
|
+
// too, since Mason's plan validator has to ask the same question.
|
|
46
|
+
if (!isThemeColorTokenRef(value)) return undefined;
|
|
47
|
+
const path = value.slice(PREFIX.length);
|
|
48
48
|
const resolved = path
|
|
49
49
|
.split(".")
|
|
50
50
|
.reduce((o, k) => (o == null ? undefined : o[k]), theme);
|
|
@@ -196,3 +196,27 @@ export function resolveThemeTokenRefs(props, theme, schemas) {
|
|
|
196
196
|
}
|
|
197
197
|
return out;
|
|
198
198
|
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* sc-8326 — resolve the `theme:` stops of a LAYOUT CONTAINER's gradient wash.
|
|
202
|
+
*
|
|
203
|
+
* A container's style never reaches `resolveThemeTokenRefs`: containers are
|
|
204
|
+
* painted by the page renderer, not by WidgetHost, which is why sc-7800 left
|
|
205
|
+
* them unbindable. Their gradient is the one place that needs a binding —
|
|
206
|
+
* sc-8197 gave the flat fill a theme-following `surface` role, but a two-stop
|
|
207
|
+
* wash has no role form, so Mason's colour-band opener could only freeze two
|
|
208
|
+
* literals out of the palette that happened to be active at build time.
|
|
209
|
+
*
|
|
210
|
+
* Resolution happens HERE, BEFORE `normaliseComponentGradient`: that hex
|
|
211
|
+
* grammar stays the injection guard for stops reaching CSS and generated export
|
|
212
|
+
* source, and an unresolvable binding fails it like any other non-hex value,
|
|
213
|
+
* dropping the wash instead of painting it blank.
|
|
214
|
+
*
|
|
215
|
+
* @param {unknown} gradient — a raw `style.backgroundGradient`.
|
|
216
|
+
* @param {object} theme — the resolved widget theme.
|
|
217
|
+
* @returns {unknown} the same value with every bound stop resolved.
|
|
218
|
+
*/
|
|
219
|
+
export function resolveGradientTokenStops(gradient, theme) {
|
|
220
|
+
if (!isPlainObject(gradient)) return gradient;
|
|
221
|
+
return resolveStyleBag(gradient, theme, 1);
|
|
222
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.153.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",
|