@lotics/app-sdk 0.19.0 → 0.20.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.
@@ -28,6 +28,15 @@ export interface QueryOptions {
28
28
  * `loadMore()` appends the next.
29
29
  */
30
30
  pageSize?: number;
31
+ /**
32
+ * When `false`, the query does not run: `rows` stays empty, `loading` is
33
+ * false, and no request is sent. Flip it back to `true` to fetch. This is the
34
+ * primitive for search-as-you-type (skip until the user types) and for detail
35
+ * queries (skip until a row is selected) — a parameterized search filter
36
+ * matches everything on an empty term, so an always-on query would dump the
37
+ * whole table on first paint. Default `true`.
38
+ */
39
+ enabled?: boolean;
31
40
  }
32
41
  /**
33
42
  * Trigger a workflow by alias from the app's manifest.
package/dist/src/hooks.js CHANGED
@@ -35,6 +35,7 @@ export function useWorkflow(alias) {
35
35
  }
36
36
  export function useQuery(alias, params, opts) {
37
37
  const pageSize = opts?.pageSize;
38
+ const enabled = opts?.enabled ?? true;
38
39
  // Stringified params key — structurally-equal-but-new param objects don't
39
40
  // re-fire the effect every render.
40
41
  const paramsKey = JSON.stringify(params ?? {});
@@ -48,7 +49,8 @@ export function useQuery(alias, params, opts) {
48
49
  const mockRows = getMockRows(alias);
49
50
  if (mockRows)
50
51
  return { rows: mockRows, loading: false, error: null, hasMore: false };
51
- return { rows: [], loading: true, error: null, hasMore: false };
52
+ // A disabled query starts idle (no loading flash), not pending.
53
+ return { rows: [], loading: enabled, error: null, hasMore: false };
52
54
  });
53
55
  const [loadingMore, setLoadingMore] = useState(false);
54
56
  useEffect(() => {
@@ -59,6 +61,10 @@ export function useQuery(alias, params, opts) {
59
61
  setState({ rows: mockRows, loading: false, error: null, hasMore: false });
60
62
  return;
61
63
  }
64
+ if (!enabled) {
65
+ setState({ rows: [], loading: false, error: null, hasMore: false });
66
+ return;
67
+ }
62
68
  let cancelled = false;
63
69
  setState((s) => ({ ...s, loading: true, error: null }));
64
70
  rpc("query", {
@@ -87,7 +93,7 @@ export function useQuery(alias, params, opts) {
87
93
  cancelled = true;
88
94
  };
89
95
  // eslint-disable-next-line react-hooks/exhaustive-deps
90
- }, [alias, paramsKey, refetchToken, pageSize]);
96
+ }, [alias, paramsKey, refetchToken, pageSize, enabled]);
91
97
  const refetch = useCallback(() => {
92
98
  setRefetchToken((n) => n + 1);
93
99
  }, []);
@@ -31,3 +31,5 @@ export { row, readLinks } from "./row.js";
31
31
  export type { ResolvedLink } from "./row.js";
32
32
  export { useOptimistic } from "./use_optimistic.js";
33
33
  export type { OptimisticApi } from "./use_optimistic.js";
34
+ export { useRecents } from "./use_recents.js";
35
+ export type { RecentsApi, RecentsOptions } from "./use_recents.js";
package/dist/src/index.js CHANGED
@@ -22,3 +22,4 @@ export { readMembers } from "./members.js";
22
22
  export { readSelect } from "./select.js";
23
23
  export { row, readLinks } from "./row.js";
24
24
  export { useOptimistic } from "./use_optimistic.js";
25
+ export { useRecents } from "./use_recents.js";
@@ -0,0 +1,19 @@
1
+ export interface RecentsApi<T> {
2
+ /** Remembered items, most-recent first (deduped by `keyOf`, capped at `max`). */
3
+ recents: T[];
4
+ /** Record `item` as most-recent: moves an existing match to the front, caps,
5
+ * and persists. Call this on select. */
6
+ remember: (item: T) => void;
7
+ /** Drop one remembered item (matched by `keyOf`). */
8
+ forget: (item: T) => void;
9
+ /** Clear the whole list. */
10
+ clear: () => void;
11
+ }
12
+ export interface RecentsOptions<T> {
13
+ /** Stable identity per item — used to dedup and to match `forget`. Default:
14
+ * `JSON.stringify`. Pass the item's id for objects you'll re-create. */
15
+ keyOf?: (item: T) => string;
16
+ /** Maximum items kept. Default 5. */
17
+ max?: number;
18
+ }
19
+ export declare function useRecents<T>(key: string, options?: RecentsOptions<T>): RecentsApi<T>;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Remember a small, most-recent-first list of items across sessions — the
3
+ * "recently used" affordance a search box shows when focused but empty.
4
+ *
5
+ * Persistence is `localStorage`, namespaced per `key`, so two comboboxes on one
6
+ * page keep separate lists and a deployed app's recents survive reloads. The
7
+ * SDK is the right home (not `@lotics/ui`): `localStorage` is web-only, and
8
+ * `@lotics/ui` also builds for native, where it doesn't exist. The list is
9
+ * always state-backed, so the hook keeps working in-memory if storage is
10
+ * unavailable (private mode / quota) — recents is an enhancement, never a
11
+ * reason to crash the app.
12
+ */
13
+ import { useCallback, useEffect, useRef, useState } from "react";
14
+ const PREFIX = "lotics.recents.";
15
+ function read(storageKey) {
16
+ try {
17
+ const raw = localStorage.getItem(storageKey);
18
+ if (raw == null)
19
+ return [];
20
+ const parsed = JSON.parse(raw);
21
+ return Array.isArray(parsed) ? parsed : [];
22
+ }
23
+ catch {
24
+ // Storage unavailable or corrupt JSON: start empty, keep working in-memory.
25
+ return [];
26
+ }
27
+ }
28
+ function write(storageKey, items) {
29
+ try {
30
+ localStorage.setItem(storageKey, JSON.stringify(items));
31
+ }
32
+ catch {
33
+ // Storage unavailable (private mode / quota): the in-memory list still
34
+ // updates; only cross-reload persistence is lost.
35
+ }
36
+ }
37
+ export function useRecents(key, options = {}) {
38
+ const storageKey = PREFIX + key;
39
+ // Config is captured in refs so the returned callbacks stay referentially
40
+ // stable across renders even when the caller passes an inline `keyOf`.
41
+ const keyOfRef = useRef(options.keyOf ?? ((item) => JSON.stringify(item)));
42
+ keyOfRef.current = options.keyOf ?? ((item) => JSON.stringify(item));
43
+ const maxRef = useRef(options.max ?? 5);
44
+ maxRef.current = options.max ?? 5;
45
+ const [recents, setRecents] = useState(() => read(storageKey).slice(0, maxRef.current));
46
+ // Re-hydrate when the namespace changes (a different list on the same screen).
47
+ useEffect(() => {
48
+ setRecents(read(storageKey).slice(0, maxRef.current));
49
+ }, [storageKey]);
50
+ const remember = useCallback((item) => {
51
+ setRecents((prev) => {
52
+ const k = keyOfRef.current(item);
53
+ const next = [item, ...prev.filter((p) => keyOfRef.current(p) !== k)].slice(0, maxRef.current);
54
+ write(storageKey, next);
55
+ return next;
56
+ });
57
+ }, [storageKey]);
58
+ const forget = useCallback((item) => {
59
+ setRecents((prev) => {
60
+ const k = keyOfRef.current(item);
61
+ const next = prev.filter((p) => keyOfRef.current(p) !== k);
62
+ write(storageKey, next);
63
+ return next;
64
+ });
65
+ }, [storageKey]);
66
+ const clear = useCallback(() => {
67
+ write(storageKey, []);
68
+ setRecents([]);
69
+ }, [storageKey]);
70
+ return { recents, remember, forget, clear };
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.19.0",
3
+ "version": "0.20.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": {