@lotics/app-sdk 0.100.0 → 0.101.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.
Files changed (108) hide show
  1. package/AGENTS.md +32 -47
  2. package/dist/agent_stream.d.ts +131 -0
  3. package/dist/ask_ai.d.ts +27 -0
  4. package/dist/attachments.d.ts +58 -0
  5. package/dist/chunk-ARV5FAU5.js +1132 -0
  6. package/dist/comments.d.ts +89 -0
  7. package/dist/error_report.d.ts +9 -0
  8. package/dist/folder_pick.d.ts +8 -0
  9. package/dist/geolocation.d.ts +42 -0
  10. package/dist/hooks.d.ts +251 -0
  11. package/dist/{src/index.d.ts → index.d.ts} +13 -22
  12. package/dist/index.js +31309 -0
  13. package/dist/index.js.LEGAL.txt +11 -0
  14. package/dist/members.d.ts +32 -0
  15. package/dist/mock.d.ts +37 -0
  16. package/dist/mount.d.ts +19 -0
  17. package/dist/new_record.d.ts +37 -0
  18. package/dist/open_app.d.ts +12 -0
  19. package/dist/open_external.d.ts +10 -0
  20. package/dist/overlay.d.ts +25 -0
  21. package/dist/queries.d.ts +231 -0
  22. package/dist/recording.d.ts +47 -0
  23. package/dist/recording_state.d.ts +43 -0
  24. package/dist/rename_file.d.ts +13 -0
  25. package/dist/router.d.ts +10 -0
  26. package/dist/router.js +97 -0
  27. package/dist/row.d.ts +87 -0
  28. package/dist/rpc.d.ts +114 -0
  29. package/dist/select.d.ts +24 -0
  30. package/dist/shared_types.d.ts +8 -0
  31. package/dist/store.d.ts +43 -0
  32. package/dist/types.d.ts +36 -0
  33. package/dist/upload/optimize.d.ts +30 -0
  34. package/dist/upload/pipeline.d.ts +36 -0
  35. package/dist/upload/transport.d.ts +19 -0
  36. package/dist/url_params.d.ts +55 -0
  37. package/dist/use_recents.d.ts +15 -0
  38. package/dist/{src/use_url_state.d.ts → use_url_state.d.ts} +0 -2
  39. package/dist/viewer.d.ts +41 -0
  40. package/dist/written.d.ts +77 -0
  41. package/docs/ai.md +74 -133
  42. package/docs/data_fetching.md +209 -290
  43. package/docs/files.md +61 -51
  44. package/docs/members_and_options.md +93 -63
  45. package/docs/mutations.md +135 -205
  46. package/docs/navigation_and_state.md +26 -35
  47. package/docs/queries.md +144 -207
  48. package/docs/recipes.md +21 -45
  49. package/docs/runtime.md +74 -137
  50. package/docs/security.md +8 -11
  51. package/docs/workflows.md +189 -174
  52. package/package.json +27 -28
  53. package/dist/src/agent_stream.d.ts +0 -200
  54. package/dist/src/agent_stream.js +0 -314
  55. package/dist/src/ask_ai.d.ts +0 -40
  56. package/dist/src/ask_ai.js +0 -35
  57. package/dist/src/attachments.d.ts +0 -68
  58. package/dist/src/attachments.js +0 -93
  59. package/dist/src/comments.d.ts +0 -127
  60. package/dist/src/comments.js +0 -192
  61. package/dist/src/download.js +0 -54
  62. package/dist/src/geolocation.d.ts +0 -64
  63. package/dist/src/geolocation.js +0 -96
  64. package/dist/src/hooks.d.ts +0 -781
  65. package/dist/src/hooks.js +0 -860
  66. package/dist/src/index.js +0 -34
  67. package/dist/src/members.d.ts +0 -105
  68. package/dist/src/members.js +0 -62
  69. package/dist/src/mock.d.ts +0 -118
  70. package/dist/src/mock.js +0 -124
  71. package/dist/src/mount.d.ts +0 -47
  72. package/dist/src/mount.js +0 -34
  73. package/dist/src/new_record.d.ts +0 -74
  74. package/dist/src/new_record.js +0 -117
  75. package/dist/src/open_app.d.ts +0 -15
  76. package/dist/src/open_app.js +0 -18
  77. package/dist/src/open_external.d.ts +0 -16
  78. package/dist/src/open_external.js +0 -19
  79. package/dist/src/recording.d.ts +0 -59
  80. package/dist/src/recording.js +0 -30
  81. package/dist/src/recording_state.d.ts +0 -59
  82. package/dist/src/recording_state.js +0 -94
  83. package/dist/src/router.d.ts +0 -17
  84. package/dist/src/router.js +0 -144
  85. package/dist/src/row.d.ts +0 -159
  86. package/dist/src/row.js +0 -254
  87. package/dist/src/rpc.d.ts +0 -207
  88. package/dist/src/rpc.js +0 -904
  89. package/dist/src/select.d.ts +0 -34
  90. package/dist/src/select.js +0 -40
  91. package/dist/src/types.d.ts +0 -115
  92. package/dist/src/types.js +0 -1
  93. package/dist/src/upload/optimize.d.ts +0 -54
  94. package/dist/src/upload/optimize.js +0 -207
  95. package/dist/src/upload/pipeline.d.ts +0 -55
  96. package/dist/src/upload/pipeline.js +0 -52
  97. package/dist/src/upload/transport.d.ts +0 -42
  98. package/dist/src/upload/transport.js +0 -128
  99. package/dist/src/url_params.d.ts +0 -93
  100. package/dist/src/url_params.js +0 -215
  101. package/dist/src/use_optimistic.d.ts +0 -27
  102. package/dist/src/use_optimistic.js +0 -27
  103. package/dist/src/use_recents.d.ts +0 -19
  104. package/dist/src/use_recents.js +0 -71
  105. package/dist/src/use_url_state.js +0 -73
  106. package/dist/src/viewer.d.ts +0 -26
  107. package/dist/src/viewer.js +0 -47
  108. /package/dist/{src/download.d.ts → download.d.ts} +0 -0
