@colixsystems/widget-sdk 0.238.0 → 0.240.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 +26 -6
- package/dist/contract.cjs +9 -2
- package/dist/contract.js +9 -2
- package/dist/index.d.ts +18 -6
- package/dist/property-schema.js +6 -0
- package/package.json +1 -1
- package/src/contract.js +9 -2
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
24
24
|
| **CORE** | `useUser()` | `{ id, email, displayName, photoUrl, roles, groupIds }` | `ctx.user` (host-built context, **camelCase** — not a wire payload; `id` null when anonymous) — no scope. `photoUrl` is the user's own profile photo as an absolute URL, or `null`. |
|
|
25
25
|
| **CORE** | `useUserPhoto()` | `{ url, canEdit, upload, remove, uploading, error }` | `ctx.user` + the optional `ctx.account` — no scope. Changes the signed-in user's OWN profile photo. `canEdit` is false when nobody is signed in or the host cannot change it; `upload` / `remove` then reject. |
|
|
26
26
|
| **CORE** | `useAnonymousAccount()` | `{ supported, canCreate, creating, error, create }` | The optional `ctx.account.anonymous` — no scope. Offers an account with a generated login code and password and no email. `create()` opens the app's own dialog, which confirms, creates the account, shows the pair once and signs the visitor in; the widget never receives the pair and `create()` resolves `{ created: true }`. `supported` is false where the workspace has not turned anonymous accounts on, on the Builder canvas, in a preview and for a badge counter; `canCreate` is false while someone is signed in. |
|
|
27
|
-
| **CORE** | `useNavigation()` | `{ goTo, goToStartPage, goBack, push, replace, back, currentRoute, openLink }` | `ctx.navigation` — no scope. `goTo(pageIdOrSlug, params?)` accepts a page UUID (a `pageRef` prop) **or its slug** (a row's page-key field) — both hosts resolve either; `goToStartPage()` opens the signed-in user's own start page (`openLink` for a link of unknown shape; a known external URL can also use the `Linking` primitive) |
|
|
27
|
+
| **CORE** | `useNavigation()` | `{ goTo, goToStartPage, scrollToSection, goBack, push, replace, back, currentRoute, openLink }` | `ctx.navigation` — no scope. `goTo(pageIdOrSlug, params?)` accepts a page UUID (a `pageRef` prop) **or its slug** (a row's page-key field) — both hosts resolve either; `goToStartPage()` opens the signed-in user's own start page; `scrollToSection(id)` scrolls this page to a section picked with a `sectionRef` prop (`openLink` for a link of unknown shape; a known external URL can also use the `Linking` primitive) |
|
|
28
28
|
| **CORE** | `useFilePreview(file)` | `{ uri }` | No scope. A temporary address an `<Image>` can show for a file the person picked but has not uploaded yet (a photo in a composer). Native picks use their own uri; on the web the SDK creates and releases an object URL, which widgets may not do themselves. `uri` is null for no file. |
|
|
29
29
|
| **CORE** | `usePageBadge(count)` | `{ supported }` | `ctx.navigation.setBadge` — no scope. Puts `count` on the menu entry or tab of the page the widget is on (a chat's unread messages). The host sums the page's widgets and keeps the last count after the widget leaves the screen, until it reports again; `0` clears this widget's share. `supported` is false where the host draws no page badges (the Builder canvas). |
|
|
30
30
|
| **CORE** | `useRouteParams()` | `{ [paramKey]: value }` | `ctx.navigation.currentRoute.params` — no scope. The nav params the previous page passed via `goTo(pageId, params)`; the flat accessor for master→detail (read `recordId` on a detail page). Empty object when none. |
|
|
@@ -84,7 +84,27 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
84
84
|
|
|
85
85
|
## Status
|
|
86
86
|
|
|
87
|
-
`v0.
|
|
87
|
+
`v0.240.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**.
|
|
88
|
+
|
|
89
|
+
### What's new in 0.240.0 (contract 1.194.0)
|
|
90
|
+
|
|
91
|
+
**A `datastoreTemplate` can describe a whole system.** A template may now declare up to 100 tables of up to 200 columns, up to 100 sample `rows` per table and up to 32 `roles`. Sample rows have one more bound: the whole template seeds at most 20000 cells, where each row counts the keys it sets and at least one, summed over every row of every table. A template past any limit is refused at publish with a message naming the limit and the count. Every template valid under 0.239.0 is still valid.
|
|
92
|
+
|
|
93
|
+
### What's new in 0.239.0 (contract 1.193.0)
|
|
94
|
+
|
|
95
|
+
**A widget can scroll its page to a section — `useNavigation().scrollToSection(sectionId)`.** A one-page site jumps from "See pricing" to the pricing section further down. Bind the target with a `sectionRef` property: the Studio renders a picker of the page's sections (its root sections and every layout container), and the value is the id you pass to `scrollToSection`.
|
|
96
|
+
|
|
97
|
+
```jsx
|
|
98
|
+
import { useNavigation, Button } from "@colixsystems/widget-sdk";
|
|
99
|
+
|
|
100
|
+
export default function JumpButton({ label, target }) {
|
|
101
|
+
const { scrollToSection } = useNavigation();
|
|
102
|
+
return <Button title={label} onPress={() => scrollToSection(target)} />;
|
|
103
|
+
}
|
|
104
|
+
// propertySchema: { target: { type: "sectionRef", label: "Section" } }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The web Player and the exported app both scroll smoothly (instantly under reduced motion) and stop with the section just below a pinned site header. An unknown, deleted or hidden section does nothing, and so does the Studio canvas. Additive.
|
|
88
108
|
|
|
89
109
|
### What's new in 0.238.0 (contract 1.192.0)
|
|
90
110
|
|
|
@@ -1585,7 +1605,7 @@ datastoreTemplate: {
|
|
|
1585
1605
|
|
|
1586
1606
|
`roles` is declared ONCE per template and referenced by every table, so three widgets over two tables ask the installing workspace **one** question per audience rather than one per grant. At install they pick one of their own groups for each role, or have one created for them under the role's label.
|
|
1587
1607
|
|
|
1588
|
-
- **`key`** is lowercase-kebab (`/^[a-z][a-z0-9-]*$/`)
|
|
1608
|
+
- **`key`** is lowercase-kebab (`/^[a-z][a-z0-9-]*$/`); the roles cap is in `WidgetDatastoreTemplate`. A `roleGrants` entry naming a role you did not declare is a publish error, as is granting the same role twice on one table or granting it no verb at all.
|
|
1589
1609
|
- **`canDelete` is row-level only.** A table-scope grant answers "may I create a record"; only a record permission answers a question about a row. Both halves are written for you from the one declaration.
|
|
1590
1610
|
- **An unbound role is an ordinary outcome, never a failed install.** If the installer skips a role, no grant is written and they are told which role is still unbound — they can bind it later from the table's permissions.
|
|
1591
1611
|
- **Purely additive.** A template declaring neither `roles` nor `roleGrants` behaves exactly as it does today.
|
|
@@ -2558,7 +2578,7 @@ useEffect(() => {
|
|
|
2558
2578
|
|
|
2559
2579
|
### What's new in 0.51.0
|
|
2560
2580
|
|
|
2561
|
-
**`datastoreTemplate` tables can ship sample `rows` (sc-2070).** Each `WidgetDatastoreTemplateTable` now takes an optional `rows` array — sample data seeded into the table at install time so the widget renders with real content instead of an empty state. Each entry is an object keyed by column `name`; only the scalar/array column types are seedable (`STRING`, `TEXT`, `NUMBER`, `FLOAT`, `BOOL`, `DATE`, `STRING_ARRAY`, `INT_ARRAY`). RELATION, FILE, USER, and USER_GROUP columns are rejected at validation time (a sample row has no way to express their ids). A `null` value skips that cell;
|
|
2581
|
+
**`datastoreTemplate` tables can ship sample `rows` (sc-2070).** Each `WidgetDatastoreTemplateTable` now takes an optional `rows` array — sample data seeded into the table at install time so the widget renders with real content instead of an empty state. Each entry is an object keyed by column `name`; only the scalar/array column types are seedable (`STRING`, `TEXT`, `NUMBER`, `FLOAT`, `BOOL`, `DATE`, `STRING_ARRAY`, `INT_ARRAY`). RELATION, FILE, USER, and USER_GROUP columns are rejected at validation time (a sample row has no way to express their ids). A `null` value skips that cell; the per-table row cap is on `WidgetDatastoreTemplateTable.rows`. The "Seed data" action and every install path seed these rows in the same transaction that creates the tables. Additive — `rows` is optional and existing templates that omit it behave exactly as before.
|
|
2562
2582
|
|
|
2563
2583
|
### What's new in 0.49.0
|
|
2564
2584
|
|
|
@@ -2923,7 +2943,7 @@ The "split-implementation + vetted package list" pivot.
|
|
|
2923
2943
|
|
|
2924
2944
|
### What was in 0.3.0
|
|
2925
2945
|
|
|
2926
|
-
Additive: `WidgetManifest` carries an optional `datastoreTemplate` field. When a tenant installs a widget that declares one, the table set is seeded into their workspace alongside the install. Tables follow the built-in template semantics (auto-suffixed naming, creator-grants, public grants, cross-relation inheritance) and persist when the widget is later uninstalled. See `WidgetDatastoreTemplate` in `src/index.d.ts` for the
|
|
2946
|
+
Additive: `WidgetManifest` carries an optional `datastoreTemplate` field. When a tenant installs a widget that declares one, the table set is seeded into their workspace alongside the install. Tables follow the built-in template semantics (auto-suffixed naming, creator-grants, public grants, cross-relation inheritance) and persist when the widget is later uninstalled. See `WidgetDatastoreTemplate` in `src/index.d.ts` for the current limits; RELATION columns address siblings by `suffix`.
|
|
2927
2947
|
|
|
2928
2948
|
## Public API
|
|
2929
2949
|
|
|
@@ -2933,7 +2953,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
2933
2953
|
|
|
2934
2954
|
- `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
|
|
2935
2955
|
- `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
|
|
2936
|
-
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `useWidgetInput`, `usePayments`, `useSendNotification`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useRouteParams`, `usePageContext`, `useWidgetRoute`, `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` carrying `code`, the server's user-safe `message`, and `retryable` (`false` = this charge cannot succeed until the workspace, manifest, or amount changes — show the message, not a retry). `useSendNotification()` returns `{ send, sending, error }` and requires the `notifications.send:appUser` scope; `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace (cross-workspace `recipient_user_id` is rejected), must be called from an event handler rather than render, and rejects with a `NotificationError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (camelCase — the host-built context object, not a wire payload; `id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goToStartPage, goBack, push, replace, back, currentRoute, openLink }` for internal page navigation; `goToStartPage()` opens the signed-in user's own start page (the page the app opens on for them); `openLink(link)` follows an author- or data-supplied link of unknown shape through the host's shared resolver (in-app page → internal route, off-app http(s) → opened outside, unsafe → refused) and is the safe choice for any value your code did not construct, while `Linking.openURL(url)` is for an external URL you built yourself. `useRouteParams()` returns the current route's params object (`currentRoute.params`) — the flat master→detail accessor; read a param off it (e.g. `recordId`), never call it. `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). `useWidgetInput(inputName)` returns the latest payload a sibling widget published on the event the page author wired to this widget's declared `inputs` entry (`undefined` when unwired or not yet published).
|
|
2956
|
+
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `useWidgetInput`, `usePayments`, `useSendNotification`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useRouteParams`, `usePageContext`, `useWidgetRoute`, `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` carrying `code`, the server's user-safe `message`, and `retryable` (`false` = this charge cannot succeed until the workspace, manifest, or amount changes — show the message, not a retry). `useSendNotification()` returns `{ send, sending, error }` and requires the `notifications.send:appUser` scope; `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace (cross-workspace `recipient_user_id` is rejected), must be called from an event handler rather than render, and rejects with a `NotificationError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (camelCase — the host-built context object, not a wire payload; `id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goToStartPage, scrollToSection, goBack, push, replace, back, currentRoute, openLink }` for internal page navigation; `goToStartPage()` opens the signed-in user's own start page (the page the app opens on for them); `scrollToSection(sectionId)` scrolls the current page to one of its sections (bind the id with a `sectionRef` prop); `openLink(link)` follows an author- or data-supplied link of unknown shape through the host's shared resolver (in-app page → internal route, off-app http(s) → opened outside, unsafe → refused) and is the safe choice for any value your code did not construct, while `Linking.openURL(url)` is for an external URL you built yourself. `useRouteParams()` returns the current route's params object (`currentRoute.params`) — the flat master→detail accessor; read a param off it (e.g. `recordId`), never call it. `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). `useWidgetInput(inputName)` returns the latest payload a sibling widget published on the event the page author wired to this widget's declared `inputs` entry (`undefined` when unwired or not yet published).
|
|
2937
2957
|
- `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.
|
|
2938
2958
|
- `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` on native and renders `<input type="date|time|datetime-local">` directly on web because the RN library has no react-native-web mapping). The web build aliases `react-native` to `react-native-web` so the RN-re-exported primitives 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.
|
|
2939
2959
|
- `Overlay` — the screen-level overlay (sc-6607). `<Overlay visible={!!preview} onRequestClose={() => setPreview(null)} size="full">…</Overlay>` renders its children OUTSIDE the widget's layout box on both hosts (web portals it to the document root, native uses the OS modal), so no clipping card, `ScrollView` or neighbouring widget can cut off a document/media preview, lightbox or confirm dialog — which absolute positioning inside the widget cannot achieve on either platform. Children stay in your own React tree, so state and hooks work normally. `onRequestClose` carries the backdrop press, Escape on web, and the Android back button; `size` is `sm` | `md` (default) | `lg` | `full`; the scrim, surface, radius, padding and elevation come from the workspace theme. An anchored dropdown still belongs inside your own root.
|
package/dist/contract.cjs
CHANGED
|
@@ -1203,6 +1203,8 @@ const HOOKS = [
|
|
|
1203
1203
|
openLink: "(link: string) => boolean",
|
|
1204
1204
|
goToStartPage:
|
|
1205
1205
|
"() => void — opens the signed-in user's own start page, the page the app opens on for them (it can differ per user, since page access is restricted per group). Use it for a 'back to start' or 'home' button instead of hard-coding a page. A no-op where there is no live router (the Studio canvas, a badge counter).",
|
|
1206
|
+
scrollToSection:
|
|
1207
|
+
"(sectionId: string) => void — scrolls THIS page to one of its sections (a root section or a layout container), landing its top just below any pinned site header; smooth unless the visitor prefers reduced motion. Take the id from a `sectionRef` prop the author picks; never hard-code a node id. An unknown, deleted or hidden section is a no-op, and so is the Studio canvas.",
|
|
1206
1208
|
goBack: "() => void",
|
|
1207
1209
|
push: "(pageIdOrSlug: string, params?: object) => void",
|
|
1208
1210
|
replace: "(pageIdOrSlug: string, params?: object) => void",
|
|
@@ -3688,7 +3690,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3688
3690
|
type: "object",
|
|
3689
3691
|
required: false,
|
|
3690
3692
|
description:
|
|
3691
|
-
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits:
|
|
3693
|
+
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 100 tables, 200 columns per table, 100 sample rows per table and 20000 sample cells across the whole template (each row counts the keys it sets, and at least one, summed over every table). RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 32 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
3692
3694
|
},
|
|
3693
3695
|
translations: {
|
|
3694
3696
|
type: "object",
|
|
@@ -6085,7 +6087,12 @@ const CONTRACT = deepFreeze({
|
|
|
6085
6087
|
// on `<FilePicker>` (which gains onFocus/onBlur; `muted` lists it).
|
|
6086
6088
|
// `<List variant="card">`, `<ListRow actions>`, `defaults` on both.
|
|
6087
6089
|
// `<Select multiple values onValuesChange>`.
|
|
6088
|
-
|
|
6090
|
+
// 1.193.0: additive (sc-11821) — `useNavigation().scrollToSection(id)`
|
|
6091
|
+
// scrolls the page to one of its sections on both hosts, and the
|
|
6092
|
+
// `sectionRef` propertySchema type gives the author a section picker.
|
|
6093
|
+
// 1.194.0: more permissive (sc-11945) — a datastoreTemplate takes 100
|
|
6094
|
+
// tables, 200 columns, 100 rows per table, 20000 cells and 32 roles.
|
|
6095
|
+
version: "1.194.0",
|
|
6089
6096
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6090
6097
|
hooks: HOOKS,
|
|
6091
6098
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1203,6 +1203,8 @@ const HOOKS = [
|
|
|
1203
1203
|
openLink: "(link: string) => boolean",
|
|
1204
1204
|
goToStartPage:
|
|
1205
1205
|
"() => void — opens the signed-in user's own start page, the page the app opens on for them (it can differ per user, since page access is restricted per group). Use it for a 'back to start' or 'home' button instead of hard-coding a page. A no-op where there is no live router (the Studio canvas, a badge counter).",
|
|
1206
|
+
scrollToSection:
|
|
1207
|
+
"(sectionId: string) => void — scrolls THIS page to one of its sections (a root section or a layout container), landing its top just below any pinned site header; smooth unless the visitor prefers reduced motion. Take the id from a `sectionRef` prop the author picks; never hard-code a node id. An unknown, deleted or hidden section is a no-op, and so is the Studio canvas.",
|
|
1206
1208
|
goBack: "() => void",
|
|
1207
1209
|
push: "(pageIdOrSlug: string, params?: object) => void",
|
|
1208
1210
|
replace: "(pageIdOrSlug: string, params?: object) => void",
|
|
@@ -3684,7 +3686,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3684
3686
|
type: "object",
|
|
3685
3687
|
required: false,
|
|
3686
3688
|
description:
|
|
3687
|
-
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits:
|
|
3689
|
+
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 100 tables, 200 columns per table, 100 sample rows per table and 20000 sample cells across the whole template (each row counts the keys it sets, and at least one, summed over every table). RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 32 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
3688
3690
|
},
|
|
3689
3691
|
translations: {
|
|
3690
3692
|
type: "object",
|
|
@@ -6081,7 +6083,12 @@ const CONTRACT = deepFreeze({
|
|
|
6081
6083
|
// on `<FilePicker>` (which gains onFocus/onBlur; `muted` lists it).
|
|
6082
6084
|
// `<List variant="card">`, `<ListRow actions>`, `defaults` on both.
|
|
6083
6085
|
// `<Select multiple values onValuesChange>`.
|
|
6084
|
-
|
|
6086
|
+
// 1.193.0: additive (sc-11821) — `useNavigation().scrollToSection(id)`
|
|
6087
|
+
// scrolls the page to one of its sections on both hosts, and the
|
|
6088
|
+
// `sectionRef` propertySchema type gives the author a section picker.
|
|
6089
|
+
// 1.194.0: more permissive (sc-11945) — a datastoreTemplate takes 100
|
|
6090
|
+
// tables, 200 columns, 100 rows per table, 20000 cells and 32 roles.
|
|
6091
|
+
version: "1.194.0",
|
|
6085
6092
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6086
6093
|
hooks: HOOKS,
|
|
6087
6094
|
primitives: PRIMITIVES,
|
package/dist/index.d.ts
CHANGED
|
@@ -39,6 +39,9 @@ export type WidgetPropertyType =
|
|
|
39
39
|
// REQ-WDG-PAGEREF: page picker that emits a bare AppPage UUID; a widget
|
|
40
40
|
// reads it and navigates via useNavigation().goTo(pageId, params).
|
|
41
41
|
| "pageRef"
|
|
42
|
+
// REQ-BLD-19: section picker for the widget's own page → bare node id; a
|
|
43
|
+
// widget scrolls to it via useNavigation().scrollToSection(id).
|
|
44
|
+
| "sectionRef"
|
|
42
45
|
// REQ-WDG-RICHTEXT: rich-text (HTML string) edited via a Tiptap editor.
|
|
43
46
|
| "richText"
|
|
44
47
|
// REQ-WDG-ASSET: file/asset picker → bare File UUID (Asset Manager picker).
|
|
@@ -152,12 +155,15 @@ export interface WidgetInputDescriptor {
|
|
|
152
155
|
* tenant may have authored records into them.
|
|
153
156
|
*
|
|
154
157
|
* The shape mirrors the built-in template registry on the backend, with
|
|
155
|
-
*
|
|
156
|
-
*
|
|
158
|
+
* limits a third-party widget must satisfy (enforced at submission time by
|
|
159
|
+
* the static analyzer):
|
|
157
160
|
*
|
|
158
|
-
* - At most
|
|
159
|
-
*
|
|
160
|
-
* - At most
|
|
161
|
+
* - At most 100 tables per widget.
|
|
162
|
+
* - At most 200 columns per table.
|
|
163
|
+
* - At most 100 sample rows per table, and 20000 sample cells across the
|
|
164
|
+
* whole template (each row counts the keys it sets, and at least one,
|
|
165
|
+
* summed over every table).
|
|
166
|
+
* - At most 32 roles.
|
|
161
167
|
*
|
|
162
168
|
* RELATION columns reference siblings within the same template via
|
|
163
169
|
* `targetSuffix` — that suffix MUST belong to a table in the same array,
|
|
@@ -313,7 +319,9 @@ export interface WidgetDatastoreTemplateTable {
|
|
|
313
319
|
* Optional sample rows seeded into the table at install time so the widget
|
|
314
320
|
* renders with real data instead of an empty state. Each key is a column
|
|
315
321
|
* `name`; only non-relation/file/user columns are seedable (RELATION, FILE,
|
|
316
|
-
* USER, USER_GROUP are rejected). `null` skips that cell. Max
|
|
322
|
+
* USER, USER_GROUP are rejected). `null` skips that cell. Max 100 rows, and
|
|
323
|
+
* each row counts the keys it sets, and at least one, toward the
|
|
324
|
+
* template's 20000-cell budget.
|
|
317
325
|
*/
|
|
318
326
|
rows?: Array<
|
|
319
327
|
Record<string, string | number | boolean | string[] | number[] | null>
|
|
@@ -841,6 +849,7 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
841
849
|
navigation: {
|
|
842
850
|
goTo(pageId: string, params?: Record<string, string>): void;
|
|
843
851
|
goToStartPage(): void;
|
|
852
|
+
scrollToSection(sectionId: string): void;
|
|
844
853
|
goBack(): void;
|
|
845
854
|
push(pageId: string, params?: Record<string, unknown>): void;
|
|
846
855
|
replace(pageId: string, params?: Record<string, unknown>): void;
|
|
@@ -2210,12 +2219,15 @@ export function useFitScreenInsets(): FitScreenInsets;
|
|
|
2210
2219
|
* The host-provided navigation surface. `goTo(pageId, params?)` navigates
|
|
2211
2220
|
* to an internal app page; `goBack()` pops the stack. `goToStartPage()`
|
|
2212
2221
|
* opens the signed-in user's own start page, which can differ per user.
|
|
2222
|
+
* `scrollToSection(sectionId)` scrolls this page to one of its sections (the
|
|
2223
|
+
* id comes from a `sectionRef` prop); an unknown section is a no-op.
|
|
2213
2224
|
* Missing methods degrade to no-ops on the Studio canvas preview where no
|
|
2214
2225
|
* live router is mounted. For external URLs use the `Linking` primitive.
|
|
2215
2226
|
*/
|
|
2216
2227
|
export function useNavigation(): {
|
|
2217
2228
|
goTo(pageId: string, params?: Record<string, unknown>): void;
|
|
2218
2229
|
goToStartPage(): void;
|
|
2230
|
+
scrollToSection(sectionId: string): void;
|
|
2219
2231
|
goBack(): void;
|
|
2220
2232
|
push(pageId: string, params?: Record<string, unknown>): void;
|
|
2221
2233
|
replace(pageId: string, params?: Record<string, unknown>): void;
|
package/dist/property-schema.js
CHANGED
|
@@ -34,6 +34,11 @@ const VALID_TYPES = new Set([
|
|
|
34
34
|
// `useNavigation().goTo(pageId, params)`. Bare uuid string — tenant-copy
|
|
35
35
|
// remaps it through the layout walk's pageId map (see tenant-copy.service.js).
|
|
36
36
|
"pageRef",
|
|
37
|
+
// REQ-BLD-19: `sectionRef` is a section picker for the widget's OWN page,
|
|
38
|
+
// emitting a bare layout node id; a widget scrolls to it via
|
|
39
|
+
// `useNavigation().scrollToSection(id)`. Node ids survive Copy Workspace
|
|
40
|
+
// unchanged, and page duplication remaps them, so no per-prop hook.
|
|
41
|
+
"sectionRef",
|
|
37
42
|
// REQ-WDG-RICHTEXT: `richText` is a rich-text (HTML) string. Renders a
|
|
38
43
|
// Tiptap editor in the Studio Properties Panel; the widget renders the
|
|
39
44
|
// sanitised HTML. Stored as a string, so no tenant-copy remap is needed.
|
|
@@ -153,6 +158,7 @@ function coerceLeaf(def, value, path, errors) {
|
|
|
153
158
|
case "recordBinding":
|
|
154
159
|
case "groupRef":
|
|
155
160
|
case "pageRef":
|
|
161
|
+
case "sectionRef":
|
|
156
162
|
case "agentRef":
|
|
157
163
|
case "richText":
|
|
158
164
|
case "asset":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.240.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|
package/src/contract.js
CHANGED
|
@@ -1203,6 +1203,8 @@ const HOOKS = [
|
|
|
1203
1203
|
openLink: "(link: string) => boolean",
|
|
1204
1204
|
goToStartPage:
|
|
1205
1205
|
"() => void — opens the signed-in user's own start page, the page the app opens on for them (it can differ per user, since page access is restricted per group). Use it for a 'back to start' or 'home' button instead of hard-coding a page. A no-op where there is no live router (the Studio canvas, a badge counter).",
|
|
1206
|
+
scrollToSection:
|
|
1207
|
+
"(sectionId: string) => void — scrolls THIS page to one of its sections (a root section or a layout container), landing its top just below any pinned site header; smooth unless the visitor prefers reduced motion. Take the id from a `sectionRef` prop the author picks; never hard-code a node id. An unknown, deleted or hidden section is a no-op, and so is the Studio canvas.",
|
|
1206
1208
|
goBack: "() => void",
|
|
1207
1209
|
push: "(pageIdOrSlug: string, params?: object) => void",
|
|
1208
1210
|
replace: "(pageIdOrSlug: string, params?: object) => void",
|
|
@@ -3684,7 +3686,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3684
3686
|
type: "object",
|
|
3685
3687
|
required: false,
|
|
3686
3688
|
description:
|
|
3687
|
-
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits:
|
|
3689
|
+
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 100 tables, 200 columns per table, 100 sample rows per table and 20000 sample cells across the whole template (each row counts the keys it sets, and at least one, summed over every table). RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 32 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
3688
3690
|
},
|
|
3689
3691
|
translations: {
|
|
3690
3692
|
type: "object",
|
|
@@ -6081,7 +6083,12 @@ const CONTRACT = deepFreeze({
|
|
|
6081
6083
|
// on `<FilePicker>` (which gains onFocus/onBlur; `muted` lists it).
|
|
6082
6084
|
// `<List variant="card">`, `<ListRow actions>`, `defaults` on both.
|
|
6083
6085
|
// `<Select multiple values onValuesChange>`.
|
|
6084
|
-
|
|
6086
|
+
// 1.193.0: additive (sc-11821) — `useNavigation().scrollToSection(id)`
|
|
6087
|
+
// scrolls the page to one of its sections on both hosts, and the
|
|
6088
|
+
// `sectionRef` propertySchema type gives the author a section picker.
|
|
6089
|
+
// 1.194.0: more permissive (sc-11945) — a datastoreTemplate takes 100
|
|
6090
|
+
// tables, 200 columns, 100 rows per table, 20000 cells and 32 roles.
|
|
6091
|
+
version: "1.194.0",
|
|
6085
6092
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6086
6093
|
hooks: HOOKS,
|
|
6087
6094
|
primitives: PRIMITIVES,
|