@colixsystems/widget-sdk 0.185.0 → 0.187.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 +49 -2
- package/dist/contract.cjs +54 -3
- package/dist/contract.js +54 -3
- package/dist/hooks.js +112 -10
- package/dist/host.d.ts +1 -1
- package/dist/index.d.ts +56 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/navigation.cjs +3 -3
- package/dist/navigation.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,8 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
20
20
|
| **CORE** | `useTheme()` | `{ colors, elevation, interaction, spacing, spacingScale, radii, typography, components, widgetStyles }` | `ctx.workspace.theme` — no scope. `elevation` is the shared depth scale (`none / sm / md / lg / xl`) you spread into a style; `colors` includes the accent's quiet tiers (`primarySoft` / `onPrimarySoft` / `primaryStrong`) and `loader`, the spinner colour for your own loading state. `components` is HOST-OWNED (the theme's per-component style tokens); the host has already folded it into your `props.style`, so read `useWidgetStyle()` and ignore this slice. |
|
|
21
21
|
| **CORE** | `useWorkspaceCurrency()` | `{ currency, formatMoney }` | `ctx.workspace.currency` — no scope. The currency this workspace charges its app users in, resolved at RENDER time. Render every price as `formatMoney(minorUnits)` and never write a currency symbol or code into a widget: the owner can change it after the widget ships, and a baked label then contradicts the charge. |
|
|
22
22
|
| **CORE** | `useWidgetStyle()` | `{ [styleField]: value }` | `ctx.props.style` — no scope. The author-set per-widget style values declared in `manifest.styleSchema`; apply each onto whatever element you choose. |
|
|
23
|
-
| **CORE** | `useUser()` | `{ id, email, displayName, roles, groupIds }` | `ctx.user` (host-built context, **camelCase** — not a wire payload; `id` null when anonymous) — no scope |
|
|
23
|
+
| **CORE** | `useUser()` | `{ id, email, displayName, photoUrl, roles, groupIds }` | `ctx.user` (host-built context, **camelCase** — not a wire payload; `id` null when anonymous) — no scope. `photoUrl` is the user's own profile photo as an absolute URL, or `null`. |
|
|
24
|
+
| **CORE** | `useUserPhoto()` | `{ url, canEdit, upload, remove, uploading, error }` | `ctx.user` + the optional `ctx.account` — no scope. Changes the signed-in user's OWN profile photo. `canEdit` is false when nobody is signed in or the host cannot change it; `upload` / `remove` then reject. |
|
|
24
25
|
| **CORE** | `useNavigation()` | `{ goTo, goBack, push, replace, back, currentRoute, openLink }` | `ctx.navigation` — no scope. `goTo(pageIdOrSlug, params?)` accepts a page UUID (a `pageRef` prop) **or its slug** (a row's page-key field) — both hosts resolve either (`openLink` for a link of unknown shape; a known external URL can also use the `Linking` primitive) |
|
|
25
26
|
| **CORE** | `useRouteParams()` | `{ [paramKey]: value }` | `ctx.navigation.currentRoute.params` — no scope. The nav params the previous page passed via `goTo(pageId, params)`; the flat accessor for master→detail (read `recordId` on a detail page). Empty object when none. |
|
|
26
27
|
| **CORE** | `usePageContext()` | `{ params, records }` | `ctx.pageContext` — no scope. The page's DECLARED parameters, resolved once by the host: `params` are coerced to their declared types, `records` holds the row already fetched for each `record` param (read it instead of fetching again). Both empty when the page declares none. |
|
|
@@ -74,7 +75,53 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
74
75
|
|
|
75
76
|
## Status
|
|
76
77
|
|
|
77
|
-
`v0.
|
|
78
|
+
`v0.187.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**.
|
|
79
|
+
|
|
80
|
+
### What's new in 0.187.0 (contract 1.148.0)
|
|
81
|
+
|
|
82
|
+
**App users have a profile photo, and a widget can show and change it.** `useUser()` now returns `photoUrl`, and the new `useUserPhoto()` hook lets the signed-in person replace or remove their own picture:
|
|
83
|
+
|
|
84
|
+
```jsx
|
|
85
|
+
import { useUser, useUserPhoto, Avatar, FilePicker, Pressable, Text, View } from "@colixsystems/widget-sdk";
|
|
86
|
+
|
|
87
|
+
function ProfileHeader() {
|
|
88
|
+
const user = useUser();
|
|
89
|
+
const { canEdit, upload, remove, uploading, error } = useUserPhoto();
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<View>
|
|
93
|
+
<Avatar name={user.displayName} url={user.photoUrl} size="xl" />
|
|
94
|
+
{canEdit ? (
|
|
95
|
+
<FilePicker
|
|
96
|
+
accept="image/png,image/jpeg,image/webp"
|
|
97
|
+
disabled={uploading}
|
|
98
|
+
onPick={(file) => upload(file).catch(() => {})}
|
|
99
|
+
>
|
|
100
|
+
<Text>{uploading ? "Saving…" : "Change photo"}</Text>
|
|
101
|
+
</FilePicker>
|
|
102
|
+
) : null}
|
|
103
|
+
{canEdit && user.photoUrl ? (
|
|
104
|
+
<Pressable disabled={uploading} onPress={() => remove().catch(() => {})}>
|
|
105
|
+
<Text>Remove photo</Text>
|
|
106
|
+
</Pressable>
|
|
107
|
+
) : null}
|
|
108
|
+
{error ? <Text>{error.status === 413 ? "That image is too large." : "Could not save the photo."}</Text> : null}
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- **`useUser().photoUrl`** is an absolute URL you pass straight to `<Avatar url>` or `<Image source={{ uri }}>`. It is `null` when no photo is set, for anonymous visitors, and on a host that does not provide one. `<Avatar>` falls back to the name's initials, so the slot is never blank.
|
|
115
|
+
- **`useUserPhoto()`** returns `{ url, canEdit, upload, remove, uploading, error }`. `url` is the same value as `useUser().photoUrl`. `upload(file)` takes the file `<FilePicker>` gives you, or `asset.file` from `useCamera()` / `useImageEditor()` — never the asset itself (a browser `File` on web, expo-file-system's `File` in the exported app) and resolves `{ photoUrl }`; `remove()` resolves `{ photoUrl: null }`. Once either resolves, every widget on the page reads the new `useUser().photoUrl`.
|
|
116
|
+
- **The server crops the image to a square and resizes it.** PNG, JPEG or WebP, 10 MB at most. A refusal carries `err.status`: `413` too large, `415` unsupported type, `400` unreadable image.
|
|
117
|
+
- **Check `canEdit` before offering the control.** It is false when nobody is signed in and on a host that cannot change the photo, such as the Builder canvas preview. `upload()` and `remove()` then reject (`"Sign in to change your photo"` / `"Changing your photo is not available here"`) and set `error`; the hook itself never throws at render.
|
|
118
|
+
- **Only the signed-in user's own photo.** There is no way to read another user's photo: `useUsers()` and `useDirectory()` rows do not carry one.
|
|
119
|
+
- **No new scope.** The hook only ever touches the caller's own account.
|
|
120
|
+
|
|
121
|
+
A host backs the hook with the new optional `WidgetContext.account` slice, `{ setPhoto(file), removePhoto() }`. A host without it needs no change: `photoUrl` reads `null` and `canEdit` is false.
|
|
122
|
+
### What's new in 0.186.0 (contract 1.147.0)
|
|
123
|
+
|
|
124
|
+
**A page's top navigation bar puts its tabs on the left or the right; the centred option is gone.** `resolveTopBarTokens().tabAlignment` is now `"left"` | `"right"`. A `theme_config.topBar.tabAlignment` of `"center"` resolves to `"left"`, like any other unknown value, and the Studio drops it the next time the theme is saved. Host-integration surface only — no author-facing hook, prop, primitive, or manifest field changed.
|
|
78
125
|
|
|
79
126
|
### What's new in 0.185.0 (contract 1.146.0)
|
|
80
127
|
|
package/dist/contract.cjs
CHANGED
|
@@ -808,12 +808,39 @@ const HOOKS = [
|
|
|
808
808
|
id: "string | null",
|
|
809
809
|
email: "string | null",
|
|
810
810
|
displayName: "string | null",
|
|
811
|
+
photoUrl:
|
|
812
|
+
"string | null // the signed-in user's own profile photo, an absolute URL ready for <Avatar url> or <Image source={{ uri }}>; null when none is set, for anonymous visitors, and on a host that does not provide it",
|
|
811
813
|
roles: "string[]",
|
|
812
814
|
groupIds: "string[]",
|
|
813
815
|
},
|
|
814
816
|
requiredContextSlice: ["user"],
|
|
815
817
|
scopes: null,
|
|
816
818
|
},
|
|
819
|
+
{
|
|
820
|
+
name: "useUserPhoto",
|
|
821
|
+
signature: "useUserPhoto()",
|
|
822
|
+
description:
|
|
823
|
+
"Read and change the signed-in user's OWN profile photo. url mirrors useUser().photoUrl. upload(file) takes a browser " +
|
|
824
|
+
"File on web and expo-file-system's File on native: the file <FilePicker> gives you, or asset.file from useCamera() / " +
|
|
825
|
+
"useImageEditor() — NEVER the asset itself — and the host sends it; never build a FormData or call an endpoint yourself. The server crops " +
|
|
826
|
+
"the image to a square and resizes it. PNG, JPEG or WebP, 10 MB at most; a refusal carries err.status: 413 too large, " +
|
|
827
|
+
"415 unsupported type, 400 unreadable image. remove() clears the photo. After either resolves, useUser().photoUrl and " +
|
|
828
|
+
"url update in every widget on the page. canEdit is false when nobody is signed in and on a host that cannot change " +
|
|
829
|
+
"the photo (the Builder canvas preview): hide or disable the control then — upload() / remove() reject with an Error and " +
|
|
830
|
+
"set error rather than the hook throwing at render, as does upload() with no file. uploading is true while any call is in flight; error holds the last " +
|
|
831
|
+
"failure and clears on the next attempt. Needs no scope: it only ever touches the caller's own account. There is NO way " +
|
|
832
|
+
"to read another user's photo — useUsers() and useDirectory() rows carry none. Identical on web (Player) and the Expo export.",
|
|
833
|
+
returnShape: {
|
|
834
|
+
url: "string | null // same value as useUser().photoUrl",
|
|
835
|
+
canEdit: "boolean // signed in AND the host can change the photo",
|
|
836
|
+
upload: "(file: File) => Promise<{ photoUrl: string | null }>",
|
|
837
|
+
remove: "() => Promise<{ photoUrl: null }>",
|
|
838
|
+
uploading: "boolean",
|
|
839
|
+
error: "Error | null",
|
|
840
|
+
},
|
|
841
|
+
requiredContextSlice: ["user", "account.setPhoto", "account.removePhoto"],
|
|
842
|
+
scopes: null,
|
|
843
|
+
},
|
|
817
844
|
{
|
|
818
845
|
name: "useFill",
|
|
819
846
|
signature: "useFill()",
|
|
@@ -2792,9 +2819,26 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2792
2819
|
},
|
|
2793
2820
|
user: {
|
|
2794
2821
|
description:
|
|
2795
|
-
"Signed-in user, host-built context object (camelCase: { id, email, displayName, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. Not a data-client.",
|
|
2822
|
+
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. Not a data-client.",
|
|
2796
2823
|
required: true,
|
|
2797
|
-
fields: {
|
|
2824
|
+
fields: {
|
|
2825
|
+
id: "string",
|
|
2826
|
+
email: "string",
|
|
2827
|
+
displayName: "string",
|
|
2828
|
+
photoUrl: "string | null",
|
|
2829
|
+
},
|
|
2830
|
+
},
|
|
2831
|
+
// Optional so a host that cannot change the photo needs no stub:
|
|
2832
|
+
// useUserPhoto() reports canEdit false when it is absent.
|
|
2833
|
+
account: {
|
|
2834
|
+
description:
|
|
2835
|
+
"Optional self-service account actions for the signed-in user: " +
|
|
2836
|
+
"{ setPhoto(file) -> Promise<{ photoUrl }>, removePhoto() -> Promise<{ photoUrl: null }> }. " +
|
|
2837
|
+
"Backs useUserPhoto(). The host performs the request AND updates its own session, so user.photoUrl changes for every " +
|
|
2838
|
+
"widget. setPhoto takes a browser File on web and expo-file-system's File on native. Absent on a host that cannot " +
|
|
2839
|
+
"change the photo (the Builder canvas preview), where useUserPhoto() degrades to canEdit: false rather than throwing.",
|
|
2840
|
+
required: false,
|
|
2841
|
+
fields: { setPhoto: "function", removePhoto: "function" },
|
|
2798
2842
|
},
|
|
2799
2843
|
workspace: {
|
|
2800
2844
|
description:
|
|
@@ -4813,7 +4857,14 @@ const CONTRACT = deepFreeze({
|
|
|
4813
4857
|
// sized to their content. `<Button>` / `<ListRow>` read their `style`
|
|
4814
4858
|
// through the same flatten; their behaviour is unchanged. `<Stat>`'s
|
|
4815
4859
|
// icon, figure and delta wrap in a tile too narrow to hold them.
|
|
4816
|
-
|
|
4860
|
+
// 1.147.0: narrowing (sc-10123) — `resolveTopBarTokens().tabAlignment` is
|
|
4861
|
+
// "left" | "right". A stored "center" resolves to "left".
|
|
4862
|
+
// 1.148.0: additive (sc-10142) — `useUser()` returns `photoUrl`, the
|
|
4863
|
+
// signed-in user's own profile photo (null when unset or not provided).
|
|
4864
|
+
// New `useUserPhoto()` changes it through the new OPTIONAL context
|
|
4865
|
+
// slice `account` ({ setPhoto, removePhoto }); a host without it reads
|
|
4866
|
+
// `canEdit: false`.
|
|
4867
|
+
version: "1.148.0",
|
|
4817
4868
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4818
4869
|
hooks: HOOKS,
|
|
4819
4870
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -808,12 +808,39 @@ const HOOKS = [
|
|
|
808
808
|
id: "string | null",
|
|
809
809
|
email: "string | null",
|
|
810
810
|
displayName: "string | null",
|
|
811
|
+
photoUrl:
|
|
812
|
+
"string | null // the signed-in user's own profile photo, an absolute URL ready for <Avatar url> or <Image source={{ uri }}>; null when none is set, for anonymous visitors, and on a host that does not provide it",
|
|
811
813
|
roles: "string[]",
|
|
812
814
|
groupIds: "string[]",
|
|
813
815
|
},
|
|
814
816
|
requiredContextSlice: ["user"],
|
|
815
817
|
scopes: null,
|
|
816
818
|
},
|
|
819
|
+
{
|
|
820
|
+
name: "useUserPhoto",
|
|
821
|
+
signature: "useUserPhoto()",
|
|
822
|
+
description:
|
|
823
|
+
"Read and change the signed-in user's OWN profile photo. url mirrors useUser().photoUrl. upload(file) takes a browser " +
|
|
824
|
+
"File on web and expo-file-system's File on native: the file <FilePicker> gives you, or asset.file from useCamera() / " +
|
|
825
|
+
"useImageEditor() — NEVER the asset itself — and the host sends it; never build a FormData or call an endpoint yourself. The server crops " +
|
|
826
|
+
"the image to a square and resizes it. PNG, JPEG or WebP, 10 MB at most; a refusal carries err.status: 413 too large, " +
|
|
827
|
+
"415 unsupported type, 400 unreadable image. remove() clears the photo. After either resolves, useUser().photoUrl and " +
|
|
828
|
+
"url update in every widget on the page. canEdit is false when nobody is signed in and on a host that cannot change " +
|
|
829
|
+
"the photo (the Builder canvas preview): hide or disable the control then — upload() / remove() reject with an Error and " +
|
|
830
|
+
"set error rather than the hook throwing at render, as does upload() with no file. uploading is true while any call is in flight; error holds the last " +
|
|
831
|
+
"failure and clears on the next attempt. Needs no scope: it only ever touches the caller's own account. There is NO way " +
|
|
832
|
+
"to read another user's photo — useUsers() and useDirectory() rows carry none. Identical on web (Player) and the Expo export.",
|
|
833
|
+
returnShape: {
|
|
834
|
+
url: "string | null // same value as useUser().photoUrl",
|
|
835
|
+
canEdit: "boolean // signed in AND the host can change the photo",
|
|
836
|
+
upload: "(file: File) => Promise<{ photoUrl: string | null }>",
|
|
837
|
+
remove: "() => Promise<{ photoUrl: null }>",
|
|
838
|
+
uploading: "boolean",
|
|
839
|
+
error: "Error | null",
|
|
840
|
+
},
|
|
841
|
+
requiredContextSlice: ["user", "account.setPhoto", "account.removePhoto"],
|
|
842
|
+
scopes: null,
|
|
843
|
+
},
|
|
817
844
|
{
|
|
818
845
|
name: "useFill",
|
|
819
846
|
signature: "useFill()",
|
|
@@ -2788,9 +2815,26 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2788
2815
|
},
|
|
2789
2816
|
user: {
|
|
2790
2817
|
description:
|
|
2791
|
-
"Signed-in user, host-built context object (camelCase: { id, email, displayName, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. Not a data-client.",
|
|
2818
|
+
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. Not a data-client.",
|
|
2792
2819
|
required: true,
|
|
2793
|
-
fields: {
|
|
2820
|
+
fields: {
|
|
2821
|
+
id: "string",
|
|
2822
|
+
email: "string",
|
|
2823
|
+
displayName: "string",
|
|
2824
|
+
photoUrl: "string | null",
|
|
2825
|
+
},
|
|
2826
|
+
},
|
|
2827
|
+
// Optional so a host that cannot change the photo needs no stub:
|
|
2828
|
+
// useUserPhoto() reports canEdit false when it is absent.
|
|
2829
|
+
account: {
|
|
2830
|
+
description:
|
|
2831
|
+
"Optional self-service account actions for the signed-in user: " +
|
|
2832
|
+
"{ setPhoto(file) -> Promise<{ photoUrl }>, removePhoto() -> Promise<{ photoUrl: null }> }. " +
|
|
2833
|
+
"Backs useUserPhoto(). The host performs the request AND updates its own session, so user.photoUrl changes for every " +
|
|
2834
|
+
"widget. setPhoto takes a browser File on web and expo-file-system's File on native. Absent on a host that cannot " +
|
|
2835
|
+
"change the photo (the Builder canvas preview), where useUserPhoto() degrades to canEdit: false rather than throwing.",
|
|
2836
|
+
required: false,
|
|
2837
|
+
fields: { setPhoto: "function", removePhoto: "function" },
|
|
2794
2838
|
},
|
|
2795
2839
|
workspace: {
|
|
2796
2840
|
description:
|
|
@@ -4809,7 +4853,14 @@ const CONTRACT = deepFreeze({
|
|
|
4809
4853
|
// sized to their content. `<Button>` / `<ListRow>` read their `style`
|
|
4810
4854
|
// through the same flatten; their behaviour is unchanged. `<Stat>`'s
|
|
4811
4855
|
// icon, figure and delta wrap in a tile too narrow to hold them.
|
|
4812
|
-
|
|
4856
|
+
// 1.147.0: narrowing (sc-10123) — `resolveTopBarTokens().tabAlignment` is
|
|
4857
|
+
// "left" | "right". A stored "center" resolves to "left".
|
|
4858
|
+
// 1.148.0: additive (sc-10142) — `useUser()` returns `photoUrl`, the
|
|
4859
|
+
// signed-in user's own profile photo (null when unset or not provided).
|
|
4860
|
+
// New `useUserPhoto()` changes it through the new OPTIONAL context
|
|
4861
|
+
// slice `account` ({ setPhoto, removePhoto }); a host without it reads
|
|
4862
|
+
// `canEdit: false`.
|
|
4863
|
+
version: "1.148.0",
|
|
4813
4864
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4814
4865
|
hooks: HOOKS,
|
|
4815
4866
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -242,8 +242,10 @@ export function useWidgetStyle() {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
|
-
* Returns the active end-user identity
|
|
246
|
-
* `{ id, email, displayName, roles, groupIds }` (camelCase — host-built context, not a wire payload).
|
|
245
|
+
* Returns the active end-user identity the host provides, e.g.
|
|
246
|
+
* `{ id, email, displayName, photoUrl, roles, groupIds }` (camelCase — host-built context, not a wire payload).
|
|
247
|
+
* It is the host's own object, or a memoised copy carrying `photoUrl: null`
|
|
248
|
+
* when the host omits that field.
|
|
247
249
|
*
|
|
248
250
|
* `id` is `null` for anonymous visitors (and on the Studio canvas preview,
|
|
249
251
|
* which renders widgets as if signed-out so the public branch shows). The
|
|
@@ -251,12 +253,112 @@ export function useWidgetStyle() {
|
|
|
251
253
|
* fields; widgets read them directly. The current principal is host-provided
|
|
252
254
|
* — this hook does NOT read a data-client.
|
|
253
255
|
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
+
* `photoUrl` is the user's own profile photo as an absolute URL, ready for
|
|
257
|
+
* `<Avatar url>` or `<Image source={{ uri }}>`; `null` when none is set, for
|
|
258
|
+
* anonymous visitors, and on a host that does not provide it.
|
|
259
|
+
*
|
|
260
|
+
* Use this to render the signed-in user's name and photo in a header, branch
|
|
261
|
+
* on roles, or stamp a created-by field.
|
|
256
262
|
*/
|
|
257
263
|
export function useUser() {
|
|
258
264
|
const ctx = useWidgetContextOrThrow("useUser");
|
|
259
|
-
|
|
265
|
+
const user = ctx.user;
|
|
266
|
+
// A host predating photoUrl omits it; the type promises null.
|
|
267
|
+
return useMemo(
|
|
268
|
+
() => (user && user.photoUrl === undefined ? { ...user, photoUrl: null } : user),
|
|
269
|
+
[user],
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const USER_PHOTO_UNAVAILABLE = "Changing your photo is not available here";
|
|
274
|
+
const USER_PHOTO_SIGNED_OUT = "Sign in to change your photo";
|
|
275
|
+
const USER_PHOTO_NO_FILE = "useUserPhoto: file is required";
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Read and change the signed-in user's OWN profile photo. Returns
|
|
279
|
+
* `{ url, canEdit, upload, remove, uploading, error }`.
|
|
280
|
+
*
|
|
281
|
+
* `url` mirrors `useUser().photoUrl`. `upload(file)` takes a browser `File`
|
|
282
|
+
* on the web Player and expo-file-system's `File` on the Expo export: the
|
|
283
|
+
* file `<FilePicker>` gives you, or `asset.file` from `useCamera()` /
|
|
284
|
+
* `useImageEditor()` — never the asset itself. The server crops the image to
|
|
285
|
+
* a square and resizes it; PNG, JPEG or WebP, 10 MB at most. A refusal
|
|
286
|
+
* carries `err.status`: 413 too large, 415 unsupported type, 400 unreadable
|
|
287
|
+
* image. `uploading` is true while any `upload` / `remove` is in flight.
|
|
288
|
+
*
|
|
289
|
+
* The host owns the request and its own session (`ctx.account`), so a change
|
|
290
|
+
* reaches every widget's `useUser().photoUrl` at once. `ctx.account` is
|
|
291
|
+
* optional: where it is absent (the Builder canvas preview) or nobody is
|
|
292
|
+
* signed in, `canEdit` is false and `upload` / `remove` reject instead of the
|
|
293
|
+
* hook throwing at render.
|
|
294
|
+
*
|
|
295
|
+
* There is no way to read ANOTHER user's photo: `useUsers()` and
|
|
296
|
+
* `useDirectory()` rows carry none.
|
|
297
|
+
*/
|
|
298
|
+
export function useUserPhoto() {
|
|
299
|
+
const ctx = useWidgetContextOrThrow("useUserPhoto");
|
|
300
|
+
const user = ctx.user;
|
|
301
|
+
const account = ctx.account;
|
|
302
|
+
const hostCanEdit =
|
|
303
|
+
Boolean(account) &&
|
|
304
|
+
typeof account.setPhoto === "function" &&
|
|
305
|
+
typeof account.removePhoto === "function";
|
|
306
|
+
const signedIn = Boolean(user && user.id);
|
|
307
|
+
const canEdit = hostCanEdit && signedIn;
|
|
308
|
+
const url =
|
|
309
|
+
user && typeof user.photoUrl === "string" && user.photoUrl
|
|
310
|
+
? user.photoUrl
|
|
311
|
+
: null;
|
|
312
|
+
|
|
313
|
+
const accountRef = useRef(null);
|
|
314
|
+
accountRef.current = hostCanEdit ? account : null;
|
|
315
|
+
const refusalRef = useRef(null);
|
|
316
|
+
refusalRef.current = !hostCanEdit
|
|
317
|
+
? USER_PHOTO_UNAVAILABLE
|
|
318
|
+
: !signedIn
|
|
319
|
+
? USER_PHOTO_SIGNED_OUT
|
|
320
|
+
: null;
|
|
321
|
+
|
|
322
|
+
const inFlightRef = useRef(0);
|
|
323
|
+
const [uploading, setUploading] = useState(false);
|
|
324
|
+
const [error, setError] = useState(null);
|
|
325
|
+
|
|
326
|
+
const change = useCallback(async (perform, invalidCall) => {
|
|
327
|
+
const refusal = refusalRef.current || invalidCall;
|
|
328
|
+
if (refusal) {
|
|
329
|
+
const err = new Error(refusal);
|
|
330
|
+
setError(err);
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
inFlightRef.current += 1;
|
|
334
|
+
setUploading(true);
|
|
335
|
+
setError(null);
|
|
336
|
+
try {
|
|
337
|
+
return await perform(accountRef.current);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
setError(err);
|
|
340
|
+
throw err;
|
|
341
|
+
} finally {
|
|
342
|
+
// upload() and remove() may overlap; the last one clears it.
|
|
343
|
+
inFlightRef.current -= 1;
|
|
344
|
+
setUploading(inFlightRef.current > 0);
|
|
345
|
+
}
|
|
346
|
+
}, []);
|
|
347
|
+
|
|
348
|
+
const upload = useCallback(
|
|
349
|
+
(file) =>
|
|
350
|
+
change(
|
|
351
|
+
(host) => host.setPhoto(file),
|
|
352
|
+
file ? null : USER_PHOTO_NO_FILE,
|
|
353
|
+
),
|
|
354
|
+
[change],
|
|
355
|
+
);
|
|
356
|
+
const remove = useCallback(
|
|
357
|
+
() => change((host) => host.removePhoto()),
|
|
358
|
+
[change],
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
return { url, canEdit, upload, remove, uploading, error };
|
|
260
362
|
}
|
|
261
363
|
|
|
262
364
|
/**
|
|
@@ -3030,11 +3132,11 @@ export function useAgent(agentRef) {
|
|
|
3030
3132
|
cancelled ||
|
|
3031
3133
|
agentIdRef.current !== agentId ||
|
|
3032
3134
|
resumeGenerationRef.current !== generation ||
|
|
3033
|
-
suppressResumeRef.current ||
|
|
3034
|
-
// A `send` that started during the resume already latched a thread.
|
|
3035
|
-
// Overwriting it would orphan that PAID turn in a conversation
|
|
3036
|
-
// nothing reads again, and point the next send somewhere else.
|
|
3037
|
-
conversationRef.current
|
|
3135
|
+
suppressResumeRef.current ||
|
|
3136
|
+
// A `send` that started during the resume already latched a thread.
|
|
3137
|
+
// Overwriting it would orphan that PAID turn in a conversation
|
|
3138
|
+
// nothing reads again, and point the next send somewhere else.
|
|
3139
|
+
conversationRef.current
|
|
3038
3140
|
) {
|
|
3039
3141
|
return;
|
|
3040
3142
|
}
|
package/dist/host.d.ts
CHANGED
|
@@ -292,7 +292,7 @@ export interface TopBarTokens {
|
|
|
292
292
|
* current tab so the page shows through it. */
|
|
293
293
|
tabStyle: "underline" | "attached";
|
|
294
294
|
/** Where the row's tabs sit across its width; `left` unless authored. */
|
|
295
|
-
tabAlignment: "left" | "
|
|
295
|
+
tabAlignment: "left" | "right";
|
|
296
296
|
/** The column the tabs sit in: the whole bar, or the page content's. The
|
|
297
297
|
* bar's surface spans the full width either way. */
|
|
298
298
|
tabRowWidth: "full" | "content";
|
package/dist/index.d.ts
CHANGED
|
@@ -646,9 +646,21 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
646
646
|
id: string | null;
|
|
647
647
|
email: string | null;
|
|
648
648
|
displayName: string | null;
|
|
649
|
+
/** The user's own profile photo as an absolute URL; null when none is set. */
|
|
650
|
+
photoUrl: string | null;
|
|
649
651
|
roles: string[];
|
|
650
652
|
groupIds: string[];
|
|
651
653
|
};
|
|
654
|
+
/**
|
|
655
|
+
* Optional self-service account actions backing `useUserPhoto()`. The host
|
|
656
|
+
* performs the request and updates its own session, so `user.photoUrl`
|
|
657
|
+
* changes for every widget. Absent where the photo cannot be changed (the
|
|
658
|
+
* Builder canvas preview); `useUserPhoto().canEdit` is then false.
|
|
659
|
+
*/
|
|
660
|
+
account?: {
|
|
661
|
+
setPhoto(file: UserPhotoFile): Promise<{ photoUrl: string | null }>;
|
|
662
|
+
removePhoto(): Promise<{ photoUrl: null }>;
|
|
663
|
+
};
|
|
652
664
|
workspace: {
|
|
653
665
|
id: string;
|
|
654
666
|
slug: string;
|
|
@@ -1510,10 +1522,54 @@ export function useUser(): {
|
|
|
1510
1522
|
id: string | null;
|
|
1511
1523
|
email: string | null;
|
|
1512
1524
|
displayName: string | null;
|
|
1525
|
+
/**
|
|
1526
|
+
* The user's own profile photo as an absolute URL, ready for `<Avatar url>`
|
|
1527
|
+
* or `<Image source={{ uri }}>`. Null when none is set, for anonymous
|
|
1528
|
+
* visitors, and on a host that does not provide it.
|
|
1529
|
+
*/
|
|
1530
|
+
photoUrl: string | null;
|
|
1513
1531
|
roles: string[];
|
|
1514
1532
|
groupIds: string[];
|
|
1515
1533
|
};
|
|
1516
1534
|
|
|
1535
|
+
/**
|
|
1536
|
+
* The file `useUserPhoto().upload` takes: a browser `File` on the web Player
|
|
1537
|
+
* and expo-file-system's `File` on the Expo export. It is the file
|
|
1538
|
+
* `<FilePicker>` gives you, or `asset.file` from `useCamera()` /
|
|
1539
|
+
* `useImageEditor()` — never the asset itself.
|
|
1540
|
+
*/
|
|
1541
|
+
export type UserPhotoFile = Blob & { name?: string };
|
|
1542
|
+
|
|
1543
|
+
export interface UserPhotoApi {
|
|
1544
|
+
/** Same value as `useUser().photoUrl`. */
|
|
1545
|
+
url: string | null;
|
|
1546
|
+
/** True when a user is signed in and the host can change the photo. */
|
|
1547
|
+
canEdit: boolean;
|
|
1548
|
+
/**
|
|
1549
|
+
* Replace the photo. The server crops the image to a square and resizes it.
|
|
1550
|
+
* PNG, JPEG or WebP, 10 MB at most. A refusal carries `status`: 413 too
|
|
1551
|
+
* large, 415 unsupported type, 400 unreadable image. Rejects, and sets
|
|
1552
|
+
* `error`, when `canEdit` is false or no file is passed.
|
|
1553
|
+
*/
|
|
1554
|
+
upload(file: UserPhotoFile): Promise<{ photoUrl: string | null }>;
|
|
1555
|
+
/** Clear the photo. Rejects when `canEdit` is false. */
|
|
1556
|
+
remove(): Promise<{ photoUrl: null }>;
|
|
1557
|
+
/** True while any `upload` or `remove` call is in flight. */
|
|
1558
|
+
uploading: boolean;
|
|
1559
|
+
/** The last failure; cleared by the next attempt. */
|
|
1560
|
+
error: (Error & { status?: number }) | null;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* Read and change the signed-in user's OWN profile photo. Never throws at
|
|
1565
|
+
* render: where nobody is signed in, or the host cannot change the photo,
|
|
1566
|
+
* `canEdit` is false and `upload` / `remove` reject. Pass `upload` the
|
|
1567
|
+
* file `<FilePicker>` gives you, or `asset.file` from `useCamera()` /
|
|
1568
|
+
* `useImageEditor()`. There is no way to read another user's photo;
|
|
1569
|
+
* `useUsers()` and `useDirectory()` rows carry none.
|
|
1570
|
+
*/
|
|
1571
|
+
export function useUserPhoto(): UserPhotoApi;
|
|
1572
|
+
|
|
1517
1573
|
/**
|
|
1518
1574
|
* REQ-LAY-08 — returns `true` when the host has sized this widget to fill its
|
|
1519
1575
|
* layout slot's available height (a page-grid tile set to "Fill tile height",
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/dist/navigation.cjs
CHANGED
|
@@ -535,7 +535,7 @@ function resolveTopStripeLayout(theme) {
|
|
|
535
535
|
};
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
-
const TAB_ALIGNMENTS = ["left", "
|
|
538
|
+
const TAB_ALIGNMENTS = ["left", "right"];
|
|
539
539
|
const TAB_ROW_WIDTHS = ["full", "content"];
|
|
540
540
|
|
|
541
541
|
function resolveTopBarTokens(theme) {
|
|
@@ -569,8 +569,8 @@ function resolveTopBarTokens(theme) {
|
|
|
569
569
|
// surface and meets the page beneath it, so the two read as one plane. Meaningless on a shape that draws no tab row — a host
|
|
570
570
|
// reads it only where it has tabs to draw.
|
|
571
571
|
tabStyle: topBar.tabStyle === "attached" ? "attached" : "underline",
|
|
572
|
-
// sc-10123:
|
|
573
|
-
//
|
|
572
|
+
// sc-10123: which end of the row the tabs sit at. Both hosts place them
|
|
573
|
+
// with a flexible spacer rather than justifying the row, so a row that
|
|
574
574
|
// overflows still scrolls from its first tab instead of clipping it.
|
|
575
575
|
tabAlignment: TAB_ALIGNMENTS.includes(topBar.tabAlignment)
|
|
576
576
|
? topBar.tabAlignment
|
package/dist/navigation.js
CHANGED
|
@@ -526,7 +526,7 @@ export function resolveTopStripeLayout(theme) {
|
|
|
526
526
|
};
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
-
const TAB_ALIGNMENTS = ["left", "
|
|
529
|
+
const TAB_ALIGNMENTS = ["left", "right"];
|
|
530
530
|
const TAB_ROW_WIDTHS = ["full", "content"];
|
|
531
531
|
|
|
532
532
|
export function resolveTopBarTokens(theme) {
|
|
@@ -560,8 +560,8 @@ export function resolveTopBarTokens(theme) {
|
|
|
560
560
|
// surface and meets the page beneath it, so the two read as one plane. Meaningless on a shape that draws no tab row — a host
|
|
561
561
|
// reads it only where it has tabs to draw.
|
|
562
562
|
tabStyle: topBar.tabStyle === "attached" ? "attached" : "underline",
|
|
563
|
-
// sc-10123:
|
|
564
|
-
//
|
|
563
|
+
// sc-10123: which end of the row the tabs sit at. Both hosts place them
|
|
564
|
+
// with a flexible spacer rather than justifying the row, so a row that
|
|
565
565
|
// overflows still scrolls from its first tab instead of clipping it.
|
|
566
566
|
tabAlignment: TAB_ALIGNMENTS.includes(topBar.tabAlignment)
|
|
567
567
|
? topBar.tabAlignment
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.187.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",
|