@colixsystems/widget-sdk 0.45.0 → 0.47.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 +19 -1
- package/dist/contract.cjs +64 -1
- package/dist/contract.js +64 -1
- package/dist/filepicker.js +93 -0
- package/dist/filepicker.native.js +100 -0
- package/dist/hooks.js +76 -0
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/linter.cjs +40 -3
- package/dist/linter.js +45 -3
- package/dist/primitives.js +6 -0
- package/dist/primitives.native.js +5 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -49,7 +49,24 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
49
49
|
|
|
50
50
|
## Status
|
|
51
51
|
|
|
52
|
-
`v0.
|
|
52
|
+
`v0.47.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.47.0
|
|
55
|
+
|
|
56
|
+
**`<FilePicker>` native variant is real (sc-1378 follow-up).** The Expo-export shell of `<FilePicker>` now wraps the vetted `expo-document-picker` and reports `FilePicker.isSupported = true`. Result: the Files widget's `allowUpload` toggle works on **both** the web Player and the native Expo export — no more disabled stub on mobile. `expo-document-picker` is added to `CONTRACT.vettedImports` (`platforms: ["native"]`); the compiler's `generatePackageJson` was already pinning it for the export build, so no new export-pin work was needed. `useFilestoreUpload` accepts whatever the host's FormData reads as a binary part — a browser `File` on web, the React Native `{ uri, name, type }` shape from the picker on native — so the same hook code path feeds the multipart on both platforms. `CONTRACT.version` → `1.34.0`, additive.
|
|
57
|
+
|
|
58
|
+
### What's new in 0.46.0
|
|
59
|
+
|
|
60
|
+
**End-user upload primitive + hook (sc-1378).** Widgets can now let an end user upload a file to the Filestore from the published app. Two pieces ship together:
|
|
61
|
+
|
|
62
|
+
- **New SDK hook `useFilestoreUpload({ spaceType, folderId? })`.** Resolves `owner_id` the same way the read filestore hooks do, builds a multipart `FormData` with the snake_case fields the backend reads verbatim (`space_type`, `owner_id`, `folder_id`, plus the binary `file`), and POSTs through `ctx.filestore.files.upload`. Returns `{ upload(file, { folderId? }), uploading, error, lastUploaded }`. A 404 from the backend means the destination folder denied a write (REQ-FSH `canWrite` gate).
|
|
63
|
+
- **New SDK primitive `<FilePicker accept onPick>`.** Web wraps a hidden DOM `<input type="file">` (children render as the visible click target inside a `<label>` so the click reaches the file dialog without ref plumbing). Native renders a disabled trigger and exposes `FilePicker.isSupported = false` until a vetted Expo picker pin lands.
|
|
64
|
+
- **Files widget `allowUpload`.** The built-in Files widget grew an `Allow uploading files` toggle (manifest v2.2.0). When enabled, the toolbar renders an Upload trigger that pipes the picked file through the new hook into the currently-open folder; the `typeFilter` setting narrows the `accept` MIME so an "Images" filter shows only images in the OS dialog.
|
|
65
|
+
- **`CONTRACT.version` → `1.33.0`.** Additive — new hook + new primitive; no existing hook, primitive, manifest field, or token changed shape.
|
|
66
|
+
|
|
67
|
+
### What's new in 0.45.1
|
|
68
|
+
|
|
69
|
+
**Fix `lucide-unknown-icon` false positive across adjacent imports (sc-1373).** The rule's import regex matched the brace block lazily (`[\s\S]*?`), so when another braced import preceded the lucide one — e.g. `import { View, Text, Pressable } from "react-native"` then `import { Sparkles } from "lucide-react-native"` — the capture spanned both and validated the `react-native` names (`Pressable`, …) as lucide icons, blocking a near-universal widget pattern. The capture is now `[^}]*`, which cannot cross a `}` into a neighbouring import. Fix-only; the rule's intent and the committed name set are unchanged.
|
|
53
70
|
|
|
54
71
|
### What's new in 0.45.0
|
|
55
72
|
|
|
@@ -147,6 +164,7 @@ Also: `useFileSignatures(fileIds)` is now **self-scoped** (the caller's own sign
|
|
|
147
164
|
**Filestore browsing + BankID file signing for widgets (REQ-FS / REQ-SIGN).** Three new hooks read a newly-injected `ctx.filestore` (the `@colixsystems/filestore-client`, now constructed by both the web and native hosts):
|
|
148
165
|
- `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.
|
|
149
166
|
- `useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })` → `{ folders, loading, error, refetch }` — the folder-navigation companion to `useFilestoreFiles`; pass `enabled:false` to suspend fetching.
|
|
167
|
+
- `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.
|
|
150
168
|
- `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).
|
|
151
169
|
|
|
152
170
|
`CONTRACT.version` → `1.20.0` (additive — no existing hook changed).
|
package/dist/contract.cjs
CHANGED
|
@@ -214,6 +214,27 @@ const HOOKS = [
|
|
|
214
214
|
requiredContextSlice: ["filestore.files"],
|
|
215
215
|
scopes: ["files.read:*"],
|
|
216
216
|
},
|
|
217
|
+
{
|
|
218
|
+
name: "useFilestoreUpload",
|
|
219
|
+
signature: "useFilestoreUpload({ spaceType, folderId? })",
|
|
220
|
+
description:
|
|
221
|
+
"Upload a file into the end-user's Filestore space. The widget passes " +
|
|
222
|
+
"the SPACE (`{ spaceType, folderId? }`); the hook resolves owner_id " +
|
|
223
|
+
"from the host context, builds a multipart FormData with the " +
|
|
224
|
+
"snake_case fields the backend reads verbatim (`space_type`, " +
|
|
225
|
+
"`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
|
|
226
|
+
"ctx.filestore.files.upload. `upload(file, { folderId? })` resolves " +
|
|
227
|
+
"to the created file row or throws the wire error; a 404 means the " +
|
|
228
|
+
"destination folder denied a write (REQ-FSH canWrite gate).",
|
|
229
|
+
returnShape: {
|
|
230
|
+
upload: "(file, { folderId? }) => Promise<FilestoreFile>",
|
|
231
|
+
uploading: "boolean",
|
|
232
|
+
error: "Error | null",
|
|
233
|
+
lastUploaded: "FilestoreFile | null",
|
|
234
|
+
},
|
|
235
|
+
requiredContextSlice: ["filestore.files"],
|
|
236
|
+
scopes: ["files.write:*"],
|
|
237
|
+
},
|
|
217
238
|
{
|
|
218
239
|
name: "useFilestoreFolders",
|
|
219
240
|
signature: "useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })",
|
|
@@ -710,6 +731,19 @@ const PRIMITIVES = [
|
|
|
710
731
|
rnComponent: "@react-native-community/datetimepicker",
|
|
711
732
|
docsUrl: "https://github.com/react-native-datetimepicker/datetimepicker",
|
|
712
733
|
},
|
|
734
|
+
// sc-1378 — `<FilePicker accept onPick>` lets a widget surface a native
|
|
735
|
+
// file-picker on the web Player so end users can upload files to the
|
|
736
|
+
// backend (paired with useFilestoreUpload or useAssets.upload). Web wraps
|
|
737
|
+
// a hidden DOM `<input type="file">`; native renders a disabled trigger
|
|
738
|
+
// and exposes `FilePicker.isSupported = false` until a vetted Expo
|
|
739
|
+
// picker pin lands.
|
|
740
|
+
{
|
|
741
|
+
name: "FilePicker",
|
|
742
|
+
description:
|
|
743
|
+
'End-user file picker primitive. `<FilePicker accept="image/*" multiple={false} disabled={false} onPick={(file) => …}>…trigger…</FilePicker>`. Children render as the visible click target (a Pressable + Icon + Text is the common shape) and are wrapped in a hidden-input <label> so the click reaches the file dialog without any ref plumbing. Pair with useFilestoreUpload to upload the picked File to the end-user filestore. `FilePicker.isSupported` is `true` on the web Player and `false` on the native Expo export — widgets should branch on it to hide the trigger when uploads are not yet wired natively.',
|
|
744
|
+
rnComponent: null,
|
|
745
|
+
docsUrl: null,
|
|
746
|
+
},
|
|
713
747
|
];
|
|
714
748
|
|
|
715
749
|
const CATEGORIES = [
|
|
@@ -1169,6 +1203,13 @@ const VETTED_IMPORTS = [
|
|
|
1169
1203
|
description:
|
|
1170
1204
|
"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
1205
|
},
|
|
1206
|
+
{
|
|
1207
|
+
specifier: "expo-document-picker",
|
|
1208
|
+
platforms: ["native"],
|
|
1209
|
+
category: "files",
|
|
1210
|
+
description:
|
|
1211
|
+
"Native file picker used by the SDK's <FilePicker> primitive on the Expo export (web uses a hidden DOM <input type=\"file\">, mapped to the same { accept, onPick } contract). Most widgets reach <FilePicker> through the SDK rather than importing this directly; a widget that does import it goes in widget.native.jsx and is pinned by the compiler's generatePackageJson — Expo SDK 56 ships 56.0.x.",
|
|
1212
|
+
},
|
|
1172
1213
|
{
|
|
1173
1214
|
specifier: "lucide-react-native",
|
|
1174
1215
|
platforms: ["web", "native"],
|
|
@@ -1681,7 +1722,29 @@ const CONTRACT = deepFreeze({
|
|
|
1681
1722
|
// instance — one StyleSheet/context, no double-instance conflicts. The
|
|
1682
1723
|
// vetted-import SET is unchanged (react-native was already vetted); only
|
|
1683
1724
|
// its web resolution + description changed — minor bump.
|
|
1684
|
-
|
|
1725
|
+
//
|
|
1726
|
+
// 1.33.0: additive (sc-1378) — `useFilestoreUpload({ spaceType, folderId? })`
|
|
1727
|
+
// posts a multipart upload to `ctx.filestore.files.upload` after the
|
|
1728
|
+
// same owner_id resolution the read filestore hooks use (tenant for
|
|
1729
|
+
// PROJECT, app user for PERSONAL). Paired with the new `<FilePicker>`
|
|
1730
|
+
// primitive — web wraps a hidden DOM `<input type="file">`, native
|
|
1731
|
+
// renders a disabled trigger and exposes `FilePicker.isSupported =
|
|
1732
|
+
// false` until a vetted Expo picker pin lands. Backs the Files widget's
|
|
1733
|
+
// new `allowUpload` toggle. No existing hook, primitive, manifest
|
|
1734
|
+
// field, or token changed shape — minor bump.
|
|
1735
|
+
//
|
|
1736
|
+
// 1.34.0: additive (sc-1378 follow-up) — `<FilePicker>` native variant
|
|
1737
|
+
// now wraps the vetted `expo-document-picker` and reports
|
|
1738
|
+
// `FilePicker.isSupported = true` on the Expo export (the build was
|
|
1739
|
+
// already pinning the package in `generatePackageJson`). Closes the
|
|
1740
|
+
// REQ-FW-03 native gap; the Files widget's Upload button now works on
|
|
1741
|
+
// both the web Player and the Expo export. New vetted entry
|
|
1742
|
+
// `expo-document-picker` (`platforms: ["native"]`). `useFilestoreUpload`
|
|
1743
|
+
// accepts both a browser `File` and the React Native `{ uri, name,
|
|
1744
|
+
// type }` shape the native picker returns, so the same hook code path
|
|
1745
|
+
// feeds the multipart in both hosts. No existing hook, primitive, or
|
|
1746
|
+
// manifest field changed shape — minor bump.
|
|
1747
|
+
version: "1.34.0",
|
|
1685
1748
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1686
1749
|
hooks: HOOKS,
|
|
1687
1750
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -214,6 +214,27 @@ const HOOKS = [
|
|
|
214
214
|
requiredContextSlice: ["filestore.files"],
|
|
215
215
|
scopes: ["files.read:*"],
|
|
216
216
|
},
|
|
217
|
+
{
|
|
218
|
+
name: "useFilestoreUpload",
|
|
219
|
+
signature: "useFilestoreUpload({ spaceType, folderId? })",
|
|
220
|
+
description:
|
|
221
|
+
"Upload a file into the end-user's Filestore space. The widget passes " +
|
|
222
|
+
"the SPACE (`{ spaceType, folderId? }`); the hook resolves owner_id " +
|
|
223
|
+
"from the host context, builds a multipart FormData with the " +
|
|
224
|
+
"snake_case fields the backend reads verbatim (`space_type`, " +
|
|
225
|
+
"`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
|
|
226
|
+
"ctx.filestore.files.upload. `upload(file, { folderId? })` resolves " +
|
|
227
|
+
"to the created file row or throws the wire error; a 404 means the " +
|
|
228
|
+
"destination folder denied a write (REQ-FSH canWrite gate).",
|
|
229
|
+
returnShape: {
|
|
230
|
+
upload: "(file, { folderId? }) => Promise<FilestoreFile>",
|
|
231
|
+
uploading: "boolean",
|
|
232
|
+
error: "Error | null",
|
|
233
|
+
lastUploaded: "FilestoreFile | null",
|
|
234
|
+
},
|
|
235
|
+
requiredContextSlice: ["filestore.files"],
|
|
236
|
+
scopes: ["files.write:*"],
|
|
237
|
+
},
|
|
217
238
|
{
|
|
218
239
|
name: "useFilestoreFolders",
|
|
219
240
|
signature: "useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })",
|
|
@@ -710,6 +731,19 @@ const PRIMITIVES = [
|
|
|
710
731
|
rnComponent: "@react-native-community/datetimepicker",
|
|
711
732
|
docsUrl: "https://github.com/react-native-datetimepicker/datetimepicker",
|
|
712
733
|
},
|
|
734
|
+
// sc-1378 — `<FilePicker accept onPick>` lets a widget surface a native
|
|
735
|
+
// file-picker on the web Player so end users can upload files to the
|
|
736
|
+
// backend (paired with useFilestoreUpload or useAssets.upload). Web wraps
|
|
737
|
+
// a hidden DOM `<input type="file">`; native renders a disabled trigger
|
|
738
|
+
// and exposes `FilePicker.isSupported = false` until a vetted Expo
|
|
739
|
+
// picker pin lands.
|
|
740
|
+
{
|
|
741
|
+
name: "FilePicker",
|
|
742
|
+
description:
|
|
743
|
+
'End-user file picker primitive. `<FilePicker accept="image/*" multiple={false} disabled={false} onPick={(file) => …}>…trigger…</FilePicker>`. Children render as the visible click target (a Pressable + Icon + Text is the common shape) and are wrapped in a hidden-input <label> so the click reaches the file dialog without any ref plumbing. Pair with useFilestoreUpload to upload the picked File to the end-user filestore. `FilePicker.isSupported` is `true` on the web Player and `false` on the native Expo export — widgets should branch on it to hide the trigger when uploads are not yet wired natively.',
|
|
744
|
+
rnComponent: null,
|
|
745
|
+
docsUrl: null,
|
|
746
|
+
},
|
|
713
747
|
];
|
|
714
748
|
|
|
715
749
|
const CATEGORIES = [
|
|
@@ -1169,6 +1203,13 @@ const VETTED_IMPORTS = [
|
|
|
1169
1203
|
description:
|
|
1170
1204
|
"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
1205
|
},
|
|
1206
|
+
{
|
|
1207
|
+
specifier: "expo-document-picker",
|
|
1208
|
+
platforms: ["native"],
|
|
1209
|
+
category: "files",
|
|
1210
|
+
description:
|
|
1211
|
+
"Native file picker used by the SDK's <FilePicker> primitive on the Expo export (web uses a hidden DOM <input type=\"file\">, mapped to the same { accept, onPick } contract). Most widgets reach <FilePicker> through the SDK rather than importing this directly; a widget that does import it goes in widget.native.jsx and is pinned by the compiler's generatePackageJson — Expo SDK 56 ships 56.0.x.",
|
|
1212
|
+
},
|
|
1172
1213
|
{
|
|
1173
1214
|
specifier: "lucide-react-native",
|
|
1174
1215
|
platforms: ["web", "native"],
|
|
@@ -1681,7 +1722,29 @@ const CONTRACT = deepFreeze({
|
|
|
1681
1722
|
// instance — one StyleSheet/context, no double-instance conflicts. The
|
|
1682
1723
|
// vetted-import SET is unchanged (react-native was already vetted); only
|
|
1683
1724
|
// its web resolution + description changed — minor bump.
|
|
1684
|
-
|
|
1725
|
+
//
|
|
1726
|
+
// 1.33.0: additive (sc-1378) — `useFilestoreUpload({ spaceType, folderId? })`
|
|
1727
|
+
// posts a multipart upload to `ctx.filestore.files.upload` after the
|
|
1728
|
+
// same owner_id resolution the read filestore hooks use (tenant for
|
|
1729
|
+
// PROJECT, app user for PERSONAL). Paired with the new `<FilePicker>`
|
|
1730
|
+
// primitive — web wraps a hidden DOM `<input type="file">`, native
|
|
1731
|
+
// renders a disabled trigger and exposes `FilePicker.isSupported =
|
|
1732
|
+
// false` until a vetted Expo picker pin lands. Backs the Files widget's
|
|
1733
|
+
// new `allowUpload` toggle. No existing hook, primitive, manifest
|
|
1734
|
+
// field, or token changed shape — minor bump.
|
|
1735
|
+
//
|
|
1736
|
+
// 1.34.0: additive (sc-1378 follow-up) — `<FilePicker>` native variant
|
|
1737
|
+
// now wraps the vetted `expo-document-picker` and reports
|
|
1738
|
+
// `FilePicker.isSupported = true` on the Expo export (the build was
|
|
1739
|
+
// already pinning the package in `generatePackageJson`). Closes the
|
|
1740
|
+
// REQ-FW-03 native gap; the Files widget's Upload button now works on
|
|
1741
|
+
// both the web Player and the Expo export. New vetted entry
|
|
1742
|
+
// `expo-document-picker` (`platforms: ["native"]`). `useFilestoreUpload`
|
|
1743
|
+
// accepts both a browser `File` and the React Native `{ uri, name,
|
|
1744
|
+
// type }` shape the native picker returns, so the same hook code path
|
|
1745
|
+
// feeds the multipart in both hosts. No existing hook, primitive, or
|
|
1746
|
+
// manifest field changed shape — minor bump.
|
|
1747
|
+
version: "1.34.0",
|
|
1685
1748
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1686
1749
|
hooks: HOOKS,
|
|
1687
1750
|
primitives: PRIMITIVES,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// sc-1378 — `<FilePicker>` SDK primitive (web implementation).
|
|
2
|
+
//
|
|
3
|
+
// React Native (and react-native-web) has no native equivalent of the
|
|
4
|
+
// browser's `<input type="file">`, so the web build renders the DOM input
|
|
5
|
+
// directly (the same trick the web DateTimePicker uses: `React.createElement
|
|
6
|
+
// ("input", ...)`). The input is visually hidden so authors style the
|
|
7
|
+
// trigger themselves — children are rendered inside a label so any
|
|
8
|
+
// Pressable / View / Text the widget passes in becomes the click target.
|
|
9
|
+
//
|
|
10
|
+
// Public contract (mirror in filepicker.native.js — keep them in lockstep):
|
|
11
|
+
// accept: string | undefined — same value as the DOM `accept`
|
|
12
|
+
// attribute. "image/*" / "image/png,image/jpeg" narrow the
|
|
13
|
+
// picker; omit / "" lets the OS picker show every file type.
|
|
14
|
+
// multiple: boolean — default false. When true, `onPick`
|
|
15
|
+
// fires once with an array of File objects.
|
|
16
|
+
// disabled: boolean — gates the click; renders a muted
|
|
17
|
+
// label.
|
|
18
|
+
// onPick: (file | File[]) => void
|
|
19
|
+
// children: ReactNode — the visible trigger (a Pressable +
|
|
20
|
+
// an icon + a label is the common shape). Wrapped in a
|
|
21
|
+
// `<label>` so the click reaches the hidden input
|
|
22
|
+
// everywhere — no manual ref / `.click()` plumbing needed.
|
|
23
|
+
// accessibilityLabel: string? — written to the wrapper label.
|
|
24
|
+
|
|
25
|
+
import React, { useRef, useCallback } from "react";
|
|
26
|
+
|
|
27
|
+
const HIDDEN_INPUT_STYLE = {
|
|
28
|
+
position: "absolute",
|
|
29
|
+
width: 1,
|
|
30
|
+
height: 1,
|
|
31
|
+
padding: 0,
|
|
32
|
+
margin: -1,
|
|
33
|
+
overflow: "hidden",
|
|
34
|
+
clip: "rect(0, 0, 0, 0)",
|
|
35
|
+
whiteSpace: "nowrap",
|
|
36
|
+
border: 0,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const LABEL_STYLE = {
|
|
40
|
+
display: "inline-flex",
|
|
41
|
+
cursor: "pointer",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const LABEL_STYLE_DISABLED = {
|
|
45
|
+
...LABEL_STYLE,
|
|
46
|
+
cursor: "not-allowed",
|
|
47
|
+
opacity: 0.5,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function FilePicker({
|
|
51
|
+
accept,
|
|
52
|
+
multiple = false,
|
|
53
|
+
disabled = false,
|
|
54
|
+
onPick,
|
|
55
|
+
children,
|
|
56
|
+
accessibilityLabel,
|
|
57
|
+
}) {
|
|
58
|
+
const inputRef = useRef(null);
|
|
59
|
+
|
|
60
|
+
const handleChange = useCallback(
|
|
61
|
+
(event) => {
|
|
62
|
+
const list = event && event.target && event.target.files;
|
|
63
|
+
if (!list || list.length === 0) return;
|
|
64
|
+
if (typeof onPick === "function") {
|
|
65
|
+
onPick(multiple ? Array.from(list) : list[0]);
|
|
66
|
+
}
|
|
67
|
+
// Reset the input so picking the SAME file twice in a row still fires
|
|
68
|
+
// onChange — DOM dedupes by value, the SDK contract does not.
|
|
69
|
+
event.target.value = "";
|
|
70
|
+
},
|
|
71
|
+
[onPick, multiple],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return React.createElement(
|
|
75
|
+
"label",
|
|
76
|
+
{
|
|
77
|
+
style: disabled ? LABEL_STYLE_DISABLED : LABEL_STYLE,
|
|
78
|
+
"aria-label": accessibilityLabel,
|
|
79
|
+
},
|
|
80
|
+
React.createElement("input", {
|
|
81
|
+
ref: inputRef,
|
|
82
|
+
type: "file",
|
|
83
|
+
accept: accept || undefined,
|
|
84
|
+
multiple,
|
|
85
|
+
disabled,
|
|
86
|
+
onChange: handleChange,
|
|
87
|
+
style: HIDDEN_INPUT_STYLE,
|
|
88
|
+
}),
|
|
89
|
+
children,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
FilePicker.isSupported = true;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// sc-1378 — `<FilePicker>` SDK primitive (native — Expo export).
|
|
2
|
+
//
|
|
3
|
+
// Wraps `expo-document-picker` (a vetted import, native-only; pinned by
|
|
4
|
+
// the compiler's generatePackageJson at expo-document-picker ~56.0.x).
|
|
5
|
+
// The web variant in ./filepicker.js wraps a hidden DOM
|
|
6
|
+
// `<input type="file">` because react-native-web has no equivalent of the
|
|
7
|
+
// system file picker; this native variant calls the OS document picker
|
|
8
|
+
// directly. Both honour the same public contract — `accept` / `multiple`
|
|
9
|
+
// / `disabled` / `onPick` / `children` / `accessibilityLabel`.
|
|
10
|
+
//
|
|
11
|
+
// `onPick` receives the same shape on both platforms:
|
|
12
|
+
// - web → the browser's `File` object (a `Blob` with name + type).
|
|
13
|
+
// - native → `{ uri, name, type, size }` — React Native's
|
|
14
|
+
// FormData-friendly file part. `useFilestoreUpload` appends whichever
|
|
15
|
+
// verbatim into the multipart body; the React Native fetch
|
|
16
|
+
// implementation reads `uri` and streams the bytes.
|
|
17
|
+
|
|
18
|
+
import React, { useCallback, useMemo } from "react";
|
|
19
|
+
import { Pressable, View } from "react-native";
|
|
20
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
21
|
+
import { getDocumentAsync } from "expo-document-picker";
|
|
22
|
+
|
|
23
|
+
// Map an HTML `accept` string (the web contract — "image/*", "audio/*",
|
|
24
|
+
// "image/png,image/jpeg", …) to expo-document-picker's `type` arg, which
|
|
25
|
+
// accepts either a single MIME string or an array of MIME strings. We
|
|
26
|
+
// pass the array form unchanged so a comma-separated list narrows the
|
|
27
|
+
// same way it does on web. An empty / falsy accept maps to "*/*"
|
|
28
|
+
// (DocumentPicker's wide-open default — every file type is offered).
|
|
29
|
+
function _mapAccept(accept) {
|
|
30
|
+
if (!accept || typeof accept !== "string") return "*/*";
|
|
31
|
+
const parts = accept
|
|
32
|
+
.split(",")
|
|
33
|
+
.map((s) => s.trim())
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
if (parts.length === 0) return "*/*";
|
|
36
|
+
if (parts.length === 1) return parts[0];
|
|
37
|
+
return parts;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// expo-document-picker returns `{ canceled, assets: [{ uri, name,
|
|
41
|
+
// mimeType, size }] }`. Normalise each asset to the FormData-friendly
|
|
42
|
+
// `{ uri, name, type, size }` shape useFilestoreUpload appends into the
|
|
43
|
+
// multipart body — `type` is the field React Native's FormData reads
|
|
44
|
+
// for the part's content-type, mirroring the browser File's `.type`.
|
|
45
|
+
function _asset(picked) {
|
|
46
|
+
return {
|
|
47
|
+
uri: picked.uri,
|
|
48
|
+
name: picked.name || "upload",
|
|
49
|
+
type: picked.mimeType || "application/octet-stream",
|
|
50
|
+
size: typeof picked.size === "number" ? picked.size : undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function FilePicker({
|
|
55
|
+
accept,
|
|
56
|
+
multiple = false,
|
|
57
|
+
disabled = false,
|
|
58
|
+
onPick,
|
|
59
|
+
children,
|
|
60
|
+
accessibilityLabel,
|
|
61
|
+
}) {
|
|
62
|
+
const type = useMemo(() => _mapAccept(accept), [accept]);
|
|
63
|
+
|
|
64
|
+
const handlePress = useCallback(async () => {
|
|
65
|
+
if (disabled) return;
|
|
66
|
+
let result;
|
|
67
|
+
try {
|
|
68
|
+
result = await getDocumentAsync({
|
|
69
|
+
type,
|
|
70
|
+
multiple,
|
|
71
|
+
copyToCacheDirectory: true,
|
|
72
|
+
});
|
|
73
|
+
} catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!result || result.canceled) return;
|
|
77
|
+
const assets = Array.isArray(result.assets) ? result.assets : [];
|
|
78
|
+
if (assets.length === 0) return;
|
|
79
|
+
if (typeof onPick !== "function") return;
|
|
80
|
+
if (multiple) {
|
|
81
|
+
onPick(assets.map(_asset));
|
|
82
|
+
} else {
|
|
83
|
+
onPick(_asset(assets[0]));
|
|
84
|
+
}
|
|
85
|
+
}, [disabled, type, multiple, onPick]);
|
|
86
|
+
|
|
87
|
+
return React.createElement(
|
|
88
|
+
Pressable,
|
|
89
|
+
{
|
|
90
|
+
onPress: handlePress,
|
|
91
|
+
disabled,
|
|
92
|
+
accessibilityRole: "button",
|
|
93
|
+
accessibilityLabel,
|
|
94
|
+
accessibilityState: { disabled },
|
|
95
|
+
},
|
|
96
|
+
React.createElement(View, { style: disabled ? { opacity: 0.5 } : undefined }, children),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
FilePicker.isSupported = true;
|
package/dist/hooks.js
CHANGED
|
@@ -1364,6 +1364,82 @@ export function useFilestoreFiles(options) {
|
|
|
1364
1364
|
return { files, loading, error, refetch };
|
|
1365
1365
|
}
|
|
1366
1366
|
|
|
1367
|
+
/**
|
|
1368
|
+
* sc-1378 — upload a file into the end-user's Filestore space. Returns
|
|
1369
|
+
* `{ upload, uploading, error, lastUploaded }`. The widget passes the
|
|
1370
|
+
* SPACE (`{ spaceType, folderId? }`); the hook resolves `owner_id` the
|
|
1371
|
+
* same way the read hooks do (tenant for PROJECT, app user for PERSONAL)
|
|
1372
|
+
* and posts a multipart FormData to `ctx.filestore.files.upload`.
|
|
1373
|
+
*
|
|
1374
|
+
* `upload(file, { folderId? })` accepts whatever the host's FormData
|
|
1375
|
+
* implementation reads as a binary file part — on the web Player a
|
|
1376
|
+
* browser `File`, on the Expo export the React Native shape
|
|
1377
|
+
* `{ uri, name, type }` returned by `<FilePicker>` (which wraps
|
|
1378
|
+
* `expo-document-picker` natively and a hidden DOM `<input type="file">`
|
|
1379
|
+
* on web). FormData serialises both verbatim, so the same hook code path
|
|
1380
|
+
* feeds the multipart on both platforms.
|
|
1381
|
+
*
|
|
1382
|
+
* A 404 from the backend means the destination folder denied a write —
|
|
1383
|
+
* the widget should surface a friendly "you can't upload here" message
|
|
1384
|
+
* rather than the raw error.
|
|
1385
|
+
*/
|
|
1386
|
+
export function useFilestoreUpload(options) {
|
|
1387
|
+
const ctx = useWidgetContextOrThrow("useFilestoreUpload");
|
|
1388
|
+
if (
|
|
1389
|
+
!ctx.filestore ||
|
|
1390
|
+
!ctx.filestore.files ||
|
|
1391
|
+
typeof ctx.filestore.files.upload !== "function"
|
|
1392
|
+
) {
|
|
1393
|
+
throw new Error(
|
|
1394
|
+
"useFilestoreUpload: host did not inject a filestore client (ctx.filestore.files.upload)",
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
const { spaceType = "project", folderId: defaultFolderId = null } = options || {};
|
|
1398
|
+
const ownerId = _filestoreOwnerId(ctx, spaceType);
|
|
1399
|
+
|
|
1400
|
+
const [uploading, setUploading] = useState(false);
|
|
1401
|
+
const [error, setError] = useState(null);
|
|
1402
|
+
const [lastUploaded, setLastUploaded] = useState(null);
|
|
1403
|
+
|
|
1404
|
+
const filesRef = useRef(ctx.filestore.files);
|
|
1405
|
+
filesRef.current = ctx.filestore.files;
|
|
1406
|
+
|
|
1407
|
+
const upload = useCallback(
|
|
1408
|
+
async (file, overrides) => {
|
|
1409
|
+
if (!file) throw new Error("useFilestoreUpload: file is required");
|
|
1410
|
+
if (!ownerId) {
|
|
1411
|
+
const err = new Error("Sign in to upload");
|
|
1412
|
+
setError(err);
|
|
1413
|
+
throw err;
|
|
1414
|
+
}
|
|
1415
|
+
const folderId =
|
|
1416
|
+
overrides && Object.prototype.hasOwnProperty.call(overrides, "folderId")
|
|
1417
|
+
? overrides.folderId
|
|
1418
|
+
: defaultFolderId;
|
|
1419
|
+
const form = new FormData();
|
|
1420
|
+
form.append("space_type", String(spaceType || "project").toUpperCase());
|
|
1421
|
+
form.append("owner_id", ownerId);
|
|
1422
|
+
if (folderId) form.append("folder_id", folderId);
|
|
1423
|
+
form.append("file", file);
|
|
1424
|
+
setUploading(true);
|
|
1425
|
+
setError(null);
|
|
1426
|
+
try {
|
|
1427
|
+
const created = await filesRef.current.upload(form);
|
|
1428
|
+
setLastUploaded(created || null);
|
|
1429
|
+
setUploading(false);
|
|
1430
|
+
return created;
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
setError(err);
|
|
1433
|
+
setUploading(false);
|
|
1434
|
+
throw err;
|
|
1435
|
+
}
|
|
1436
|
+
},
|
|
1437
|
+
[ownerId, defaultFolderId, spaceType],
|
|
1438
|
+
);
|
|
1439
|
+
|
|
1440
|
+
return { upload, uploading, error, lastUploaded };
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1367
1443
|
/**
|
|
1368
1444
|
* Browse the end-user's Filestore folders. Returns { folders, loading, error,
|
|
1369
1445
|
* refetch }. Mirrors useFilestoreFiles for subfolder navigation: the widget
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
useAsset,
|
|
18
18
|
useAssetsByTag,
|
|
19
19
|
useFilestoreFiles,
|
|
20
|
+
useFilestoreUpload,
|
|
20
21
|
useFilestoreFolders,
|
|
21
22
|
useFileSignature,
|
|
22
23
|
useFileSignatures,
|
|
@@ -61,6 +62,7 @@ export {
|
|
|
61
62
|
Linking,
|
|
62
63
|
Icon,
|
|
63
64
|
DateTimePicker,
|
|
65
|
+
FilePicker,
|
|
64
66
|
} from "./primitives.js";
|
|
65
67
|
export { lintSource, bannedIdentifiers } from "./linter.js";
|
|
66
68
|
export { CONTRACT, isHookAllowed, requiredContextKeys } from "./contract.js";
|
package/dist/index.native.js
CHANGED
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
useAsset,
|
|
18
18
|
useAssetsByTag,
|
|
19
19
|
useFilestoreFiles,
|
|
20
|
+
useFilestoreUpload,
|
|
20
21
|
useFilestoreFolders,
|
|
21
22
|
useFileSignature,
|
|
22
23
|
useFileSignatures,
|
|
@@ -57,6 +58,7 @@ export {
|
|
|
57
58
|
Linking,
|
|
58
59
|
Icon,
|
|
59
60
|
DateTimePicker,
|
|
61
|
+
FilePicker,
|
|
60
62
|
} from "./primitives.native.js";
|
|
61
63
|
export { lintSource, bannedIdentifiers } from "./linter.js";
|
|
62
64
|
export { CONTRACT, isHookAllowed, requiredContextKeys } from "./contract.js";
|
package/dist/linter.cjs
CHANGED
|
@@ -240,11 +240,31 @@ function _classifySpecifier(spec) {
|
|
|
240
240
|
return "bare";
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
// The package root of a bare specifier: `leaflet/dist/leaflet.css` → "leaflet",
|
|
244
|
+
// `@scope/pkg/sub` → "@scope/pkg". A subpath/asset import of a vetted package
|
|
245
|
+
// belongs to that package, so the root carries the vetting + platforms.
|
|
246
|
+
// Mirror of linter.js.
|
|
247
|
+
function _packageRoot(spec) {
|
|
248
|
+
const parts = spec.split("/");
|
|
249
|
+
// A traversal / empty segment is a non-canonical specifier — never resolve it
|
|
250
|
+
// to a vetted root, or "leaflet/../evil" would pass the gate as "leaflet".
|
|
251
|
+
if (parts.some((p) => p === "" || p === "." || p === "..")) return null;
|
|
252
|
+
return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
253
|
+
}
|
|
254
|
+
|
|
243
255
|
function _importRules(source, manifest) {
|
|
244
256
|
const findings = [];
|
|
245
257
|
const allowed = new Map(
|
|
246
258
|
CONTRACT.vettedImports.map((v) => [v.specifier, v]),
|
|
247
259
|
);
|
|
260
|
+
// Core-infra specifiers (react-dom, react-dom/client) are host-shimmed on
|
|
261
|
+
// both runtimes but kept off the author-facing vetted list — accept them so
|
|
262
|
+
// a bundle's transitive react-dom import isn't flagged import-not-vetted.
|
|
263
|
+
for (const spec of CONTRACT.allowedBareImports) {
|
|
264
|
+
if (!allowed.has(spec)) {
|
|
265
|
+
allowed.set(spec, { specifier: spec, platforms: ["web", "native"] });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
248
268
|
const declaredPlatforms =
|
|
249
269
|
manifest &&
|
|
250
270
|
Array.isArray(manifest.supportedPlatforms) &&
|
|
@@ -279,7 +299,7 @@ function _importRules(source, manifest) {
|
|
|
279
299
|
});
|
|
280
300
|
continue;
|
|
281
301
|
}
|
|
282
|
-
const entry = allowed.get(spec);
|
|
302
|
+
const entry = allowed.get(spec) || allowed.get(_packageRoot(spec));
|
|
283
303
|
if (!entry) {
|
|
284
304
|
findings.push({
|
|
285
305
|
rule: "import-not-vetted",
|
|
@@ -523,8 +543,11 @@ function _manifestActionRules(manifest) {
|
|
|
523
543
|
// so the agent's repair loop fixes them. The valid set is committed data
|
|
524
544
|
// (lucideIconNames.cjs) because the linter runs where lucide is not installed.
|
|
525
545
|
const LUCIDE_NAME_SET = new Set(LUCIDE_ICON_NAMES);
|
|
546
|
+
// `[^}]*` (not `[\s\S]*?`) so the brace capture can never cross a `}` into a
|
|
547
|
+
// neighbouring import — otherwise a preceding `import { View, Pressable } from
|
|
548
|
+
// "react-native"` is swallowed and its names get validated as lucide icons.
|
|
526
549
|
const LUCIDE_IMPORT_RE =
|
|
527
|
-
/import\s+(?:[A-Za-z0-9_$]+\s*,\s*)?\{([
|
|
550
|
+
/import\s+(?:[A-Za-z0-9_$]+\s*,\s*)?\{([^}]*)\}\s*from\s*["']lucide-react-native["']/g;
|
|
528
551
|
|
|
529
552
|
// lucide re-exports each base icon as `<Name>`, `<Name>Icon`, and `Lucide<Name>`;
|
|
530
553
|
// the committed set holds only the base names, so normalise the alias forms
|
|
@@ -571,6 +594,20 @@ function _lucideIconRules(source) {
|
|
|
571
594
|
return findings;
|
|
572
595
|
}
|
|
573
596
|
|
|
597
|
+
// Narrow a split-impl widget's manifest to the platform a single bundle file
|
|
598
|
+
// ships to, so `import-platform-mismatch` lints each file against what it
|
|
599
|
+
// actually targets. Mirror of linter.js.
|
|
600
|
+
function narrowManifestForFile(manifest, filename) {
|
|
601
|
+
if (!manifest || typeof manifest !== "object") return manifest;
|
|
602
|
+
if (filename === "widget.web.jsx") {
|
|
603
|
+
return { ...manifest, supportedPlatforms: ["web"] };
|
|
604
|
+
}
|
|
605
|
+
if (filename === "widget.native.jsx") {
|
|
606
|
+
return { ...manifest, supportedPlatforms: ["native"] };
|
|
607
|
+
}
|
|
608
|
+
return manifest;
|
|
609
|
+
}
|
|
610
|
+
|
|
574
611
|
function lintSource(source, options) {
|
|
575
612
|
if (typeof source !== "string") {
|
|
576
613
|
return {
|
|
@@ -627,4 +664,4 @@ function lintSource(source, options) {
|
|
|
627
664
|
return { ok: !hasErrors, findings };
|
|
628
665
|
}
|
|
629
666
|
|
|
630
|
-
module.exports = { lintSource, bannedIdentifiers };
|
|
667
|
+
module.exports = { lintSource, bannedIdentifiers, narrowManifestForFile };
|
package/dist/linter.js
CHANGED
|
@@ -266,9 +266,29 @@ function _classifySpecifier(spec) {
|
|
|
266
266
|
return "bare";
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
// The package root of a bare specifier: `leaflet/dist/leaflet.css` → "leaflet",
|
|
270
|
+
// `@scope/pkg/sub` → "@scope/pkg". A subpath/asset import of a vetted package
|
|
271
|
+
// (its CSS, a marker PNG, react-dom/client) belongs to that package, so the
|
|
272
|
+
// root carries the vetting + platform metadata.
|
|
273
|
+
function _packageRoot(spec) {
|
|
274
|
+
const parts = spec.split("/");
|
|
275
|
+
// A traversal / empty segment is a non-canonical specifier — never resolve it
|
|
276
|
+
// to a vetted root, or "leaflet/../evil" would pass the gate as "leaflet".
|
|
277
|
+
if (parts.some((p) => p === "" || p === "." || p === "..")) return null;
|
|
278
|
+
return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
279
|
+
}
|
|
280
|
+
|
|
269
281
|
function _importRules(source, manifest) {
|
|
270
282
|
const findings = [];
|
|
271
283
|
const allowed = new Map(CONTRACT.vettedImports.map((v) => [v.specifier, v]));
|
|
284
|
+
// Core-infra specifiers (react-dom, react-dom/client) are host-shimmed on
|
|
285
|
+
// both runtimes but kept off the author-facing vetted list — accept them so
|
|
286
|
+
// a bundle's transitive react-dom import isn't flagged import-not-vetted.
|
|
287
|
+
for (const spec of CONTRACT.allowedBareImports) {
|
|
288
|
+
if (!allowed.has(spec)) {
|
|
289
|
+
allowed.set(spec, { specifier: spec, platforms: ["web", "native"] });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
272
292
|
// Track declared `supportedPlatforms` so a widget that claims "web only"
|
|
273
293
|
// doesn't import a native-only package (and vice versa) without the
|
|
274
294
|
// marketplace listing being honest about which platforms ship.
|
|
@@ -307,8 +327,9 @@ function _importRules(source, manifest) {
|
|
|
307
327
|
});
|
|
308
328
|
continue;
|
|
309
329
|
}
|
|
310
|
-
// Bare specifier — validate against the vetted list.
|
|
311
|
-
|
|
330
|
+
// Bare specifier — validate against the vetted list. A subpath/asset
|
|
331
|
+
// import (leaflet/dist/leaflet.css) resolves to its vetted package root.
|
|
332
|
+
const entry = allowed.get(spec) || allowed.get(_packageRoot(spec));
|
|
312
333
|
if (!entry) {
|
|
313
334
|
findings.push({
|
|
314
335
|
rule: "import-not-vetted",
|
|
@@ -611,8 +632,11 @@ function _manifestActionRules(manifest) {
|
|
|
611
632
|
// so the agent's repair loop fixes them. The valid set is committed data
|
|
612
633
|
// (lucideIconNames.js) because the linter runs where lucide is not installed.
|
|
613
634
|
const LUCIDE_NAME_SET = new Set(LUCIDE_ICON_NAMES);
|
|
635
|
+
// `[^}]*` (not `[\s\S]*?`) so the brace capture can never cross a `}` into a
|
|
636
|
+
// neighbouring import — otherwise a preceding `import { View, Pressable } from
|
|
637
|
+
// "react-native"` is swallowed and its names get validated as lucide icons.
|
|
614
638
|
const LUCIDE_IMPORT_RE =
|
|
615
|
-
/import\s+(?:[A-Za-z0-9_$]+\s*,\s*)?\{([
|
|
639
|
+
/import\s+(?:[A-Za-z0-9_$]+\s*,\s*)?\{([^}]*)\}\s*from\s*["']lucide-react-native["']/g;
|
|
616
640
|
|
|
617
641
|
// lucide re-exports each base icon as `<Name>`, `<Name>Icon`, and `Lucide<Name>`;
|
|
618
642
|
// the committed set holds only the base names, so normalise the alias forms
|
|
@@ -659,6 +683,24 @@ function _lucideIconRules(source) {
|
|
|
659
683
|
return findings;
|
|
660
684
|
}
|
|
661
685
|
|
|
686
|
+
/**
|
|
687
|
+
* Narrow a split-impl widget's manifest to the platform a single bundle file
|
|
688
|
+
* ships to, so `import-platform-mismatch` lints each file against what it
|
|
689
|
+
* actually targets. `widget.web.jsx` → ["web"], `widget.native.jsx` →
|
|
690
|
+
* ["native"], cross-platform `widget.jsx` keeps the manifest's union claim.
|
|
691
|
+
* Returns a shallow clone; the original is untouched.
|
|
692
|
+
*/
|
|
693
|
+
export function narrowManifestForFile(manifest, filename) {
|
|
694
|
+
if (!manifest || typeof manifest !== "object") return manifest;
|
|
695
|
+
if (filename === "widget.web.jsx") {
|
|
696
|
+
return { ...manifest, supportedPlatforms: ["web"] };
|
|
697
|
+
}
|
|
698
|
+
if (filename === "widget.native.jsx") {
|
|
699
|
+
return { ...manifest, supportedPlatforms: ["native"] };
|
|
700
|
+
}
|
|
701
|
+
return manifest;
|
|
702
|
+
}
|
|
703
|
+
|
|
662
704
|
export function lintSource(source, options) {
|
|
663
705
|
if (typeof source !== "string") {
|
|
664
706
|
return {
|
package/dist/primitives.js
CHANGED
|
@@ -70,3 +70,9 @@ export { Icon } from "./icon.js";
|
|
|
70
70
|
// react-native-web mapping. Both implementations honour the same
|
|
71
71
|
// ISO 8601 value / onChange contract.
|
|
72
72
|
export { DateTimePicker } from "./datetimepicker.js";
|
|
73
|
+
// sc-1378 — `<FilePicker>` (web). Wraps a hidden DOM `<input type="file">`
|
|
74
|
+
// because react-native-web has no native equivalent. The native build's
|
|
75
|
+
// counterpart (./filepicker.native.js) degrades to a disabled trigger until
|
|
76
|
+
// a vetted expo-document-picker pin lands. `isSupported` is a static
|
|
77
|
+
// boolean widgets branch on to hide the trigger on native.
|
|
78
|
+
export { FilePicker } from "./filepicker.js";
|
|
@@ -31,3 +31,8 @@ export { Icon } from "./icon.js";
|
|
|
31
31
|
// `./datetimepicker.js`. Both files honour the same ISO 8601 value /
|
|
32
32
|
// onChange contract.
|
|
33
33
|
export { DateTimePicker } from "./datetimepicker.native.js";
|
|
34
|
+
// sc-1378 — `<FilePicker>` (native). Web counterpart (./filepicker.js)
|
|
35
|
+
// wraps a hidden DOM `<input type="file">`; native has no portable
|
|
36
|
+
// equivalent yet, so this build renders a disabled trigger and exposes
|
|
37
|
+
// `FilePicker.isSupported = false` for widgets to branch on.
|
|
38
|
+
export { FilePicker } from "./filepicker.native.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.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",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
],
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "node scripts/build.js",
|
|
45
|
-
"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__/lucide-icon-names.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"
|
|
45
|
+
"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-filestore-upload.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__/lucide-icon-names.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"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
48
|
"node": ">=18"
|