@colixsystems/widget-sdk 0.97.0 → 0.99.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 +100 -3
- package/dist/contract.cjs +139 -4
- package/dist/contract.js +139 -4
- package/dist/hooks.js +4659 -4344
- package/dist/host.d.ts +49 -0
- package/dist/host.js +16 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +3 -0
- package/dist/index.native.js +3 -0
- package/dist/linter.cjs +68 -0
- package/dist/linter.js +68 -0
- package/dist/navigation.cjs +124 -0
- package/dist/navigation.js +113 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
6
6
|
|
|
7
7
|
| Injected at | Package | Surface (snake_case verbatim, `list` → `{ data, meta }`) |
|
|
8
8
|
| ----------- | ------- | -------- |
|
|
9
|
-
| `ctx.datastore` | `@colixsystems/datastore-client` | `tables.{list,get}`, `schema(tableId)`, `records(tableId).{ list(query), get(id), create(values), update(id,values) [PATCH], delete(id), aggregate(spec), permissions(recordId).{ list, grant, update, revoke } }` |
|
|
9
|
+
| `ctx.datastore` | `@colixsystems/datastore-client` | `tables.{list,get}`, `schema(tableId)`, `myPermissions(tableId, { recordId? })`, `records(tableId).{ list(query), get(id), create(values), update(id,values) [PATCH], delete(id), aggregate(spec), permissions(recordId).{ list, grant, update, revoke } }` |
|
|
10
10
|
| `ctx.directory` | `@colixsystems/directory-client` | `me()`, `users.{list,get,invite,deactivate,reactivate}`, `groups.{list,create,remove,addMember,removeMember,listMine}`, `invites.{list,revoke,resend}` |
|
|
11
11
|
| `ctx.assets` | `@colixsystems/assets-client` | the Asset Manager: `get(id)`, `list(query)`, `upload(formData)` over `/files` — what `useAsset()` (single asset by id) and `useAssetsByTag()` (every asset carrying a tag) resolve |
|
|
12
12
|
| `ctx.payments` | `@colixsystems/payments-client` | `requestPayment(body)`, `getPayment(id)` |
|
|
@@ -38,12 +38,15 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
38
38
|
| **CORE** | `useSpeechToText(options?)` | `{ transcript, partial, listening, supported, error, start, stop, abort, reset }` | `ctx.device.speech` — no scope. Dictation with the device's **on-device** recogniser: no audio is uploaded and no AI credit is spent. Capture is IMPERATIVE: call `start()` from a user gesture (a tap), never on mount. `transcript` accumulates finalised speech, `partial` holds the uncommitted guess (needs `options.interimResults`); `stop()` keeps it, `abort()` discards it. Rejects with `SpeechToTextError` (`.code` in `PERMISSION_DENIED \| NO_SPEECH \| LANGUAGE_UNSUPPORTED \| NETWORK \| ABORTED \| UNSUPPORTED \| INTERNAL`). **Gate your mic button on `supported`** — Firefox ships no `SpeechRecognition`. Identical on web (`SpeechRecognition`) and the Expo export (`expo-speech-recognition`). |
|
|
39
39
|
| **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope. `t(key)` resolves the widget-namespaced key (`widget.<id>.<key>`, declared in `manifest.translations`) first, then a **predefined shared key** (`shared.<key>`) when `key` is one of the standard strings (`submit`, `cancel`, `save`, `loading`, …), then the raw key. Use a shared key for an identical default string so it translates once and any per-instance `widget.<id>.<key>` override still wins. |
|
|
40
40
|
| **CORE** | `useTranslate()` | `{ translate, translating, error, language, available }` | `ctx.i18n.translate` — no scope. Machine-translates **user-generated content** (record text, file names, API payloads) into the app user's language; `useI18n().t()` is still the answer for your own copy. `translate(str)` → `Promise<string>`, `translate(str[])` → `Promise<string[]>` in ONE request. Target defaults to the app user's language. Cached per session, per pod, and durably per workspace, so repeat text is free. Limits: 50 segments / 5 000 chars each / 20 000 total. Rejects with `TranslateError`; `available` is false where the host cannot translate. |
|
|
41
|
+
| **CORE** | `useStableQuery(buildQuery)` | `T \| undefined` (whatever `buildQuery()` returns) | No context slice, no scope. Keeps `buildQuery()`'s result at a STABLE reference across renders when its (JSON-serialised) content hasn't changed, so `useDatastoreQuery(tableId, useStableQuery(() => ({...})))` replaces a hand-rolled `useMemo` with an easy-to-get-wrong deps array. Never throws: a `buildQuery` that itself throws degrades to a stable `undefined`; a result that can't be diffed (e.g. circular) degrades to "always a new reference". |
|
|
41
42
|
| **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options?)` | `{ data, loading, error, refetch }` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
|
|
42
43
|
| **DATASTORE** | `useDatastoreRecord(table, id)` | `{ data, loading, error, refetch }` | `records(table).get` — `datastore.read:<table>` |
|
|
43
44
|
| **DATASTORE** | `useDatastoreSchema(tableId)` | `{ schema, loading, error, refetch }` | `schema(tableId)` — `datastore.read:<table>` |
|
|
45
|
+
| **DATASTORE** | `useBoundColumns(tableId, shape, props)` | `{ columns, resolved, missing, loading, error }` | `schema(tableId)` (built on `useDatastoreSchema`) — `datastore.read:<table>`. Resolves author-bound column NAMES from `props` by exact name → case-insensitive name → first unclaimed column matching `shape[key].dataType`, so a column an author renamed after install still resolves instead of `record[props.titleField]` reading `undefined`. `columns` holds the resolved NAME (`record[columns.titleField]`); `resolved` holds the full `Column`; `missing` lists non-`optional` keys that never resolved. Falsy `tableId` collapses to `{ columns: {}, resolved: {}, missing: Object.keys(shape), loading: false, error: null }`. |
|
|
44
46
|
| **DATASTORE** | `useInterpretDraft(tableId)` | `{ interpret, interpreting, error, result, available }` | `interpret(tableId, body)` — `datastore.read:<table>`. Turns ONE sentence a user typed ("walk at 11 am tomorrow") into DRAFT column values so a form can prefill itself. IMPERATIVE: call `interpret(text, { fields?, timeZone? })` from an event handler, never on mount. It DRAFTS and writes nothing — show the values for review, then submit through `useDatastoreMutation().create`. Resolves to `{ values, unresolved }`; `values` is keyed by column NAME (the shape `create()` takes) and `unresolved` names the fields the sentence did not state. Only text / number / boolean / date / datetime / array columns are drafted — `FILE`, `RELATION`, `USER` and `USER_GROUP` carry ids and are never guessed. Fails closed to an empty draft. **Every call spends the workspace's AI credits** and is rate-limited per actor, so call it once per user action (never on mount or in a render loop); once the workspace runs out the call is refused with a generic 429 — an app user is deliberately **not** told the workspace's billing state, since they have never heard of an AI credit and cannot buy one. Never surface a raw error to the person filling the form: say drafting is unavailable and keep every field editable by hand. `available` is false where the host brokers no interpreter. |
|
|
45
47
|
| **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*` |
|
|
46
48
|
| **DATASTORE** | `useRecordPermissions(tableId, recordId)` | `{ permissions, loading, error, grant, revoke, update, refetch }` | `records(table).permissions(record).{ list, grant, update, revoke }` — `acl.write:records` (+ `can_grant` on the record) |
|
|
49
|
+
| **DATASTORE** | `useCanWrite(tableId, options?)` | `{ canWrite, loading, error, refetch }` | `myPermissions(tableId, { recordId? })` — scope `datastore.read:<table>`. A FLOOR, not a full replacement for domain-specific write rules: answers "is this caller signed in AND permitted", reading the same table-ACL answer the write endpoint enforces. Pass `{ recordId }` for a per-row check. A widget whose own rule is MORE SPECIFIC than the table ACL (e.g. "only the assigned user may edit this row") must still hand-check that in addition. Pair with `useUser()` to also tell "not signed in" apart from "signed in but forbidden" — both resolve `canWrite: false` here. Falsy `tableId`, or a host that hasn't injected `myPermissions` (an older host), collapses to `{ canWrite: false, loading: false, error: null, refetch: async () => undefined }` rather than throwing. |
|
|
47
50
|
| **FILES** (`ctx.assets`) | `useAsset(id)` | `{ url, file, loading, error, refetch }` | `ctx.assets.get` — no scope |
|
|
48
51
|
| **FILES** | `useAssetsByTag(tag, { type? })` | `{ assets, loading, error, refetch }` | `ctx.assets.list` (unwraps `{ data, meta }` to `assets`) — no scope. `type` defaults to `"image"`; pass `"all"` / `"audio"` / `"video"` / `"document"` to widen. Falsy `tag` collapses to `assets: []` without a round-trip. |
|
|
49
52
|
| **DIRECTORY** (`ctx.directory`) | `useDirectory(query?)` | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
|
|
@@ -64,10 +67,50 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
64
67
|
|
|
65
68
|
## Status
|
|
66
69
|
|
|
67
|
-
`v0.
|
|
70
|
+
`v0.95.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**.
|
|
68
71
|
|
|
69
|
-
### What's new in 0.
|
|
72
|
+
### What's new in 0.95.0 (contract 1.68.0)
|
|
73
|
+
|
|
74
|
+
**One number for the quick bar's cap — `quickBarMaxItems` + `quickBarCap`.** The sidebar's secondary mobile quick bar caps at five, and that five lived in two places: a named constant in the web chrome and a bare literal in the compiler. Nothing stopped them drifting, and no Studio surface could state the number at all.
|
|
75
|
+
|
|
76
|
+
`CONTRACT.themeMenuTypes.*.quickBarMaxItems` now carries it (5 on `sidebar`, `null` on the two shapes that draw no quick bar), read through the new `quickBarCap(menuType)` host export.
|
|
77
|
+
|
|
78
|
+
The distinction from `menuItemCap` is the point and is worth keeping straight: **`quickBarCap` may drop a page** — the rail and the drawer still list every menu page, so the bar is a shortcut. **`menuItemCap` may not** — where the chrome IS the menu (`bottom-tabs`), the surplus moves behind a More sheet instead.
|
|
79
|
+
|
|
80
|
+
Host-integration surface only. `CONTRACT.version` → `1.68.0`.
|
|
81
|
+
|
|
82
|
+
### What's new in 0.94.0 (contract 1.67.0)
|
|
83
|
+
|
|
84
|
+
**The footer strip is themeable — `resolveFooterTokens` (REQ-NAV-STRUCTURE).** The bottom strip's surface was hard-coded white on both hosts and its items read the SIDEBAR's tokens. That is fine while the strip is the sidebar's secondary quick bar, and untenable once it IS the menu: the `bottom-tabs` shape hides the sidebar panel, so those tokens have nowhere to be set.
|
|
85
|
+
|
|
86
|
+
A `theme_config.footer` block now carries `backgroundColor`, `textColor`, `activeColor` and the opt-in divider `borderColor` + `borderWidth`. **Every field falls back to the sidebar's**, so a workspace that never touches it renders exactly as before and only an explicit value moves anything.
|
|
87
|
+
|
|
88
|
+
`resolveFooterTokens(theme)` (from `@colixsystems/widget-sdk/host`) is the one resolver both hosts read it with. It also settles two divergences the strip carried: the native bar ruled a permanent `#e2e8f0` hairline the theme could not reach — REQ-THEME-LOOK's rule is that the divider's **colour** is its switch — and it tinted the active tab's *label* where the web painted a filled pill, so `activeStyle: "filled"` meant two different things per host.
|
|
89
|
+
|
|
90
|
+
Host-integration surface only: no author-facing hook, prop, primitive, or manifest field changed. `CONTRACT.version` → `1.67.0`.
|
|
91
|
+
|
|
92
|
+
### What's new in 0.93.0 (contract 1.66.0)
|
|
93
|
+
|
|
94
|
+
**An app picks the SHAPE its navigation takes — `CONTRACT.themeMenuTypes` (REQ-NAV-STRUCTURE).** Until now the chrome was always a sidebar: a persistent left rail on desktop, a hamburger drawer plus an optional bottom quick bar on mobile. That is the right default for an admin tool and the wrong one for a phone-first app or a site, and there was no way to say so. `CONTRACT.themeMenuTypes` publishes the closed catalogue — `sidebar`, `top-bar`, `bottom-tabs` — each entry carrying `{ name, summary, maxItems }`.
|
|
95
|
+
|
|
96
|
+
`maxItems` caps how many menu pages the chrome draws at once, and its meaning differs per type deliberately: the sidebar's mobile quick bar is a **secondary** curated bar whose cap may drop a page (the rail still lists every one), while a `bottom-tabs` strip **is** the menu, so its cap must never drop one — the surplus moves behind a More sheet instead.
|
|
97
|
+
|
|
98
|
+
**New host exports (`@colixsystems/widget-sdk/host`)** — `normaliseNavigation(navigation)` resolves a stored `theme_config.navigation` block to the `{ menuType }` a host switches its chrome on, and `menuItemCap(menuType)` states that shape's cap. One implementation for both hosts, so a menu type cannot mean one thing in the web Player and another in the exported Expo app. An absent or unknown value resolves to `sidebar`, so every app authored before menu types existed renders and compiles byte-identically.
|
|
99
|
+
|
|
100
|
+
Host-integration surface only: no author-facing hook, prop, primitive, or manifest field changed. `CONTRACT.version` → `1.66.0`.
|
|
101
|
+
|
|
102
|
+
### What's new in 0.98.0 (contract 1.70.0)
|
|
103
|
+
|
|
104
|
+
**Three new hooks close the biggest gaps in the write-gating and query-authoring surface (sc-5206).**
|
|
70
105
|
|
|
106
|
+
- **`useBoundColumns(tableId, shape, props)`** — resolve author-bound column NAMES from a widget's own props, built on `useDatastoreSchema`. Falls back name → case-insensitive name → first unclaimed column matching `dataType`, so `record[props.titleField]` reading `undefined` after a tenant renames a column is no longer a widget's problem: read `record[bound.titleField]` (via `const { columns: bound } = useBoundColumns(tableId, shape, props)`) and it keeps resolving.
|
|
107
|
+
- **`useStableQuery(buildQuery)`** — the same content-diffed stable-reference trick `useDatastoreQuery` already applies to its own `query` argument, generalised into a reusable hook: `useDatastoreQuery(tableId, useStableQuery(() => ({...})))` replaces a hand-rolled `useMemo` and its easy-to-get-wrong deps array. Reads no `ctx` — safe outside a `WidgetContextProvider`.
|
|
108
|
+
- **`useCanWrite(tableId, options?)`** — a write-permission FLOOR reading a new `ctx.datastore.myPermissions` client method. `{ canWrite, loading, error, refetch }`; pass `{ recordId }` for a per-row check. Not a replacement for a MORE SPECIFIC domain rule (still hand-check "only the assigned user" in addition), and not a replacement for `useUser()` when the UI needs to tell "not signed in" apart from "signed in but forbidden".
|
|
109
|
+
|
|
110
|
+
The SDK linter gained two matching soft-warning rules: `raw-useMemo-into-datastore-query` (a `useDatastoreQuery` argument built from a raw `useMemo` when the file hasn't reached for `useStableQuery`) and `hand-rolled-write-gate` (a write gated on `useUser().groupIds`/`.roles` when the file hasn't reached for `useCanWrite`). Both are steering nudges (`severity: "warning"`), never publish-blocking.
|
|
111
|
+
|
|
112
|
+
- **`CONTRACT.version` → `1.70.0`** (additive: three new hooks + the new `datastore.myPermissions` context field + two linter rules). No existing export changed signature.
|
|
113
|
+
### What's new in 0.97.0 (contract 1.69.0)
|
|
71
114
|
**`CONTRACT.themeComponents` gains five scopes: `accent`, `destructive`, `muted`, `popover`, `ring` (sc-5392).** The vocabulary shipped with exactly `button`/`card`/`text` (sc-1497), so a shadcn/Tailwind app import's `--accent`, `--destructive`, `--muted`, `--popover` and `--ring` custom properties had no `themeConfig` home and were reported "no theme home" on every import. Each new scope binds to real `styleSchema` fields on the built-ins that already had a matching surface — `accent` (a highlight/tag surface: `background`/`borderColor`/`radius`) to the Label widget's own fields, `destructive` (a themed danger/delete action: `background`/`textColor`/`borderColor`) to a new "Danger" Button variant, `muted` (a subtle/secondary surface: `background`/`textColor`/`borderColor`/`radius`) to Form Input's and Form Builder's pre-existing input fields, `popover` (a dropdown/menu surface: `background`/`textColor`/`borderColor`) to the same two widgets' choice-field option list, and `ring` (the app-wide focus-visible outline: `color`/`width`) to a new emphasis border on Button. **This is HOST-ONLY plumbing, exactly like the three scopes before it** — `useTheme()`'s documented `components` slice is unchanged, no widget-authoring hook or `propertySchema` type moved, and no scope declares `universalFields`, so a third-party or AI-generated widget's contract is unaffected; the Developer guide and `DEFAULT_SYSTEM_PROMPT` need no update because neither ever documented this internal vocabulary. Fully additive: a theme with no `components` key, or one using only `button`/`card`/`text`, resolves exactly as before.
|
|
72
115
|
- **`CONTRACT.version` → `1.69.0`** (additive: five new `themeComponents` scopes + their target-field bindings). No existing scope, token, or export changed shape.
|
|
73
116
|
|
|
@@ -1002,6 +1045,60 @@ The manifest declares the matching scope:
|
|
|
1002
1045
|
|
|
1003
1046
|
The server-side gate is `canGrant` on the target record — Studio owners pass automatically; an APP_USER holds `canGrant` as the record's creator or via a delegated grant. A caller without `canGrant` receives `PermissionError { code: "FORBIDDEN" }`. The hook collapses to a stable no-op when `tableId` or `recordId` is null/empty — so a widget can render its picker first, then bind to the picked record without conditional hook tricks.
|
|
1004
1047
|
|
|
1048
|
+
## Resolving bound columns, a stable query, and a write-permission floor
|
|
1049
|
+
|
|
1050
|
+
`useBoundColumns`, `useStableQuery`, and `useCanWrite` (sc-5206) target the three most common ways a widget goes wrong against a table it doesn't fully control: a renamed column reading `undefined`, a query argument that refetches in a loop, and a write control offered to someone the table forbids.
|
|
1051
|
+
|
|
1052
|
+
```js
|
|
1053
|
+
import {
|
|
1054
|
+
Text,
|
|
1055
|
+
View,
|
|
1056
|
+
Pressable,
|
|
1057
|
+
useBoundColumns,
|
|
1058
|
+
useStableQuery,
|
|
1059
|
+
useDatastoreQuery,
|
|
1060
|
+
useCanWrite,
|
|
1061
|
+
useUser,
|
|
1062
|
+
} from "@colixsystems/widget-sdk";
|
|
1063
|
+
|
|
1064
|
+
export default function OpenTasks({ tableId, titleField, statusField }) {
|
|
1065
|
+
// Resolves to the CURRENT column names even if the author's binding is
|
|
1066
|
+
// stale (a column renamed after install), instead of reading `undefined`.
|
|
1067
|
+
const { columns: bound, loading: schemaLoading } = useBoundColumns(
|
|
1068
|
+
tableId,
|
|
1069
|
+
{ titleField: { dataType: "STRING" }, statusField: { dataType: "STRING" } },
|
|
1070
|
+
{ titleField, statusField },
|
|
1071
|
+
);
|
|
1072
|
+
|
|
1073
|
+
// A binding is undefined until the schema resolves, so hold the query back
|
|
1074
|
+
// rather than filtering on an undefined column name.
|
|
1075
|
+
const ready = Boolean(bound.statusField);
|
|
1076
|
+
// A stable query argument with no useMemo deps array to get wrong.
|
|
1077
|
+
const query = useStableQuery(() => ({
|
|
1078
|
+
filter: ready ? { [bound.statusField]: "eq:open" } : {},
|
|
1079
|
+
sort: { field: "created_at", dir: "desc" },
|
|
1080
|
+
}));
|
|
1081
|
+
const { data, loading } = useDatastoreQuery(ready ? tableId : null, query);
|
|
1082
|
+
|
|
1083
|
+
const user = useUser();
|
|
1084
|
+
const { canWrite } = useCanWrite(tableId);
|
|
1085
|
+
|
|
1086
|
+
if (schemaLoading || loading) return <Text>Loading…</Text>;
|
|
1087
|
+
return (
|
|
1088
|
+
<View>
|
|
1089
|
+
{data.map((row) => <Text key={row.id}>{row[bound.titleField]}</Text>)}
|
|
1090
|
+
{!user.id ? (
|
|
1091
|
+
<Text>Sign in to add a task</Text>
|
|
1092
|
+
) : canWrite ? (
|
|
1093
|
+
<Pressable onPress={() => {/* … */}}><Text>Add task</Text></Pressable>
|
|
1094
|
+
) : null /* signed in but not permitted — no control, not a disabled one */}
|
|
1095
|
+
</View>
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
`useCanWrite` answers "is this table's ACL open to me" — it is a FLOOR, not the whole rule. A widget whose own logic is more specific ("only the assignee may edit this row") still hand-checks that in addition, typically with `options.recordId` passed to a second `useCanWrite` call or a plain `user.id === record[assigneeField]` comparison.
|
|
1101
|
+
|
|
1005
1102
|
## Cross-platform widgets (single-file vs split)
|
|
1006
1103
|
|
|
1007
1104
|
Every widget runs in **both** the web Player and the exported native (Expo) app.
|
package/dist/contract.cjs
CHANGED
|
@@ -159,6 +159,42 @@ const THEME_WIDGET_STYLES = Object.freeze({
|
|
|
159
159
|
// Matches the styleSchema field cap the widget agent is held to.
|
|
160
160
|
maxFieldsPerWidget: 12,
|
|
161
161
|
});
|
|
162
|
+
// REQ-NAV-STRUCTURE: the SHAPE an app's navigation takes. One catalogue, four
|
|
163
|
+
// consumers -- the Studio's Navigation page, Mason's set_theme coercion, the web
|
|
164
|
+
// PlayerChrome and the compiler's navigator -- so a type cannot be offered to an
|
|
165
|
+
// author without every host actually drawing it.
|
|
166
|
+
//
|
|
167
|
+
// `maxItems` caps how many menu pages the chrome draws at once, and its meaning
|
|
168
|
+
// differs per type deliberately. The sidebar's mobile quick bar is a SECONDARY
|
|
169
|
+
// curated bar, so its own cap may drop a page -- the rail still lists every one.
|
|
170
|
+
// A bottom-tabs strip IS the menu, so its cap must never drop one: the surplus
|
|
171
|
+
// moves behind a "More" sheet instead.
|
|
172
|
+
const THEME_MENU_TYPES = Object.freeze({
|
|
173
|
+
sidebar: Object.freeze({
|
|
174
|
+
name: "Sidebar",
|
|
175
|
+
summary:
|
|
176
|
+
"A persistent left rail on desktop; a hamburger drawer plus the optional bottom quick bar on mobile.",
|
|
177
|
+
maxItems: null,
|
|
178
|
+
// The SECONDARY mobile quick bar's cap. It may drop a page past it, because
|
|
179
|
+
// the rail and the drawer still list every one -- the opposite of
|
|
180
|
+
// `bottom-tabs`' `maxItems`, where the strip IS the menu.
|
|
181
|
+
quickBarMaxItems: 5,
|
|
182
|
+
}),
|
|
183
|
+
"top-bar": Object.freeze({
|
|
184
|
+
name: "Top bar",
|
|
185
|
+
summary:
|
|
186
|
+
"A horizontal row of links in the app header, scrolling sideways when it runs out of room. No rail at any width.",
|
|
187
|
+
maxItems: null,
|
|
188
|
+
quickBarMaxItems: null,
|
|
189
|
+
}),
|
|
190
|
+
"bottom-tabs": Object.freeze({
|
|
191
|
+
name: "Bottom tabs",
|
|
192
|
+
summary:
|
|
193
|
+
"A sticky bottom strip at every width -- the phone-native shape. Pages past the cap move behind a More sheet.",
|
|
194
|
+
maxItems: 4,
|
|
195
|
+
quickBarMaxItems: null,
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
162
198
|
const THEME_SPACING_SCALE = Object.freeze({
|
|
163
199
|
min: 0.5,
|
|
164
200
|
max: 2,
|
|
@@ -474,6 +510,23 @@ const HOOKS = [
|
|
|
474
510
|
requiredContextSlice: ["i18n.locale"],
|
|
475
511
|
scopes: null,
|
|
476
512
|
},
|
|
513
|
+
{
|
|
514
|
+
name: "useStableQuery",
|
|
515
|
+
signature: "useStableQuery(buildQuery)",
|
|
516
|
+
description:
|
|
517
|
+
"sc-5206 — keep whatever buildQuery() returns at a STABLE reference across renders when its (JSON-serialised) content hasn't changed, so it can " +
|
|
518
|
+
"be passed straight into useDatastoreQuery's second argument (useDatastoreQuery(tableId, useStableQuery(() => ({...})))) instead of a hand-rolled " +
|
|
519
|
+
"useMemo with an easy-to-get-wrong deps array. This generalises the same trick useDatastoreQuery already applies internally to its own query " +
|
|
520
|
+
"argument. buildQuery is a ZERO-ARG function called every render; pure React state — no ctx, no host required, safe outside a " +
|
|
521
|
+
"WidgetContextProvider. Never throws: a buildQuery that itself throws degrades to a stable undefined, and a result JSON.stringify can't diff " +
|
|
522
|
+
"(e.g. a circular structure) degrades to \"always a new reference\".",
|
|
523
|
+
returnShape: {
|
|
524
|
+
"(returns)":
|
|
525
|
+
"T | undefined // the hook returns buildQuery()'s result directly, not a wrapper object",
|
|
526
|
+
},
|
|
527
|
+
requiredContextSlice: [],
|
|
528
|
+
scopes: null,
|
|
529
|
+
},
|
|
477
530
|
{
|
|
478
531
|
name: "useUser",
|
|
479
532
|
signature: "useUser()",
|
|
@@ -924,6 +977,29 @@ const HOOKS = [
|
|
|
924
977
|
requiredContextSlice: ["datastore.schema"],
|
|
925
978
|
scopes: ["datastore.read:<table>"],
|
|
926
979
|
},
|
|
980
|
+
{
|
|
981
|
+
name: "useBoundColumns",
|
|
982
|
+
signature: "useBoundColumns(tableId, shape, props)",
|
|
983
|
+
description:
|
|
984
|
+
"sc-5206 — resolve author-bound column NAMES from a widget's own props, built on useDatastoreSchema. shape is " +
|
|
985
|
+
"{ [propKey]: { dataType?, optional? } }; props is the widget's own props carrying author-bound column names (e.g. props.titleField === " +
|
|
986
|
+
"'Title'). Resolution order per key: (1) an exact name match on props[key], (2) a case-insensitive name match, (3) the first column whose " +
|
|
987
|
+
"data_type matches shape[key].dataType that no EARLIER key in this call already claimed, (4) unresolved. Unresolved NEVER throws — the key " +
|
|
988
|
+
"reads undefined, so a column an author renamed after install still resolves instead of crashing the widget. Returns { columns, resolved, " +
|
|
989
|
+
"missing, loading, error }: columns holds the resolved column NAME — a drop-in replacement for record[props.titleField] -> " +
|
|
990
|
+
"record[columns.titleField]; resolved holds the full Column object (snake_case verbatim, as useDatastoreSchema returns it) so a caller can " +
|
|
991
|
+
"inspect resolved.<key>.data_type; missing lists every key whose spec is not optional:true and did not resolve. Falsy tableId collapses to " +
|
|
992
|
+
"{ columns: {}, resolved: {}, missing: Object.keys(shape), loading: false, error: null } without a network round-trip.",
|
|
993
|
+
returnShape: {
|
|
994
|
+
columns: "{ [propKey]: string | undefined }",
|
|
995
|
+
resolved: "{ [propKey]: Column | undefined }",
|
|
996
|
+
missing: "string[] // required (non-optional) keys that did not resolve",
|
|
997
|
+
loading: "boolean",
|
|
998
|
+
error: "DatastoreError | null",
|
|
999
|
+
},
|
|
1000
|
+
requiredContextSlice: ["datastore.schema"],
|
|
1001
|
+
scopes: ["datastore.read:<table>"],
|
|
1002
|
+
},
|
|
927
1003
|
{
|
|
928
1004
|
name: "useInterpretDraft",
|
|
929
1005
|
signature: "useInterpretDraft(tableId)",
|
|
@@ -1295,6 +1371,28 @@ const HOOKS = [
|
|
|
1295
1371
|
requiredContextSlice: ["datastore.records"],
|
|
1296
1372
|
scopes: ["acl.write:records"],
|
|
1297
1373
|
},
|
|
1374
|
+
// sc-5206 — table/record write-permission floor, reading the injected
|
|
1375
|
+
// ctx.datastore.myPermissions.
|
|
1376
|
+
{
|
|
1377
|
+
name: "useCanWrite",
|
|
1378
|
+
signature: "useCanWrite(tableId, options?)",
|
|
1379
|
+
description:
|
|
1380
|
+
"sc-5206 — is the signed-in caller permitted to write to tableId (or, with options.recordId, to that one row)? Returns { canWrite, loading, " +
|
|
1381
|
+
"error, refetch }, reading the injected ctx.datastore.myPermissions(tableId, { recordId }). This is a FLOOR, not a full replacement for " +
|
|
1382
|
+
"domain-specific write rules: a widget whose rule is more specific than the table ACL (e.g. 'only the assigned user may edit this row') must " +
|
|
1383
|
+
"still hand-check that in addition to useCanWrite (user.id === record[assigneeField]). It only answers 'signed in AND permitted' — it does " +
|
|
1384
|
+
"NOT distinguish 'not signed in' from 'signed in but forbidden' (both resolve canWrite:false); pair it with useUser() when the UI needs to " +
|
|
1385
|
+
"tell those apart. myPermissions is a newer client method: a host that has not injected it is NOT a hard error — this hook collapses to " +
|
|
1386
|
+
"{ canWrite: false, loading: false, error: null, refetch: async () => undefined } rather than throwing. Same collapse when tableId is falsy.",
|
|
1387
|
+
returnShape: {
|
|
1388
|
+
canWrite: "boolean",
|
|
1389
|
+
loading: "boolean",
|
|
1390
|
+
error: "DatastoreError | null",
|
|
1391
|
+
refetch: "() => Promise<void>",
|
|
1392
|
+
},
|
|
1393
|
+
requiredContextSlice: ["datastore.myPermissions"],
|
|
1394
|
+
scopes: ["datastore.read:<table>"],
|
|
1395
|
+
},
|
|
1298
1396
|
// REQ-RT-07 — realtime table subscription.
|
|
1299
1397
|
{
|
|
1300
1398
|
name: "useDatastoreSubscription",
|
|
@@ -1826,13 +1924,19 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1826
1924
|
datastore: {
|
|
1827
1925
|
description:
|
|
1828
1926
|
"Injected @colixsystems/datastore-client instance. " +
|
|
1829
|
-
"{ tables: { list(), get(idOrName), interpret(idOrName, body) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
|
|
1927
|
+
"{ tables: { list(), get(idOrName), interpret(idOrName, body), myPermissions(idOrName, { recordId? }) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
|
|
1830
1928
|
"records(tableId) -> { list(query) -> Promise<{ data, meta }>, get(id), create(values), update(id, values), delete(id), aggregate(spec), " +
|
|
1831
1929
|
"permissions(recordId) -> { list() -> Promise<{ data, meta }>, grant(body), update(permId, patch), revoke(permId) } } }. " +
|
|
1832
|
-
"`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema(); `interpret` backs useInterpretDraft() (sc-4932 — drafts column values from one sentence; writes nothing). " +
|
|
1930
|
+
"`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema() and useBoundColumns(); `interpret` backs useInterpretDraft() (sc-4932 — drafts column values from one sentence; writes nothing); `myPermissions` backs useCanWrite() (sc-5206 — the caller's effective table/record write permission). " +
|
|
1833
1931
|
"List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case (author column values keep their author-given names).",
|
|
1834
1932
|
required: true,
|
|
1835
|
-
fields: {
|
|
1933
|
+
fields: {
|
|
1934
|
+
records: "function",
|
|
1935
|
+
schema: "function",
|
|
1936
|
+
tables: "object",
|
|
1937
|
+
interpret: "function",
|
|
1938
|
+
myPermissions: "function",
|
|
1939
|
+
},
|
|
1836
1940
|
},
|
|
1837
1941
|
directory: {
|
|
1838
1942
|
description:
|
|
@@ -3065,7 +3169,37 @@ const CONTRACT = deepFreeze({
|
|
|
3065
3169
|
// shadcn/Tailwind import's matching custom properties have a themeConfig
|
|
3066
3170
|
// home instead of being reported lost. Host-only plumbing: no scope
|
|
3067
3171
|
// declares `universalFields`, so a third-party manifest is unaffected.
|
|
3068
|
-
|
|
3172
|
+
// 1.70.0: additive (REQ-NAV-STRUCTURE) -- `themeMenuTypes`, the closed
|
|
3173
|
+
// catalogue of navigation shapes an app can take (`sidebar`, `top-bar`,
|
|
3174
|
+
// `bottom-tabs`) with the per-type item cap. Read from `theme_config`
|
|
3175
|
+
// as `navigation.menuType`; absent or unknown resolves to `sidebar`, so
|
|
3176
|
+
// every app that exists today renders and compiles byte-identically.
|
|
3177
|
+
// Resolved through the new `normaliseNavigation` host export rather than
|
|
3178
|
+
// a literal per host.
|
|
3179
|
+
// 1.71.0: additive (REQ-NAV-STRUCTURE) -- `resolveFooterTokens` (host
|
|
3180
|
+
// export). The footer strip's surface was hard-coded white on BOTH hosts
|
|
3181
|
+
// and its items read the SIDEBAR's tokens, which stops being tenable the
|
|
3182
|
+
// moment the strip IS the menu (`bottom-tabs` hides the sidebar panel, so
|
|
3183
|
+
// those tokens have nowhere to be set). A `theme_config.footer` block now
|
|
3184
|
+
// carries its own, every field falling back to the sidebar's so an
|
|
3185
|
+
// untouched workspace is unchanged.
|
|
3186
|
+
// 1.72.0: additive (REQ-NAV-STRUCTURE) -- `themeMenuTypes.*.quickBarMaxItems`
|
|
3187
|
+
// and the `quickBarCap` host export. The sidebar's secondary quick bar
|
|
3188
|
+
// capped at five in TWO places -- a named constant on web and a bare `5`
|
|
3189
|
+
// literal in the compiler -- so the number could drift between the hosts
|
|
3190
|
+
// and no Studio surface could state it at all.
|
|
3191
|
+
// 1.73.0: additive (sc-5206) — three new hooks: `useBoundColumns(tableId,
|
|
3192
|
+
// shape, props)` resolves author-bound column NAMES by name -> case-
|
|
3193
|
+
// insensitive name -> dataType fallback, so a renamed column still
|
|
3194
|
+
// resolves instead of the widget reading `record[props.titleField]`
|
|
3195
|
+
// directly and getting `undefined`; `useStableQuery(buildQuery)`
|
|
3196
|
+
// generalises `useDatastoreQuery`'s own content-diffed stable-reference
|
|
3197
|
+
// trick so a widget stops hand-rolling `useMemo` with a wrong deps
|
|
3198
|
+
// array; `useCanWrite(tableId, { recordId? })` reads a new
|
|
3199
|
+
// `ctx.datastore.myPermissions` client method as a write-permission
|
|
3200
|
+
// FLOOR, replacing the previous prompt-only "read useUser().groupIds /
|
|
3201
|
+
// .roles" guidance. No existing export changed signature.
|
|
3202
|
+
version: "1.73.0",
|
|
3069
3203
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3070
3204
|
hooks: HOOKS,
|
|
3071
3205
|
primitives: PRIMITIVES,
|
|
@@ -3081,6 +3215,7 @@ const CONTRACT = deepFreeze({
|
|
|
3081
3215
|
themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
|
|
3082
3216
|
themeComponentGradient: THEME_COMPONENT_GRADIENT,
|
|
3083
3217
|
themeSpacingScale: THEME_SPACING_SCALE,
|
|
3218
|
+
themeMenuTypes: THEME_MENU_TYPES,
|
|
3084
3219
|
themeWidgetStyles: THEME_WIDGET_STYLES,
|
|
3085
3220
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
3086
3221
|
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|
package/dist/contract.js
CHANGED
|
@@ -159,6 +159,42 @@ const THEME_WIDGET_STYLES = Object.freeze({
|
|
|
159
159
|
// Matches the styleSchema field cap the widget agent is held to.
|
|
160
160
|
maxFieldsPerWidget: 12,
|
|
161
161
|
});
|
|
162
|
+
// REQ-NAV-STRUCTURE: the SHAPE an app's navigation takes. One catalogue, four
|
|
163
|
+
// consumers -- the Studio's Navigation page, Mason's set_theme coercion, the web
|
|
164
|
+
// PlayerChrome and the compiler's navigator -- so a type cannot be offered to an
|
|
165
|
+
// author without every host actually drawing it.
|
|
166
|
+
//
|
|
167
|
+
// `maxItems` caps how many menu pages the chrome draws at once, and its meaning
|
|
168
|
+
// differs per type deliberately. The sidebar's mobile quick bar is a SECONDARY
|
|
169
|
+
// curated bar, so its own cap may drop a page -- the rail still lists every one.
|
|
170
|
+
// A bottom-tabs strip IS the menu, so its cap must never drop one: the surplus
|
|
171
|
+
// moves behind a "More" sheet instead.
|
|
172
|
+
const THEME_MENU_TYPES = Object.freeze({
|
|
173
|
+
sidebar: Object.freeze({
|
|
174
|
+
name: "Sidebar",
|
|
175
|
+
summary:
|
|
176
|
+
"A persistent left rail on desktop; a hamburger drawer plus the optional bottom quick bar on mobile.",
|
|
177
|
+
maxItems: null,
|
|
178
|
+
// The SECONDARY mobile quick bar's cap. It may drop a page past it, because
|
|
179
|
+
// the rail and the drawer still list every one -- the opposite of
|
|
180
|
+
// `bottom-tabs`' `maxItems`, where the strip IS the menu.
|
|
181
|
+
quickBarMaxItems: 5,
|
|
182
|
+
}),
|
|
183
|
+
"top-bar": Object.freeze({
|
|
184
|
+
name: "Top bar",
|
|
185
|
+
summary:
|
|
186
|
+
"A horizontal row of links in the app header, scrolling sideways when it runs out of room. No rail at any width.",
|
|
187
|
+
maxItems: null,
|
|
188
|
+
quickBarMaxItems: null,
|
|
189
|
+
}),
|
|
190
|
+
"bottom-tabs": Object.freeze({
|
|
191
|
+
name: "Bottom tabs",
|
|
192
|
+
summary:
|
|
193
|
+
"A sticky bottom strip at every width -- the phone-native shape. Pages past the cap move behind a More sheet.",
|
|
194
|
+
maxItems: 4,
|
|
195
|
+
quickBarMaxItems: null,
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
162
198
|
const THEME_SPACING_SCALE = Object.freeze({
|
|
163
199
|
min: 0.5,
|
|
164
200
|
max: 2,
|
|
@@ -474,6 +510,23 @@ const HOOKS = [
|
|
|
474
510
|
requiredContextSlice: ["i18n.locale"],
|
|
475
511
|
scopes: null,
|
|
476
512
|
},
|
|
513
|
+
{
|
|
514
|
+
name: "useStableQuery",
|
|
515
|
+
signature: "useStableQuery(buildQuery)",
|
|
516
|
+
description:
|
|
517
|
+
"sc-5206 — keep whatever buildQuery() returns at a STABLE reference across renders when its (JSON-serialised) content hasn't changed, so it can " +
|
|
518
|
+
"be passed straight into useDatastoreQuery's second argument (useDatastoreQuery(tableId, useStableQuery(() => ({...})))) instead of a hand-rolled " +
|
|
519
|
+
"useMemo with an easy-to-get-wrong deps array. This generalises the same trick useDatastoreQuery already applies internally to its own query " +
|
|
520
|
+
"argument. buildQuery is a ZERO-ARG function called every render; pure React state — no ctx, no host required, safe outside a " +
|
|
521
|
+
"WidgetContextProvider. Never throws: a buildQuery that itself throws degrades to a stable undefined, and a result JSON.stringify can't diff " +
|
|
522
|
+
"(e.g. a circular structure) degrades to \"always a new reference\".",
|
|
523
|
+
returnShape: {
|
|
524
|
+
"(returns)":
|
|
525
|
+
"T | undefined // the hook returns buildQuery()'s result directly, not a wrapper object",
|
|
526
|
+
},
|
|
527
|
+
requiredContextSlice: [],
|
|
528
|
+
scopes: null,
|
|
529
|
+
},
|
|
477
530
|
{
|
|
478
531
|
name: "useUser",
|
|
479
532
|
signature: "useUser()",
|
|
@@ -924,6 +977,29 @@ const HOOKS = [
|
|
|
924
977
|
requiredContextSlice: ["datastore.schema"],
|
|
925
978
|
scopes: ["datastore.read:<table>"],
|
|
926
979
|
},
|
|
980
|
+
{
|
|
981
|
+
name: "useBoundColumns",
|
|
982
|
+
signature: "useBoundColumns(tableId, shape, props)",
|
|
983
|
+
description:
|
|
984
|
+
"sc-5206 — resolve author-bound column NAMES from a widget's own props, built on useDatastoreSchema. shape is " +
|
|
985
|
+
"{ [propKey]: { dataType?, optional? } }; props is the widget's own props carrying author-bound column names (e.g. props.titleField === " +
|
|
986
|
+
"'Title'). Resolution order per key: (1) an exact name match on props[key], (2) a case-insensitive name match, (3) the first column whose " +
|
|
987
|
+
"data_type matches shape[key].dataType that no EARLIER key in this call already claimed, (4) unresolved. Unresolved NEVER throws — the key " +
|
|
988
|
+
"reads undefined, so a column an author renamed after install still resolves instead of crashing the widget. Returns { columns, resolved, " +
|
|
989
|
+
"missing, loading, error }: columns holds the resolved column NAME — a drop-in replacement for record[props.titleField] -> " +
|
|
990
|
+
"record[columns.titleField]; resolved holds the full Column object (snake_case verbatim, as useDatastoreSchema returns it) so a caller can " +
|
|
991
|
+
"inspect resolved.<key>.data_type; missing lists every key whose spec is not optional:true and did not resolve. Falsy tableId collapses to " +
|
|
992
|
+
"{ columns: {}, resolved: {}, missing: Object.keys(shape), loading: false, error: null } without a network round-trip.",
|
|
993
|
+
returnShape: {
|
|
994
|
+
columns: "{ [propKey]: string | undefined }",
|
|
995
|
+
resolved: "{ [propKey]: Column | undefined }",
|
|
996
|
+
missing: "string[] // required (non-optional) keys that did not resolve",
|
|
997
|
+
loading: "boolean",
|
|
998
|
+
error: "DatastoreError | null",
|
|
999
|
+
},
|
|
1000
|
+
requiredContextSlice: ["datastore.schema"],
|
|
1001
|
+
scopes: ["datastore.read:<table>"],
|
|
1002
|
+
},
|
|
927
1003
|
{
|
|
928
1004
|
name: "useInterpretDraft",
|
|
929
1005
|
signature: "useInterpretDraft(tableId)",
|
|
@@ -1295,6 +1371,28 @@ const HOOKS = [
|
|
|
1295
1371
|
requiredContextSlice: ["datastore.records"],
|
|
1296
1372
|
scopes: ["acl.write:records"],
|
|
1297
1373
|
},
|
|
1374
|
+
// sc-5206 — table/record write-permission floor, reading the injected
|
|
1375
|
+
// ctx.datastore.myPermissions.
|
|
1376
|
+
{
|
|
1377
|
+
name: "useCanWrite",
|
|
1378
|
+
signature: "useCanWrite(tableId, options?)",
|
|
1379
|
+
description:
|
|
1380
|
+
"sc-5206 — is the signed-in caller permitted to write to tableId (or, with options.recordId, to that one row)? Returns { canWrite, loading, " +
|
|
1381
|
+
"error, refetch }, reading the injected ctx.datastore.myPermissions(tableId, { recordId }). This is a FLOOR, not a full replacement for " +
|
|
1382
|
+
"domain-specific write rules: a widget whose rule is more specific than the table ACL (e.g. 'only the assigned user may edit this row') must " +
|
|
1383
|
+
"still hand-check that in addition to useCanWrite (user.id === record[assigneeField]). It only answers 'signed in AND permitted' — it does " +
|
|
1384
|
+
"NOT distinguish 'not signed in' from 'signed in but forbidden' (both resolve canWrite:false); pair it with useUser() when the UI needs to " +
|
|
1385
|
+
"tell those apart. myPermissions is a newer client method: a host that has not injected it is NOT a hard error — this hook collapses to " +
|
|
1386
|
+
"{ canWrite: false, loading: false, error: null, refetch: async () => undefined } rather than throwing. Same collapse when tableId is falsy.",
|
|
1387
|
+
returnShape: {
|
|
1388
|
+
canWrite: "boolean",
|
|
1389
|
+
loading: "boolean",
|
|
1390
|
+
error: "DatastoreError | null",
|
|
1391
|
+
refetch: "() => Promise<void>",
|
|
1392
|
+
},
|
|
1393
|
+
requiredContextSlice: ["datastore.myPermissions"],
|
|
1394
|
+
scopes: ["datastore.read:<table>"],
|
|
1395
|
+
},
|
|
1298
1396
|
// REQ-RT-07 — realtime table subscription.
|
|
1299
1397
|
{
|
|
1300
1398
|
name: "useDatastoreSubscription",
|
|
@@ -1826,13 +1924,19 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1826
1924
|
datastore: {
|
|
1827
1925
|
description:
|
|
1828
1926
|
"Injected @colixsystems/datastore-client instance. " +
|
|
1829
|
-
"{ tables: { list(), get(idOrName), interpret(idOrName, body) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
|
|
1927
|
+
"{ tables: { list(), get(idOrName), interpret(idOrName, body), myPermissions(idOrName, { recordId? }) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
|
|
1830
1928
|
"records(tableId) -> { list(query) -> Promise<{ data, meta }>, get(id), create(values), update(id, values), delete(id), aggregate(spec), " +
|
|
1831
1929
|
"permissions(recordId) -> { list() -> Promise<{ data, meta }>, grant(body), update(permId, patch), revoke(permId) } } }. " +
|
|
1832
|
-
"`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema(); `interpret` backs useInterpretDraft() (sc-4932 — drafts column values from one sentence; writes nothing). " +
|
|
1930
|
+
"`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema() and useBoundColumns(); `interpret` backs useInterpretDraft() (sc-4932 — drafts column values from one sentence; writes nothing); `myPermissions` backs useCanWrite() (sc-5206 — the caller's effective table/record write permission). " +
|
|
1833
1931
|
"List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case (author column values keep their author-given names).",
|
|
1834
1932
|
required: true,
|
|
1835
|
-
fields: {
|
|
1933
|
+
fields: {
|
|
1934
|
+
records: "function",
|
|
1935
|
+
schema: "function",
|
|
1936
|
+
tables: "object",
|
|
1937
|
+
interpret: "function",
|
|
1938
|
+
myPermissions: "function",
|
|
1939
|
+
},
|
|
1836
1940
|
},
|
|
1837
1941
|
directory: {
|
|
1838
1942
|
description:
|
|
@@ -3065,7 +3169,37 @@ const CONTRACT = deepFreeze({
|
|
|
3065
3169
|
// shadcn/Tailwind import's matching custom properties have a themeConfig
|
|
3066
3170
|
// home instead of being reported lost. Host-only plumbing: no scope
|
|
3067
3171
|
// declares `universalFields`, so a third-party manifest is unaffected.
|
|
3068
|
-
|
|
3172
|
+
// 1.70.0: additive (REQ-NAV-STRUCTURE) -- `themeMenuTypes`, the closed
|
|
3173
|
+
// catalogue of navigation shapes an app can take (`sidebar`, `top-bar`,
|
|
3174
|
+
// `bottom-tabs`) with the per-type item cap. Read from `theme_config`
|
|
3175
|
+
// as `navigation.menuType`; absent or unknown resolves to `sidebar`, so
|
|
3176
|
+
// every app that exists today renders and compiles byte-identically.
|
|
3177
|
+
// Resolved through the new `normaliseNavigation` host export rather than
|
|
3178
|
+
// a literal per host.
|
|
3179
|
+
// 1.71.0: additive (REQ-NAV-STRUCTURE) -- `resolveFooterTokens` (host
|
|
3180
|
+
// export). The footer strip's surface was hard-coded white on BOTH hosts
|
|
3181
|
+
// and its items read the SIDEBAR's tokens, which stops being tenable the
|
|
3182
|
+
// moment the strip IS the menu (`bottom-tabs` hides the sidebar panel, so
|
|
3183
|
+
// those tokens have nowhere to be set). A `theme_config.footer` block now
|
|
3184
|
+
// carries its own, every field falling back to the sidebar's so an
|
|
3185
|
+
// untouched workspace is unchanged.
|
|
3186
|
+
// 1.72.0: additive (REQ-NAV-STRUCTURE) -- `themeMenuTypes.*.quickBarMaxItems`
|
|
3187
|
+
// and the `quickBarCap` host export. The sidebar's secondary quick bar
|
|
3188
|
+
// capped at five in TWO places -- a named constant on web and a bare `5`
|
|
3189
|
+
// literal in the compiler -- so the number could drift between the hosts
|
|
3190
|
+
// and no Studio surface could state it at all.
|
|
3191
|
+
// 1.73.0: additive (sc-5206) — three new hooks: `useBoundColumns(tableId,
|
|
3192
|
+
// shape, props)` resolves author-bound column NAMES by name -> case-
|
|
3193
|
+
// insensitive name -> dataType fallback, so a renamed column still
|
|
3194
|
+
// resolves instead of the widget reading `record[props.titleField]`
|
|
3195
|
+
// directly and getting `undefined`; `useStableQuery(buildQuery)`
|
|
3196
|
+
// generalises `useDatastoreQuery`'s own content-diffed stable-reference
|
|
3197
|
+
// trick so a widget stops hand-rolling `useMemo` with a wrong deps
|
|
3198
|
+
// array; `useCanWrite(tableId, { recordId? })` reads a new
|
|
3199
|
+
// `ctx.datastore.myPermissions` client method as a write-permission
|
|
3200
|
+
// FLOOR, replacing the previous prompt-only "read useUser().groupIds /
|
|
3201
|
+
// .roles" guidance. No existing export changed signature.
|
|
3202
|
+
version: "1.73.0",
|
|
3069
3203
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3070
3204
|
hooks: HOOKS,
|
|
3071
3205
|
primitives: PRIMITIVES,
|
|
@@ -3081,6 +3215,7 @@ const CONTRACT = deepFreeze({
|
|
|
3081
3215
|
themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
|
|
3082
3216
|
themeComponentGradient: THEME_COMPONENT_GRADIENT,
|
|
3083
3217
|
themeSpacingScale: THEME_SPACING_SCALE,
|
|
3218
|
+
themeMenuTypes: THEME_MENU_TYPES,
|
|
3084
3219
|
themeWidgetStyles: THEME_WIDGET_STYLES,
|
|
3085
3220
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
3086
3221
|
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|