@colixsystems/widget-sdk 0.176.0 → 0.179.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -3
- package/dist/card-style.js +44 -0
- package/dist/card-view.js +21 -6
- package/dist/contract.cjs +52 -4
- package/dist/contract.js +52 -4
- package/dist/datastore-figure.js +27 -0
- package/dist/figure.js +59 -0
- package/dist/hooks.js +10 -5
- package/dist/index.d.ts +38 -1
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,8 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
43
43
|
| **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. |
|
|
44
44
|
| **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. |
|
|
45
45
|
| **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". |
|
|
46
|
-
| **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options)` (optional: options) | `{ data, loading, fetching, error, refetch }` — `loading` only until the first result for the table; a changed query / refetch keeps `data` while `fetching` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
|
|
46
|
+
| **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:*` |
|
|
47
|
+
| **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. |
|
|
47
48
|
| **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>` |
|
|
48
49
|
| **DATASTORE** | `useDatastoreSchema(tableId)` | `{ schema, loading, error, refetch }` | `schema(tableId)` — `datastore.read:<table>` |
|
|
49
50
|
| **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 }`. |
|
|
@@ -73,7 +74,29 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
73
74
|
|
|
74
75
|
## Status
|
|
75
76
|
|
|
76
|
-
`v0.
|
|
77
|
+
`v0.179.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**.
|
|
78
|
+
|
|
79
|
+
### What's new in 0.179.0 (contract 1.140.0)
|
|
80
|
+
|
|
81
|
+
**`<Card>` no longer discards the layout in its `style` (sc-9200).** `style` was read for the `card*` design fields alone, so `<Card style={{ width: "100%" }}>` sized nothing and every layout key vanished without a warning — a widget could not lay its cards out by its measured width, because the `widthTier()` branch set exactly that. A card that brings no `baseStyle` now also takes the box's **layout** from `style`: width, height, the flex properties, `alignSelf`, `gap` and position (`CARD_LAYOUT_STYLE_FIELDS`). The app's card look is kept:
|
|
82
|
+
|
|
83
|
+
```jsx
|
|
84
|
+
<Card style={{ width: "100%", maxWidth: 480, alignSelf: "center" }}>…</Card>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Layout only, deliberately: a widget is told to hand `<Card>` its whole declared style bag, so forwarding everything that is not a `card*` field would put design fields (`background`, `fontSize`, `titleColor`) on the box — and react-native-web renders an unknown key as real CSS where React Native drops it, which is one widget rendering two different cards. The paint stays the card's own vocabulary (`cardBackground`, `cardRadius`, `padding`, `shadow`); anything else about the container goes on `containerStyle`.
|
|
88
|
+
|
|
89
|
+
A card given a `baseStyle` is unchanged: that caller owns the container's layout, so its `style` is still read for the `card*` fields only — the contract the built-in widgets pass their per-instance style on.
|
|
90
|
+
|
|
91
|
+
### What's new in 0.178.0 (contract 1.139.0)
|
|
92
|
+
|
|
93
|
+
**New optional manifest field `drawsFigures` (sc-9150).** A boolean (default `false`) that declares your widget draws a strip of live figures, such as "12 pending" or "€4,200 this month", about the records it shows, or is itself a strip of figures. Set it `true` when your widget draws them (for example with `useDatastoreFigure` and `<Stat>`). The AppStudio app-builder then counts the widget as the page's figures when it reuses it on a page, and does not plan a second strip of figures beside it. Leave it `false` or omit it when the widget draws no figures. Additive: existing manifests omit it and read `false`, so no widget needs changing.
|
|
94
|
+
|
|
95
|
+
### What's new in 0.177.0 (contract 1.138.0)
|
|
96
|
+
|
|
97
|
+
**`useDatastoreFigure(tableId, { aggregate, column, filter })` — one live figure over a table, for a `<Stat>`.** A custom widget showing "12 pending" above its list had to reduce `useDatastoreQuery` rows by hand, and nothing told it to skip blank cells, apply a filter, or admit the 1000-row query ceiling. The built-in Stat widget's figure logic is now this hook, and the Stat widget computes through it. `aggregate` is `"count"` (the default), `"sum"` or `"average"` of the number `column`, and `filter` takes the same shapes as `useDatastoreQuery`. It returns `{ value, text, truncated, loading, fetching, error, refetch }`: pass `text` to `<Stat value>`. It is null while loading or when there is no figure, so the tile prints its empty value, never a fake 0. A count is exact: it reads the server's total and fetches a single row. A sum or average reads up to 1000 rows and, when the table holds more, reads "≈ n" with `truncated: true`. Filter on a bound column only once `useBoundColumns` has resolved (pass `null` as the table until then), or the first read is unfiltered.
|
|
98
|
+
|
|
99
|
+
**`useDatastoreQuery` returns `total`**: how many rows the query matches in all (the envelope's `meta.total`), not only the page in `data`.
|
|
77
100
|
|
|
78
101
|
### What's new in 0.176.0 (contract 1.137.0)
|
|
79
102
|
|
|
@@ -160,7 +183,7 @@ Installed widgets pick up the row fix with no rebuild on the web: the Player han
|
|
|
160
183
|
- **`<Badge>`** is a record's state (Active, Paid, Overdue) beside the thing it describes. `tone` is `neutral` | `info` | `success` | `warning` | `danger`, tinted from the app's theme; the label always shows.
|
|
161
184
|
- **`<Avatar>`** is a person: a picture, else the name's initials, else a person glyph. `size` is `sm` | `md` | `lg` | `xl` or a number of px.
|
|
162
185
|
- **`<Progress>`** is real progress toward a goal, as a `bar` or a `ring`; the percent is clamped to 0-100 and printed.
|
|
163
|
-
- **`<Stat>`** displays a headline figure
|
|
186
|
+
- **`<Stat>`** displays a headline figure (compute a live one with `useDatastoreFigure`), with an optional caption, icon and delta. An empty value prints `emptyValue` (default "—"), never a fake 0.
|
|
164
187
|
- **`<Divider>`** splits two groups inside one surface, optionally captioned. A `<List>` and a `<Card>` already draw their own edges.
|
|
165
188
|
|
|
166
189
|
`initialsFor(name)` is exported too. Additive: the built-in widgets render as before.
|
package/dist/card-style.js
CHANGED
|
@@ -62,6 +62,50 @@ export function cardOverlay(style) {
|
|
|
62
62
|
return { ...cardSurfaceStyle(style), ...marginStyle(style || {}) };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The React Native layout properties a card may be given through `style` —
|
|
67
|
+
* how big the box is and where it sits, nothing about how it is painted.
|
|
68
|
+
*
|
|
69
|
+
* An ALLOWLIST, because a widget is told to hand `<Card>` its whole declared
|
|
70
|
+
* style bag: forwarding whatever is not a `card*` field would put design
|
|
71
|
+
* fields (`background`, `fontSize`, `titleColor`) on the box, and
|
|
72
|
+
* react-native-web renders an unknown key as real CSS where native drops it —
|
|
73
|
+
* two hosts, one widget, different cards (CLAUDE.md §8). Paint stays the
|
|
74
|
+
* card's own vocabulary (`cardBackground`, `cardRadius`, `padding`, `shadow`);
|
|
75
|
+
* anything else on the container goes through `containerStyle`.
|
|
76
|
+
*/
|
|
77
|
+
export const CARD_LAYOUT_STYLE_FIELDS = Object.freeze([
|
|
78
|
+
"width", "minWidth", "maxWidth",
|
|
79
|
+
"height", "minHeight", "maxHeight",
|
|
80
|
+
"aspectRatio",
|
|
81
|
+
"flex", "flexBasis", "flexGrow", "flexShrink", "alignSelf",
|
|
82
|
+
"flexDirection", "flexWrap", "justifyContent", "alignItems", "alignContent",
|
|
83
|
+
"gap", "rowGap", "columnGap",
|
|
84
|
+
"position", "top", "right", "bottom", "left", "start", "end", "zIndex",
|
|
85
|
+
"display", "overflow",
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The layout a card's `style` declares. Returning it is what keeps
|
|
90
|
+
* `<Card style={{ width: "100%" }}>` from being silently dropped; a card
|
|
91
|
+
* declaring none yields null, so it emits no extra style object.
|
|
92
|
+
*
|
|
93
|
+
* An `undefined` value is skipped rather than copied: react-native-web's
|
|
94
|
+
* styleq ignores it while React Native's flattenStyle assigns it, so copying
|
|
95
|
+
* one would clear the theme's value on native alone.
|
|
96
|
+
*/
|
|
97
|
+
export function cardLayoutStyle(style) {
|
|
98
|
+
if (!style || typeof style !== "object" || Array.isArray(style)) return null;
|
|
99
|
+
const out = {};
|
|
100
|
+
let any = false;
|
|
101
|
+
for (const key of CARD_LAYOUT_STYLE_FIELDS) {
|
|
102
|
+
if (style[key] === undefined) continue;
|
|
103
|
+
out[key] = style[key];
|
|
104
|
+
any = true;
|
|
105
|
+
}
|
|
106
|
+
return any ? out : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
65
109
|
/**
|
|
66
110
|
* A gradient value as `<Gradient>` props, or null when unpaintable. A
|
|
67
111
|
* per-instance value runs through the same normaliser the theme uses, so the
|
package/dist/card-view.js
CHANGED
|
@@ -9,7 +9,12 @@ import React from "react";
|
|
|
9
9
|
import { useHostTheme } from "./hooks.js";
|
|
10
10
|
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
11
11
|
import { CONTRACT } from "./contract.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
cardOverlay,
|
|
14
|
+
cardSurfaceStyle,
|
|
15
|
+
cardGradientProps,
|
|
16
|
+
cardLayoutStyle,
|
|
17
|
+
} from "./card-style.js";
|
|
13
18
|
import { normaliseThemeComponents } from "./theme-components.js";
|
|
14
19
|
|
|
15
20
|
// A wrapped scroller stays transparent and fits inside the gradient.
|
|
@@ -75,9 +80,16 @@ export function makeCard(rn) {
|
|
|
75
80
|
|
|
76
81
|
/**
|
|
77
82
|
* @param {object} [props.style] — `card*` fields, `shadow`, `margin`
|
|
78
|
-
* (a widget's per-instance style, theme tokens already folded in).
|
|
83
|
+
* (a widget's per-instance style, theme tokens already folded in). On a
|
|
84
|
+
* card with no `baseStyle` it also carries the box's LAYOUT
|
|
85
|
+
* (`CARD_LAYOUT_STYLE_FIELDS`), so `style={{ width: "100%" }}` sizes the
|
|
86
|
+
* card rather than vanishing. Under `as` + a gradient the layout rides the
|
|
87
|
+
* gradient — the card's real box — so the content keys there
|
|
88
|
+
* (`flexDirection`, `gap`, …) arrange the wrapper, not the inner children.
|
|
79
89
|
* @param {object|Array} [props.baseStyle] — the caller's own base style. A
|
|
80
|
-
* card given none starts from the theme's surface and `card` scope.
|
|
90
|
+
* card given none starts from the theme's surface and `card` scope. A
|
|
91
|
+
* caller that supplies one owns the container's layout, so its `style` is
|
|
92
|
+
* read for `card*` fields only — that is the built-in widgets' contract.
|
|
81
93
|
* @param {React.ElementType} [props.as] — container; anything but `View`
|
|
82
94
|
* renders INSIDE the gradient, never as it. Other props land on it.
|
|
83
95
|
* @param {boolean} [props.fillInGradient] — the inner container stretches to
|
|
@@ -117,6 +129,8 @@ export function makeCard(rn) {
|
|
|
117
129
|
...explicit,
|
|
118
130
|
};
|
|
119
131
|
const base = standalone ? themedBase(theme) : baseStyle;
|
|
132
|
+
// Last, so an author's own declaration beats the theme's surface.
|
|
133
|
+
const layout = standalone ? cardLayoutStyle(style) : null;
|
|
120
134
|
|
|
121
135
|
const overlay = withoutMargin ? cardSurfaceStyle(fields) : cardOverlay(fields);
|
|
122
136
|
const gradient = cardGradientProps(fields);
|
|
@@ -124,7 +138,7 @@ export function makeCard(rn) {
|
|
|
124
138
|
if (!gradient) {
|
|
125
139
|
return React.createElement(
|
|
126
140
|
Container,
|
|
127
|
-
{ style: composeStyle([base, overlay], containerStyle), ...rest },
|
|
141
|
+
{ style: composeStyle([base, overlay, layout], containerStyle), ...rest },
|
|
128
142
|
children,
|
|
129
143
|
);
|
|
130
144
|
}
|
|
@@ -139,7 +153,7 @@ export function makeCard(rn) {
|
|
|
139
153
|
{
|
|
140
154
|
colors: gradient.colors,
|
|
141
155
|
angle: gradient.angle,
|
|
142
|
-
style: composeStyle([base, paint], containerStyle),
|
|
156
|
+
style: composeStyle([base, paint, layout], containerStyle),
|
|
143
157
|
...rest,
|
|
144
158
|
},
|
|
145
159
|
children,
|
|
@@ -153,7 +167,8 @@ export function makeCard(rn) {
|
|
|
153
167
|
{
|
|
154
168
|
colors: gradient.colors,
|
|
155
169
|
angle: gradient.angle,
|
|
156
|
-
|
|
170
|
+
// Before the clip: it keeps the gradient inside the corners, not a default.
|
|
171
|
+
style: [base, paint, layout, CLIP_TO_RADIUS],
|
|
157
172
|
},
|
|
158
173
|
React.createElement(
|
|
159
174
|
Container,
|
package/dist/contract.cjs
CHANGED
|
@@ -1395,9 +1395,43 @@ const HOOKS = [
|
|
|
1395
1395
|
"`_capabilities` { can_read, can_write, can_delete, can_grant } — what " +
|
|
1396
1396
|
"the signed-in user may do to THAT row, the answer update/delete " +
|
|
1397
1397
|
"enforce, computed for the whole page in one lookup. Gate a row's " +
|
|
1398
|
-
"Edit on `row._capabilities?.can_write` and Delete on `can_delete`."
|
|
1398
|
+
"Edit on `row._capabilities?.can_write` and Delete on `can_delete`. " +
|
|
1399
|
+
"`total` is how many rows the query matches in all (the envelope's " +
|
|
1400
|
+
"meta.total), not just the page in `data`; null until known.",
|
|
1399
1401
|
returnShape: {
|
|
1400
1402
|
data: "Record[]",
|
|
1403
|
+
total: "number | null",
|
|
1404
|
+
loading: "boolean",
|
|
1405
|
+
fetching: "boolean",
|
|
1406
|
+
error: "DatastoreError | null",
|
|
1407
|
+
refetch: "() => Promise<void>",
|
|
1408
|
+
},
|
|
1409
|
+
requiredContextSlice: ["datastore.records"],
|
|
1410
|
+
scopes: ["datastore.read:*"],
|
|
1411
|
+
},
|
|
1412
|
+
{
|
|
1413
|
+
name: "useDatastoreFigure",
|
|
1414
|
+
signature: "useDatastoreFigure(tableId, { aggregate, column, filter })",
|
|
1415
|
+
optionalArgs: ["aggregate", "column", "filter"],
|
|
1416
|
+
description:
|
|
1417
|
+
"ONE live figure over a table, for a `<Stat>`: `aggregate` is " +
|
|
1418
|
+
"\"count\" (the default; the rows, or the rows `filter` matches), \"sum\" " +
|
|
1419
|
+
"or \"average\" (of the NUMBER `column`). `filter` takes the same shapes " +
|
|
1420
|
+
"as useDatastoreQuery's; name a bound column through useBoundColumns and " +
|
|
1421
|
+
"pass null as the table until its `loading` is false, or the first read " +
|
|
1422
|
+
"runs unfiltered: useDatastoreFigure(columnsLoading ? null : tableId, " +
|
|
1423
|
+
"{ filter: [{ column: bound.statusField, operator: \"eq\", value: \"Pending\" }] }). " +
|
|
1424
|
+
"Render `<Stat value={figure.text} label=\"Pending\" />`: `text` is null " +
|
|
1425
|
+
"while loading or when there is no figure, so the tile prints its empty " +
|
|
1426
|
+
"value, never a fake 0. A count is exact, from the server's total. A sum " +
|
|
1427
|
+
"or average reads up to 1000 rows, skips blank cells rather than counting " +
|
|
1428
|
+
"them as zero, and past that reads \"≈ n\" (`truncated: true`). The stock " +
|
|
1429
|
+
"Stat widget computes its figure through this hook, so use it rather than " +
|
|
1430
|
+
"reducing useDatastoreQuery rows by hand. Call it once per figure.",
|
|
1431
|
+
returnShape: {
|
|
1432
|
+
value: "number | null",
|
|
1433
|
+
text: "string | null // the formatted figure for <Stat value>",
|
|
1434
|
+
truncated: "boolean",
|
|
1401
1435
|
loading: "boolean",
|
|
1402
1436
|
fetching: "boolean",
|
|
1403
1437
|
error: "DatastoreError | null",
|
|
@@ -2418,7 +2452,7 @@ const PRIMITIVES = [
|
|
|
2418
2452
|
{
|
|
2419
2453
|
name: "Stat",
|
|
2420
2454
|
description:
|
|
2421
|
-
'Themed headline figure. `<Stat value={openCount} label="Open orders" delta="+3 this week" deltaTone="positive" icon="package" />`. THE way to show one headline number in a summary tile; put peer tiles side by side in a row. It only DISPLAYS a figure,
|
|
2455
|
+
'Themed headline figure. `<Stat value={openCount} label="Open orders" delta="+3 this week" deltaTone="positive" icon="package" />`. THE way to show one headline number in a summary tile; put peer tiles side by side in a row. It only DISPLAYS a figure: compute a count, sum or average over a table with `useDatastoreFigure` and pass its `text`. An empty value (null, undefined, blank text, NaN) prints `emptyValue` (default "—") in the muted tier, never a fake 0. Props: `value` (string or number), `label`, `icon` (lucide name), `delta`, `deltaTone` ("neutral" | "positive" | "negative"), `emptyValue`, `color` and `fontSize` (12-96) for the figure, `style`, `testID`.',
|
|
2422
2456
|
rnComponent: null,
|
|
2423
2457
|
docsUrl: null,
|
|
2424
2458
|
},
|
|
@@ -2439,7 +2473,7 @@ const PRIMITIVES = [
|
|
|
2439
2473
|
{
|
|
2440
2474
|
name: "Card",
|
|
2441
2475
|
description:
|
|
2442
|
-
'Themed card surface. `<Card>…</Card>` — THE way to draw a card, panel or tile: with no props it takes the app\'s card look (Theme Settings > Cards: background, border, corner radius, padding, shadow, gradient) so it matches every built-in card. Override one card with `background`, `borderColor`, `radius`, `padding` (a number or `{ top, right, bottom, left }`), `shadow` ("none" | "sm" | "md" | "lg") or `gradient` (`{ from, to, angle }`), or pass `style={props.style}` when your widget declares the `cardBackground` / `cardBorderColor` / `cardRadius` / `cardPadding` / `cardGradient` / `shadow` / `margin` style fields, so an author can restyle it. A gradient paints on both hosts. `as` renders the card as another container (e.g. `Pressable`, `ScrollView`) and every other prop lands on it. Never hand-roll a card from `View` + border + radius.',
|
|
2476
|
+
'Themed card surface. `<Card>…</Card>` — THE way to draw a card, panel or tile: with no props it takes the app\'s card look (Theme Settings > Cards: background, border, corner radius, padding, shadow, gradient) so it matches every built-in card. Override one card with `background`, `borderColor`, `radius`, `padding` (a number or `{ top, right, bottom, left }`), `shadow` ("none" | "sm" | "md" | "lg") or `gradient` (`{ from, to, angle }`), or pass `style={props.style}` when your widget declares the `cardBackground` / `cardBorderColor` / `cardRadius` / `cardPadding` / `cardGradient` / `shadow` / `margin` style fields, so an author can restyle it. A card that brings no `baseStyle` also takes the box\'s LAYOUT from `style` — width, height, the flex properties, `alignSelf`, `gap`, position — so `style={{ width: \'100%\', maxWidth: 480, alignSelf: \'center\' }}` sizes the card and still keeps the app\'s card look; put anything else about the container on `containerStyle`. A gradient paints on both hosts. `as` renders the card as another container (e.g. `Pressable`, `ScrollView`) and every other prop lands on it. Never hand-roll a card from `View` + border + radius.',
|
|
2443
2477
|
rnComponent: null,
|
|
2444
2478
|
docsUrl: null,
|
|
2445
2479
|
},
|
|
@@ -2655,6 +2689,13 @@ const MANIFEST_SCHEMA = {
|
|
|
2655
2689
|
"Optional (default false). TRUE when the widget renders its OWN section header — a heading (and optional subtitle), plus any primary action for its section — making it a self-contained section. The AppStudio app-builder reads this when it places an already-installed widget: it lays the widget out as the WHOLE section and does NOT add a standalone heading or a duplicate action button above it, so the section is never double-titled. Set it to match what the component actually renders; leave it false (or omit) for a content-only widget whose heading the page supplies.",
|
|
2656
2690
|
default: false,
|
|
2657
2691
|
},
|
|
2692
|
+
drawsFigures: {
|
|
2693
|
+
type: "boolean",
|
|
2694
|
+
required: false,
|
|
2695
|
+
description:
|
|
2696
|
+
"Optional (default false). TRUE when the widget draws a strip of live figures (counts, totals, averages) about the records it shows, or is itself a strip of figures. The AppStudio app-builder reads this when it places an already-installed widget: it counts the widget as the page's figures, so it does not plan a second strip of figures beside it. Set it to match what the component actually renders; leave it false (or omit) when the widget draws no figures.",
|
|
2697
|
+
default: false,
|
|
2698
|
+
},
|
|
2658
2699
|
events: {
|
|
2659
2700
|
type: "object[]",
|
|
2660
2701
|
required: true,
|
|
@@ -4716,7 +4757,14 @@ const CONTRACT = deepFreeze({
|
|
|
4716
4757
|
// without saving it, through the new `device.pdfViewer` host slice. And
|
|
4717
4758
|
// `createObjectURL` joins `bannedApis`: widgets reached for it to fake a
|
|
4718
4759
|
// "print version" as a plain-text blob that never worked on native.
|
|
4719
|
-
|
|
4760
|
+
// 1.137.0: additive (sc-9142) — `manifest.description` is optional.
|
|
4761
|
+
// 1.138.0: additive (sc-9150) — `useDatastoreFigure`, and
|
|
4762
|
+
// `useDatastoreQuery` returns the envelope's `total`.
|
|
4763
|
+
// 1.139.0: additive (sc-9150) — new OPTIONAL manifest field `drawsFigures`
|
|
4764
|
+
// (boolean, default false): the widget draws live figures about its
|
|
4765
|
+
// records, so the app-builder counts it as the page's figures when it
|
|
4766
|
+
// reuses it. Existing manifests omit it and read false.
|
|
4767
|
+
version: "1.140.0",
|
|
4720
4768
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4721
4769
|
hooks: HOOKS,
|
|
4722
4770
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1395,9 +1395,43 @@ const HOOKS = [
|
|
|
1395
1395
|
"`_capabilities` { can_read, can_write, can_delete, can_grant } — what " +
|
|
1396
1396
|
"the signed-in user may do to THAT row, the answer update/delete " +
|
|
1397
1397
|
"enforce, computed for the whole page in one lookup. Gate a row's " +
|
|
1398
|
-
"Edit on `row._capabilities?.can_write` and Delete on `can_delete`."
|
|
1398
|
+
"Edit on `row._capabilities?.can_write` and Delete on `can_delete`. " +
|
|
1399
|
+
"`total` is how many rows the query matches in all (the envelope's " +
|
|
1400
|
+
"meta.total), not just the page in `data`; null until known.",
|
|
1399
1401
|
returnShape: {
|
|
1400
1402
|
data: "Record[]",
|
|
1403
|
+
total: "number | null",
|
|
1404
|
+
loading: "boolean",
|
|
1405
|
+
fetching: "boolean",
|
|
1406
|
+
error: "DatastoreError | null",
|
|
1407
|
+
refetch: "() => Promise<void>",
|
|
1408
|
+
},
|
|
1409
|
+
requiredContextSlice: ["datastore.records"],
|
|
1410
|
+
scopes: ["datastore.read:*"],
|
|
1411
|
+
},
|
|
1412
|
+
{
|
|
1413
|
+
name: "useDatastoreFigure",
|
|
1414
|
+
signature: "useDatastoreFigure(tableId, { aggregate, column, filter })",
|
|
1415
|
+
optionalArgs: ["aggregate", "column", "filter"],
|
|
1416
|
+
description:
|
|
1417
|
+
"ONE live figure over a table, for a `<Stat>`: `aggregate` is " +
|
|
1418
|
+
"\"count\" (the default; the rows, or the rows `filter` matches), \"sum\" " +
|
|
1419
|
+
"or \"average\" (of the NUMBER `column`). `filter` takes the same shapes " +
|
|
1420
|
+
"as useDatastoreQuery's; name a bound column through useBoundColumns and " +
|
|
1421
|
+
"pass null as the table until its `loading` is false, or the first read " +
|
|
1422
|
+
"runs unfiltered: useDatastoreFigure(columnsLoading ? null : tableId, " +
|
|
1423
|
+
"{ filter: [{ column: bound.statusField, operator: \"eq\", value: \"Pending\" }] }). " +
|
|
1424
|
+
"Render `<Stat value={figure.text} label=\"Pending\" />`: `text` is null " +
|
|
1425
|
+
"while loading or when there is no figure, so the tile prints its empty " +
|
|
1426
|
+
"value, never a fake 0. A count is exact, from the server's total. A sum " +
|
|
1427
|
+
"or average reads up to 1000 rows, skips blank cells rather than counting " +
|
|
1428
|
+
"them as zero, and past that reads \"≈ n\" (`truncated: true`). The stock " +
|
|
1429
|
+
"Stat widget computes its figure through this hook, so use it rather than " +
|
|
1430
|
+
"reducing useDatastoreQuery rows by hand. Call it once per figure.",
|
|
1431
|
+
returnShape: {
|
|
1432
|
+
value: "number | null",
|
|
1433
|
+
text: "string | null // the formatted figure for <Stat value>",
|
|
1434
|
+
truncated: "boolean",
|
|
1401
1435
|
loading: "boolean",
|
|
1402
1436
|
fetching: "boolean",
|
|
1403
1437
|
error: "DatastoreError | null",
|
|
@@ -2418,7 +2452,7 @@ const PRIMITIVES = [
|
|
|
2418
2452
|
{
|
|
2419
2453
|
name: "Stat",
|
|
2420
2454
|
description:
|
|
2421
|
-
'Themed headline figure. `<Stat value={openCount} label="Open orders" delta="+3 this week" deltaTone="positive" icon="package" />`. THE way to show one headline number in a summary tile; put peer tiles side by side in a row. It only DISPLAYS a figure,
|
|
2455
|
+
'Themed headline figure. `<Stat value={openCount} label="Open orders" delta="+3 this week" deltaTone="positive" icon="package" />`. THE way to show one headline number in a summary tile; put peer tiles side by side in a row. It only DISPLAYS a figure: compute a count, sum or average over a table with `useDatastoreFigure` and pass its `text`. An empty value (null, undefined, blank text, NaN) prints `emptyValue` (default "—") in the muted tier, never a fake 0. Props: `value` (string or number), `label`, `icon` (lucide name), `delta`, `deltaTone` ("neutral" | "positive" | "negative"), `emptyValue`, `color` and `fontSize` (12-96) for the figure, `style`, `testID`.',
|
|
2422
2456
|
rnComponent: null,
|
|
2423
2457
|
docsUrl: null,
|
|
2424
2458
|
},
|
|
@@ -2439,7 +2473,7 @@ const PRIMITIVES = [
|
|
|
2439
2473
|
{
|
|
2440
2474
|
name: "Card",
|
|
2441
2475
|
description:
|
|
2442
|
-
'Themed card surface. `<Card>…</Card>` — THE way to draw a card, panel or tile: with no props it takes the app\'s card look (Theme Settings > Cards: background, border, corner radius, padding, shadow, gradient) so it matches every built-in card. Override one card with `background`, `borderColor`, `radius`, `padding` (a number or `{ top, right, bottom, left }`), `shadow` ("none" | "sm" | "md" | "lg") or `gradient` (`{ from, to, angle }`), or pass `style={props.style}` when your widget declares the `cardBackground` / `cardBorderColor` / `cardRadius` / `cardPadding` / `cardGradient` / `shadow` / `margin` style fields, so an author can restyle it. A gradient paints on both hosts. `as` renders the card as another container (e.g. `Pressable`, `ScrollView`) and every other prop lands on it. Never hand-roll a card from `View` + border + radius.',
|
|
2476
|
+
'Themed card surface. `<Card>…</Card>` — THE way to draw a card, panel or tile: with no props it takes the app\'s card look (Theme Settings > Cards: background, border, corner radius, padding, shadow, gradient) so it matches every built-in card. Override one card with `background`, `borderColor`, `radius`, `padding` (a number or `{ top, right, bottom, left }`), `shadow` ("none" | "sm" | "md" | "lg") or `gradient` (`{ from, to, angle }`), or pass `style={props.style}` when your widget declares the `cardBackground` / `cardBorderColor` / `cardRadius` / `cardPadding` / `cardGradient` / `shadow` / `margin` style fields, so an author can restyle it. A card that brings no `baseStyle` also takes the box\'s LAYOUT from `style` — width, height, the flex properties, `alignSelf`, `gap`, position — so `style={{ width: \'100%\', maxWidth: 480, alignSelf: \'center\' }}` sizes the card and still keeps the app\'s card look; put anything else about the container on `containerStyle`. A gradient paints on both hosts. `as` renders the card as another container (e.g. `Pressable`, `ScrollView`) and every other prop lands on it. Never hand-roll a card from `View` + border + radius.',
|
|
2443
2477
|
rnComponent: null,
|
|
2444
2478
|
docsUrl: null,
|
|
2445
2479
|
},
|
|
@@ -2651,6 +2685,13 @@ const MANIFEST_SCHEMA = {
|
|
|
2651
2685
|
"Optional (default false). TRUE when the widget renders its OWN section header — a heading (and optional subtitle), plus any primary action for its section — making it a self-contained section. The AppStudio app-builder reads this when it places an already-installed widget: it lays the widget out as the WHOLE section and does NOT add a standalone heading or a duplicate action button above it, so the section is never double-titled. Set it to match what the component actually renders; leave it false (or omit) for a content-only widget whose heading the page supplies.",
|
|
2652
2686
|
default: false,
|
|
2653
2687
|
},
|
|
2688
|
+
drawsFigures: {
|
|
2689
|
+
type: "boolean",
|
|
2690
|
+
required: false,
|
|
2691
|
+
description:
|
|
2692
|
+
"Optional (default false). TRUE when the widget draws a strip of live figures (counts, totals, averages) about the records it shows, or is itself a strip of figures. The AppStudio app-builder reads this when it places an already-installed widget: it counts the widget as the page's figures, so it does not plan a second strip of figures beside it. Set it to match what the component actually renders; leave it false (or omit) when the widget draws no figures.",
|
|
2693
|
+
default: false,
|
|
2694
|
+
},
|
|
2654
2695
|
events: {
|
|
2655
2696
|
type: "object[]",
|
|
2656
2697
|
required: true,
|
|
@@ -4712,7 +4753,14 @@ const CONTRACT = deepFreeze({
|
|
|
4712
4753
|
// without saving it, through the new `device.pdfViewer` host slice. And
|
|
4713
4754
|
// `createObjectURL` joins `bannedApis`: widgets reached for it to fake a
|
|
4714
4755
|
// "print version" as a plain-text blob that never worked on native.
|
|
4715
|
-
|
|
4756
|
+
// 1.137.0: additive (sc-9142) — `manifest.description` is optional.
|
|
4757
|
+
// 1.138.0: additive (sc-9150) — `useDatastoreFigure`, and
|
|
4758
|
+
// `useDatastoreQuery` returns the envelope's `total`.
|
|
4759
|
+
// 1.139.0: additive (sc-9150) — new OPTIONAL manifest field `drawsFigures`
|
|
4760
|
+
// (boolean, default false): the widget draws live figures about its
|
|
4761
|
+
// records, so the app-builder counts it as the page's figures when it
|
|
4762
|
+
// reuses it. Existing manifests omit it and read false.
|
|
4763
|
+
version: "1.140.0",
|
|
4716
4764
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4717
4765
|
hooks: HOOKS,
|
|
4718
4766
|
primitives: PRIMITIVES,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The hook half of ./figure.js: one live figure over a table, shared by the
|
|
2
|
+
// stock Stat widget and custom widgets on both hosts.
|
|
3
|
+
import { useDatastoreQuery } from "./hooks.js";
|
|
4
|
+
import { figureQuery, reduceFigure } from "./figure.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* sc-9150 — one live count, sum or average over a table; see the
|
|
8
|
+
* useDatastoreFigure CONTRACT entry. Built on useDatastoreQuery, so a changed
|
|
9
|
+
* `filter` keeps the previous figure on screen while `fetching`.
|
|
10
|
+
*/
|
|
11
|
+
export function useDatastoreFigure(table, options) {
|
|
12
|
+
const { aggregate, column, filter } = options || {};
|
|
13
|
+
const rows = useDatastoreQuery(table || null, figureQuery(filter, aggregate));
|
|
14
|
+
const figure =
|
|
15
|
+
table && !rows.loading && !rows.error
|
|
16
|
+
? reduceFigure(rows.data, { aggregate, column }, rows.total)
|
|
17
|
+
: null;
|
|
18
|
+
return {
|
|
19
|
+
value: figure ? figure.value : null,
|
|
20
|
+
text: figure ? figure.text : null,
|
|
21
|
+
truncated: figure ? figure.truncated : false,
|
|
22
|
+
loading: rows.loading,
|
|
23
|
+
fetching: rows.fetching,
|
|
24
|
+
error: rows.error,
|
|
25
|
+
refetch: rows.refetch,
|
|
26
|
+
};
|
|
27
|
+
}
|
package/dist/figure.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The pure rules behind `useDatastoreFigure`: which rows a figure reads and how
|
|
2
|
+
// they reduce to the one number a `<Stat>` shows. Plain JS, no React, so the
|
|
3
|
+
// stock Stat and every custom widget count the same way on both hosts.
|
|
4
|
+
|
|
5
|
+
export const FIGURE_AGGREGATES = Object.freeze(["count", "sum", "average"]);
|
|
6
|
+
|
|
7
|
+
// REQ-QUERY-02 clamps every datastore query to 1000 rows, so a sum or average
|
|
8
|
+
// over a bigger table is reduced from a truncated page and must say so.
|
|
9
|
+
export const FIGURE_ROW_CEILING = 1000;
|
|
10
|
+
|
|
11
|
+
/** The aggregate to run: an unknown or missing one counts rows. */
|
|
12
|
+
export function figureAggregate(aggregate) {
|
|
13
|
+
return FIGURE_AGGREGATES.includes(aggregate) ? aggregate : "count";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hasFilter(filter) {
|
|
17
|
+
return Array.isArray(filter)
|
|
18
|
+
? filter.length > 0
|
|
19
|
+
: Boolean(filter) && typeof filter === "object" && Object.keys(filter).length > 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The rows query a figure reads. A count needs only the envelope's exact
|
|
24
|
+
* `meta.total`, so it fetches one row; a sum or average reads up to the ceiling.
|
|
25
|
+
*/
|
|
26
|
+
export function figureQuery(filter, aggregate) {
|
|
27
|
+
const limit = figureAggregate(aggregate) === "count" ? 1 : FIGURE_ROW_CEILING;
|
|
28
|
+
return hasFilter(filter) ? { limit, filter } : { limit };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Whole numbers stay whole; an average keeps two decimals at most.
|
|
32
|
+
function formatNumber(n) {
|
|
33
|
+
return Number.isInteger(n) ? String(n) : String(Math.round(n * 100) / 100);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reduce a figure query's result to `{ value, text, truncated }`, or null when
|
|
38
|
+
* there is nothing honest to show. `total` is how many rows the query matched;
|
|
39
|
+
* a sum or average over fewer rows than that is prefixed "≈ ".
|
|
40
|
+
*/
|
|
41
|
+
export function reduceFigure(rows, { aggregate, column } = {}, total = null) {
|
|
42
|
+
if (!Array.isArray(rows)) return null;
|
|
43
|
+
const known = Number.isFinite(total);
|
|
44
|
+
if (figureAggregate(aggregate) === "count") {
|
|
45
|
+
return known ? { value: total, text: String(total), truncated: false } : null;
|
|
46
|
+
}
|
|
47
|
+
if (!column) return null;
|
|
48
|
+
// Number(null) and Number("") are 0: coercing would count a blank or
|
|
49
|
+
// permission-hidden cell as a real zero. A NUMBER cell arrives as a number.
|
|
50
|
+
const numbers = rows
|
|
51
|
+
.map((row) => (row ? row[column] : undefined))
|
|
52
|
+
.filter((cell) => typeof cell === "number" && Number.isFinite(cell));
|
|
53
|
+
if (numbers.length === 0) return null;
|
|
54
|
+
const sum = numbers.reduce((acc, n) => acc + n, 0);
|
|
55
|
+
const value = aggregate === "average" ? sum / numbers.length : sum;
|
|
56
|
+
if (!Number.isFinite(value)) return null;
|
|
57
|
+
const truncated = known ? total > rows.length : rows.length >= FIGURE_ROW_CEILING;
|
|
58
|
+
return { value, text: `${truncated ? "≈ " : ""}${formatNumber(value)}`, truncated };
|
|
59
|
+
}
|
package/dist/hooks.js
CHANGED
|
@@ -2173,7 +2173,7 @@ function toDatastoreError(err) {
|
|
|
2173
2173
|
// on a genuine loop.
|
|
2174
2174
|
const VOLATILE_QUERY_RENDER_LIMIT = 10;
|
|
2175
2175
|
|
|
2176
|
-
const NO_QUERY_RESULT = { key: null, table: null, data: null, error: null, loaded: false };
|
|
2176
|
+
const NO_QUERY_RESULT = { key: null, table: null, data: null, total: null, error: null, loaded: false };
|
|
2177
2177
|
const NO_RECORD_RESULT = { key: null, data: null, error: null, loaded: false };
|
|
2178
2178
|
|
|
2179
2179
|
function serializeQuery(query) {
|
|
@@ -2189,8 +2189,8 @@ const recordResultKey = (table, recordId, includeCapabilities = false) =>
|
|
|
2189
2189
|
JSON.stringify([table, recordId, includeCapabilities]);
|
|
2190
2190
|
|
|
2191
2191
|
/**
|
|
2192
|
-
* Stateful datastore query hook. Returns { data, loading, fetching,
|
|
2193
|
-
* refetch }.
|
|
2192
|
+
* Stateful datastore query hook. Returns { data, total, loading, fetching,
|
|
2193
|
+
* error, refetch }.
|
|
2194
2194
|
*
|
|
2195
2195
|
* The host injects a `@colixsystems/datastore-client` instance at
|
|
2196
2196
|
* `ctx.datastore`. We call `ctx.datastore.records(table).list(query)`, which
|
|
@@ -2207,6 +2207,9 @@ const recordResultKey = (table, recordId, includeCapabilities = false) =>
|
|
|
2207
2207
|
* `_capabilities` ({ can_read, can_write, can_delete, can_grant }), computed
|
|
2208
2208
|
* for the whole page in one server lookup. Gate a row's Edit / Delete on it.
|
|
2209
2209
|
*
|
|
2210
|
+
* sc-9150 — `total` is the envelope's `meta.total`: how many rows the query
|
|
2211
|
+
* matches, beyond the page in `data`. null until known.
|
|
2212
|
+
*
|
|
2210
2213
|
* sc-8595 — stale-while-revalidate. `loading` is true only until the first
|
|
2211
2214
|
* result for the bound TABLE lands. A changed query on that table, `refetch()`
|
|
2212
2215
|
* and a page pull-to-refresh keep the rows already in `data` on screen with
|
|
@@ -2274,9 +2277,10 @@ export function useDatastoreQuery(table, query) {
|
|
|
2274
2277
|
// The datastore-client returns the { data, meta } list envelope
|
|
2275
2278
|
// verbatim — unwrap to the rows array (default []).
|
|
2276
2279
|
const rows = res && Array.isArray(res.data) ? res.data : [];
|
|
2280
|
+
const total = res && res.meta && Number.isFinite(res.meta.total) ? res.meta.total : null;
|
|
2277
2281
|
// Discard the result if a newer fetch has started since we kicked off.
|
|
2278
2282
|
if (runRef.current !== myRun) return;
|
|
2279
|
-
setSettled({ key, table: t, data: rows, error: null, loaded: true });
|
|
2283
|
+
setSettled({ key, table: t, data: rows, total, error: null, loaded: true });
|
|
2280
2284
|
setInFlight(false);
|
|
2281
2285
|
} catch (err) {
|
|
2282
2286
|
if (runRef.current !== myRun) return;
|
|
@@ -2285,7 +2289,7 @@ export function useDatastoreQuery(table, query) {
|
|
|
2285
2289
|
setSettled((prev) =>
|
|
2286
2290
|
prev.table === t && prev.loaded
|
|
2287
2291
|
? { ...prev, key, error }
|
|
2288
|
-
: { key, table: t, data: emptyRows, error, loaded: false },
|
|
2292
|
+
: { key, table: t, data: emptyRows, total: null, error, loaded: false },
|
|
2289
2293
|
);
|
|
2290
2294
|
setInFlight(false);
|
|
2291
2295
|
}
|
|
@@ -2340,6 +2344,7 @@ export function useDatastoreQuery(table, query) {
|
|
|
2340
2344
|
const hasRows = Boolean(table) && settled.table === table && settled.loaded;
|
|
2341
2345
|
return {
|
|
2342
2346
|
data: hasRows ? settled.data : emptyRows,
|
|
2347
|
+
total: hasRows ? settled.total : null,
|
|
2343
2348
|
loading: !hasRows && busy,
|
|
2344
2349
|
fetching: busy,
|
|
2345
2350
|
error: table && !busy && settled.table === table ? settled.error : null,
|
package/dist/index.d.ts
CHANGED
|
@@ -843,6 +843,8 @@ export interface RecordCapabilities {
|
|
|
843
843
|
|
|
844
844
|
export interface QueryResult<T> {
|
|
845
845
|
data: T[];
|
|
846
|
+
/** How many rows the query matches in all, not just this page; null until known. */
|
|
847
|
+
total: number | null;
|
|
846
848
|
/**
|
|
847
849
|
* True only until the first result for the bound table lands. A changed
|
|
848
850
|
* query on the same table, `refetch()` and pull-to-refresh keep the rows
|
|
@@ -866,6 +868,34 @@ export function useDatastoreQuery<T = unknown>(
|
|
|
866
868
|
query?: Query,
|
|
867
869
|
): QueryResult<T>;
|
|
868
870
|
|
|
871
|
+
export interface FigureOptions {
|
|
872
|
+
/** Default `"count"`. `"sum"` and `"average"` read the number `column`. */
|
|
873
|
+
aggregate?: "count" | "sum" | "average";
|
|
874
|
+
column?: string;
|
|
875
|
+
/** The rows the figure covers, in any shape `Query.filter` accepts. */
|
|
876
|
+
filter?:
|
|
877
|
+
| Query["filter"]
|
|
878
|
+
| Array<{ column: string; operator: string; value?: unknown; valueMode?: string }>;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export interface FigureResult {
|
|
882
|
+
value: number | null;
|
|
883
|
+
/** The formatted figure for `<Stat value>`; null while loading or when there is none. */
|
|
884
|
+
text: string | null;
|
|
885
|
+
/** A sum or average read fewer rows than the table holds, so `text` reads "≈ n". */
|
|
886
|
+
truncated: boolean;
|
|
887
|
+
loading: boolean;
|
|
888
|
+
fetching: boolean;
|
|
889
|
+
error: Error | null;
|
|
890
|
+
refetch(): Promise<void>;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** One live count, sum or average over a table, as the stock Stat computes it. */
|
|
894
|
+
export function useDatastoreFigure(
|
|
895
|
+
table: string | null | undefined,
|
|
896
|
+
options?: FigureOptions,
|
|
897
|
+
): FigureResult;
|
|
898
|
+
|
|
869
899
|
export function useDatastoreMutation<T = unknown>(
|
|
870
900
|
table: string,
|
|
871
901
|
): MutationApi<T>;
|
|
@@ -2775,7 +2805,14 @@ export const Card: (props: {
|
|
|
2775
2805
|
padding?: number | { top?: number; right?: number; bottom?: number; left?: number };
|
|
2776
2806
|
shadow?: "none" | "sm" | "md" | "lg";
|
|
2777
2807
|
gradient?: { from: string; to: string; angle?: number };
|
|
2778
|
-
/**
|
|
2808
|
+
/**
|
|
2809
|
+
* `card*` style fields, `shadow`, `margin` — a widget's per-instance style.
|
|
2810
|
+
* With no `baseStyle` it also carries the box's layout (width, height, the
|
|
2811
|
+
* flex properties, `alignSelf`, `gap`, position), so `{ width: "100%" }`
|
|
2812
|
+
* sizes the card; anything else about the container goes on
|
|
2813
|
+
* `containerStyle`. Read for the `card*` fields alone when `baseStyle` is
|
|
2814
|
+
* given — the contract the built-in widgets pass their whole style bag on.
|
|
2815
|
+
*/
|
|
2779
2816
|
style?: Record<string, any>;
|
|
2780
2817
|
/** The caller's own base style; a card given none starts from the theme. */
|
|
2781
2818
|
baseStyle?: any;
|
package/dist/index.js
CHANGED
|
@@ -104,6 +104,7 @@ export {
|
|
|
104
104
|
// the web variant and index.native.js picks the native variant.
|
|
105
105
|
export { useClipboard, ClipboardError } from "./clipboard.js";
|
|
106
106
|
export { useToast } from "./toast.js";
|
|
107
|
+
export { useDatastoreFigure } from "./datastore-figure.js";
|
|
107
108
|
// sc-6607 — `<Overlay>` is one shared component (./overlay-view.js) bound to
|
|
108
109
|
// each platform's primitives, so a widget's modal escapes its container box
|
|
109
110
|
// identically on the Player and the Expo export.
|
package/dist/index.native.js
CHANGED
|
@@ -102,6 +102,7 @@ export {
|
|
|
102
102
|
// REQ-WSDK-PLATFORM §6 — Tier A hooks (native variants).
|
|
103
103
|
export { useClipboard, ClipboardError } from "./clipboard.native.js";
|
|
104
104
|
export { useToast } from "./toast.native.js";
|
|
105
|
+
export { useDatastoreFigure } from "./datastore-figure.js";
|
|
105
106
|
// sc-6607 — see the note on the web mirror in ./index.js.
|
|
106
107
|
export { Overlay } from "./overlay.native.js";
|
|
107
108
|
// sc-8104 — see the note on the web mirror in ./index.js.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.179.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",
|