@colixsystems/widget-sdk 0.223.0 → 0.228.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
@@ -84,7 +84,69 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
84
84
 
85
85
  ## Status
86
86
 
87
- `v0.223.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.228.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.228.0 (contract 1.184.0)
90
+
91
+ **A widget can ask for the whole screen — manifest `prefersFitToScreen` and `useFitScreen()`.** A page section can be set to "Fit to screen": it runs edge to edge across the app's content area (no page margins, no width cap) and takes the rest of the screen below the sections above it, on every screen size, in the Player and in the exported app. Set `prefersFitToScreen: true` in your manifest when your widget is built for that — it scrolls its own content inside the height it is given and keeps its controls in view, like a messenger, a map or a full-height board. In a fit section the host then gives your widget a box exactly the height left; a widget without the field grows with its content there, and the page scrolls. A maker who drops your widget at the top level of a page then gets the setting turned on for that section (they can switch it off, and it stays off), and the AppStudio app-builder fits the section it plans your widget into. Leave it `false` or omit it otherwise.
92
+
93
+ When your widget is that section's surface, `useFill()` is `true` and so is the new `useFitScreen()`. Drop your own frame then — border, rounded corners, shadow — so the widget meets the screen's edges cleanly; the host has already removed your outer margin.
94
+
95
+ ```jsx
96
+ import { useFill, useFitScreen, View } from "@colixsystems/widget-sdk";
97
+
98
+ export default function Board() {
99
+ const fill = useFill();
100
+ const edgeToEdge = useFitScreen();
101
+ return (
102
+ <View style={[fill ? { flex: 1, minHeight: 0 } : { height: 480 }, !edgeToEdge && { borderWidth: 1, borderRadius: 12 }]}>
103
+ {/* ... */}
104
+ </View>
105
+ );
106
+ }
107
+ ```
108
+
109
+ Additive: existing manifests omit the field and read `false`, and a host that never fits a widget leaves both hooks `false`. A published widget that must also run in an app whose Player is older than 0.228.0 reads the hook off a namespace import and treats a missing one as `false` (`import * as sdk from "@colixsystems/widget-sdk"; const useFitScreen = typeof sdk.useFitScreen === "function" ? sdk.useFitScreen : () => false;`, at module level): a named import of an export the host does not have stops the widget loading.
110
+
111
+ ### What's new in 0.227.0 (contract 1.183.0)
112
+
113
+ **Columns the server keeps, and rules every client meets.** A `datastoreTemplate` can now say what a widget used to write itself and the server could not check:
114
+
115
+ - `latestVia: "<relation column on the target that points back>"` on a ONE_TO_ONE / ONE_TO_MANY relation (with `onDelete` left at `SET_NULL`; the relation back must have `inheritAcl: true` and `immutable: true`) keeps it pointing at the newest related row — the newest message of a conversation.
116
+ - `copyOf: { via, column }` keeps a column equal to `column` on the row that latest-row column points at — the last message's text, time and author on the conversation row. Text copied into a STRING column is cut to 140 characters; a copy of an encrypted column must be `encrypted` too, and a copy cannot `inheritAcl`.
117
+
118
+ Both are written only by the server, in the same write as the related row, so a list reads who wrote last, and when, without trusting whoever wrote the parent row. A client value for them is ignored; read them back.
119
+
120
+ A table may also declare `constraints`, checked on every write and refused as `CONSTRAINT_VIOLATION`: `unique` (now with `unlessRelated`, waived for rows whose related row has a yes/no column set), `no-overlap`, `related-open` (no row is added, changed or removed while the related row's column is filled — votes on a closed poll) and `related-index` (a whole number that must be a position in the related row's list — a poll option). Additive.
121
+
122
+ ### What's new in 0.226.0 (contract 1.182.0)
123
+
124
+ **Rank by a column — `useDatastoreAggregate(tableId, { groupBy, order, limit })`.** Group a table by an ordinary column (text, number, yes/no, date or user) and get `groups: [{ group, count, sum }]`, counted on the server over every row the viewer can read, so it stays exact past the 1000-row page a list read returns. A user column's `group` is the user id, so a ranking is one call:
125
+
126
+ ```jsx
127
+ const { groups } = useDatastoreAggregate(props.tableId, {
128
+ groupBy: "Reporter",
129
+ order: "count:desc",
130
+ limit: 10,
131
+ filter: { Reporter: "nempty:" },
132
+ });
133
+ // groups: [{ group: "<user id>", count: 42, sum: 0 }, …]
134
+ ```
135
+
136
+ `order` is `count`, `sum` or `group`, then `:asc` or `:desc` (ascending when left out; by value when there is no `order`); `limit` is 1–1000. Rows with no value form one `Uncategorized` group. Encrypted, array, file and relation columns cannot be grouped.
137
+
138
+ ### What's new in 0.225.0 (contract 1.181.0)
139
+
140
+ **Chart a table over time — `useDatastoreAggregate(tableId, spec)`.** Counts rows per UTC day or ISO week (Monday) on the server, over every row the viewer may read, so the series stays exact past the 1000-row page a list read returns. Every bucket of the range comes back, empty ones as `count: 0`, so a chart has no gaps. `groupBy` is `"created_at"` (new rows, the default) or `"updated_at"` (changed rows); any other column is refused with a `VALIDATION` error. `sumColumn` adds up a plain number column per bucket. At most 31 buckets.
141
+
142
+ ```jsx
143
+ const { buckets, loading } = useDatastoreAggregate(props.tableId, { bucket: "week", from: twelveWeeksAgo });
144
+ // buckets: [{ date: "2026-09-28", count: 14, sum: 0 }, …]
145
+ ```
146
+
147
+ ### What's new in 0.224.0 (contract 1.180.0)
148
+
149
+ **A logged-out visitor can attach files to the row they just created.** On a table that lets everyone create rows, the create → `upload(file, { recordId: row.id })` → `update(row.id, { File: stored.id })` flow now works without signing in, for a few minutes after the create. The host carries a grant for that one row from the create to its upload and update, so widget code does not change. The visitor cannot edit the row's other fields, read the file back on a private table, attach more than ten files, or attach to any other row; editing an existing row's files still needs a signed-in user. Additive.
88
150
 
89
151
  ### What's new in 0.223.0 (contract 1.179.0)
90
152
 
package/dist/contract.cjs CHANGED
@@ -991,6 +991,24 @@ const HOOKS = [
991
991
  requiredContextSlice: ["fill"],
992
992
  scopes: null,
993
993
  },
994
+ {
995
+ name: "useFitScreen",
996
+ signature: "useFitScreen()",
997
+ description:
998
+ "Returns true when this widget is the surface of a page section set to " +
999
+ "\"Fit to screen\": the section runs edge to edge across the app's " +
1000
+ "content area and takes the rest of the screen, so the widget should " +
1001
+ "drop its own frame (border, rounded corners, shadow) and meet the " +
1002
+ "edges cleanly. useFill() is true as well then, and the host has already " +
1003
+ "removed the widget's outer margin. Defaults to false wherever the host " +
1004
+ "has not fitted the widget, so calling it is always safe. The SAME value " +
1005
+ "is injected by the web Player and the native export.",
1006
+ returnShape: {
1007
+ "(returns)": "boolean",
1008
+ },
1009
+ requiredContextSlice: ["fitScreen"],
1010
+ scopes: null,
1011
+ },
994
1012
  {
995
1013
  name: "useChildRenderer",
996
1014
  signature: "useChildRenderer()",
@@ -1397,7 +1415,8 @@ const HOOKS = [
1397
1415
  "create the row, call upload(file, { recordId: row.id }) — the per-call " +
1398
1416
  "recordId names the row the hook could not know at render — then write " +
1399
1417
  "the returned id into the FILE column with update(). A logged-out " +
1400
- "visitor cannot upload into a record space. For a file that belongs to " +
1418
+ "visitor may do this only for a row they just created, within a few " +
1419
+ "minutes, and never edits other rows' files. For a file that belongs to " +
1401
1420
  "no row: 'public' for content the app shows to anyone browsing, " +
1402
1421
  "'project' for signed-in workspace users only, 'personal' for a file " +
1403
1422
  "private to the uploading app user.",
@@ -1796,6 +1815,45 @@ const HOOKS = [
1796
1815
  requiredContextSlice: ["datastore.records"],
1797
1816
  scopes: ["datastore.read:*"],
1798
1817
  },
1818
+ // sc-10942 — rows per day or week, counted on the server over every row the
1819
+ // viewer may read. Date columns only until column grouping is exact too.
1820
+ {
1821
+ name: "useDatastoreAggregate",
1822
+ signature:
1823
+ "useDatastoreAggregate(tableId, { groupBy, bucket, from, to, sumColumn, filter, order, limit })",
1824
+ optionalArgs: ["groupBy", "bucket", "from", "to", "sumColumn", "filter", "order", "limit"],
1825
+ description:
1826
+ "Counts rows on the server over EVERY row the viewer can read, so it is " +
1827
+ "exact past the 1000-row page a list read returns — never group " +
1828
+ "useDatastoreQuery rows by hand. With `sumColumn` it also totals a plain " +
1829
+ "number column. BY DATE (`groupBy` \"created_at\", the default, or " +
1830
+ "\"updated_at\"): a SERIES for a chart, one entry per UTC day or ISO week " +
1831
+ "(Monday) in `buckets`; `bucket` is \"day\" (default) or \"week\"; " +
1832
+ "`from` / `to` are ISO dates (default: the 30 days, or 12 weeks, up to " +
1833
+ "now), at most 31 buckets, empty ones as count 0 so a chart has no gaps. " +
1834
+ "BY A COLUMN (any other `groupBy`: a text, number, yes/no, date or user " +
1835
+ "column): one entry per value in `groups`. A USER column's `group` is the " +
1836
+ "user id (resolve names with useDirectory({ ids })), so `order: \"count:desc\", " +
1837
+ "limit: 10` ranks the ten people with the most rows. `order` is count, sum " +
1838
+ "or group, then :asc or :desc (default: by value); `limit` is 1-1000. Rows " +
1839
+ "with no value form one \"Uncategorized\" group; on a user column, leave " +
1840
+ "them out with a `nempty:` filter. Encrypted, array, file and relation " +
1841
+ "columns cannot be grouped (VALIDATION). `filter` takes the same shapes " +
1842
+ "as useDatastoreQuery's, created_at ranges included. Pass null as the " +
1843
+ "table until a bound table is known.",
1844
+ returnShape: {
1845
+ buckets:
1846
+ "Array<{ date, count, sum }> // date = the bucket's start, YYYY-MM-DD; sum is 0 without sumColumn",
1847
+ groups:
1848
+ "Array<{ group, count, sum }> // by a column; a USER column's group is the user id",
1849
+ loading: "boolean",
1850
+ fetching: "boolean",
1851
+ error: "DatastoreError | null",
1852
+ refetch: "() => Promise<void>",
1853
+ },
1854
+ requiredContextSlice: ["datastore.records"],
1855
+ scopes: ["datastore.read:*"],
1856
+ },
1799
1857
  {
1800
1858
  name: "useDatastoreSchema",
1801
1859
  signature: "useDatastoreSchema(tableId)",
@@ -3532,6 +3590,13 @@ const MANIFEST_SCHEMA = {
3532
3590
  "Optional (default false). TRUE when every platform entry also exports useBadgeCount(props): a hook returning the count the widget puts on its page's menu entry with usePageBadge, so the app keeps that badge live while the page is closed. The host runs counters for installed marketplace widgets (the workspace's own published widgets included); the platform's built-in widgets declare none. It calls the hook with the instance's resolved props, inside the widget's own context; the hook returns a whole number, or null while the count is not known yet. It only reads, and the host enforces that: the counter's datastore and directory expose only their read methods (plus realtime subscriptions) and notification mutes only get/list, while writes, typing signals, payments, notifications.send, files, assets, AI agents, translation, link previews, device capabilities, identification and account actions all answer as unavailable. It also renders nothing, navigates nowhere, emits no events or toasts, marks nothing read, and calls none of usePageBadge, useScreenTitle, useSectionEmpty or useWidgetRoute's setter. The linter refuses a declared counter whose entry has no useBadgeCount export. The AppStudio widget agent leaves this unset: a generated widget never declares a badge counter.",
3533
3591
  default: false,
3534
3592
  },
3593
+ prefersFitToScreen: {
3594
+ type: "boolean",
3595
+ required: false,
3596
+ description:
3597
+ "Optional (default false). TRUE when the widget is built to own the rest of the screen: it scrolls its own content and keeps its controls in view, like a messenger, a map or a full-height board. Dropping the widget at the top level of a page in the Builder turns that section's \"Fit to screen\" setting on (the maker can switch it off, and it stays off), and the app-builder fits the section it plans the widget into. A fit section runs edge to edge across the content area and takes the rest of the screen; the widget learns it through useFill() and useFitScreen(), and the host gives it a box exactly the height left, which it scrolls inside (a widget without the field grows with its content there, and the page scrolls). Set it only for a widget that honours useFill() and scrolls inside the height it is given; leave it false (or omit) for everything else.",
3598
+ default: false,
3599
+ },
3535
3600
  events: {
3536
3601
  type: "object[]",
3537
3602
  required: true,
@@ -3563,7 +3628,7 @@ const MANIFEST_SCHEMA = {
3563
3628
  type: "object",
3564
3629
  required: false,
3565
3630
  description:
3566
- "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.",
3631
+ "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.",
3567
3632
  },
3568
3633
  translations: {
3569
3634
  type: "object",
@@ -3607,6 +3672,16 @@ const WIDGET_CONTEXT_SHAPE = {
3607
3672
  required: false,
3608
3673
  fields: {},
3609
3674
  },
3675
+ // sc-11348 — optional host hint backing useFitScreen(): the widget is the
3676
+ // surface of a section set to Fit to screen, so it drops its own frame.
3677
+ fitScreen: {
3678
+ description:
3679
+ "Optional host layout hint (boolean). True when this widget is the " +
3680
+ "surface of a page section set to Fit to screen: edge to edge, the rest " +
3681
+ "of the screen. Backs useFitScreen(); defaults to false when absent.",
3682
+ required: false,
3683
+ fields: {},
3684
+ },
3610
3685
  user: {
3611
3686
  description:
3612
3687
  "Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. email is null for an account created without one. Not a data-client.",
@@ -5882,7 +5957,28 @@ const CONTRACT = deepFreeze({
5882
5957
  // `{ colors, onImage }`: the theme colours as they read over the picture.
5883
5958
  // 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
5884
5959
  // the signed-in user's own start page on both hosts.
5885
- version: "1.179.0",
5960
+ // 1.180.0: additive (sc-11244) — a logged-out visitor may attach files to
5961
+ // the row they just created, for a few minutes: the host carries a grant
5962
+ // from the create to that row's upload and update. Widget code is the
5963
+ // same create → upload → update flow.
5964
+ // 1.181.0: additive (sc-10942) — `useDatastoreAggregate(tableId, spec)`
5965
+ // counts rows per day or week on the server (date columns only), through
5966
+ // `records(t).aggregate`'s new bucket/from/to.
5967
+ // 1.182.0: additive (sc-10737) — `useDatastoreAggregate` groups by an
5968
+ // ordinary column too, into `groups` ([{ group, count, sum }]), with
5969
+ // `order` and `limit` for a top N; counted on the server over every
5970
+ // readable row. A user column groups by user id.
5971
+ // 1.183.0: additive (sc-10766) — a datastoreTemplate column may declare
5972
+ // `latestVia` (a server-kept pointer to the newest related row) or
5973
+ // `copyOf: { via, column }` (a server-kept copy read through one), and
5974
+ // a table may declare `constraints` (`unique` with `unlessRelated`,
5975
+ // `no-overlap`, `related-open`, `related-index`).
5976
+ // 1.184.0: additive (sc-11348) — OPTIONAL manifest field
5977
+ // `prefersFitToScreen` (dropping the widget at a page's top level turns
5978
+ // that section's Fit to screen on), and `useFitScreen()` with its
5979
+ // optional `fitScreen` host hint: the widget is a fit section's surface,
5980
+ // so it drops its own frame. Existing manifests and hosts read false.
5981
+ version: "1.184.0",
5886
5982
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
5887
5983
  hooks: HOOKS,
5888
5984
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -991,6 +991,24 @@ const HOOKS = [
991
991
  requiredContextSlice: ["fill"],
992
992
  scopes: null,
993
993
  },
994
+ {
995
+ name: "useFitScreen",
996
+ signature: "useFitScreen()",
997
+ description:
998
+ "Returns true when this widget is the surface of a page section set to " +
999
+ "\"Fit to screen\": the section runs edge to edge across the app's " +
1000
+ "content area and takes the rest of the screen, so the widget should " +
1001
+ "drop its own frame (border, rounded corners, shadow) and meet the " +
1002
+ "edges cleanly. useFill() is true as well then, and the host has already " +
1003
+ "removed the widget's outer margin. Defaults to false wherever the host " +
1004
+ "has not fitted the widget, so calling it is always safe. The SAME value " +
1005
+ "is injected by the web Player and the native export.",
1006
+ returnShape: {
1007
+ "(returns)": "boolean",
1008
+ },
1009
+ requiredContextSlice: ["fitScreen"],
1010
+ scopes: null,
1011
+ },
994
1012
  {
995
1013
  name: "useChildRenderer",
996
1014
  signature: "useChildRenderer()",
@@ -1397,7 +1415,8 @@ const HOOKS = [
1397
1415
  "create the row, call upload(file, { recordId: row.id }) — the per-call " +
1398
1416
  "recordId names the row the hook could not know at render — then write " +
1399
1417
  "the returned id into the FILE column with update(). A logged-out " +
1400
- "visitor cannot upload into a record space. For a file that belongs to " +
1418
+ "visitor may do this only for a row they just created, within a few " +
1419
+ "minutes, and never edits other rows' files. For a file that belongs to " +
1401
1420
  "no row: 'public' for content the app shows to anyone browsing, " +
1402
1421
  "'project' for signed-in workspace users only, 'personal' for a file " +
1403
1422
  "private to the uploading app user.",
@@ -1796,6 +1815,45 @@ const HOOKS = [
1796
1815
  requiredContextSlice: ["datastore.records"],
1797
1816
  scopes: ["datastore.read:*"],
1798
1817
  },
1818
+ // sc-10942 — rows per day or week, counted on the server over every row the
1819
+ // viewer may read. Date columns only until column grouping is exact too.
1820
+ {
1821
+ name: "useDatastoreAggregate",
1822
+ signature:
1823
+ "useDatastoreAggregate(tableId, { groupBy, bucket, from, to, sumColumn, filter, order, limit })",
1824
+ optionalArgs: ["groupBy", "bucket", "from", "to", "sumColumn", "filter", "order", "limit"],
1825
+ description:
1826
+ "Counts rows on the server over EVERY row the viewer can read, so it is " +
1827
+ "exact past the 1000-row page a list read returns — never group " +
1828
+ "useDatastoreQuery rows by hand. With `sumColumn` it also totals a plain " +
1829
+ "number column. BY DATE (`groupBy` \"created_at\", the default, or " +
1830
+ "\"updated_at\"): a SERIES for a chart, one entry per UTC day or ISO week " +
1831
+ "(Monday) in `buckets`; `bucket` is \"day\" (default) or \"week\"; " +
1832
+ "`from` / `to` are ISO dates (default: the 30 days, or 12 weeks, up to " +
1833
+ "now), at most 31 buckets, empty ones as count 0 so a chart has no gaps. " +
1834
+ "BY A COLUMN (any other `groupBy`: a text, number, yes/no, date or user " +
1835
+ "column): one entry per value in `groups`. A USER column's `group` is the " +
1836
+ "user id (resolve names with useDirectory({ ids })), so `order: \"count:desc\", " +
1837
+ "limit: 10` ranks the ten people with the most rows. `order` is count, sum " +
1838
+ "or group, then :asc or :desc (default: by value); `limit` is 1-1000. Rows " +
1839
+ "with no value form one \"Uncategorized\" group; on a user column, leave " +
1840
+ "them out with a `nempty:` filter. Encrypted, array, file and relation " +
1841
+ "columns cannot be grouped (VALIDATION). `filter` takes the same shapes " +
1842
+ "as useDatastoreQuery's, created_at ranges included. Pass null as the " +
1843
+ "table until a bound table is known.",
1844
+ returnShape: {
1845
+ buckets:
1846
+ "Array<{ date, count, sum }> // date = the bucket's start, YYYY-MM-DD; sum is 0 without sumColumn",
1847
+ groups:
1848
+ "Array<{ group, count, sum }> // by a column; a USER column's group is the user id",
1849
+ loading: "boolean",
1850
+ fetching: "boolean",
1851
+ error: "DatastoreError | null",
1852
+ refetch: "() => Promise<void>",
1853
+ },
1854
+ requiredContextSlice: ["datastore.records"],
1855
+ scopes: ["datastore.read:*"],
1856
+ },
1799
1857
  {
1800
1858
  name: "useDatastoreSchema",
1801
1859
  signature: "useDatastoreSchema(tableId)",
@@ -3528,6 +3586,13 @@ const MANIFEST_SCHEMA = {
3528
3586
  "Optional (default false). TRUE when every platform entry also exports useBadgeCount(props): a hook returning the count the widget puts on its page's menu entry with usePageBadge, so the app keeps that badge live while the page is closed. The host runs counters for installed marketplace widgets (the workspace's own published widgets included); the platform's built-in widgets declare none. It calls the hook with the instance's resolved props, inside the widget's own context; the hook returns a whole number, or null while the count is not known yet. It only reads, and the host enforces that: the counter's datastore and directory expose only their read methods (plus realtime subscriptions) and notification mutes only get/list, while writes, typing signals, payments, notifications.send, files, assets, AI agents, translation, link previews, device capabilities, identification and account actions all answer as unavailable. It also renders nothing, navigates nowhere, emits no events or toasts, marks nothing read, and calls none of usePageBadge, useScreenTitle, useSectionEmpty or useWidgetRoute's setter. The linter refuses a declared counter whose entry has no useBadgeCount export. The AppStudio widget agent leaves this unset: a generated widget never declares a badge counter.",
3529
3587
  default: false,
3530
3588
  },
3589
+ prefersFitToScreen: {
3590
+ type: "boolean",
3591
+ required: false,
3592
+ description:
3593
+ "Optional (default false). TRUE when the widget is built to own the rest of the screen: it scrolls its own content and keeps its controls in view, like a messenger, a map or a full-height board. Dropping the widget at the top level of a page in the Builder turns that section's \"Fit to screen\" setting on (the maker can switch it off, and it stays off), and the app-builder fits the section it plans the widget into. A fit section runs edge to edge across the content area and takes the rest of the screen; the widget learns it through useFill() and useFitScreen(), and the host gives it a box exactly the height left, which it scrolls inside (a widget without the field grows with its content there, and the page scrolls). Set it only for a widget that honours useFill() and scrolls inside the height it is given; leave it false (or omit) for everything else.",
3594
+ default: false,
3595
+ },
3531
3596
  events: {
3532
3597
  type: "object[]",
3533
3598
  required: true,
@@ -3559,7 +3624,7 @@ const MANIFEST_SCHEMA = {
3559
3624
  type: "object",
3560
3625
  required: false,
3561
3626
  description:
3562
- "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.",
3627
+ "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.",
3563
3628
  },
3564
3629
  translations: {
3565
3630
  type: "object",
@@ -3603,6 +3668,16 @@ const WIDGET_CONTEXT_SHAPE = {
3603
3668
  required: false,
3604
3669
  fields: {},
3605
3670
  },
3671
+ // sc-11348 — optional host hint backing useFitScreen(): the widget is the
3672
+ // surface of a section set to Fit to screen, so it drops its own frame.
3673
+ fitScreen: {
3674
+ description:
3675
+ "Optional host layout hint (boolean). True when this widget is the " +
3676
+ "surface of a page section set to Fit to screen: edge to edge, the rest " +
3677
+ "of the screen. Backs useFitScreen(); defaults to false when absent.",
3678
+ required: false,
3679
+ fields: {},
3680
+ },
3606
3681
  user: {
3607
3682
  description:
3608
3683
  "Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. email is null for an account created without one. Not a data-client.",
@@ -5878,7 +5953,28 @@ const CONTRACT = deepFreeze({
5878
5953
  // `{ colors, onImage }`: the theme colours as they read over the picture.
5879
5954
  // 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
5880
5955
  // the signed-in user's own start page on both hosts.
5881
- version: "1.179.0",
5956
+ // 1.180.0: additive (sc-11244) — a logged-out visitor may attach files to
5957
+ // the row they just created, for a few minutes: the host carries a grant
5958
+ // from the create to that row's upload and update. Widget code is the
5959
+ // same create → upload → update flow.
5960
+ // 1.181.0: additive (sc-10942) — `useDatastoreAggregate(tableId, spec)`
5961
+ // counts rows per day or week on the server (date columns only), through
5962
+ // `records(t).aggregate`'s new bucket/from/to.
5963
+ // 1.182.0: additive (sc-10737) — `useDatastoreAggregate` groups by an
5964
+ // ordinary column too, into `groups` ([{ group, count, sum }]), with
5965
+ // `order` and `limit` for a top N; counted on the server over every
5966
+ // readable row. A user column groups by user id.
5967
+ // 1.183.0: additive (sc-10766) — a datastoreTemplate column may declare
5968
+ // `latestVia` (a server-kept pointer to the newest related row) or
5969
+ // `copyOf: { via, column }` (a server-kept copy read through one), and
5970
+ // a table may declare `constraints` (`unique` with `unlessRelated`,
5971
+ // `no-overlap`, `related-open`, `related-index`).
5972
+ // 1.184.0: additive (sc-11348) — OPTIONAL manifest field
5973
+ // `prefersFitToScreen` (dropping the widget at a page's top level turns
5974
+ // that section's Fit to screen on), and `useFitScreen()` with its
5975
+ // optional `fitScreen` host hint: the widget is a fit section's surface,
5976
+ // so it drops its own frame. Existing manifests and hosts read false.
5977
+ version: "1.184.0",
5882
5978
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
5883
5979
  hooks: HOOKS,
5884
5980
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -488,6 +488,19 @@ export function useFill() {
488
488
  return ctx.fill === true;
489
489
  }
490
490
 
491
+ /**
492
+ * Returns `true` when this widget is the surface of a page section set to
493
+ * "Fit to screen": the section runs edge to edge across the app's content
494
+ * area and takes the rest of the screen. Drop the widget's own frame then —
495
+ * border, rounded corners, shadow — so it meets the edges cleanly; `useFill()`
496
+ * is `true` as well, and the host has already removed the outer margin.
497
+ * Defaults to `false`, and both hosts inject the same value.
498
+ */
499
+ export function useFitScreen() {
500
+ const ctx = useWidgetContextOrThrow("useFitScreen");
501
+ return ctx.fitScreen === true;
502
+ }
503
+
491
504
  /**
492
505
  * Returns the host's child-node renderer:
493
506
  * { renderNode(node) }.
@@ -2706,7 +2719,8 @@ function isRetryableDomainStatus(status) {
2706
2719
  return status >= 500;
2707
2720
  }
2708
2721
 
2709
- function toDatastoreError(err) {
2722
+ // Internal: shared with stats-hooks.js, not re-exported by the index.
2723
+ export function toDatastoreError(err) {
2710
2724
  // An already-mapped SDK error passes straight through. NOTE: the datastore
2711
2725
  // CLIENT exports its own class of the same name, so a client rejection does
2712
2726
  // NOT match here — it is handled by the envelope read below, which is exactly
@@ -4804,7 +4818,8 @@ export function useFilestoreUpload(options) {
4804
4818
  setUploading(true);
4805
4819
  setError(null);
4806
4820
  try {
4807
- const created = await filesRef.current.upload(form);
4821
+ // sc-11244: names the row, so a visitor's grant for it can travel.
4822
+ const created = await filesRef.current.upload(form, isRecordSpace ? { recordId: ownerId } : undefined);
4808
4823
  setLastUploaded(created || null);
4809
4824
  setUploading(false);
4810
4825
  return created;
package/dist/index.d.ts CHANGED
@@ -228,8 +228,59 @@ export interface WidgetDatastoreTemplateColumn {
228
228
  * `MANY_TO_MANY` relation, whose links live outside the row.
229
229
  */
230
230
  immutable?: boolean;
231
+ /**
232
+ * ONE_TO_ONE / ONE_TO_MANY RELATION only, with `onDelete` left at
233
+ * `SET_NULL`: the server keeps this column pointing at the newest row of its
234
+ * target table whose relation column named here points back at this row.
235
+ * That relation must have `inheritAcl: true`, so only rows this row's
236
+ * readers can read are followed, and `immutable: true`, so a row never
237
+ * moves under another parent. Written only by the server; any value a
238
+ * client sends is ignored.
239
+ */
240
+ latestVia?: string;
241
+ /**
242
+ * The server keeps this column equal to `column` on the row the latest-row
243
+ * column `via` (on this table) points at: the same type, or text into
244
+ * text, cut to 140 characters in a STRING column. A copy of an encrypted
245
+ * column must be `encrypted` too, and a copy cannot `inheritAcl`. Written
246
+ * only by the server, so a list can show who wrote the newest row, and
247
+ * when, without trusting whoever wrote this one.
248
+ */
249
+ copyOf?: { via: string; column: string };
231
250
  }
232
251
 
252
+ /**
253
+ * A rule the server checks on every write to the table, for every client. A
254
+ * refused write is a `CONSTRAINT_VIOLATION`.
255
+ */
256
+ export type WidgetDatastoreTemplateConstraint =
257
+ | {
258
+ /** No two rows share these values; `unlessRelated` waives it for rows
259
+ * whose related row has that yes/no column set. */
260
+ type: "unique";
261
+ config: {
262
+ columns: string[];
263
+ partitionColumns?: string[];
264
+ unlessRelated?: { relation: string; column: string };
265
+ };
266
+ }
267
+ | {
268
+ /** No two rows' `startColumn`..`endColumn` intervals overlap. */
269
+ type: "no-overlap";
270
+ config: { startColumn: string; endColumn: string; partitionColumns?: string[] };
271
+ }
272
+ | {
273
+ /** No row is added, changed or removed while its related row's
274
+ * `closedColumn` is filled (a yes/no column: while it is yes). */
275
+ type: "related-open";
276
+ config: { relation: string; closedColumn: string };
277
+ }
278
+ | {
279
+ /** `column` must be a position (0-based) in the related row's list. */
280
+ type: "related-index";
281
+ config: { column: string; relation: string; listColumn: string };
282
+ };
283
+
233
284
  export interface WidgetDatastoreTemplateTable {
234
285
  /** Stable identifier within the template; appears in the created table name. */
235
286
  suffix: string;
@@ -267,6 +318,8 @@ export interface WidgetDatastoreTemplateTable {
267
318
  rows?: Array<
268
319
  Record<string, string | number | boolean | string[] | number[] | null>
269
320
  >;
321
+ /** Rules the server checks on every write, by column name. */
322
+ constraints?: WidgetDatastoreTemplateConstraint[];
270
323
  }
271
324
 
272
325
  /**
@@ -409,6 +462,13 @@ export interface WidgetManifest {
409
462
  * while the widget's page is closed so its menu badge stays live.
410
463
  */
411
464
  badgeCounter?: boolean;
465
+ /**
466
+ * Optional (sc-11348, default false). True when the widget owns the rest of
467
+ * the screen (a messenger, a map): dropping it at the top level of a page
468
+ * turns that section's "Fit to screen" on. Honour `useFill()` and
469
+ * `useFitScreen()` with it.
470
+ */
471
+ prefersFitToScreen?: boolean;
412
472
  }
413
473
 
414
474
  /**
@@ -733,6 +793,12 @@ export interface WidgetContext<TProps = unknown> {
733
793
  * Absent / `false` everywhere the host has not opted the widget into filling.
734
794
  */
735
795
  fill?: boolean;
796
+ /**
797
+ * sc-11348 — optional host hint backing `useFitScreen()`: `true` when this
798
+ * widget is the surface of a section set to Fit to screen (edge to edge,
799
+ * the rest of the screen). Absent / `false` everywhere else.
800
+ */
801
+ fitScreen?: boolean;
736
802
  /** Active end-user identity from the host-built context (camelCase, not a wire payload). `id` is null when anonymous. */
737
803
  user: {
738
804
  id: string | null;
@@ -1048,6 +1114,65 @@ export function useDatastoreFigure(
1048
1114
  options?: FigureOptions,
1049
1115
  ): FigureResult;
1050
1116
 
1117
+ export interface AggregateBucketSpec {
1118
+ /** Bucket a system date column: `created_at` (default) or `updated_at`. */
1119
+ groupBy?: "created_at" | "updated_at";
1120
+ /** UTC days or ISO weeks (Monday); `day` by default. */
1121
+ bucket?: "day" | "week";
1122
+ /** ISO 8601. Defaults to 30 days (or 12 weeks) before `to`. */
1123
+ from?: string;
1124
+ /** ISO 8601. Defaults to now. At most 31 buckets in the range. */
1125
+ to?: string;
1126
+ /** A plain number column to add up per bucket. */
1127
+ sumColumn?: string;
1128
+ filter?: FigureOptions["filter"];
1129
+ }
1130
+
1131
+ export interface AggregateGroupSpec {
1132
+ /** A text, number, yes/no, date or user column. */
1133
+ groupBy: string;
1134
+ /** A plain number column to add up per group. */
1135
+ sumColumn?: string;
1136
+ filter?: FigureOptions["filter"];
1137
+ /** Default: by value. `count:desc` with a `limit` ranks the biggest groups. */
1138
+ order?: "count:desc" | "count:asc" | "sum:desc" | "sum:asc" | "group:asc" | "group:desc";
1139
+ /** 1–1000. */
1140
+ limit?: number;
1141
+ }
1142
+
1143
+ export interface AggregateBucket {
1144
+ /** The bucket's start date, `YYYY-MM-DD`. */
1145
+ date: string;
1146
+ count: number;
1147
+ /** 0 unless `sumColumn` was given. */
1148
+ sum: number;
1149
+ }
1150
+
1151
+ export interface AggregateGroup {
1152
+ /** The value; a user column's user id; "Uncategorized" for rows with none. */
1153
+ group: string;
1154
+ count: number;
1155
+ /** 0 unless `sumColumn` was given. */
1156
+ sum: number;
1157
+ }
1158
+
1159
+ export interface AggregateResult {
1160
+ /** By date: every bucket of the range, empty ones as `count: 0`. */
1161
+ buckets: AggregateBucket[];
1162
+ /** By a column: one entry per value. */
1163
+ groups: AggregateGroup[];
1164
+ loading: boolean;
1165
+ fetching: boolean;
1166
+ error: Error | null;
1167
+ refetch(): Promise<void>;
1168
+ }
1169
+
1170
+ /** Rows per day, week or column value over every row the viewer can read, counted on the server. */
1171
+ export function useDatastoreAggregate(
1172
+ table: string | null | undefined,
1173
+ spec?: AggregateBucketSpec | AggregateGroupSpec,
1174
+ ): AggregateResult;
1175
+
1051
1176
  export interface UserStatsQuery {
1052
1177
  days?: 7 | 30 | 90;
1053
1178
  /** A `useGroups()` row id: narrows user and invite figures to that group. */
@@ -1492,6 +1617,15 @@ export interface DatastoreSchemaColumn {
1492
1617
  display_column_id?: string | null;
1493
1618
  /** True when this column is the table's display/identification column. */
1494
1619
  is_identification?: boolean;
1620
+ /**
1621
+ * Set on a relation the server keeps at the newest related row; the id of
1622
+ * the related table's relation back. Clients cannot write such a column.
1623
+ */
1624
+ latest_via_column_id?: string | null;
1625
+ /** Set on a copy the server keeps: the latest-row column it reads through. */
1626
+ copy_via_column_id?: string | null;
1627
+ /** Set on a copy the server keeps: the related table's column it copies. */
1628
+ copy_of_column_id?: string | null;
1495
1629
  }
1496
1630
 
1497
1631
  export interface DatastoreSchema {
@@ -2018,6 +2152,14 @@ export function useAnonymousAccount(): AnonymousAccountApi;
2018
2152
  */
2019
2153
  export function useFill(): boolean;
2020
2154
 
2155
+ /**
2156
+ * sc-11348 — returns `true` when this widget is the surface of a page section
2157
+ * set to "Fit to screen": it runs edge to edge and takes the rest of the
2158
+ * screen, so drop your own frame (border, rounded corners, shadow).
2159
+ * `useFill()` is `true` too. Defaults to `false`; the same on web and native.
2160
+ */
2161
+ export function useFitScreen(): boolean;
2162
+
2021
2163
  /**
2022
2164
  * The host-provided navigation surface. `goTo(pageId, params?)` navigates
2023
2165
  * to an internal app page; `goBack()` pops the stack. `goToStartPage()`
package/dist/index.js CHANGED
@@ -80,6 +80,7 @@ export {
80
80
  useUserPhoto,
81
81
  useAnonymousAccount,
82
82
  useFill,
83
+ useFitScreen,
83
84
  useNavigation,
84
85
  useRouteParams,
85
86
  usePageBadge,
@@ -125,6 +126,7 @@ export {
125
126
  } from "./audio.js";
126
127
  export { useDatastoreFigure } from "./datastore-figure.js";
127
128
  export {
129
+ useDatastoreAggregate,
128
130
  useUserStats,
129
131
  useUserActivity,
130
132
  useActiveUsers,
@@ -80,6 +80,7 @@ export {
80
80
  useUserPhoto,
81
81
  useAnonymousAccount,
82
82
  useFill,
83
+ useFitScreen,
83
84
  useNavigation,
84
85
  useRouteParams,
85
86
  usePageBadge,
@@ -123,6 +124,7 @@ export {
123
124
  } from "./audio.js";
124
125
  export { useDatastoreFigure } from "./datastore-figure.js";
125
126
  export {
127
+ useDatastoreAggregate,
126
128
  useUserStats,
127
129
  useUserActivity,
128
130
  useActiveUsers,
package/dist/manifest.cjs CHANGED
@@ -331,6 +331,12 @@ function validateManifest(m) {
331
331
  "manifest.badgeCounter must be a boolean when present",
332
332
  );
333
333
 
334
+ pushIf(
335
+ errors,
336
+ manifest.prefersFitToScreen === undefined || typeof manifest.prefersFitToScreen === "boolean",
337
+ "manifest.prefersFitToScreen must be a boolean when present",
338
+ );
339
+
334
340
  return errors.length === 0 ? { ok: true } : { ok: false, errors };
335
341
  }
336
342
 
package/dist/manifest.js CHANGED
@@ -377,6 +377,12 @@ function validateManifest(m) {
377
377
  "manifest.badgeCounter must be a boolean when present",
378
378
  );
379
379
 
380
+ pushIf(
381
+ errors,
382
+ manifest.prefersFitToScreen === undefined || typeof manifest.prefersFitToScreen === "boolean",
383
+ "manifest.prefersFitToScreen must be a boolean when present",
384
+ );
385
+
380
386
  return errors.length === 0 ? { ok: true } : { ok: false, errors };
381
387
  }
382
388
 
@@ -1,8 +1,16 @@
1
- // sc-10691 — read-only hooks for an app's user totals and user activity. Each
2
- // is one keyed read, stale-while-revalidate like useDatastoreQuery, and each
3
- // re-runs on the page's pull-to-refresh.
1
+ // sc-10691 — read-only aggregate hooks: record counts per day, week or column
2
+ // value, and an app's user totals. Both are one keyed read, stale-while-revalidate like
3
+ // useDatastoreQuery, and both re-run on the page's pull-to-refresh.
4
4
  import { useCallback, useEffect, useRef, useState } from "react";
5
- import { toDirectoryError, useRefresh, useWidgetContextOrThrow } from "./hooks.js";
5
+ import {
6
+ toDatastoreError,
7
+ toDirectoryError,
8
+ useRefresh,
9
+ useWidgetContextOrThrow,
10
+ } from "./hooks.js";
11
+
12
+ // The system date columns bucket by day or week; any other column groups by value.
13
+ const DATE_GROUP_FIELDS = new Set(["created_at", "updated_at"]);
6
14
 
7
15
  const EMPTY = { identity: null, key: null, data: null, error: null };
8
16
 
@@ -66,6 +74,54 @@ function useKeyedRead(read, identity, key, mapError) {
66
74
  };
67
75
  }
68
76
 
77
+ /**
78
+ * Counts (and optionally sums) a table's rows on the server, over every row
79
+ * the viewer may read — not just one page. `spec`: `{ groupBy, sumColumn?,
80
+ * filter? }` plus, by date (`groupBy` "created_at" | "updated_at"), `bucket?:
81
+ * "day" | "week", from?, to?`, or, by a column, `order?` ("count:desc",
82
+ * "count:asc", "sum:desc", "sum:asc", "group:asc", "group:desc") and `limit?`
83
+ * (1–1000). Returns `{ buckets, groups, loading, fetching, error, refetch }`:
84
+ * by date, `buckets` is `[{ date, count, sum }]` with every bucket of the range
85
+ * present (empty ones as 0); by a column, `groups` is `[{ group, count, sum }]`,
86
+ * a user column's `group` being the user id and rows with no value one
87
+ * "Uncategorized" group. A falsy `table` settles to nothing.
88
+ */
89
+ export function useDatastoreAggregate(table, spec) {
90
+ const ctx = useWidgetContextOrThrow("useDatastoreAggregate");
91
+ if (!ctx.datastore || typeof ctx.datastore.records !== "function") {
92
+ throw new Error("useDatastoreAggregate: host did not inject a datastore client");
93
+ }
94
+ const { groupBy = "created_at", bucket, from, to, sumColumn, filter, order, limit } = spec || {};
95
+ const byDate = DATE_GROUP_FIELDS.has(groupBy);
96
+ const key = table
97
+ ? serialize(byDate ? [groupBy, bucket, from, to, sumColumn, filter] : [groupBy, sumColumn, filter, order, limit])
98
+ : null;
99
+ const records = ctx.datastore.records;
100
+
101
+ const result = useKeyedRead(
102
+ async () => {
103
+ const rows = await records(table).aggregate(
104
+ byDate
105
+ ? { groupBy, bucket, from, to, sumField: sumColumn, filter }
106
+ : { groupBy, sumField: sumColumn, filter, order, limit },
107
+ );
108
+ return Array.isArray(rows) ? rows : [];
109
+ },
110
+ // The mode is part of the identity: switching it must not show the other's rows.
111
+ table ? `${table}|${byDate ? "date" : "column"}` : null,
112
+ key,
113
+ toDatastoreError,
114
+ );
115
+
116
+ const { data, ...state } = result;
117
+ const rows = data || [];
118
+ return {
119
+ ...state,
120
+ buckets: byDate ? rows.map((row) => ({ date: row.group, count: row.count, sum: row.sum })) : [],
121
+ groups: byDate ? [] : rows.map((row) => ({ group: row.group, count: row.count, sum: row.sum })),
122
+ };
123
+ }
124
+
69
125
  /**
70
126
  * The app's user totals for its own admins — never a list of people.
71
127
  * `query`: `{ days?: 7 | 30 | 90, group_id? }`. Returns `{ stats, loading,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.223.0",
3
+ "version": "0.228.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
@@ -991,6 +991,24 @@ const HOOKS = [
991
991
  requiredContextSlice: ["fill"],
992
992
  scopes: null,
993
993
  },
994
+ {
995
+ name: "useFitScreen",
996
+ signature: "useFitScreen()",
997
+ description:
998
+ "Returns true when this widget is the surface of a page section set to " +
999
+ "\"Fit to screen\": the section runs edge to edge across the app's " +
1000
+ "content area and takes the rest of the screen, so the widget should " +
1001
+ "drop its own frame (border, rounded corners, shadow) and meet the " +
1002
+ "edges cleanly. useFill() is true as well then, and the host has already " +
1003
+ "removed the widget's outer margin. Defaults to false wherever the host " +
1004
+ "has not fitted the widget, so calling it is always safe. The SAME value " +
1005
+ "is injected by the web Player and the native export.",
1006
+ returnShape: {
1007
+ "(returns)": "boolean",
1008
+ },
1009
+ requiredContextSlice: ["fitScreen"],
1010
+ scopes: null,
1011
+ },
994
1012
  {
995
1013
  name: "useChildRenderer",
996
1014
  signature: "useChildRenderer()",
@@ -1397,7 +1415,8 @@ const HOOKS = [
1397
1415
  "create the row, call upload(file, { recordId: row.id }) — the per-call " +
1398
1416
  "recordId names the row the hook could not know at render — then write " +
1399
1417
  "the returned id into the FILE column with update(). A logged-out " +
1400
- "visitor cannot upload into a record space. For a file that belongs to " +
1418
+ "visitor may do this only for a row they just created, within a few " +
1419
+ "minutes, and never edits other rows' files. For a file that belongs to " +
1401
1420
  "no row: 'public' for content the app shows to anyone browsing, " +
1402
1421
  "'project' for signed-in workspace users only, 'personal' for a file " +
1403
1422
  "private to the uploading app user.",
@@ -1796,6 +1815,45 @@ const HOOKS = [
1796
1815
  requiredContextSlice: ["datastore.records"],
1797
1816
  scopes: ["datastore.read:*"],
1798
1817
  },
1818
+ // sc-10942 — rows per day or week, counted on the server over every row the
1819
+ // viewer may read. Date columns only until column grouping is exact too.
1820
+ {
1821
+ name: "useDatastoreAggregate",
1822
+ signature:
1823
+ "useDatastoreAggregate(tableId, { groupBy, bucket, from, to, sumColumn, filter, order, limit })",
1824
+ optionalArgs: ["groupBy", "bucket", "from", "to", "sumColumn", "filter", "order", "limit"],
1825
+ description:
1826
+ "Counts rows on the server over EVERY row the viewer can read, so it is " +
1827
+ "exact past the 1000-row page a list read returns — never group " +
1828
+ "useDatastoreQuery rows by hand. With `sumColumn` it also totals a plain " +
1829
+ "number column. BY DATE (`groupBy` \"created_at\", the default, or " +
1830
+ "\"updated_at\"): a SERIES for a chart, one entry per UTC day or ISO week " +
1831
+ "(Monday) in `buckets`; `bucket` is \"day\" (default) or \"week\"; " +
1832
+ "`from` / `to` are ISO dates (default: the 30 days, or 12 weeks, up to " +
1833
+ "now), at most 31 buckets, empty ones as count 0 so a chart has no gaps. " +
1834
+ "BY A COLUMN (any other `groupBy`: a text, number, yes/no, date or user " +
1835
+ "column): one entry per value in `groups`. A USER column's `group` is the " +
1836
+ "user id (resolve names with useDirectory({ ids })), so `order: \"count:desc\", " +
1837
+ "limit: 10` ranks the ten people with the most rows. `order` is count, sum " +
1838
+ "or group, then :asc or :desc (default: by value); `limit` is 1-1000. Rows " +
1839
+ "with no value form one \"Uncategorized\" group; on a user column, leave " +
1840
+ "them out with a `nempty:` filter. Encrypted, array, file and relation " +
1841
+ "columns cannot be grouped (VALIDATION). `filter` takes the same shapes " +
1842
+ "as useDatastoreQuery's, created_at ranges included. Pass null as the " +
1843
+ "table until a bound table is known.",
1844
+ returnShape: {
1845
+ buckets:
1846
+ "Array<{ date, count, sum }> // date = the bucket's start, YYYY-MM-DD; sum is 0 without sumColumn",
1847
+ groups:
1848
+ "Array<{ group, count, sum }> // by a column; a USER column's group is the user id",
1849
+ loading: "boolean",
1850
+ fetching: "boolean",
1851
+ error: "DatastoreError | null",
1852
+ refetch: "() => Promise<void>",
1853
+ },
1854
+ requiredContextSlice: ["datastore.records"],
1855
+ scopes: ["datastore.read:*"],
1856
+ },
1799
1857
  {
1800
1858
  name: "useDatastoreSchema",
1801
1859
  signature: "useDatastoreSchema(tableId)",
@@ -3528,6 +3586,13 @@ const MANIFEST_SCHEMA = {
3528
3586
  "Optional (default false). TRUE when every platform entry also exports useBadgeCount(props): a hook returning the count the widget puts on its page's menu entry with usePageBadge, so the app keeps that badge live while the page is closed. The host runs counters for installed marketplace widgets (the workspace's own published widgets included); the platform's built-in widgets declare none. It calls the hook with the instance's resolved props, inside the widget's own context; the hook returns a whole number, or null while the count is not known yet. It only reads, and the host enforces that: the counter's datastore and directory expose only their read methods (plus realtime subscriptions) and notification mutes only get/list, while writes, typing signals, payments, notifications.send, files, assets, AI agents, translation, link previews, device capabilities, identification and account actions all answer as unavailable. It also renders nothing, navigates nowhere, emits no events or toasts, marks nothing read, and calls none of usePageBadge, useScreenTitle, useSectionEmpty or useWidgetRoute's setter. The linter refuses a declared counter whose entry has no useBadgeCount export. The AppStudio widget agent leaves this unset: a generated widget never declares a badge counter.",
3529
3587
  default: false,
3530
3588
  },
3589
+ prefersFitToScreen: {
3590
+ type: "boolean",
3591
+ required: false,
3592
+ description:
3593
+ "Optional (default false). TRUE when the widget is built to own the rest of the screen: it scrolls its own content and keeps its controls in view, like a messenger, a map or a full-height board. Dropping the widget at the top level of a page in the Builder turns that section's \"Fit to screen\" setting on (the maker can switch it off, and it stays off), and the app-builder fits the section it plans the widget into. A fit section runs edge to edge across the content area and takes the rest of the screen; the widget learns it through useFill() and useFitScreen(), and the host gives it a box exactly the height left, which it scrolls inside (a widget without the field grows with its content there, and the page scrolls). Set it only for a widget that honours useFill() and scrolls inside the height it is given; leave it false (or omit) for everything else.",
3594
+ default: false,
3595
+ },
3531
3596
  events: {
3532
3597
  type: "object[]",
3533
3598
  required: true,
@@ -3559,7 +3624,7 @@ const MANIFEST_SCHEMA = {
3559
3624
  type: "object",
3560
3625
  required: false,
3561
3626
  description:
3562
- "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.",
3627
+ "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.",
3563
3628
  },
3564
3629
  translations: {
3565
3630
  type: "object",
@@ -3603,6 +3668,16 @@ const WIDGET_CONTEXT_SHAPE = {
3603
3668
  required: false,
3604
3669
  fields: {},
3605
3670
  },
3671
+ // sc-11348 — optional host hint backing useFitScreen(): the widget is the
3672
+ // surface of a section set to Fit to screen, so it drops its own frame.
3673
+ fitScreen: {
3674
+ description:
3675
+ "Optional host layout hint (boolean). True when this widget is the " +
3676
+ "surface of a page section set to Fit to screen: edge to edge, the rest " +
3677
+ "of the screen. Backs useFitScreen(); defaults to false when absent.",
3678
+ required: false,
3679
+ fields: {},
3680
+ },
3606
3681
  user: {
3607
3682
  description:
3608
3683
  "Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. email is null for an account created without one. Not a data-client.",
@@ -5878,7 +5953,28 @@ const CONTRACT = deepFreeze({
5878
5953
  // `{ colors, onImage }`: the theme colours as they read over the picture.
5879
5954
  // 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
5880
5955
  // the signed-in user's own start page on both hosts.
5881
- version: "1.179.0",
5956
+ // 1.180.0: additive (sc-11244) — a logged-out visitor may attach files to
5957
+ // the row they just created, for a few minutes: the host carries a grant
5958
+ // from the create to that row's upload and update. Widget code is the
5959
+ // same create → upload → update flow.
5960
+ // 1.181.0: additive (sc-10942) — `useDatastoreAggregate(tableId, spec)`
5961
+ // counts rows per day or week on the server (date columns only), through
5962
+ // `records(t).aggregate`'s new bucket/from/to.
5963
+ // 1.182.0: additive (sc-10737) — `useDatastoreAggregate` groups by an
5964
+ // ordinary column too, into `groups` ([{ group, count, sum }]), with
5965
+ // `order` and `limit` for a top N; counted on the server over every
5966
+ // readable row. A user column groups by user id.
5967
+ // 1.183.0: additive (sc-10766) — a datastoreTemplate column may declare
5968
+ // `latestVia` (a server-kept pointer to the newest related row) or
5969
+ // `copyOf: { via, column }` (a server-kept copy read through one), and
5970
+ // a table may declare `constraints` (`unique` with `unlessRelated`,
5971
+ // `no-overlap`, `related-open`, `related-index`).
5972
+ // 1.184.0: additive (sc-11348) — OPTIONAL manifest field
5973
+ // `prefersFitToScreen` (dropping the widget at a page's top level turns
5974
+ // that section's Fit to screen on), and `useFitScreen()` with its
5975
+ // optional `fitScreen` host hint: the widget is a fit section's surface,
5976
+ // so it drops its own frame. Existing manifests and hosts read false.
5977
+ version: "1.184.0",
5882
5978
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
5883
5979
  hooks: HOOKS,
5884
5980
  primitives: PRIMITIVES,