@colixsystems/widget-sdk 0.14.1 → 0.16.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 +28 -4
- package/dist/clipboard.js +88 -0
- package/dist/clipboard.native.js +64 -0
- package/dist/contract.cjs +224 -12
- package/dist/contract.js +181 -10
- package/dist/datetimepicker.js +102 -0
- package/dist/hooks.js +3 -1
- package/dist/icon.js +29 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +7 -0
- package/dist/index.native.js +5 -0
- package/dist/linter.cjs +148 -9
- package/dist/linter.js +193 -13
- package/dist/primitives.js +8 -0
- package/dist/primitives.native.js +9 -0
- package/dist/toast.js +73 -0
- package/dist/toast.native.js +46 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,9 +6,33 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
|
-
`v0.
|
|
9
|
+
`v0.16.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**.
|
|
10
10
|
|
|
11
|
-
### What's new in 0.
|
|
11
|
+
### What's new in 0.16.0
|
|
12
|
+
|
|
13
|
+
REQ-THEME — the tenant's **Theme Settings** now flow all the way into `useTheme()`.
|
|
14
|
+
|
|
15
|
+
- **`themeTokens.colors` gains `secondary` + `onSecondary`.** `useTheme().colors.secondary` reflects the tenant's *Secondary Color* picker (with `onSecondary` as its readable contrast color), alongside the existing `primary` / `onPrimary`. Built-in widgets like Button use it for their secondary variant; third-party widgets can use it for a branded second accent. The full `colors` shape is now `{ primary, onPrimary, secondary, onSecondary, surface, onSurface, surfaceMuted, onSurfaceMuted, border, danger, success, warning, info }`.
|
|
16
|
+
- **`colors.primary` / `colors.secondary` / `typography.fontFamily` are tenant-resolved.** The host maps the Studio Theme Settings blob (Primary Color, Secondary Color, Global Font) onto the default tokens before handing them to `useTheme()`, on both the live Player and the exported app — so a widget that reads tokens re-themes automatically. (Custom Google fonts render in the Player today; the exported app falls back to the system face for non-system fonts until font bundling lands.)
|
|
17
|
+
- **`CONTRACT.version` → `1.6.0`** (additive: two new `themeTokens.colors` keys). No existing export changed signature.
|
|
18
|
+
|
|
19
|
+
### What's new in 0.15.0
|
|
20
|
+
|
|
21
|
+
REQ-WSDK-PLATFORM — the "split-implementation + vetted package list" pivot. See [`docs/design/req-widget-sdk-cross-platform-primitives.md`](../../docs/design/req-widget-sdk-cross-platform-primitives.md) for the full design and rationale.
|
|
22
|
+
|
|
23
|
+
- **`CONTRACT.vettedImports` (new).** A curated allowlist of bare specifiers a widget may import — `react`, `@colixsystems/widget-sdk`, `react-native`, `axios`, `date-fns`, `react-native-svg`, `lucide-react-native`, `react-native-maps`, `leaflet`, `react-leaflet`, `expo-av`, `@react-native-community/datetimepicker`, `expo-clipboard`, `expo-haptics`. Each entry carries `platforms` (one or both of `"web"` / `"native"`) and a `category` so the linter and the marketplace listing can render honest platform badges. `CONTRACT.allowedBareImports` (the existing field) is now derived from `vettedImports` and stays a plain `string[]` for back-compat.
|
|
24
|
+
- **`fetch` and `XMLHttpRequest` come off `CONTRACT.bannedApis`.** Widgets may call third-party APIs directly. Calls to the host's own `/api/*` surface will 401 because the JWT token is never shared with widget code; the linter emits a soft `no-host-api-url` warning when it sees host-URL substrings so authors learn the rule statically. Use SDK hooks (`useDatastoreQuery`, `useUsers`, `useFile`, …) for workspace data; use `axios` / `fetch` for third-party APIs.
|
|
25
|
+
- **`import-not-vetted` linter rule (new).** Every bare `import` specifier is validated against `CONTRACT.vettedImports`. Relative imports inside the bundle (`./shared.js`) are allowed so split-impl widgets can share helpers; `../` and absolute paths are rejected.
|
|
26
|
+
- **`import-platform-mismatch` linter rule (new).** A single-source widget that imports a native-only package while `manifest.supportedPlatforms` includes `"web"` fails the lint. The author either drops the platform from the manifest OR ships a `widget.web.jsx` + `widget.native.jsx` pair where the platform-specific import lives in the file that targets its platform.
|
|
27
|
+
- **Lint findings carry `severity`.** `"error"` (default) blocks publish; `"warning"` (currently only `no-host-api-url`) surfaces to reviewers without blocking. The `lintSource(...)` return shape stays `{ ok, findings }` — `ok` is true iff no error-severity findings exist.
|
|
28
|
+
- **Four Tier A SDK additions:**
|
|
29
|
+
- `<Icon>` primitive — `<Icon name="check" size={16} color={theme.colors.primary} />`. Wraps `lucide-react-native`; works on both platforms.
|
|
30
|
+
- `<DateTimePicker>` primitive — `<DateTimePicker value={iso} onChange={iso => …} mode="date" | "time" | "datetime" />`. Wraps `@react-native-community/datetimepicker` and normalizes the value to ISO 8601 strings (the datastore wire format).
|
|
31
|
+
- `useClipboard()` hook — `{ copy, paste, hasContent }`. Web via `navigator.clipboard`; native via `expo-clipboard`. Rejections are a structured `ClipboardError` with `.code` in `PERMISSION_DENIED | INTERNAL`.
|
|
32
|
+
- `useToast()` hook — `{ showToast }`. The host installs a workspace-themed renderer at `WidgetContext.toast.showToast`; if omitted, the web variant dispatches an `appstudio:widget-toast` CustomEvent and native logs to the console.
|
|
33
|
+
- **`CONTRACT.version` → `1.5.0`** (additive: two new contract fields — `vettedImports`, `hostApiUrlPatterns` — two banned APIs removed, two primitives + two hooks added, one optional `widgetContextShape.toast` slot). No existing export changed signature.
|
|
34
|
+
|
|
35
|
+
### What was in 0.14.1
|
|
12
36
|
|
|
13
37
|
- **`groupRef` property type (REQ-USERMGMT M4 / §4.8).** Authors can now declare `{ type: 'groupRef', label: 'Group' }` in their `propertySchema` to render a Group picker in the Studio Properties Panel. The widget receives a bare `AppUserGroup` UUID — REQ-GEN-07 compliant, so tenant-copy walks the value transparently. Used by the built-in `appstudio.user-management` widget for its `defaultGroupId` prop and available to third-party widgets that need to anchor behaviour on a specific group.
|
|
14
38
|
- **Patch bump** — additive enumeration entry, no exported function signature changed. `CONTRACT.version` stays `1.4.0`.
|
|
@@ -101,9 +125,9 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
101
125
|
|
|
102
126
|
- `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
|
|
103
127
|
- `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
|
|
104
|
-
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useFile`, `useWidgetEvent`, `usePayments`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer` — hooks that read from the host-provided `WidgetContext
|
|
128
|
+
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useFile`, `useWidgetEvent`, `usePayments`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (`id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute }` for internal page navigation — for external URLs use the `Linking` primitive (`Linking.openURL(url)`). `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useFile(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case).
|
|
105
129
|
- `WidgetTree({ node })` — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
|
|
106
|
-
- `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet` — re-exported from `react-native
|
|
130
|
+
- `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`, `DateTimePicker` wraps `@react-native-community/datetimepicker`). The web build aliases `react-native` to `react-native-web` so widgets render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
|
|
107
131
|
- `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
|
|
108
132
|
|
|
109
133
|
## Managing app users from a widget
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// REQ-WSDK-PLATFORM §6 — `useClipboard()` hook (web implementation).
|
|
2
|
+
//
|
|
3
|
+
// Wraps the browser's `navigator.clipboard` API. Returns a stable object:
|
|
4
|
+
//
|
|
5
|
+
// const { copy, paste, hasContent } = useClipboard();
|
|
6
|
+
//
|
|
7
|
+
// - `copy(text)` → Promise<void>. Writes a string. Rejects with a
|
|
8
|
+
// `ClipboardError` on failure (most common code:
|
|
9
|
+
// "PERMISSION_DENIED" — the page lacks the
|
|
10
|
+
// clipboard-write permission, often because the
|
|
11
|
+
// call wasn't triggered by a user gesture).
|
|
12
|
+
// - `paste()` → Promise<string>. Reads the current clipboard
|
|
13
|
+
// text. Returns "" when empty, rejects with
|
|
14
|
+
// `ClipboardError` (code "PERMISSION_DENIED" on
|
|
15
|
+
// sites that haven't asked for clipboard-read
|
|
16
|
+
// permission).
|
|
17
|
+
// - `hasContent()` → Promise<boolean>. Best-effort: reads the
|
|
18
|
+
// clipboard and reports whether it had a
|
|
19
|
+
// non-empty string. Same permission caveats as
|
|
20
|
+
// `paste()`.
|
|
21
|
+
//
|
|
22
|
+
// The native build uses ./clipboard.native.js (Metro picks via the
|
|
23
|
+
// package.json `react-native` resolution path through index.native.js).
|
|
24
|
+
|
|
25
|
+
export class ClipboardError extends Error {
|
|
26
|
+
constructor(code, message, opts) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "ClipboardError";
|
|
29
|
+
this.code = code;
|
|
30
|
+
if (opts && opts.cause) this.cause = opts.cause;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _isPermissionError(err) {
|
|
35
|
+
if (!err) return false;
|
|
36
|
+
if (err.name === "NotAllowedError" || err.name === "SecurityError") return true;
|
|
37
|
+
const msg = typeof err.message === "string" ? err.message.toLowerCase() : "";
|
|
38
|
+
return msg.includes("permission") || msg.includes("not allowed");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _wrap(err, label) {
|
|
42
|
+
if (err instanceof ClipboardError) return err;
|
|
43
|
+
const code = _isPermissionError(err) ? "PERMISSION_DENIED" : "INTERNAL";
|
|
44
|
+
return new ClipboardError(code, `${label}: ${err && err.message ? err.message : err}`, {
|
|
45
|
+
cause: err,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function useClipboard() {
|
|
50
|
+
// Each method is a fresh function but the returned identity is stable
|
|
51
|
+
// per call site — callers passing one into useEffect dep arrays should
|
|
52
|
+
// pin the method, not the whole object.
|
|
53
|
+
return {
|
|
54
|
+
async copy(text) {
|
|
55
|
+
const value = text == null ? "" : String(text);
|
|
56
|
+
try {
|
|
57
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
58
|
+
throw new ClipboardError("INTERNAL", "Clipboard API unavailable.");
|
|
59
|
+
}
|
|
60
|
+
await navigator.clipboard.writeText(value);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
throw _wrap(err, "clipboard.copy");
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
async paste() {
|
|
66
|
+
try {
|
|
67
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
68
|
+
throw new ClipboardError("INTERNAL", "Clipboard API unavailable.");
|
|
69
|
+
}
|
|
70
|
+
const text = await navigator.clipboard.readText();
|
|
71
|
+
return typeof text === "string" ? text : "";
|
|
72
|
+
} catch (err) {
|
|
73
|
+
throw _wrap(err, "clipboard.paste");
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
async hasContent() {
|
|
77
|
+
try {
|
|
78
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) return false;
|
|
79
|
+
const text = await navigator.clipboard.readText();
|
|
80
|
+
return typeof text === "string" && text.length > 0;
|
|
81
|
+
} catch {
|
|
82
|
+
// Permission-denied reads return false instead of throwing —
|
|
83
|
+
// hasContent() is the "best-effort" probe, not a write gate.
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// REQ-WSDK-PLATFORM §6 — `useClipboard()` hook (native implementation).
|
|
2
|
+
//
|
|
3
|
+
// Wraps expo-clipboard. Same return shape as the web counterpart
|
|
4
|
+
// (./clipboard.js) so widget code is byte-for-byte identical across
|
|
5
|
+
// platforms. The two implementations only differ in how `copy` / `paste`
|
|
6
|
+
// reach the system clipboard.
|
|
7
|
+
//
|
|
8
|
+
// Errors are normalized to a structured `ClipboardError` with a stable
|
|
9
|
+
// `.code`. Native clipboard reads cannot fail on permission grounds
|
|
10
|
+
// (Android doesn't prompt; iOS reveals a paste banner but still
|
|
11
|
+
// resolves), so "PERMISSION_DENIED" is web-only.
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
14
|
+
import * as Clipboard from "expo-clipboard";
|
|
15
|
+
|
|
16
|
+
export class ClipboardError extends Error {
|
|
17
|
+
constructor(code, message, opts) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "ClipboardError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
if (opts && opts.cause) this.cause = opts.cause;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function _wrap(err, label) {
|
|
26
|
+
if (err instanceof ClipboardError) return err;
|
|
27
|
+
return new ClipboardError(
|
|
28
|
+
"INTERNAL",
|
|
29
|
+
`${label}: ${err && err.message ? err.message : err}`,
|
|
30
|
+
{ cause: err },
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useClipboard() {
|
|
35
|
+
return {
|
|
36
|
+
async copy(text) {
|
|
37
|
+
const value = text == null ? "" : String(text);
|
|
38
|
+
try {
|
|
39
|
+
await Clipboard.setStringAsync(value);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw _wrap(err, "clipboard.copy");
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
async paste() {
|
|
45
|
+
try {
|
|
46
|
+
const text = await Clipboard.getStringAsync();
|
|
47
|
+
return typeof text === "string" ? text : "";
|
|
48
|
+
} catch (err) {
|
|
49
|
+
throw _wrap(err, "clipboard.paste");
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
async hasContent() {
|
|
53
|
+
try {
|
|
54
|
+
if (typeof Clipboard.hasStringAsync === "function") {
|
|
55
|
+
return !!(await Clipboard.hasStringAsync());
|
|
56
|
+
}
|
|
57
|
+
const text = await Clipboard.getStringAsync();
|
|
58
|
+
return typeof text === "string" && text.length > 0;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
package/dist/contract.cjs
CHANGED
|
@@ -12,6 +12,8 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
|
|
|
12
12
|
colors: Object.freeze({
|
|
13
13
|
primary: "#ff6b5b",
|
|
14
14
|
onPrimary: "#ffffff",
|
|
15
|
+
secondary: "#475569",
|
|
16
|
+
onSecondary: "#ffffff",
|
|
15
17
|
surface: "#ffffff",
|
|
16
18
|
onSurface: "#111827",
|
|
17
19
|
surfaceMuted: "#f8fafc",
|
|
@@ -37,7 +39,7 @@ const HOOKS = [
|
|
|
37
39
|
signature: "useTheme()",
|
|
38
40
|
returnShape: {
|
|
39
41
|
colors:
|
|
40
|
-
"{ primary, onPrimary, surface, onSurface, surfaceMuted, onSurfaceMuted, border, danger, success, warning, info }",
|
|
42
|
+
"{ primary, onPrimary, secondary, onSecondary, surface, onSurface, surfaceMuted, onSurfaceMuted, border, danger, success, warning, info }",
|
|
41
43
|
spacing: "{ xs, sm, md, lg, xl }",
|
|
42
44
|
radii: "{ sm, md, lg, pill }",
|
|
43
45
|
typography: "{ fontFamily, sizes: { xs, sm, md, lg, xl, xxl } }",
|
|
@@ -240,6 +242,42 @@ const HOOKS = [
|
|
|
240
242
|
],
|
|
241
243
|
scopes: ["groups.read:*"],
|
|
242
244
|
},
|
|
245
|
+
// REQ-WSDK-PLATFORM §6 — Tier A SDK hooks. Mirror of contract.js.
|
|
246
|
+
{
|
|
247
|
+
name: "useClipboard",
|
|
248
|
+
signature: "useClipboard()",
|
|
249
|
+
description:
|
|
250
|
+
"Cross-platform clipboard access. Returns { copy, paste, hasContent }. " +
|
|
251
|
+
"All methods return Promises; rejections surface a structured ClipboardError " +
|
|
252
|
+
"with .code in PERMISSION_DENIED | INTERNAL. On web the browser may " +
|
|
253
|
+
"require a user gesture for read access — surface the error to the " +
|
|
254
|
+
"user as a 'try again after clicking' prompt when code === PERMISSION_DENIED.",
|
|
255
|
+
returnShape: {
|
|
256
|
+
copy:
|
|
257
|
+
"(text: string) => Promise<void> // rejects with ClipboardError",
|
|
258
|
+
paste:
|
|
259
|
+
"() => Promise<string> // rejects with ClipboardError",
|
|
260
|
+
hasContent: "() => Promise<boolean> // best-effort, never throws",
|
|
261
|
+
},
|
|
262
|
+
requiredContextSlice: [],
|
|
263
|
+
scopes: null,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: "useToast",
|
|
267
|
+
signature: "useToast()",
|
|
268
|
+
description:
|
|
269
|
+
"Surfaces a short auto-dismissing notification. Returns { showToast }. " +
|
|
270
|
+
"showToast({ kind: 'success' | 'error' | 'info' | 'warning', message }) " +
|
|
271
|
+
"asks the host to render a workspace-themed toast. If the host hasn't " +
|
|
272
|
+
"wired a renderer, the web variant dispatches an 'appstudio:widget-toast' " +
|
|
273
|
+
"CustomEvent on window; native logs to the console. The widget never " +
|
|
274
|
+
"owns the toast UI — that's the host's responsibility.",
|
|
275
|
+
returnShape: {
|
|
276
|
+
showToast: "({ kind, message }) => void",
|
|
277
|
+
},
|
|
278
|
+
requiredContextSlice: ["toast.showToast"],
|
|
279
|
+
scopes: null,
|
|
280
|
+
},
|
|
243
281
|
];
|
|
244
282
|
|
|
245
283
|
// REQ-WSDK-RN-WEB: the SDK exposes the React Native primitive API
|
|
@@ -334,6 +372,22 @@ const PRIMITIVES = [
|
|
|
334
372
|
rnComponent: "Linking",
|
|
335
373
|
docsUrl: "https://reactnative.dev/docs/linking",
|
|
336
374
|
},
|
|
375
|
+
// REQ-WSDK-PLATFORM §6 — Tier A SDK primitive.
|
|
376
|
+
{
|
|
377
|
+
name: "Icon",
|
|
378
|
+
description:
|
|
379
|
+
'Lucide icon. `<Icon name="check" size={16} color="..." />`. Unknown names render the Square fallback so the canvas always shows something visible. Names are the lucide icon ids (`https://lucide.dev/icons`). Works on both web and native.',
|
|
380
|
+
rnComponent: "lucide-react-native",
|
|
381
|
+
docsUrl: "https://lucide.dev/icons",
|
|
382
|
+
},
|
|
383
|
+
// REQ-WSDK-PLATFORM §6 — Tier A SDK primitive.
|
|
384
|
+
{
|
|
385
|
+
name: "DateTimePicker",
|
|
386
|
+
description:
|
|
387
|
+
'Cross-platform date / time / datetime picker. `<DateTimePicker value={iso} onChange={iso => …} mode="date" | "time" | "datetime" />`. The value prop and the onChange callback both speak ISO 8601 strings (the datastore wire format) — authors never round-trip through `new Date()`. Web renders the browser\'s native input via react-native-web; native uses @react-native-community/datetimepicker.',
|
|
388
|
+
rnComponent: "@react-native-community/datetimepicker",
|
|
389
|
+
docsUrl: "https://github.com/react-native-datetimepicker/datetimepicker",
|
|
390
|
+
},
|
|
337
391
|
];
|
|
338
392
|
|
|
339
393
|
const CATEGORIES = [
|
|
@@ -551,6 +605,17 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
551
605
|
error: "function",
|
|
552
606
|
},
|
|
553
607
|
},
|
|
608
|
+
// REQ-WSDK-PLATFORM §6 — backs useToast(). Mirror of contract.js.
|
|
609
|
+
toast: {
|
|
610
|
+
description:
|
|
611
|
+
"Optional host toast slot. { showToast({ kind, message }): void }. " +
|
|
612
|
+
"The host populates this to render workspace-themed notifications " +
|
|
613
|
+
"from any widget that calls useToast(). When omitted the SDK falls " +
|
|
614
|
+
"back to dispatching an 'appstudio:widget-toast' CustomEvent on web " +
|
|
615
|
+
"and console.log on native.",
|
|
616
|
+
required: false,
|
|
617
|
+
fields: { showToast: "function" },
|
|
618
|
+
},
|
|
554
619
|
};
|
|
555
620
|
|
|
556
621
|
const BUNDLE_EXPORT_CONTRACT = [
|
|
@@ -573,6 +638,15 @@ const BUNDLE_EXPORT_CONTRACT = [
|
|
|
573
638
|
},
|
|
574
639
|
];
|
|
575
640
|
|
|
641
|
+
// REQ-WSDK-PLATFORM (docs/design/req-widget-sdk-cross-platform-primitives.md
|
|
642
|
+
// §3.5, §8): `fetch` and `XMLHttpRequest` are NOT banned. Widgets may call
|
|
643
|
+
// third-party APIs directly. Same-origin requests to the host's own
|
|
644
|
+
// `/api/*` surface are rejected at runtime by the WidgetContextProvider's
|
|
645
|
+
// network gate (`no host-api access from widgets`) — the JWT token is
|
|
646
|
+
// never shared with widget code, so the call would 401 anyway; the runtime
|
|
647
|
+
// gate makes the failure mode "blocked" instead of "401 noise". A soft
|
|
648
|
+
// linter warning (`no-host-api-url`) flags obvious host-URL substrings at
|
|
649
|
+
// submission so authors learn the rule statically.
|
|
576
650
|
const BANNED_APIS = [
|
|
577
651
|
{ identifier: "eval", reason: "Arbitrary code evaluation." },
|
|
578
652
|
{
|
|
@@ -601,23 +675,144 @@ const BANNED_APIS = [
|
|
|
601
675
|
reason: "Same reason as localStorage.",
|
|
602
676
|
},
|
|
603
677
|
{
|
|
604
|
-
identifier: "
|
|
605
|
-
reason:
|
|
606
|
-
|
|
678
|
+
identifier: "import(",
|
|
679
|
+
reason: "Dynamic import bypasses the loader's allowlist.",
|
|
680
|
+
},
|
|
681
|
+
{ identifier: "globalThis", reason: "Host environment escape." },
|
|
682
|
+
];
|
|
683
|
+
|
|
684
|
+
// REQ-WSDK-PLATFORM §3.4, §5: vetted package allowlist. Each entry is a
|
|
685
|
+
// specifier the widget bundle may import as a bare module specifier. The
|
|
686
|
+
// linter validates every `import ... from "<spec>"` line against this
|
|
687
|
+
// list; specifiers not on the list fail the lint. The compiler reads it
|
|
688
|
+
// to know which packages to add to the exported Expo app's package.json.
|
|
689
|
+
//
|
|
690
|
+
// `platforms` is one or both of "web" / "native". A widget that imports a
|
|
691
|
+
// native-only package implicitly drops "web" from the package's effective
|
|
692
|
+
// `supportedPlatforms` (and vice versa) — the marketplace upload pipeline
|
|
693
|
+
// surfaces the derived set so the listing shows honest badges.
|
|
694
|
+
//
|
|
695
|
+
// Adding a package is a CONTRACT change. Review burden: confirm the
|
|
696
|
+
// package does what it claims, has a credible maintainer, and the
|
|
697
|
+
// import shape (named exports, default export) is stable. After that,
|
|
698
|
+
// every widget using the package inherits the review — the marketplace
|
|
699
|
+
// reviewer only spot-checks usage, not the package source.
|
|
700
|
+
const VETTED_IMPORTS = [
|
|
701
|
+
{
|
|
702
|
+
specifier: "react",
|
|
703
|
+
platforms: ["web", "native"],
|
|
704
|
+
category: "core",
|
|
705
|
+
description: "React. Hooks, JSX, lifecycle. Unchanged.",
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
specifier: "@colixsystems/widget-sdk",
|
|
709
|
+
platforms: ["web", "native"],
|
|
710
|
+
category: "core",
|
|
711
|
+
description:
|
|
712
|
+
"The AppStudio widget SDK — primitives, hooks, manifest helpers. Unchanged.",
|
|
607
713
|
},
|
|
608
714
|
{
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
715
|
+
specifier: "react-native",
|
|
716
|
+
platforms: ["web", "native"],
|
|
717
|
+
category: "primitive",
|
|
718
|
+
description:
|
|
719
|
+
"Direct RN imports for APIs the SDK hasn't re-exported yet. On web the host bundler aliases this to react-native-web; on native Metro resolves the real library.",
|
|
612
720
|
},
|
|
613
721
|
{
|
|
614
|
-
|
|
615
|
-
|
|
722
|
+
specifier: "axios",
|
|
723
|
+
platforms: ["web", "native"],
|
|
724
|
+
category: "network",
|
|
725
|
+
description:
|
|
726
|
+
"HTTP client for third-party APIs. Calls to the host's /api/* surface are blocked at runtime — widgets get no JWT token, so use SDK hooks for workspace data.",
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
specifier: "date-fns",
|
|
730
|
+
platforms: ["web", "native"],
|
|
731
|
+
category: "utility",
|
|
732
|
+
description: "Pure-JS date math. Works on both platforms unchanged.",
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
specifier: "react-native-svg",
|
|
736
|
+
platforms: ["web", "native"],
|
|
737
|
+
category: "drawing",
|
|
738
|
+
description:
|
|
739
|
+
"Cross-platform SVG drawing primitives. Used by the built-in Chart widget; works on both platforms.",
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
specifier: "lucide-react-native",
|
|
743
|
+
platforms: ["web", "native"],
|
|
744
|
+
category: "iconography",
|
|
745
|
+
description:
|
|
746
|
+
"Lucide icon set as React components. Used by the built-in Icon widget; works on both platforms.",
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
specifier: "react-native-maps",
|
|
750
|
+
platforms: ["native"],
|
|
751
|
+
category: "geo",
|
|
752
|
+
description:
|
|
753
|
+
"Native map view + markers. Native-only; pair with leaflet/react-leaflet in widget.web.jsx for a web variant.",
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
specifier: "leaflet",
|
|
757
|
+
platforms: ["web"],
|
|
758
|
+
category: "geo",
|
|
759
|
+
description:
|
|
760
|
+
"Web-only mapping library. Use alongside react-leaflet in widget.web.jsx as the web counterpart to react-native-maps.",
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
specifier: "react-leaflet",
|
|
764
|
+
platforms: ["web"],
|
|
765
|
+
category: "geo",
|
|
766
|
+
description: "React bindings for leaflet. Web-only.",
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
specifier: "expo-av",
|
|
770
|
+
platforms: ["native"],
|
|
771
|
+
category: "media",
|
|
772
|
+
description:
|
|
773
|
+
"Native audio + video playback. Native-only; pair with browser <audio>/<video> in widget.web.jsx.",
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
specifier: "@react-native-community/datetimepicker",
|
|
777
|
+
platforms: ["native"],
|
|
778
|
+
category: "input",
|
|
779
|
+
description:
|
|
780
|
+
"Native date/time picker. The SDK's <DateTimePicker> primitive already wraps this; reach for it directly only if you need RN-specific options.",
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
specifier: "expo-clipboard",
|
|
784
|
+
platforms: ["native"],
|
|
785
|
+
category: "system",
|
|
786
|
+
description:
|
|
787
|
+
"Native clipboard. The SDK's useClipboard() hook already wraps this; reach for it directly only if you need RN-specific options.",
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
specifier: "expo-haptics",
|
|
791
|
+
platforms: ["native"],
|
|
792
|
+
category: "system",
|
|
793
|
+
description:
|
|
794
|
+
"Native haptic feedback. Pair with navigator.vibrate in widget.web.jsx.",
|
|
616
795
|
},
|
|
617
|
-
{ identifier: "globalThis", reason: "Host environment escape." },
|
|
618
796
|
];
|
|
619
797
|
|
|
620
|
-
|
|
798
|
+
// Back-compat shape — every existing consumer (widgetLoader, the static
|
|
799
|
+
// analyzer's DEPENDENCY_ALLOWLIST, the AI prompt's "Allowed imports" line)
|
|
800
|
+
// reads a plain `string[]` from `CONTRACT.allowedBareImports`. Derive it
|
|
801
|
+
// from the rich list so a single edit in VETTED_IMPORTS propagates to
|
|
802
|
+
// every surface.
|
|
803
|
+
const ALLOWED_BARE_IMPORTS = VETTED_IMPORTS.map((v) => v.specifier);
|
|
804
|
+
|
|
805
|
+
// REQ-WSDK-PLATFORM §3.5: host-API URL patterns the linter scans for as a
|
|
806
|
+
// soft warning. None of these block the lint by themselves — they prompt
|
|
807
|
+
// the marketplace reviewer to spot-check, and they help authors learn the
|
|
808
|
+
// rule statically. Strings appear as literal substring matches (the URL
|
|
809
|
+
// is reachable in widget code by composing it at runtime, so this is
|
|
810
|
+
// best-effort).
|
|
811
|
+
const HOST_API_URL_PATTERNS = [
|
|
812
|
+
"/api/v1",
|
|
813
|
+
"/uploads/",
|
|
814
|
+
"Authorization: Bearer",
|
|
815
|
+
];
|
|
621
816
|
|
|
622
817
|
function deepFreeze(value) {
|
|
623
818
|
if (value === null || typeof value !== "object") return value;
|
|
@@ -627,7 +822,22 @@ function deepFreeze(value) {
|
|
|
627
822
|
}
|
|
628
823
|
|
|
629
824
|
const CONTRACT = deepFreeze({
|
|
630
|
-
|
|
825
|
+
// REQ-WSDK-PLATFORM bump:
|
|
826
|
+
// - `vettedImports` is a new field (rich allowlist with platforms +
|
|
827
|
+
// category + description).
|
|
828
|
+
// - `hostApiUrlPatterns` is a new field (soft-warning substrings).
|
|
829
|
+
// - `bannedApis` no longer lists `fetch` / `XMLHttpRequest`.
|
|
830
|
+
// - `allowedBareImports` is now derived from `vettedImports` (same
|
|
831
|
+
// shape; same contents grow with each vetted addition).
|
|
832
|
+
// Permissive-direction change: minor bump on the contract's own
|
|
833
|
+
// versioning (per CLAUDE.md §4, pre-1.0 minor is the breaking channel —
|
|
834
|
+
// the package.json version bumps accordingly).
|
|
835
|
+
//
|
|
836
|
+
// 1.6.0: additive — `themeTokens.colors` gains `secondary` + `onSecondary`
|
|
837
|
+
// so the tenant's Theme Settings "Secondary Color" flows through
|
|
838
|
+
// `useTheme().colors.secondary` (Button secondary variant + any widget
|
|
839
|
+
// that wants the brand's second accent).
|
|
840
|
+
version: "1.6.0",
|
|
631
841
|
hooks: HOOKS,
|
|
632
842
|
primitives: PRIMITIVES,
|
|
633
843
|
manifestSchema: MANIFEST_SCHEMA,
|
|
@@ -637,7 +847,9 @@ const CONTRACT = deepFreeze({
|
|
|
637
847
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
638
848
|
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|
|
639
849
|
bannedApis: BANNED_APIS,
|
|
850
|
+
vettedImports: VETTED_IMPORTS,
|
|
640
851
|
allowedBareImports: ALLOWED_BARE_IMPORTS,
|
|
852
|
+
hostApiUrlPatterns: HOST_API_URL_PATTERNS,
|
|
641
853
|
});
|
|
642
854
|
|
|
643
855
|
function isHookAllowed(name) {
|