@colixsystems/widget-sdk 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,22 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
6
6
 
7
7
  ## Status
8
8
 
9
- `v0.15.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**.
9
+ `v0.17.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**.
10
+
11
+ ### What's new in 0.17.0
12
+
13
+ REQ-WBLT-FORMBUILDER — a runtime schema resolver so widgets can render by column type.
14
+
15
+ - **`useDatastoreSchema(tableId)` is wired** + the new `WidgetContext.datastore.schema` slice. Returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, dataType, required, relationType, targetTableId, isIdentification }] }` (`null` until loaded). It reads the **existing** ACL-gated `GET /api/v1/tables/:id` — structure only, never row data — so a public-grant table resolves for anonymous visitors exactly like a record read. Use it to resolve a stored `columnId` to its column name / dataType / relation target at runtime (the built-in Form Builder uses it to render an input per column type). Reads need the `datastore.read:<table>` scope. Backed by a new `datastore.schema(tableId)` host facade (web: `widgetHostDatastore`; native: `hostDatastore` in the export's `widgetHost.js`). Additive.
16
+ - **`CONTRACT.version` → `1.7.0`** (additive: one new hook, one new `datastore.schema` context field). No existing export changed signature.
17
+
18
+ ### What's new in 0.16.0
19
+
20
+ REQ-THEME — the tenant's **Theme Settings** now flow all the way into `useTheme()`.
21
+
22
+ - **`themeTokens.colors` gains `secondary` + `onSecondary`.** `useTheme().colors.secondary` reflects the tenant's *Secondary Color* picker (with `onSecondary` as its readable contrast color), alongside the existing `primary` / `onPrimary`. Built-in widgets like Button use it for their secondary variant; third-party widgets can use it for a branded second accent. The full `colors` shape is now `{ primary, onPrimary, secondary, onSecondary, surface, onSurface, surfaceMuted, onSurfaceMuted, border, danger, success, warning, info }`.
23
+ - **`colors.primary` / `colors.secondary` / `typography.fontFamily` are tenant-resolved.** The host maps the Studio Theme Settings blob (Primary Color, Secondary Color, Global Font) onto the default tokens before handing them to `useTheme()`, on both the live Player and the exported app — so a widget that reads tokens re-themes automatically. (Custom Google fonts render in the Player today; the exported app falls back to the system face for non-system fonts until font bundling lands.)
24
+ - **`CONTRACT.version` → `1.6.0`** (additive: two new `themeTokens.colors` keys). No existing export changed signature.
10
25
 
11
26
  ### What's new in 0.15.0
12
27
 
@@ -117,7 +132,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
117
132
 
118
133
  - `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
