@lotics/app-sdk 0.49.1 → 0.50.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/AGENTS.md CHANGED
@@ -22,7 +22,7 @@ signature; open the file.**
22
22
  | [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`. |
23
23
  | [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming `items` → `AgentRun`) and `askAi` — plus the fields-vs-file razor for choosing between them. |
24
24
  | [docs/security.md](./docs/security.md) | **Read before shipping** — the owner-principal model, `is_current_member` scoping, write attribution, group gates, public-app bounds, what runtime refinement cannot widen. |
25
- | [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, `rpc()`, `openExternal`/`downloadFile`, geofencing, analytics, and the publish chain for package contributors. |
25
+ | [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, `rpc()`, `openExternal`/`downloadFile`, geofencing, analytics, `useConfig` (App-Packages installation config), and the publish chain for package contributors. |
26
26
 
27
27
  ## Non-negotiables (each detailed in its doc)
28
28
 
@@ -0,0 +1,24 @@
1
+ import type { AppConfigValue } from "./rpc.js";
2
+ /**
3
+ * Read the installation's customization config — the first rung of the App
4
+ * Packages customization ladder (see docs/app_packages.md § The customization
5
+ * ladder). A package declares typed config knobs with defaults (labels, feature
6
+ * toggles, theme, column choices); each installation stores the customized values
7
+ * and the app reads them here. Updates flow: editing config in the product
8
+ * changes what this returns, with no re-deploy.
9
+ *
10
+ * `defaults` is the package contract's declared defaults — the canonical fallback
11
+ * (the codegen surface emits them so the call is fully typed). They fill any knob
12
+ * the installation hasn't overridden and provide a flicker-free first paint:
13
+ * before the context resolves the stored map is empty, so the defaults show
14
+ * immediately and the stored values overlay them once resolved.
15
+ *
16
+ * const { config } = useConfig({ "deal.label": "Deal", "show_archived": false });
17
+ * <Text>{config["deal.label"]}</Text>
18
+ *
19
+ * Gate on `loading` only for config-derived layout that must not flash a default.
20
+ */
21
+ export declare function useConfig<T extends Record<string, AppConfigValue> = Record<string, AppConfigValue>>(defaults?: T): {
22
+ config: T;
23
+ loading: boolean;
24
+ };
@@ -0,0 +1,33 @@
1
+ import { useMemo } from "react";
2
+ import { useAppContext } from "./viewer.js";
3
+ /**
4
+ * Read the installation's customization config — the first rung of the App
5
+ * Packages customization ladder (see docs/app_packages.md § The customization
6
+ * ladder). A package declares typed config knobs with defaults (labels, feature
7
+ * toggles, theme, column choices); each installation stores the customized values
8
+ * and the app reads them here. Updates flow: editing config in the product
9
+ * changes what this returns, with no re-deploy.
10
+ *
11
+ * `defaults` is the package contract's declared defaults — the canonical fallback
12
+ * (the codegen surface emits them so the call is fully typed). They fill any knob
13
+ * the installation hasn't overridden and provide a flicker-free first paint:
14
+ * before the context resolves the stored map is empty, so the defaults show
15
+ * immediately and the stored values overlay them once resolved.
16
+ *
17
+ * const { config } = useConfig({ "deal.label": "Deal", "show_archived": false });
18
+ * <Text>{config["deal.label"]}</Text>
19
+ *
20
+ * Gate on `loading` only for config-derived layout that must not flash a default.
21
+ */
22
+ export function useConfig(defaults) {
23
+ const ctx = useAppContext();
24
+ const stored = ctx.config;
25
+ // Key the memo on the defaults CONTENT, not identity. An inline literal is a
26
+ // fresh reference each render, so a content hash keeps the result reference
27
+ // stable while values are unchanged AND recomputes when a default value changes
28
+ // (e.g. an i18n label after a locale switch) — which a ref-captured defaults
29
+ // would silently miss. Config maps are tiny, so the stringify cost is trivial.
30
+ const defaultsKey = JSON.stringify(defaults ?? {});
31
+ const config = useMemo(() => ({ ...(defaults ?? {}), ...stored }), [stored, defaultsKey]);
32
+ return { config, loading: !ctx.resolved };
33
+ }
@@ -21,6 +21,8 @@ export type { UploadedFile, AttachedFile, BaseQueryOptions, QueryOptions, Infini
21
21
  export { useComments, useCommentCounts } from "./comments.js";
22
22
  export type { AppComment, AppCommentFile, CommentsState, UseCommentsArgs, CommentCountsState, UseCommentCountsArgs, } from "./comments.js";
23
23
  export { useViewer } from "./viewer.js";
24
+ export { useConfig } from "./config.js";
25
+ export type { AppConfigValue } from "./rpc.js";
24
26
  export { requestGeofencedLocation, isWithinZone } from "./geolocation.js";
25
27
  export type { GeofenceZone, GeoCoords, GeofenceOutcome, GeofenceOptions } from "./geolocation.js";
26
28
  export { rpc, isEmbedded } from "./rpc.js";
package/dist/src/index.js CHANGED
@@ -18,6 +18,7 @@ export { mount } from "./mount.js";
18
18
  export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFieldOptions, useFileUpload, useAttachments, useMembers, useAgentRun, useAgentRuns, } from "./hooks.js";
19
19
  export { useComments, useCommentCounts } from "./comments.js";
20
20
  export { useViewer } from "./viewer.js";
21
+ export { useConfig } from "./config.js";
21
22
  export { requestGeofencedLocation, isWithinZone } from "./geolocation.js";
22
23
  export { rpc, isEmbedded } from "./rpc.js";
23
24
  export { openExternal } from "./open_external.js";
package/dist/src/rpc.d.ts CHANGED
@@ -45,6 +45,15 @@ export interface AgentRunHandle {
45
45
  * The PostHog key/host are not here — they're the public project key, hardcoded
46
46
  * in `analytics.ts`.
47
47
  */
48
+ /**
49
+ * A single installation config value. An app package declares typed config knobs
50
+ * (labels, toggles, theme, column choices) with defaults; an installation stores
51
+ * the customized values, which the SDK's `useConfig()` reads. The canonical type
52
+ * lives in `@lotics/shared` (`AppInstallationConfigValue`); the SDK mirrors it
53
+ * here so it stays a zero-internal-dependency published package (the boundary
54
+ * mirror, like `AppContext` itself).
55
+ */
56
+ export type AppConfigValue = string | number | boolean;
48
57
  export interface AppContext {
49
58
  app_id: string;
50
59
  app_name: string;
@@ -57,6 +66,13 @@ export interface AppContext {
57
66
  * app that didn't opt in, and (vacuously) for standalone visitors.
58
67
  */
59
68
  comments_enabled: boolean;
69
+ /**
70
+ * The installation's customization values — the package's config knobs by
71
+ * alias (see `useConfig`). Empty for a bespoke app (no package) and until the
72
+ * context resolves. Read-only; an app changes config through the product, not
73
+ * the SDK.
74
+ */
75
+ config: Record<string, AppConfigValue>;
60
76
  }
61
77
  /**
62
78
  * Whether the app is running embedded in a Lotics host (vs. standalone at its
package/dist/src/rpc.js CHANGED
@@ -529,6 +529,7 @@ async function standaloneContext() {
529
529
  // comments are unavailable regardless of the capability flag.
530
530
  member_id: null,
531
531
  comments_enabled: info.comments_enabled,
532
+ config: info.config ?? {},
532
533
  };
533
534
  }
534
535
  async function standaloneQuery(p) {
@@ -1,11 +1,13 @@
1
+ import { type AppConfigValue } from "./rpc.js";
1
2
  /**
2
3
  * Read the app's context once, shared across every hook via a stable SWR key.
3
4
  * The host (the product iframe, or `lotics app dev`) supplies the signed-in
4
- * member and the app's declared capabilities.
5
+ * member, the app's declared capabilities, and the installation's config.
5
6
  */
6
7
  export declare function useAppContext(): {
7
8
  memberId: string | null;
8
9
  commentsEnabled: boolean;
10
+ config: Record<string, AppConfigValue>;
9
11
  resolved: boolean;
10
12
  };
11
13
  /**
@@ -1,9 +1,14 @@
1
1
  import useSWR from "swr";
2
2
  import { rpc } from "./rpc.js";
3
+ // Stable identity for the not-yet-resolved / bespoke-app case. An inline `{}`
4
+ // fallback would be a fresh reference every render, defeating useConfig's
5
+ // memo (its result would recompute — and re-render consumers — on every
6
+ // render even with unchanged values).
7
+ const EMPTY_CONFIG = {};
3
8
  /**
4
9
  * Read the app's context once, shared across every hook via a stable SWR key.
5
10
  * The host (the product iframe, or `lotics app dev`) supplies the signed-in
6
- * member and the app's declared capabilities.
11
+ * member, the app's declared capabilities, and the installation's config.
7
12
  */
8
13
  export function useAppContext() {
9
14
  const { data } = useSWR("app-context", () => rpc("context", {}), {
@@ -15,6 +20,7 @@ export function useAppContext() {
15
20
  return {
16
21
  memberId: data?.member_id ?? null,
17
22
  commentsEnabled: data?.comments_enabled ?? false,
23
+ config: data?.config ?? EMPTY_CONFIG,
18
24
  resolved: data !== undefined,
19
25
  };
20
26
  }
package/docs/runtime.md CHANGED
@@ -222,10 +222,13 @@ The **streaming** agent-run op is *not* reachable through `rpc()` — its
222
222
  response is a chunk stream, not a single value; it's internal to `useAgentRun`.
223
223
 
224
224
  `rpc("context", {})` resolves the app's identity: `{ app_id, app_name,
225
- workspace_id, organization_id, member_id, comments_enabled }`. `member_id` is
226
- the signed-in member when embedded, `null` standalone. **Limitation:** the
227
- context type is not exported from the package root type the result yourself
228
- via the `rpc<T>` generic.
225
+ workspace_id, organization_id, member_id, comments_enabled, config }`.
226
+ `member_id` is the signed-in member when embedded, `null` standalone; `config`
227
+ is the installation's customization mapread it through
228
+ [`useConfig`](#installation-config--useconfig), not off this raw op. **Limitation:**
229
+ the context type is not exported from the package root — type the result
230
+ yourself via the `rpc<T>` generic (`AppConfigValue`, the config value type, *is*
231
+ exported).
229
232
 
230
233
  ## `openExternal()` — open a link in a new tab
231
234
 
@@ -366,6 +369,37 @@ install `posthog-js` or call any analytics API from app code.
366
369
  browsers (e.g. Playwright) are never tracked, so analytics cannot be verified
367
370
  through headless automation.
368
371
 
372
+ ## Installation config — `useConfig()`
373
+
374
+ **`useConfig(defaults?)`** → `{ config, loading }` — the App-Packages customization
375
+ knobs for this installation (labels, toggles, theme, column choices), keyed by the
376
+ alias the package contract declares. Exact signature: `dist/src/config.d.ts`.
377
+
378
+ ```tsx
379
+ const { config } = useConfig({ board_title: "Tasks", show_archive: false });
380
+ ```
381
+
382
+ - **Pass the contract's defaults.** They fill un-overridden knobs and paint
383
+ flicker-free before the context resolves; the installation's stored values
384
+ overlay them (a stored value always wins over the same-key default). `loading`
385
+ is true only until the **one-shot** context RPC settles (it never revalidates)
386
+ — gate on it only for config-derived layout that must not flash a default.
387
+ - Values are `string | number | boolean` (`AppConfigValue`, exported from the
388
+ package root). The returned object is referentially stable across renders while
389
+ the values are unchanged — the merge is memoized, and the defaults are keyed by
390
+ their *content*, so passing a fresh inline `{…}` literal every render is fine
391
+ (no churn); it's safe as a `useEffect`/`useMemo` dependency.
392
+ - **Bespoke apps** (not installed from a package) get `{}` from the host — the
393
+ hook resolves to just the defaults, so shared code needs no package check.
394
+ - **A standalone (public) app behind a password never receives its stored config
395
+ in the client.** The knobs are gated server-side (they can carry business
396
+ terms), and the SDK resolves the app's identity once — before the visitor
397
+ authenticates — so `useConfig` returns the defaults only, even after unlock.
398
+ Design a public, password-gated app to be correct on its defaults alone.
399
+ (Embedded and standalone-*unprotected* apps both get the stored config.)
400
+ - **Read-only.** Config is edited in the product (the installation's settings),
401
+ never written from app code — there is no setter.
402
+
369
403
  ## For package contributors
370
404
 
371
405
  Everything below concerns changing `@lotics/app-sdk` itself (in the Lotics
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.49.1",
3
+ "version": "0.50.0",
4
4
  "description": "Runtime SDK for Lotics custom-code apps — typed hooks, postMessage bridge, mount entry point",
5
5
  "type": "module",
6
6
  "exports": {