@colixsystems/widget-sdk 0.6.0 → 0.8.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,9 +6,19 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
6
6
 
7
7
  ## Status
8
8
 
9
- `v0.6.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.8.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
10
 
11
- ### What's new in 0.6.0
11
+ ### What's new in 0.8.0
12
+
13
+ - **`useDirectory()` — read-only user directory hook (REQ-DIR-01).** Returns `{ users, loading, error, refetch }` where each user is `{ id, name, role }`. Backed by a new `WidgetContext.directory.listUsers(query)` slice and gated by the new `directory.read:users` scope. Use it to build a chat people-list, an @-mention picker, or to resolve an author id to a display name. The host reads `GET /api/v1/app/users`, which hands non-Studio (Player) callers the reduced `{ id, name, role }` projection — email and other admin-only fields never leave the server for an app end-user. `query` is an optional `{ q, role, isActive, limit, offset }` (`q` substring-matches the display name; `role` is `"USER"` (default), `"INTEGRATION"`, or `"ALL"`). Mutating users is not part of the widget surface — the directory is read-only.
14
+ - **`CONTRACT.version` → `1.1.0`** (additive: one new hook, one new context slice, one new scope). No existing export changed signature.
15
+
16
+ ### What was in 0.7.0
17
+
18
+ - **`datastoreTemplate` is now part of the public manifest contract.** `CONTRACT.manifestSchema` carries an optional `datastoreTemplate` entry alongside the existing fields. The TypeScript `WidgetManifest` already declared this since 0.3.0, but the runtime contract did not — the agent's system prompt only generated the field set advertised by `CONTRACT.manifestSchema`, which silently omitted `datastoreTemplate` from every AI-generated draft. The mismatch meant most AI-generated DATA widgets shipped without a seeded table, forcing the end-user to hand-build it before the widget would render anything useful. With the schema entry in place, the agent now defaults to including a `datastoreTemplate` whenever the widget reads or writes data, matching the no-code experience the platform promises.
19
+ - **Agent system prompt** ([backend/src/core/services/ai-widget-agent.service.js](../../backend/src/core/services/ai-widget-agent.service.js)) gains a dedicated `===== DATASTORE TEMPLATE =====` section, an updated DATA-widget example with a template, and a CONVERSATION BEHAVIOUR rule pinning the default. The note-list example (TextInput + create + update + delete) now shows the matching template too, including the column-name-match rule (`record.Body` ↔ `"name": "Body"`).
20
+
21
+ ### What was in 0.6.0
12
22
 
13
23
  - **Primitives are now React Native through-and-through.** Web (Studio + Player) bundles with `react-native-web`; native (exported Expo app) uses the real `react-native`. The hand-written DOM wrappers in `primitives.js` are gone — both web and native files are now one-line re-exports from `react-native`. The widget API surface is unchanged for the components that already existed (`Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`), so existing 0.5.0 widgets keep running without source edits.
14
24
  - **More primitives available for free.** `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, and `StyleSheet` are now exported alongside the original five. Authors who want them just import from `@colixsystems/widget-sdk` like any other primitive.
@@ -56,7 +66,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
56
66
 
57
67
  - `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
58
68
  - `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
59
- - `useDatastoreQuery`, `useDatastoreMutation`, `useWidgetEvent`, `useTheme`, `useI18n` — hooks that read from the host-provided `WidgetContext`.
69
+ - `useDatastoreQuery`, `useDatastoreMutation`, `useDirectory`, `useWidgetEvent`, `useTheme`, `useI18n` — hooks that read from the host-provided `WidgetContext`. `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope.
60
70
  - `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet` — re-exported from `react-native`. 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. See https://reactnative.dev/docs/ for per-component props.
61
71
  - `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
62
72
 
package/dist/contract.cjs CHANGED
@@ -78,6 +78,18 @@ const HOOKS = [
78
78
  requiredContextSlice: ["datastore.records"],
79
79
  scopes: ["datastore.write:*"],
80
80
  },
81
+ {
82
+ name: "useDirectory",
83
+ signature: "useDirectory(query?)",
84
+ returnShape: {
85
+ users: "Array<{ id, name, role }>",
86
+ loading: "boolean",
87
+ error: "DatastoreError | null",
88
+ refetch: "() => Promise<void>",
89
+ },
90
+ requiredContextSlice: ["directory.listUsers"],
91
+ scopes: ["directory.read:users"],
92
+ },
81
93
  {
82
94
  name: "useWidgetEvent",
83
95
  signature: "useWidgetEvent(eventName)",
@@ -264,6 +276,12 @@ const MANIFEST_SCHEMA = {
264
276
  description: "Declared events. Each entry { name, payloadSchema? }.",
265
277
  default: [],
266
278
  },
279
+ datastoreTemplate: {
280
+ type: "object",
281
+ required: false,
282
+ description:
283
+ "Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (must be declared earlier in the array). Tables persist across uninstalls.",
284
+ },
267
285
  };
