@colixsystems/widget-sdk 0.108.0 → 0.110.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 +12 -1
- package/dist/contract.cjs +35 -2
- package/dist/contract.js +35 -2
- package/dist/hooks.js +24 -3
- package/dist/host.d.ts +37 -0
- package/dist/host.js +5 -0
- package/dist/index.d.ts +12 -0
- package/dist/navigation.cjs +94 -2
- package/dist/navigation.js +93 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
46
46
|
| **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 }`. |
|
|
47
47
|
| **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. |
|
|
48
48
|
| **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*` |
|
|
49
|
+
| **DATASTORE** | `useDatastoreSubscription(table, handlers, options?)` | `{ status }` — `"connecting" \| "live" \| "reconnecting" \| "fallback"` | `records(table).subscribe` — `datastore.read:<table>`. Live `onCreated` / `onUpdated` / `onDeleted` off the REQ-RT-07 socket; never throws, resolving to `{ status: "fallback" }` so the widget polls instead. A whole-table subscribe is gated on read-EVERY-row, because one envelope reaches every subscriber of the table — so for a table governed by per-record grants pass `options.scope`: `{ kind: "record", record_id }` for one row, or `{ kind: "parent", relation_column, record_id }` for the rows whose RELATION column points at that parent (the column must carry `inheritAcl`, else the subscribe reports `"fallback"`). Re-subscribes on the scope's VALUES, so a fresh object literal each render is fine. |
|
|
49
50
|
| **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) |
|
|
50
51
|
| **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. |
|
|
51
52
|
| **FILES** (`ctx.assets`) | `useAsset(id)` | `{ url, file, loading, error, refetch }` | `ctx.assets.get` — no scope |
|
|
@@ -68,7 +69,17 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
68
69
|
|
|
69
70
|
## Status
|
|
70
71
|
|
|
71
|
-
`v0.
|
|
72
|
+
`v0.110.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**.
|
|
73
|
+
|
|
74
|
+
### What's new in 0.110.0 (contract 1.84.0)
|
|
75
|
+
|
|
76
|
+
**`resolveSidebarTokens` and `resolveTopBarTokens` — the rail's and the app bar's tokens, resolved once for both hosts (sc-6289).** The footer strip got a shared resolver in 1.71.0; the two chrome parts beside it did not, so their defaults lived inline in the web Player and again in the compiler — and had drifted. An unset `sidebar.backgroundColor` painted the app background in the Expo export where the Player painted it white, so a dark app shipped a dark drawer beside a white rail; an unset `topBar.textColor` painted the app name slate in the export where the Player has always used the brand colour.
|
|
77
|
+
|
|
78
|
+
Both resolvers now own their defaults, which are what the web Player renders, so the export follows the appearance the author approved in the Studio rather than the other way round. Unlike `resolveFooterTokens`, the colours are never `null` — a default that lives in the resolver cannot drift, and one that lived in each host already had. `borderColor` stays nullable: the colour is the divider's switch.
|
|
79
|
+
|
|
80
|
+
The top bar resolves **two** text colours. An unthemed bar paints its icons slate and its app name in the brand colour, and React Navigation's single `headerTintColor` cannot say both — so `tintColor` and `titleColor` are separate, and an authored `topBar.textColor` drives both. `show` is deliberately not among them: it depends on the menu type rather than the theme, and REQ-THEME-14 makes it a no-op on native.
|
|
81
|
+
|
|
82
|
+
Host-integration surface only — nothing a widget imports changed. `CONTRACT.version` → `1.84.0`.
|
|
72
83
|
|
|
73
84
|
### What's new in 0.108.0 (contract 1.82.0)
|
|
74
85
|
|
package/dist/contract.cjs
CHANGED
|
@@ -1469,7 +1469,15 @@ const HOOKS = [
|
|
|
1469
1469
|
"{ status: 'fallback' } WITHOUT throwing so the widget can poll instead. " +
|
|
1470
1470
|
"Reads the same datastore.read scope as useDatastoreQuery — declare " +
|
|
1471
1471
|
"datastore.read for the table you subscribe to. Pair with " +
|
|
1472
|
-
"useDatastoreQuery for the initial load and merge the streamed
|
|
1472
|
+
"useDatastoreQuery for the initial load and merge the streamed " +
|
|
1473
|
+
"envelopes. `options.scope` narrows the subscription: " +
|
|
1474
|
+
"{ kind: 'record', record_id } streams one record, and " +
|
|
1475
|
+
"{ kind: 'parent', relation_column, record_id } streams the rows whose " +
|
|
1476
|
+
"RELATION column points at that parent — the only way to subscribe to a " +
|
|
1477
|
+
"table governed by per-record grants, since a scope-less subscribe must " +
|
|
1478
|
+
"prove read-every-row. A `parent` scope requires the column be an " +
|
|
1479
|
+
"inheritAcl RELATION, else the subscribe resolves to " +
|
|
1480
|
+
"{ status: 'fallback' }.",
|
|
1473
1481
|
returnShape: {
|
|
1474
1482
|
status:
|
|
1475
1483
|
"'connecting' | 'live' | 'reconnecting' | 'fallback' // transport state; 'fallback' → poll",
|
|
@@ -3341,7 +3349,32 @@ const CONTRACT = deepFreeze({
|
|
|
3341
3349
|
// dark theme's spinner visible instead of grey-on-grey.
|
|
3342
3350
|
// `useTheme().colors.loader` is the value a widget reads for its own
|
|
3343
3351
|
// loading state; both hosts paint their chrome spinners from the same key.
|
|
3344
|
-
|
|
3352
|
+
//
|
|
3353
|
+
// 1.83.0: additive (sc-6270) — `useDatastoreSubscription` gains
|
|
3354
|
+
// `options.scope`, narrowing a subscription from the whole table to one
|
|
3355
|
+
// record (`{ kind: "record", record_id }`) or to a parent's inheriting
|
|
3356
|
+
// children (`{ kind: "parent", relation_column, record_id }`). Without it
|
|
3357
|
+
// a table whose rows are governed by PER-RECORD grants could not stream
|
|
3358
|
+
// at all: the plane fans one envelope to every subscriber, so a
|
|
3359
|
+
// scope-less subscribe must prove read-every-row (sc-4311) and a channel
|
|
3360
|
+
// member never can. A scoped subscribe is gated on the per-record read
|
|
3361
|
+
// check instead — the same predicate REST applies per row — and `parent`
|
|
3362
|
+
// additionally requires an `inheritAcl` RELATION column, which is what
|
|
3363
|
+
// makes the stream a strict subset of what `list()` already returns.
|
|
3364
|
+
// Backed by datastore-client 0.15.0. Omitting `scope` is unchanged.
|
|
3365
|
+
// 1.84.0: additive (sc-6289) -- `resolveSidebarTokens` and
|
|
3366
|
+
// `resolveTopBarTokens` (host exports), the rail's and the app bar's
|
|
3367
|
+
// tokens beside the footer's. Their defaults used to be written separately
|
|
3368
|
+
// in each host and had drifted: an unset rail took the app background in
|
|
3369
|
+
// the export where the Player painted it white, and an unset header
|
|
3370
|
+
// painted the app name slate where the Player used the brand colour. The
|
|
3371
|
+
// resolvers own the defaults, so a host cannot keep its own. The top bar
|
|
3372
|
+
// resolves `tintColor` and `titleColor` separately because the web bar has
|
|
3373
|
+
// always painted its icons slate and its name in the brand colour, which
|
|
3374
|
+
// one `headerTintColor` cannot express; an authored `topBar.textColor`
|
|
3375
|
+
// drives both. `show` is deliberately absent -- it depends on the menu
|
|
3376
|
+
// type, not the theme, and is a no-op on native.
|
|
3377
|
+
version: "1.84.0",
|
|
3345
3378
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3346
3379
|
hooks: HOOKS,
|
|
3347
3380
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1469,7 +1469,15 @@ const HOOKS = [
|
|
|
1469
1469
|
"{ status: 'fallback' } WITHOUT throwing so the widget can poll instead. " +
|
|
1470
1470
|
"Reads the same datastore.read scope as useDatastoreQuery — declare " +
|
|
1471
1471
|
"datastore.read for the table you subscribe to. Pair with " +
|
|
1472
|
-
"useDatastoreQuery for the initial load and merge the streamed
|
|
1472
|
+
"useDatastoreQuery for the initial load and merge the streamed " +
|
|
1473
|
+
"envelopes. `options.scope` narrows the subscription: " +
|
|
1474
|
+
"{ kind: 'record', record_id } streams one record, and " +
|
|
1475
|
+
"{ kind: 'parent', relation_column, record_id } streams the rows whose " +
|
|
1476
|
+
"RELATION column points at that parent — the only way to subscribe to a " +
|
|
1477
|
+
"table governed by per-record grants, since a scope-less subscribe must " +
|
|
1478
|
+
"prove read-every-row. A `parent` scope requires the column be an " +
|
|
1479
|
+
"inheritAcl RELATION, else the subscribe resolves to " +
|
|
1480
|
+
"{ status: 'fallback' }.",
|
|
1473
1481
|
returnShape: {
|
|
1474
1482
|
status:
|
|
1475
1483
|
"'connecting' | 'live' | 'reconnecting' | 'fallback' // transport state; 'fallback' → poll",
|
|
@@ -3341,7 +3349,32 @@ const CONTRACT = deepFreeze({
|
|
|
3341
3349
|
// dark theme's spinner visible instead of grey-on-grey.
|
|
3342
3350
|
// `useTheme().colors.loader` is the value a widget reads for its own
|
|
3343
3351
|
// loading state; both hosts paint their chrome spinners from the same key.
|
|
3344
|
-
|
|
3352
|
+
//
|
|
3353
|
+
// 1.83.0: additive (sc-6270) — `useDatastoreSubscription` gains
|
|
3354
|
+
// `options.scope`, narrowing a subscription from the whole table to one
|
|
3355
|
+
// record (`{ kind: "record", record_id }`) or to a parent's inheriting
|
|
3356
|
+
// children (`{ kind: "parent", relation_column, record_id }`). Without it
|
|
3357
|
+
// a table whose rows are governed by PER-RECORD grants could not stream
|
|
3358
|
+
// at all: the plane fans one envelope to every subscriber, so a
|
|
3359
|
+
// scope-less subscribe must prove read-every-row (sc-4311) and a channel
|
|
3360
|
+
// member never can. A scoped subscribe is gated on the per-record read
|
|
3361
|
+
// check instead — the same predicate REST applies per row — and `parent`
|
|
3362
|
+
// additionally requires an `inheritAcl` RELATION column, which is what
|
|
3363
|
+
// makes the stream a strict subset of what `list()` already returns.
|
|
3364
|
+
// Backed by datastore-client 0.15.0. Omitting `scope` is unchanged.
|
|
3365
|
+
// 1.84.0: additive (sc-6289) -- `resolveSidebarTokens` and
|
|
3366
|
+
// `resolveTopBarTokens` (host exports), the rail's and the app bar's
|
|
3367
|
+
// tokens beside the footer's. Their defaults used to be written separately
|
|
3368
|
+
// in each host and had drifted: an unset rail took the app background in
|
|
3369
|
+
// the export where the Player painted it white, and an unset header
|
|
3370
|
+
// painted the app name slate where the Player used the brand colour. The
|
|
3371
|
+
// resolvers own the defaults, so a host cannot keep its own. The top bar
|
|
3372
|
+
// resolves `tintColor` and `titleColor` separately because the web bar has
|
|
3373
|
+
// always painted its icons slate and its name in the brand colour, which
|
|
3374
|
+
// one `headerTintColor` cannot express; an authored `topBar.textColor`
|
|
3375
|
+
// drives both. `show` is deliberately absent -- it depends on the menu
|
|
3376
|
+
// type, not the theme, and is a no-op on native.
|
|
3377
|
+
version: "1.84.0",
|
|
3345
3378
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3346
3379
|
hooks: HOOKS,
|
|
3347
3380
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -2482,7 +2482,9 @@ export function useCanWrite(tableId, options) {
|
|
|
2482
2482
|
*
|
|
2483
2483
|
* @param {string} table Bound table id (falsy → no subscription, status "fallback").
|
|
2484
2484
|
* @param {{ onCreated?, onUpdated?, onDeleted? }} [handlers] Per-event callbacks; each receives the snake_case record.
|
|
2485
|
-
* @param {{ fallbackAfterMs?: number }} [options]
|
|
2485
|
+
* @param {{ fallbackAfterMs?: number, scope?: { kind: "record"|"parent", relation_column?: string, record_id: string } }} [options]
|
|
2486
|
+
* `scope` narrows the stream below whole-table — required for a table
|
|
2487
|
+
* governed by per-record grants. Re-subscribes on its VALUES, not identity.
|
|
2486
2488
|
* @returns {{ status: "connecting" | "live" | "reconnecting" | "fallback" }}
|
|
2487
2489
|
*/
|
|
2488
2490
|
export function useDatastoreSubscription(table, handlers, options) {
|
|
@@ -2510,7 +2512,23 @@ export function useDatastoreSubscription(table, handlers, options) {
|
|
|
2510
2512
|
? options.fallbackAfterMs
|
|
2511
2513
|
: undefined;
|
|
2512
2514
|
|
|
2515
|
+
// sc-6270: a scope narrows the subscription to one record or to a parent's
|
|
2516
|
+
// inheriting children. Authors pass a fresh object literal every render, so
|
|
2517
|
+
// the effect keys on the scope's VALUES — depending on its identity would
|
|
2518
|
+
// tear down and reopen the socket on each render.
|
|
2519
|
+
const scope = options && options.scope ? options.scope : null;
|
|
2520
|
+
const scopeKey = scope
|
|
2521
|
+
? [scope.kind, scope.relation_column, scope.record_id]
|
|
2522
|
+
.map((part) => part || "")
|
|
2523
|
+
.join("|")
|
|
2524
|
+
: "";
|
|
2525
|
+
const scopeRef = useRef(scope);
|
|
2526
|
+
scopeRef.current = scope;
|
|
2527
|
+
|
|
2513
2528
|
useEffect(() => {
|
|
2529
|
+
// Re-scoping starts a new connection, so a stale "live" from the previous
|
|
2530
|
+
// scope must not mask the gap while it opens.
|
|
2531
|
+
setStatus("connecting");
|
|
2514
2532
|
if (!table || typeof recordsRef.current !== "function") {
|
|
2515
2533
|
setStatus("fallback");
|
|
2516
2534
|
return undefined;
|
|
@@ -2527,6 +2545,9 @@ export function useDatastoreSubscription(table, handlers, options) {
|
|
|
2527
2545
|
setStatus("fallback");
|
|
2528
2546
|
return undefined;
|
|
2529
2547
|
}
|
|
2548
|
+
const subscribeOptions = {};
|
|
2549
|
+
if (fallbackAfterMs != null) subscribeOptions.fallbackAfterMs = fallbackAfterMs;
|
|
2550
|
+
if (scopeRef.current) subscribeOptions.scope = scopeRef.current;
|
|
2530
2551
|
const stop = ns.subscribe(
|
|
2531
2552
|
{
|
|
2532
2553
|
onCreated: (r) => handlersRef.current?.onCreated?.(r),
|
|
@@ -2534,13 +2555,13 @@ export function useDatastoreSubscription(table, handlers, options) {
|
|
|
2534
2555
|
onDeleted: (r) => handlersRef.current?.onDeleted?.(r),
|
|
2535
2556
|
onStatus: (s) => setStatus(s),
|
|
2536
2557
|
},
|
|
2537
|
-
|
|
2558
|
+
Object.keys(subscribeOptions).length > 0 ? subscribeOptions : undefined,
|
|
2538
2559
|
);
|
|
2539
2560
|
return () => {
|
|
2540
2561
|
if (typeof stop === "function") stop();
|
|
2541
2562
|
};
|
|
2542
2563
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2543
|
-
}, [table, fallbackAfterMs]);
|
|
2564
|
+
}, [table, fallbackAfterMs, scopeKey]);
|
|
2544
2565
|
|
|
2545
2566
|
return { status };
|
|
2546
2567
|
}
|
package/dist/host.d.ts
CHANGED
|
@@ -194,6 +194,43 @@ export interface FooterTokens {
|
|
|
194
194
|
*/
|
|
195
195
|
export function resolveFooterTokens(theme: unknown): FooterTokens;
|
|
196
196
|
|
|
197
|
+
/** The sidebar rail's resolved tokens. Unlike the footer's, the colours are
|
|
198
|
+
* never `null`: the default lives here so the two hosts cannot each keep a
|
|
199
|
+
* different one. `borderColor` is still nullable — no colour, no divider. */
|
|
200
|
+
export interface SidebarTokens {
|
|
201
|
+
backgroundColor: string;
|
|
202
|
+
textColor: string;
|
|
203
|
+
activeColor: string;
|
|
204
|
+
activeStyle: "filled" | "accent";
|
|
205
|
+
borderColor: string | null;
|
|
206
|
+
borderWidth: number | null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Resolves the sidebar rail's tokens from a whole `theme_config`. Every default
|
|
211
|
+
* is what the web Player renders, so the export follows the Player rather than
|
|
212
|
+
* the other way round.
|
|
213
|
+
*/
|
|
214
|
+
export function resolveSidebarTokens(theme: unknown): SidebarTokens;
|
|
215
|
+
|
|
216
|
+
/** The top app bar's resolved tokens. `tintColor` and `titleColor` are separate
|
|
217
|
+
* because an unthemed bar paints its icons slate and its app name in the brand
|
|
218
|
+
* colour; an authored `topBar.textColor` drives both. */
|
|
219
|
+
export interface TopBarTokens {
|
|
220
|
+
backgroundColor: string;
|
|
221
|
+
tintColor: string;
|
|
222
|
+
titleColor: string;
|
|
223
|
+
borderColor: string | null;
|
|
224
|
+
borderWidth: number | null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Resolves the top app bar's tokens from a whole `theme_config`. `show` is not
|
|
229
|
+
* among them: it depends on the menu type, not the theme, and is a no-op on
|
|
230
|
+
* native.
|
|
231
|
+
*/
|
|
232
|
+
export function resolveTopBarTokens(theme: unknown): TopBarTokens;
|
|
233
|
+
|
|
197
234
|
/** A value a widget may persist: a scalar, or a flat array of them. */
|
|
198
235
|
export type WidgetRouteScalar = string | number | boolean;
|
|
199
236
|
export type WidgetRouteValue = WidgetRouteScalar | WidgetRouteScalar[];
|
package/dist/host.js
CHANGED
|
@@ -50,11 +50,16 @@ export {
|
|
|
50
50
|
// the sidebar's so an untouched workspace keeps the appearance it has. It
|
|
51
51
|
// exists because the strip IS the menu under `bottom-tabs`, where the sidebar
|
|
52
52
|
// panel is hidden and those tokens can no longer be set at all.
|
|
53
|
+
// REQ-THEME-14: `resolveSidebarTokens` and `resolveTopBarTokens` are the same
|
|
54
|
+
// idea for the rail and the app bar, whose defaults used to live inline in each
|
|
55
|
+
// host and had already drifted apart.
|
|
53
56
|
export {
|
|
54
57
|
normaliseNavigation,
|
|
55
58
|
menuItemCap,
|
|
56
59
|
quickBarCap,
|
|
57
60
|
resolveFooterTokens,
|
|
61
|
+
resolveSidebarTokens,
|
|
62
|
+
resolveTopBarTokens,
|
|
58
63
|
} from "./navigation.js";
|
|
59
64
|
|
|
60
65
|
// sc-5717: the codec behind `useWidgetRoute()` — how a widget's own internal
|
package/dist/index.d.ts
CHANGED
|
@@ -1137,8 +1137,16 @@ export interface DatastoreSubscriptionHandlers {
|
|
|
1137
1137
|
onDeleted?: (record: Record<string, unknown>) => void;
|
|
1138
1138
|
}
|
|
1139
1139
|
|
|
1140
|
+
// sc-6270: see SubscriptionScope in @colixsystems/datastore-client. A scoped
|
|
1141
|
+
// subscribe is gated on the per-record read check instead of read-every-row,
|
|
1142
|
+
// which is what lets a per-record-ACL table stream at all.
|
|
1143
|
+
export type DatastoreSubscriptionScope =
|
|
1144
|
+
| { kind: "record"; record_id: string }
|
|
1145
|
+
| { kind: "parent"; relation_column: string; record_id: string };
|
|
1146
|
+
|
|
1140
1147
|
export interface DatastoreSubscriptionOptions {
|
|
1141
1148
|
fallbackAfterMs?: number;
|
|
1149
|
+
scope?: DatastoreSubscriptionScope;
|
|
1142
1150
|
}
|
|
1143
1151
|
|
|
1144
1152
|
/**
|
|
@@ -1148,6 +1156,10 @@ export interface DatastoreSubscriptionOptions {
|
|
|
1148
1156
|
* is `"fallback"` (no socket support, ACL-rejected, or connect timed out) run
|
|
1149
1157
|
* REST polling instead. Never throws — degrades to `{ status: "fallback" }` on
|
|
1150
1158
|
* a host whose datastore client predates realtime.
|
|
1159
|
+
*
|
|
1160
|
+
* `options.scope` narrows the stream to one record or to a parent's
|
|
1161
|
+
* inheriting children; re-subscribes when the scope's values change, not on
|
|
1162
|
+
* every render.
|
|
1151
1163
|
*/
|
|
1152
1164
|
export function useDatastoreSubscription(
|
|
1153
1165
|
tableId: string | null | undefined,
|
package/dist/navigation.cjs
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// the SAME literal, so what a planner may persist and what a host draws cannot
|
|
26
26
|
// diverge.
|
|
27
27
|
|
|
28
|
-
const { CONTRACT } = require("./contract.cjs");
|
|
28
|
+
const { CONTRACT, isHexColor } = require("./contract.cjs");
|
|
29
29
|
|
|
30
30
|
// Absent or unknown resolves here, so every app authored before menu types
|
|
31
31
|
// existed renders and compiles byte-identically.
|
|
@@ -114,6 +114,98 @@ function resolveFooterTokens(theme) {
|
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// REQ-THEME-14 — the rail's and the app bar's tokens, resolved once for both
|
|
118
|
+
// hosts.
|
|
119
|
+
//
|
|
120
|
+
// These return FULLY resolved values where `resolveFooterTokens` above returns
|
|
121
|
+
// raw-or-null, and the difference is deliberate. The footer's two hosts happen
|
|
122
|
+
// to agree on their separately-written defaults; the rail's and the bar's did
|
|
123
|
+
// not. An unset `sidebar.backgroundColor` painted #ffffff on web and the app
|
|
124
|
+
// background in the export, and an unset `topBar.textColor` painted the brand
|
|
125
|
+
// primary on web and slate-700 in the export. A default that lives in the
|
|
126
|
+
// resolver cannot drift; one that lives in each host already had. The footer
|
|
127
|
+
// wants this same shape next — the same fix, not yet applied.
|
|
128
|
+
//
|
|
129
|
+
// Every default is what the WEB Player renders today, so the Player does not
|
|
130
|
+
// move and the export converges onto it. That is the direction the footer was
|
|
131
|
+
// converged in too (compiler.service's footerBg defaults to the web's #ffffff).
|
|
132
|
+
|
|
133
|
+
const DEFAULT_PRIMARY_COLOR = "#3b82f6";
|
|
134
|
+
// The web rail's and app bar's `bg-white`.
|
|
135
|
+
const DEFAULT_CHROME_SURFACE = "#ffffff";
|
|
136
|
+
// The web nav item's `text-slate-600`.
|
|
137
|
+
const DEFAULT_CHROME_TEXT = "#475569";
|
|
138
|
+
// The web mobile header's icon colour, `text-slate-700`.
|
|
139
|
+
const DEFAULT_TOP_BAR_TINT = "#334155";
|
|
140
|
+
|
|
141
|
+
// The contract's own guard, which already admits the 8-digit form a look uses
|
|
142
|
+
// to float a translucent rail (#RRGGBBAA).
|
|
143
|
+
function hexOrNull(value) {
|
|
144
|
+
return isHexColor(value) ? value : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function hexOr(value, fallback) {
|
|
148
|
+
return hexOrNull(value) || fallback;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function brandPrimary(config) {
|
|
152
|
+
return hexOr(config.primaryColor, DEFAULT_PRIMARY_COLOR);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The sidebar rail's surface, labels, active item and opt-in divider.
|
|
157
|
+
*
|
|
158
|
+
* @param {unknown} theme — the whole `theme_config`; `primaryColor` is read
|
|
159
|
+
* because an unset active colour resolves to the brand.
|
|
160
|
+
* @returns {{ backgroundColor: string, textColor: string, activeColor: string,
|
|
161
|
+
* activeStyle: string, borderColor: string|null, borderWidth: number|null }}
|
|
162
|
+
*/
|
|
163
|
+
function resolveSidebarTokens(theme) {
|
|
164
|
+
const config = isPlainObject(theme) ? theme : {};
|
|
165
|
+
const sidebar = isPlainObject(config.sidebar) ? config.sidebar : {};
|
|
166
|
+
const borderColor = hexOrNull(sidebar.borderColor);
|
|
167
|
+
return {
|
|
168
|
+
backgroundColor: hexOr(sidebar.backgroundColor, DEFAULT_CHROME_SURFACE),
|
|
169
|
+
textColor: hexOr(sidebar.textColor, DEFAULT_CHROME_TEXT),
|
|
170
|
+
activeColor: hexOr(sidebar.activeColor, brandPrimary(config)),
|
|
171
|
+
activeStyle: sidebar.activeStyle === "accent" ? "accent" : "filled",
|
|
172
|
+
// REQ-THEME-LOOK: the colour is the switch — no colour, no line.
|
|
173
|
+
borderColor,
|
|
174
|
+
borderWidth: borderColor ? borderWidthOr(sidebar.borderWidth) : null,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The top app bar's surface, its two text colours and its opt-in divider.
|
|
180
|
+
*
|
|
181
|
+
* `tintColor` and `titleColor` are SEPARATE because the web bar has always
|
|
182
|
+
* painted them differently when the author names nothing: the hamburger and
|
|
183
|
+
* icons take slate, the app name takes the brand. React Navigation's single
|
|
184
|
+
* `headerTintColor` could not express that, which is why the export used to
|
|
185
|
+
* paint the name slate. One token, two resolved fields, both hosts agree.
|
|
186
|
+
*
|
|
187
|
+
* `show` is deliberately absent: it depends on the menu TYPE, not the theme,
|
|
188
|
+
* and REQ-THEME-14 makes it a no-op on native — so it is not a shared token.
|
|
189
|
+
*
|
|
190
|
+
* @param {unknown} theme — the whole `theme_config`.
|
|
191
|
+
* @returns {{ backgroundColor: string, tintColor: string, titleColor: string,
|
|
192
|
+
* borderColor: string|null, borderWidth: number|null }}
|
|
193
|
+
*/
|
|
194
|
+
function resolveTopBarTokens(theme) {
|
|
195
|
+
const config = isPlainObject(theme) ? theme : {};
|
|
196
|
+
const topBar = isPlainObject(config.topBar) ? config.topBar : {};
|
|
197
|
+
const borderColor = hexOrNull(topBar.borderColor);
|
|
198
|
+
// An explicit colour drives BOTH slots; only the unset case splits.
|
|
199
|
+
const authored = hexOrNull(topBar.textColor);
|
|
200
|
+
return {
|
|
201
|
+
backgroundColor: hexOr(topBar.backgroundColor, DEFAULT_CHROME_SURFACE),
|
|
202
|
+
tintColor: authored || DEFAULT_TOP_BAR_TINT,
|
|
203
|
+
titleColor: authored || brandPrimary(config),
|
|
204
|
+
borderColor,
|
|
205
|
+
borderWidth: borderColor ? borderWidthOr(topBar.borderWidth) : null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
117
209
|
function borderWidthOr(...values) {
|
|
118
210
|
for (const value of values) {
|
|
119
211
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
@@ -121,4 +213,4 @@ function borderWidthOr(...values) {
|
|
|
121
213
|
return 1;
|
|
122
214
|
}
|
|
123
215
|
|
|
124
|
-
module.exports = { normaliseNavigation, menuItemCap, quickBarCap, resolveFooterTokens };
|
|
216
|
+
module.exports = { normaliseNavigation, menuItemCap, quickBarCap, resolveFooterTokens, resolveSidebarTokens, resolveTopBarTokens };
|
package/dist/navigation.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// the SAME literal, so what a planner may persist and what a host draws cannot
|
|
17
17
|
// diverge.
|
|
18
18
|
|
|
19
|
-
import { CONTRACT } from "./contract.js";
|
|
19
|
+
import { CONTRACT, isHexColor } from "./contract.js";
|
|
20
20
|
|
|
21
21
|
// Absent or unknown resolves here, so every app authored before menu types
|
|
22
22
|
// existed renders and compiles byte-identically.
|
|
@@ -105,6 +105,98 @@ export function resolveFooterTokens(theme) {
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// REQ-THEME-14 — the rail's and the app bar's tokens, resolved once for both
|
|
109
|
+
// hosts.
|
|
110
|
+
//
|
|
111
|
+
// These return FULLY resolved values where `resolveFooterTokens` above returns
|
|
112
|
+
// raw-or-null, and the difference is deliberate. The footer's two hosts happen
|
|
113
|
+
// to agree on their separately-written defaults; the rail's and the bar's did
|
|
114
|
+
// not. An unset `sidebar.backgroundColor` painted #ffffff on web and the app
|
|
115
|
+
// background in the export, and an unset `topBar.textColor` painted the brand
|
|
116
|
+
// primary on web and slate-700 in the export. A default that lives in the
|
|
117
|
+
// resolver cannot drift; one that lives in each host already had. The footer
|
|
118
|
+
// wants this same shape next — the same fix, not yet applied.
|
|
119
|
+
//
|
|
120
|
+
// Every default is what the WEB Player renders today, so the Player does not
|
|
121
|
+
// move and the export converges onto it. That is the direction the footer was
|
|
122
|
+
// converged in too (compiler.service's footerBg defaults to the web's #ffffff).
|
|
123
|
+
|
|
124
|
+
const DEFAULT_PRIMARY_COLOR = "#3b82f6";
|
|
125
|
+
// The web rail's and app bar's `bg-white`.
|
|
126
|
+
const DEFAULT_CHROME_SURFACE = "#ffffff";
|
|
127
|
+
// The web nav item's `text-slate-600`.
|
|
128
|
+
const DEFAULT_CHROME_TEXT = "#475569";
|
|
129
|
+
// The web mobile header's icon colour, `text-slate-700`.
|
|
130
|
+
const DEFAULT_TOP_BAR_TINT = "#334155";
|
|
131
|
+
|
|
132
|
+
// The contract's own guard, which already admits the 8-digit form a look uses
|
|
133
|
+
// to float a translucent rail (#RRGGBBAA).
|
|
134
|
+
function hexOrNull(value) {
|
|
135
|
+
return isHexColor(value) ? value : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function hexOr(value, fallback) {
|
|
139
|
+
return hexOrNull(value) || fallback;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function brandPrimary(config) {
|
|
143
|
+
return hexOr(config.primaryColor, DEFAULT_PRIMARY_COLOR);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The sidebar rail's surface, labels, active item and opt-in divider.
|
|
148
|
+
*
|
|
149
|
+
* @param {unknown} theme — the whole `theme_config`; `primaryColor` is read
|
|
150
|
+
* because an unset active colour resolves to the brand.
|
|
151
|
+
* @returns {{ backgroundColor: string, textColor: string, activeColor: string,
|
|
152
|
+
* activeStyle: string, borderColor: string|null, borderWidth: number|null }}
|
|
153
|
+
*/
|
|
154
|
+
export function resolveSidebarTokens(theme) {
|
|
155
|
+
const config = isPlainObject(theme) ? theme : {};
|
|
156
|
+
const sidebar = isPlainObject(config.sidebar) ? config.sidebar : {};
|
|
157
|
+
const borderColor = hexOrNull(sidebar.borderColor);
|
|
158
|
+
return {
|
|
159
|
+
backgroundColor: hexOr(sidebar.backgroundColor, DEFAULT_CHROME_SURFACE),
|
|
160
|
+
textColor: hexOr(sidebar.textColor, DEFAULT_CHROME_TEXT),
|
|
161
|
+
activeColor: hexOr(sidebar.activeColor, brandPrimary(config)),
|
|
162
|
+
activeStyle: sidebar.activeStyle === "accent" ? "accent" : "filled",
|
|
163
|
+
// REQ-THEME-LOOK: the colour is the switch — no colour, no line.
|
|
164
|
+
borderColor,
|
|
165
|
+
borderWidth: borderColor ? borderWidthOr(sidebar.borderWidth) : null,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The top app bar's surface, its two text colours and its opt-in divider.
|
|
171
|
+
*
|
|
172
|
+
* `tintColor` and `titleColor` are SEPARATE because the web bar has always
|
|
173
|
+
* painted them differently when the author names nothing: the hamburger and
|
|
174
|
+
* icons take slate, the app name takes the brand. React Navigation's single
|
|
175
|
+
* `headerTintColor` could not express that, which is why the export used to
|
|
176
|
+
* paint the name slate. One token, two resolved fields, both hosts agree.
|
|
177
|
+
*
|
|
178
|
+
* `show` is deliberately absent: it depends on the menu TYPE, not the theme,
|
|
179
|
+
* and REQ-THEME-14 makes it a no-op on native — so it is not a shared token.
|
|
180
|
+
*
|
|
181
|
+
* @param {unknown} theme — the whole `theme_config`.
|
|
182
|
+
* @returns {{ backgroundColor: string, tintColor: string, titleColor: string,
|
|
183
|
+
* borderColor: string|null, borderWidth: number|null }}
|
|
184
|
+
*/
|
|
185
|
+
export function resolveTopBarTokens(theme) {
|
|
186
|
+
const config = isPlainObject(theme) ? theme : {};
|
|
187
|
+
const topBar = isPlainObject(config.topBar) ? config.topBar : {};
|
|
188
|
+
const borderColor = hexOrNull(topBar.borderColor);
|
|
189
|
+
// An explicit colour drives BOTH slots; only the unset case splits.
|
|
190
|
+
const authored = hexOrNull(topBar.textColor);
|
|
191
|
+
return {
|
|
192
|
+
backgroundColor: hexOr(topBar.backgroundColor, DEFAULT_CHROME_SURFACE),
|
|
193
|
+
tintColor: authored || DEFAULT_TOP_BAR_TINT,
|
|
194
|
+
titleColor: authored || brandPrimary(config),
|
|
195
|
+
borderColor,
|
|
196
|
+
borderWidth: borderColor ? borderWidthOr(topBar.borderWidth) : null,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
108
200
|
function borderWidthOr(...values) {
|
|
109
201
|
for (const value of values) {
|
|
110
202
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.110.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|