@colixsystems/widget-sdk 0.266.1 → 0.267.1
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 +68 -2
- package/dist/contract.cjs +12 -6
- package/dist/contract.js +12 -6
- package/dist/hooks.js +3 -3
- package/dist/index.d.ts +28 -2
- package/package.json +1 -1
- package/src/contract.js +12 -6
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
50
50
|
| **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. |
|
|
51
51
|
| **CORE** | `useLinkPreview(url)` | `{ preview, loading, error, available }` — `preview` is `{ url, title, description, site_name, image_url } \| null` | `ctx.linkPreviews.get` — no scope. A link's title, description, site name and image, fetched by the platform (the device never contacts the linked site). A page with nothing to show resolves with null fields, not an error — render the plain link. A non-http(s) `url` makes no request. Memoized for up to an hour across widgets; concurrent calls for one URL share a request. `error` is a `LinkPreviewError` (`VALIDATION` \| `AUTH_REQUIRED` \| `RATE_LIMITED` \| `NETWORK` \| `INTERNAL`). `available` is false on a host with no link-preview client |
|
|
52
52
|
| **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". |
|
|
53
|
-
| **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options)` (optional: options) | `{ data, total, loading, fetching, error, refetch }` — `loading` only until the first result for the table; a changed query / refetch keeps `data` while `fetching`; `total` is the envelope's `meta.total` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read
|
|
53
|
+
| **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options)` (optional: options) | `{ data, total, loading, fetching, error, refetch }` — `loading` only until the first result for the table; a changed query / refetch keeps `data` while `fetching`; `total` is the envelope's `meta.total` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*`. `options` is `{ filter, filterMode, q, sort, limit, offset, includeCapabilities }`, applied on the server: see *Querying records*. |
|
|
54
54
|
| **DATASTORE** | `useDatastoreFigure(table, { aggregate, column, filter })` (optional: options) | `{ value, text, truncated, loading, fetching, error, refetch }` | `records(table).list` (a count reads the envelope total; a sum or average up to 1000 rows) — `datastore.read:*`. One count / sum / average for a `<Stat>`, computed exactly as the built-in Stat widget does. |
|
|
55
55
|
| **DATASTORE** | `useDatastoreRecord(table, id, options)` (optional: options) | `{ data, loading, fetching, error, refetch }` — a refetch of the same record keeps `data` while `fetching`; `{ includeCapabilities: true }` adds `data._capabilities` | `records(table).get` — `datastore.read:<table>` |
|
|
56
56
|
| **DATASTORE** | `useDatastoreSchema(tableId)` | `{ schema, loading, error, refetch }` | `schema(tableId)` — `datastore.read:<table>` |
|
|
@@ -86,7 +86,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
86
86
|
|
|
87
87
|
## Status
|
|
88
88
|
|
|
89
|
-
`v0.
|
|
89
|
+
`v0.267.1` — 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**.
|
|
90
|
+
|
|
91
|
+
### What's new in 0.267.1 (contract 1.217.0)
|
|
92
|
+
|
|
93
|
+
**The record query options are documented where an author reads them.** A new *Querying records* section lists everything `useDatastoreQuery` sends to the server — `filter` and its operators, `filterMode`, `q`, `sort`, `limit` (50 by default, at most 1000) and `offset` — with `total` for paging and a searchable, paged list to start from. The `Query` type now declares `q`, `offset`, `filterMode` and both filter and sort shapes, and marks `cursor`, which the datastore never read, deprecated. No runtime change.
|
|
94
|
+
|
|
95
|
+
### What's new in 0.267.0 (contract 1.217.0)
|
|
96
|
+
|
|
97
|
+
**Every text field can raise the right keyboard.** The `TextInput` primitive (and `<TextField>`, which passes every `TextInput` prop through) now documents `keyboardType` — `"email-address"`, `"phone-pad"`, `"url"`, `"number-pad"`, `"decimal-pad"` — plus `autoCapitalize`, `autoCorrect`, `autoComplete` and `secureTextEntry`. They already worked on both hosts (react-native-web turns `keyboardType` into the input's `type` / `inputMode`); now the contract and the AI widget agent say to set them. A `useDatastoreSchema` column's `text_format` (`"EMAIL"` | `"PHONE"` | `"URL"` | a JSON format | `null`) is typed on `DatastoreSchemaColumn`, so a typed widget can pick the keyboard from it.
|
|
90
98
|
|
|
91
99
|
### What's new in 0.266.1 (contract 1.216.0)
|
|
92
100
|
|
|
@@ -3539,6 +3547,64 @@ export default function OpenTasks({ tableId, titleField, statusField }) {
|
|
|
3539
3547
|
|
|
3540
3548
|
`useCanWrite(tableId)` answers "may I create a row here". It says nothing about changing an EXISTING row: gate a list's Edit / Delete on each row's `_capabilities` (read with `includeCapabilities: true`, see *What's new in 0.174.0*), and a single-record page on `useCanWrite(tableId, { recordId })` or `useDatastoreRecord(…, { includeCapabilities: true })`. Never call `useCanWrite` with a `recordId` per list row — that is one request per row. Either answer 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 (`user.id === record[assigneeField]`).
|
|
3541
3549
|
|
|
3550
|
+
## Querying records
|
|
3551
|
+
|
|
3552
|
+
`useDatastoreQuery(tableId, query)` hands `query` to the datastore, which filters, searches, sorts and pages the table before a row reaches the widget. Every key is optional. Build the object with `useStableQuery` so it does not refetch every render.
|
|
3553
|
+
|
|
3554
|
+
| Option | What the server does with it |
|
|
3555
|
+
| --- | --- |
|
|
3556
|
+
| `filter` | Keeps the rows that match. A map `{ column: "op:value" }` (an array of expressions puts a range on one column), or an array `[{ column, operator, value }]`. |
|
|
3557
|
+
| `filterMode` | `"and"` (the default) keeps rows matching every condition; `"or"` keeps rows matching any. |
|
|
3558
|
+
| `q` | Free-text search: a case-insensitive substring match across every text column of the table (encrypted columns excepted). |
|
|
3559
|
+
| `sort` | `{ field, dir }` with `dir` `"asc"` or `"desc"`, or the string `"field:desc"`. One column; `created_at` and `updated_at` work too. |
|
|
3560
|
+
| `limit` | Rows per page: 50 when left out, at most 1000. |
|
|
3561
|
+
| `offset` | Rows to skip, for the next page. |
|
|
3562
|
+
| `includeCapabilities` | `true` stamps each row with `_capabilities`, what the signed-in user may do to it. |
|
|
3563
|
+
|
|
3564
|
+
The filter operators are `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains` (a case-insensitive substring), `empty` and `nempty` (no value). In the array form a condition may carry `valueMode`: `"currentUser"` or `"currentUserGroup"` with `eq` or `neq` matches the signed-in user (or their groups) on a USER or USER_GROUP column, and `"relativeDate"` with an ordering operator reads `value` as "that many days ago".
|
|
3565
|
+
|
|
3566
|
+
The result's `total` is how many rows match in all, not just this page: show "12 of 312" and page with `offset`. For a count, a sum or an average, use `useDatastoreFigure(tableId, { aggregate, column, filter })` instead of reading rows.
|
|
3567
|
+
|
|
3568
|
+
**Narrow on the server, never over a fetched batch.** A widget that fetches `limit: 1000` and then filters or searches the rows in JavaScript only ever sees the first 1000; once the table grows past that, it misses rows without saying so. Send the search box's text as `q`, a chip's condition as `filter`, and page with `offset`. The checker warns about the batch pattern.
|
|
3569
|
+
|
|
3570
|
+
```js
|
|
3571
|
+
import { useEffect, useState } from "react";
|
|
3572
|
+
import { Pressable, Text, TextInput, View, useDatastoreQuery, useStableQuery } from "@colixsystems/widget-sdk";
|
|
3573
|
+
|
|
3574
|
+
const PAGE = 50;
|
|
3575
|
+
|
|
3576
|
+
export default function ContactList({ tableId }) {
|
|
3577
|
+
const [draft, setDraft] = useState("");
|
|
3578
|
+
const [search, setSearch] = useState("");
|
|
3579
|
+
const [status, setStatus] = useState("");
|
|
3580
|
+
const [page, setPage] = useState(0);
|
|
3581
|
+
useEffect(() => {
|
|
3582
|
+
const wait = setTimeout(() => { setSearch(draft.trim()); setPage(0); }, 250);
|
|
3583
|
+
return () => clearTimeout(wait);
|
|
3584
|
+
}, [draft]);
|
|
3585
|
+
const query = useStableQuery(() => ({
|
|
3586
|
+
q: search,
|
|
3587
|
+
filter: status ? { status: `eq:${status}` } : undefined,
|
|
3588
|
+
sort: { field: "last_contacted", dir: "desc" },
|
|
3589
|
+
limit: PAGE,
|
|
3590
|
+
offset: page * PAGE,
|
|
3591
|
+
}));
|
|
3592
|
+
const { data, total, loading } = useDatastoreQuery(tableId, query);
|
|
3593
|
+
const pick = (next) => { setStatus(next); setPage(0); };
|
|
3594
|
+
return (
|
|
3595
|
+
<View>
|
|
3596
|
+
<TextInput value={draft} onChangeText={setDraft} placeholder="Search" />
|
|
3597
|
+
<Pressable onPress={() => pick(status === "Won" ? "" : "Won")}><Text>Won</Text></Pressable>
|
|
3598
|
+
{loading ? <Text>Loading…</Text> : data.map((row) => <Text key={row.id}>{row.name}</Text>)}
|
|
3599
|
+
<Text>{total ?? 0} contacts</Text>
|
|
3600
|
+
{(page + 1) * PAGE < (total ?? 0) ? (
|
|
3601
|
+
<Pressable onPress={() => setPage(page + 1)}><Text>Next page</Text></Pressable>
|
|
3602
|
+
) : null}
|
|
3603
|
+
</View>
|
|
3604
|
+
);
|
|
3605
|
+
}
|
|
3606
|
+
```
|
|
3607
|
+
|
|
3542
3608
|
## Cross-platform widgets (single-file vs split)
|
|
3543
3609
|
|
|
3544
3610
|
Every widget runs in **both** the web Player and the exported native (Expo) app.
|
package/dist/contract.cjs
CHANGED
|
@@ -2028,13 +2028,16 @@ const HOOKS = [
|
|
|
2028
2028
|
"widget can resolve a stored columnId to the column's name / dataType / " +
|
|
2029
2029
|
"relation target at runtime (e.g. a form that renders inputs by column " +
|
|
2030
2030
|
"type). Returns { schema, loading, error, refetch } where schema is " +
|
|
2031
|
-
"{ id, name, columns: [{ id, name,
|
|
2032
|
-
"
|
|
2031
|
+
"{ id, name, columns: [{ id, name, data_type, required, relation_type, " +
|
|
2032
|
+
"target_table_id, is_identification, text_format }] } (null until " +
|
|
2033
|
+
"loaded; snake_case as the server sends it). `text_format` is " +
|
|
2034
|
+
"\"EMAIL\" | \"PHONE\" | \"URL\" (or a JSON format, or null) and names the " +
|
|
2035
|
+
"keyboard a field for that column should raise. Reads only the " +
|
|
2033
2036
|
"structure, never row data; gated by the same datastore.read ACL as " +
|
|
2034
2037
|
"record reads, so a public-grant table resolves for anonymous visitors.",
|
|
2035
2038
|
returnShape: {
|
|
2036
2039
|
schema:
|
|
2037
|
-
"{ id, name, columns: [{ id, name,
|
|
2040
|
+
"{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification, text_format }] } | null",
|
|
2038
2041
|
loading: "boolean",
|
|
2039
2042
|
error: "DatastoreError | null",
|
|
2040
2043
|
refetch: "() => Promise<void>",
|
|
@@ -3248,7 +3251,7 @@ const PRIMITIVES = [
|
|
|
3248
3251
|
{
|
|
3249
3252
|
name: "TextInput",
|
|
3250
3253
|
description:
|
|
3251
|
-
"Single-line or multi-line text field. Controlled — pass `value` + `onChangeText`. Set `multiline` for a textarea-style field (`numberOfLines` controls visible rows). See https://reactnative.dev/docs/textinput.",
|
|
3254
|
+
"Single-line or multi-line text field. Controlled — pass `value` + `onChangeText`. Set `multiline` for a textarea-style field (`numberOfLines` controls visible rows). Pick the on-screen keyboard from what the field holds — the default alphabetic keyboard is wrong for most structured values: `keyboardType=\"email-address\"` for an email, `\"phone-pad\"` for a phone number, `\"url\"` for a link, `\"number-pad\"` for whole digits (a card or code), `\"decimal-pad\"` for an amount. For an email, link, username or code also pass `autoCapitalize=\"none\"` and `autoCorrect={false}`, so the keyboard never rewrites the value; `autoComplete` (\"email\", \"tel\", \"name\", \"one-time-code\") lets the device offer a saved value, and `secureTextEntry` masks a password. The same props reach the web: react-native-web turns `keyboardType` into the input's `type` / `inputMode`. A column's declared format (`text_format` on a `useDatastoreSchema` column: \"EMAIL\" | \"PHONE\" | \"URL\") names the keyboard for a field bound to it. See https://reactnative.dev/docs/textinput.",
|
|
3252
3255
|
rnComponent: "TextInput",
|
|
3253
3256
|
docsUrl: "https://reactnative.dev/docs/textinput",
|
|
3254
3257
|
},
|
|
@@ -3607,7 +3610,7 @@ const PRIMITIVES = [
|
|
|
3607
3610
|
{
|
|
3608
3611
|
name: "TextField",
|
|
3609
3612
|
description:
|
|
3610
|
-
'Themed text-entry field. `<TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />` — every `TextInput` prop works, and its background, text colour, border, corner radius and height come from the app\'s input look (Design > Components > Inputs), so a widget\'s fields match the built-in forms. `multiline` grows the field to a ceiling and then scrolls inside it. Override one field with `background`, `textColor`, `borderColor`, `radius` or `height`; `style` is merged last. `label` draws a label above the field and names it for a screen reader, `helper` a hint below, `error` a message in the helper\'s place with a danger border (`true` marks it without a message), `disabled` makes it read-only and dimmed; `containerStyle` styles the box around a labelled field, or around the field and its icons. An icon goes INSIDE the field, never beside it: `leadingIcon` / `trailingIcon` take a lucide name (`<TextField trailingIcon="map-pin" … />`), and `onTrailingPress` with a `trailingAccessibilityLabel` makes the trailing one a button (a clear control, "use my location"). Never wrap a field in a bordered row of your own to put an icon or a button beside it: the field draws its own border, so that shows a box inside a box. Use it by default instead of styling a bare `TextInput`; a field it cannot draw (a one-time code split into digit boxes, an amount with a stepper inside its border) is your own build that keeps a label, the input colours of the theme and an error message.',
|
|
3613
|
+
'Themed text-entry field. `<TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />` — every `TextInput` prop works (set `keyboardType` from what the field holds: `"email-address"`, `"phone-pad"`, `"url"`, `"number-pad"`, `"decimal-pad"`), and its background, text colour, border, corner radius and height come from the app\'s input look (Design > Components > Inputs), so a widget\'s fields match the built-in forms. `multiline` grows the field to a ceiling and then scrolls inside it. Override one field with `background`, `textColor`, `borderColor`, `radius` or `height`; `style` is merged last. `label` draws a label above the field and names it for a screen reader, `helper` a hint below, `error` a message in the helper\'s place with a danger border (`true` marks it without a message), `disabled` makes it read-only and dimmed; `containerStyle` styles the box around a labelled field, or around the field and its icons. An icon goes INSIDE the field, never beside it: `leadingIcon` / `trailingIcon` take a lucide name (`<TextField trailingIcon="map-pin" … />`), and `onTrailingPress` with a `trailingAccessibilityLabel` makes the trailing one a button (a clear control, "use my location"). Never wrap a field in a bordered row of your own to put an icon or a button beside it: the field draws its own border, so that shows a box inside a box. Use it by default instead of styling a bare `TextInput`; a field it cannot draw (a one-time code split into digit boxes, an amount with a stepper inside its border) is your own build that keeps a label, the input colours of the theme and an error message.',
|
|
3611
3614
|
rnComponent: null,
|
|
3612
3615
|
docsUrl: null,
|
|
3613
3616
|
},
|
|
@@ -6466,7 +6469,10 @@ const CONTRACT = deepFreeze({
|
|
|
6466
6469
|
// `useUsers().invite` takes `send_invitation` (false stores the invite
|
|
6467
6470
|
// unsent), invite rows carry `sent_at`, `not_sent` is a status, and
|
|
6468
6471
|
// `useInvites().resend` performs the first send.
|
|
6469
|
-
|
|
6472
|
+
// 1.217.0: additive (sc-4747) — `TextInput` documents `keyboardType`,
|
|
6473
|
+
// `autoCapitalize`, `autoCorrect`, `autoComplete`, `secureTextEntry`;
|
|
6474
|
+
// `useDatastoreSchema` columns document `text_format` (snake_case wire).
|
|
6475
|
+
version: "1.217.0",
|
|
6470
6476
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6471
6477
|
hooks: HOOKS,
|
|
6472
6478
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -2025,13 +2025,16 @@ const HOOKS = [
|
|
|
2025
2025
|
"widget can resolve a stored columnId to the column's name / dataType / " +
|
|
2026
2026
|
"relation target at runtime (e.g. a form that renders inputs by column " +
|
|
2027
2027
|
"type). Returns { schema, loading, error, refetch } where schema is " +
|
|
2028
|
-
"{ id, name, columns: [{ id, name,
|
|
2029
|
-
"
|
|
2028
|
+
"{ id, name, columns: [{ id, name, data_type, required, relation_type, " +
|
|
2029
|
+
"target_table_id, is_identification, text_format }] } (null until " +
|
|
2030
|
+
"loaded; snake_case as the server sends it). `text_format` is " +
|
|
2031
|
+
"\"EMAIL\" | \"PHONE\" | \"URL\" (or a JSON format, or null) and names the " +
|
|
2032
|
+
"keyboard a field for that column should raise. Reads only the " +
|
|
2030
2033
|
"structure, never row data; gated by the same datastore.read ACL as " +
|
|
2031
2034
|
"record reads, so a public-grant table resolves for anonymous visitors.",
|
|
2032
2035
|
returnShape: {
|
|
2033
2036
|
schema:
|
|
2034
|
-
"{ id, name, columns: [{ id, name,
|
|
2037
|
+
"{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification, text_format }] } | null",
|
|
2035
2038
|
loading: "boolean",
|
|
2036
2039
|
error: "DatastoreError | null",
|
|
2037
2040
|
refetch: "() => Promise<void>",
|
|
@@ -3245,7 +3248,7 @@ const PRIMITIVES = [
|
|
|
3245
3248
|
{
|
|
3246
3249
|
name: "TextInput",
|
|
3247
3250
|
description:
|
|
3248
|
-
"Single-line or multi-line text field. Controlled — pass `value` + `onChangeText`. Set `multiline` for a textarea-style field (`numberOfLines` controls visible rows). See https://reactnative.dev/docs/textinput.",
|
|
3251
|
+
"Single-line or multi-line text field. Controlled — pass `value` + `onChangeText`. Set `multiline` for a textarea-style field (`numberOfLines` controls visible rows). Pick the on-screen keyboard from what the field holds — the default alphabetic keyboard is wrong for most structured values: `keyboardType=\"email-address\"` for an email, `\"phone-pad\"` for a phone number, `\"url\"` for a link, `\"number-pad\"` for whole digits (a card or code), `\"decimal-pad\"` for an amount. For an email, link, username or code also pass `autoCapitalize=\"none\"` and `autoCorrect={false}`, so the keyboard never rewrites the value; `autoComplete` (\"email\", \"tel\", \"name\", \"one-time-code\") lets the device offer a saved value, and `secureTextEntry` masks a password. The same props reach the web: react-native-web turns `keyboardType` into the input's `type` / `inputMode`. A column's declared format (`text_format` on a `useDatastoreSchema` column: \"EMAIL\" | \"PHONE\" | \"URL\") names the keyboard for a field bound to it. See https://reactnative.dev/docs/textinput.",
|
|
3249
3252
|
rnComponent: "TextInput",
|
|
3250
3253
|
docsUrl: "https://reactnative.dev/docs/textinput",
|
|
3251
3254
|
},
|
|
@@ -3604,7 +3607,7 @@ const PRIMITIVES = [
|
|
|
3604
3607
|
{
|
|
3605
3608
|
name: "TextField",
|
|
3606
3609
|
description:
|
|
3607
|
-
'Themed text-entry field. `<TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />` — every `TextInput` prop works, and its background, text colour, border, corner radius and height come from the app\'s input look (Design > Components > Inputs), so a widget\'s fields match the built-in forms. `multiline` grows the field to a ceiling and then scrolls inside it. Override one field with `background`, `textColor`, `borderColor`, `radius` or `height`; `style` is merged last. `label` draws a label above the field and names it for a screen reader, `helper` a hint below, `error` a message in the helper\'s place with a danger border (`true` marks it without a message), `disabled` makes it read-only and dimmed; `containerStyle` styles the box around a labelled field, or around the field and its icons. An icon goes INSIDE the field, never beside it: `leadingIcon` / `trailingIcon` take a lucide name (`<TextField trailingIcon="map-pin" … />`), and `onTrailingPress` with a `trailingAccessibilityLabel` makes the trailing one a button (a clear control, "use my location"). Never wrap a field in a bordered row of your own to put an icon or a button beside it: the field draws its own border, so that shows a box inside a box. Use it by default instead of styling a bare `TextInput`; a field it cannot draw (a one-time code split into digit boxes, an amount with a stepper inside its border) is your own build that keeps a label, the input colours of the theme and an error message.',
|
|
3610
|
+
'Themed text-entry field. `<TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />` — every `TextInput` prop works (set `keyboardType` from what the field holds: `"email-address"`, `"phone-pad"`, `"url"`, `"number-pad"`, `"decimal-pad"`), and its background, text colour, border, corner radius and height come from the app\'s input look (Design > Components > Inputs), so a widget\'s fields match the built-in forms. `multiline` grows the field to a ceiling and then scrolls inside it. Override one field with `background`, `textColor`, `borderColor`, `radius` or `height`; `style` is merged last. `label` draws a label above the field and names it for a screen reader, `helper` a hint below, `error` a message in the helper\'s place with a danger border (`true` marks it without a message), `disabled` makes it read-only and dimmed; `containerStyle` styles the box around a labelled field, or around the field and its icons. An icon goes INSIDE the field, never beside it: `leadingIcon` / `trailingIcon` take a lucide name (`<TextField trailingIcon="map-pin" … />`), and `onTrailingPress` with a `trailingAccessibilityLabel` makes the trailing one a button (a clear control, "use my location"). Never wrap a field in a bordered row of your own to put an icon or a button beside it: the field draws its own border, so that shows a box inside a box. Use it by default instead of styling a bare `TextInput`; a field it cannot draw (a one-time code split into digit boxes, an amount with a stepper inside its border) is your own build that keeps a label, the input colours of the theme and an error message.',
|
|
3608
3611
|
rnComponent: null,
|
|
3609
3612
|
docsUrl: null,
|
|
3610
3613
|
},
|
|
@@ -6463,7 +6466,10 @@ const CONTRACT = deepFreeze({
|
|
|
6463
6466
|
// `useUsers().invite` takes `send_invitation` (false stores the invite
|
|
6464
6467
|
// unsent), invite rows carry `sent_at`, `not_sent` is a status, and
|
|
6465
6468
|
// `useInvites().resend` performs the first send.
|
|
6466
|
-
|
|
6469
|
+
// 1.217.0: additive (sc-4747) — `TextInput` documents `keyboardType`,
|
|
6470
|
+
// `autoCapitalize`, `autoCorrect`, `autoComplete`, `secureTextEntry`;
|
|
6471
|
+
// `useDatastoreSchema` columns document `text_format` (snake_case wire).
|
|
6472
|
+
version: "1.217.0",
|
|
6467
6473
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6468
6474
|
hooks: HOOKS,
|
|
6469
6475
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -3165,9 +3165,9 @@ export function useDatastoreRecord(table, recordId, options) {
|
|
|
3165
3165
|
|
|
3166
3166
|
/**
|
|
3167
3167
|
* Stateful table-schema resolver hook. Returns { schema, loading, error,
|
|
3168
|
-
* refetch } where `schema` is `{ id, name, columns: [{ id, name,
|
|
3169
|
-
* required,
|
|
3170
|
-
* loaded).
|
|
3168
|
+
* refetch } where `schema` is `{ id, name, columns: [{ id, name, data_type,
|
|
3169
|
+
* required, relation_type, target_table_id, is_identification, text_format }] }`
|
|
3170
|
+
* (`null` until loaded) — the server's snake_case, verbatim.
|
|
3171
3171
|
*
|
|
3172
3172
|
* The host's datastore client exposes `schema(table)` which resolves to the
|
|
3173
3173
|
* table's structural metadata — columns and their types, NOT row data. Use it
|
package/dist/index.d.ts
CHANGED
|
@@ -1074,10 +1074,23 @@ export function hasCornerRadius(
|
|
|
1074
1074
|
radius: ResolvedCornerRadius | null | undefined,
|
|
1075
1075
|
): boolean;
|
|
1076
1076
|
|
|
1077
|
+
/** A record query the datastore applies on the server; see the README's "Querying records". */
|
|
1077
1078
|
export interface Query {
|
|
1078
|
-
|
|
1079
|
-
|
|
1079
|
+
/** `{ column: "op:value" }` (an array of expressions for a range), or `[{ column, operator, value, valueMode }]`. */
|
|
1080
|
+
filter?:
|
|
1081
|
+
| Record<string, string | string[]>
|
|
1082
|
+
| Array<{ column: string; operator?: string; value?: unknown; valueMode?: string }>;
|
|
1083
|
+
/** `"and"` (the default) keeps rows matching every condition, `"or"` rows matching any. */
|
|
1084
|
+
filterMode?: "and" | "or";
|
|
1085
|
+
/** A case-insensitive substring search across the table's text columns. */
|
|
1086
|
+
q?: string;
|
|
1087
|
+
/** `{ field, dir }` or `"field:desc"`; one column, so an array uses its first entry. */
|
|
1088
|
+
sort?: string | { field: string; dir?: "asc" | "desc" } | Array<{ field: string; dir?: "asc" | "desc" }>;
|
|
1089
|
+
/** Rows per page: 50 when omitted, at most 1000. */
|
|
1080
1090
|
limit?: number;
|
|
1091
|
+
/** Rows to skip, for the next page. */
|
|
1092
|
+
offset?: number;
|
|
1093
|
+
/** @deprecated The datastore ignores it; page with `offset`. */
|
|
1081
1094
|
cursor?: string;
|
|
1082
1095
|
/**
|
|
1083
1096
|
* Stamp each row with `_capabilities` — what the signed-in user may do to
|
|
@@ -1667,6 +1680,19 @@ export interface DatastoreSchemaColumn {
|
|
|
1667
1680
|
copy_via_column_id?: string | null;
|
|
1668
1681
|
/** Set on a copy the server keeps: the related table's column it copies. */
|
|
1669
1682
|
copy_of_column_id?: string | null;
|
|
1683
|
+
/**
|
|
1684
|
+
* STRING / TEXT columns only — the value's declared format. EMAIL, PHONE and
|
|
1685
|
+
* URL name the keyboard a field for this column should raise
|
|
1686
|
+
* (`keyboardType` "email-address" / "phone-pad" / "url").
|
|
1687
|
+
*/
|
|
1688
|
+
text_format?:
|
|
1689
|
+
| "EMAIL"
|
|
1690
|
+
| "PHONE"
|
|
1691
|
+
| "URL"
|
|
1692
|
+
| "JSON_ARRAY"
|
|
1693
|
+
| "JSON_OBJECT"
|
|
1694
|
+
| "JSON_VALUE"
|
|
1695
|
+
| null;
|
|
1670
1696
|
}
|
|
1671
1697
|
|
|
1672
1698
|
export interface DatastoreSchema {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.267.1",
|
|
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
|
@@ -2025,13 +2025,16 @@ const HOOKS = [
|
|
|
2025
2025
|
"widget can resolve a stored columnId to the column's name / dataType / " +
|
|
2026
2026
|
"relation target at runtime (e.g. a form that renders inputs by column " +
|
|
2027
2027
|
"type). Returns { schema, loading, error, refetch } where schema is " +
|
|
2028
|
-
"{ id, name, columns: [{ id, name,
|
|
2029
|
-
"
|
|
2028
|
+
"{ id, name, columns: [{ id, name, data_type, required, relation_type, " +
|
|
2029
|
+
"target_table_id, is_identification, text_format }] } (null until " +
|
|
2030
|
+
"loaded; snake_case as the server sends it). `text_format` is " +
|
|
2031
|
+
"\"EMAIL\" | \"PHONE\" | \"URL\" (or a JSON format, or null) and names the " +
|
|
2032
|
+
"keyboard a field for that column should raise. Reads only the " +
|
|
2030
2033
|
"structure, never row data; gated by the same datastore.read ACL as " +
|
|
2031
2034
|
"record reads, so a public-grant table resolves for anonymous visitors.",
|
|
2032
2035
|
returnShape: {
|
|
2033
2036
|
schema:
|
|
2034
|
-
"{ id, name, columns: [{ id, name,
|
|
2037
|
+
"{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification, text_format }] } | null",
|
|
2035
2038
|
loading: "boolean",
|
|
2036
2039
|
error: "DatastoreError | null",
|
|
2037
2040
|
refetch: "() => Promise<void>",
|
|
@@ -3245,7 +3248,7 @@ const PRIMITIVES = [
|
|
|
3245
3248
|
{
|
|
3246
3249
|
name: "TextInput",
|
|
3247
3250
|
description:
|
|
3248
|
-
"Single-line or multi-line text field. Controlled — pass `value` + `onChangeText`. Set `multiline` for a textarea-style field (`numberOfLines` controls visible rows). See https://reactnative.dev/docs/textinput.",
|
|
3251
|
+
"Single-line or multi-line text field. Controlled — pass `value` + `onChangeText`. Set `multiline` for a textarea-style field (`numberOfLines` controls visible rows). Pick the on-screen keyboard from what the field holds — the default alphabetic keyboard is wrong for most structured values: `keyboardType=\"email-address\"` for an email, `\"phone-pad\"` for a phone number, `\"url\"` for a link, `\"number-pad\"` for whole digits (a card or code), `\"decimal-pad\"` for an amount. For an email, link, username or code also pass `autoCapitalize=\"none\"` and `autoCorrect={false}`, so the keyboard never rewrites the value; `autoComplete` (\"email\", \"tel\", \"name\", \"one-time-code\") lets the device offer a saved value, and `secureTextEntry` masks a password. The same props reach the web: react-native-web turns `keyboardType` into the input's `type` / `inputMode`. A column's declared format (`text_format` on a `useDatastoreSchema` column: \"EMAIL\" | \"PHONE\" | \"URL\") names the keyboard for a field bound to it. See https://reactnative.dev/docs/textinput.",
|
|
3249
3252
|
rnComponent: "TextInput",
|
|
3250
3253
|
docsUrl: "https://reactnative.dev/docs/textinput",
|
|
3251
3254
|
},
|
|
@@ -3604,7 +3607,7 @@ const PRIMITIVES = [
|
|
|
3604
3607
|
{
|
|
3605
3608
|
name: "TextField",
|
|
3606
3609
|
description:
|
|
3607
|
-
'Themed text-entry field. `<TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />` — every `TextInput` prop works, and its background, text colour, border, corner radius and height come from the app\'s input look (Design > Components > Inputs), so a widget\'s fields match the built-in forms. `multiline` grows the field to a ceiling and then scrolls inside it. Override one field with `background`, `textColor`, `borderColor`, `radius` or `height`; `style` is merged last. `label` draws a label above the field and names it for a screen reader, `helper` a hint below, `error` a message in the helper\'s place with a danger border (`true` marks it without a message), `disabled` makes it read-only and dimmed; `containerStyle` styles the box around a labelled field, or around the field and its icons. An icon goes INSIDE the field, never beside it: `leadingIcon` / `trailingIcon` take a lucide name (`<TextField trailingIcon="map-pin" … />`), and `onTrailingPress` with a `trailingAccessibilityLabel` makes the trailing one a button (a clear control, "use my location"). Never wrap a field in a bordered row of your own to put an icon or a button beside it: the field draws its own border, so that shows a box inside a box. Use it by default instead of styling a bare `TextInput`; a field it cannot draw (a one-time code split into digit boxes, an amount with a stepper inside its border) is your own build that keeps a label, the input colours of the theme and an error message.',
|
|
3610
|
+
'Themed text-entry field. `<TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />` — every `TextInput` prop works (set `keyboardType` from what the field holds: `"email-address"`, `"phone-pad"`, `"url"`, `"number-pad"`, `"decimal-pad"`), and its background, text colour, border, corner radius and height come from the app\'s input look (Design > Components > Inputs), so a widget\'s fields match the built-in forms. `multiline` grows the field to a ceiling and then scrolls inside it. Override one field with `background`, `textColor`, `borderColor`, `radius` or `height`; `style` is merged last. `label` draws a label above the field and names it for a screen reader, `helper` a hint below, `error` a message in the helper\'s place with a danger border (`true` marks it without a message), `disabled` makes it read-only and dimmed; `containerStyle` styles the box around a labelled field, or around the field and its icons. An icon goes INSIDE the field, never beside it: `leadingIcon` / `trailingIcon` take a lucide name (`<TextField trailingIcon="map-pin" … />`), and `onTrailingPress` with a `trailingAccessibilityLabel` makes the trailing one a button (a clear control, "use my location"). Never wrap a field in a bordered row of your own to put an icon or a button beside it: the field draws its own border, so that shows a box inside a box. Use it by default instead of styling a bare `TextInput`; a field it cannot draw (a one-time code split into digit boxes, an amount with a stepper inside its border) is your own build that keeps a label, the input colours of the theme and an error message.',
|
|
3608
3611
|
rnComponent: null,
|
|
3609
3612
|
docsUrl: null,
|
|
3610
3613
|
},
|
|
@@ -6463,7 +6466,10 @@ const CONTRACT = deepFreeze({
|
|
|
6463
6466
|
// `useUsers().invite` takes `send_invitation` (false stores the invite
|
|
6464
6467
|
// unsent), invite rows carry `sent_at`, `not_sent` is a status, and
|
|
6465
6468
|
// `useInvites().resend` performs the first send.
|
|
6466
|
-
|
|
6469
|
+
// 1.217.0: additive (sc-4747) — `TextInput` documents `keyboardType`,
|
|
6470
|
+
// `autoCapitalize`, `autoCorrect`, `autoComplete`, `secureTextEntry`;
|
|
6471
|
+
// `useDatastoreSchema` columns document `text_format` (snake_case wire).
|
|
6472
|
+
version: "1.217.0",
|
|
6467
6473
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6468
6474
|
hooks: HOOKS,
|
|
6469
6475
|
primitives: PRIMITIVES,
|