@colixsystems/widget-sdk 0.42.0 → 0.44.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 CHANGED
@@ -8,7 +8,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
8
8
  | ----------- | ------- | -------- |
9
9
  | `ctx.datastore` | `@colixsystems/datastore-client` | `tables.{list,get}`, `schema(tableId)`, `records(tableId).{ list(query), get(id), create(values), update(id,values) [PATCH], delete(id), aggregate(spec), permissions(recordId).{ list, grant, update, revoke } }` |
10
10
  | `ctx.directory` | `@colixsystems/directory-client` | `me()`, `users.{list,get,invite,deactivate,reactivate}`, `groups.{list,create,remove,addMember,removeMember,listMine}`, `invites.{list,revoke,resend}` |
11
- | `ctx.assets` | `@colixsystems/assets-client` | the Asset Manager: `get(id)`, `list(query)`, `upload(formData)` over `/files` — what `useAsset()` resolves |
11
+ | `ctx.assets` | `@colixsystems/assets-client` | the Asset Manager: `get(id)`, `list(query)`, `upload(formData)` over `/files` — what `useAsset()` (single asset by id) and `useAssetsByTag()` (every asset carrying a tag) resolve |
12
12
  | `ctx.payments` | `@colixsystems/payments-client` | `requestPayment(body)`, `getPayment(id)` |
13
13
 
14
14
  **Wire / casing: snake_case end to end.** The clients send and return snake_case **verbatim** (`created_at`, `group_ids`, `can_read`, `amount_cents`, `data_type`, `is_active`, …). There is **no case transform anywhere** — not on the client and not in the backend; the only casing boundary is Prisma `@map` (snake_case field → camelCase column). Author-defined record column values pass through verbatim. Every `list(...)` returns the `{ data, meta }` envelope; the read hooks unwrap `res.data` for you.
@@ -34,6 +34,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
34
34
  | **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*` |
35
35
  | **DATASTORE** | `useRecordPermissions(tableId, recordId)` | `{ permissions, loading, error, grant, revoke, update, refetch }` | `records(table).permissions(record).{ list, grant, update, revoke }` — `acl.write:records` (+ `can_grant` on the record) |
36
36
  | **FILES** (`ctx.assets`) | `useAsset(id)` | `{ url, file, loading, error, refetch }` | `ctx.assets.get` — no scope |
37
+ | **FILES** | `useAssetsByTag(tag, { type? })` | `{ assets, loading, error, refetch }` | `ctx.assets.list` (unwraps `{ data, meta }` to `assets`) — no scope. `type` defaults to `"image"`; pass `"all"` / `"audio"` / `"video"` / `"document"` to widen. Falsy `tag` collapses to `assets: []` without a round-trip. |
37
38
  | **DIRECTORY** (`ctx.directory`) | `useDirectory(query?)` | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
38
39
  | **DIRECTORY** | `useUsers(query?)` | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` | `directory.users.*` — `users.read:*` (edits also `users.write:*`; `remove()` also `users.delete:*`) |
39
40
  | **DIRECTORY** | `useGroups(query?)` | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
@@ -48,7 +49,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
48
49
 
49
50
  ## Status
50
51
 
