@colixsystems/widget-sdk 0.124.0 → 0.126.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 +37 -1
- package/dist/contract.cjs +69 -20
- package/dist/contract.js +69 -20
- package/dist/hooks.js +167 -0
- package/dist/host.d.ts +0 -6
- package/dist/index.d.ts +80 -0
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/navigation.cjs +10 -19
- package/dist/navigation.js +10 -19
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -70,7 +70,43 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
70
70
|
|
|
71
71
|
## Status
|
|
72
72
|
|
|
73
|
-
`v0.
|
|
73
|
+
`v0.126.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**.
|
|
74
|
+
|
|
75
|
+
### What's new in 0.126.0 (contract 1.98.0)
|
|
76
|
+
|
|
77
|
+
**A widget can edit an image now, not just take one — new `useImageEditor()` (sc-7193).** `useCamera()` (0.121.0) let a widget capture a photo and `ctx.assets.upload` let it send one, but nothing could *change* one: no resize before upload, no crop to an aspect ratio, no straightening a sideways shot. A profile-picture widget had to upload the full-resolution original and hope the server-side normaliser did something acceptable.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
const { capture } = useCamera();
|
|
81
|
+
const { edit } = useImageEditor();
|
|
82
|
+
|
|
83
|
+
const shot = await capture();
|
|
84
|
+
const small = await edit(shot.uri, [{ resize: { width: 800 } }], { format: "jpeg", compress: 0.8 });
|
|
85
|
+
|
|
86
|
+
const fd = new FormData();
|
|
87
|
+
fd.append("file", small.file);
|
|
88
|
+
await ctx.assets.upload(fd);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`edit(uri, actions, options?)` applies `actions` in order and resolves the SAME normalised asset shape `useCamera()` yields, so capture → edit → upload is one code path on both hosts. Actions are `{ resize: { width?, height? } }`, `{ crop: { originX, originY, width, height } }`, `{ rotate: degrees }` and `{ flip: "horizontal" | "vertical" }`; output is `{ format: "jpeg" | "png" | "webp", compress, base64? }`.
|
|
92
|
+
|
|
93
|
+
**Host-brokered, not a vetted import** — the same call `expo-image-picker` and `expo-speech-recognition` already got. Widgets reach it through the hook and never import the package, so it stays out of every widget bundle and parity is the SDK's problem rather than each author's. The web Player brokers it on a canvas *inside the host* (your widget never touches the DOM); the Expo export uses `expo-image-manipulator`.
|
|
94
|
+
|
|
95
|
+
There is deliberately **no `extent` action**. It exists only on web in `expo-image-manipulator`, and a capability the Player has but the export does not is the direction CLAUDE.md §8 forbids.
|
|
96
|
+
|
|
97
|
+
The `device.imageEditor` slice is OPTIONAL, so a host that brokers nothing degrades the hook to `supported: false` rather than throwing — gate your edit control on it. Additive: no existing hook, primitive, manifest field or `propertySchema` type changed. `CONTRACT.version` → `1.98.0`.
|
|
98
|
+
|
|
99
|
+
### What's new in 0.125.0 (contract 1.97.0)
|
|
100
|
+
|
|
101
|
+
**A `top-bar` app no longer chooses which row carries its menu — `CONTRACT.themeTopBarMenuStyles` is REMOVED and `normaliseNavigation` returns `menuType` alone (sc-7044).** 0.120.0 gave the shape two menu rows to pick between: `links`, the row of text links beside the brand it had always drawn, and `tabs`, a dedicated tab row beneath the bar. `links` turned out to be the site-mode header's own row — literally the same `header` nav variant, sitting in the Studio beside "Show a header on site-mode pages" and its "Menu links" toggle, so an author was offered three adjacent settings that render the same thing. The tab row is the one that reads as an app's global navigation, so it is now the only one.
|
|
102
|
+
|
|
103
|
+
A host branches on `menuType` alone:
|
|
104
|
+
|
|
105
|
+
- `CONTRACT.themeTopBarMenuStyles` is gone, and so is the `ThemeTopBarMenuStyle` type. `ResolvedNavigation` is `{ menuType }`.
|
|
106
|
+
- `resolveTopBarTokens` is UNCHANGED — the whole `topBar` tab vocabulary (`tabStyle`, `contentSurface`, `tabIndicatorWidth`, `tabCornerRadius`, `tabPaddingX` / `tabPaddingY`, `tabBackgroundColor`, `tabActiveBackgroundColor`) now applies to every `top-bar` app rather than only to one that opted into tabs.
|
|
107
|
+
- A stored `navigation.topBarMenuStyle` is **inert**, not migrated. Nothing reads it, so no backfill runs; a `top-bar` app that never chose `tabs` renders the tab row from this version on.
|
|
108
|
+
|
|
109
|
+
**BREAKING, host-integration surface only** — a host that destructured `topBarMenuStyle` must stop. Nothing a widget author imports moved: no hook, primitive, manifest field or `propertySchema` type changed. `CONTRACT.version` → `1.97.0`.
|
|
74
110
|
|
|
75
111
|
### What's new in 0.124.0 (contract 1.96.0)
|
|
76
112
|
|
package/dist/contract.cjs
CHANGED
|
@@ -216,7 +216,7 @@ const THEME_MENU_TYPES = Object.freeze({
|
|
|
216
216
|
"top-bar": Object.freeze({
|
|
217
217
|
name: "Top bar",
|
|
218
218
|
summary:
|
|
219
|
-
"A
|
|
219
|
+
"A tab row beneath the app header at every width -- icon and label per page, scrolling sideways when it runs out of room. No rail at any width.",
|
|
220
220
|
maxItems: null,
|
|
221
221
|
quickBarMaxItems: null,
|
|
222
222
|
}),
|
|
@@ -228,18 +228,6 @@ const THEME_MENU_TYPES = Object.freeze({
|
|
|
228
228
|
quickBarMaxItems: null,
|
|
229
229
|
}),
|
|
230
230
|
});
|
|
231
|
-
const THEME_TOP_BAR_MENU_STYLES = Object.freeze({
|
|
232
|
-
links: Object.freeze({
|
|
233
|
-
name: "Links",
|
|
234
|
-
summary:
|
|
235
|
-
"Text links beside the brand, dropping to their own row only when a phone leaves them no space.",
|
|
236
|
-
}),
|
|
237
|
-
tabs: Object.freeze({
|
|
238
|
-
name: "Tabs",
|
|
239
|
-
summary:
|
|
240
|
-
"A dedicated tab row under the bar at every width -- icon and label per page, marked by the brand, scrolling sideways when it runs out of room.",
|
|
241
|
-
}),
|
|
242
|
-
});
|
|
243
231
|
const THEME_SPACING_SCALE = Object.freeze({
|
|
244
232
|
min: 0.5,
|
|
245
233
|
max: 2,
|
|
@@ -1682,6 +1670,38 @@ const HOOKS = [
|
|
|
1682
1670
|
requiredContextSlice: [],
|
|
1683
1671
|
scopes: null,
|
|
1684
1672
|
},
|
|
1673
|
+
// sc-7193 — host-brokered image editing. Optional slice; the hook reports
|
|
1674
|
+
// supported:false rather than throwing at render.
|
|
1675
|
+
{
|
|
1676
|
+
name: "useImageEditor",
|
|
1677
|
+
signature: "useImageEditor()",
|
|
1678
|
+
description:
|
|
1679
|
+
"Resize, crop, rotate or flip an image. Returns { result, editing, error, supported, edit, reset }. " +
|
|
1680
|
+
"Editing is IMPERATIVE — call edit() from an event handler, never during render. " +
|
|
1681
|
+
"edit(uri, actions, options?) applies `actions` IN ORDER and resolves the SAME normalised asset shape useCamera() " +
|
|
1682
|
+
"yields — { uri, name, mimeType, width, height, size, base64?, file } — so capture -> edit -> upload is ONE code path: " +
|
|
1683
|
+
"append result.file to a FormData as `file` and pass it to ctx.assets.upload(fd). " +
|
|
1684
|
+
"Each action entry carries exactly one of { resize: { width?, height? } } (aspect preserved when only one is given), " +
|
|
1685
|
+
"{ crop: { originX, originY, width, height } }, { rotate: degrees } (positive is clockwise), or " +
|
|
1686
|
+
"{ flip: \"horizontal\" | \"vertical\" }. options: { format: \"jpeg\" | \"png\" | \"webp\" (default jpeg), " +
|
|
1687
|
+
"compress: 0..1 (default 0.8, ignored for png), base64: boolean (off by default — it is expensive) }. " +
|
|
1688
|
+
"Rejects with an ImageEditorError whose .code is one of UNSUPPORTED | INVALID_ACTION | DECODE_FAILED | ENCODE_FAILED | " +
|
|
1689
|
+
"INTERNAL. There is deliberately NO `extent` action: it exists only on web, and a web-only capability is the direction " +
|
|
1690
|
+
"CLAUDE.md §8 forbids. Check `supported` before rendering an edit control. Identical on web (a canvas in the host, so " +
|
|
1691
|
+
"the widget never touches the DOM) and the Expo export (expo-image-manipulator).",
|
|
1692
|
+
returnShape: {
|
|
1693
|
+
result:
|
|
1694
|
+
"{ uri, name, mimeType, width, height, size, base64?, file } | null",
|
|
1695
|
+
editing: "boolean",
|
|
1696
|
+
error: "ImageEditorError | null",
|
|
1697
|
+
supported: "boolean // false when the host brokers no image editor",
|
|
1698
|
+
edit:
|
|
1699
|
+
"(uri, actions, options?) => Promise<result | null> // rejects with ImageEditorError",
|
|
1700
|
+
reset: "() => void // clear the result + error and release it",
|
|
1701
|
+
},
|
|
1702
|
+
requiredContextSlice: [],
|
|
1703
|
+
scopes: null,
|
|
1704
|
+
},
|
|
1685
1705
|
];
|
|
1686
1706
|
|
|
1687
1707
|
// REQ-WSDK-RN-WEB: the SDK exposes the React Native primitive API
|
|
@@ -2322,10 +2342,13 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2322
2342
|
"isBackgroundWatching() -> boolean, subscribeBackgroundPositions(cb) -> unsubscribe, " +
|
|
2323
2343
|
"subscribeBackgroundWatchState(cb) -> unsubscribe }, " +
|
|
2324
2344
|
"speech: { isSupported() -> boolean, start(options, { onResult, onError, onEnd }) -> Promise<{ stop(), abort() }> }, " +
|
|
2325
|
-
"camera: { isSupported() -> boolean, capture(options?) -> Promise<asset | null>, pick(options?) -> Promise<asset | null> }
|
|
2326
|
-
"
|
|
2327
|
-
"
|
|
2328
|
-
"
|
|
2345
|
+
"camera: { isSupported() -> boolean, capture(options?) -> Promise<asset | null>, pick(options?) -> Promise<asset | null> }, " +
|
|
2346
|
+
"imageEditor: { isSupported() -> boolean, edit(uri, actions, options?) -> Promise<asset> } }. " +
|
|
2347
|
+
"Backs useGeolocation(), useSpeechToText(), useCamera() and useImageEditor(). The web Player brokers them via " +
|
|
2348
|
+
"navigator.geolocation, window.SpeechRecognition, a getUserMedia camera preview and a host-side canvas; the Expo export via " +
|
|
2349
|
+
"expo-location, expo-speech-recognition, expo-image-picker and expo-image-manipulator. " +
|
|
2350
|
+
"imageEditor.edit applies resize / crop / rotate / flip in order and rejects with an ImageEditorError " +
|
|
2351
|
+
"(.code UNSUPPORTED | INVALID_ACTION | DECODE_FAILED | ENCODE_FAILED | INTERNAL). " +
|
|
2329
2352
|
"getCurrentPosition rejects with a GeolocationError (.code PERMISSION_DENIED | UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL). " +
|
|
2330
2353
|
"speech.start streams { transcript, isFinal } to onResult and runs ON DEVICE — it uploads no audio and spends no AI credit; " +
|
|
2331
2354
|
"its onError carries the Web Speech error vocabulary (not-allowed | no-speech | language-not-supported | network | aborted). " +
|
|
@@ -2339,7 +2362,12 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2339
2362
|
"foreground service), and a sibling widget may start or stop it — so subscribeBackgroundWatchState is how every mounted " +
|
|
2340
2363
|
"widget stays truthful, and isBackgroundWatching() is only the synchronous first read.",
|
|
2341
2364
|
required: false,
|
|
2342
|
-
fields: {
|
|
2365
|
+
fields: {
|
|
2366
|
+
geolocation: "object",
|
|
2367
|
+
speech: "object",
|
|
2368
|
+
camera: "object",
|
|
2369
|
+
imageEditor: "object",
|
|
2370
|
+
},
|
|
2343
2371
|
},
|
|
2344
2372
|
};
|
|
2345
2373
|
|
|
@@ -3671,7 +3699,29 @@ const CONTRACT = deepFreeze({
|
|
|
3671
3699
|
// privacy remains the record ACL's job. Existing `literal` and
|
|
3672
3700
|
// `relativeDate` conditions are untouched -- minor bump on the pre-1.0
|
|
3673
3701
|
// channel.
|
|
3674
|
-
|
|
3702
|
+
// 1.97.0: BREAKING (sc-7044) -- `themeTopBarMenuStyles` is REMOVED and
|
|
3703
|
+
// `normaliseNavigation` no longer returns `topBarMenuStyle`. A `top-bar`
|
|
3704
|
+
// app has ONE menu shape: the tab row beneath the bar. The `links` style
|
|
3705
|
+
// drew the menu on the same row-of-text-links the site-mode header draws,
|
|
3706
|
+
// so the Studio offered three adjacent settings that an author could not
|
|
3707
|
+
// tell apart. A host now branches on `menuType` alone; a stored
|
|
3708
|
+
// `navigation.topBarMenuStyle` is inert rather than migrated, so a
|
|
3709
|
+
// `top-bar` app that never chose `tabs` moves to the tab row.
|
|
3710
|
+
// 1.98.0: additive (sc-7193) — new `useImageEditor()` hook + the optional
|
|
3711
|
+
// `device.imageEditor` host slice it reads. A widget could take a photo
|
|
3712
|
+
// (useCamera) and upload one, but not CHANGE one: no resize before
|
|
3713
|
+
// upload, no crop to an aspect ratio, no straightening a sideways shot.
|
|
3714
|
+
// Host-brokered rather than a vetted import, the same call expo-image-picker
|
|
3715
|
+
// and expo-speech-recognition already got — the package stays out of every
|
|
3716
|
+
// widget bundle and parity is the SDK's problem, not each author's. The web
|
|
3717
|
+
// Player brokers it on a host-side canvas (the widget never touches the
|
|
3718
|
+
// DOM), the Expo export via expo-image-manipulator; both implement the same
|
|
3719
|
+
// four actions and the same three output formats. `extent` is deliberately
|
|
3720
|
+
// absent — it is web-only in expo-image-manipulator, and a web-only
|
|
3721
|
+
// capability is the direction §8 forbids. The slice is OPTIONAL, so a host
|
|
3722
|
+
// that brokers nothing degrades the hook to supported:false rather than
|
|
3723
|
+
// throwing. Minor bump on the pre-1.0 channel.
|
|
3724
|
+
version: "1.98.0",
|
|
3675
3725
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3676
3726
|
hooks: HOOKS,
|
|
3677
3727
|
primitives: PRIMITIVES,
|
|
@@ -3688,7 +3738,6 @@ const CONTRACT = deepFreeze({
|
|
|
3688
3738
|
themeComponentGradient: THEME_COMPONENT_GRADIENT,
|
|
3689
3739
|
themeSpacingScale: THEME_SPACING_SCALE,
|
|
3690
3740
|
themeMenuTypes: THEME_MENU_TYPES,
|
|
3691
|
-
themeTopBarMenuStyles: THEME_TOP_BAR_MENU_STYLES,
|
|
3692
3741
|
themeWidgetStyles: THEME_WIDGET_STYLES,
|
|
3693
3742
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
3694
3743
|
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|
package/dist/contract.js
CHANGED
|
@@ -216,7 +216,7 @@ const THEME_MENU_TYPES = Object.freeze({
|
|
|
216
216
|
"top-bar": Object.freeze({
|
|
217
217
|
name: "Top bar",
|
|
218
218
|
summary:
|
|
219
|
-
"A
|
|
219
|
+
"A tab row beneath the app header at every width -- icon and label per page, scrolling sideways when it runs out of room. No rail at any width.",
|
|
220
220
|
maxItems: null,
|
|
221
221
|
quickBarMaxItems: null,
|
|
222
222
|
}),
|
|
@@ -228,18 +228,6 @@ const THEME_MENU_TYPES = Object.freeze({
|
|
|
228
228
|
quickBarMaxItems: null,
|
|
229
229
|
}),
|
|
230
230
|
});
|
|
231
|
-
const THEME_TOP_BAR_MENU_STYLES = Object.freeze({
|
|
232
|
-
links: Object.freeze({
|
|
233
|
-
name: "Links",
|
|
234
|
-
summary:
|
|
235
|
-
"Text links beside the brand, dropping to their own row only when a phone leaves them no space.",
|
|
236
|
-
}),
|
|
237
|
-
tabs: Object.freeze({
|
|
238
|
-
name: "Tabs",
|
|
239
|
-
summary:
|
|
240
|
-
"A dedicated tab row under the bar at every width -- icon and label per page, marked by the brand, scrolling sideways when it runs out of room.",
|
|
241
|
-
}),
|
|
242
|
-
});
|
|
243
231
|
const THEME_SPACING_SCALE = Object.freeze({
|
|
244
232
|
min: 0.5,
|
|
245
233
|
max: 2,
|
|
@@ -1682,6 +1670,38 @@ const HOOKS = [
|
|
|
1682
1670
|
requiredContextSlice: [],
|
|
1683
1671
|
scopes: null,
|
|
1684
1672
|
},
|
|
1673
|
+
// sc-7193 — host-brokered image editing. Optional slice; the hook reports
|
|
1674
|
+
// supported:false rather than throwing at render.
|
|
1675
|
+
{
|
|
1676
|
+
name: "useImageEditor",
|
|
1677
|
+
signature: "useImageEditor()",
|
|
1678
|
+
description:
|
|
1679
|
+
"Resize, crop, rotate or flip an image. Returns { result, editing, error, supported, edit, reset }. " +
|
|
1680
|
+
"Editing is IMPERATIVE — call edit() from an event handler, never during render. " +
|
|
1681
|
+
"edit(uri, actions, options?) applies `actions` IN ORDER and resolves the SAME normalised asset shape useCamera() " +
|
|
1682
|
+
"yields — { uri, name, mimeType, width, height, size, base64?, file } — so capture -> edit -> upload is ONE code path: " +
|
|
1683
|
+
"append result.file to a FormData as `file` and pass it to ctx.assets.upload(fd). " +
|
|
1684
|
+
"Each action entry carries exactly one of { resize: { width?, height? } } (aspect preserved when only one is given), " +
|
|
1685
|
+
"{ crop: { originX, originY, width, height } }, { rotate: degrees } (positive is clockwise), or " +
|
|
1686
|
+
"{ flip: \"horizontal\" | \"vertical\" }. options: { format: \"jpeg\" | \"png\" | \"webp\" (default jpeg), " +
|
|
1687
|
+
"compress: 0..1 (default 0.8, ignored for png), base64: boolean (off by default — it is expensive) }. " +
|
|
1688
|
+
"Rejects with an ImageEditorError whose .code is one of UNSUPPORTED | INVALID_ACTION | DECODE_FAILED | ENCODE_FAILED | " +
|
|
1689
|
+
"INTERNAL. There is deliberately NO `extent` action: it exists only on web, and a web-only capability is the direction " +
|
|
1690
|
+
"CLAUDE.md §8 forbids. Check `supported` before rendering an edit control. Identical on web (a canvas in the host, so " +
|
|
1691
|
+
"the widget never touches the DOM) and the Expo export (expo-image-manipulator).",
|
|
1692
|
+
returnShape: {
|
|
1693
|
+
result:
|
|
1694
|
+
"{ uri, name, mimeType, width, height, size, base64?, file } | null",
|
|
1695
|
+
editing: "boolean",
|
|
1696
|
+
error: "ImageEditorError | null",
|
|
1697
|
+
supported: "boolean // false when the host brokers no image editor",
|
|
1698
|
+
edit:
|
|
1699
|
+
"(uri, actions, options?) => Promise<result | null> // rejects with ImageEditorError",
|
|
1700
|
+
reset: "() => void // clear the result + error and release it",
|
|
1701
|
+
},
|
|
1702
|
+
requiredContextSlice: [],
|
|
1703
|
+
scopes: null,
|
|
1704
|
+
},
|
|
1685
1705
|
];
|
|
1686
1706
|
|
|
1687
1707
|
// REQ-WSDK-RN-WEB: the SDK exposes the React Native primitive API
|
|
@@ -2322,10 +2342,13 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2322
2342
|
"isBackgroundWatching() -> boolean, subscribeBackgroundPositions(cb) -> unsubscribe, " +
|
|
2323
2343
|
"subscribeBackgroundWatchState(cb) -> unsubscribe }, " +
|
|
2324
2344
|
"speech: { isSupported() -> boolean, start(options, { onResult, onError, onEnd }) -> Promise<{ stop(), abort() }> }, " +
|
|
2325
|
-
"camera: { isSupported() -> boolean, capture(options?) -> Promise<asset | null>, pick(options?) -> Promise<asset | null> }
|
|
2326
|
-
"
|
|
2327
|
-
"
|
|
2328
|
-
"
|
|
2345
|
+
"camera: { isSupported() -> boolean, capture(options?) -> Promise<asset | null>, pick(options?) -> Promise<asset | null> }, " +
|
|
2346
|
+
"imageEditor: { isSupported() -> boolean, edit(uri, actions, options?) -> Promise<asset> } }. " +
|
|
2347
|
+
"Backs useGeolocation(), useSpeechToText(), useCamera() and useImageEditor(). The web Player brokers them via " +
|
|
2348
|
+
"navigator.geolocation, window.SpeechRecognition, a getUserMedia camera preview and a host-side canvas; the Expo export via " +
|
|
2349
|
+
"expo-location, expo-speech-recognition, expo-image-picker and expo-image-manipulator. " +
|
|
2350
|
+
"imageEditor.edit applies resize / crop / rotate / flip in order and rejects with an ImageEditorError " +
|
|
2351
|
+
"(.code UNSUPPORTED | INVALID_ACTION | DECODE_FAILED | ENCODE_FAILED | INTERNAL). " +
|
|
2329
2352
|
"getCurrentPosition rejects with a GeolocationError (.code PERMISSION_DENIED | UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL). " +
|
|
2330
2353
|
"speech.start streams { transcript, isFinal } to onResult and runs ON DEVICE — it uploads no audio and spends no AI credit; " +
|
|
2331
2354
|
"its onError carries the Web Speech error vocabulary (not-allowed | no-speech | language-not-supported | network | aborted). " +
|
|
@@ -2339,7 +2362,12 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2339
2362
|
"foreground service), and a sibling widget may start or stop it — so subscribeBackgroundWatchState is how every mounted " +
|
|
2340
2363
|
"widget stays truthful, and isBackgroundWatching() is only the synchronous first read.",
|
|
2341
2364
|
required: false,
|
|
2342
|
-
fields: {
|
|
2365
|
+
fields: {
|
|
2366
|
+
geolocation: "object",
|
|
2367
|
+
speech: "object",
|
|
2368
|
+
camera: "object",
|
|
2369
|
+
imageEditor: "object",
|
|
2370
|
+
},
|
|
2343
2371
|
},
|
|
2344
2372
|
};
|
|
2345
2373
|
|
|
@@ -3671,7 +3699,29 @@ const CONTRACT = deepFreeze({
|
|
|
3671
3699
|
// privacy remains the record ACL's job. Existing `literal` and
|
|
3672
3700
|
// `relativeDate` conditions are untouched -- minor bump on the pre-1.0
|
|
3673
3701
|
// channel.
|
|
3674
|
-
|
|
3702
|
+
// 1.97.0: BREAKING (sc-7044) -- `themeTopBarMenuStyles` is REMOVED and
|
|
3703
|
+
// `normaliseNavigation` no longer returns `topBarMenuStyle`. A `top-bar`
|
|
3704
|
+
// app has ONE menu shape: the tab row beneath the bar. The `links` style
|
|
3705
|
+
// drew the menu on the same row-of-text-links the site-mode header draws,
|
|
3706
|
+
// so the Studio offered three adjacent settings that an author could not
|
|
3707
|
+
// tell apart. A host now branches on `menuType` alone; a stored
|
|
3708
|
+
// `navigation.topBarMenuStyle` is inert rather than migrated, so a
|
|
3709
|
+
// `top-bar` app that never chose `tabs` moves to the tab row.
|
|
3710
|
+
// 1.98.0: additive (sc-7193) — new `useImageEditor()` hook + the optional
|
|
3711
|
+
// `device.imageEditor` host slice it reads. A widget could take a photo
|
|
3712
|
+
// (useCamera) and upload one, but not CHANGE one: no resize before
|
|
3713
|
+
// upload, no crop to an aspect ratio, no straightening a sideways shot.
|
|
3714
|
+
// Host-brokered rather than a vetted import, the same call expo-image-picker
|
|
3715
|
+
// and expo-speech-recognition already got — the package stays out of every
|
|
3716
|
+
// widget bundle and parity is the SDK's problem, not each author's. The web
|
|
3717
|
+
// Player brokers it on a host-side canvas (the widget never touches the
|
|
3718
|
+
// DOM), the Expo export via expo-image-manipulator; both implement the same
|
|
3719
|
+
// four actions and the same three output formats. `extent` is deliberately
|
|
3720
|
+
// absent — it is web-only in expo-image-manipulator, and a web-only
|
|
3721
|
+
// capability is the direction §8 forbids. The slice is OPTIONAL, so a host
|
|
3722
|
+
// that brokers nothing degrades the hook to supported:false rather than
|
|
3723
|
+
// throwing. Minor bump on the pre-1.0 channel.
|
|
3724
|
+
version: "1.98.0",
|
|
3675
3725
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3676
3726
|
hooks: HOOKS,
|
|
3677
3727
|
primitives: PRIMITIVES,
|
|
@@ -3688,7 +3738,6 @@ const CONTRACT = deepFreeze({
|
|
|
3688
3738
|
themeComponentGradient: THEME_COMPONENT_GRADIENT,
|
|
3689
3739
|
themeSpacingScale: THEME_SPACING_SCALE,
|
|
3690
3740
|
themeMenuTypes: THEME_MENU_TYPES,
|
|
3691
|
-
themeTopBarMenuStyles: THEME_TOP_BAR_MENU_STYLES,
|
|
3692
3741
|
themeWidgetStyles: THEME_WIDGET_STYLES,
|
|
3693
3742
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
3694
3743
|
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|
package/dist/hooks.js
CHANGED
|
@@ -1589,6 +1589,173 @@ export function useCamera(options) {
|
|
|
1589
1589
|
return { asset, loading, error, supported, capture, pick, reset };
|
|
1590
1590
|
}
|
|
1591
1591
|
|
|
1592
|
+
/**
|
|
1593
|
+
* Structured error thrown by `useImageEditor` callbacks.
|
|
1594
|
+
*
|
|
1595
|
+
* `code` is one of:
|
|
1596
|
+
* - "UNSUPPORTED" — this host brokers no image editor.
|
|
1597
|
+
* - "INVALID_ACTION" — an action or output option the contract doesn't define.
|
|
1598
|
+
* - "DECODE_FAILED" — the source could not be read as an image.
|
|
1599
|
+
* - "ENCODE_FAILED" — the host could not write the requested output format.
|
|
1600
|
+
* - "INTERNAL" — anything else.
|
|
1601
|
+
*/
|
|
1602
|
+
export class ImageEditorError extends Error {
|
|
1603
|
+
constructor(code, message, opts) {
|
|
1604
|
+
super(message);
|
|
1605
|
+
this.name = "ImageEditorError";
|
|
1606
|
+
this.code = code;
|
|
1607
|
+
if (opts && opts.cause) this.cause = opts.cause;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/** Coerce a thrown value into an ImageEditorError with a stable code. */
|
|
1612
|
+
function toImageEditorError(err) {
|
|
1613
|
+
if (err instanceof ImageEditorError) return err;
|
|
1614
|
+
const raw = err && err.code !== undefined ? err.code : null;
|
|
1615
|
+
const known = [
|
|
1616
|
+
"UNSUPPORTED",
|
|
1617
|
+
"INVALID_ACTION",
|
|
1618
|
+
"DECODE_FAILED",
|
|
1619
|
+
"ENCODE_FAILED",
|
|
1620
|
+
];
|
|
1621
|
+
const code = known.includes(raw) ? raw : "INTERNAL";
|
|
1622
|
+
const message =
|
|
1623
|
+
(err && typeof err.message === "string" && err.message) ||
|
|
1624
|
+
"Image edit failed";
|
|
1625
|
+
return new ImageEditorError(code, message, { cause: err });
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/**
|
|
1629
|
+
* Resize, crop, rotate or flip an image. Returns
|
|
1630
|
+
* `{ result, editing, error, supported, edit, reset }`.
|
|
1631
|
+
*
|
|
1632
|
+
* Editing is IMPERATIVE — call `edit()` from an event handler, never during
|
|
1633
|
+
* render. It resolves the SAME normalised asset shape `useCamera()` yields, so
|
|
1634
|
+
* capture → edit → upload is one code path on both hosts:
|
|
1635
|
+
*
|
|
1636
|
+
* const { asset, capture } = useCamera();
|
|
1637
|
+
* const { edit } = useImageEditor();
|
|
1638
|
+
* const shot = await capture();
|
|
1639
|
+
* const small = await edit(shot.uri, [{ resize: { width: 800 } }]);
|
|
1640
|
+
* const fd = new FormData();
|
|
1641
|
+
* fd.append("file", small.file);
|
|
1642
|
+
* await ctx.assets.upload(fd);
|
|
1643
|
+
*
|
|
1644
|
+
* `actions` is an ordered array applied in sequence; each entry carries exactly
|
|
1645
|
+
* one of:
|
|
1646
|
+
* - `{ resize: { width?, height? } }` — aspect preserved when one is given
|
|
1647
|
+
* - `{ crop: { originX, originY, width, height } }`
|
|
1648
|
+
* - `{ rotate: degrees }` — positive is clockwise
|
|
1649
|
+
* - `{ flip: "horizontal" | "vertical" }`
|
|
1650
|
+
*
|
|
1651
|
+
* `options` is `{ format: "jpeg" | "png" | "webp", compress: 0..1, base64? }`.
|
|
1652
|
+
* There is deliberately NO `extent` action: it exists only on web, and a
|
|
1653
|
+
* web-only capability is the direction CLAUDE.md §8 forbids.
|
|
1654
|
+
*
|
|
1655
|
+
* Check `supported` before rendering an edit control; a host with no broker
|
|
1656
|
+
* reports false rather than throwing at render.
|
|
1657
|
+
*/
|
|
1658
|
+
export function useImageEditor() {
|
|
1659
|
+
const ctx = useWidgetContextOrThrow("useImageEditor");
|
|
1660
|
+
const [result, setResult] = useState(null);
|
|
1661
|
+
const [editing, setEditing] = useState(false);
|
|
1662
|
+
const [error, setError] = useState(null);
|
|
1663
|
+
|
|
1664
|
+
// `ctx` is a fresh identity every host render — hold the live client in a ref
|
|
1665
|
+
// so the returned callbacks stay stable.
|
|
1666
|
+
const clientRef = useRef(ctx.device && ctx.device.imageEditor);
|
|
1667
|
+
clientRef.current = ctx.device && ctx.device.imageEditor;
|
|
1668
|
+
// Web hands back a blob: URL per result; abandoning it leaks the blob for the
|
|
1669
|
+
// life of the document, so the hook owns revoking the one it replaced.
|
|
1670
|
+
const releaseRef = useRef(null);
|
|
1671
|
+
const runRef = useRef(0);
|
|
1672
|
+
|
|
1673
|
+
const supported = Boolean(
|
|
1674
|
+
clientRef.current &&
|
|
1675
|
+
typeof clientRef.current.edit === "function" &&
|
|
1676
|
+
(typeof clientRef.current.isSupported !== "function" ||
|
|
1677
|
+
clientRef.current.isSupported()),
|
|
1678
|
+
);
|
|
1679
|
+
|
|
1680
|
+
const release = useCallback(() => {
|
|
1681
|
+
const revoke = releaseRef.current;
|
|
1682
|
+
releaseRef.current = null;
|
|
1683
|
+
if (typeof revoke === "function") {
|
|
1684
|
+
try {
|
|
1685
|
+
revoke();
|
|
1686
|
+
} catch {
|
|
1687
|
+
/* the host already released it */
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}, []);
|
|
1691
|
+
|
|
1692
|
+
useEffect(() => () => release(), [release]);
|
|
1693
|
+
|
|
1694
|
+
const reset = useCallback(() => {
|
|
1695
|
+
runRef.current += 1;
|
|
1696
|
+
release();
|
|
1697
|
+
setResult(null);
|
|
1698
|
+
setError(null);
|
|
1699
|
+
}, [release]);
|
|
1700
|
+
|
|
1701
|
+
const edit = useCallback(
|
|
1702
|
+
async (uri, actions, options) => {
|
|
1703
|
+
const client = clientRef.current;
|
|
1704
|
+
if (
|
|
1705
|
+
!client ||
|
|
1706
|
+
typeof client.edit !== "function" ||
|
|
1707
|
+
(typeof client.isSupported === "function" && !client.isSupported())
|
|
1708
|
+
) {
|
|
1709
|
+
const e = new ImageEditorError(
|
|
1710
|
+
"UNSUPPORTED",
|
|
1711
|
+
"This host does not provide image editing.",
|
|
1712
|
+
);
|
|
1713
|
+
setError(e);
|
|
1714
|
+
throw e;
|
|
1715
|
+
}
|
|
1716
|
+
if (typeof uri !== "string" || uri === "") {
|
|
1717
|
+
const e = new ImageEditorError(
|
|
1718
|
+
"INVALID_ACTION",
|
|
1719
|
+
"edit(uri, actions) needs a source uri.",
|
|
1720
|
+
);
|
|
1721
|
+
setError(e);
|
|
1722
|
+
throw e;
|
|
1723
|
+
}
|
|
1724
|
+
const run = (runRef.current += 1);
|
|
1725
|
+
setEditing(true);
|
|
1726
|
+
setError(null);
|
|
1727
|
+
try {
|
|
1728
|
+
const next = await client.edit(
|
|
1729
|
+
uri,
|
|
1730
|
+
Array.isArray(actions) ? actions : [],
|
|
1731
|
+
options || {},
|
|
1732
|
+
);
|
|
1733
|
+
// A reset() or a newer edit landed while this one was running — drop
|
|
1734
|
+
// the result rather than clobbering what the widget now shows.
|
|
1735
|
+
if (run !== runRef.current) {
|
|
1736
|
+
if (next && typeof next.release === "function") next.release();
|
|
1737
|
+
return null;
|
|
1738
|
+
}
|
|
1739
|
+
if (!next) return null;
|
|
1740
|
+
release();
|
|
1741
|
+
releaseRef.current =
|
|
1742
|
+
typeof next.release === "function" ? next.release : null;
|
|
1743
|
+
setResult(next);
|
|
1744
|
+
return next;
|
|
1745
|
+
} catch (err) {
|
|
1746
|
+
const ie = toImageEditorError(err);
|
|
1747
|
+
if (run === runRef.current) setError(ie);
|
|
1748
|
+
throw ie;
|
|
1749
|
+
} finally {
|
|
1750
|
+
if (run === runRef.current) setEditing(false);
|
|
1751
|
+
}
|
|
1752
|
+
},
|
|
1753
|
+
[release],
|
|
1754
|
+
);
|
|
1755
|
+
|
|
1756
|
+
return { result, editing, error, supported, edit, reset };
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1592
1759
|
/* ============================================================================
|
|
1593
1760
|
* DATASTORE CLIENT — ctx.datastore (@colixsystems/datastore-client)
|
|
1594
1761
|
*
|
package/dist/host.d.ts
CHANGED
|
@@ -155,14 +155,8 @@ export function createToastController(
|
|
|
155
155
|
|
|
156
156
|
export type ThemeMenuType = "sidebar" | "top-bar" | "bottom-tabs";
|
|
157
157
|
|
|
158
|
-
// How a `top-bar` app draws its menu — closed by
|
|
159
|
-
// `CONTRACT.themeTopBarMenuStyles`. Meaningless on the other two shapes, where
|
|
160
|
-
// the resolver always reports "links".
|
|
161
|
-
export type ThemeTopBarMenuStyle = "links" | "tabs";
|
|
162
|
-
|
|
163
158
|
export interface ResolvedNavigation {
|
|
164
159
|
menuType: ThemeMenuType;
|
|
165
|
-
topBarMenuStyle: ThemeTopBarMenuStyle;
|
|
166
160
|
}
|
|
167
161
|
|
|
168
162
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1666,6 +1666,86 @@ export class CameraError extends Error {
|
|
|
1666
1666
|
);
|
|
1667
1667
|
}
|
|
1668
1668
|
|
|
1669
|
+
/** One edit step. Exactly one key per entry; the array applies in order. */
|
|
1670
|
+
export type ImageEditAction =
|
|
1671
|
+
| { resize: { width?: number; height?: number } }
|
|
1672
|
+
| { crop: { originX: number; originY: number; width: number; height: number } }
|
|
1673
|
+
| { rotate: number }
|
|
1674
|
+
| { flip: "horizontal" | "vertical" };
|
|
1675
|
+
|
|
1676
|
+
/** Output settings for `useImageEditor().edit(...)`. */
|
|
1677
|
+
export interface ImageEditOptions {
|
|
1678
|
+
/** Encoding of the result. Defaults to "jpeg". */
|
|
1679
|
+
format?: "jpeg" | "png" | "webp";
|
|
1680
|
+
/** 0–1 quality for the lossy formats. Defaults to 0.8. Ignored for png. */
|
|
1681
|
+
compress?: number;
|
|
1682
|
+
/** Also return the bytes as base64. Off by default — it is expensive. */
|
|
1683
|
+
base64?: boolean;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/**
|
|
1687
|
+
* An edited image, normalised across hosts. Structurally the same shape
|
|
1688
|
+
* `useCamera()` yields, so capture → edit → upload is one code path.
|
|
1689
|
+
*/
|
|
1690
|
+
export interface EditedImage {
|
|
1691
|
+
uri: string;
|
|
1692
|
+
name: string;
|
|
1693
|
+
mimeType: string;
|
|
1694
|
+
width: number | null;
|
|
1695
|
+
height: number | null;
|
|
1696
|
+
size: number | null;
|
|
1697
|
+
/** Present only when `base64` was requested. */
|
|
1698
|
+
base64?: string;
|
|
1699
|
+
/** Ready-to-upload part — a `File` on web, `{ uri, name, type }` on native. */
|
|
1700
|
+
file: unknown;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
export interface ImageEditorResult {
|
|
1704
|
+
/** The most recent edit, or null before the first call / after reset. */
|
|
1705
|
+
result: EditedImage | null;
|
|
1706
|
+
editing: boolean;
|
|
1707
|
+
error: ImageEditorError | null;
|
|
1708
|
+
/** False when the host brokers no image editor. */
|
|
1709
|
+
supported: boolean;
|
|
1710
|
+
/** Apply `actions` in order and encode per `options`. */
|
|
1711
|
+
edit(
|
|
1712
|
+
uri: string,
|
|
1713
|
+
actions: ImageEditAction[],
|
|
1714
|
+
options?: ImageEditOptions,
|
|
1715
|
+
): Promise<EditedImage | null>;
|
|
1716
|
+
/** Clear `result` and `error`, releasing the held image. */
|
|
1717
|
+
reset(): void;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
/**
|
|
1721
|
+
* Resize, crop, rotate or flip an image. Imperative — call `edit()` from an
|
|
1722
|
+
* event handler, never during render. The web Player brokers it on a canvas,
|
|
1723
|
+
* the Expo export via `expo-image-manipulator`; both implement the same four
|
|
1724
|
+
* actions and the same output formats. There is deliberately no `extent`
|
|
1725
|
+
* action — it exists only on web, and a web-only capability is the direction
|
|
1726
|
+
* CLAUDE.md §8 forbids. Safe to call on a host that brokers no editor:
|
|
1727
|
+
* `supported` is then false, so gate the control on it.
|
|
1728
|
+
*/
|
|
1729
|
+
export function useImageEditor(): ImageEditorResult;
|
|
1730
|
+
|
|
1731
|
+
/**
|
|
1732
|
+
* Error surfaced by `useImageEditor()` — thrown by `edit()` and stored in the
|
|
1733
|
+
* hook's `error` slot. `code` is a stable categorisation.
|
|
1734
|
+
*/
|
|
1735
|
+
export class ImageEditorError extends Error {
|
|
1736
|
+
code:
|
|
1737
|
+
| "UNSUPPORTED"
|
|
1738
|
+
| "INVALID_ACTION"
|
|
1739
|
+
| "DECODE_FAILED"
|
|
1740
|
+
| "ENCODE_FAILED"
|
|
1741
|
+
| "INTERNAL";
|
|
1742
|
+
constructor(
|
|
1743
|
+
code: ImageEditorError["code"],
|
|
1744
|
+
message: string,
|
|
1745
|
+
opts?: { cause?: unknown },
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1669
1749
|
/**
|
|
1670
1750
|
* Error class thrown by useDatastoreMutation callbacks (and surfaced by
|
|
1671
1751
|
* useDatastoreQuery in its `error` slot). The `code` is a stable
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/dist/navigation.cjs
CHANGED
|
@@ -30,9 +30,6 @@ const { CONTRACT, isHexColor } = require("./contract.cjs");
|
|
|
30
30
|
// Absent or unknown resolves here, so every app authored before menu types
|
|
31
31
|
// existed renders and compiles byte-identically.
|
|
32
32
|
const DEFAULT_MENU_TYPE = "sidebar";
|
|
33
|
-
// Today's row of text links, so an app that never made the choice draws exactly
|
|
34
|
-
// what it drew before it existed.
|
|
35
|
-
const DEFAULT_TOP_BAR_MENU_STYLE = "links";
|
|
36
33
|
|
|
37
34
|
function isPlainObject(value) {
|
|
38
35
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -45,8 +42,7 @@ function isPlainObject(value) {
|
|
|
45
42
|
* yields the default sidebar rather than something a host has to guard against.
|
|
46
43
|
*
|
|
47
44
|
* @param {unknown} navigation — the raw `theme_config.navigation` value.
|
|
48
|
-
* @returns {{ menuType: string
|
|
49
|
-
* navigation structure.
|
|
45
|
+
* @returns {{ menuType: string }} the resolved navigation structure.
|
|
50
46
|
*/
|
|
51
47
|
function normaliseNavigation(navigation) {
|
|
52
48
|
const block = isPlainObject(navigation) ? navigation : {};
|
|
@@ -55,16 +51,11 @@ function normaliseNavigation(navigation) {
|
|
|
55
51
|
typeof raw === "string" && Object.hasOwn(CONTRACT.themeMenuTypes, raw)
|
|
56
52
|
? raw
|
|
57
53
|
: DEFAULT_MENU_TYPE;
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
typeof rawStyle === "string" &&
|
|
64
|
-
Object.hasOwn(CONTRACT.themeTopBarMenuStyles, rawStyle)
|
|
65
|
-
? rawStyle
|
|
66
|
-
: DEFAULT_TOP_BAR_MENU_STYLE;
|
|
67
|
-
return { menuType, topBarMenuStyle };
|
|
54
|
+
// sc-7044: the SHAPE is the whole answer. A `top-bar` app used to also choose
|
|
55
|
+
// which row carried its menu, and the `links` option drew the site header's
|
|
56
|
+
// own row of text links — a second way to say the same thing. The tab row is
|
|
57
|
+
// the only shape now, so a stored `topBarMenuStyle` is read by nobody.
|
|
58
|
+
return { menuType };
|
|
68
59
|
}
|
|
69
60
|
|
|
70
61
|
/**
|
|
@@ -339,8 +330,8 @@ function resolveTopBarTokens(theme) {
|
|
|
339
330
|
// `underline` keeps the tab in the bar's surface and marks it with an
|
|
340
331
|
// indicator. `attached` makes it a real folder tab: it takes the CONTENT's
|
|
341
332
|
// surface and sits over the row's divider, so the tab and the page beneath
|
|
342
|
-
// read as one plane. Meaningless
|
|
343
|
-
//
|
|
333
|
+
// read as one plane. Meaningless on a shape that draws no tab row — a host
|
|
334
|
+
// reads it only where it has tabs to draw.
|
|
344
335
|
tabStyle: topBar.tabStyle === "attached" ? "attached" : "underline",
|
|
345
336
|
// The surface an `attached` tab AND its content panel share. One value for
|
|
346
337
|
// both, so the join cannot come apart: whatever the current tab is painted,
|
|
@@ -369,8 +360,8 @@ function resolveTopBarTokens(theme) {
|
|
|
369
360
|
tabPaddingX: tabSpaceOr(topBar.tabPaddingX, DEFAULT_TAB_PADDING_X),
|
|
370
361
|
tabPaddingY: tabSpaceOr(topBar.tabPaddingY, DEFAULT_TAB_PADDING_Y),
|
|
371
362
|
// A tab's own surface, authored or nothing. Null means the tab paints none
|
|
372
|
-
// and the bar shows through it, which is what
|
|
373
|
-
//
|
|
363
|
+
// and the bar shows through it, which is what the row did before the keys
|
|
364
|
+
// existed — so an app that never set them is unchanged.
|
|
374
365
|
tabBackgroundColor: hexOrNull(topBar.tabBackgroundColor),
|
|
375
366
|
// Only meaningful where the style draws no panel: under `attached` the
|
|
376
367
|
// active surface IS `contentSurface` above. Null under `underline` leaves
|
package/dist/navigation.js
CHANGED
|
@@ -21,9 +21,6 @@ import { CONTRACT, isHexColor } from "./contract.js";
|
|
|
21
21
|
// Absent or unknown resolves here, so every app authored before menu types
|
|
22
22
|
// existed renders and compiles byte-identically.
|
|
23
23
|
const DEFAULT_MENU_TYPE = "sidebar";
|
|
24
|
-
// Today's row of text links, so an app that never made the choice draws exactly
|
|
25
|
-
// what it drew before it existed.
|
|
26
|
-
const DEFAULT_TOP_BAR_MENU_STYLE = "links";
|
|
27
24
|
|
|
28
25
|
function isPlainObject(value) {
|
|
29
26
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -36,8 +33,7 @@ function isPlainObject(value) {
|
|
|
36
33
|
* yields the default sidebar rather than something a host has to guard against.
|
|
37
34
|
*
|
|
38
35
|
* @param {unknown} navigation — the raw `theme_config.navigation` value.
|
|
39
|
-
* @returns {{ menuType: string
|
|
40
|
-
* navigation structure.
|
|
36
|
+
* @returns {{ menuType: string }} the resolved navigation structure.
|
|
41
37
|
*/
|
|
42
38
|
export function normaliseNavigation(navigation) {
|
|
43
39
|
const block = isPlainObject(navigation) ? navigation : {};
|
|
@@ -46,16 +42,11 @@ export function normaliseNavigation(navigation) {
|
|
|
46
42
|
typeof raw === "string" && Object.hasOwn(CONTRACT.themeMenuTypes, raw)
|
|
47
43
|
? raw
|
|
48
44
|
: DEFAULT_MENU_TYPE;
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
typeof rawStyle === "string" &&
|
|
55
|
-
Object.hasOwn(CONTRACT.themeTopBarMenuStyles, rawStyle)
|
|
56
|
-
? rawStyle
|
|
57
|
-
: DEFAULT_TOP_BAR_MENU_STYLE;
|
|
58
|
-
return { menuType, topBarMenuStyle };
|
|
45
|
+
// sc-7044: the SHAPE is the whole answer. A `top-bar` app used to also choose
|
|
46
|
+
// which row carried its menu, and the `links` option drew the site header's
|
|
47
|
+
// own row of text links — a second way to say the same thing. The tab row is
|
|
48
|
+
// the only shape now, so a stored `topBarMenuStyle` is read by nobody.
|
|
49
|
+
return { menuType };
|
|
59
50
|
}
|
|
60
51
|
|
|
61
52
|
/**
|
|
@@ -330,8 +321,8 @@ export function resolveTopBarTokens(theme) {
|
|
|
330
321
|
// `underline` keeps the tab in the bar's surface and marks it with an
|
|
331
322
|
// indicator. `attached` makes it a real folder tab: it takes the CONTENT's
|
|
332
323
|
// surface and sits over the row's divider, so the tab and the page beneath
|
|
333
|
-
// read as one plane. Meaningless
|
|
334
|
-
//
|
|
324
|
+
// read as one plane. Meaningless on a shape that draws no tab row — a host
|
|
325
|
+
// reads it only where it has tabs to draw.
|
|
335
326
|
tabStyle: topBar.tabStyle === "attached" ? "attached" : "underline",
|
|
336
327
|
// The surface an `attached` tab AND its content panel share. One value for
|
|
337
328
|
// both, so the join cannot come apart: whatever the current tab is painted,
|
|
@@ -360,8 +351,8 @@ export function resolveTopBarTokens(theme) {
|
|
|
360
351
|
tabPaddingX: tabSpaceOr(topBar.tabPaddingX, DEFAULT_TAB_PADDING_X),
|
|
361
352
|
tabPaddingY: tabSpaceOr(topBar.tabPaddingY, DEFAULT_TAB_PADDING_Y),
|
|
362
353
|
// A tab's own surface, authored or nothing. Null means the tab paints none
|
|
363
|
-
// and the bar shows through it, which is what
|
|
364
|
-
//
|
|
354
|
+
// and the bar shows through it, which is what the row did before the keys
|
|
355
|
+
// existed — so an app that never set them is unchanged.
|
|
365
356
|
tabBackgroundColor: hexOrNull(topBar.tabBackgroundColor),
|
|
366
357
|
// Only meaningful where the style draws no panel: under `attached` the
|
|
367
358
|
// active surface IS `contentSurface` above. Null under `underline` leaves
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.126.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",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "node scripts/build.js",
|
|
52
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/vetted-imports-audit.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-page-url.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-hardcoded-design.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__/flatten-entry.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/corner-radius.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/interaction-lift.test.js src/__tests__/toast-host.test.js src/__tests__/overlay-tokens.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 src/__tests__/hooks-camera.test.js src/__tests__/hooks-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js src/__tests__/linter-html-in-content.test.js src/__tests__/markdown.test.js src/__tests__/markdown-edit.test.js src/__tests__/richtext-tokens.test.js"
|
|
52
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/vetted-imports-audit.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-page-url.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-hardcoded-design.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__/flatten-entry.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/corner-radius.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/interaction-lift.test.js src/__tests__/toast-host.test.js src/__tests__/overlay-tokens.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 src/__tests__/hooks-camera.test.js src/__tests__/hooks-image-editor.test.js src/__tests__/hooks-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js src/__tests__/linter-html-in-content.test.js src/__tests__/markdown.test.js src/__tests__/markdown-edit.test.js src/__tests__/richtext-tokens.test.js"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
55
55
|
"node": ">=18"
|