@colixsystems/widget-sdk 0.197.0 → 0.198.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 +57 -1
- package/dist/avatar-color.js +35 -0
- package/dist/contract.cjs +184 -24
- package/dist/contract.js +184 -24
- package/dist/detail-parts-view.js +9 -4
- package/dist/hooks.js +535 -28
- package/dist/index.d.ts +177 -4
- package/dist/index.js +8 -0
- package/dist/index.native.js +8 -0
- package/dist/initials.js +3 -2
- package/dist/manifest.cjs +2 -1
- package/dist/manifest.js +2 -1
- package/dist/overlay-view.js +2 -5
- package/dist/overlay.js +12 -5
- package/dist/primitives.js +2 -0
- package/dist/primitives.native.js +2 -0
- package/dist/video.js +51 -0
- package/dist/video.native.js +61 -0
- package/dist/web-modal.js +139 -0
- package/package.json +5 -1
- package/src/contract.js +184 -24
package/README.md
CHANGED
|
@@ -23,6 +23,8 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
23
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
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. |
|
|
25
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) |
|
|
26
|
+
| **CORE** | `useFilePreview(file)` | `{ uri }` | No scope. A temporary address an `<Image>` can show for a file the person picked but has not uploaded yet (a photo in a composer). Native picks use their own uri; on the web the SDK creates and releases an object URL, which widgets may not do themselves. `uri` is null for no file. |
|
|
27
|
+
| **CORE** | `usePageBadge(count)` | `{ supported }` | `ctx.navigation.setBadge` — no scope. Puts `count` on the menu entry or tab of the page the widget is on (a chat's unread messages). The host sums the page's widgets and keeps the last count after the widget leaves the screen, until it reports again; `0` clears this widget's share. `supported` is false where the host draws no page badges (the Builder canvas). |
|
|
26
28
|
| **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. |
|
|
27
29
|
| **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. |
|
|
28
30
|
| **CORE** | `useWidgetRoute(initial)` | `[state, setState]` | `ctx.widgetRoute` — no scope. Where YOUR WIDGET is, persisted by the host so it survives a reload and travels in a shared link (the `w_<instanceId>` query key on web, the screen's route params natively): the folder a browser has opened, a wizard step, a selected tab, a list's sort and search. `useState` semantics over an object — writes MERGE, `null` clears a key back to its `initial`, and `initial` is read once. Values are scalars or flat arrays of scalars, size- and length-capped; anything else is not stored. NOT history (Back still leaves the page, on both platforms). Degrades to component state on the Studio canvas. |
|
|
@@ -67,6 +69,8 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
67
69
|
| **FILESTORE** (`ctx.filestore`) | `usePdfExport({ spaceType, folderId })` (optional: spaceType, folderId) | `{ openPdf, opening, canOpenPdf, exportToPdf, exporting, error, lastExported }` | `ctx.filestore.files.renderPdf` + `ctx.device.pdfViewer`, `ctx.filestore.files.exportPdf` — `files.write:*`. `openPdf(html, { fileName? })` renders the HTML to a PDF server-side and OPENS it (web: PDF tab; native: print / share sheet) without saving. `exportToPdf(html, { fileName?, folderId? })` renders it and saves it as a file (`application/pdf`). Same server-side renderer on web + native. |
|
|
68
70
|
| **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser`. Rejects with `PaymentError { code, message, retryable }`; when `retryable` is `false` show `message` and drop the retry. Charges are accepted ONLY in the currency the workspace sells in — omit `currency` and the platform applies it (a disagreeing literal is a publish-blocking `payment-currency` finding). |
|
|
69
71
|
| **NOTIFICATIONS** (`ctx.notifications`) | `useSendNotification()` | `{ send, sending, error }` | `ctx.notifications.send` — `notifications.send:appUser`. `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace; call from an event handler (never render); rejects with `NotificationError`. |
|
|
72
|
+
| **NOTIFICATIONS** (`ctx.notifications`) | `useRecordMutes(tableId)` | `{ isMuted, loading, error, refetch }` | `ctx.notifications.mutes` — no scope. Which records of one table the signed-in user has muted, in one request for a whole list; `isMuted(recordId)` turns false when a timed mute lapses, and a `useRecordMute` write shows at once. |
|
|
73
|
+
| **NOTIFICATIONS** (`ctx.notifications`) | `useRecordMute(tableId, recordId)` | `{ muted, mutedUntil, loading, error, mute, unmute, refetch }` | `ctx.notifications.mutes` — no scope. Mutes one record's notifications for the signed-in user, `mute(until)` with an ISO time, a `Date`, or `null` for until unmuted. Needs read access to the record (`RECORD_NOT_FOUND` otherwise). |
|
|
70
74
|
| **IDENTIFICATION** (`ctx.identification`) | `useIdentification({ provider, purpose, pollIntervalMs })` (optional: every key, and the whole argument) | `{ available, status, qr, autoStartToken, message, identity, identificationId, start, refresh, cancel, reset, … }` | `ctx.identification.*` — no scope (the visitor is deliberately NOT signed in). Gate the UI on `available`; `start()` opens the order and the hook polls to completion. `identity` carries `personal_number_masked` + a stable `subject_hash` — never a raw personal number. |
|
|
71
75
|
|
|
72
76
|
All list calls return the `{ data, meta }` envelope; the read hooks unwrap `res.data` for you. There is no `useWorkspace()` or `useLogger()` hook — read the theme via `useTheme()` and the locale via `useI18n()`; the host logger lives on `ctx.logger` (`{ debug, info, warn, error }`).
|
|
@@ -77,7 +81,59 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
77
81
|
|
|
78
82
|
## Status
|
|
79
83
|
|
|
80
|
-
`v0.
|
|
84
|
+
`v0.198.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**.
|
|
85
|
+
|
|
86
|
+
### What's new in 0.198.0 (contract 1.158.0)
|
|
87
|
+
|
|
88
|
+
**`manifest.translations` may declare up to 200 keys (was 100).** A messenger or a form builder outgrows 100 strings once it ships every label its users read. Widening: every manifest that validated before still does.
|
|
89
|
+
|
|
90
|
+
**`useDirectory({ ids })` resolves names for the people a widget already knows about.** Pass up to 100 user ids — message senders, members of a conversation — and get their `{ id, name, role }` rows without loading the whole roster; pair it with `q` for a server-side people search. Additive.
|
|
91
|
+
|
|
92
|
+
**Files can be attached to a datastore record and stay private to it.** `useFilestoreUpload({ spaceType: "record", recordId })` attaches the uploaded file to that record, and `useFilestoreFiles({ spaceType: "record", recordId, type })` lists its attachments newest first (`type: "media"` for images and videos, `"document"` for the rest). Exactly the people who may read the record may read its attachments, through the same permission check as the record itself. Uploading needs write on the record, only the uploader may delete an attachment, and removing the record removes its attachments. A record attachment's URL is issued to one viewer and lives about ten minutes, so re-resolve it (`useFilestoreFile(id).refetch()`) when an image fails to load. The new `useFilestoreDelete()` returns `{ remove, removing, error }`: `remove(fileId)` deletes a record attachment permanently (only its uploader may) and moves any other file to the trash. `useFilestoreCopy().copy(fileId, { spaceType: "record", recordId })` attaches a copy of a file to a record, with the same write check as uploading there. `useCamera().pick({ multiple: true })` lets the user choose up to 10 photos from the device library at once and resolves an array of assets; `pick()` alone is unchanged. `<Avatar seed={user.id} />` draws the initials on that person's own colour, the same one the Studio's member lists use, and `avatarColorFor(seed)` returns it.
|
|
93
|
+
|
|
94
|
+
**A `datastoreTemplate` can make inherited access read-only and stamp author columns on the server.** An `inheritAcl` RELATION column may add `inheritAclReadOnly: true`. Rows then inherit only READ access from the row they point at: members of a conversation read every message in it and may post into it, but change or delete only the messages they created (pair it with `grantCreatorPermissions`). This also holds down a chain, such as a reaction on a message in a conversation. A column may declare `defaultValue`, which is written when a create omits the column. Use `"@currentUser"` on a USER column, `"@now"` on a DATE or DATETIME column, or a literal of the column's own type. FILE, RELATION and USER_GROUP columns take no default, because ids belong to the installing workspace. `defaultLocked: true` on a `"@currentUser"` or `"@now"` default makes the server always stamp it. Any value a client sends for the column is ignored on create and on update, so a `Sender` or `CreatedBy` column is always the real author. `immutable: true` makes a column write-once: a create sets it from the value sent or the default, and any value sent for it on update is ignored, so a message's `Conversation` cannot be moved after it is posted. It is not valid on a MANY_TO_MANY relation, whose links live outside the row. Additive.
|
|
95
|
+
|
|
96
|
+
**A user can mute one record's notifications.** `useRecordMute(tableId, recordId)` returns `{ muted, mutedUntil, loading, error, mute, unmute, refetch }`. `mute(until)` takes an ISO time or a `Date`, or `null` (or nothing) to mute until `unmute()`. While a mute is active, a server action's `notifications.notifyRecordSubjects` on that record sends the user nothing: no inbox row, no push, no email. `mutedUntil` is the ISO time the mute ends, or `null` when it lasts until unmuted, and `muted` turns false by itself when that time passes. The user must be able to read the record, so a record they cannot read fails with `NotificationError { code: "RECORD_NOT_FOUND" }`, just like one that does not exist. A falsy id returns a stable not-muted result with no request. On a host without `ctx.notifications.mutes` (the Builder canvas preview) the hook reads not muted, and `mute()` / `unmute()` reject with `NOTIFICATIONS_UNAVAILABLE`. No scope is needed, because it only changes the user's own setting. To show mutes in a list, call `useRecordMutes(tableId)` once: `isMuted(recordId)` answers for every row from one request, and a mute made through `useRecordMute` on the same table shows at once. `NotificationError.code` now carries the server's code when it sends one (`INVALID_TITLE`, `RECORD_NOT_FOUND`, …), and a 401 reads `AUTH_REQUIRED`. Additive.
|
|
97
|
+
|
|
98
|
+
```jsx
|
|
99
|
+
import { useRecordMute, Pressable, Text } from "@colixsystems/widget-sdk";
|
|
100
|
+
|
|
101
|
+
function MuteToggle({ tableId, conversationId }) {
|
|
102
|
+
const { muted, mute, unmute } = useRecordMute(tableId, conversationId);
|
|
103
|
+
return (
|
|
104
|
+
<Pressable onPress={() => (muted ? unmute() : mute(null)).catch(() => {})}>
|
|
105
|
+
<Text>{muted ? "Unmute" : "Mute"}</Text>
|
|
106
|
+
</Pressable>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**A list can show every muted record at once.** `useRecordMutes(tableId)` returns `{ isMuted, loading, error, refetch }` from one request, so a conversation list marks its muted rows without a read per row. No scope. Additive.
|
|
112
|
+
|
|
113
|
+
**A widget can put a count on its page's menu entry.** `usePageBadge(count)` returns `{ supported }` and shows `count` on the menu entry or tab of the page the widget is on, on the web Player and in the exported app, through the new optional `ctx.navigation.setBadge`. The host adds up the counts of the page's widgets and keeps the last one after the widget leaves the screen; `0` clears it. No scope. Additive.
|
|
114
|
+
|
|
115
|
+
**A picked file can be shown before it is uploaded.** `useFilePreview(file)` returns `{ uri }` for an `<Image>`: a native pick's own `uri`, or an object URL the hook releases on the web. No scope. Additive.
|
|
116
|
+
|
|
117
|
+
**Videos play inline on both hosts.** `<Video source={url} />` plays a file with the platform's own controls and shows its first frame before playback: the browser's `<video>` on the web, expo-video's VideoView in the exported app. `contentFit` is `"contain"` (default) or `"cover"`, `controls` defaults to `true`, `autoPlay` starts it muted, and `onError()` fires when this host cannot play the file, so offer a download instead. A browser plays only the codecs it supports; H.264 MP4 plays everywhere.
|
|
118
|
+
|
|
119
|
+
```jsx
|
|
120
|
+
import { Video } from "@colixsystems/widget-sdk";
|
|
121
|
+
|
|
122
|
+
<Video
|
|
123
|
+
source={file.url}
|
|
124
|
+
contentFit="cover"
|
|
125
|
+
style={{ width: 240, height: 180, borderRadius: theme.radii.md, overflow: "hidden" }}
|
|
126
|
+
onError={() => setPlayable(false)}
|
|
127
|
+
/>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**A widget with pages of its own can answer Android's back button.** `useHardwareBack(handler)` runs `handler` on a back press while the widget's screen is in view; return `true` when it went back a step (closed a thread, left a settings page), `false` to let the app go back, and pass `null` while there is nothing to go back from. The exported app answers only for the screen in view, so a widget kept mounted on another tab never swallows the press. `supported` is `false` on the web Player, where browsers have no hardware back, so keep a visible back control too. No scope. Additive.
|
|
131
|
+
|
|
132
|
+
**The photo picker chooses several, and videos.** `useCamera().pick({ multiple: true })` resolves an array of up to 10 assets, and `pick({ videos: true })` offers videos beside photos. A video asset has a `video/*` `mimeType` and `durationMs` (`null` for a photo); on iOS it arrives as H.264 so other devices can play it. Additive.
|
|
133
|
+
|
|
134
|
+
**A person keeps one avatar everywhere.** `<Avatar seed={userId}>` and `avatarColorFor(seed)` give a person the monogram colour the Studio's member lists use, and `initialsFor` now gives a one-word name two letters ("Börje" → "BÖ"), so an `<Avatar>` of a single-name user changes from one letter to two.
|
|
135
|
+
|
|
136
|
+
**Fixes.** `<Overlay>` opens at once under React StrictMode on the web: react-native-web's `Modal` lost its page container to StrictMode's effect replay, so a sheet stayed invisible until an unrelated re-render; the web binding now uses its own modal layer with the same dialog role, Escape, focus trap and focus return. `useDatastoreMutation` returns one stable object while its methods are unchanged, so a widget can list it as a dependency. `NotificationError.code` carries the server's code when it sends one.
|
|
81
137
|
|
|
82
138
|
### What's new in 0.197.0 (contract 1.157.0)
|
|
83
139
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// sc-10268 — the ONE person tint, shared by the Studio's member lists and the
|
|
2
|
+
// SDK `<Avatar seed>`, so a person seeded the same way is one colour everywhere.
|
|
3
|
+
|
|
4
|
+
// Brand-neutral accents, each dark enough for white initials.
|
|
5
|
+
const AVATAR_COLORS = Object.freeze([
|
|
6
|
+
"#0f766e",
|
|
7
|
+
"#3f5bd6",
|
|
8
|
+
"#b0562a",
|
|
9
|
+
"#7a4bcc",
|
|
10
|
+
"#b83b5e",
|
|
11
|
+
"#2f7d4f",
|
|
12
|
+
"#9333ea",
|
|
13
|
+
"#0369a1",
|
|
14
|
+
"#c2410c",
|
|
15
|
+
"#0e7490",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A stable colour for a seed (a user id, an email), or null for no seed.
|
|
20
|
+
*
|
|
21
|
+
* FNV-1a plus a murmur-style avalanche: a plain `hash * 31 % N` keys the bucket
|
|
22
|
+
* off the low bits, which barely mix, so most seeds collide on one colour.
|
|
23
|
+
*/
|
|
24
|
+
export function avatarColorFor(seed) {
|
|
25
|
+
if (typeof seed !== "string" || !seed) return null;
|
|
26
|
+
let h = 2166136261;
|
|
27
|
+
for (let i = 0; i < seed.length; i += 1) {
|
|
28
|
+
h ^= seed.charCodeAt(i);
|
|
29
|
+
h = Math.imul(h, 16777619);
|
|
30
|
+
}
|
|
31
|
+
h ^= h >>> 13;
|
|
32
|
+
h = Math.imul(h, 0x5bd1e995);
|
|
33
|
+
h ^= h >>> 15;
|
|
34
|
+
return AVATAR_COLORS[(h >>> 0) % AVATAR_COLORS.length];
|
|
35
|
+
}
|
package/dist/contract.cjs
CHANGED
|
@@ -1044,6 +1044,51 @@ const HOOKS = [
|
|
|
1044
1044
|
requiredContextSlice: ["navigation"],
|
|
1045
1045
|
scopes: null,
|
|
1046
1046
|
},
|
|
1047
|
+
{
|
|
1048
|
+
name: "useFilePreview",
|
|
1049
|
+
signature: "useFilePreview(file)",
|
|
1050
|
+
description:
|
|
1051
|
+
"{ uri } an <Image> can show for a file the person picked but has not uploaded yet (a photo waiting in a " +
|
|
1052
|
+
"composer, before it is sent). Native picks carry their own file uri; on the web the SDK makes a temporary " +
|
|
1053
|
+
"address and releases it when the file changes or the component unmounts. uri is null for no file, or a host " +
|
|
1054
|
+
"that cannot preview one. Widgets may not call createObjectURL themselves; this is the sanctioned way. No scope. " +
|
|
1055
|
+
"Identical on web (Player) and the Expo export.",
|
|
1056
|
+
returnShape: {
|
|
1057
|
+
uri: "string | null // pass as <Image source={{ uri }} />; null when there is nothing to show",
|
|
1058
|
+
},
|
|
1059
|
+
requiredContextSlice: [],
|
|
1060
|
+
scopes: null,
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
name: "usePageBadge",
|
|
1064
|
+
signature: "usePageBadge(count)",
|
|
1065
|
+
description:
|
|
1066
|
+
"Puts count on the menu entry or tab of the page this widget is on (a chat's unread messages, open tasks). " +
|
|
1067
|
+
"The host sums every widget on the page and keeps the last count after the widget leaves the screen, until it " +
|
|
1068
|
+
"reports again; 0, or anything not a positive number, clears this widget's share. Returns { supported }: false " +
|
|
1069
|
+
"on a host without page badges (the Builder canvas), where it does nothing. Call it at render with the current " +
|
|
1070
|
+
"count, never in a loop. Backed by ctx.navigation.setBadge. Identical on web (Player) and the Expo export.",
|
|
1071
|
+
returnShape: {
|
|
1072
|
+
supported: "boolean // false where the host draws no page badges",
|
|
1073
|
+
},
|
|
1074
|
+
requiredContextSlice: ["navigation"],
|
|
1075
|
+
scopes: null,
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
name: "useHardwareBack",
|
|
1079
|
+
signature: "useHardwareBack(handler)",
|
|
1080
|
+
description:
|
|
1081
|
+
"Android's back button, for a widget with pages of its own (a thread over a list, a settings page over a thread). " +
|
|
1082
|
+
"handler runs on a back press while the widget's screen is in view: return true when it went back a step, false to " +
|
|
1083
|
+
"let the app go back. Pass null while there is nothing to go back from. Returns { supported }: false on the web " +
|
|
1084
|
+
"Player, where browsers have no hardware back, so always keep a visible back control too. Backed by the optional " +
|
|
1085
|
+
"ctx.navigation.onHardwareBack(listener) → unsubscribe, which the Expo export answers only for the screen in view.",
|
|
1086
|
+
returnShape: {
|
|
1087
|
+
supported: "boolean // false on the web, which has no hardware back",
|
|
1088
|
+
},
|
|
1089
|
+
requiredContextSlice: ["navigation"],
|
|
1090
|
+
scopes: null,
|
|
1091
|
+
},
|
|
1047
1092
|
{
|
|
1048
1093
|
name: "usePageContext",
|
|
1049
1094
|
signature: "usePageContext()",
|
|
@@ -1128,14 +1173,18 @@ const HOOKS = [
|
|
|
1128
1173
|
},
|
|
1129
1174
|
{
|
|
1130
1175
|
name: "useFilestoreFiles",
|
|
1131
|
-
signature: "useFilestoreFiles({ spaceType, folderId, q, type })",
|
|
1132
|
-
optionalArgs: ["folderId", "q", "type"],
|
|
1176
|
+
signature: "useFilestoreFiles({ spaceType, recordId, folderId, q, type })",
|
|
1177
|
+
optionalArgs: ["recordId", "folderId", "q", "type"],
|
|
1133
1178
|
description:
|
|
1134
|
-
"Browse the end-user's Filestore files in a project, personal, or
|
|
1135
|
-
"space. The hook resolves owner_id from the host context (tenant
|
|
1136
|
-
"project/public, app user for personal) — the widget only chooses the " +
|
|
1179
|
+
"Browse the end-user's Filestore files in a project, personal, public or " +
|
|
1180
|
+
"record space. The hook resolves owner_id from the host context (tenant " +
|
|
1181
|
+
"for project/public, app user for personal) — the widget only chooses the " +
|
|
1137
1182
|
"space. A public space is world-readable: its files render even for " +
|
|
1138
|
-
"logged-out Player visitors.
|
|
1183
|
+
"logged-out Player visitors. A record space (`{ spaceType: \"record\", " +
|
|
1184
|
+
"recordId }`) holds one datastore record's attachments, readable by " +
|
|
1185
|
+
"exactly whoever may read that record; it has no folders, lists newest " +
|
|
1186
|
+
"first, and `type: \"media\"` narrows it to images and videos " +
|
|
1187
|
+
"(`\"document\"` to everything else). Reads ctx.filestore.files.list and unwraps " +
|
|
1139
1188
|
"{ data, meta } to the files array. Every row carries a ready-to-render " +
|
|
1140
1189
|
"`url` (its absolutized presigned_url, aliased by the filestore client) " +
|
|
1141
1190
|
"so a gallery renders `files.map(f => f.url)` directly. Every row ALSO " +
|
|
@@ -1190,11 +1239,13 @@ const HOOKS = [
|
|
|
1190
1239
|
},
|
|
1191
1240
|
{
|
|
1192
1241
|
name: "useFilestoreUpload",
|
|
1193
|
-
signature: "useFilestoreUpload({ spaceType, folderId, compress })",
|
|
1194
|
-
optionalArgs: ["folderId", "compress"],
|
|
1242
|
+
signature: "useFilestoreUpload({ spaceType, recordId, folderId, compress })",
|
|
1243
|
+
optionalArgs: ["recordId", "folderId", "compress"],
|
|
1195
1244
|
description:
|
|
1196
1245
|
"Upload a file into the end-user's Filestore space. The widget passes " +
|
|
1197
|
-
"the SPACE (`{ spaceType, folderId? }
|
|
1246
|
+
"the SPACE (`{ spaceType, folderId? }`, or `{ spaceType: \"record\", " +
|
|
1247
|
+
"recordId }` to attach the file to a datastore record: that needs write " +
|
|
1248
|
+
"on the record, and only the uploader may later delete it); the hook resolves owner_id " +
|
|
1198
1249
|
"from the host context, builds a multipart FormData with the " +
|
|
1199
1250
|
"snake_case fields the backend reads verbatim (`space_type`, " +
|
|
1200
1251
|
"`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
|
|
@@ -1318,9 +1369,12 @@ const HOOKS = [
|
|
|
1318
1369
|
signature: "useFilestoreCopy()",
|
|
1319
1370
|
description:
|
|
1320
1371
|
"Copy a Filestore file into any space and folder the caller may add files " +
|
|
1321
|
-
"to, including ACROSS spaces (project, public,
|
|
1322
|
-
"personal space
|
|
1323
|
-
"
|
|
1372
|
+
"to, including ACROSS spaces (project, public, the signed-in user's " +
|
|
1373
|
+
"personal space, and a datastore record's attachments). " +
|
|
1374
|
+
"`copy(fileId, { spaceType, recordId?, folderId?, name? })` names the " +
|
|
1375
|
+
"DESTINATION (spaceType is required; `{ spaceType: \"record\", recordId }` " +
|
|
1376
|
+
"attaches the copy to that record, which needs write access to it and has " +
|
|
1377
|
+
"no folders); the hook resolves its owner_id from " +
|
|
1324
1378
|
"the host context and posts through ctx.filestore.files.copy. Resolves to " +
|
|
1325
1379
|
"the NEW file row (its own id and url) — the source is untouched and the " +
|
|
1326
1380
|
"bytes are duplicated, so later changes to one never affect the other. A " +
|
|
@@ -1332,7 +1386,7 @@ const HOOKS = [
|
|
|
1332
1386
|
"STORAGE_QUOTA_EXCEEDED when the destination space is full; 409 " +
|
|
1333
1387
|
"FILE_PROCESSING while a video is still being processed.",
|
|
1334
1388
|
returnShape: {
|
|
1335
|
-
copy: "(fileId, { spaceType, folderId?, name? }) => Promise<FilestoreFile>",
|
|
1389
|
+
copy: "(fileId, { spaceType, recordId?, folderId?, name? }) => Promise<FilestoreFile> // recordId with spaceType \"record\"",
|
|
1336
1390
|
copying: "boolean",
|
|
1337
1391
|
error: "Error | null",
|
|
1338
1392
|
lastCopied: "FilestoreFile | null",
|
|
@@ -1340,6 +1394,26 @@ const HOOKS = [
|
|
|
1340
1394
|
requiredContextSlice: ["filestore.files"],
|
|
1341
1395
|
scopes: ["files.write:*"],
|
|
1342
1396
|
},
|
|
1397
|
+
{
|
|
1398
|
+
name: "useFilestoreDelete",
|
|
1399
|
+
signature: "useFilestoreDelete()",
|
|
1400
|
+
description:
|
|
1401
|
+
"Delete a Filestore file the caller may delete. `remove(fileId)` calls " +
|
|
1402
|
+
"ctx.filestore.files.remove: a record attachment (a file in a record " +
|
|
1403
|
+
"space) is deleted permanently and only its uploader may do it; a file in " +
|
|
1404
|
+
"any other space moves to the trash. Use it when the thing that showed " +
|
|
1405
|
+
"the file is withdrawn, such as an unsent chat message, so the file stops " +
|
|
1406
|
+
"being readable rather than merely hidden. Refusals arrive as " +
|
|
1407
|
+
"`err.status`: 403 when the caller may not delete it, 404 when it is not " +
|
|
1408
|
+
"visible.",
|
|
1409
|
+
returnShape: {
|
|
1410
|
+
remove: "(fileId) => Promise<void>",
|
|
1411
|
+
removing: "boolean",
|
|
1412
|
+
error: "Error | null",
|
|
1413
|
+
},
|
|
1414
|
+
requiredContextSlice: ["filestore.files"],
|
|
1415
|
+
scopes: ["files.write:*"],
|
|
1416
|
+
},
|
|
1343
1417
|
{
|
|
1344
1418
|
name: "useFileSignature",
|
|
1345
1419
|
signature: "useFileSignature(fileId, existingSignatureId)",
|
|
@@ -1658,6 +1732,8 @@ const HOOKS = [
|
|
|
1658
1732
|
name: "useDirectory",
|
|
1659
1733
|
signature: "useDirectory(query)",
|
|
1660
1734
|
optionalArgs: ["query"],
|
|
1735
|
+
description:
|
|
1736
|
+
"Read-only roster of app users. query is { q?, role?, is_active?, group_id?, ids?, limit?, offset? }; ids (1 to 100) resolves the names behind ids a widget already holds.",
|
|
1661
1737
|
returnShape: {
|
|
1662
1738
|
users: "Array<{ id, name, role }> // snake_case rows; unwrapped from { data, meta }",
|
|
1663
1739
|
loading: "boolean",
|
|
@@ -1745,6 +1821,53 @@ const HOOKS = [
|
|
|
1745
1821
|
requiredContextSlice: ["notifications.send"],
|
|
1746
1822
|
scopes: ["notifications.send:appUser"],
|
|
1747
1823
|
},
|
|
1824
|
+
// sc-10268 — the signed-in user's mute of one record's notifications.
|
|
1825
|
+
// Reads ctx.notifications.mutes. No scope: it only touches the caller's
|
|
1826
|
+
// own setting, and the server requires their read access to the record.
|
|
1827
|
+
{
|
|
1828
|
+
name: "useRecordMute",
|
|
1829
|
+
signature: "useRecordMute(tableId, recordId)",
|
|
1830
|
+
description:
|
|
1831
|
+
"The signed-in user's mute of the notifications ONE datastore record produces for them (a chat conversation, a " +
|
|
1832
|
+
"thread). mute(until) takes an ISO time or a Date, or null (or nothing) to mute until unmute(). While muted, a " +
|
|
1833
|
+
"server action's notifications.notifyRecordSubjects on that record sends this user nothing: no inbox row, no push, " +
|
|
1834
|
+
"no email. mutedUntil is an ISO string, or null when muted until unmuted; muted turns false by itself once " +
|
|
1835
|
+
"mutedUntil passes. The user must be able to read the record, otherwise every call fails with NotificationError " +
|
|
1836
|
+
"{ code: \"RECORD_NOT_FOUND\" }, exactly as for a missing record. A falsy tableId or recordId returns a stable " +
|
|
1837
|
+
"not-muted result with no network call. On a host that cannot mute (the Builder canvas preview) it reads not " +
|
|
1838
|
+
"muted and mute() / unmute() reject with NOTIFICATIONS_UNAVAILABLE instead of the hook throwing at render. " +
|
|
1839
|
+
"Needs no scope: it only ever changes the caller's own setting. Identical on web (Player) and the Expo export.",
|
|
1840
|
+
returnShape: {
|
|
1841
|
+
muted: "boolean",
|
|
1842
|
+
mutedUntil: "string | null // ISO time the mute ends; null when muted until unmuted, or not muted",
|
|
1843
|
+
loading: "boolean",
|
|
1844
|
+
error: "NotificationError | null",
|
|
1845
|
+
mute: "(until?: string | Date | null) => Promise<void> // null or omitted = until unmuted",
|
|
1846
|
+
unmute: "() => Promise<void>",
|
|
1847
|
+
refetch: "() => Promise<void>",
|
|
1848
|
+
},
|
|
1849
|
+
requiredContextSlice: ["notifications.mutes"],
|
|
1850
|
+
scopes: null,
|
|
1851
|
+
},
|
|
1852
|
+
{
|
|
1853
|
+
name: "useRecordMutes",
|
|
1854
|
+
signature: "useRecordMutes(tableId)",
|
|
1855
|
+
description:
|
|
1856
|
+
"Which records of ONE table the signed-in user has muted, read in one request for a whole list (a chat's " +
|
|
1857
|
+
"conversations), never one useRecordMute per row. isMuted(recordId) is true while that record's mute is active " +
|
|
1858
|
+
"and turns false by itself once a timed mute lapses. A mute or unmute made through useRecordMute on the same table " +
|
|
1859
|
+
"shows here at once. A falsy tableId, or a host that cannot mute (the Builder canvas preview), reads nothing " +
|
|
1860
|
+
"muted with no network call. Needs no scope: it only reads the caller's own settings. Identical on web (Player) " +
|
|
1861
|
+
"and the Expo export.",
|
|
1862
|
+
returnShape: {
|
|
1863
|
+
isMuted: "(recordId: string) => boolean",
|
|
1864
|
+
loading: "boolean",
|
|
1865
|
+
error: "NotificationError | null",
|
|
1866
|
+
refetch: "() => Promise<void>",
|
|
1867
|
+
},
|
|
1868
|
+
requiredContextSlice: ["notifications.mutes"],
|
|
1869
|
+
scopes: null,
|
|
1870
|
+
},
|
|
1748
1871
|
// REQ-USERMGMT / REQ-ACL-SYS M3 — AppUser administration. Returns
|
|
1749
1872
|
// `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }`.
|
|
1750
1873
|
// Reads need `users.read:*` scope; edit-style mutations (invite /
|
|
@@ -2298,9 +2421,12 @@ const HOOKS = [
|
|
|
2298
2421
|
optionalArgs: ["options"],
|
|
2299
2422
|
description:
|
|
2300
2423
|
"Take a photo or choose one from the device library. Returns { asset, loading, error, supported, capture, pick, reset }. " +
|
|
2424
|
+
"pick({ multiple: true }) lets the user choose up to 10 photos at once and resolves an ARRAY of assets (asset then holds that array); " +
|
|
2425
|
+
"pick({ videos: true }) offers videos beside photos — a video asset has a video/* mimeType and durationMs (null for a photo); play it with <Video>. " +
|
|
2426
|
+
"capture() always takes one. " +
|
|
2301
2427
|
"Capture is IMPERATIVE — call capture() or pick() from a user gesture (a tap); the browser and the mobile OS gate the " +
|
|
2302
2428
|
"permission prompt on a gesture, so it NEVER opens on mount. Both resolve to a normalised asset " +
|
|
2303
|
-
"{ uri, name, mimeType, width, height, size, file }, or NULL when the user dismisses the picker — dismissal is the " +
|
|
2429
|
+
"{ uri, name, mimeType, width, height, size, durationMs, file }, or NULL when the user dismisses the picker — dismissal is the " +
|
|
2304
2430
|
"common case and is deliberately NOT an error, so no try/catch is needed on the happy path. They reject with a " +
|
|
2305
2431
|
"CameraError whose .code is one of PERMISSION_DENIED | UNSUPPORTED | INTERNAL. `asset.file` is already the right " +
|
|
2306
2432
|
"upload part for the host — a BLOB on both (a File on web, expo-file-system's File on native), because Expo's fetch " +
|
|
@@ -2313,13 +2439,13 @@ const HOOKS = [
|
|
|
2313
2439
|
"rejects UNSUPPORTED — there is no silent fall back to a file dialog.",
|
|
2314
2440
|
returnShape: {
|
|
2315
2441
|
asset:
|
|
2316
|
-
"{ uri, name, mimeType, width, height, size, file } | null",
|
|
2442
|
+
"{ uri, name, mimeType, width, height, size, durationMs, file } | null",
|
|
2317
2443
|
loading: "boolean",
|
|
2318
2444
|
error: "CameraError | null",
|
|
2319
2445
|
supported: "boolean // false when the host brokers no camera",
|
|
2320
2446
|
capture:
|
|
2321
2447
|
"() => Promise<asset | null> // null if dismissed; rejects with CameraError",
|
|
2322
|
-
pick: "() => Promise<asset | null> // null if dismissed; rejects with CameraError",
|
|
2448
|
+
pick: "(options?: { multiple?: boolean, videos?: boolean }) => Promise<asset | asset[] | null> // { multiple: true } resolves an array of up to 10; { videos: true } offers videos too; null if dismissed; rejects with CameraError",
|
|
2323
2449
|
reset: "() => void // clear the asset + error and release it",
|
|
2324
2450
|
},
|
|
2325
2451
|
requiredContextSlice: [],
|
|
@@ -2529,6 +2655,14 @@ const PRIMITIVES = [
|
|
|
2529
2655
|
rnComponent: "expo-linear-gradient",
|
|
2530
2656
|
docsUrl: "https://docs.expo.dev/versions/latest/sdk/linear-gradient/",
|
|
2531
2657
|
},
|
|
2658
|
+
// sc-10268 — inline video playback with no platform split.
|
|
2659
|
+
{
|
|
2660
|
+
name: "Video",
|
|
2661
|
+
description:
|
|
2662
|
+
"Plays a video file inline with the platform's own controls. `<Video source={url} contentFit=\"cover\" style={{ width: 240, height: 180, borderRadius: theme.radii.md, overflow: \"hidden\" }} />`. `source` is a playable URL (a filestore file's url, or a picked asset's `uri` for a preview); `controls` (default true) shows play/seek/fullscreen; `autoPlay` starts it muted (browsers only autoplay muted video); `contentFit` is \"contain\" (default, letterboxed) or \"cover\"; `onError()` fires when the file cannot be played, so offer a download instead. Before playback it shows the first frame. Size it with `style` (it fills its box). Web uses the browser's <video>; native uses expo-video's VideoView, so the same widget plays on both hosts. A browser plays only the codecs it supports, so H.264 MP4 is the format that plays everywhere.",
|
|
2663
|
+
rnComponent: "expo-video",
|
|
2664
|
+
docsUrl: "https://docs.expo.dev/versions/latest/sdk/video/",
|
|
2665
|
+
},
|
|
2532
2666
|
// sc-6531 (REQ-AI-AGENT-DESIGN-LIFT) — the hover/press lift affordance. A
|
|
2533
2667
|
// FUNCTION primitive, not a component: it resolves the interaction tokens per
|
|
2534
2668
|
// input, so one declaration answers the pointer on web and the press on
|
|
@@ -2628,7 +2762,7 @@ const PRIMITIVES = [
|
|
|
2628
2762
|
{
|
|
2629
2763
|
name: "Avatar",
|
|
2630
2764
|
description:
|
|
2631
|
-
'Themed picture of a person. `<Avatar name={row.owner.label} url={photoUrl} size="sm" />`. THE way to show a PERSON: in a `<ListRow>` leading slot, a profile header, a list of members. It falls back from the picture to the name\'s initials to a person glyph, so the slot is never blank. Never use it for a place, product or topic. Props: `name` (the initials and the accessibility label come from it), `url` (an absolute image URL; resolve a file field to a URL first), `size` ("sm" 32 | "md" 40 | "lg" 56 | "xl" 80, or a number of px), `shape` ("circle" | "rounded"), `accessibilityLabel`, `style`, `testID`.',
|
|
2765
|
+
'Themed picture of a person. `<Avatar name={row.owner.label} url={photoUrl} size="sm" />`. THE way to show a PERSON: in a `<ListRow>` leading slot, a profile header, a list of members. It falls back from the picture to the name\'s initials to a person glyph, so the slot is never blank. Never use it for a place, product or topic. Props: `name` (the initials and the accessibility label come from it), `seed` (a stable id such as the user\'s id: the initials sit on that person\'s own colour, the same one the Studio\'s member lists use, so people in a list tell apart at a glance; `avatarColorFor(seed)` returns it), `url` (an absolute image URL; resolve a file field to a URL first), `size` ("sm" 32 | "md" 40 | "lg" 56 | "xl" 80, or a number of px), `shape` ("circle" | "rounded"), `accessibilityLabel`, `style`, `testID`.',
|
|
2632
2766
|
rnComponent: null,
|
|
2633
2767
|
docsUrl: null,
|
|
2634
2768
|
},
|
|
@@ -2917,13 +3051,13 @@ const MANIFEST_SCHEMA = {
|
|
|
2917
3051
|
type: "object",
|
|
2918
3052
|
required: false,
|
|
2919
3053
|
description:
|
|
2920
|
-
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 8 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected.",
|
|
3054
|
+
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 8 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation).",
|
|
2921
3055
|
},
|
|
2922
3056
|
translations: {
|
|
2923
3057
|
type: "object",
|
|
2924
3058
|
required: false,
|
|
2925
3059
|
description:
|
|
2926
|
-
"Optional (REQ-L10N-WIDGET). Translation strings the widget ships. Shape { <key>: { en: string, <locale>?: string, ... } } — `en` is REQUIRED per key. At install the host merges these into the tenant's localization dictionary under a per-widget namespace (`widget.<manifest.id>.<key>`); `useI18n().t(\"<key>\")` resolves the namespaced key automatically, so authors never type the prefix. Non-destructive: an existing admin-edited value is never overwritten, and a language the tenant has not added is never created (values for absent locales are dropped; the widget's `en` seeds the tenant's base language so every key renders). Keys persist across uninstalls; admins prune them from the Translations admin (bulk delete by `widget.<id>.` prefix). Limits: ≤
|
|
3060
|
+
"Optional (REQ-L10N-WIDGET). Translation strings the widget ships. Shape { <key>: { en: string, <locale>?: string, ... } } — `en` is REQUIRED per key. At install the host merges these into the tenant's localization dictionary under a per-widget namespace (`widget.<manifest.id>.<key>`); `useI18n().t(\"<key>\")` resolves the namespaced key automatically, so authors never type the prefix. Non-destructive: an existing admin-edited value is never overwritten, and a language the tenant has not added is never created (values for absent locales are dropped; the widget's `en` seeds the tenant's base language so every key renders). Keys persist across uninstalls; admins prune them from the Translations admin (bulk delete by `widget.<id>.` prefix). Limits: ≤200 keys; each key matches /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/; each value ≤1 KB; the namespaced key must fit the dictionary key cap (128 chars).",
|
|
2927
3061
|
default: {},
|
|
2928
3062
|
},
|
|
2929
3063
|
};
|
|
@@ -2991,7 +3125,8 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2991
3125
|
},
|
|
2992
3126
|
},
|
|
2993
3127
|
navigation: {
|
|
2994
|
-
description:
|
|
3128
|
+
description:
|
|
3129
|
+
"{ push(path), replace(path), back() }, and optionally setBadge(count) — the page badge usePageBadge() writes, bound by the host to this widget on its page — and onHardwareBack(listener) → unsubscribe, Android's back button for useHardwareBack(), which the native host answers only for the screen in view.",
|
|
2995
3130
|
required: true,
|
|
2996
3131
|
fields: { push: "function", replace: "function", back: "function" },
|
|
2997
3132
|
},
|
|
@@ -3162,9 +3297,9 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3162
3297
|
// sc-890 — backs useSendNotification(). Mirror of contract.js.
|
|
3163
3298
|
notifications: {
|
|
3164
3299
|
description:
|
|
3165
|
-
"Injected @colixsystems/notifications-client instance (sc-890). { send(body) -> Promise<notification
|
|
3300
|
+
"Injected @colixsystems/notifications-client instance (sc-890). { send(body) -> Promise<notification>, mutes: { get, set, clear } }. send backs useSendNotification(); requires the notifications.send:appUser scope. The host POSTs the body snake_case verbatim ({ recipient_user_id, title, body, link?, payload? }) to /notifications/send and returns the created notification row; the call is imperative (a send never fires on mount). mutes backs useRecordMute() and useRecordMutes() (sc-10268): get({ table_id, record_id }) / set({ table_id, record_id, muted_until }) -> { muted, muted_until }, clear({ table_id, record_id }) -> undefined, list({ table_id, limit?, offset? }) -> { data: [{ record_id, muted_until }], meta }. mutes is optional — a host without it (the Builder canvas preview) leaves useRecordMute() reading not muted.",
|
|
3166
3301
|
required: true,
|
|
3167
|
-
fields: { send: "function" },
|
|
3302
|
+
fields: { send: "function", mutes: "object" },
|
|
3168
3303
|
},
|
|
3169
3304
|
// REQ-IDENT (sc-4313) — backs useIdentification(). Mirror of contract.js.
|
|
3170
3305
|
identification: {
|
|
@@ -3475,7 +3610,8 @@ const VETTED_IMPORTS = [
|
|
|
3475
3610
|
platforms: ["native"],
|
|
3476
3611
|
category: "media",
|
|
3477
3612
|
description:
|
|
3478
|
-
"Native video playback (Expo SDK 56; replaces the removed expo-av). Native-only; pair with the browser <video> element in widget.web.jsx."
|
|
3613
|
+
"Native video playback (Expo SDK 56; replaces the removed expo-av). Native-only; pair with the browser <video> element in widget.web.jsx. " +
|
|
3614
|
+
"To play a video file on BOTH hosts, prefer the SDK's <Video> primitive — no platform split needed.",
|
|
3479
3615
|
},
|
|
3480
3616
|
{
|
|
3481
3617
|
specifier: "@react-native-community/datetimepicker",
|
|
@@ -5108,7 +5244,31 @@ const CONTRACT = deepFreeze({
|
|
|
5108
5244
|
// as before; a malformed `via` is dropped, or voids the gradient under the
|
|
5109
5245
|
// new `{ strict: true }` option. New `componentGradientColors` / `componentGradientMidpoint`
|
|
5110
5246
|
// read the paint list and the ink basis off a normalised gradient.
|
|
5111
|
-
|
|
5247
|
+
// 1.158.0: additive (sc-10268) — a datastoreTemplate column may declare
|
|
5248
|
+
// `inheritAclReadOnly` (with `inheritAcl`), `defaultValue`, and
|
|
5249
|
+
// `defaultLocked` (with a "@currentUser" / "@now" default), and
|
|
5250
|
+
// `immutable` (write-once; not on MANY_TO_MANY).
|
|
5251
|
+
// (sc-10269) `useFilestoreFiles` / `useFilestoreUpload` accept
|
|
5252
|
+
// `spaceType: "record"` with `recordId`: a record's private attachments.
|
|
5253
|
+
// New `useFilestoreDelete()` → `{ remove, removing, error }`.
|
|
5254
|
+
// `useCamera().pick({ multiple: true })` resolves an array of up to 10.
|
|
5255
|
+
// `<Avatar seed>` + `avatarColorFor(seed)`: a person's own monogram
|
|
5256
|
+
// colour, shared with the Studio's member lists.
|
|
5257
|
+
// New `useRecordMute(tableId, recordId)` mutes one record's
|
|
5258
|
+
// notifications for the signed-in user, through the new
|
|
5259
|
+
// `ctx.notifications.mutes` ({ get, set, clear }).
|
|
5260
|
+
// `useFilestoreCopy().copy` takes `{ spaceType: "record", recordId }`.
|
|
5261
|
+
// `useDirectory({ ids })` resolves the names behind up to 100 user ids.
|
|
5262
|
+
// `manifest.translations` may declare up to 200 keys (was 100).
|
|
5263
|
+
// New `useRecordMutes(tableId)` lists the muted records at once;
|
|
5264
|
+
// `usePageBadge(count)` counts on the page's menu entry through the new
|
|
5265
|
+
// optional `ctx.navigation.setBadge`; `useFilePreview(file)` shows a
|
|
5266
|
+
// picked file before upload. New `<Video>` plays a file inline on both
|
|
5267
|
+
// hosts, and `useCamera().pick({ videos: true })` offers videos, an
|
|
5268
|
+
// asset carrying `durationMs`. New `useHardwareBack(handler)` walks a
|
|
5269
|
+
// widget's own pages on Android's back button (optional
|
|
5270
|
+
// `ctx.navigation.onHardwareBack`; unsupported on the web).
|
|
5271
|
+
version: "1.158.0",
|
|
5112
5272
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5113
5273
|
hooks: HOOKS,
|
|
5114
5274
|
primitives: PRIMITIVES,
|