119
134
  - `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
120
- - `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useFile`, `useWidgetEvent`, `usePayments`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (`id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute }` for internal page navigation — for external URLs use the `Linking` primitive (`Linking.openURL(url)`). `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useFile(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case).
135
+ - `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useFile`, `useWidgetEvent`, `usePayments`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (`id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute }` for internal page navigation — for external URLs use the `Linking` primitive (`Linking.openURL(url)`). `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, dataType, required, relationType, targetTableId, isIdentification }] }` (structure only, no row data) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `useFile(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case).
121
136
  - `WidgetTree({ node })` — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
122
137
  - `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`, `DateTimePicker` wraps `@react-native-community/datetimepicker`). The web build aliases `react-native` to `react-native-web` so widgets render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
123
138
  - `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
package/dist/contract.cjs CHANGED
@@ -12,6 +12,8 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
12
12
  colors: Object.freeze({
13
13
  primary: "#ff6b5b",
14
14
  onPrimary: "#ffffff",
15
+ secondary: "#475569",
16
+ onSecondary: "#ffffff",
15
17
  surface: "#ffffff",
16
18
  onSurface: "#111827",
17
19
  surfaceMuted: "#f8fafc",
@@ -37,7 +39,7 @@ const HOOKS = [
37
39
  signature: "useTheme()",
38
40
  returnShape: {
39
41
  colors:
40
- "{ primary, onPrimary, surface, onSurface, surfaceMuted, onSurfaceMuted, border, danger, success, warning, info }",
42
+ "{ primary, onPrimary, secondary, onSecondary, surface, onSurface, surfaceMuted, onSurfaceMuted, border, danger, success, warning, info }",
41
43
  spacing: "{ xs, sm, md, lg, xl }",
42
44
  radii: "{ sm, md, lg, pill }",
43
45
  typography: "{ fontFamily, sizes: { xs, sm, md, lg, xl, xxl } }",
@@ -128,6 +130,28 @@ const HOOKS = [
128
130
  requiredContextSlice: ["datastore.records"],
129
131
  scopes: ["datastore.read:*"],
130
132
  },
133
+ {
134
+ name: "useDatastoreSchema",
135
+ signature: "useDatastoreSchema(tableId)",
136
+ description:
137
+ "Reads a table's structural schema — its columns and their types — so a " +
138
+ "widget can resolve a stored columnId to the column's name / dataType / " +
139
+ "relation target at runtime (e.g. a form that renders inputs by column " +
140
+ "type). Returns { schema, loading, error, refetch } where schema is " +
141
+ "{ id, name, columns: [{ id, name, dataType, required, relationType, " +
142
+ "targetTableId, isIdentification }] } (null until loaded). Reads only the " +
143
+ "structure, never row data; gated by the same datastore.read ACL as " +
144
+ "record reads, so a public-grant table resolves for anonymous visitors.",
145
+ returnShape: {
146
+ schema:
147
+ "{ id, name, columns: [{ id, name, dataType, required, relationType, targetTableId, isIdentification }] } | null",
148
+ loading: "boolean",
149
+ error: "DatastoreError | null",
150
+ refetch: "() => Promise<void>",
151
+ },
152
+ requiredContextSlice: ["datastore.schema"],
153
+ scopes: ["datastore.read:<table>"],
154
+ },
131
155
  {
132
156
  name: "useDatastoreMutation",
133
157
  signature: "useDatastoreMutation(tableId)",
@@ -522,9 +546,9 @@ const WIDGET_CONTEXT_SHAPE = {
522
546
  },
523
547
  datastore: {
524
548
  description:
525
- "{ records(table) -> { list(query?), get(id), create(values), update(id, values), delete(id) } }.",
549
+ "{ records(table) -> { list(query?), get(id), create(values), update(id, values), delete(id) }, schema(table) -> Promise<{ id, name, columns: [...] }> }. `records` backs the query/record/mutation hooks; `schema` backs useDatastoreSchema() and resolves a table's column structure (no row data).",
526
550
  required: true,
527
- fields: { records: "function" },
551
+ fields: { records: "function", schema: "function" },
528
552
  },
529
553
  directory: {
530
554
  description:
@@ -830,7 +854,18 @@ const CONTRACT = deepFreeze({
830
854
  // Permissive-direction change: minor bump on the contract's own
831
855
  // versioning (per CLAUDE.md §4, pre-1.0 minor is the breaking channel —
832
856
  // the package.json version bumps accordingly).
833
- version: "1.5.0",
857
+ //
858
+ // 1.6.0: additive — `themeTokens.colors` gains `secondary` + `onSecondary`
859
+ // so the tenant's Theme Settings "Secondary Color" flows through
860
+ // `useTheme().colors.secondary` (Button secondary variant + any widget
861
+ // that wants the brand's second accent).
862
+ //
863
+ // 1.7.0: additive — new `useDatastoreSchema(tableId)` hook + the
864
+ // `datastore.schema` host-context slice it reads. Lets a widget resolve a
865
+ // stored columnId to its column name / dataType / relation target at
866
+ // runtime (Form Builder renders inputs by column type). Reads the existing
867
+ // ACL-gated `GET /tables/:id` — structure only, no row data.
868
+ version: "1.7.0",
834
869
  hooks: HOOKS,
835
870
  primitives: PRIMITIVES,
836
871
  manifestSchema: MANIFEST_SCHEMA,
package/dist/contract.js CHANGED
@@ -12,6 +12,8 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
12
12
  colors: Object.freeze({
13
13
  primary: "#ff6b5b",
14
14
  onPrimary: "#ffffff",
15
+ secondary: "#475569",
16
+ onSecondary: "#ffffff",
15
17
  surface: "#ffffff",
16
18
  onSurface: "#111827",
17
19
  surfaceMuted: "#f8fafc",
@@ -37,7 +39,7 @@ const HOOKS = [
37
39
  signature: "useTheme()",
38
40
  returnShape: {
39
41
  colors:
40
- "{ primary, onPrimary, surface, onSurface, surfaceMuted, onSurfaceMuted, border, danger, success, warning, info }",
42
+ "{ primary, onPrimary, secondary, onSecondary, surface, onSurface, surfaceMuted, onSurfaceMuted, border, danger, success, warning, info }",
41
43
  spacing: "{ xs, sm, md, lg, xl }",
42
44
  radii: "{ sm, md, lg, pill }",
43
45
  typography: "{ fontFamily, sizes: { xs, sm, md, lg, xl, xxl } }",
@@ -128,6 +130,28 @@ const HOOKS = [
128
130
  requiredContextSlice: ["datastore.records"],
129
131
  scopes: ["datastore.read:*"],
130
132
  },
133
+ {
134
+ name: "useDatastoreSchema",
135
+ signature: "useDatastoreSchema(tableId)",
136
+ description:
137
+ "Reads a table's structural schema — its columns and their types — so a " +
138
+ "widget can resolve a stored columnId to the column's name / dataType / " +
139
+ "relation target at runtime (e.g. a form that renders inputs by column " +
140
+ "type). Returns { schema, loading, error, refetch } where schema is " +
141
+ "{ id, name, columns: [{ id, name, dataType, required, relationType, " +
142
+ "targetTableId, isIdentification }] } (null until loaded). Reads only the " +
143
+ "structure, never row data; gated by the same datastore.read ACL as " +
144
+ "record reads, so a public-grant table resolves for anonymous visitors.",
145
+ returnShape: {
146
+ schema:
147
+ "{ id, name, columns: [{ id, name, dataType, required, relationType, targetTableId, isIdentification }] } | null",
148
+ loading: "boolean",
149
+ error: "DatastoreError | null",
150
+ refetch: "() => Promise<void>",
151
+ },
152
+ requiredContextSlice: ["datastore.schema"],
153
+ scopes: ["datastore.read:<table>"],
154
+ },
131
155
  {
132
156
  name: "useDatastoreMutation",
133
157
  signature: "useDatastoreMutation(tableId)",
@@ -516,9 +540,9 @@ const WIDGET_CONTEXT_SHAPE = {
516
540
  },
517
541
  datastore: {
518
542
  description:
519
- "{ records(table) -> { list(query?), get(id), create(values), update(id, values), delete(id) } }.",
543
+ "{ records(table) -> { list(query?), get(id), create(values), update(id, values), delete(id) }, schema(table) -> Promise<{ id, name, columns: [...] }> }. `records` backs the query/record/mutation hooks; `schema` backs useDatastoreSchema() and resolves a table's column structure (no row data).",
520
544
  required: true,
521
- fields: { records: "function" },
545
+ fields: { records: "function", schema: "function" },
522
546
  },
523
547
  directory: {
524
548
  description:
@@ -786,7 +810,10 @@ function deepFreeze(value) {
786
810
  }
787
811
 
788
812
  const CONTRACT = deepFreeze({
789
- version: "1.5.0",
813
+ // 1.7.0: additive — new useDatastoreSchema(tableId) hook + the
814
+ // datastore.schema host-context slice it reads (resolves a table's column
815
+ // structure at runtime via the existing ACL-gated GET /tables/:id).
816
+ version: "1.7.0",
790
817
  hooks: HOOKS,
791
818
  primitives: PRIMITIVES,
792
819
  manifestSchema: MANIFEST_SCHEMA,
package/dist/hooks.js CHANGED
@@ -297,6 +297,81 @@ export function useDatastoreRecord(table, recordId) {
297
297
  return { data, loading, error, refetch };
298
298
  }
299
299
 
300
+ /**
301
+ * Stateful table-schema resolver hook. Returns { schema, loading, error,
302
+ * refetch } where `schema` is `{ id, name, columns: [{ id, name, dataType,
303
+ * required, relationType, targetTableId, isIdentification }] }` (`null` until
304
+ * loaded).
305
+ *
306
+ * The host's datastore client exposes `schema(table)` which resolves to the
307
+ * table's structural metadata — columns and their types, NOT row data. Use it
308
+ * to resolve a stored `columnId` to its column name / dataType / relation
309
+ * target at runtime (e.g. a form that renders an input per column type). It
310
+ * reads the existing ACL-gated `GET /tables/:id`, so a public-grant table
311
+ * resolves for anonymous visitors exactly like a record read.
312
+ *
313
+ * When `tableId` is falsy (the author hasn't bound a `tableRef` yet) the hook
314
+ * collapses to { schema: null, loading: false, error: null, refetch } without
315
+ * a network round-trip. A 404 (table absent / not readable / cross-tenant)
316
+ * surfaces as a DatastoreError with `code: "NOT_FOUND"`.
317
+ */
318
+ export function useDatastoreSchema(tableId) {
319
+ const ctx = useWidgetContextOrThrow("useDatastoreSchema");
320
+ if (!ctx.datastore || typeof ctx.datastore.schema !== "function") {
321
+ throw new Error(
322
+ "useDatastoreSchema: host did not inject a datastore schema client",
323
+ );
324
+ }
325
+ const ready = Boolean(tableId);
326
+ const [schema, setSchema] = useState(null);
327
+ const [loading, setLoading] = useState(ready);
328
+ const [error, setError] = useState(null);
329
+
330
+ // Same ref discipline as useDatastoreRecord — `ctx` is a fresh object
331
+ // identity on every host render, so we hold the live inputs in refs to
332
+ // keep `refetch` a stable callback.
333
+ const tableIdRef = useRef(tableId);
334
+ const schemaFnRef = useRef(ctx.datastore.schema);
335
+ tableIdRef.current = tableId;
336
+ schemaFnRef.current = ctx.datastore.schema;
337
+
338
+ const runRef = useRef(0);
339
+
340
+ const doFetch = useCallback(async () => {
341
+ const myRun = ++runRef.current;
342
+ const t = tableIdRef.current;
343
+ if (!t) {
344
+ setLoading(false);
345
+ setError(null);
346
+ setSchema(null);
347
+ return;
348
+ }
349
+ setLoading(true);
350
+ setError(null);
351
+ try {
352
+ const result = await schemaFnRef.current(t);
353
+ if (runRef.current !== myRun) return;
354
+ setSchema(result || null);
355
+ setLoading(false);
356
+ } catch (err) {
357
+ if (runRef.current !== myRun) return;
358
+ setError(toDatastoreError(err));
359
+ setLoading(false);
360
+ }
361
+ }, []);
362
+
363
+ useEffect(() => {
364
+ doFetch();
365
+ // eslint-disable-next-line react-hooks/exhaustive-deps
366
+ }, [tableId]);
367
+
368
+ const refetch = useCallback(async () => {
369
+ await doFetch();
370
+ }, [doFetch]);
371
+
372
+ return { schema, loading, error, refetch };
373
+ }
374
+
300
375
  /**
301
376
  * Stateful file-asset resolver hook. Returns { url, file, loading, error,
302
377
  * refetch }.
package/dist/index.d.ts CHANGED
@@ -152,6 +152,9 @@ export interface WidgetManifest {
152
152
  export interface ThemeTokens {
153
153
  colors: {
154
154
  primary: string;
155
+ onPrimary: string;
156
+ secondary: string;
157
+ onSecondary: string;
155
158
  surface: string;
156
159
  onSurface: string;
157
160
  danger: string;
@@ -380,6 +383,59 @@ export function useDatastoreRecord(
380
383
  refetch(): Promise<void>;
381
384
  };
382
385
 
386
+ /**
387
+ * One column in a table's schema, as returned by `useDatastoreSchema`.
388
+ * Structural metadata only — never row data.
389
+ */
390
+ export interface DatastoreSchemaColumn {
391
+ id: string;
392
+ name: string;
393
+ dataType:
394
+ | "STRING"
395
+ | "TEXT"
396
+ | "NUMBER"
397
+ | "FLOAT"
398
+ | "BOOL"
399
+ | "DATE"
400
+ | "FILE"
401
+ | "STRING_ARRAY"
402
+ | "INT_ARRAY"
403
+ | "RELATION"
404
+ | "USER"
405
+ | "USER_GROUP";
406
+ required: boolean;
407
+ /** For RELATION columns only. */
408
+ relationType?: "ONE_TO_ONE" | "ONE_TO_MANY" | "MANY_TO_MANY" | null;
409
+ /** For RELATION columns only — the id of the table this column points at. */
410
+ targetTableId?: string | null;
411
+ /** True when this column is the table's display/identification column. */
412
+ isIdentification?: boolean;
413
+ }
414
+
415
+ export interface DatastoreSchema {
416
+ id: string;
417
+ name: string;
418
+ columns: DatastoreSchemaColumn[];
419
+ }
420
+
421
+ export interface SchemaResult {
422
+ schema: DatastoreSchema | null;
423
+ loading: boolean;
424
+ error: DatastoreError | null;
425
+ refetch(): Promise<void>;
426
+ }
427
+
428
+ /**
429
+ * Stateful table-schema resolver hook. Returns `{ schema, loading, error,
430
+ * refetch }` where `schema` is the bound table's column structure (`null`
431
+ * until loaded). Reads the existing ACL-gated `GET /tables/:id` — structure
432
+ * only, no row data. Use it to resolve a stored `columnId` to its column
433
+ * name / dataType / relation target at runtime.
434
+ */
435
+ export function useDatastoreSchema(
436
+ tableId: string | null | undefined,
437
+ ): SchemaResult;
438
+
383
439
  /**
384
440
  * Stateful file-asset resolver hook. Returns `{ url, file, loading, error,
385
441
  * refetch }`. The `url` is an absolute URL composed against the host's API
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export {
12
12
  DirectoryError,
13
13
  useDatastoreQuery,
14
14
  useDatastoreRecord,
15
+ useDatastoreSchema,
15
16
  useFile,
16
17
  useDatastoreMutation,
17
18
  useDirectory,
@@ -12,6 +12,7 @@ export {
12
12
  DirectoryError,
13
13
  useDatastoreQuery,
14
14
  useDatastoreRecord,
15
+ useDatastoreSchema,
15
16
  useFile,
16
17
  useDatastoreMutation,
17
18
  useDirectory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.15.0",
3
+ "version": "0.17.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",
@@ -35,7 +35,7 @@
35
35
  ],
36
36
  "scripts": {
37
37
  "build": "node scripts/build.js",
38
- "test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/linter-users-scope.test.js"
38
+ "test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/linter-users-scope.test.js"
39
39
  },
40
40
  "engines": {
41
41
  "node": ">=18"