@colixsystems/widget-sdk 0.99.0 → 0.100.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
@@ -99,6 +99,20 @@ Host-integration surface only: no author-facing hook, prop, primitive, or manife
99
99
 
100
100
  Host-integration surface only: no author-facing hook, prop, primitive, or manifest field changed. `CONTRACT.version` → `1.66.0`.
101
101
 
102
+ ### What's new in 0.100.0 (contract 1.74.0)
103
+
104
+ **Opt out of image compression on upload — `useFilestoreUpload({ compress })` (sc-5402).** A file uploaded through `POST /api/v1/filestore/files` now has its raster images compressed to WebP again (EXIF-stripped, longest edge capped at 4096 px), which is the right default for anything the app renders. When the ORIGINAL bytes matter — a document archive, a photo the user re-downloads, anything with an exact-bytes requirement — pass `compress: false`, either on the hook or per call:
105
+
106
+ ```js
107
+ const { upload } = useFilestoreUpload({ spaceType: 'personal', compress: false });
108
+ // …or per upload, which wins over the hook default:
109
+ await upload(file, { compress: false });
110
+ ```
111
+
112
+ Only raster images are ever compressed. SVG keeps its vector, and video, audio, and documents are stored verbatim under both values — an upload is never queued for background transcoding. The opt-out is the only value put on the wire, so the backend stays the single source of the default.
113
+
114
+ `CONTRACT.version` → `1.74.0`. Additive — existing callers are byte-for-byte unchanged on the wire.
115
+
102
116
  ### What's new in 0.98.0 (contract 1.70.0)
103
117
 
104
118
  **Three new hooks close the biggest gaps in the write-gating and query-authoring surface (sc-5206).**
