@colixsystems/widget-sdk 0.97.0 → 0.98.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 +31 -1
- package/dist/contract.cjs +57 -1
- package/dist/contract.js +57 -1
- package/dist/host.d.ts +49 -0
- package/dist/host.js +16 -0
- package/dist/navigation.cjs +124 -0
- package/dist/navigation.js +113 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -64,7 +64,37 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
64
64
|
|
|
65
65
|
## Status
|
|
66
66
|
|
|
67
|
-
`v0.
|
|
67
|
+
`v0.95.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**.
|
|
68
|
+
|
|
69
|
+
### What's new in 0.95.0 (contract 1.68.0)
|
|
70
|
+
|
|
71
|
+
**One number for the quick bar's cap — `quickBarMaxItems` + `quickBarCap`.** The sidebar's secondary mobile quick bar caps at five, and that five lived in two places: a named constant in the web chrome and a bare literal in the compiler. Nothing stopped them drifting, and no Studio surface could state the number at all.
|
|
72
|
+
|
|
73
|
+
`CONTRACT.themeMenuTypes.*.quickBarMaxItems` now carries it (5 on `sidebar`, `null` on the two shapes that draw no quick bar), read through the new `quickBarCap(menuType)` host export.
|
|
74
|
+
|
|
75
|
+
The distinction from `menuItemCap` is the point and is worth keeping straight: **`quickBarCap` may drop a page** — the rail and the drawer still list every menu page, so the bar is a shortcut. **`menuItemCap` may not** — where the chrome IS the menu (`bottom-tabs`), the surplus moves behind a More sheet instead.
|
|
76
|
+
|
|
77
|
+
Host-integration surface only. `CONTRACT.version` → `1.68.0`.
|
|
78
|
+
|
|
79
|
+
### What's new in 0.94.0 (contract 1.67.0)
|
|
80
|
+
|
|
81
|
+
**The footer strip is themeable — `resolveFooterTokens` (REQ-NAV-STRUCTURE).** The bottom strip's surface was hard-coded white on both hosts and its items read the SIDEBAR's tokens. That is fine while the strip is the sidebar's secondary quick bar, and untenable once it IS the menu: the `bottom-tabs` shape hides the sidebar panel, so those tokens have nowhere to be set.
|
|
82
|
+
|
|
83
|
+
A `theme_config.footer` block now carries `backgroundColor`, `textColor`, `activeColor` and the opt-in divider `borderColor` + `borderWidth`. **Every field falls back to the sidebar's**, so a workspace that never touches it renders exactly as before and only an explicit value moves anything.
|
|
84
|
+
|
|
85
|
+
`resolveFooterTokens(theme)` (from `@colixsystems/widget-sdk/host`) is the one resolver both hosts read it with. It also settles two divergences the strip carried: the native bar ruled a permanent `#e2e8f0` hairline the theme could not reach — REQ-THEME-LOOK's rule is that the divider's **colour** is its switch — and it tinted the active tab's *label* where the web painted a filled pill, so `activeStyle: "filled"` meant two different things per host.
|
|
86
|
+
|
|
87
|
+
Host-integration surface only: no author-facing hook, prop, primitive, or manifest field changed. `CONTRACT.version` → `1.67.0`.
|
|
88
|
+
|
|
89
|
+
### What's new in 0.93.0 (contract 1.66.0)
|
|
90
|
+
|
|
91
|
+
**An app picks the SHAPE its navigation takes — `CONTRACT.themeMenuTypes` (REQ-NAV-STRUCTURE).** Until now the chrome was always a sidebar: a persistent left rail on desktop, a hamburger drawer plus an optional bottom quick bar on mobile. That is the right default for an admin tool and the wrong one for a phone-first app or a site, and there was no way to say so. `CONTRACT.themeMenuTypes` publishes the closed catalogue — `sidebar`, `top-bar`, `bottom-tabs` — each entry carrying `{ name, summary, maxItems }`.
|
|
92
|
+
|
|
93
|
+
`maxItems` caps how many menu pages the chrome draws at once, and its meaning differs per type deliberately: the sidebar's mobile quick bar is a **secondary** curated bar whose cap may drop a page (the rail still lists every one), while a `bottom-tabs` strip **is** the menu, so its cap must never drop one — the surplus moves behind a More sheet instead.
|
|
94
|
+
|
|
95
|
+
**New host exports (`@colixsystems/widget-sdk/host`)** — `normaliseNavigation(navigation)` resolves a stored `theme_config.navigation` block to the `{ menuType }` a host switches its chrome on, and `menuItemCap(menuType)` states that shape's cap. One implementation for both hosts, so a menu type cannot mean one thing in the web Player and another in the exported Expo app. An absent or unknown value resolves to `sidebar`, so every app authored before menu types existed renders and compiles byte-identically.
|
|
96
|
+
|
|
97
|
+
Host-integration surface only: no author-facing hook, prop, primitive, or manifest field changed. `CONTRACT.version` → `1.66.0`.
|
|
68
98
|
|
|
69
99
|
### What's new in 0.97.0 (contract 1.69.0)
|
|
70
100
|
|
package/dist/contract.cjs
CHANGED
|
@@ -159,6 +159,42 @@ const THEME_WIDGET_STYLES = Object.freeze({
|
|
|
159
159
|
// Matches the styleSchema field cap the widget agent is held to.
|
|
160
160
|
maxFieldsPerWidget: 12,
|
|
161
161
|
});
|
|
162
|
+
// REQ-NAV-STRUCTURE: the SHAPE an app's navigation takes. One catalogue, four
|
|
163
|
+
// consumers -- the Studio's Navigation page, Mason's set_theme coercion, the web
|
|
164
|
+
// PlayerChrome and the compiler's navigator -- so a type cannot be offered to an
|
|
165
|
+
// author without every host actually drawing it.
|
|
166
|
+
//
|
|
167
|
+
// `maxItems` caps how many menu pages the chrome draws at once, and its meaning
|
|
168
|
+
// differs per type deliberately. The sidebar's mobile quick bar is a SECONDARY
|
|
169
|
+
// curated bar, so its own cap may drop a page -- the rail still lists every one.
|
|
170
|
+
// A bottom-tabs strip IS the menu, so its cap must never drop one: the surplus
|
|
171
|
+
// moves behind a "More" sheet instead.
|
|
172
|
+
const THEME_MENU_TYPES = Object.freeze({
|
|
173
|
+
sidebar: Object.freeze({
|
|
174
|
+
name: "Sidebar",
|
|
175
|
+
summary:
|
|
176
|
+
"A persistent left rail on desktop; a hamburger drawer plus the optional bottom quick bar on mobile.",
|
|
177
|
+
maxItems: null,
|
|
178
|
+
// The SECONDARY mobile quick bar's cap. It may drop a page past it, because
|
|
179
|
+
// the rail and the drawer still list every one -- the opposite of
|
|
180
|
+
// `bottom-tabs`' `maxItems`, where the strip IS the menu.
|
|
181
|
+
quickBarMaxItems: 5,
|
|
182
|
+
}),
|
|
183
|
+
"top-bar": Object.freeze({
|
|
184
|
+
name: "Top bar",
|
|
185
|
+
summary:
|
|
186
|
+
"A horizontal row of links in the app header, scrolling sideways when it runs out of room. No rail at any width.",
|
|
187
|
+
maxItems: null,
|
|
188
|
+
quickBarMaxItems: null,
|
|
189
|
+
}),
|
|
190
|
+
"bottom-tabs": Object.freeze({
|
|
191
|
+
name: "Bottom tabs",
|
|
192
|
+
summary:
|
|
193
|
+
"A sticky bottom strip at every width -- the phone-native shape. Pages past the cap move behind a More sheet.",
|
|
194
|
+
maxItems: 4,
|
|
195
|
+
quickBarMaxItems: null,
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
162
198
|
const THEME_SPACING_SCALE = Object.freeze({
|
|
163
199
|
min: 0.5,
|
|
164
200
|
max: 2,
|
|
@@ -3065,7 +3101,26 @@ const CONTRACT = deepFreeze({
|
|
|
3065
3101
|
// shadcn/Tailwind import's matching custom properties have a themeConfig
|
|
3066
3102
|
// home instead of being reported lost. Host-only plumbing: no scope
|
|
3067
3103
|
// declares `universalFields`, so a third-party manifest is unaffected.
|
|
3068
|
-
|
|
3104
|
+
// 1.70.0: additive (REQ-NAV-STRUCTURE) -- `themeMenuTypes`, the closed
|
|
3105
|
+
// catalogue of navigation shapes an app can take (`sidebar`, `top-bar`,
|
|
3106
|
+
// `bottom-tabs`) with the per-type item cap. Read from `theme_config`
|
|
3107
|
+
// as `navigation.menuType`; absent or unknown resolves to `sidebar`, so
|
|
3108
|
+
// every app that exists today renders and compiles byte-identically.
|
|
3109
|
+
// Resolved through the new `normaliseNavigation` host export rather than
|
|
3110
|
+
// a literal per host.
|
|
3111
|
+
// 1.71.0: additive (REQ-NAV-STRUCTURE) -- `resolveFooterTokens` (host
|
|
3112
|
+
// export). The footer strip's surface was hard-coded white on BOTH hosts
|
|
3113
|
+
// and its items read the SIDEBAR's tokens, which stops being tenable the
|
|
3114
|
+
// moment the strip IS the menu (`bottom-tabs` hides the sidebar panel, so
|
|
3115
|
+
// those tokens have nowhere to be set). A `theme_config.footer` block now
|
|
3116
|
+
// carries its own, every field falling back to the sidebar's so an
|
|
3117
|
+
// untouched workspace is unchanged.
|
|
3118
|
+
// 1.72.0: additive (REQ-NAV-STRUCTURE) -- `themeMenuTypes.*.quickBarMaxItems`
|
|
3119
|
+
// and the `quickBarCap` host export. The sidebar's secondary quick bar
|
|
3120
|
+
// capped at five in TWO places -- a named constant on web and a bare `5`
|
|
3121
|
+
// literal in the compiler -- so the number could drift between the hosts
|
|
3122
|
+
// and no Studio surface could state it at all.
|
|
3123
|
+
version: "1.72.0",
|
|
3069
3124
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3070
3125
|
hooks: HOOKS,
|
|
3071
3126
|
primitives: PRIMITIVES,
|
|
@@ -3081,6 +3136,7 @@ const CONTRACT = deepFreeze({
|
|
|
3081
3136
|
themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
|
|
3082
3137
|
themeComponentGradient: THEME_COMPONENT_GRADIENT,
|
|
3083
3138
|
themeSpacingScale: THEME_SPACING_SCALE,
|
|
3139
|
+
themeMenuTypes: THEME_MENU_TYPES,
|
|
3084
3140
|
themeWidgetStyles: THEME_WIDGET_STYLES,
|
|
3085
3141
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
3086
3142
|
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|
package/dist/contract.js
CHANGED
|
@@ -159,6 +159,42 @@ const THEME_WIDGET_STYLES = Object.freeze({
|
|
|
159
159
|
// Matches the styleSchema field cap the widget agent is held to.
|
|
160
160
|
maxFieldsPerWidget: 12,
|
|
161
161
|
});
|
|
162
|
+
// REQ-NAV-STRUCTURE: the SHAPE an app's navigation takes. One catalogue, four
|
|
163
|
+
// consumers -- the Studio's Navigation page, Mason's set_theme coercion, the web
|
|
164
|
+
// PlayerChrome and the compiler's navigator -- so a type cannot be offered to an
|
|
165
|
+
// author without every host actually drawing it.
|
|
166
|
+
//
|
|
167
|
+
// `maxItems` caps how many menu pages the chrome draws at once, and its meaning
|
|
168
|
+
// differs per type deliberately. The sidebar's mobile quick bar is a SECONDARY
|
|
169
|
+
// curated bar, so its own cap may drop a page -- the rail still lists every one.
|
|
170
|
+
// A bottom-tabs strip IS the menu, so its cap must never drop one: the surplus
|
|
171
|
+
// moves behind a "More" sheet instead.
|
|
172
|
+
const THEME_MENU_TYPES = Object.freeze({
|
|
173
|
+
sidebar: Object.freeze({
|
|
174
|
+
name: "Sidebar",
|
|
175
|
+
summary:
|
|
176
|
+
"A persistent left rail on desktop; a hamburger drawer plus the optional bottom quick bar on mobile.",
|
|
177
|
+
maxItems: null,
|
|
178
|
+
// The SECONDARY mobile quick bar's cap. It may drop a page past it, because
|
|
179
|
+
// the rail and the drawer still list every one -- the opposite of
|
|
180
|
+
// `bottom-tabs`' `maxItems`, where the strip IS the menu.
|
|
181
|
+
quickBarMaxItems: 5,
|
|
182
|
+
}),
|
|
183
|
+
"top-bar": Object.freeze({
|
|
184
|
+
name: "Top bar",
|
|
185
|
+
summary:
|
|
186
|
+
"A horizontal row of links in the app header, scrolling sideways when it runs out of room. No rail at any width.",
|
|
187
|
+
maxItems: null,
|
|
188
|
+
quickBarMaxItems: null,
|
|
189
|
+
}),
|
|
190
|
+
"bottom-tabs": Object.freeze({
|
|
191
|
+
name: "Bottom tabs",
|
|
192
|
+
summary:
|
|
193
|
+
"A sticky bottom strip at every width -- the phone-native shape. Pages past the cap move behind a More sheet.",
|
|
194
|
+
maxItems: 4,
|
|
195
|
+
quickBarMaxItems: null,
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
162
198
|
const THEME_SPACING_SCALE = Object.freeze({
|
|
163
199
|
min: 0.5,
|
|
164
200
|
max: 2,
|
|
@@ -3065,7 +3101,26 @@ const CONTRACT = deepFreeze({
|
|
|
3065
3101
|
// shadcn/Tailwind import's matching custom properties have a themeConfig
|
|
3066
3102
|
// home instead of being reported lost. Host-only plumbing: no scope
|
|
3067
3103
|
// declares `universalFields`, so a third-party manifest is unaffected.
|
|
3068
|
-
|
|
3104
|
+
// 1.70.0: additive (REQ-NAV-STRUCTURE) -- `themeMenuTypes`, the closed
|
|
3105
|
+
// catalogue of navigation shapes an app can take (`sidebar`, `top-bar`,
|
|
3106
|
+
// `bottom-tabs`) with the per-type item cap. Read from `theme_config`
|
|
3107
|
+
// as `navigation.menuType`; absent or unknown resolves to `sidebar`, so
|
|
3108
|
+
// every app that exists today renders and compiles byte-identically.
|
|
3109
|
+
// Resolved through the new `normaliseNavigation` host export rather than
|
|
3110
|
+
// a literal per host.
|
|
3111
|
+
// 1.71.0: additive (REQ-NAV-STRUCTURE) -- `resolveFooterTokens` (host
|
|
3112
|
+
// export). The footer strip's surface was hard-coded white on BOTH hosts
|
|
3113
|
+
// and its items read the SIDEBAR's tokens, which stops being tenable the
|
|
3114
|
+
// moment the strip IS the menu (`bottom-tabs` hides the sidebar panel, so
|
|
3115
|
+
// those tokens have nowhere to be set). A `theme_config.footer` block now
|
|
3116
|
+
// carries its own, every field falling back to the sidebar's so an
|
|
3117
|
+
// untouched workspace is unchanged.
|
|
3118
|
+
// 1.72.0: additive (REQ-NAV-STRUCTURE) -- `themeMenuTypes.*.quickBarMaxItems`
|
|
3119
|
+
// and the `quickBarCap` host export. The sidebar's secondary quick bar
|
|
3120
|
+
// capped at five in TWO places -- a named constant on web and a bare `5`
|
|
3121
|
+
// literal in the compiler -- so the number could drift between the hosts
|
|
3122
|
+
// and no Studio surface could state it at all.
|
|
3123
|
+
version: "1.72.0",
|
|
3069
3124
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3070
3125
|
hooks: HOOKS,
|
|
3071
3126
|
primitives: PRIMITIVES,
|
|
@@ -3081,6 +3136,7 @@ const CONTRACT = deepFreeze({
|
|
|
3081
3136
|
themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
|
|
3082
3137
|
themeComponentGradient: THEME_COMPONENT_GRADIENT,
|
|
3083
3138
|
themeSpacingScale: THEME_SPACING_SCALE,
|
|
3139
|
+
themeMenuTypes: THEME_MENU_TYPES,
|
|
3084
3140
|
themeWidgetStyles: THEME_WIDGET_STYLES,
|
|
3085
3141
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
3086
3142
|
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|
package/dist/host.d.ts
CHANGED
|
@@ -133,3 +133,52 @@ export function resolveToastTokens(
|
|
|
133
133
|
export function createToastController(
|
|
134
134
|
opts?: ToastControllerOptions,
|
|
135
135
|
): ToastController;
|
|
136
|
+
|
|
137
|
+
// REQ-NAV-STRUCTURE — the navigation SHAPE a host draws its chrome in. The
|
|
138
|
+
// vocabulary is closed by `CONTRACT.themeMenuTypes`; these are the resolvers
|
|
139
|
+
// both hosts switch on.
|
|
140
|
+
|
|
141
|
+
export type ThemeMenuType = "sidebar" | "top-bar" | "bottom-tabs";
|
|
142
|
+
|
|
143
|
+
export interface ResolvedNavigation {
|
|
144
|
+
menuType: ThemeMenuType;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolves a stored `theme_config.navigation` block. Always returns a usable
|
|
149
|
+
* shape — a junk, partial, or absent input yields the default sidebar, so a
|
|
150
|
+
* host never has to guard the value it switches on.
|
|
151
|
+
*/
|
|
152
|
+
export function normaliseNavigation(navigation: unknown): ResolvedNavigation;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* How many menu pages this shape may draw at once, or `null` for no cap. The
|
|
156
|
+
* cap's meaning differs per type: the sidebar's quick bar may drop a page past
|
|
157
|
+
* it, a bottom-tabs strip must move its surplus behind a More sheet.
|
|
158
|
+
*/
|
|
159
|
+
export function menuItemCap(menuType: string): number | null;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* How many pages the SECONDARY mobile quick bar draws, or `null` where the shape
|
|
163
|
+
* draws none (only `sidebar` has one). Unlike `menuItemCap` this cap MAY drop a
|
|
164
|
+
* page: the rail and drawer still list every menu page.
|
|
165
|
+
*/
|
|
166
|
+
export function quickBarCap(menuType: string): number | null;
|
|
167
|
+
|
|
168
|
+
/** The footer strip's resolved tokens. A `null` colour means the host keeps its
|
|
169
|
+
* own default; a `null` `borderColor` means no divider is drawn at all. */
|
|
170
|
+
export interface FooterTokens {
|
|
171
|
+
backgroundColor: string | null;
|
|
172
|
+
textColor: string | null;
|
|
173
|
+
activeColor: string | null;
|
|
174
|
+
borderColor: string | null;
|
|
175
|
+
borderWidth: number | null;
|
|
176
|
+
activeStyle: "filled" | "accent";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Resolves the footer strip's tokens from a whole `theme_config`. Every field
|
|
181
|
+
* falls back to the sidebar's, so a workspace that never opens the Footer panel
|
|
182
|
+
* renders exactly as it did before the block existed.
|
|
183
|
+
*/
|
|
184
|
+
export function resolveFooterTokens(theme: unknown): FooterTokens;
|
package/dist/host.js
CHANGED
|
@@ -39,3 +39,19 @@ export {
|
|
|
39
39
|
normalizeToastKind,
|
|
40
40
|
TOAST_DEFAULTS,
|
|
41
41
|
} from "./toast-host.js";
|
|
42
|
+
|
|
43
|
+
// REQ-NAV-STRUCTURE: the navigation-shape resolver both hosts draw their chrome
|
|
44
|
+
// from. `normaliseNavigation` turns a stored `theme_config.navigation` block into
|
|
45
|
+
// the resolved `{ menuType }` the chrome switches on; `menuItemCap` states how
|
|
46
|
+
// many menu pages that shape may draw at once. One implementation, so a menu
|
|
47
|
+
// type cannot mean one thing in the Player and another in the Expo export.
|
|
48
|
+
// `resolveFooterTokens` is the footer strip's own token set, falling back to
|
|
49
|
+
// the sidebar's so an untouched workspace keeps the appearance it has. It
|
|
50
|
+
// exists because the strip IS the menu under `bottom-tabs`, where the sidebar
|
|
51
|
+
// panel is hidden and those tokens can no longer be set at all.
|
|
52
|
+
export {
|
|
53
|
+
normaliseNavigation,
|
|
54
|
+
menuItemCap,
|
|
55
|
+
quickBarCap,
|
|
56
|
+
resolveFooterTokens,
|
|
57
|
+
} from "./navigation.js";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// CommonJS mirror of navigation.js — the CJS compiler and the Mason build runner
|
|
2
|
+
// resolve a `theme_config.navigation` block with the SAME rules the web host
|
|
3
|
+
// applies, so an app's menu type cannot mean one thing on web and another in the
|
|
4
|
+
// exported Expo app.
|
|
5
|
+
//
|
|
6
|
+
// The BODY below is copied VERBATIM from navigation.js; only the module syntax
|
|
7
|
+
// differs. navigation-parity.test.js pins both facts — identical bodies AND
|
|
8
|
+
// identical behaviour over a shared case table — so drift fails CI.
|
|
9
|
+
|
|
10
|
+
// REQ-NAV-STRUCTURE — the host side of an app's navigation SHAPE and the
|
|
11
|
+
// tokens its footer strip is painted with.
|
|
12
|
+
//
|
|
13
|
+
// Host-integration surface, re-exported from `host.js`: consumed by the platform
|
|
14
|
+
// hosts that draw an app's chrome (the web PlayerChrome, the compiler that emits
|
|
15
|
+
// the exported Expo app's navigator) and by the Studio surface that authors it —
|
|
16
|
+
// never by a widget author.
|
|
17
|
+
//
|
|
18
|
+
// Living here — one implementation both hosts import — is what makes a menu type
|
|
19
|
+
// behave identically in the Player and the export (widget-parity skill). The
|
|
20
|
+
// alternative, a resolver copied per host, is the drift this file exists to
|
|
21
|
+
// prevent.
|
|
22
|
+
//
|
|
23
|
+
// `CONTRACT.themeMenuTypes` is the single source of the vocabulary: which shapes
|
|
24
|
+
// exist and each one's item cap. Mason's `set_theme` coercion validates against
|
|
25
|
+
// the SAME literal, so what a planner may persist and what a host draws cannot
|
|
26
|
+
// diverge.
|
|
27
|
+
|
|
28
|
+
const { CONTRACT } = require("./contract.cjs");
|
|
29
|
+
|
|
30
|
+
// Absent or unknown resolves here, so every app authored before menu types
|
|
31
|
+
// existed renders and compiles byte-identically.
|
|
32
|
+
const DEFAULT_MENU_TYPE = "sidebar";
|
|
33
|
+
|
|
34
|
+
function isPlainObject(value) {
|
|
35
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a `theme_config.navigation` block to the shape both hosts read.
|
|
40
|
+
*
|
|
41
|
+
* Always returns a fully resolved object — a junk, partial, or absent input
|
|
42
|
+
* yields the default sidebar rather than something a host has to guard against.
|
|
43
|
+
*
|
|
44
|
+
* @param {unknown} navigation — the raw `theme_config.navigation` value.
|
|
45
|
+
* @returns {{ menuType: string }} the resolved navigation structure.
|
|
46
|
+
*/
|
|
47
|
+
function normaliseNavigation(navigation) {
|
|
48
|
+
const raw = isPlainObject(navigation) ? navigation.menuType : undefined;
|
|
49
|
+
const menuType =
|
|
50
|
+
typeof raw === "string" && Object.hasOwn(CONTRACT.themeMenuTypes, raw)
|
|
51
|
+
? raw
|
|
52
|
+
: DEFAULT_MENU_TYPE;
|
|
53
|
+
return { menuType };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* How many MENU pages the chrome may draw at once, or `null` for no cap.
|
|
58
|
+
*
|
|
59
|
+
* Where the chrome IS the menu (`bottom-tabs`) this cap must never drop a page:
|
|
60
|
+
* the surplus moves behind a More sheet. Contrast `quickBarCap` below.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} menuType — a resolved menu type.
|
|
63
|
+
* @returns {number|null}
|
|
64
|
+
*/
|
|
65
|
+
function menuItemCap(menuType) {
|
|
66
|
+
const entry = CONTRACT.themeMenuTypes[menuType];
|
|
67
|
+
return entry ? entry.maxItems : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* How many pages the SECONDARY mobile quick bar draws, or `null` where that
|
|
72
|
+
* shape draws none. Only the `sidebar` shape has one.
|
|
73
|
+
*
|
|
74
|
+
* This cap MAY drop a page, and that is the difference from `menuItemCap`: the
|
|
75
|
+
* rail and the drawer still list every menu page, so the bar is a shortcut
|
|
76
|
+
* rather than the menu. It lived as a constant on one host and a literal on the
|
|
77
|
+
* other before this, which is exactly how the two would have drifted.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} menuType — a resolved menu type.
|
|
80
|
+
* @returns {number|null}
|
|
81
|
+
*/
|
|
82
|
+
function quickBarCap(menuType) {
|
|
83
|
+
const entry = CONTRACT.themeMenuTypes[menuType];
|
|
84
|
+
return entry ? entry.quickBarMaxItems : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The strip's surface was hard-coded white on BOTH hosts and its items read the
|
|
88
|
+
// sidebar's tokens — which is fine while it is the sidebar's secondary quick bar
|
|
89
|
+
// and fatal once it IS the menu (`bottom-tabs` hides the sidebar panel, so those
|
|
90
|
+
// tokens have nowhere to be set). `footer` gives it tokens of its own.
|
|
91
|
+
//
|
|
92
|
+
// Every field falls back to the sidebar's, so a workspace that never opens the
|
|
93
|
+
// Footer panel keeps exactly the appearance it has today; only an explicit
|
|
94
|
+
// `footer` value moves anything.
|
|
95
|
+
function resolveFooterTokens(theme) {
|
|
96
|
+
const config = isPlainObject(theme) ? theme : {};
|
|
97
|
+
const footer = isPlainObject(config.footer) ? config.footer : {};
|
|
98
|
+
const sidebar = isPlainObject(config.sidebar) ? config.sidebar : {};
|
|
99
|
+
const pick = (key) => footer[key] || sidebar[key] || null;
|
|
100
|
+
const borderColor = pick("borderColor");
|
|
101
|
+
const activeStyle = footer.activeStyle || sidebar.activeStyle;
|
|
102
|
+
return {
|
|
103
|
+
backgroundColor: pick("backgroundColor"),
|
|
104
|
+
textColor: pick("textColor"),
|
|
105
|
+
// Unlike the others this has no null state on either host: an unset active
|
|
106
|
+
// colour resolves to the brand primary, which the caller supplies.
|
|
107
|
+
activeColor: pick("activeColor"),
|
|
108
|
+
// REQ-THEME-LOOK: the divider's COLOUR is its switch. Unset draws no line at
|
|
109
|
+
// all — which the native quick bar did not honour before this, ruling a
|
|
110
|
+
// hairline across a tinted strip that had asked for none.
|
|
111
|
+
borderColor,
|
|
112
|
+
borderWidth: borderColor ? borderWidthOr(footer.borderWidth, sidebar.borderWidth) : null,
|
|
113
|
+
activeStyle: activeStyle === "accent" ? "accent" : "filled",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function borderWidthOr(...values) {
|
|
118
|
+
for (const value of values) {
|
|
119
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
120
|
+
}
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { normaliseNavigation, menuItemCap, quickBarCap, resolveFooterTokens };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// REQ-NAV-STRUCTURE — the host side of an app's navigation SHAPE and the
|
|
2
|
+
// tokens its footer strip is painted with.
|
|
3
|
+
//
|
|
4
|
+
// Host-integration surface, re-exported from `host.js`: consumed by the platform
|
|
5
|
+
// hosts that draw an app's chrome (the web PlayerChrome, the compiler that emits
|
|
6
|
+
// the exported Expo app's navigator) and by the Studio surface that authors it —
|
|
7
|
+
// never by a widget author.
|
|
8
|
+
//
|
|
9
|
+
// Living here — one implementation both hosts import — is what makes a menu type
|
|
10
|
+
// behave identically in the Player and the export (widget-parity skill). The
|
|
11
|
+
// alternative, a resolver copied per host, is the drift this file exists to
|
|
12
|
+
// prevent.
|
|
13
|
+
//
|
|
14
|
+
// `CONTRACT.themeMenuTypes` is the single source of the vocabulary: which shapes
|
|
15
|
+
// exist and each one's item cap. Mason's `set_theme` coercion validates against
|
|
16
|
+
// the SAME literal, so what a planner may persist and what a host draws cannot
|
|
17
|
+
// diverge.
|
|
18
|
+
|
|
19
|
+
import { CONTRACT } from "./contract.js";
|
|
20
|
+
|
|
21
|
+
// Absent or unknown resolves here, so every app authored before menu types
|
|
22
|
+
// existed renders and compiles byte-identically.
|
|
23
|
+
const DEFAULT_MENU_TYPE = "sidebar";
|
|
24
|
+
|
|
25
|
+
function isPlainObject(value) {
|
|
26
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a `theme_config.navigation` block to the shape both hosts read.
|
|
31
|
+
*
|
|
32
|
+
* Always returns a fully resolved object — a junk, partial, or absent input
|
|
33
|
+
* yields the default sidebar rather than something a host has to guard against.
|
|
34
|
+
*
|
|
35
|
+
* @param {unknown} navigation — the raw `theme_config.navigation` value.
|
|
36
|
+
* @returns {{ menuType: string }} the resolved navigation structure.
|
|
37
|
+
*/
|
|
38
|
+
export function normaliseNavigation(navigation) {
|
|
39
|
+
const raw = isPlainObject(navigation) ? navigation.menuType : undefined;
|
|
40
|
+
const menuType =
|
|
41
|
+
typeof raw === "string" && Object.hasOwn(CONTRACT.themeMenuTypes, raw)
|
|
42
|
+
? raw
|
|
43
|
+
: DEFAULT_MENU_TYPE;
|
|
44
|
+
return { menuType };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* How many MENU pages the chrome may draw at once, or `null` for no cap.
|
|
49
|
+
*
|
|
50
|
+
* Where the chrome IS the menu (`bottom-tabs`) this cap must never drop a page:
|
|
51
|
+
* the surplus moves behind a More sheet. Contrast `quickBarCap` below.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} menuType — a resolved menu type.
|
|
54
|
+
* @returns {number|null}
|
|
55
|
+
*/
|
|
56
|
+
export function menuItemCap(menuType) {
|
|
57
|
+
const entry = CONTRACT.themeMenuTypes[menuType];
|
|
58
|
+
return entry ? entry.maxItems : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* How many pages the SECONDARY mobile quick bar draws, or `null` where that
|
|
63
|
+
* shape draws none. Only the `sidebar` shape has one.
|
|
64
|
+
*
|
|
65
|
+
* This cap MAY drop a page, and that is the difference from `menuItemCap`: the
|
|
66
|
+
* rail and the drawer still list every menu page, so the bar is a shortcut
|
|
67
|
+
* rather than the menu. It lived as a constant on one host and a literal on the
|
|
68
|
+
* other before this, which is exactly how the two would have drifted.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} menuType — a resolved menu type.
|
|
71
|
+
* @returns {number|null}
|
|
72
|
+
*/
|
|
73
|
+
export function quickBarCap(menuType) {
|
|
74
|
+
const entry = CONTRACT.themeMenuTypes[menuType];
|
|
75
|
+
return entry ? entry.quickBarMaxItems : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The strip's surface was hard-coded white on BOTH hosts and its items read the
|
|
79
|
+
// sidebar's tokens — which is fine while it is the sidebar's secondary quick bar
|
|
80
|
+
// and fatal once it IS the menu (`bottom-tabs` hides the sidebar panel, so those
|
|
81
|
+
// tokens have nowhere to be set). `footer` gives it tokens of its own.
|
|
82
|
+
//
|
|
83
|
+
// Every field falls back to the sidebar's, so a workspace that never opens the
|
|
84
|
+
// Footer panel keeps exactly the appearance it has today; only an explicit
|
|
85
|
+
// `footer` value moves anything.
|
|
86
|
+
export function resolveFooterTokens(theme) {
|
|
87
|
+
const config = isPlainObject(theme) ? theme : {};
|
|
88
|
+
const footer = isPlainObject(config.footer) ? config.footer : {};
|
|
89
|
+
const sidebar = isPlainObject(config.sidebar) ? config.sidebar : {};
|
|
90
|
+
const pick = (key) => footer[key] || sidebar[key] || null;
|
|
91
|
+
const borderColor = pick("borderColor");
|
|
92
|
+
const activeStyle = footer.activeStyle || sidebar.activeStyle;
|
|
93
|
+
return {
|
|
94
|
+
backgroundColor: pick("backgroundColor"),
|
|
95
|
+
textColor: pick("textColor"),
|
|
96
|
+
// Unlike the others this has no null state on either host: an unset active
|
|
97
|
+
// colour resolves to the brand primary, which the caller supplies.
|
|
98
|
+
activeColor: pick("activeColor"),
|
|
99
|
+
// REQ-THEME-LOOK: the divider's COLOUR is its switch. Unset draws no line at
|
|
100
|
+
// all — which the native quick bar did not honour before this, ruling a
|
|
101
|
+
// hairline across a tinted strip that had asked for none.
|
|
102
|
+
borderColor,
|
|
103
|
+
borderWidth: borderColor ? borderWidthOr(footer.borderWidth, sidebar.borderWidth) : null,
|
|
104
|
+
activeStyle: activeStyle === "accent" ? "accent" : "filled",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function borderWidthOr(...values) {
|
|
109
|
+
for (const value of values) {
|
|
110
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
111
|
+
}
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.98.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "node scripts/build.js",
|
|
51
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/theme-components-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/toast-host.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js"
|
|
51
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/toast-host.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=18"
|