268
286
 
269
287
  const WIDGET_CONTEXT_SHAPE = {
@@ -306,6 +324,12 @@ const WIDGET_CONTEXT_SHAPE = {
306
324
  required: true,
307
325
  fields: { records: "function" },
308
326
  },
327
+ directory: {
328
+ description:
329
+ "Read-only user directory. { listUsers(query?) -> Promise<Array<{ id, name, role }>> }. Backs useDirectory(); for chat people-lists / @-mention pickers / author-id resolution. Requires the directory.read:users scope.",
330
+ required: true,
331
+ fields: { listUsers: "function" },
332
+ },
309
333
  events: {
310
334
  description: "{ emit(name, payload) }.",
311
335
  required: true,
@@ -403,7 +427,7 @@ function deepFreeze(value) {
403
427
  }
404
428
 
405
429
  const CONTRACT = deepFreeze({
406
- version: "1.0.0",
430
+ version: "1.1.0",
407
431
  hooks: HOOKS,
408
432
  primitives: PRIMITIVES,
409
433
  manifestSchema: MANIFEST_SCHEMA,
package/dist/contract.js CHANGED
@@ -79,6 +79,18 @@ const HOOKS = [
79
79
  requiredContextSlice: ["datastore.records"],
80
80
  scopes: ["datastore.write:*"],
81
81
  },
82
+ {
83
+ name: "useDirectory",
84
+ signature: "useDirectory(query?)",
85
+ returnShape: {
86
+ users: "Array<{ id, name, role }>",
87
+ loading: "boolean",
88
+ error: "DatastoreError | null",
89
+ refetch: "() => Promise<void>",
90
+ },
91
+ requiredContextSlice: ["directory.listUsers"],
92
+ scopes: ["directory.read:users"],
93
+ },
82
94
  {
83
95
  name: "useWidgetEvent",
84
96
  signature: "useWidgetEvent(eventName)",
@@ -259,6 +271,12 @@ const MANIFEST_SCHEMA = {
259
271
  description: "Declared events. Each entry { name, payloadSchema? }.",
260
272
  default: [],
261
273
  },
274
+ datastoreTemplate: {
275
+ type: "object",
276
+ required: false,
277
+ description:
278
+ "Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (must be declared earlier in the array). Tables persist across uninstalls.",
279
+ },
262
280
  };
263
281
 
264
282
  const WIDGET_CONTEXT_SHAPE = {
@@ -300,6 +318,12 @@ const WIDGET_CONTEXT_SHAPE = {
300
318
  required: true,
301
319
  fields: { records: "function" },
302
320
  },
321
+ directory: {
322
+ description:
323
+ "Read-only user directory. { listUsers(query?) -> Promise<Array<{ id, name, role }>> }. Backs useDirectory(); for chat people-lists / @-mention pickers / author-id resolution. Requires the directory.read:users scope.",
324
+ required: true,
325
+ fields: { listUsers: "function" },
326
+ },
303
327
  events: {
304
328
  description: "{ emit(name, payload) }.",
305
329
  required: true,
@@ -395,7 +419,7 @@ function deepFreeze(value) {
395
419
  }
396
420
 
397
421
  const CONTRACT = deepFreeze({
398
- version: "1.0.0",
422
+ version: "1.1.0",
399
423
  hooks: HOOKS,
400
424
  primitives: PRIMITIVES,
401
425
  manifestSchema: MANIFEST_SCHEMA,
package/dist/hooks.js CHANGED
@@ -258,6 +258,80 @@ export function useDatastoreMutation(table) {
258
258
  };
259
259
  }
260
260
 
261
+ /**
262
+ * Stateful user-directory query hook. Returns { users, loading, error,
263
+ * refetch }.
264
+ *
265
+ * The host's directory client exposes `listUsers(query)` which resolves
266
+ * to an array of `{ id, name, role }` rows — the privacy-reduced
267
+ * directory projection the backend hands to non-Studio (Player) callers.
268
+ * Use it to build a chat people-list, an @-mention picker, or to resolve
269
+ * an author id to a display name. Mutating users is NOT part of this
270
+ * surface; the directory is read-only from a widget.
271
+ *
272
+ * `query` is an optional `{ q?, role?, isActive?, limit?, offset? }`
273
+ * object. `q` substring-matches the display name; `role` is `"USER"`
274
+ * (default) or `"INTEGRATION"` or `"ALL"`. The hook re-fetches whenever
275
+ * `JSON.stringify(query)` changes and exposes `refetch` for on-demand
276
+ * reloads (e.g. a chat roster refresh).
277
+ *
278
+ * Requires the `directory.read:users` scope in the widget manifest's
279
+ * `requestedScopes`.
280
+ */
281
+ export function useDirectory(query) {
282
+ const ctx = useWidgetContextOrThrow("useDirectory");
283
+ if (!ctx.directory || typeof ctx.directory.listUsers !== "function") {
284
+ throw new Error("useDirectory: host did not inject a directory client");
285
+ }
286
+ const [users, setUsers] = useState([]);
287
+ const [loading, setLoading] = useState(true);
288
+ const [error, setError] = useState(null);
289
+
290
+ // Same ref discipline as useDatastoreQuery: the host rebuilds the
291
+ // WidgetContext value every render, so we capture the live query +
292
+ // client in refs to keep `refetch` a stable identity.
293
+ const queryRef = useRef(query);
294
+ const listUsersRef = useRef(ctx.directory.listUsers);
295
+ queryRef.current = query;
296
+ listUsersRef.current = ctx.directory.listUsers;
297
+
298
+ const runRef = useRef(0);
299
+
300
+ const doFetch = useCallback(async () => {
301
+ const myRun = ++runRef.current;
302
+ setLoading(true);
303
+ setError(null);
304
+ try {
305
+ const rows = await listUsersRef.current(queryRef.current);
306
+ if (runRef.current !== myRun) return;
307
+ setUsers(Array.isArray(rows) ? rows : []);
308
+ setLoading(false);
309
+ } catch (err) {
310
+ if (runRef.current !== myRun) return;
311
+ setError(toDatastoreError(err));
312
+ setLoading(false);
313
+ }
314
+ }, []);
315
+
316
+ const queryKey = (() => {
317
+ try {
318
+ return JSON.stringify(query);
319
+ } catch (_e) {
320
+ return null;
321
+ }
322
+ })();
323
+ useEffect(() => {
324
+ doFetch();
325
+ // eslint-disable-next-line react-hooks/exhaustive-deps
326
+ }, [queryKey]);
327
+
328
+ const refetch = useCallback(async () => {
329
+ await doFetch();
330
+ }, [doFetch]);
331
+
332
+ return { users, loading, error, refetch };
333
+ }
334
+
261
335
  /**
262
336
  * Emit a named widget event through ctx.events.emit. Page-level event
263
337
  * bindings (subscribed by the host) decide what happens next.
package/dist/index.d.ts CHANGED
@@ -184,6 +184,9 @@ export interface WidgetContext<TProps = unknown> {
184
184
  currentRoute: { pageId: string; params: Record<string, string> };
185
185
  };
186
186
  datastore: unknown; // typed by @colixsystems/datastore-client
187
+ directory: {
188
+ listUsers(query?: DirectoryQuery): Promise<DirectoryUser[]>;
189
+ };
187
190
  events: { emit(eventName: string, payload?: unknown): void };
188
191
  i18n: {
189
192
  locale: string;
@@ -274,6 +277,43 @@ export function useDatastoreMutation<T = unknown>(
274
277
  table: string,
275
278
  ): MutationApi<T>;
276
279
 
280
+ /**
281
+ * A single row from the read-only user directory. `role` is `"USER"`
282
+ * for a human end-user or `"INTEGRATION"` for a service account. The
283
+ * directory deliberately omits email and other admin-only fields.
284
+ */
285
+ export interface DirectoryUser {
286
+ id: string;
287
+ name: string;
288
+ role: "USER" | "INTEGRATION";
289
+ }
290
+
291
+ export interface DirectoryQuery {
292
+ /** Case-insensitive substring match on the display name. */
293
+ q?: string;
294
+ /** `"USER"` (default), `"INTEGRATION"`, or `"ALL"`. */
295
+ role?: "USER" | "INTEGRATION" | "ALL";
296
+ /** Filter by active state. */
297
+ isActive?: boolean;
298
+ limit?: number;
299
+ offset?: number;
300
+ }
301
+
302
+ export interface DirectoryResult {
303
+ users: DirectoryUser[];
304
+ loading: boolean;
305
+ error: DatastoreError | null;
306
+ refetch(): Promise<void>;
307
+ }
308
+
309
+ /**
310
+ * Read-only user directory hook. Resolves the tenant's app users to
311
+ * `{ id, name, role }` rows for chat people-lists, @-mention pickers, or
312
+ * author-id → display-name resolution. Requires the
313
+ * `directory.read:users` scope in the widget manifest.
314
+ */
315
+ export function useDirectory(query?: DirectoryQuery): DirectoryResult;
316
+
277
317
  export function useWidgetEvent(name: string): (payload?: unknown) => void;
278
318
 
279
319
  export function useTheme(): ThemeTokens;
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export {
10
10
  DatastoreError,
11
11
  useDatastoreQuery,
12
12
  useDatastoreMutation,
13
+ useDirectory,
13
14
  useWidgetEvent,
14
15
  useTheme,
15
16
  useI18n,
@@ -10,6 +10,7 @@ export {
10
10
  DatastoreError,
11
11
  useDatastoreQuery,
12
12
  useDatastoreMutation,
13
+ useDirectory,
13
14
  useWidgetEvent,
14
15
  useTheme,
15
16
  useI18n,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.6.0",
3
+ "version": "0.8.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",