@colixsystems/widget-sdk 0.37.0 → 0.39.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 +27 -10
- package/dist/cli.js +11 -3
- package/dist/contract.cjs +107 -13
- package/dist/contract.js +107 -13
- package/dist/dev-shims.js +255 -0
- package/dist/devserver.js +640 -73
- package/dist/hooks.js +60 -31
- package/dist/index.d.ts +10 -10
- package/dist/index.js +1 -1
- package/dist/index.native.js +1 -1
- package/dist/webbundle.js +125 -0
- package/package.json +7 -2
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.
|
|
11
|
+
| `ctx.assets` | `@colixsystems/assets-client` | the Asset Manager: `get(id)`, `list(query)`, `upload(formData)` over `/files` — what `useAsset()` resolves |
|
|
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.
|
|
@@ -26,13 +26,13 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
26
26
|
| **CORE** | `useFill()` | `boolean` | `ctx.fill` — no scope. `true` when the host sized this widget to fill its page-grid tile's reserved height (containers + media fill by default; the author can override per tile). Media-style widgets switch to a `flex: 1` / `height: "100%"` layout; others ignore it. Defaults `false`. |
|
|
27
27
|
| **CORE** | `useClipboard()` | `{ copy, paste, hasContent }` | platform clipboard (web `navigator.clipboard` / native `expo-clipboard`); rejects with `ClipboardError` — no scope |
|
|
28
28
|
| **CORE** | `useToast()` | `{ showToast }` | `ctx.toast.showToast` (falls back to a CustomEvent / console) — no scope |
|
|
29
|
-
| **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope. `t(key)` resolves the widget-namespaced key (`widget.<id>.<key>`, declared in `manifest.translations`) then
|
|
29
|
+
| **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope. `t(key)` resolves the widget-namespaced key (`widget.<id>.<key>`, declared in `manifest.translations`) first, then a **predefined shared key** (`shared.<key>`) when `key` is one of the standard strings (`submit`, `cancel`, `save`, `loading`, …), then the raw key. Use a shared key for an identical default string so it translates once and any per-instance `widget.<id>.<key>` override still wins. |
|
|
30
30
|
| **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options?)` | `{ data, loading, error, refetch }` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
|
|
31
31
|
| **DATASTORE** | `useDatastoreRecord(table, id)` | `{ data, loading, error, refetch }` | `records(table).get` — `datastore.read:<table>` |
|
|
32
32
|
| **DATASTORE** | `useDatastoreSchema(tableId)` | `{ schema, loading, error, refetch }` | `schema(tableId)` — `datastore.read:<table>` |
|
|
33
33
|
| **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*` |
|
|
34
34
|
| **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) |
|
|
35
|
-
| **FILES** (`ctx.
|
|
35
|
+
| **FILES** (`ctx.assets`) | `useAsset(id)` | `{ url, file, loading, error, refetch }` | `ctx.assets.get` — no scope |
|
|
36
36
|
| **DIRECTORY** (`ctx.directory`) | `useDirectory(query?)` | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
|
|
37
37
|
| **DIRECTORY** | `useUsers(query?)` | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` | `directory.users.*` — `users.read:*` (edits also `users.write:*`; `remove()` also `users.delete:*`) |
|
|
38
38
|
| **DIRECTORY** | `useGroups(query?)` | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
|
|
@@ -47,7 +47,24 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
47
47
|
|
|
48
48
|
## Status
|
|
49
49
|
|
|
50
|
-
`v0.
|
|
50
|
+
`v0.39.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**.
|
|
51
|
+
|
|
52
|
+
### What's new in 0.39.0
|
|
53
|
+
|
|
54
|
+
**Web entry is bundled, not just transpiled (sc-1064).** A first-party / dev widget's WEB entry (`widget.web.jsx`, or the cross-platform `widget.jsx`) is now esbuild-**bundled** by both `appstudio-widget dev` and the publish packer, so vetted web-only deps the host doesn't shim — `react-leaflet`, `leaflet`, its CSS, and its marker PNG assets — are inlined instead of left as bare imports the Studio loader can't resolve. The native entry (`widget.native.jsx`) is still Sucrase transpile-only (Metro bundles its native deps in the export).
|
|
55
|
+
|
|
56
|
+
- **New `./dev-shims` exports**: `hostExternalSpecifiers()` — the canonical, single-source list of bare specifiers the runtime web host resolves (react family, `react-dom` + `react-dom/client`, `@colixsystems/widget-sdk`, and the vetted shimmed packages), i.e. exactly what the bundler externalises; and `REACT_DOM_NAMED_EXPORTS` — the react-dom named surface (`createPortal`, …) the host shim re-exports so `react-leaflet`'s portal-based Popup/Pane share the host's single react-dom instance.
|
|
57
|
+
- **New optional dependency `esbuild`** — lazily imported only on the `dev`/pack bundle paths (same pattern as the optional `sucrase`); the published runtime never forces it.
|
|
58
|
+
|
|
59
|
+
### What's new in 0.38.0
|
|
60
|
+
|
|
61
|
+
**Predefined SHARED translation keys (REQ-L10N-SHARED).** The standard strings the default widgets repeat ("Submit", "Cancel", "Save", "Loading…", …) now have a tenant-wide shared namespace `shared.<key>` so an identical string is translated **once** and every widget that uses it inherits the translation.
|
|
62
|
+
|
|
63
|
+
- **New contract field `CONTRACT.sharedTranslationKeys`** — the predefined map (`{ <key>: { en } }`) and the single source the host seeder reads.
|
|
64
|
+
- **New exported helpers** `sharedTranslationPrefix()` / `sharedTranslationKey(key)` / `isSharedTranslationKey(key)` (from `@colixsystems/widget-sdk/contract`).
|
|
65
|
+
- **`useI18n().t(key)` resolution is now three-step**: the per-widget key (`widget.<id>.<key>`) first, then the shared key (`shared.<key>`) when `key` is one of the predefined shared keys, then the raw key / fallback. So a default widget that calls `t("submit")` picks up the shared translation with no manual key entry, and an author who sets `widget.<id>.submit` in the Translations admin still overrides **that instance only**. An author-invented bare key is never silently shared.
|
|
66
|
+
- **The host auto-registers the shared keys** for a tenant at workspace-content seed time and whenever a marketplace or AI-generated widget is added — idempotent and non-destructive (an admin edit is never overwritten). Authors manage / translate them in the Studio Translations screen like any other key.
|
|
67
|
+
- **`CONTRACT.version` → `1.28.0`** (additive: one new contract field + three helper exports + the `useI18n` shared-key step). No existing export changed signature.
|
|
51
68
|
|
|
52
69
|
### What's new in 0.37.0
|
|
53
70
|
|
|
@@ -175,10 +192,10 @@ Also: `useFileSignatures(fileIds)` is now **self-scoped** (the caller's own sign
|
|
|
175
192
|
|
|
176
193
|
**The data layer splits into four injected domain clients; the SDK becomes core-only.**
|
|
177
194
|
|
|
178
|
-
- **The SDK no longer owns any data facade.** The bespoke per-hook facades that used to live on `WidgetContext` (`ctx.datastore` as an opaque host object, `ctx.directory.listUsers`, `ctx.users`, `ctx.groups`, `ctx.recordPermissions`, `ctx.
|
|
195
|
+
- **The SDK no longer owns any data facade.** The bespoke per-hook facades that used to live on `WidgetContext` (`ctx.datastore` as an opaque host object, `ctx.directory.listUsers`, `ctx.users`, `ctx.groups`, `ctx.recordPermissions`, `ctx.assets.get`) are replaced by four host-instantiated, host-injected domain clients: `ctx.datastore` (`@colixsystems/datastore-client`), `ctx.directory` (`@colixsystems/directory-client`), `ctx.assets` (`@colixsystems/assets-client`, flattened), `ctx.payments` (`@colixsystems/payments-client`). The SDK imports none of them and ships no HTTP.
|
|
179
196
|
- **`ctx.recordPermissions`, `ctx.users`, `ctx.groups` are removed.** Per-record permission management moved under `ctx.datastore.records(tableId).permissions(recordId)`; user / group administration moved under `ctx.directory.users` / `ctx.directory.groups`. The hooks (`useRecordPermissions`, `useUsers`, `useGroups`) keep the same names and signatures — only the client slice they read changed.
|
|
180
197
|
- **snake_case end to end, no client-side transform.** Clients send and return snake_case verbatim (`group_ids`, `can_read`, `is_active`, `amount_cents`, `data_type`, `created_at`, …). The SDK passes bodies straight through and unwraps the `{ data, meta }` list envelope without renaming a single field. Hook return rows and `useUser()` fields are therefore snake_case (`display_name`, `group_ids`, `is_active`, `member_count`, and `useRecordPermissions` rows carry `user_id` / `group_id` / `can_read` / `can_write` / `can_delete` / `can_grant`).
|
|
181
|
-
- **Companion package versions:** `datastore-client 0.5.0`, `
|
|
198
|
+
- **Companion package versions:** `datastore-client 0.5.0`, `assets-client 0.4.0`, `directory-client 0.1.0`, `payments-client 0.1.0`.
|
|
182
199
|
- **`CONTRACT.version` → `1.9.0`.** Breaking for `WidgetContext` consumers (removed slices, renamed wire fields); the hook export surface is unchanged.
|
|
183
200
|
|
|
184
201
|
### What was in 0.18.0
|
|
@@ -217,7 +234,7 @@ The tenant's **Theme Settings** now flow all the way into `useTheme()`.
|
|
|
217
234
|
The "split-implementation + vetted package list" pivot.
|
|
218
235
|
|
|
219
236
|
- **`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.
|
|
220
|
-
- **`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`, `
|
|
237
|
+
- **`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`, `useAsset`, …) for workspace data; use `axios` / `fetch` for third-party APIs.
|
|
221
238
|
- **`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.
|
|
222
239
|
- **`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.
|
|
223
240
|
- **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.
|
|
@@ -247,7 +264,7 @@ The "split-implementation + vetted package list" pivot.
|
|
|
247
264
|
### What's new in 0.12.0
|
|
248
265
|
|
|
249
266
|
- **`useDatastoreRecord(tableId, recordId)` is wired.** Returns `{ data, loading, error, refetch }` for a single record fetched through the host's `records(table).get(id)`. Sister to `useDatastoreQuery`; mirrors its ref discipline so `refetch` stays a stable callback identity. A 404 surfaces as `DatastoreError.code === "NOT_FOUND"`. Additive.
|
|
250
|
-
- **`
|
|
267
|
+
- **`useAsset(fileId)` is wired** + new `WidgetContext.assets` slice. Returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL the widget can drop straight into `<Image source>`. Backed by a new `files.get(fileId)` host facade (web: `widgetHostFiles` through `api/client`; native: `hostFiles` in the export's `widgetHost.js`). Additive.
|
|
251
268
|
|
|
252
269
|
### What's new in 0.11.0
|
|
253
270
|
|
|
@@ -321,7 +338,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
321
338
|
|
|
322
339
|
- `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
|
|
323
340
|
- `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
|
|
324
|
-
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `
|
|
341
|
+
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `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, display_name, roles, group_ids }` (snake_case verbatim; `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). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification }] }` (structure only, no row data; snake_case verbatim) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `useAsset(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).
|
|
325
342
|
- `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.
|
|
326
343
|
- `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.
|
|
327
344
|
- `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
|
|
@@ -337,7 +354,7 @@ A widget that works but looks unfinished is only half done. `useTheme()` is the
|
|
|
337
354
|
- **Contain and elevate.** Wrap a logical unit in a surface: `colors.surface` + padding + `radii.md` + a `colors.border` hairline or a subtle shadow. Use the status roles (`danger / success / warning / info`) for state.
|
|
338
355
|
- **Respond to touch.** Give every `Pressable` a pressed state via the function-style `style={({ pressed }) => [base, pressed && { opacity: 0.7 }]}`.
|
|
339
356
|
- **Use icons for clarity.** Pair a `lucide-react-native` icon with its label at a consistent size, coloured from the theme.
|
|
340
|
-
- **Use imagery deliberately.** Render pictures with the `Image` primitive (`source` takes a URL or `{ uri }`); resolve workspace assets via `
|
|
357
|
+
- **Use imagery deliberately.** Render pictures with the `Image` primitive (`source` takes a URL or `{ uri }`); resolve workspace assets via `useAsset()`. Give every image a sized, `radii`-clipped container so it never renders as a raw rectangle, and never hardcode a credentialed image URL — expose an `image`-type property instead.
|
|
341
358
|
- **Design the empty, loading, and error states.** A blank box on a fresh install reads as broken — show a short helper line when a list is empty, a calm loading line, and a single human sentence in `colors.danger` on error.
|
|
342
359
|
|
|
343
360
|
**Honest ceilings:** the styling surface is React Native style objects, not full CSS. There are no per-widget gradients, no custom CSS keyframe animations or `transition` strings, and shadows are limited to the five elevation presets (`none / sm / md / lg / xl`). Aim for clean, confident, professional polish within those bounds.
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,10 @@ function usage() {
|
|
|
14
14
|
stderr.write(
|
|
15
15
|
"Usage:\n" +
|
|
16
16
|
" appstudio-widget lint <path>\n" +
|
|
17
|
-
" appstudio-widget dev <entry.jsx> [--port <n>] [--manifest <path>]\n"
|
|
17
|
+
" appstudio-widget dev <entry.jsx|widget-dir> [--port <n>] [--manifest <path>]\n" +
|
|
18
|
+
" A directory containing widget.json runs in multi-file mode " +
|
|
19
|
+
"(REQ-WSDK-DEVKIT v2): the dev server reads the canonical web entry, " +
|
|
20
|
+
"serves siblings under /file/<rel>, and watches the whole tree.\n",
|
|
18
21
|
);
|
|
19
22
|
exit(2);
|
|
20
23
|
}
|
|
@@ -93,14 +96,19 @@ async function runDev(rest) {
|
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
stdout.write(
|
|
96
|
-
`\nappstudio-widget dev — serving ${resolve(entry)}
|
|
99
|
+
`\nappstudio-widget dev — serving ${resolve(entry)} ` +
|
|
100
|
+
`(${handle.directoryMode ? "directory" : "single-file"} mode)\n` +
|
|
97
101
|
` bundle: ${handle.url}/widget.mjs\n` +
|
|
98
102
|
` manifest: ${handle.url}/manifest.json\n` +
|
|
99
103
|
` reload: ${handle.url}/__dev/events (SSE)\n` +
|
|
104
|
+
(handle.directoryMode
|
|
105
|
+
? ` files: ${handle.url}/file/<relpath>\n shims: ${handle.url}/shim/<slug>\n`
|
|
106
|
+
: "") +
|
|
100
107
|
(handle.manifestId ? ` id: ${handle.manifestId}\n` : "") +
|
|
101
108
|
`\nIn the Studio Builder (dev mode), open the "Dev widgets" panel and add:\n` +
|
|
102
109
|
` ${handle.url}\n` +
|
|
103
|
-
`Edits to
|
|
110
|
+
`Edits to ${handle.directoryMode ? "any file in the widget directory" : "the entry"} ` +
|
|
111
|
+
`hot-reload the widget on the canvas. Ctrl+C to stop.\n\n`,
|
|
104
112
|
);
|
|
105
113
|
|
|
106
114
|
const shutdown = () => {
|
package/dist/contract.cjs
CHANGED
|
@@ -143,16 +143,16 @@ const HOOKS = [
|
|
|
143
143
|
scopes: ["datastore.read:<table>"],
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
|
-
name: "
|
|
147
|
-
signature: "
|
|
146
|
+
name: "useAsset",
|
|
147
|
+
signature: "useAsset(assetId)",
|
|
148
148
|
returnShape: {
|
|
149
149
|
url: "string | null",
|
|
150
|
-
|
|
150
|
+
asset: "{ id, url, storedFilename, mimeType, sizeBytes, ... } | null",
|
|
151
151
|
loading: "boolean",
|
|
152
152
|
error: "DatastoreError | null",
|
|
153
153
|
refetch: "() => Promise<void>",
|
|
154
154
|
},
|
|
155
|
-
requiredContextSlice: ["
|
|
155
|
+
requiredContextSlice: ["assets.get"],
|
|
156
156
|
scopes: null,
|
|
157
157
|
},
|
|
158
158
|
{
|
|
@@ -900,11 +900,11 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
900
900
|
required: true,
|
|
901
901
|
fields: { users: "object", groups: "object", bankid: "object" },
|
|
902
902
|
},
|
|
903
|
-
|
|
903
|
+
assets: {
|
|
904
904
|
description:
|
|
905
|
-
"Injected @colixsystems/
|
|
906
|
-
"{ get(id) -> Promise<{ id, url, ... }>, list(query) -> Promise<{ data, meta }>, upload(formData)
|
|
907
|
-
"Backs
|
|
905
|
+
"Injected @colixsystems/assets-client instance, FLATTENED so asset ops are top-level. " +
|
|
906
|
+
"{ get(id) -> Promise<{ id, url, ... }>, list(query) -> Promise<{ data, meta }>, upload(formData) }. " +
|
|
907
|
+
"Backs useAsset(); the returned asset already carries an absolute url the widget can drop into an <Image source>.",
|
|
908
908
|
required: true,
|
|
909
909
|
fields: { get: "function" },
|
|
910
910
|
},
|
|
@@ -1245,12 +1245,27 @@ const VETTED_IMPORTS = [
|
|
|
1245
1245
|
},
|
|
1246
1246
|
];
|
|
1247
1247
|
|
|
1248
|
+
// sc-1064: CORE React infrastructure specifiers the host RESOLVES at runtime
|
|
1249
|
+
// but that are NOT author-facing "vetted widget libraries". `react-dom` and
|
|
1250
|
+
// `react-dom/client` are transitive — `react-leaflet`'s Popup/Pane render
|
|
1251
|
+
// through `createPortal` from `react-dom`, so the host externalises + shims
|
|
1252
|
+
// them — but widget authors should never be encouraged to import react-dom
|
|
1253
|
+
// directly (unlike axios/date-fns/leaflet/…). They belong on
|
|
1254
|
+
// `allowedBareImports` (so the loader's shim⊆vetted guard treats them as
|
|
1255
|
+
// vetted) WITHOUT bloating the rich `VETTED_IMPORTS` reference the AI prompt /
|
|
1256
|
+
// Developer guide enumerate — the SAME treatment `react/jsx-runtime` gets, but
|
|
1257
|
+
// kept out of the author-facing list. Mirror of contract.js.
|
|
1258
|
+
const CORE_INFRA_BARE_IMPORTS = ["react-dom", "react-dom/client"];
|
|
1259
|
+
|
|
1248
1260
|
// Back-compat shape — every existing consumer (widgetLoader, the static
|
|
1249
1261
|
// analyzer's DEPENDENCY_ALLOWLIST, the AI prompt's "Allowed imports" line)
|
|
1250
1262
|
// reads a plain `string[]` from `CONTRACT.allowedBareImports`. Derive it
|
|
1251
|
-
// from the rich list so a single edit
|
|
1252
|
-
// every surface.
|
|
1253
|
-
const ALLOWED_BARE_IMPORTS =
|
|
1263
|
+
// from the rich list (plus the CORE infra specifiers above) so a single edit
|
|
1264
|
+
// in VETTED_IMPORTS propagates to every surface.
|
|
1265
|
+
const ALLOWED_BARE_IMPORTS = [
|
|
1266
|
+
...VETTED_IMPORTS.map((v) => v.specifier),
|
|
1267
|
+
...CORE_INFRA_BARE_IMPORTS,
|
|
1268
|
+
];
|
|
1254
1269
|
|
|
1255
1270
|
// REQ-WSDK-PLATFORM §3.5: host-API URL patterns the linter scans for as a
|
|
1256
1271
|
// soft warning. None of these block the lint by themselves — they prompt
|
|
@@ -1285,6 +1300,65 @@ function widgetTranslationKey(id, key) {
|
|
|
1285
1300
|
return `widget.${id}.${key}`;
|
|
1286
1301
|
}
|
|
1287
1302
|
|
|
1303
|
+
// REQ-L10N-SHARED — predefined SHARED translation keys for the standard
|
|
1304
|
+
// strings the default widgets repeat ("Submit", "Cancel", "Loading…", …).
|
|
1305
|
+
// They live in ONE tenant-wide namespace (`shared.<key>`) rather than the
|
|
1306
|
+
// per-widget namespace, so identical strings translate ONCE and every widget
|
|
1307
|
+
// that calls `t("<sharedKey>")` resolves the same dictionary value. An author
|
|
1308
|
+
// still overrides any instance by setting `widget.<id>.<key>` in the
|
|
1309
|
+
// Translations admin — `useI18n` resolves the per-widget key FIRST, then the
|
|
1310
|
+
// shared key, then the raw key, so the override is per-instance.
|
|
1311
|
+
//
|
|
1312
|
+
// This is the analogue of `widget.<id>.` for cross-widget reuse: one prefix,
|
|
1313
|
+
// one definition (the hook reads + the backend seeder writes both call these),
|
|
1314
|
+
// so the wire key and the lookup key can never drift.
|
|
1315
|
+
const SHARED_TRANSLATION_PREFIX = "shared.";
|
|
1316
|
+
function sharedTranslationPrefix() {
|
|
1317
|
+
return SHARED_TRANSLATION_PREFIX;
|
|
1318
|
+
}
|
|
1319
|
+
function sharedTranslationKey(key) {
|
|
1320
|
+
return `${SHARED_TRANSLATION_PREFIX}${key}`;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// The predefined set. Map of bare key -> { en } (English default only; the
|
|
1324
|
+
// tenant adds target-language values once in the Translations admin and every
|
|
1325
|
+
// widget inherits them). Adding/removing a key is a CONTRACT change (it grows
|
|
1326
|
+
// the dictionary every tenant gets), so the list lives with the contract and
|
|
1327
|
+
// the seeder reads it from here — never a second copy.
|
|
1328
|
+
const SHARED_TRANSLATION_KEYS = Object.freeze({
|
|
1329
|
+
submit: { en: "Submit" },
|
|
1330
|
+
cancel: { en: "Cancel" },
|
|
1331
|
+
save: { en: "Save" },
|
|
1332
|
+
delete: { en: "Delete" },
|
|
1333
|
+
edit: { en: "Edit" },
|
|
1334
|
+
close: { en: "Close" },
|
|
1335
|
+
confirm: { en: "Confirm" },
|
|
1336
|
+
back: { en: "Back" },
|
|
1337
|
+
next: { en: "Next" },
|
|
1338
|
+
previous: { en: "Previous" },
|
|
1339
|
+
yes: { en: "Yes" },
|
|
1340
|
+
no: { en: "No" },
|
|
1341
|
+
ok: { en: "OK" },
|
|
1342
|
+
loading: { en: "Loading…" },
|
|
1343
|
+
search: { en: "Search" },
|
|
1344
|
+
no_results: { en: "No results" },
|
|
1345
|
+
required: { en: "Required" },
|
|
1346
|
+
retry: { en: "Retry" },
|
|
1347
|
+
add: { en: "Add" },
|
|
1348
|
+
remove: { en: "Remove" },
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
// True when `key` is one of the predefined shared keys — the discriminator
|
|
1352
|
+
// `useI18n` uses to decide whether to try the `shared.<key>` namespace. A key
|
|
1353
|
+
// the author invents is NOT shared (it stays per-widget) so authors can't
|
|
1354
|
+
// accidentally collide with another widget by reusing a bare name.
|
|
1355
|
+
function isSharedTranslationKey(key) {
|
|
1356
|
+
return (
|
|
1357
|
+
typeof key === "string" &&
|
|
1358
|
+
Object.prototype.hasOwnProperty.call(SHARED_TRANSLATION_KEYS, key)
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1288
1362
|
const CONTRACT = deepFreeze({
|
|
1289
1363
|
// REQ-WSDK-PLATFORM bump:
|
|
1290
1364
|
// - `vettedImports` is a new field (rich allowlist with platforms +
|
|
@@ -1331,7 +1405,7 @@ const CONTRACT = deepFreeze({
|
|
|
1331
1405
|
// records(t).permissions(r) replaces the deleted ctx.recordPermissions),
|
|
1332
1406
|
// ctx.directory is a @colixsystems/directory-client (users/groups
|
|
1333
1407
|
// namespaces replace the deleted ctx.users / ctx.groups; list returns
|
|
1334
|
-
// envelopes), ctx.
|
|
1408
|
+
// envelopes), ctx.assets is a flattened @colixsystems/assets-client,
|
|
1335
1409
|
// ctx.payments is a @colixsystems/payments-client. All rows/bodies are
|
|
1336
1410
|
// snake_case. Hooks now unwrap the list envelope (res.data ?? []). The
|
|
1337
1411
|
// SDK stays duck-typed — it imports none of the four data SDKs. Minor
|
|
@@ -1486,7 +1560,23 @@ const CONTRACT = deepFreeze({
|
|
|
1486
1560
|
// on `react/jsx-runtime`. Adding them converges the linter onto the same
|
|
1487
1561
|
// contract every other surface already honoured (CLAUDE.md §3). No
|
|
1488
1562
|
// existing entry changed shape — minor bump on the pre-1.0 channel.
|
|
1489
|
-
|
|
1563
|
+
//
|
|
1564
|
+
// 1.28.0: additive (REQ-L10N-SHARED) — predefined SHARED translation keys.
|
|
1565
|
+
// A new tenant-wide namespace `shared.<key>` carries the standard strings
|
|
1566
|
+
// the default widgets repeat ("Submit", "Cancel", "Loading…", …) so an
|
|
1567
|
+
// identical string is translated ONCE and every widget that calls
|
|
1568
|
+
// `t("<sharedKey>")` inherits it. New contract field
|
|
1569
|
+
// `sharedTranslationKeys` (the predefined map, the single source the
|
|
1570
|
+
// backend seeder reads) + new exported helpers `sharedTranslationPrefix()`
|
|
1571
|
+
// / `sharedTranslationKey(key)` / `isSharedTranslationKey(key)`.
|
|
1572
|
+
// `useI18n().t(key)` now resolves the per-widget key first, THEN the
|
|
1573
|
+
// shared key (when `key` is one of the predefined shared keys), then the
|
|
1574
|
+
// raw key — so a default widget picks up the shared translation with no
|
|
1575
|
+
// manual key entry, and a `widget.<id>.<key>` override still wins per
|
|
1576
|
+
// instance. No existing hook, primitive, manifest field, or token changed
|
|
1577
|
+
// shape — minor bump on the pre-1.0 channel.
|
|
1578
|
+
version: "1.28.0",
|
|
1579
|
+
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1490
1580
|
hooks: HOOKS,
|
|
1491
1581
|
primitives: PRIMITIVES,
|
|
1492
1582
|
manifestSchema: MANIFEST_SCHEMA,
|
|
@@ -1524,4 +1614,8 @@ module.exports = {
|
|
|
1524
1614
|
requiredContextKeys,
|
|
1525
1615
|
widgetTranslationPrefix,
|
|
1526
1616
|
widgetTranslationKey,
|
|
1617
|
+
sharedTranslationPrefix,
|
|
1618
|
+
sharedTranslationKey,
|
|
1619
|
+
isSharedTranslationKey,
|
|
1620
|
+
SHARED_TRANSLATION_KEYS,
|
|
1527
1621
|
};
|
package/dist/contract.js
CHANGED
|
@@ -143,16 +143,16 @@ const HOOKS = [
|
|
|
143
143
|
scopes: ["datastore.read:<table>"],
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
|
-
name: "
|
|
147
|
-
signature: "
|
|
146
|
+
name: "useAsset",
|
|
147
|
+
signature: "useAsset(assetId)",
|
|
148
148
|
returnShape: {
|
|
149
149
|
url: "string | null",
|
|
150
|
-
|
|
150
|
+
asset: "{ id, url, storedFilename, mimeType, sizeBytes, ... } | null",
|
|
151
151
|
loading: "boolean",
|
|
152
152
|
error: "DatastoreError | null",
|
|
153
153
|
refetch: "() => Promise<void>",
|
|
154
154
|
},
|
|
155
|
-
requiredContextSlice: ["
|
|
155
|
+
requiredContextSlice: ["assets.get"],
|
|
156
156
|
scopes: null,
|
|
157
157
|
},
|
|
158
158
|
{
|
|
@@ -900,11 +900,11 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
900
900
|
required: true,
|
|
901
901
|
fields: { users: "object", groups: "object", bankid: "object" },
|
|
902
902
|
},
|
|
903
|
-
|
|
903
|
+
assets: {
|
|
904
904
|
description:
|
|
905
|
-
"Injected @colixsystems/
|
|
906
|
-
"{ get(id) -> Promise<{ id, url, ... }>, list(query) -> Promise<{ data, meta }>, upload(formData)
|
|
907
|
-
"Backs
|
|
905
|
+
"Injected @colixsystems/assets-client instance, FLATTENED so asset ops are top-level. " +
|
|
906
|
+
"{ get(id) -> Promise<{ id, url, ... }>, list(query) -> Promise<{ data, meta }>, upload(formData) }. " +
|
|
907
|
+
"Backs useAsset(); the returned asset already carries an absolute url the widget can drop into an <Image source>.",
|
|
908
908
|
required: true,
|
|
909
909
|
fields: { get: "function" },
|
|
910
910
|
},
|
|
@@ -1245,12 +1245,27 @@ const VETTED_IMPORTS = [
|
|
|
1245
1245
|
},
|
|
1246
1246
|
];
|
|
1247
1247
|
|
|
1248
|
+
// sc-1064: CORE React infrastructure specifiers the host RESOLVES at runtime
|
|
1249
|
+
// but that are NOT author-facing "vetted widget libraries". `react-dom` and
|
|
1250
|
+
// `react-dom/client` are transitive — `react-leaflet`'s Popup/Pane render
|
|
1251
|
+
// through `createPortal` from `react-dom`, so the host externalises + shims
|
|
1252
|
+
// them — but widget authors should never be encouraged to import react-dom
|
|
1253
|
+
// directly (unlike axios/date-fns/leaflet/…). They belong on
|
|
1254
|
+
// `allowedBareImports` (so the loader's shim⊆vetted guard treats them as
|
|
1255
|
+
// vetted) WITHOUT bloating the rich `VETTED_IMPORTS` reference the AI prompt /
|
|
1256
|
+
// Developer guide enumerate — the SAME treatment `react/jsx-runtime` gets, but
|
|
1257
|
+
// kept out of the author-facing list. Mirror of contract.cjs.
|
|
1258
|
+
const CORE_INFRA_BARE_IMPORTS = ["react-dom", "react-dom/client"];
|
|
1259
|
+
|
|
1248
1260
|
// Back-compat shape — every existing consumer (widgetLoader, the static
|
|
1249
1261
|
// analyzer's DEPENDENCY_ALLOWLIST, the AI prompt's "Allowed imports" line)
|
|
1250
1262
|
// reads a plain `string[]` from `CONTRACT.allowedBareImports`. Derive it
|
|
1251
|
-
// from the rich list so a single edit
|
|
1252
|
-
// every surface.
|
|
1253
|
-
const ALLOWED_BARE_IMPORTS =
|
|
1263
|
+
// from the rich list (plus the CORE infra specifiers above) so a single edit
|
|
1264
|
+
// in VETTED_IMPORTS propagates to every surface.
|
|
1265
|
+
const ALLOWED_BARE_IMPORTS = [
|
|
1266
|
+
...VETTED_IMPORTS.map((v) => v.specifier),
|
|
1267
|
+
...CORE_INFRA_BARE_IMPORTS,
|
|
1268
|
+
];
|
|
1254
1269
|
|
|
1255
1270
|
// REQ-WSDK-PLATFORM §3.5: host-API URL patterns the linter scans for as a
|
|
1256
1271
|
// soft warning. None of these block the lint by themselves — they prompt
|
|
@@ -1285,6 +1300,65 @@ function widgetTranslationKey(id, key) {
|
|
|
1285
1300
|
return `widget.${id}.${key}`;
|
|
1286
1301
|
}
|
|
1287
1302
|
|
|
1303
|
+
// REQ-L10N-SHARED — predefined SHARED translation keys for the standard
|
|
1304
|
+
// strings the default widgets repeat ("Submit", "Cancel", "Loading…", …).
|
|
1305
|
+
// They live in ONE tenant-wide namespace (`shared.<key>`) rather than the
|
|
1306
|
+
// per-widget namespace, so identical strings translate ONCE and every widget
|
|
1307
|
+
// that calls `t("<sharedKey>")` resolves the same dictionary value. An author
|
|
1308
|
+
// still overrides any instance by setting `widget.<id>.<key>` in the
|
|
1309
|
+
// Translations admin — `useI18n` resolves the per-widget key FIRST, then the
|
|
1310
|
+
// shared key, then the raw key, so the override is per-instance.
|
|
1311
|
+
//
|
|
1312
|
+
// This is the analogue of `widget.<id>.` for cross-widget reuse: one prefix,
|
|
1313
|
+
// one definition (the hook reads + the backend seeder writes both call these),
|
|
1314
|
+
// so the wire key and the lookup key can never drift.
|
|
1315
|
+
const SHARED_TRANSLATION_PREFIX = "shared.";
|
|
1316
|
+
function sharedTranslationPrefix() {
|
|
1317
|
+
return SHARED_TRANSLATION_PREFIX;
|
|
1318
|
+
}
|
|
1319
|
+
function sharedTranslationKey(key) {
|
|
1320
|
+
return `${SHARED_TRANSLATION_PREFIX}${key}`;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// The predefined set. Map of bare key -> { en } (English default only; the
|
|
1324
|
+
// tenant adds target-language values once in the Translations admin and every
|
|
1325
|
+
// widget inherits them). Adding/removing a key is a CONTRACT change (it grows
|
|
1326
|
+
// the dictionary every tenant gets), so the list lives with the contract and
|
|
1327
|
+
// the seeder reads it from here — never a second copy.
|
|
1328
|
+
const SHARED_TRANSLATION_KEYS = Object.freeze({
|
|
1329
|
+
submit: { en: "Submit" },
|
|
1330
|
+
cancel: { en: "Cancel" },
|
|
1331
|
+
save: { en: "Save" },
|
|
1332
|
+
delete: { en: "Delete" },
|
|
1333
|
+
edit: { en: "Edit" },
|
|
1334
|
+
close: { en: "Close" },
|
|
1335
|
+
confirm: { en: "Confirm" },
|
|
1336
|
+
back: { en: "Back" },
|
|
1337
|
+
next: { en: "Next" },
|
|
1338
|
+
previous: { en: "Previous" },
|
|
1339
|
+
yes: { en: "Yes" },
|
|
1340
|
+
no: { en: "No" },
|
|
1341
|
+
ok: { en: "OK" },
|
|
1342
|
+
loading: { en: "Loading…" },
|
|
1343
|
+
search: { en: "Search" },
|
|
1344
|
+
no_results: { en: "No results" },
|
|
1345
|
+
required: { en: "Required" },
|
|
1346
|
+
retry: { en: "Retry" },
|
|
1347
|
+
add: { en: "Add" },
|
|
1348
|
+
remove: { en: "Remove" },
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
// True when `key` is one of the predefined shared keys — the discriminator
|
|
1352
|
+
// `useI18n` uses to decide whether to try the `shared.<key>` namespace. A key
|
|
1353
|
+
// the author invents is NOT shared (it stays per-widget) so authors can't
|
|
1354
|
+
// accidentally collide with another widget by reusing a bare name.
|
|
1355
|
+
function isSharedTranslationKey(key) {
|
|
1356
|
+
return (
|
|
1357
|
+
typeof key === "string" &&
|
|
1358
|
+
Object.prototype.hasOwnProperty.call(SHARED_TRANSLATION_KEYS, key)
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1288
1362
|
const CONTRACT = deepFreeze({
|
|
1289
1363
|
// REQ-WSDK-PLATFORM bump:
|
|
1290
1364
|
// - `vettedImports` is a new field (rich allowlist with platforms +
|
|
@@ -1331,7 +1405,7 @@ const CONTRACT = deepFreeze({
|
|
|
1331
1405
|
// records(t).permissions(r) replaces the deleted ctx.recordPermissions),
|
|
1332
1406
|
// ctx.directory is a @colixsystems/directory-client (users/groups
|
|
1333
1407
|
// namespaces replace the deleted ctx.users / ctx.groups; list returns
|
|
1334
|
-
// envelopes), ctx.
|
|
1408
|
+
// envelopes), ctx.assets is a flattened @colixsystems/assets-client,
|
|
1335
1409
|
// ctx.payments is a @colixsystems/payments-client. All rows/bodies are
|
|
1336
1410
|
// snake_case. Hooks now unwrap the list envelope (res.data ?? []). The
|
|
1337
1411
|
// SDK stays duck-typed — it imports none of the four data SDKs. Minor
|
|
@@ -1486,7 +1560,23 @@ const CONTRACT = deepFreeze({
|
|
|
1486
1560
|
// on `react/jsx-runtime`. Adding them converges the linter onto the same
|
|
1487
1561
|
// contract every other surface already honoured (CLAUDE.md §3). No
|
|
1488
1562
|
// existing entry changed shape — minor bump on the pre-1.0 channel.
|
|
1489
|
-
|
|
1563
|
+
//
|
|
1564
|
+
// 1.28.0: additive (REQ-L10N-SHARED) — predefined SHARED translation keys.
|
|
1565
|
+
// A new tenant-wide namespace `shared.<key>` carries the standard strings
|
|
1566
|
+
// the default widgets repeat ("Submit", "Cancel", "Loading…", …) so an
|
|
1567
|
+
// identical string is translated ONCE and every widget that calls
|
|
1568
|
+
// `t("<sharedKey>")` inherits it. New contract field
|
|
1569
|
+
// `sharedTranslationKeys` (the predefined map, the single source the
|
|
1570
|
+
// backend seeder reads) + new exported helpers `sharedTranslationPrefix()`
|
|
1571
|
+
// / `sharedTranslationKey(key)` / `isSharedTranslationKey(key)`.
|
|
1572
|
+
// `useI18n().t(key)` now resolves the per-widget key first, THEN the
|
|
1573
|
+
// shared key (when `key` is one of the predefined shared keys), then the
|
|
1574
|
+
// raw key — so a default widget picks up the shared translation with no
|
|
1575
|
+
// manual key entry, and a `widget.<id>.<key>` override still wins per
|
|
1576
|
+
// instance. No existing hook, primitive, manifest field, or token changed
|
|
1577
|
+
// shape — minor bump on the pre-1.0 channel.
|
|
1578
|
+
version: "1.28.0",
|
|
1579
|
+
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1490
1580
|
hooks: HOOKS,
|
|
1491
1581
|
primitives: PRIMITIVES,
|
|
1492
1582
|
manifestSchema: MANIFEST_SCHEMA,
|
|
@@ -1524,4 +1614,8 @@ export {
|
|
|
1524
1614
|
requiredContextKeys,
|
|
1525
1615
|
widgetTranslationPrefix,
|
|
1526
1616
|
widgetTranslationKey,
|
|
1617
|
+
sharedTranslationPrefix,
|
|
1618
|
+
sharedTranslationKey,
|
|
1619
|
+
isSharedTranslationKey,
|
|
1620
|
+
SHARED_TRANSLATION_KEYS,
|
|
1527
1621
|
};
|