51
- `v0.42.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**.
52
+ `v0.44.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**.
53
+
54
+ ### What's new in 0.44.0
55
+
56
+ **Vetted `@shopify/react-native-skia` for canvas-style graphics & games (sc-1270).** Widgets that need true 2D/GPU canvas drawing (games, custom visualisations) can now `import` Skia. It is **native-only** (`platforms: ["native"]`, like `react-native-maps` / `lottie-react-native`): author it in `widget.native.jsx` and pair it with a web variant in `widget.web.jsx` — a browser `<canvas>` or `react-native-svg`. This closes the gap where a raw `<canvas>` rendered in the web Player but crashed the Expo export ("View config getter callback for component `canvas` … received undefined") because React Native has no `<canvas>`. The compiler pins `@shopify/react-native-skia` in the exported app's `package.json`; no host shim is added (native-only packages are never shimmed, and there is no Skia web build wired into the Player). Unified Skia-on-web (CanvasKit/WASM) is a documented follow-up. Additive — `CONTRACT.version` bumped to 1.31.0.
57
+
58
+ ### What's new in 0.43.0
59
+
60
+ **New `useAssetsByTag(tag, { type? })` hook (sc-1241).** Lists every tenant asset carrying a given tag — backs the built-in Gallery widget's "All images with a tag" source mode (which used to render a stub) and is a general SDK primitive any widget can call (an audio playlist filtered by mood, a document index by category, an image wall by topic). Reads `ctx.assets.list({ tag, type, limit })` (already injected by both the web Player and the native Expo export) and unwraps `{ data, meta }`. `type` defaults to `"image"` so the common Gallery case gets only images back; pass `"all"` (or `"audio"` / `"video"` / `"document"`) to widen. Falsy `tag` collapses to `assets: []` without a network round-trip. Additive — `CONTRACT.version` bumped to 1.30.0. The `assets` context slice grows a new required field `list: "function"` (both hosts already inject it).
52
61
 
53
62
  ### What's new in 0.42.0
54
63
 
package/dist/contract.cjs CHANGED
@@ -175,6 +175,28 @@ const HOOKS = [
175
175
  requiredContextSlice: ["assets.get"],
176
176
  scopes: null,
177
177
  },