package/dist/src/index.js DELETED
@@ -1,34 +0,0 @@
1
- /**
2
- * Lotics App SDK — the runtime + typed hooks bundled into every custom-code
3
- * app at build time. Apps `import { mount, useQuery, useWorkflow } from
4
- * "@lotics/app-sdk"` and ship the resulting bundle via `lotics app deploy`.
5
- *
6
- * This SDK is data + RPC only — it deliberately does NOT re-export any
7
- * `@lotics/ui` component, so an app's dependency on the kit is its own and one
8
- * version answers for it. That is a packaging choice, NOT a limitation: apps
9
- * import `@lotics/ui` directly as an ordinary dependency. The starter scaffold
10
- * (`packages/sdk/src/starter_template.ts`) wires it — the kit as a dependency,
11
- * `@lotics/ui/styles.css` + `fonts.css` in the entry, and `loticsResolve()` as
12
- * the whole `resolve` block. Build screens by composing kit components (Card,
13
- * Metric, charts, Table, …), not raw HTML/CSS. See `docs/apps.md` → "Styling
14
- * & components".
15
- */
16
- export { mount } from "./mount.js";
17
- export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useCount, useFieldOptions, useFileUpload, useAttachments, useMembers, useAgentRun, useAgentRuns, useAiContext, buildChoiceOutput, } from "./hooks.js";
18
- export { useComments, useCommentCounts } from "./comments.js";
19
- export { useViewer } from "./viewer.js";
20
- export { useRecording } from "./recording.js";
21
- export { requestGeofencedLocation, isWithinZone } from "./geolocation.js";
22
- export { rpc, isEmbedded } from "./rpc.js";
23
- export { openExternal } from "./open_external.js";
24
- export { openApp } from "./open_app.js";
25
- export { askAi } from "./ask_ai.js";
26
- export { downloadFile } from "./download.js";
27
- export { readMembers } from "./members.js";
28
- export { readSelect } from "./select.js";
29
- export { row, readLinks, readFiles, readLocked, readCreatedAt, readUpdatedAt } from "./row.js";
30
- export { useOptimistic } from "./use_optimistic.js";
31
- export { useNewRecord, newRecordId } from "./new_record.js";
32
- export { useRecents } from "./use_recents.js";
33
- export { useUrlState } from "./use_url_state.js";
34
- export { urlParam } from "./url_params.js";
@@ -1,105 +0,0 @@
1
- /**
2
- * Reader for `select_member` cells in `useQuery` rows.
3
- *
4
- * The server (`backend/lib/select_member_resolver.ts`) rewrites every
5
- * `select_member` column from its storage shape (`string[]` of bare member
6
- * IDs) into `ResolvedMember[]` before the row reaches the app. Apps used to
7
- * hardcode an id→name fallback map because the SDK didn't expose the
8
- * resolved shape; this helper makes the right shape the obvious one.
9
- *
10
- * If the wire format changes, the resolver and this reader move together.
11
- */
12
- /**
13
- * A member, in the ONE shape every door returns — a `select_member` cell in a
14
- * `useQuery` row and the `useMembers` roster alike.
15
- *
16
- * The two used to disagree: the roster carried an avatar, a resolved cell did
17
- * not, and both were typed `ResolvedMember`, so an app author holding one could
18
- * not tell which. That is why no register row ever rendered an avatar — the
19
- * data was absent and nothing said so.
20
- *
21
- * It happened a second time, and the fix is the same: the roster returned
22
- * neither `groups` nor `role` long after a resolved cell carried both, so an
23
- * app reading `m.groups` off `useMembers()` got `undefined` and drew nothing.
24
- * Read a field here and you may read it on either door.
25
- *
26
- * The private fields share ONE boundary, not four: an authenticated member of
27
- * the app's own org sees `email`, `image`, `groups` and `role`; an anonymous
28
- * visitor to a public app sees `id` and `name` (plus `archived`, which is not
29
- * private — see the field). Absent ≠ empty — `image: null` means the member has
30
- * no photo, `image` MISSING means you were never told, and `groups: []` means
31
- * they are on no team while a missing `groups` means the same "not told". Never
32
- * collapse the two: one is a fact about a colleague, the other is a fact about
33
- * your own permissions.
34
- *
35
- * The one field the doors legitimately differ on is `archived`, and it is a
36
- * difference of ROW SET rather than of shape: a resolved cell must keep naming
37
- * whoever handled a record two years ago, while the roster answers "who may I
38
- * assign?" and never offers a departed member at all.
39
- */
40
- export interface ResolvedMember {
41
- id: string;
42
- /** `null` when the id resolves outside the app's org (e.g. removed
43
- * member) — surfaces the missing state explicitly instead of an
44
- * empty string. */
45
- name: string | null;
46
- /** Present only on authenticated responses. Omitted on public-app
47
- * responses (no PII exposure to anonymous visitors). */
48
- email?: string | null;
49
- /** Avatar URL, already presigned — render it directly. `null` when the member
50
- * has no profile image (the common case: photos are opt-in). Omitted on
51
- * public-app responses. */
52
- image?: string | null;
53
- /** The member's group names — the platform's "department". There is no
54
- * department field; a member group is what an org uses to say Sale, Kế toán,
55
- * CSKH. `[]` when the member is in none. Omitted on public-app responses. */
56
- groups?: string[];
57
- /**
58
- * The member's ORGANIZATION role. Omitted on public-app responses, and also
59
- * when the id did not resolve — there is no member to have a level.
60
- *
61
- * It arrives RAW, and it is your job to translate it: the platform ships no
62
- * display word for it, because a server that picked one would leak English
63
- * into every localized app. Map it yourself (`admin` → "Quản trị viên") next
64
- * to the rest of your vocabulary.
65
- *
66
- * It is a PERMISSION level and not a job title. Everyone who reads "Admin"
67
- * beside a name on a sales register will read it as rank; if the question
68
- * your screen answers is "who is this person in the company", the answer is
69
- * `groups`, not this.
70
- */
71
- role?: "owner" | "admin" | "member";
72
- /**
73
- * ISO timestamp of when this person joined the organization. Omitted on
74
- * public-app responses, and when the id did not resolve — there is no
75
- * membership to have begun.
76
- *
77
- * Render it at whatever precision your question needs; `@lotics/ui`'s
78
- * `MemberProfileCard` shows month and year, because "is this the new person?"
79
- * does not want a day.
80
- */
81
- joined?: string;
82
- /**
83
- * `true` when this person has LEFT the organization — omitted otherwise,
84
- * never `false`, because it rides every cell of every row and current staff
85
- * are the overwhelming case.
86
- *
87
- * It arrives on both audiences: a departed colleague reading as a current
88
- * assignee is wrong on a public app too, and it discloses less than the name
89
- * already beside it. Feed it to `inactive` on `MemberChip` /
90
- * `MemberProfileCard` so a record that still names them reads as history
91
- * rather than as a live assignment.
92
- *
93
- * You will not see it on the `useMembers` roster, and that is correct rather
94
- * than missing: the roster answers "who may I ASSIGN?" and departed members
95
- * are not candidates, so it never returns one.
96
- */
97
- archived?: true;
98
- }
99
- /**
100
- * Parse a `useQuery` cell value into `ResolvedMember[]`. Returns `[]` for
101
- * null/undefined/empty cells and for any unexpected shape — callers iterate
102
- * uniformly without null-checks. Entries that fail the shape check are
103
- * dropped silently rather than corrupting the array with partial data.
104
- */
105
- export declare function readMembers(value: unknown): ResolvedMember[];
@@ -1,62 +0,0 @@
1
- /**
2
- * Reader for `select_member` cells in `useQuery` rows.
3
- *
4
- * The server (`backend/lib/select_member_resolver.ts`) rewrites every
5
- * `select_member` column from its storage shape (`string[]` of bare member
6
- * IDs) into `ResolvedMember[]` before the row reaches the app. Apps used to
7
- * hardcode an id→name fallback map because the SDK didn't expose the
8
- * resolved shape; this helper makes the right shape the obvious one.
9
- *
10
- * If the wire format changes, the resolver and this reader move together.
11
- */
12
- /**
13
- * Parse a `useQuery` cell value into `ResolvedMember[]`. Returns `[]` for
14
- * null/undefined/empty cells and for any unexpected shape — callers iterate
15
- * uniformly without null-checks. Entries that fail the shape check are
16
- * dropped silently rather than corrupting the array with partial data.
17
- */
18
- export function readMembers(value) {
19
- if (!Array.isArray(value))
20
- return [];
21
- const out = [];
22
- for (const entry of value) {
23
- if (!entry || typeof entry !== "object")
24
- continue;
25
- const obj = entry;
26
- const id = obj.id;
27
- if (typeof id !== "string" || id === "")
28
- continue;
29
- const name = typeof obj.name === "string" ? obj.name : obj.name === null ? null : null;
30
- const m = { id, name };
31
- // PRESENT ⇒ copy, ABSENT ⇒ leave off. The distinction is the permission
32
- // boundary itself: a missing key means a public reader was never told,
33
- // while `null` means the member genuinely has none. Defaulting either to
34
- // null here would erase that and make a gated field look like an empty one.
35
- if ("email" in obj)
36
- m.email = typeof obj.email === "string" ? obj.email : null;
37
- if ("image" in obj)
38
- m.image = typeof obj.image === "string" ? obj.image : null;
39
- if ("groups" in obj) {
40
- m.groups = Array.isArray(obj.groups)
41
- ? obj.groups.filter((g) => typeof g === "string")
42
- : [];
43
- }
44
- // A CLOSED enum, so an unrecognized value is dropped rather than passed
45
- // through: the field's whole worth is that a caller can switch on it, and a
46
- // server that grew a fourth role must not have apps rendering the raw word
47
- // in a UI that has no translation for it.
48
- if (obj.role === "owner" || obj.role === "admin" || obj.role === "member")
49
- m.role = obj.role;
50
- // A non-empty STRING or nothing: an empty date is not a date, and letting
51
- // "" through would render an empty labelled row rather than no row.
52
- if (typeof obj.joined === "string" && obj.joined !== "")
53
- m.joined = obj.joined;
54
- // The server sends this ONLY for a departed member and only as `true`, so
55
- // the truthy test is the whole contract — there is no `false` to carry, and
56
- // inventing one would put the key on every current member's cell.
57
- if (obj.archived === true)
58
- m.archived = true;
59
- out.push(m);
60
- }
61
- return out;
62
- }
@@ -1,118 +0,0 @@
1
- /**
2
- * Demo / design-time fixture support for `useQuery` and `useWorkflow`.
3
- *
4
- * Activation contract:
5
- *
6
- * 1. App passes `{ fixture }` to `mount(<App />, { fixture })`. The fixture
7
- * is a `{ queries: { alias: MockQuery }, workflows: { alias: MockWorkflow } }`
8
- * map keyed by the same aliases the app declared in
9
- * `package.json#lotics.queries` / `#lotics.workflows`.
10
- * 2. At runtime, the user (or a screenshot script) loads the app with the
11
- * `?__mock=1` URL search param. Without that param the SDK ignores the
12
- * fixture entirely and both hooks flow through the RPC bridge as usual.
13
- *
14
- * The two-step gate keeps demo data shipping in the bundle from leaking into
15
- * normal traffic — the param namespace (`__mock` prefix) is reserved and
16
- * unlikely to collide with app-side query state. Apps that don't pass a
17
- * fixture pay nothing: `getMockRows` / `getMockWorkflow` return `null` for
18
- * every alias and the hook path is unchanged.
19
- *
20
- * `workflows` exists because the side-effect argument runs the other way: a
21
- * mocked workflow does not RUN, so it produces no notification and no audit
22
- * trail — it PREVENTS them. What it cannot produce is the followup state a
23
- * subsequent `useQuery` would read, which is the author's call and is already
24
- * true of a mocked query. Without it, an app whose only AI surface is a
25
- * workflow that reads and calls `agent(...)` — the standard shape — had no
26
- * non-billing path to its own in-flight / done / error states at all, so those
27
- * three screens could not be reviewed without spending on a live workspace.
28
- *
29
- * A fixture entry may be the RESULT, or a FUNCTION of the call. For a workflow
30
- * the function form is what makes the in-flight state reachable: resolve on a
31
- * timer and the app renders the pending branch it otherwise never shows. It also
32
- * lets one alias answer differently per input, which is how an error branch is
33
- * reviewed beside a success one. For a query it is what reproduces a NARROWED
34
- * surface: the server applies `filter`/`sort`/`limit` after the named query, and
35
- * a static row array cannot, so a master/detail drawer under `?__mock=1` would
36
- * otherwise show every parent's children under every parent.
37
- *
38
- * `recordings` hands `useRecording` a canned state per alias, so each state a
39
- * recording act passes through can be drawn without a host that records.
40
- *
41
- * What's deliberately *not* mocked: `useFileUpload`. Bytes and progress are a
42
- * different shape from a request/response pair, and nothing has needed it.
43
- */
44
- import type { WorkflowResult } from "./hooks.js";
45
- import type { RecordingState } from "./recording_state.js";
46
- /**
47
- * What a mocked workflow answers with: a fixed result, or a function of the
48
- * inputs it was called with. Return a promise from the function to hold the
49
- * caller in its pending state for as long as the review needs.
50
- */
51
- export type MockWorkflow = WorkflowResult | ((inputs: Record<string, unknown>) => WorkflowResult | Promise<WorkflowResult>);
52
- /** The call a mocked query is answering: the alias's params plus the per-call
53
- * refinement the screen passed. A master/detail surface narrows by `filter`,
54
- * so a fixture that ignores it renders every parent's children under every
55
- * parent — right-looking and wrong. */
56
- export interface MockQueryCall {
57
- params: Record<string, unknown>;
58
- filter?: unknown;
59
- sort?: unknown;
60
- limit?: number;
61
- }
62
- /**
63
- * What a mocked query answers with: fixed rows, or a function of the call.
64
- *
65
- * The function form is the one that reproduces a FILTERED surface. The server
66
- * applies `filter`/`sort`/`limit` after the named query; the fixture path has no
67
- * query engine, and re-implementing the filter grammar here would ship a second,
68
- * divergent copy of it — so the fixture decides, from the call it was handed,
69
- * which rows that call returns.
70
- */
71
- export type MockQuery = Array<Record<string, unknown>> | ((call: MockQueryCall) => Array<Record<string, unknown>>);
72
- export interface AppFixture {
73
- /** Map of query alias → the rows the hook returns when mock mode is on, or a
74
- * function of the call for a surface that narrows per call. */
75
- queries?: Record<string, MockQuery>;
76
- /** Map of workflow alias → the result it resolves with when mock mode is on.
77
- * The workflow never executes, so nothing it would have written is written. */
78
- workflows?: Record<string, MockWorkflow>;
79
- /** Map of workflow alias → the state `useRecording(alias)` reports when mock
80
- * mode is on. It reads as available, and `start`/`stop` resolve without
81
- * recording anything or changing the state. */
82
- recordings?: Record<string, RecordingState>;
83
- }
84
- /**
85
- * Called by `mount({ fixture })`. Module-level state because the SDK has no
86
- * React context boundary around the iframe — every hook resolves against the
87
- * same registration. Calling twice replaces (last-write-wins) which is fine
88
- * for HMR.
89
- */
90
- export declare function registerMockFixture(fixture: AppFixture | undefined): void;
91
- /**
92
- * True iff the iframe URL carries the `__mock=1` activation flag. Throws never;
93
- * a malformed URL or missing `window` (SSR / jsdom without location) silently
94
- * returns false. Distinct from `isMockMode`: a caller may key off the raw flag
95
- * (a design-time / screenshot load emits no events regardless of fixtures),
96
- * while query mocking additionally requires a registered fixture.
97
- */
98
- export declare function hasMockFlag(): boolean;
99
- /**
100
- * Returns the fixture rows for an alias when mock mode is active *and* the
101
- * fixture has an entry for that alias. Otherwise null — the hook falls
102
- * through to the real RPC path. The distinction matters: an app may mock
103
- * only some queries and let the rest flow through to real data.
104
- */
105
- export declare function getMockRows(alias: string, call: MockQueryCall): Array<Record<string, unknown>> | null;
106
- /**
107
- * The fixture entry for a workflow alias when mock mode is active *and* the
108
- * fixture has an entry for it. Otherwise null — the hook falls through to the
109
- * real RPC path, so an app may mock one workflow and let the rest execute.
110
- *
111
- * Resolved when the workflow is CALLED rather than when the hook is created, so
112
- * a fixture registered after mount (or replaced by HMR) is picked up, and an
113
- * app that never calls the workflow pays nothing.
114
- */
115
- export declare function getMockWorkflow(alias: string): MockWorkflow | null;
116
- /** The canned recording states when mock mode is active and the fixture names
117
- * any; otherwise null, and `useRecording` reads the host. */
118
- export declare function getMockRecordings(): Record<string, RecordingState> | null;
package/dist/src/mock.js DELETED
@@ -1,124 +0,0 @@
1
- /**
2
- * Demo / design-time fixture support for `useQuery` and `useWorkflow`.
3
- *
4
- * Activation contract:
5
- *
6
- * 1. App passes `{ fixture }` to `mount(<App />, { fixture })`. The fixture
7
- * is a `{ queries: { alias: MockQuery }, workflows: { alias: MockWorkflow } }`
8
- * map keyed by the same aliases the app declared in
9
- * `package.json#lotics.queries` / `#lotics.workflows`.
10
- * 2. At runtime, the user (or a screenshot script) loads the app with the
11
- * `?__mock=1` URL search param. Without that param the SDK ignores the
12
- * fixture entirely and both hooks flow through the RPC bridge as usual.
13
- *
14
- * The two-step gate keeps demo data shipping in the bundle from leaking into
15
- * normal traffic — the param namespace (`__mock` prefix) is reserved and
16
- * unlikely to collide with app-side query state. Apps that don't pass a
17
- * fixture pay nothing: `getMockRows` / `getMockWorkflow` return `null` for
18
- * every alias and the hook path is unchanged.
19
- *
20
- * `workflows` exists because the side-effect argument runs the other way: a
21
- * mocked workflow does not RUN, so it produces no notification and no audit
22
- * trail — it PREVENTS them. What it cannot produce is the followup state a
23
- * subsequent `useQuery` would read, which is the author's call and is already
24
- * true of a mocked query. Without it, an app whose only AI surface is a
25
- * workflow that reads and calls `agent(...)` — the standard shape — had no
26
- * non-billing path to its own in-flight / done / error states at all, so those
27
- * three screens could not be reviewed without spending on a live workspace.
28
- *
29
- * A fixture entry may be the RESULT, or a FUNCTION of the call. For a workflow
30
- * the function form is what makes the in-flight state reachable: resolve on a
31
- * timer and the app renders the pending branch it otherwise never shows. It also
32
- * lets one alias answer differently per input, which is how an error branch is
33
- * reviewed beside a success one. For a query it is what reproduces a NARROWED
34
- * surface: the server applies `filter`/`sort`/`limit` after the named query, and
35
- * a static row array cannot, so a master/detail drawer under `?__mock=1` would
36
- * otherwise show every parent's children under every parent.
37
- *
38
- * `recordings` hands `useRecording` a canned state per alias, so each state a
39
- * recording act passes through can be drawn without a host that records.
40
- *
41
- * What's deliberately *not* mocked: `useFileUpload`. Bytes and progress are a
42
- * different shape from a request/response pair, and nothing has needed it.
43
- */
44
- let registeredFixture;
45
- /** Aliases already warned about a static fixture under a filtered call. */
46
- const warnedStaticFilter = new Set();
47
- /**
48
- * Called by `mount({ fixture })`. Module-level state because the SDK has no
49
- * React context boundary around the iframe — every hook resolves against the
50
- * same registration. Calling twice replaces (last-write-wins) which is fine
51
- * for HMR.
52
- */
53
- export function registerMockFixture(fixture) {
54
- registeredFixture = fixture;
55
- }
56
- /**
57
- * True iff the iframe URL carries the `__mock=1` activation flag. Throws never;
58
- * a malformed URL or missing `window` (SSR / jsdom without location) silently
59
- * returns false. Distinct from `isMockMode`: a caller may key off the raw flag
60
- * (a design-time / screenshot load emits no events regardless of fixtures),
61
- * while query mocking additionally requires a registered fixture.
62
- */
63
- export function hasMockFlag() {
64
- try {
65
- return new URLSearchParams(window.location.search).get("__mock") === "1";
66
- }
67
- catch {
68
- return false;
69
- }
70
- }
71
- /**
72
- * True iff the iframe URL carries the `__mock=1` activation flag AND a
73
- * fixture was registered. Safe to call before mount — returns false when no
74
- * fixture exists.
75
- */
76
- function isMockMode() {
77
- return Boolean(registeredFixture) && hasMockFlag();
78
- }
79
- /**
80
- * Returns the fixture rows for an alias when mock mode is active *and* the
81
- * fixture has an entry for that alias. Otherwise null — the hook falls
82
- * through to the real RPC path. The distinction matters: an app may mock
83
- * only some queries and let the rest flow through to real data.
84
- */
85
- export function getMockRows(alias, call) {
86
- if (!isMockMode())
87
- return null;
88
- const fixture = registeredFixture?.queries?.[alias];
89
- if (fixture === undefined)
90
- return null;
91
- if (typeof fixture === "function")
92
- return fixture(call);
93
- // A static fixture cannot honour the call's narrowing, and the divergence is
94
- // otherwise invisible — a drawer showing four rows where production shows two
95
- // reads as a correct render. Said once per alias, at the first call that asks.
96
- if (call.filter !== undefined && !warnedStaticFilter.has(alias)) {
97
- warnedStaticFilter.add(alias);
98
- console.warn(`Mock fixture for query "${alias}" is a static row array, so the filter passed to this call ` +
99
- `is not applied — the mocked screen shows rows production would exclude. Make ` +
100
- `fixture.queries.${alias} a function of the call to narrow it.`);
101
- }
102
- return fixture;
103
- }
104
- /**
105
- * The fixture entry for a workflow alias when mock mode is active *and* the
106
- * fixture has an entry for it. Otherwise null — the hook falls through to the
107
- * real RPC path, so an app may mock one workflow and let the rest execute.
108
- *
109
- * Resolved when the workflow is CALLED rather than when the hook is created, so
110
- * a fixture registered after mount (or replaced by HMR) is picked up, and an
111
- * app that never calls the workflow pays nothing.
112
- */
113
- export function getMockWorkflow(alias) {
114
- if (!isMockMode())
115
- return null;
116
- return registeredFixture?.workflows?.[alias] ?? null;
117
- }
118
- /** The canned recording states when mock mode is active and the fixture names
119
- * any; otherwise null, and `useRecording` reads the host. */
120
- export function getMockRecordings() {
121
- if (!isMockMode())
122
- return null;
123
- return registeredFixture?.recordings ?? null;
124
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * Entry point for Lotics custom-code apps.
3
- *
4
- * Usage in a user's `src/main.tsx`:
5
- *
6
- * ```tsx
7
- * import { mount } from "@lotics/app-sdk";
8
- * import App from "./App";
9
- *
10
- * mount(<App />);
11
- * ```
12
- *
13
- * Optional second argument registers a demo/design-time fixture. When the
14
- * iframe is loaded with `?__mock=1`, `useQuery` returns rows from the
15
- * fixture instead of hitting the RPC bridge. Without the URL flag the
16
- * fixture is inert — useful for taking screenshots or iterating on
17
- * dashboard layouts before real data exists.
18
- *
19
- * ```tsx
20
- * mount(<App />, {
21
- * fixture: {
22
- * queries: { customers: MOCK_CUSTOMERS, deals: MOCK_DEALS },
23
- * },
24
- * });
25
- * ```
26
- *
27
- * `mount` wires up React 19's createRoot against `#root` in the iframe shell,
28
- * sets up window.onerror/unhandledrejection forwarding so runtime crashes
29
- * surface in the parent's debug pane, registers the fixture (if any), renders
30
- * the user's tree.
31
- *
32
- * If the bundler doesn't ship #root in the user's `index.html`, we create it
33
- * — Vite's default scaffold provides one, but defensive creation keeps the
34
- * mount resilient.
35
- */
36
- import type { ReactNode } from "react";
37
- import { type AppFixture } from "./mock.js";
38
- export interface MountOptions {
39
- /**
40
- * Optional demo fixture. When the iframe URL contains `?__mock=1`,
41
- * `useQuery(alias)` returns `fixture.queries[alias]` instead of calling
42
- * the RPC bridge. Aliases not present in the fixture still flow through
43
- * the real path — partial mocking is supported.
44
- */
45
- fixture?: AppFixture;
46
- }
47
- export declare function mount(element: ReactNode, options?: MountOptions): void;
package/dist/src/mount.js DELETED
@@ -1,34 +0,0 @@
1
- import { createRoot } from "react-dom/client";
2
- import { registerMockFixture } from "./mock.js";
3
- export function mount(element, options = {}) {
4
- registerMockFixture(options.fixture);
5
- let container = document.getElementById("root");
6
- if (!container) {
7
- container = document.createElement("div");
8
- container.id = "root";
9
- document.body.appendChild(container);
10
- }
11
- // Surface the most common silent-failure modes (a thrown render error or an
12
- // unhandled rejection) as visible text in the iframe so the developer sees
13
- // *something* even before the parent's debug telemetry is wired up.
14
- installVisibleErrorHandlers(container);
15
- createRoot(container).render(element);
16
- }
17
- function installVisibleErrorHandlers(container) {
18
- const showError = (message) => {
19
- const banner = document.createElement("pre");
20
- banner.style.cssText =
21
- "position: fixed; top: 0; left: 0; right: 0; padding: 12px 16px; " +
22
- "background: #fef2f2; color: #991b1b; font: 13px/1.4 monospace; " +
23
- "white-space: pre-wrap; border-bottom: 1px solid #fecaca; margin: 0; z-index: 99999;";
24
- banner.textContent = message;
25
- container.parentElement?.insertBefore(banner, container);
26
- };
27
- window.addEventListener("error", (e) => {
28
- showError(`Uncaught error: ${e.message}\n${e.error?.stack ?? ""}`);
29
- });
30
- window.addEventListener("unhandledrejection", (e) => {
31
- const reason = e.reason instanceof Error ? `${e.reason.message}\n${e.reason.stack ?? ""}` : String(e.reason);
32
- showError(`Unhandled rejection: ${reason}`);
33
- });
34
- }
@@ -1,74 +0,0 @@
1
- /**
2
- * Mint a record id locally, in the shape the platform mints.
3
- *
4
- * The generator is restated here rather than imported because the canonical one
5
- * lives in a package that is not published, and this one is. The duplication is
6
- * safe by construction rather than by discipline: the server validates the shape on
7
- * every write, so a client that drifted would be rejected loudly at the first call
8
- * instead of quietly persisting a malformed primary key.
9
- *
10
- * Uses `crypto.getRandomValues` — available in every browser this SDK runs in — and
11
- * rejection-samples so each character is uniformly drawn from the alphabet. A plain
12
- * `% 62` over bytes would bias the first 8 characters, which is a poor property for
13
- * something used as a primary key.
14
- */
15
- export declare function newRecordId(): string;
16
- export interface NewRecordApi<P> {
17
- /**
18
- * The id this surface writes under, stable from the first render — before the
19
- * record exists, and unchanged by its creation.
20
- */
21
- id: string;
22
- /**
23
- * Persist a patch. The first call creates the record, every later one updates it.
24
- * Calls are serialised, so firing several before the first resolves is safe.
25
- *
26
- * Rejects with whatever `create`/`update` rejected with; a failed create leaves the
27
- * record uncreated and the next call will try again.
28
- */
29
- save: (patch: P) => Promise<void>;
30
- }
31
- /**
32
- * A record that does not exist yet, named before it does.
33
- *
34
- * The problem this removes: when the server mints the id, a surface editing a new
35
- * record has nothing to identify it by until the first write returns. Everything
36
- * keyed on that id — the route, the drawer, a list selection — therefore changes
37
- * identity mid-edit, which React resolves by remounting the surface the user is
38
- * typing into. Minting the id locally makes it stable from the first render, so the
39
- * create stops being an event the UI has to survive.
40
- *
41
- * Creation still happens on the first write, not on mount: a surface the user opens
42
- * and abandons should leave nothing behind.
43
- *
44
- * The transport is the caller's. Like `useOptimistic`, this hook has no idea how the
45
- * app persists anything — it takes `create` and `update` thunks and owns only the id
46
- * and the ordering, which is the part that is easy to get wrong:
47
- *
48
- * - Two blur-saves fired before the first resolves must not both create. That is a
49
- * duplicate record, and it is the failure this exists to prevent.
50
- * - A save arriving mid-create must wait for it, or it updates a row that is not
51
- * there yet.
52
- * - A create that FAILS must not latch. Otherwise every later save updates a record
53
- * that was never written, and the user's work goes nowhere while looking saved.
54
- *
55
- * ```tsx
56
- * const { id, save } = useNewRecord({
57
- * create: (id, patch) => createCustomer({ record_id: id, ...patch }),
58
- * update: (id, patch) => updateCustomer({ record_id: id, ...patch }),
59
- * onCreated: (id) => select(id), // it exists now — the list re-reads itself
60
- * });
61
- * <InlineText onBlur={(name) => save({ name })} />
62
- * ```
63
- */
64
- export declare function useNewRecord<P>(opts: {
65
- create: (id: string, patch: P) => Promise<unknown>;
66
- update: (id: string, patch: P) => Promise<unknown>;
67
- /**
68
- * Runs once, after the record first exists, with its id. NOT for refetching a
69
- * list — the `create` workflow's own success already re-read it. This is for
70
- * what only the id can drive: routing to the record, selecting it, dropping the
71
- * surface's "new" state.
72
- */
73
- onCreated?: (id: string) => void;
74
- }): NewRecordApi<P>;