@colixsystems/widget-sdk 0.96.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 +36 -1
- package/dist/contract.cjs +166 -1
- package/dist/contract.js +166 -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,42 @@ 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`.
|
|
98
|
+
|
|
99
|
+
### What's new in 0.97.0 (contract 1.69.0)
|
|
100
|
+
|
|
101
|
+
**`CONTRACT.themeComponents` gains five scopes: `accent`, `destructive`, `muted`, `popover`, `ring` (sc-5392).** The vocabulary shipped with exactly `button`/`card`/`text` (sc-1497), so a shadcn/Tailwind app import's `--accent`, `--destructive`, `--muted`, `--popover` and `--ring` custom properties had no `themeConfig` home and were reported "no theme home" on every import. Each new scope binds to real `styleSchema` fields on the built-ins that already had a matching surface — `accent` (a highlight/tag surface: `background`/`borderColor`/`radius`) to the Label widget's own fields, `destructive` (a themed danger/delete action: `background`/`textColor`/`borderColor`) to a new "Danger" Button variant, `muted` (a subtle/secondary surface: `background`/`textColor`/`borderColor`/`radius`) to Form Input's and Form Builder's pre-existing input fields, `popover` (a dropdown/menu surface: `background`/`textColor`/`borderColor`) to the same two widgets' choice-field option list, and `ring` (the app-wide focus-visible outline: `color`/`width`) to a new emphasis border on Button. **This is HOST-ONLY plumbing, exactly like the three scopes before it** — `useTheme()`'s documented `components` slice is unchanged, no widget-authoring hook or `propertySchema` type moved, and no scope declares `universalFields`, so a third-party or AI-generated widget's contract is unaffected; the Developer guide and `DEFAULT_SYSTEM_PROMPT` need no update because neither ever documented this internal vocabulary. Fully additive: a theme with no `components` key, or one using only `button`/`card`/`text`, resolves exactly as before.
|
|
102
|
+
- **`CONTRACT.version` → `1.69.0`** (additive: five new `themeComponents` scopes + their target-field bindings). No existing scope, token, or export changed shape.
|
|
68
103
|
|
|
69
104
|
### What's new in 0.96.0 (contract 1.68.0)
|
|
70
105
|
|
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,
|
|
@@ -211,6 +247,34 @@ const CARD_UNIVERSAL_FIELDS = Object.freeze({
|
|
|
211
247
|
gradient: "cardGradient",
|
|
212
248
|
});
|
|
213
249
|
|
|
250
|
+
// The Form widgets' text-entry surface, already read by both form widgets
|
|
251
|
+
// (REQ-THEME-13, predating this scope) -- a genuine subtle/secondary surface
|
|
252
|
+
// distinct from the card that hosts it.
|
|
253
|
+
const MUTED_INPUT_FIELDS = Object.freeze({
|
|
254
|
+
background: "inputBackground",
|
|
255
|
+
textColor: "inputTextColor",
|
|
256
|
+
borderColor: "inputBorderColor",
|
|
257
|
+
radius: "inputRadius",
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// The Form widgets' choice-field option list -- the closest thing this
|
|
261
|
+
// catalog has to a menu/dropdown surface (singleChoice/multiChoice render an
|
|
262
|
+
// inline list of options here).
|
|
263
|
+
const POPOVER_FIELDS = Object.freeze({
|
|
264
|
+
background: "popoverBackground",
|
|
265
|
+
textColor: "popoverTextColor",
|
|
266
|
+
borderColor: "popoverBorderColor",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// The Button widget's danger-variant fields -- kept separate from its own
|
|
270
|
+
// `background`/`textColor`/`borderColor` (the `button` scope's fields above)
|
|
271
|
+
// so a themed destructive action never collides with the button's normal look.
|
|
272
|
+
const DANGER_BUTTON_FIELDS = Object.freeze({
|
|
273
|
+
background: "dangerBackground",
|
|
274
|
+
textColor: "dangerTextColor",
|
|
275
|
+
borderColor: "dangerBorderColor",
|
|
276
|
+
});
|
|
277
|
+
|
|
214
278
|
const THEME_COMPONENTS = Object.freeze({
|
|
215
279
|
button: Object.freeze({
|
|
216
280
|
label: "Buttons",
|
|
@@ -277,6 +341,82 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
277
341
|
"appstudio.data-value": TEXT_TYPOGRAPHY_FIELDS,
|
|
278
342
|
}),
|
|
279
343
|
}),
|
|
344
|
+
// sc-5392 -- a highlight/tag surface. Bound to Label's OWN background/
|
|
345
|
+
// borderColor/radius fields (REQ-THEME-13, predating this scope) rather
|
|
346
|
+
// than its `color` field, which the `text` scope already owns -- two
|
|
347
|
+
// scopes writing the same field on the same widget would race.
|
|
348
|
+
accent: Object.freeze({
|
|
349
|
+
label: "Accent",
|
|
350
|
+
tokens: Object.freeze({
|
|
351
|
+
background: Object.freeze({ type: "color", uiDefault: "colors.primarySoft" }),
|
|
352
|
+
borderColor: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
|
|
353
|
+
radius: Object.freeze({ type: "size", min: 0, max: 48, uiDefault: "radii.pill" }),
|
|
354
|
+
}),
|
|
355
|
+
targets: Object.freeze({
|
|
356
|
+
"appstudio.label": Object.freeze({
|
|
357
|
+
background: "background",
|
|
358
|
+
borderColor: "borderColor",
|
|
359
|
+
radius: "radius",
|
|
360
|
+
}),
|
|
361
|
+
}),
|
|
362
|
+
}),
|
|
363
|
+
// sc-5392 -- the Button widget's danger/delete variant. Separate token set
|
|
364
|
+
// from `button` above so "make the buttons coral" never recolours Delete.
|
|
365
|
+
destructive: Object.freeze({
|
|
366
|
+
label: "Destructive",
|
|
367
|
+
tokens: Object.freeze({
|
|
368
|
+
background: Object.freeze({ type: "color", uiDefault: "colors.danger" }),
|
|
369
|
+
textColor: Object.freeze({ type: "color", uiDefault: "colors.onPrimary" }),
|
|
370
|
+
borderColor: Object.freeze({ type: "color", uiDefault: "colors.danger" }),
|
|
371
|
+
}),
|
|
372
|
+
targets: Object.freeze({
|
|
373
|
+
"appstudio.button": DANGER_BUTTON_FIELDS,
|
|
374
|
+
}),
|
|
375
|
+
}),
|
|
376
|
+
// sc-5392 -- a subtle/secondary surface, bound to the form widgets' own
|
|
377
|
+
// text-entry fields (already REQ-THEME-13 style fields, predating this
|
|
378
|
+
// scope) rather than to Text/Label/Data Value, whose `color` field the
|
|
379
|
+
// `text` scope already owns.
|
|
380
|
+
muted: Object.freeze({
|
|
381
|
+
label: "Muted",
|
|
382
|
+
tokens: Object.freeze({
|
|
383
|
+
background: Object.freeze({ type: "color", uiDefault: "colors.surfaceMuted" }),
|
|
384
|
+
textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurfaceMuted" }),
|
|
385
|
+
borderColor: Object.freeze({ type: "color", uiDefault: "colors.border" }),
|
|
386
|
+
radius: Object.freeze({ type: "size", min: 0, max: 48, uiDefault: "radii.sm" }),
|
|
387
|
+
}),
|
|
388
|
+
targets: Object.freeze({
|
|
389
|
+
"appstudio.form-input": MUTED_INPUT_FIELDS,
|
|
390
|
+
"appstudio.form-builder": MUTED_INPUT_FIELDS,
|
|
391
|
+
}),
|
|
392
|
+
}),
|
|
393
|
+
// sc-5392 -- dropdown/menu surfaces. The form widgets' choice-field option
|
|
394
|
+
// list is the closest thing the built-in catalog has to a popover panel.
|
|
395
|
+
popover: Object.freeze({
|
|
396
|
+
label: "Popover",
|
|
397
|
+
tokens: Object.freeze({
|
|
398
|
+
background: Object.freeze({ type: "color", uiDefault: "colors.surface" }),
|
|
399
|
+
textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurface" }),
|
|
400
|
+
borderColor: Object.freeze({ type: "color", uiDefault: "colors.border" }),
|
|
401
|
+
}),
|
|
402
|
+
targets: Object.freeze({
|
|
403
|
+
"appstudio.form-input": POPOVER_FIELDS,
|
|
404
|
+
"appstudio.form-builder": POPOVER_FIELDS,
|
|
405
|
+
}),
|
|
406
|
+
}),
|
|
407
|
+
// sc-5392 -- the app-wide focus-visible outline. A themed emphasis border
|
|
408
|
+
// on the Button widget, applied after its variant colours and beaten by the
|
|
409
|
+
// widget's own per-instance `borderColor` (REQ-THEME-13 stays final).
|
|
410
|
+
ring: Object.freeze({
|
|
411
|
+
label: "Focus ring",
|
|
412
|
+
tokens: Object.freeze({
|
|
413
|
+
color: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
|
|
414
|
+
width: Object.freeze({ type: "size", min: 0, max: 8 }),
|
|
415
|
+
}),
|
|
416
|
+
targets: Object.freeze({
|
|
417
|
+
"appstudio.button": Object.freeze({ color: "ringColor", width: "ringWidth" }),
|
|
418
|
+
}),
|
|
419
|
+
}),
|
|
280
420
|
});
|
|
281
421
|
|
|
282
422
|
// sc-4505 — the value types a widget event payload field may declare. Narrower
|
|
@@ -2956,7 +3096,31 @@ const CONTRACT = deepFreeze({
|
|
|
2956
3096
|
// action script now sees a `request` global — `{ body }` on a run fired
|
|
2957
3097
|
// by the workspace-granted `http_post` webhook trigger, null otherwise.
|
|
2958
3098
|
// `app` and `http_post` stay operator-granted and are NOT declarable.
|
|
2959
|
-
|
|
3099
|
+
// 1.69.0: additive (sc-5392) — `themeComponents` gains five scopes:
|
|
3100
|
+
// `accent`, `destructive`, `muted`, `popover` and `ring`, so a
|
|
3101
|
+
// shadcn/Tailwind import's matching custom properties have a themeConfig
|
|
3102
|
+
// home instead of being reported lost. Host-only plumbing: no scope
|
|
3103
|
+
// declares `universalFields`, so a third-party manifest is unaffected.
|
|
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",
|
|
2960
3124
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2961
3125
|
hooks: HOOKS,
|
|
2962
3126
|
primitives: PRIMITIVES,
|
|
@@ -2972,6 +3136,7 @@ const CONTRACT = deepFreeze({
|
|
|
2972
3136
|
themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
|
|
2973
3137
|
themeComponentGradient: THEME_COMPONENT_GRADIENT,
|
|
2974
3138
|
themeSpacingScale: THEME_SPACING_SCALE,
|
|
3139
|
+
themeMenuTypes: THEME_MENU_TYPES,
|
|
2975
3140
|
themeWidgetStyles: THEME_WIDGET_STYLES,
|
|
2976
3141
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
2977
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,
|
|
@@ -211,6 +247,34 @@ const CARD_UNIVERSAL_FIELDS = Object.freeze({
|
|
|
211
247
|
gradient: "cardGradient",
|
|
212
248
|
});
|
|
213
249
|
|
|
250
|
+
// The Form widgets' text-entry surface, already read by both form widgets
|
|
251
|
+
// (REQ-THEME-13, predating this scope) -- a genuine subtle/secondary surface
|
|
252
|
+
// distinct from the card that hosts it.
|
|
253
|
+
const MUTED_INPUT_FIELDS = Object.freeze({
|
|
254
|
+
background: "inputBackground",
|
|
255
|
+
textColor: "inputTextColor",
|
|
256
|
+
borderColor: "inputBorderColor",
|
|
257
|
+
radius: "inputRadius",
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// The Form widgets' choice-field option list -- the closest thing this
|
|
261
|
+
// catalog has to a menu/dropdown surface (singleChoice/multiChoice render an
|
|
262
|
+
// inline list of options here).
|
|
263
|
+
const POPOVER_FIELDS = Object.freeze({
|
|
264
|
+
background: "popoverBackground",
|
|
265
|
+
textColor: "popoverTextColor",
|
|
266
|
+
borderColor: "popoverBorderColor",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// The Button widget's danger-variant fields -- kept separate from its own
|
|
270
|
+
// `background`/`textColor`/`borderColor` (the `button` scope's fields above)
|
|
271
|
+
// so a themed destructive action never collides with the button's normal look.
|
|
272
|
+
const DANGER_BUTTON_FIELDS = Object.freeze({
|
|
273
|
+
background: "dangerBackground",
|
|
274
|
+
textColor: "dangerTextColor",
|
|
275
|
+
borderColor: "dangerBorderColor",
|
|
276
|
+
});
|
|
277
|
+
|
|
214
278
|
const THEME_COMPONENTS = Object.freeze({
|
|
215
279
|
button: Object.freeze({
|
|
216
280
|
label: "Buttons",
|
|
@@ -277,6 +341,82 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
277
341
|
"appstudio.data-value": TEXT_TYPOGRAPHY_FIELDS,
|
|
278
342
|
}),
|
|
279
343
|
}),
|
|
344
|
+
// sc-5392 -- a highlight/tag surface. Bound to Label's OWN background/
|
|
345
|
+
// borderColor/radius fields (REQ-THEME-13, predating this scope) rather
|
|
346
|
+
// than its `color` field, which the `text` scope already owns -- two
|
|
347
|
+
// scopes writing the same field on the same widget would race.
|
|
348
|
+
accent: Object.freeze({
|
|
349
|
+
label: "Accent",
|
|
350
|
+
tokens: Object.freeze({
|
|
351
|
+
background: Object.freeze({ type: "color", uiDefault: "colors.primarySoft" }),
|
|
352
|
+
borderColor: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
|
|
353
|
+
radius: Object.freeze({ type: "size", min: 0, max: 48, uiDefault: "radii.pill" }),
|
|
354
|
+
}),
|
|
355
|
+
targets: Object.freeze({
|
|
356
|
+
"appstudio.label": Object.freeze({
|
|
357
|
+
background: "background",
|
|
358
|
+
borderColor: "borderColor",
|
|
359
|
+
radius: "radius",
|
|
360
|
+
}),
|
|
361
|
+
}),
|
|
362
|
+
}),
|
|
363
|
+
// sc-5392 -- the Button widget's danger/delete variant. Separate token set
|
|
364
|
+
// from `button` above so "make the buttons coral" never recolours Delete.
|
|
365
|
+
destructive: Object.freeze({
|
|
366
|
+
label: "Destructive",
|
|
367
|
+
tokens: Object.freeze({
|
|
368
|
+
background: Object.freeze({ type: "color", uiDefault: "colors.danger" }),
|
|
369
|
+
textColor: Object.freeze({ type: "color", uiDefault: "colors.onPrimary" }),
|
|
370
|
+
borderColor: Object.freeze({ type: "color", uiDefault: "colors.danger" }),
|
|
371
|
+
}),
|
|
372
|
+
targets: Object.freeze({
|
|
373
|
+
"appstudio.button": DANGER_BUTTON_FIELDS,
|
|
374
|
+
}),
|
|
375
|
+
}),
|
|
376
|
+
// sc-5392 -- a subtle/secondary surface, bound to the form widgets' own
|
|
377
|
+
// text-entry fields (already REQ-THEME-13 style fields, predating this
|
|
378
|
+
// scope) rather than to Text/Label/Data Value, whose `color` field the
|
|
379
|
+
// `text` scope already owns.
|
|
380
|
+
muted: Object.freeze({
|
|
381
|
+
label: "Muted",
|
|
382
|
+
tokens: Object.freeze({
|
|
383
|
+
background: Object.freeze({ type: "color", uiDefault: "colors.surfaceMuted" }),
|
|
384
|
+
textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurfaceMuted" }),
|
|
385
|
+
borderColor: Object.freeze({ type: "color", uiDefault: "colors.border" }),
|
|
386
|
+
radius: Object.freeze({ type: "size", min: 0, max: 48, uiDefault: "radii.sm" }),
|
|
387
|
+
}),
|
|
388
|
+
targets: Object.freeze({
|
|
389
|
+
"appstudio.form-input": MUTED_INPUT_FIELDS,
|
|
390
|
+
"appstudio.form-builder": MUTED_INPUT_FIELDS,
|
|
391
|
+
}),
|
|
392
|
+
}),
|
|
393
|
+
// sc-5392 -- dropdown/menu surfaces. The form widgets' choice-field option
|
|
394
|
+
// list is the closest thing the built-in catalog has to a popover panel.
|
|
395
|
+
popover: Object.freeze({
|
|
396
|
+
label: "Popover",
|
|
397
|
+
tokens: Object.freeze({
|
|
398
|
+
background: Object.freeze({ type: "color", uiDefault: "colors.surface" }),
|
|
399
|
+
textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurface" }),
|
|
400
|
+
borderColor: Object.freeze({ type: "color", uiDefault: "colors.border" }),
|
|
401
|
+
}),
|
|
402
|
+
targets: Object.freeze({
|
|
403
|
+
"appstudio.form-input": POPOVER_FIELDS,
|
|
404
|
+
"appstudio.form-builder": POPOVER_FIELDS,
|
|
405
|
+
}),
|
|
406
|
+
}),
|
|
407
|
+
// sc-5392 -- the app-wide focus-visible outline. A themed emphasis border
|
|
408
|
+
// on the Button widget, applied after its variant colours and beaten by the
|
|
409
|
+
// widget's own per-instance `borderColor` (REQ-THEME-13 stays final).
|
|
410
|
+
ring: Object.freeze({
|
|
411
|
+
label: "Focus ring",
|
|
412
|
+
tokens: Object.freeze({
|
|
413
|
+
color: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
|
|
414
|
+
width: Object.freeze({ type: "size", min: 0, max: 8 }),
|
|
415
|
+
}),
|
|
416
|
+
targets: Object.freeze({
|
|
417
|
+
"appstudio.button": Object.freeze({ color: "ringColor", width: "ringWidth" }),
|
|
418
|
+
}),
|
|
419
|
+
}),
|
|
280
420
|
});
|
|
281
421
|
|
|
282
422
|
// sc-4505 — the value types a widget event payload field may declare. Narrower
|
|
@@ -2956,7 +3096,31 @@ const CONTRACT = deepFreeze({
|
|
|
2956
3096
|
// action script now sees a `request` global — `{ body }` on a run fired
|
|
2957
3097
|
// by the workspace-granted `http_post` webhook trigger, null otherwise.
|
|
2958
3098
|
// `app` and `http_post` stay operator-granted and are NOT declarable.
|
|
2959
|
-
|
|
3099
|
+
// 1.69.0: additive (sc-5392) — `themeComponents` gains five scopes:
|
|
3100
|
+
// `accent`, `destructive`, `muted`, `popover` and `ring`, so a
|
|
3101
|
+
// shadcn/Tailwind import's matching custom properties have a themeConfig
|
|
3102
|
+
// home instead of being reported lost. Host-only plumbing: no scope
|
|
3103
|
+
// declares `universalFields`, so a third-party manifest is unaffected.
|
|
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",
|
|
2960
3124
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2961
3125
|
hooks: HOOKS,
|
|
2962
3126
|
primitives: PRIMITIVES,
|
|
@@ -2972,6 +3136,7 @@ const CONTRACT = deepFreeze({
|
|
|
2972
3136
|
themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
|
|
2973
3137
|
themeComponentGradient: THEME_COMPONENT_GRADIENT,
|
|
2974
3138
|
themeSpacingScale: THEME_SPACING_SCALE,
|
|
3139
|
+
themeMenuTypes: THEME_MENU_TYPES,
|
|
2975
3140
|
themeWidgetStyles: THEME_WIDGET_STYLES,
|
|
2976
3141
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
2977
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"
|