@@ -640,7 +654,7 @@ Also: `useFileSignatures(fileIds)` is now **self-scoped** (the caller's own sign
640
654
  - `useFilestoreFiles({ spaceType, folderId?, q?, type? })` → `{ files, loading, error, refetch }` — browses the end-user's Filestore space. The hook resolves `owner_id` from the host context (tenant for a project space, the app user for a personal space), so the widget only picks the space. Every row carries a ready-to-render `url` (its absolutized `presigned_url`), so a gallery renders `files.map(f => f.url)` directly — no per-row `useFilestoreFile` call.
641
655
  - `useFilestoreFile(fileId)` → `{ file, url, loading, error, refetch }` — resolves ONE file id to a displayable `url` (its `presigned_url`, absolutized for web + native) via `ctx.filestore.files.get(id)`. **Read the top-level `url`** — the returned `file` is `null` until the fetch resolves (and for an empty id), so `file.url` throws on the first render; it is correct only after a null check. **Never compose a file URL yourself** — the bytes are served only from a server-signed token URL, so a hand-built path like `/api/files/<id>` can never resolve (the linter's `no-host-api-url` rule flags it, including the `${location.origin}/api/files/…` form). A datastore `FILE` column holds — and reads back as — that **bare file-id string**; it is NOT hydrated into an object the way a `RELATION` (`{ id, label }`) or `USER` (`{ id, name }`) column is, so pass the value straight to the hook (no `{ id }` / `{ url }` unwrapping guard). Empty / deleted / not-found ids resolve to `url: null` so a display widget shows a fallback. When you render `url` in an `<Image>` that fills its container, size it with `aspectRatio` (e.g. `{ width: "100%", aspectRatio: 1 }`) or pixels — never a percentage `height`, which React Native collapses to 0 against a content-sized parent, so the image loads but is invisible. Requires `files.read:*`.
642
656
  - `useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })` → `{ folders, loading, error, refetch }` — the folder-navigation companion to `useFilestoreFiles`; pass `enabled:false` to suspend fetching.
643
- - `useFilestoreUpload({ spaceType, folderId? })` → `{ upload, uploading, error, lastUploaded }` — POSTs a multipart upload to `ctx.filestore.files.upload`. Resolves `owner_id` from the host context (like the read hooks) so the widget only picks the space + destination folder. Pair with the `<FilePicker>` primitive for the visible trigger. Requires the `files.write:*` scope.
657
+ - `useFilestoreUpload({ spaceType, folderId?, compress? })` → `{ upload, uploading, error, lastUploaded }` — POSTs a multipart upload to `ctx.filestore.files.upload`. Resolves `owner_id` from the host context (like the read hooks) so the widget only picks the space + destination folder. Uploaded images are compressed to WebP by the backend; pass `compress: false` (on the hook, or per `upload(file, { compress })`) to store the file byte-for-byte — use it whenever the original matters. Pair with the `<FilePicker>` primitive for the visible trigger. Requires the `files.write:*` scope.
644
658
  - `useFileSignature(fileId)` → `{ status, qr, signerName, verdict, initiate, refresh, cancel, verify, … }` — drives a BankID signing flow for a file (the backend hashes the bytes server-side, binds the digest into the signature, and verifies the proof offline).
645
659
 
646
660
  `CONTRACT.version` → `1.20.0` (additive — no existing hook changed).
package/dist/contract.cjs CHANGED
@@ -786,16 +786,21 @@ const HOOKS = [
786
786
  },
787
787
  {
788
788
  name: "useFilestoreUpload",
789
- signature: "useFilestoreUpload({ spaceType, folderId? })",
789
+ signature: "useFilestoreUpload({ spaceType, folderId?, compress? })",
790
790
  description:
791
791
  "Upload a file into the end-user's Filestore space. The widget passes " +
792
792
  "the SPACE (`{ spaceType, folderId? }`); the hook resolves owner_id " +
793
793
  "from the host context, builds a multipart FormData with the " +
794
794
  "snake_case fields the backend reads verbatim (`space_type`, " +
795
795
  "`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
796
- "ctx.filestore.files.upload. `upload(file, { folderId? })` resolves " +
797
- "to the created file row or throws the wire error; a 404 means the " +
798
- "destination folder denied a write (REQ-FSH canWrite gate). " +
796
+ "ctx.filestore.files.upload. `upload(file, { folderId?, compress? })` " +
797
+ "resolves to the created file row or throws the wire error; a 404 " +
798
+ "means the destination folder denied a write (REQ-FSH canWrite gate). " +
799
+ "Uploaded images are compressed to WebP by the backend; pass " +
800
+ "`compress: false` (on the hook or per upload) to store the file " +
801
+ "byte-for-byte as the user provided it — use that whenever the " +
802
+ "ORIGINAL matters (a document archive, a photo the user re-downloads, " +
803
+ "anything with an exact-bytes requirement). " +
799
804
  "ALWAYS pass spaceType explicitly — omitting it falls back to " +
800
805
  "'project', which is unreadable by a logged-out visitor. Choose it by " +
801
806
  "who must SEE the file: 'public' for content the app displays to " +
@@ -803,7 +808,7 @@ const HOOKS = [
803
808
  "'project' for content restricted to signed-in workspace users, " +
804
809
  "'personal' for a file private to the uploading app user.",
805
810
  returnShape: {
806
- upload: "(file, { folderId? }) => Promise<FilestoreFile>",
811
+ upload: "(file, { folderId?, compress? }) => Promise<FilestoreFile>",
807
812
  uploading: "boolean",
808
813
  error: "Error | null",
809
814
  lastUploaded: "FilestoreFile | null",
@@ -3199,7 +3204,12 @@ const CONTRACT = deepFreeze({
3199
3204
  // `ctx.datastore.myPermissions` client method as a write-permission
3200
3205
  // FLOOR, replacing the previous prompt-only "read useUser().groupIds /
3201
3206
  // .roles" guidance. No existing export changed signature.
3202
- version: "1.73.0",
3207
+ // 1.74.0: additive (sc-5402) — `useFilestoreUpload` accepts `compress`, on
3208
+ // the hook options and per `upload(file, { compress })`. Uploaded images
3209
+ // are compressed to WebP by default; `compress: false` stores the file
3210
+ // byte-for-byte. Existing callers are unaffected — the field is only sent
3211
+ // when the opt-out is chosen.
3212
+ version: "1.74.0",
3203
3213
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3204
3214
  hooks: HOOKS,
3205
3215
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -786,16 +786,21 @@ const HOOKS = [
786
786
  },
787
787
  {
788
788
  name: "useFilestoreUpload",
789
- signature: "useFilestoreUpload({ spaceType, folderId? })",
789
+ signature: "useFilestoreUpload({ spaceType, folderId?, compress? })",
790
790
  description:
791
791
  "Upload a file into the end-user's Filestore space. The widget passes " +
792
792
  "the SPACE (`{ spaceType, folderId? }`); the hook resolves owner_id " +
793
793
  "from the host context, builds a multipart FormData with the " +
794
794
  "snake_case fields the backend reads verbatim (`space_type`, " +
795
795
  "`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
796
- "ctx.filestore.files.upload. `upload(file, { folderId? })` resolves " +
797
- "to the created file row or throws the wire error; a 404 means the " +
798
- "destination folder denied a write (REQ-FSH canWrite gate). " +
796
+ "ctx.filestore.files.upload. `upload(file, { folderId?, compress? })` " +
797
+ "resolves to the created file row or throws the wire error; a 404 " +
798
+ "means the destination folder denied a write (REQ-FSH canWrite gate). " +
799
+ "Uploaded images are compressed to WebP by the backend; pass " +
800
+ "`compress: false` (on the hook or per upload) to store the file " +
801
+ "byte-for-byte as the user provided it — use that whenever the " +
802
+ "ORIGINAL matters (a document archive, a photo the user re-downloads, " +
803
+ "anything with an exact-bytes requirement). " +
799
804
  "ALWAYS pass spaceType explicitly — omitting it falls back to " +
800
805
  "'project', which is unreadable by a logged-out visitor. Choose it by " +
801
806
  "who must SEE the file: 'public' for content the app displays to " +
@@ -803,7 +808,7 @@ const HOOKS = [
803
808
  "'project' for content restricted to signed-in workspace users, " +
804
809
  "'personal' for a file private to the uploading app user.",
805
810
  returnShape: {
806
- upload: "(file, { folderId? }) => Promise<FilestoreFile>",
811
+ upload: "(file, { folderId?, compress? }) => Promise<FilestoreFile>",
807
812
  uploading: "boolean",
808
813
  error: "Error | null",
809
814
  lastUploaded: "FilestoreFile | null",
@@ -3199,7 +3204,12 @@ const CONTRACT = deepFreeze({
3199
3204
  // `ctx.datastore.myPermissions` client method as a write-permission
3200
3205
  // FLOOR, replacing the previous prompt-only "read useUser().groupIds /
3201
3206
  // .roles" guidance. No existing export changed signature.
3202
- version: "1.73.0",
3207
+ // 1.74.0: additive (sc-5402) — `useFilestoreUpload` accepts `compress`, on
3208
+ // the hook options and per `upload(file, { compress })`. Uploaded images
3209
+ // are compressed to WebP by default; `compress: false` stores the file
3210
+ // byte-for-byte. Existing callers are unaffected — the field is only sent
3211
+ // when the opt-out is chosen.
3212
+ version: "1.74.0",
3203
3213
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3204
3214
  hooks: HOOKS,
3205
3215
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -2801,7 +2801,11 @@ export function useFilestoreUpload(options) {
2801
2801
  "useFilestoreUpload: host did not inject a filestore client (ctx.filestore.files.upload)",
2802
2802
  );
2803
2803
  }
2804
- const { spaceType = "project", folderId: defaultFolderId = null } = options || {};
2804
+ const {
2805
+ spaceType = "project",
2806
+ folderId: defaultFolderId = null,
2807
+ compress: defaultCompress = true,
2808
+ } = options || {};
2805
2809
  const ownerId = _filestoreOwnerId(ctx, spaceType);
2806
2810
 
2807
2811
  const [uploading, setUploading] = useState(false);
@@ -2823,10 +2827,17 @@ export function useFilestoreUpload(options) {
2823
2827
  overrides && Object.prototype.hasOwnProperty.call(overrides, "folderId")
2824
2828
  ? overrides.folderId
2825
2829
  : defaultFolderId;
2830
+ const compress =
2831
+ overrides && Object.prototype.hasOwnProperty.call(overrides, "compress")
2832
+ ? overrides.compress
2833
+ : defaultCompress;
2826
2834
  const form = new FormData();
2827
2835
  form.append("space_type", String(spaceType || "project").toUpperCase());
2828
2836
  form.append("owner_id", ownerId);
2829
2837
  if (folderId) form.append("folder_id", folderId);
2838
+ // sc-5402: only the opt-out is sent, so the backend stays the single
2839
+ // source of the default (it compresses images to WebP).
2840
+ if (compress === false) form.append("compress", "false");
2830
2841
  form.append("file", file);
2831
2842
  setUploading(true);
2832
2843
  setError(null);
@@ -2841,7 +2852,7 @@ export function useFilestoreUpload(options) {
2841
2852
  throw err;
2842
2853
  }
2843
2854
  },
2844
- [ownerId, defaultFolderId, spaceType],
2855
+ [ownerId, defaultFolderId, defaultCompress, spaceType],
2845
2856
  );
2846
2857
 
2847
2858
  return { upload, uploading, error, lastUploaded };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.99.0",
3
+ "version": "0.100.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",