@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
@@ -0,0 +1,11 @@
1
+ Bundled license information:
2
+
3
+ jexpr/lib/constants.js:
4
+ jexpr/lib/tokenizer.js:
5
+ jexpr/lib/parser.js:
6
+ jexpr/lib/ast_factory.js:
7
+ jexpr/lib/eval.js:
8
+ /*
9
+ * @license
10
+ * Portions Copyright (c) 2013, the Dart project authors.
11
+ */
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A member, in the one shape a `select_member` cell and the `useMembers`
3
+ * roster both return. `email`, `image`, `groups`, `role` and `joined` reach
4
+ * only an authenticated member of the org; a public visitor sees `id`, `name`
5
+ * and `archived`. Absent is not empty: `image: null` is "no photo", a missing
6
+ * `image` is "not told".
7
+ */
8
+ export interface ResolvedMember {
9
+ id: string;
10
+ /** `null` for an id outside the org (a removed member). */
11
+ name: string | null;
12
+ email?: string | null;
13
+ /** Presigned; `null` without a photo. */
14
+ image?: string | null;
15
+ /** Group names — the platform's "department"; `[]` when in none. */
16
+ groups?: string[];
17
+ /**
18
+ * The organization PERMISSION level, not a job title — "who is this person"
19
+ * is `groups`. Raw: translate it yourself.
20
+ */
21
+ role?: "owner" | "admin" | "member";
22
+ /** ISO timestamp of joining the organization. */
23
+ joined?: string;
24
+ /**
25
+ * `true` for a member who has left; never `false`. Feed it to `inactive` on
26
+ * `MemberChip` / `MemberProfileCard`. The roster never returns one, since a
27
+ * departed member cannot be assigned.
28
+ */
29
+ archived?: true;
30
+ }
31
+ /** `[]` for an empty or malformed cell; malformed entries are dropped. */
32
+ export declare function readMembers(value: unknown): ResolvedMember[];
package/dist/mock.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Design-time fixtures, active only with BOTH `mount(<App />, { fixture })`
3
+ * and the `?__mock=1` URL param, so demo data in a bundle never answers real
4
+ * traffic. A mocked workflow never runs, so it writes nothing and nothing is
5
+ * drawn ahead of it; a success re-reads every mounted read.
6
+ * `useFileUpload` is not mocked.
7
+ */
8
+ import type { QueryAggregate } from "./shared_types.js";
9
+ import type { WorkflowResult } from "./hooks.js";
10
+ import type { QuerySortKey } from "./queries.js";
11
+ import type { RecordingState } from "./recording_state.js";
12
+ /** A result, or a function of the inputs — return a slow promise to review the pending state. */
13
+ export type MockWorkflow = WorkflowResult | ((inputs: Record<string, unknown>) => WorkflowResult | Promise<WorkflowResult>);
14
+ export interface MockQueryCall {
15
+ params: Record<string, unknown>;
16
+ filter?: unknown;
17
+ sort?: readonly QuerySortKey[];
18
+ limit?: number;
19
+ /** The groups the call asks for: a fixture answers the query's rows, and they are folded into these after it. */
20
+ aggregate?: QueryAggregate;
21
+ }
22
+ /** Rows, or a function of the call — the only way to honour `filter`/`sort`/`limit`, which the fixture path has no engine for. */
23
+ export type MockQuery = Array<Record<string, unknown>> | ((call: MockQueryCall) => Array<Record<string, unknown>>);
24
+ export interface AppFixture {
25
+ queries?: Record<string, MockQuery>;
26
+ workflows?: Record<string, MockWorkflow>;
27
+ /** `useRecording(alias)` reads as available; `start`/`stop` change nothing. */
28
+ recordings?: Record<string, RecordingState>;
29
+ }
30
+ export declare function registerMockFixture(fixture: AppFixture | undefined): void;
31
+ /** The raw `?__mock=1` flag, fixture or not; false where `window.location` is absent. */
32
+ export declare function hasMockFlag(): boolean;
33
+ /** Null for an unmocked alias, which reads real data. */
34
+ export declare function getMockRows(alias: string, call: MockQueryCall): Array<Record<string, unknown>> | null;
35
+ /** Null for an unmocked alias, which executes for real. */
36
+ export declare function getMockWorkflow(alias: string): MockWorkflow | null;
37
+ export declare function getMockRecordings(): Record<string, RecordingState> | null;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * An app's entry: `mount(<App />)` in `src/main.tsx`. A `fixture` answers
3
+ * reads only under `?__mock=1`:
4
+ *
5
+ * ```tsx
6
+ * mount(<App />, {
7
+ * fixture: {
8
+ * queries: { customers: MOCK_CUSTOMERS, deals: MOCK_DEALS },
9
+ * },
10
+ * });
11
+ * ```
12
+ */
13
+ import type { ReactNode } from "react";
14
+ import { type AppFixture } from "./mock.js";
15
+ export interface MountOptions {
16
+ /** Under `?__mock=1`; an alias it lacks reads real data. */
17
+ fixture?: AppFixture;
18
+ }
19
+ export declare function mount(element: ReactNode, options?: MountOptions): void;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A record id in the platform's shape, restated because the canonical
3
+ * generator is unpublished; the server validates the shape on every write.
4
+ * Rejection-sampled, since `% 62` over bytes would bias the first 8 characters.
5
+ */
6
+ export declare function newRecordId(): string;
7
+ export interface NewRecordApi<P> {
8
+ /** Stable from the first render, before and after the record exists. */
9
+ id: string;
10
+ /**
11
+ * The first call creates, later ones update, serialised in order. Rejects as
12
+ * `create`/`update` did; a failed create leaves the next call to try again.
13
+ */
14
+ save: (patch: P) => Promise<void>;
15
+ }
16
+ /**
17
+ * A new record named before it exists, so a surface keyed on its id never
18
+ * remounts on the first save. Created on the first write, not on mount, so an
19
+ * abandoned surface leaves nothing. The hook owns the id and the ordering: two
20
+ * early saves never both create, a save mid-create waits, and a failed create
21
+ * does not latch.
22
+ *
23
+ * ```tsx
24
+ * const { id, save } = useNewRecord({
25
+ * create: (id, patch) => createCustomer({ record_id: id, ...patch }),
26
+ * update: (id, patch) => updateCustomer({ record_id: id, ...patch }),
27
+ * onCreated: (id) => select(id), // it exists now — the list re-reads itself
28
+ * });
29
+ * <InlineText onBlur={(name) => save({ name })} />
30
+ * ```
31
+ */
32
+ export declare function useNewRecord<P>(opts: {
33
+ create: (id: string, patch: P) => Promise<unknown>;
34
+ update: (id: string, patch: P) => Promise<unknown>;
35
+ /** Once the record exists — for routing or selecting it, not refetching (the create already re-read). */
36
+ onCreated?: (id: string) => void;
37
+ }): NewRecordApi<P>;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Land on `route` — the target app's own path — inside sibling app `appId`,
3
+ * same tab.
4
+ *
5
+ * ```tsx
6
+ * import { openApp } from "@lotics/app-sdk";
7
+ * await openApp(PLANNING_APP_ID, `/plan/${record.id}`);
8
+ * ```
9
+ *
10
+ * Rejects standalone; gate on `isEmbedded()`.
11
+ */
12
+ export declare function openApp(appId: string, route?: string): Promise<void>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Open a URL in a new tab — the sandboxed iframe drops a direct `window.open`.
3
+ * Only `http`/`https`; anything else rejects.
4
+ *
5
+ * ```tsx
6
+ * import { openExternal } from "@lotics/app-sdk";
7
+ * await openExternal(invoiceUrl);
8
+ * ```
9
+ */
10
+ export declare function openExternal(url: string): Promise<void>;
@@ -0,0 +1,25 @@
1
+ import { type QueryAggregate } from "./shared_types.js";
2
+ import type { TableRecordFilters } from "./shared_types.js";
3
+ import { type Row } from "./written.js";
4
+ export interface Overlaid<T = readonly Row[]> {
5
+ rows: T;
6
+ /** A write this app made touches this read, and not all of what it changes is drawn. */
7
+ pending: boolean;
8
+ }
9
+ type SortKeys = readonly {
10
+ field_key: string;
11
+ order: "asc" | "desc";
12
+ blank_position?: "top" | "bottom";
13
+ }[] | undefined;
14
+ /**
15
+ * A read's answer with this app's writes staged over it: a row read's cells, membership and new rows (`adds`:
16
+ * the first page only), or a declared total's counts and sums.
17
+ */
18
+ export declare function overlayRead(alias: string, params: Readonly<Record<string, unknown>>, narrowing: TableRecordFilters | undefined, sort: SortKeys, askedAt: number, rows: readonly Row[], adds: boolean): Overlaid;
19
+ /** A read's rows asked for their groups at run time, with each staged write counted into them. */
20
+ export declare function overlayGroups(alias: string, params: Readonly<Record<string, unknown>>, narrowing: TableRecordFilters | undefined, aggregate: QueryAggregate, askedAt: number, rows: readonly Row[]): Overlaid;
21
+ /** The count of a read's rows with each staged write counted in or out. */
22
+ export declare function overlayCount(alias: string, params: Readonly<Record<string, unknown>>, narrowing: TableRecordFilters | undefined, askedAt: number, total: number): Overlaid<number>;
23
+ /** A count per value of `by`, keyed as a row carries it, with each staged write counted in or out. */
24
+ export declare function overlayCounts(alias: string, params: Readonly<Record<string, unknown>>, narrowing: TableRecordFilters | undefined, by: string, askedAt: number, counts: Readonly<Record<string, number>>): Overlaid<Readonly<Record<string, number>>>;
25
+ export {};
@@ -0,0 +1,231 @@
1
+ import type { QueryAggregate } from "./shared_types.js";
2
+ import type { AppQueries, AppQueryColumns } from "./types.js";
3
+ import type { ResolvedOption } from "./select.js";
4
+ /**
5
+ * One row of a query whose columns codegen could not read (a dynamic alias, a
6
+ * bare `from_table`). Values stay `unknown` — the cell readers narrow them.
7
+ * Only the two addressing columns are typed, because they are handed raw to a
8
+ * `record_id` / `table_id`; every other `__*` column has a reader that owns it.
9
+ */
10
+ export interface QueryRow {
11
+ [column: string]: unknown;
12
+ /** The record this row came from. Absent on a grouped/aggregated row. */
13
+ __source_record_id?: string;
14
+ /** The table that record lives in. Absent on a grouped/aggregated row. */
15
+ __source_table_id?: string;
16
+ }
17
+ /**
18
+ * The row type of alias `K`: its projected columns plus the two addressing
19
+ * columns, and no index signature — a column the query does not carry reads
20
+ * back `undefined` and renders blank, so it must fail `tsc` instead.
21
+ */
22
+ export type RowOf<K extends string> = K extends keyof AppQueryColumns ? {
23
+ [C in AppQueryColumns[K] & string]: unknown;
24
+ } & Pick<QueryRow, "__source_record_id" | "__source_table_id"> : QueryRow;
25
+ /** Applied after the named query, bounded to its projected columns. */
26
+ export interface QuerySortKey<C extends string = string> {
27
+ field_key: C;
28
+ order: "asc" | "desc";
29
+ /** Where blank cells sit; `"bottom"` when omitted. */
30
+ blank_position?: "top" | "bottom";
31
+ }
32
+ export interface QueryFilterFieldCondition<C extends string = string> {
33
+ node_type: "condition";
34
+ field_key: C;
35
+ type?: string;
36
+ operator: string;
37
+ value?: unknown;
38
+ /** A comparison on a figure read in each row's own unit or currency: the option, by key, its value is in. */
39
+ unit_option?: string;
40
+ }
41
+ /** A condition on the row's own id — no `field_key`, since no field holds it. */
42
+ export interface QueryFilterRecordIdCondition {
43
+ node_type: "condition";
44
+ type: "record_id";
45
+ operator: string;
46
+ value?: unknown;
47
+ }
48
+ export type QueryFilterCondition<C extends string = string> = QueryFilterFieldCondition<C> | QueryFilterRecordIdCondition;
49
+ export interface QueryFilterGroup<C extends string = string> {
50
+ node_type: "group";
51
+ logic: "and" | "or";
52
+ children: Array<QueryFilterCondition<C> | QueryFilterGroup<C>>;
53
+ }
54
+ /**
55
+ * Runtime filter applied after the named query, bounded to its projected
56
+ * columns — by `C` at compile time and again on the server. Build a group with
57
+ * `columnFilterToConditions` (`@lotics/ui/column_filter`).
58
+ */
59
+ export type QueryFilter<C extends string = string> = QueryFilterCondition<C> | QueryFilterGroup<C>;
60
+ /** A `filter`/`sort` key on alias `K`: its projected columns, else `string`. */
61
+ export type ColumnKeyOf<K extends string> = K extends keyof AppQueryColumns ? AppQueryColumns[K] & string : string;
62
+ export interface QueryOptions<C extends string = string> {
63
+ /**
64
+ * `false` sends no request and leaves `rows` empty — gate a search until
65
+ * there is a term, since an empty-term search matches every row. Default `true`.
66
+ */
67
+ enabled?: boolean;
68
+ /** `false` skips a re-read on focus and reconnect. Default `true`. */
69
+ revalidateOnFocus?: boolean;
70
+ /** Part of the read's key; empty keeps the query's own order. */
71
+ sort?: QuerySortKey<C>[];
72
+ /** Part of the read's key. */
73
+ filter?: QueryFilter<C>;
74
+ /** The first `limit` rows only — a cap, not pages; the server clamps to its own maximum. */
75
+ limit?: number;
76
+ /** Numbered pages of `page` rows: `page`, `setPage`, `pageCount`, and the total they are counted from. */
77
+ page?: number;
78
+ /**
79
+ * The page in view where the screen keeps it — in its address, so a way back or a reload opens on it:
80
+ * `at` is read, `setPage` calls `onChange`, and a changed read never resets it. Absent, the read keeps its own.
81
+ */
82
+ pageAt?: {
83
+ at: number;
84
+ onChange: (page: number) => void;
85
+ };
86
+ /** A feed read `more` rows at a time: `loadMore` appends the next, and a shifting set never skips a row. */
87
+ more?: number;
88
+ /**
89
+ * The total the rows come to — a count of the whole set, its own read, so the rows never wait for it —
90
+ * or with `by` a count per value of that column from the same scan. On by default with `page`.
91
+ */
92
+ total?: boolean | {
93
+ by: C;
94
+ };
95
+ /** `false` reads no rows: a total alone. Default `true`. */
96
+ rows?: boolean;
97
+ }
98
+ export interface QueryState<R> {
99
+ rows: R[];
100
+ /** The cap (`limit`, else the server's) cut the rows, so anything folded from them is under-reported. */
101
+ truncated: boolean;
102
+ /** The whole set's count, where `total` is asked; `undefined` until it answers. */
103
+ total: number | undefined;
104
+ /** Rows per value of `total.by`, keyed as a row carries it (an option's `opt_…`); a value no row holds is absent. */
105
+ counts: Readonly<Record<string, number>> | undefined;
106
+ /** Only the first read of a key with nothing to show; a re-read and a new key keep the rows on screen. */
107
+ loading: boolean;
108
+ /** Any request of this read is in flight. */
109
+ isValidating: boolean;
110
+ error: string | null;
111
+ /**
112
+ * A write this app made is in flight over these rows. They already show what it will change where that
113
+ * is known; the rest lands with the server's answer.
114
+ */
115
+ pending: boolean;
116
+ refetch: () => void;
117
+ /** The page shown, from 0; always 0 without `page`. */
118
+ page: number;
119
+ /** Clamped at 0. */
120
+ setPage: (page: number) => void;
121
+ /** `undefined` without `page`, and until the total answers. */
122
+ pageCount: number | undefined;
123
+ /** A next page (`page`) or more rows (`more`) are there to read. */
124
+ hasMore: boolean;
125
+ /** Appends the next rows (`more`); nothing otherwise, or while one is being read. */
126
+ loadMore: () => void;
127
+ loadingMore: boolean;
128
+ }
129
+ /**
130
+ * The (params, opts) tail for alias `K`. One conditional signature, never a
131
+ * typed overload beside a loose one, or a rejected filter key would fall
132
+ * through to the loose overload and compile.
133
+ */
134
+ type QueryArgs<K extends string, O> = K extends keyof AppQueries ? AppQueries[K] extends Record<string, never> ? [params?: Record<string, never>, opts?: O] : [params: AppQueries[K], opts?: O] : [params?: Record<string, unknown>, opts?: O];
135
+ /**
136
+ * Read a declared query by alias, filling its `{{params.x}}` holes; the server holds the AST. Its rows, a
137
+ * page of them (`page`), a feed of them (`more`), its total (`total`), or the total alone (`rows: false`).
138
+ *
139
+ * ```tsx
140
+ * const { rows, loading } = useQuery("openOrders", { status: "open" });
141
+ * const orders = useQuery("orders", { q }, { page: 25, sort, filter }); // orders.page, setPage, pageCount
142
+ * const feed = useQuery("feed", {}, { more: 30 }); // feed.loadMore, hasMore
143
+ * const { total, counts } = useQuery("orders", {}, { rows: false, total: { by: "stage" } });
144
+ * ```
145
+ */
146
+ export declare function useQuery<K extends string>(alias: K, ...args: QueryArgs<K, QueryOptions<ColumnKeyOf<K>>>): QueryState<RowOf<K>>;
147
+ /**
148
+ * One read of `useQueries`: a declared query, the params its holes take, a runtime narrowing of its rows, the
149
+ * order they are read in (empty keeps the query's own) — so a cap keeps the first rows of THAT order — its
150
+ * total where asked, and, asked for its groups in place of its rows, the aggregate over what the narrowing keeps.
151
+ */
152
+ export interface QueryCall {
153
+ alias: string;
154
+ params?: Record<string, unknown>;
155
+ filter?: QueryFilter;
156
+ sort?: readonly QuerySortKey[];
157
+ aggregate?: QueryAggregate;
158
+ /** The whole set's count, its own read. */
159
+ total?: boolean;
160
+ /** `false` reads no rows: a total alone. Default `true`. */
161
+ rows?: boolean;
162
+ }
163
+ /**
164
+ * Several declared queries read together, one state per call in the calls' order — for a surface whose
165
+ * reads are data (a spec's blocks, a count per stage) rather than a fixed call per component. Each reads as
166
+ * `useQuery` with no `limit` does: the whole result up to the server's cap, `truncated` past it; a call that
167
+ * fails carries its own `error`, the others their rows.
168
+ */
169
+ export declare function useQueries(calls: readonly QueryCall[], opts?: Pick<QueryOptions, "enabled" | "revalidateOnFocus">): QueryState<QueryRow>[];
170
+ /**
171
+ * Every row a declared query matches, read a page at a time to the end — for what must hold every row
172
+ * (a saved sheet), never for a screen, which pages. The server's rows as stored: no write is drawn over them.
173
+ */
174
+ export declare function queryAll(alias: string, params?: Record<string, unknown>, opts?: {
175
+ filter?: QueryFilter;
176
+ sort?: readonly QuerySortKey[];
177
+ }): Promise<QueryRow[]>;
178
+ /** One select column's full option set. */
179
+ export interface FieldOptions {
180
+ /** The source field's display name. */
181
+ label: string;
182
+ /** Every option, including ones no current row holds. */
183
+ options: ResolvedOption[];
184
+ /** `undefined` for an option removed after the cell was written. */
185
+ byKey: (key: string) => ResolvedOption | undefined;
186
+ }
187
+ /** The units or currencies a number column is read in, row by row: the options of the select on its row naming each row's. */
188
+ export interface FigureUnits {
189
+ vocabulary: "unit" | "currency";
190
+ options: ResolvedOption[];
191
+ }
192
+ export interface FieldOptionsState<C extends string = string> {
193
+ /**
194
+ * Keyed by the alias's own output columns. Every key is optional: a non-select
195
+ * column, a UNION whose arms disagree, or a computed column has none.
196
+ */
197
+ fields: Partial<Record<C, FieldOptions>>;
198
+ /**
199
+ * A number column read in each row's own unit or currency, keyed the same way: a
200
+ * condition comparing it names one of these as its `unit_option` — pass it whole as
201
+ * a `FilterChip` column's `units`. A figure read in one unit for every row has none.
202
+ */
203
+ units: Partial<Record<C, FigureUnits>>;
204
+ loading: boolean;
205
+ isValidating: boolean;
206
+ error: string | null;
207
+ refetch: () => void;
208
+ }
209
+ export interface FieldOptionsOptions {
210
+ /** Default `true`. */
211
+ enabled?: boolean;
212
+ }
213
+ /**
214
+ * The complete option set, with colors, of each `select` column the alias
215
+ * projects — a cell carries only the options its record holds — and the units
216
+ * of each number column read in each row's own unit.
217
+ *
218
+ * ```tsx
219
+ * const { fields, units } = useFieldOptions("records");
220
+ * // populate + color a picker:
221
+ * <Select variant="native" options={fields.status?.options ?? []}
222
+ * renderOptionContent={(o) => <Status option={o} />} />
223
+ * // color a stored value:
224
+ * <Status option={fields.status?.byKey(readSelect(r.status)[0]?.key ?? "")} />
225
+ * // a range on a figure each row reads in its own unit:
226
+ * <FilterChip column={{ key: "weight", label: "Weight", type: "number", units: units.weight }} … />
227
+ * ```
228
+ */
229
+ export declare function useFieldOptions<K extends keyof AppQueries & string>(alias: K, opts?: FieldOptionsOptions): FieldOptionsState<ColumnKeyOf<K>>;
230
+ export declare function useFieldOptions(alias: string, opts?: FieldOptionsOptions): FieldOptionsState;
231
+ export {};
@@ -0,0 +1,47 @@
1
+ import { type RecordingState } from "./recording_state.js";
2
+ import type { AppWorkflows } from "./types.js";
3
+ /** The platform fills `recording`; the app passes the rest. */
4
+ type ActInputsOf<K extends keyof AppWorkflows & string> = AppWorkflows[K] extends Record<string, unknown> ? Omit<AppWorkflows[K], "recording"> : Record<string, unknown>;
5
+ export interface UseRecording<I> {
6
+ /**
7
+ * False standalone, in an older host, without transcription, and until the
8
+ * context answers. Draw a start control only when true.
9
+ */
10
+ available: boolean;
11
+ /** The latest recording through this alias. */
12
+ state: RecordingState;
13
+ /** Any of this app's recordings is live; the host records one at a time. */
14
+ busy: boolean;
15
+ /**
16
+ * Opens the host's capture dialog. `{ started: false }` when the member
17
+ * closes it; rejects with the host's reason. Once transcribed, the host runs
18
+ * the workflow with these inputs plus `recording`.
19
+ */
20
+ start: {} extends I ? (inputs?: I) => Promise<{
21
+ started: boolean;
22
+ }> : (inputs: I) => Promise<{
23
+ started: boolean;
24
+ }>;
25
+ /** Resolves whether or not one was live. */
26
+ stop: () => Promise<void>;
27
+ }
28
+ /**
29
+ * Record through the host and file it by `alias`, a workflow declaring the
30
+ * `recording` input, run once when the transcript is ready.
31
+ *
32
+ * ```tsx
33
+ * const visit = useRecording("log_visit");
34
+ * if (!visit.available) return null;
35
+ * if (visit.state.phase === "live" && visit.state.inputs.site === siteId) {
36
+ * return <Button onPress={() => visit.stop()}>Stop</Button>;
37
+ * }
38
+ * return (
39
+ * <Button disabled={visit.busy} onPress={() => visit.start({ site: siteId })}>
40
+ * Record the visit
41
+ * </Button>
42
+ * );
43
+ * ```
44
+ */
45
+ export declare function useRecording<K extends keyof AppWorkflows & string>(alias: K): UseRecording<ActInputsOf<K>>;
46
+ export declare function useRecording(alias: string): UseRecording<Record<string, unknown>>;
47
+ export {};
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The latest recording per workflow alias this app started. A leaf module:
3
+ * `rpc.ts` applies pushes, `viewer.ts` the context snapshot, both in order on
4
+ * one channel, so the later arrival is newer.
5
+ */
6
+ /** The `start` inputs echoed back, so a per-row act can tell which row is recording. */
7
+ export type RecordingInputs = Record<string, unknown>;
8
+ /**
9
+ * `interrupted`: the tab closed mid-call, resumable from the recording bar.
10
+ * `processing`: transcribing. `failed`: transcription, filing or the run
11
+ * failed; kept, and retried from the recording bar.
12
+ */
13
+ export type RecordingState = {
14
+ phase: "idle";
15
+ } | {
16
+ phase: "live";
17
+ elapsed_seconds: number;
18
+ inputs: RecordingInputs;
19
+ } | {
20
+ phase: "interrupted";
21
+ elapsed_seconds: number;
22
+ inputs: RecordingInputs;
23
+ } | {
24
+ phase: "processing";
25
+ inputs: RecordingInputs;
26
+ } | {
27
+ phase: "filed";
28
+ execution_id: string;
29
+ inputs: RecordingInputs;
30
+ } | {
31
+ phase: "failed";
32
+ message: string;
33
+ inputs: RecordingInputs;
34
+ };
35
+ export declare const IDLE_RECORDING: RecordingState;
36
+ export declare function subscribeRecordings(listener: () => void): () => void;
37
+ export declare function recordingStateOf(alias: string): RecordingState;
38
+ export declare function anyRecordingLive(): boolean;
39
+ export declare function applyRecordingChange(alias: string, state: RecordingState): void;
40
+ /** The snapshot `context` carries: every alias it omits is idle. */
41
+ export declare function replaceRecordings(snapshot: Record<string, RecordingState>): void;
42
+ export declare function readRecordingState(raw: unknown): RecordingState | null;
43
+ export declare function readRecordingSnapshot(raw: unknown): Record<string, RecordingState>;
@@ -0,0 +1,13 @@
1
+ import type { AppFile } from "./row.js";
2
+ /**
3
+ * A file under another name: a NEW file over the same bytes, its extension kept exactly (`"scan.pdf"` →
4
+ * `"Invoice 9.pdf"`, never `.html`). Nothing holds it until a save puts it in the old one's place — a files field's
5
+ * change taking the old id out and putting this one in, which lands it where the old one stood — so the rename is
6
+ * refused wherever that save is, and reaches no other field holding the old file.
7
+ *
8
+ * ```tsx
9
+ * const renamed = await renameFile(file.id, "Invoice 9.pdf");
10
+ * await save({ record_id, papers_added: [renamed.id], papers_removed: [file.id] });
11
+ * ```
12
+ */
13
+ export declare function renameFile(file_id: string, filename: string): Promise<AppFile>;
@@ -0,0 +1,10 @@
1
+ import { type RouteObject } from "react-router";
2
+ /** The SDK ships no locale, so a non-English app passes its own. */
3
+ export interface NotFoundWords {
4
+ message: (path: string) => string;
5
+ firstScreen: string;
6
+ }
7
+ export declare function AppRouter({ routes, notFound }: {
8
+ routes: RouteObject[];
9
+ notFound?: NotFoundWords;
10
+ }): import("react").JSX.Element;
package/dist/router.js ADDED
@@ -0,0 +1,97 @@
1
+ import {
2
+ isEmbedded,
3
+ setUrlParams
4
+ } from "./chunk-ARV5FAU5.js";
5
+
6
+ // src/router.tsx
7
+ import { useEffect } from "react";
8
+ import {
9
+ BrowserRouter,
10
+ Link,
11
+ useLocation,
12
+ useRoutes
13
+ } from "react-router";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ var LOC_KEY = "_loc";
16
+ var HOST_KEY = "lotics_host";
17
+ function screenHref(loc) {
18
+ const search = new URLSearchParams(loc.search);
19
+ search.delete(HOST_KEY);
20
+ const qs = search.toString();
21
+ return loc.pathname + (qs ? `?${qs}` : "") + loc.hash;
22
+ }
23
+ function HostScreenMirror() {
24
+ const location = useLocation();
25
+ const href = screenHref(location);
26
+ useEffect(() => {
27
+ void setUrlParams({ [LOC_KEY]: href });
28
+ }, [href]);
29
+ return null;
30
+ }
31
+ function RoutedRoutes({ routes }) {
32
+ return useRoutes(routes);
33
+ }
34
+ var NOT_FOUND_ROOT = {
35
+ display: "flex",
36
+ flexDirection: "column",
37
+ alignItems: "center",
38
+ justifyContent: "center",
39
+ gap: "var(--lotics-space-8, 8px)",
40
+ paddingBlock: "var(--lotics-space-48, 48px)",
41
+ paddingInline: "var(--lotics-space-16, 16px)",
42
+ textAlign: "center",
43
+ fontFamily: "var(--font-sans, system-ui, sans-serif)"
44
+ };
45
+ var NOT_FOUND_MESSAGE = {
46
+ margin: 0,
47
+ fontSize: "var(--lotics-text-sm, 14px)",
48
+ color: "var(--lotics-ink-muted, #71717a)"
49
+ };
50
+ var NOT_FOUND_LINK = {
51
+ fontSize: "var(--lotics-text-sm, 14px)",
52
+ color: "var(--lotics-accent, #2563eb)"
53
+ };
54
+ var NOT_FOUND_ENGLISH = {
55
+ message: (path) => `No screen at ${path}`,
56
+ firstScreen: "Go to the first screen"
57
+ };
58
+ function NotFoundScreen({ home, words }) {
59
+ const { pathname } = useLocation();
60
+ return /* @__PURE__ */ jsxs("div", { style: NOT_FOUND_ROOT, children: [
61
+ /* @__PURE__ */ jsx("p", { style: NOT_FOUND_MESSAGE, children: words.message(pathname) }),
62
+ /* @__PURE__ */ jsx(Link, { to: home, style: NOT_FOUND_LINK, children: words.firstScreen })
63
+ ] });
64
+ }
65
+ function withNotFound(routes, element) {
66
+ return [
67
+ ...routes.map(
68
+ (route) => route.children === void 0 ? route : { ...route, children: withNotFound(route.children, element) }
69
+ ),
70
+ { path: "*", element }
71
+ ];
72
+ }
73
+ function firstScreenPath(routes) {
74
+ const first = routes.find((route) => route.path !== void 0 && route.path !== "*");
75
+ return first?.path ?? "/";
76
+ }
77
+ function AppRouter({
78
+ routes,
79
+ notFound = NOT_FOUND_ENGLISH
80
+ }) {
81
+ const embedded = isEmbedded();
82
+ return /* @__PURE__ */ jsxs(BrowserRouter, { children: [
83
+ embedded ? /* @__PURE__ */ jsx(HostScreenMirror, {}) : null,
84
+ /* @__PURE__ */ jsx(
85
+ RoutedRoutes,
86
+ {
87
+ routes: withNotFound(
88
+ routes,
89
+ /* @__PURE__ */ jsx(NotFoundScreen, { home: firstScreenPath(routes), words: notFound })
90
+ )
91
+ }
92
+ )
93
+ ] });
94
+ }
95
+ export {
96
+ AppRouter
97
+ };