@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.
- package/AGENTS.md +32 -47
- package/dist/agent_stream.d.ts +131 -0
- package/dist/ask_ai.d.ts +27 -0
- package/dist/attachments.d.ts +58 -0
- package/dist/chunk-ARV5FAU5.js +1132 -0
- package/dist/comments.d.ts +89 -0
- package/dist/error_report.d.ts +9 -0
- package/dist/folder_pick.d.ts +8 -0
- package/dist/geolocation.d.ts +42 -0
- package/dist/hooks.d.ts +251 -0
- package/dist/{src/index.d.ts → index.d.ts} +13 -22
- package/dist/index.js +31309 -0
- package/dist/index.js.LEGAL.txt +11 -0
- package/dist/members.d.ts +32 -0
- package/dist/mock.d.ts +37 -0
- package/dist/mount.d.ts +19 -0
- package/dist/new_record.d.ts +37 -0
- package/dist/open_app.d.ts +12 -0
- package/dist/open_external.d.ts +10 -0
- package/dist/overlay.d.ts +25 -0
- package/dist/queries.d.ts +231 -0
- package/dist/recording.d.ts +47 -0
- package/dist/recording_state.d.ts +43 -0
- package/dist/rename_file.d.ts +13 -0
- package/dist/router.d.ts +10 -0
- package/dist/router.js +97 -0
- package/dist/row.d.ts +87 -0
- package/dist/rpc.d.ts +114 -0
- package/dist/select.d.ts +24 -0
- package/dist/shared_types.d.ts +8 -0
- package/dist/store.d.ts +43 -0
- package/dist/types.d.ts +36 -0
- package/dist/upload/optimize.d.ts +30 -0
- package/dist/upload/pipeline.d.ts +36 -0
- package/dist/upload/transport.d.ts +19 -0
- package/dist/url_params.d.ts +55 -0
- package/dist/use_recents.d.ts +15 -0
- package/dist/{src/use_url_state.d.ts → use_url_state.d.ts} +0 -2
- package/dist/viewer.d.ts +41 -0
- package/dist/written.d.ts +77 -0
- package/docs/ai.md +74 -133
- package/docs/data_fetching.md +209 -290
- package/docs/files.md +61 -51
- package/docs/members_and_options.md +93 -63
- package/docs/mutations.md +135 -205
- package/docs/navigation_and_state.md +26 -35
- package/docs/queries.md +144 -207
- package/docs/recipes.md +21 -45
- package/docs/runtime.md +74 -137
- package/docs/security.md +8 -11
- package/docs/workflows.md +189 -174
- package/package.json +27 -28
- package/dist/src/agent_stream.d.ts +0 -200
- package/dist/src/agent_stream.js +0 -314
- package/dist/src/ask_ai.d.ts +0 -40
- package/dist/src/ask_ai.js +0 -35
- package/dist/src/attachments.d.ts +0 -68
- package/dist/src/attachments.js +0 -93
- package/dist/src/comments.d.ts +0 -127
- package/dist/src/comments.js +0 -192
- package/dist/src/download.js +0 -54
- package/dist/src/geolocation.d.ts +0 -64
- package/dist/src/geolocation.js +0 -96
- package/dist/src/hooks.d.ts +0 -781
- package/dist/src/hooks.js +0 -860
- package/dist/src/index.js +0 -34
- package/dist/src/members.d.ts +0 -105
- package/dist/src/members.js +0 -62
- package/dist/src/mock.d.ts +0 -118
- package/dist/src/mock.js +0 -124
- package/dist/src/mount.d.ts +0 -47
- package/dist/src/mount.js +0 -34
- package/dist/src/new_record.d.ts +0 -74
- package/dist/src/new_record.js +0 -117
- package/dist/src/open_app.d.ts +0 -15
- package/dist/src/open_app.js +0 -18
- package/dist/src/open_external.d.ts +0 -16
- package/dist/src/open_external.js +0 -19
- package/dist/src/recording.d.ts +0 -59
- package/dist/src/recording.js +0 -30
- package/dist/src/recording_state.d.ts +0 -59
- package/dist/src/recording_state.js +0 -94
- package/dist/src/router.d.ts +0 -17
- package/dist/src/router.js +0 -144
- package/dist/src/row.d.ts +0 -159
- package/dist/src/row.js +0 -254
- package/dist/src/rpc.d.ts +0 -207
- package/dist/src/rpc.js +0 -904
- package/dist/src/select.d.ts +0 -34
- package/dist/src/select.js +0 -40
- package/dist/src/types.d.ts +0 -115
- package/dist/src/types.js +0 -1
- package/dist/src/upload/optimize.d.ts +0 -54
- package/dist/src/upload/optimize.js +0 -207
- package/dist/src/upload/pipeline.d.ts +0 -55
- package/dist/src/upload/pipeline.js +0 -52
- package/dist/src/upload/transport.d.ts +0 -42
- package/dist/src/upload/transport.js +0 -128
- package/dist/src/url_params.d.ts +0 -93
- package/dist/src/url_params.js +0 -215
- package/dist/src/use_optimistic.d.ts +0 -27
- package/dist/src/use_optimistic.js +0 -27
- package/dist/src/use_recents.d.ts +0 -19
- package/dist/src/use_recents.js +0 -71
- package/dist/src/use_url_state.js +0 -73
- package/dist/src/viewer.d.ts +0 -26
- package/dist/src/viewer.js +0 -47
- /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";
|
package/dist/src/members.d.ts
DELETED
|
@@ -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[];
|
package/dist/src/members.js
DELETED
|
@@ -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
|
-
}
|
package/dist/src/mock.d.ts
DELETED
|
@@ -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
|
-
}
|
package/dist/src/mount.d.ts
DELETED
|
@@ -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
|
-
}
|
package/dist/src/new_record.d.ts
DELETED
|
@@ -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>;
|