@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 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.238.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**.
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-]*$/`), at most 8 roles per template. 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.
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; at most 25 rows per table. 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.
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 constraints — at most 8 tables per widget, 24 columns per table, RELATION columns address siblings by `suffix`.
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: 8 tables, 24 columns per 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 8 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.",
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
- version: "1.192.0",
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: 8 tables, 24 columns per 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 8 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.",
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
- version: "1.192.0",
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
- * two limits a third-party widget must satisfy (enforced at submission
156
- * time by the static analyzer):
158
+ * limits a third-party widget must satisfy (enforced at submission time by
159
+ * the static analyzer):
157
160
  *
158
- * - At most 8 tables per widget. Templates exist to seed schema, not
159
- * to be a full database design tool.
160
- * - At most 24 columns per table. Same reason.
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 25 rows.
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;
@@ -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.238.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: 8 tables, 24 columns per 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 8 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.",
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
- version: "1.192.0",
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,