178
+ {
179
+ name: "useAssetsByTag",
180
+ signature: "useAssetsByTag(tag, { type? } = {})",
181
+ description:
182
+ "List every tenant asset carrying a given tag. Backs the Gallery " +
183
+ "widget's tag-source mode but is a general SDK primitive — any widget " +
184
+ "can render a collection of assets the author groups by tag (image " +
185
+ "wall, audio playlist filtered by mood, document index by category). " +
186
+ "Reads ctx.assets.list({ tag, type, limit }) and unwraps the " +
187
+ "{ data, meta } envelope to the assets array. When tag is falsy the " +
188
+ "hook collapses to an empty result without a network round-trip. " +
189
+ "type defaults to 'image' so a Gallery using it gets only images " +
190
+ "back; pass type: 'all' (or 'audio' / 'video' / 'document') to widen.",
191
+ returnShape: {
192
+ assets: "Asset[]",
193
+ loading: "boolean",
194
+ error: "DatastoreError | null",
195
+ refetch: "() => Promise<void>",
196
+ },
197
+ requiredContextSlice: ["assets.list"],
198
+ scopes: null,
199
+ },
178
200
  {
179
201
  name: "useFilestoreFiles",
180
202
  signature: "useFilestoreFiles({ spaceType, folderId?, q?, type? })",
@@ -924,9 +946,9 @@ const WIDGET_CONTEXT_SHAPE = {
924
946
  description:
925
947
  "Injected @colixsystems/assets-client instance, FLATTENED so asset ops are top-level. " +
926
948
  "{ get(id) -> Promise<{ id, url, ... }>, list(query) -> Promise<{ data, meta }>, upload(formData) }. " +
927
- "Backs useAsset(); the returned asset already carries an absolute url the widget can drop into an <Image source>.",
949
+ "Backs useAsset() (resolve one asset by id) and useAssetsByTag() (list every tenant asset carrying a tag); the returned assets already carry absolute urls the widget can drop into an <Image source>.",
928
950
  required: true,
929
- fields: { get: "function" },
951
+ fields: { get: "function", list: "function" },
930
952
  },
931
953
  filestore: {
932
954
  description:
@@ -1140,6 +1162,13 @@ const VETTED_IMPORTS = [
1140
1162
  description:
1141
1163
  "Cross-platform SVG drawing primitives. Used by the built-in Chart widget; works on both platforms.",
1142
1164
  },
1165
+ {
1166
+ specifier: "@shopify/react-native-skia",
1167
+ platforms: ["native"],
1168
+ category: "drawing",
1169
+ description:
1170
+ "Canvas-style 2D/GPU drawing & animation (games, custom graphics) on native. Native-only — author it in widget.native.jsx and pair it with a web variant in widget.web.jsx (a <canvas> or react-native-svg). There is no Skia web build wired into the Player.",
1171
+ },
1143
1172
  {
1144
1173
  specifier: "lucide-react-native",
1145
1174
  platforms: ["web", "native"],
@@ -1627,7 +1656,23 @@ const CONTRACT = deepFreeze({
1627
1656
  // useDatastoreRecord / useAsset) auto-subscribe their own `refetch`.
1628
1657
  // No existing hook, primitive, manifest field, or token changed shape
1629
1658
  // — minor bump on the pre-1.0 channel.
1630
- version: "1.29.0",
1659
+ //
1660
+ // 1.30.0: additive (sc-1241) — `useAssetsByTag(tag, { type? })` lists every
1661
+ // tenant asset carrying a given tag. Backs the Gallery widget's
1662
+ // "All images with a tag" source mode (which used to render a stub),
1663
+ // but is a general SDK primitive any widget can call. Reads
1664
+ // `ctx.assets.list({ tag, type, limit })` (already injected by both
1665
+ // hosts) and unwraps `{ data, meta }`. New required field on the
1666
+ // `assets` context slice: `list: "function"`. No existing hook,
1667
+ // primitive, manifest field, or token changed shape — minor bump.
1668
+ //
1669
+ // 1.31.0: additive (sc-1270) — vetted `@shopify/react-native-skia` for
1670
+ // canvas-style 2D/GPU drawing & games. Native-only (platforms:
1671
+ // ["native"]); authors pair it with a <canvas>/react-native-svg web
1672
+ // variant in widget.web.jsx. Pinned in the compiler's export
1673
+ // package.json; no host shim (native-only is never shimmed). No hook,
1674
+ // primitive, manifest field, or token changed shape — minor bump.
1675
+ version: "1.31.0",
1631
1676
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1632
1677
  hooks: HOOKS,
1633
1678
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -175,6 +175,28 @@ const HOOKS = [
175
175
  requiredContextSlice: ["assets.get"],
176
176
  scopes: null,
177
177
  },
178
+ {
179
+ name: "useAssetsByTag",
180
+ signature: "useAssetsByTag(tag, { type? } = {})",
181
+ description:
182
+ "List every tenant asset carrying a given tag. Backs the Gallery " +
183
+ "widget's tag-source mode but is a general SDK primitive — any widget " +
184
+ "can render a collection of assets the author groups by tag (image " +
185
+ "wall, audio playlist filtered by mood, document index by category). " +
186
+ "Reads ctx.assets.list({ tag, type, limit }) and unwraps the " +
187
+ "{ data, meta } envelope to the assets array. When tag is falsy the " +
188
+ "hook collapses to an empty result without a network round-trip. " +
189
+ "type defaults to 'image' so a Gallery using it gets only images " +
190
+ "back; pass type: 'all' (or 'audio' / 'video' / 'document') to widen.",
191
+ returnShape: {
192
+ assets: "Asset[]",
193
+ loading: "boolean",
194
+ error: "DatastoreError | null",
195
+ refetch: "() => Promise<void>",
196
+ },
197
+ requiredContextSlice: ["assets.list"],
198
+ scopes: null,
199
+ },
178
200
  {
179
201
  name: "useFilestoreFiles",
180
202
  signature: "useFilestoreFiles({ spaceType, folderId?, q?, type? })",
@@ -924,9 +946,9 @@ const WIDGET_CONTEXT_SHAPE = {
924
946
  description:
925
947
  "Injected @colixsystems/assets-client instance, FLATTENED so asset ops are top-level. " +
926
948
  "{ get(id) -> Promise<{ id, url, ... }>, list(query) -> Promise<{ data, meta }>, upload(formData) }. " +
927
- "Backs useAsset(); the returned asset already carries an absolute url the widget can drop into an <Image source>.",
949
+ "Backs useAsset() (resolve one asset by id) and useAssetsByTag() (list every tenant asset carrying a tag); the returned assets already carry absolute urls the widget can drop into an <Image source>.",
928
950
  required: true,
929
- fields: { get: "function" },
951
+ fields: { get: "function", list: "function" },
930
952
  },
931
953
  filestore: {
932
954
  description:
@@ -1140,6 +1162,13 @@ const VETTED_IMPORTS = [
1140
1162
  description:
1141
1163
  "Cross-platform SVG drawing primitives. Used by the built-in Chart widget; works on both platforms.",
1142
1164
  },
1165
+ {
1166
+ specifier: "@shopify/react-native-skia",
1167
+ platforms: ["native"],
1168
+ category: "drawing",
1169
+ description:
1170
+ "Canvas-style 2D/GPU drawing & animation (games, custom graphics) on native. Native-only — author it in widget.native.jsx and pair it with a web variant in widget.web.jsx (a <canvas> or react-native-svg). There is no Skia web build wired into the Player.",
1171
+ },
1143
1172
  {
1144
1173
  specifier: "lucide-react-native",
1145
1174
  platforms: ["web", "native"],
@@ -1627,7 +1656,23 @@ const CONTRACT = deepFreeze({
1627
1656
  // useDatastoreRecord / useAsset) auto-subscribe their own `refetch`.
1628
1657
  // No existing hook, primitive, manifest field, or token changed shape
1629
1658
  // — minor bump on the pre-1.0 channel.
1630
- version: "1.29.0",
1659
+ //
1660
+ // 1.30.0: additive (sc-1241) — `useAssetsByTag(tag, { type? })` lists every
1661
+ // tenant asset carrying a given tag. Backs the Gallery widget's
1662
+ // "All images with a tag" source mode (which used to render a stub),
1663
+ // but is a general SDK primitive any widget can call. Reads
1664
+ // `ctx.assets.list({ tag, type, limit })` (already injected by both
1665
+ // hosts) and unwraps `{ data, meta }`. New required field on the
1666
+ // `assets` context slice: `list: "function"`. No existing hook,
1667
+ // primitive, manifest field, or token changed shape — minor bump.
1668
+ //
1669
+ // 1.31.0: additive (sc-1270) — vetted `@shopify/react-native-skia` for
1670
+ // canvas-style 2D/GPU drawing & games. Native-only (platforms:
1671
+ // ["native"]); authors pair it with a <canvas>/react-native-svg web
1672
+ // variant in widget.web.jsx. Pinned in the compiler's export
1673
+ // package.json; no host shim (native-only is never shimmed). No hook,
1674
+ // primitive, manifest field, or token changed shape — minor bump.
1675
+ version: "1.31.0",
1631
1676
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1632
1677
  hooks: HOOKS,
1633
1678
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -1197,6 +1197,80 @@ export function useAsset(assetId) {
1197
1197
  return { url, asset, loading, error, refetch };
1198
1198
  }
1199
1199
 
1200
+ /**
1201
+ * List every tenant asset carrying a given tag. Returns
1202
+ * `{ assets, loading, error, refetch }`. The widget passes a single `tag`
1203
+ * string plus an optional `{ type }` filter (defaults to `"image"` so the
1204
+ * common gallery case gets only images back; pass `"all"` / `"audio"` /
1205
+ * `"video"` / `"document"` to widen). The hook calls
1206
+ * `ctx.assets.list({ tag, type, limit: 1000 })` and unwraps the
1207
+ * `{ data, meta }` envelope to the array. When `tag` is falsy or whitespace
1208
+ * the hook collapses to an empty result without a network round-trip.
1209
+ *
1210
+ * Like `useAsset`, every returned row already carries an absolute `url` the
1211
+ * widget can drop into `<Image source>`.
1212
+ */
1213
+ export function useAssetsByTag(tag, options) {
1214
+ const ctx = useWidgetContextOrThrow("useAssetsByTag");
1215
+ if (!ctx.assets || typeof ctx.assets.list !== "function") {
1216
+ throw new Error(
1217
+ "useAssetsByTag: host did not inject an assets client (ctx.assets.list)",
1218
+ );
1219
+ }
1220
+ const type = (options && options.type) || "image";
1221
+ const trimmedTag = typeof tag === "string" ? tag.trim() : "";
1222
+ const ready = trimmedTag.length > 0;
1223
+
1224
+ const [assets, setAssets] = useState([]);
1225
+ const [loading, setLoading] = useState(ready);
1226
+ const [error, setError] = useState(null);
1227
+
1228
+ const listRef = useRef(ctx.assets.list);
1229
+ listRef.current = ctx.assets.list;
1230
+ const argsRef = useRef({ tag: trimmedTag, type });
1231
+ argsRef.current = { tag: trimmedTag, type };
1232
+ const runRef = useRef(0);
1233
+
1234
+ const doFetch = useCallback(async () => {
1235
+ const myRun = ++runRef.current;
1236
+ const { tag: t, type: ty } = argsRef.current;
1237
+ if (!t) {
1238
+ setLoading(false);
1239
+ setError(null);
1240
+ setAssets([]);
1241
+ return;
1242
+ }
1243
+ setLoading(true);
1244
+ setError(null);
1245
+ try {
1246
+ const res = await listRef.current({ tag: t, type: ty, limit: 1000 });
1247
+ const rows = res && Array.isArray(res.data) ? res.data : [];
1248
+ if (runRef.current !== myRun) return;
1249
+ setAssets(rows);
1250
+ setLoading(false);
1251
+ } catch (err) {
1252
+ if (runRef.current !== myRun) return;
1253
+ setError(toDatastoreError(err));
1254
+ setLoading(false);
1255
+ }
1256
+ }, []);
1257
+
1258
+ useEffect(() => {
1259
+ doFetch();
1260
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1261
+ }, [trimmedTag, type]);
1262
+
1263
+ const refetch = useCallback(async () => {
1264
+ await doFetch();
1265
+ }, [doFetch]);
1266
+
1267
+ // sc-1179 — auto-subscribe to the page-level refresh tick so a Gallery in
1268
+ // tag mode picks up newly tagged assets when the author refreshes.
1269
+ useRefresh(refetch);
1270
+
1271
+ return { assets, loading, error, refetch };
1272
+ }
1273
+
1200
1274
  /* ============================================================================
1201
1275
  * FILESTORE CLIENT — ctx.filestore (@colixsystems/filestore-client)
1202
1276
  *
package/dist/index.d.ts CHANGED
@@ -764,6 +764,35 @@ export function useAsset(assetId: string | null | undefined): {
764
764
  refetch(): Promise<void>;
765
765
  };
766
766
 
767
+ /**
768
+ * List every tenant asset carrying a given tag. Reads `ctx.assets.list({
769
+ * tag, type, limit })` and unwraps the `{ data, meta }` envelope.
770
+ *
771
+ * `type` defaults to `"image"` so the common Gallery case gets only images
772
+ * back; pass `"all"` (or `"audio"` / `"video"` / `"document"`) to widen.
773
+ * When `tag` is falsy the hook collapses to an empty result without a
774
+ * network round-trip.
775
+ */
776
+ export function useAssetsByTag(
777
+ tag: string | null | undefined,
778
+ options?: {
779
+ type?: "image" | "audio" | "video" | "document" | "all";
780
+ },
781
+ ): {
782
+ assets: Array<{
783
+ id: string;
784
+ url: string;
785
+ stored_filename?: string;
786
+ mime_type?: string;
787
+ size_bytes?: number;
788
+ tags?: string[];
789
+ [k: string]: unknown;
790
+ }>;
791
+ loading: boolean;
792
+ error: DatastoreError | null;
793
+ refetch(): Promise<void>;
794
+ };
795
+
767
796
  export function useI18n(): {
768
797
  locale: string;
769
798
  t(key: string, fallback?: string): string;
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  useDatastoreRecord,
16
16
  useDatastoreSchema,
17
17
  useAsset,
18
+ useAssetsByTag,
18
19
  useFilestoreFiles,
19
20
  useFilestoreFolders,
20
21
  useFileSignature,
@@ -15,6 +15,7 @@ export {
15
15
  useDatastoreRecord,
16
16
  useDatastoreSchema,
17
17
  useAsset,
18
+ useAssetsByTag,
18
19
  useFilestoreFiles,
19
20
  useFilestoreFolders,
20
21
  useFileSignature,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -39,7 +39,7 @@
39
39
  ],
40
40
  "scripts": {
41
41
  "build": "node scripts/build.js",
42
- "test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js"
42
+ "test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=18"