@colixsystems/widget-sdk 0.7.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,14 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
6
6
 
7
7
  ## Status
8
8
 
9
- `v0.7.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.7.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
12
17
 
13
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.
14
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"`).
@@ -61,7 +66,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
61
66
 
62
67
  - `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
63
68
  - `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
64
- - `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.
65
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.
66
71
  - `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
67
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)",
@@ -312,6 +324,12 @@ const WIDGET_CONTEXT_SHAPE = {
312
324
  required: true,
313
325
  fields: { records: "function" },
314
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
+ },
315
333
  events: {
316
334
  description: "{ emit(name, payload) }.",
317
335
  required: true,
@@ -409,7 +427,7 @@ function deepFreeze(value) {
409
427
  }
410
428
 
411
429
  const CONTRACT = deepFreeze({
412
- version: "1.0.0",
430
+ version: "1.1.0",
413
431
  hooks: HOOKS,
414
432
  primitives: PRIMITIVES,
415
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)",
@@ -306,6 +318,12 @@ const WIDGET_CONTEXT_SHAPE = {
306
318
  required: true,
307
319
  fields: { records: "function" },
308
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
+ },
309
327
  events: {
310
328
  description: "{ emit(name, payload) }.",
311
329
  required: true,
@@ -401,7 +419,7 @@ function deepFreeze(value) {
401
419
  }
402
420
 
403
421
  const CONTRACT = deepFreeze({
404
- version: "1.0.0",
422
+ version: "1.1.0",
405
423
  hooks: HOOKS,
406
424
  primitives: PRIMITIVES,
407
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.7.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",