@lotics/app-sdk 0.89.0 → 0.90.1
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 +1 -1
- package/dist/src/hooks.d.ts +5 -5
- package/dist/src/hooks.js +1 -1
- package/dist/src/index.d.ts +9 -9
- package/dist/src/index.js +9 -9
- package/dist/src/open_app.d.ts +15 -0
- package/dist/src/open_app.js +18 -0
- package/dist/src/rpc.d.ts +1 -1
- package/dist/src/rpc.js +4 -0
- package/dist/src/select.d.ts +1 -1
- package/docs/data_fetching.md +6 -6
- package/docs/files.md +9 -10
- package/docs/members_and_options.md +12 -12
- package/docs/navigation_and_state.md +1 -0
- package/docs/runtime.md +40 -9
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -24,7 +24,7 @@ signature; open the file.**
|
|
|
24
24
|
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`. |
|
|
25
25
|
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming ai-sdk `parts` → `AgentRun`, the agent's ask-back — `pendingChoice`/`answerChoice` over the parked `awaiting_input` state — and the `AgentRunLanding` every leg resolves), `askAi` — plus the fields-vs-file razor for choosing between them — and `useAiContext` (push the current screen's view state to the member's ambient chat agent; caps, push-only semantics; a chat mutation refetches your queries through the realtime channel, not a separate poke). **A `file` input carries its own content** — no reader tool to declare. **An agent reaches record DATA only through its declared `query_aliases` / `workflow_aliases`.** Also **what the member's own chat agent can do with your app while it is open** — the alias catalog it reads and how to shape a mutating alias for it. |
|
|
26
26
|
| [docs/security.md](./docs/security.md) | **Read before shipping** — the owner-principal model, `is_current_member` scoping, write attribution, group gates, public-app bounds, what runtime refinement cannot widen, and why a per-input bound is a tenancy floor rather than an authorization check (a caller-supplied id must be intersected with the record server-side). |
|
|
27
|
-
| [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, `rpc()`, the design-time mock harness (`fixture` + `?__mock=1` — queries AND workflows, so an AI screen's in-flight/done/error states are reviewable without running or paying for anything), `openExternal`/`downloadFile`, geofencing, and the publish chain for SDK contributors. |
|
|
27
|
+
| [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, `rpc()`, the design-time mock harness (`fixture` + `?__mock=1` — queries AND workflows, so an AI screen's in-flight/done/error states are reviewable without running or paying for anything), `openExternal`/`openApp`/`downloadFile`, geofencing, and the publish chain for SDK contributors. |
|
|
28
28
|
|
|
29
29
|
## Non-negotiables (each detailed in its doc)
|
|
30
30
|
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -383,10 +383,10 @@ export interface FieldOptionsOptions {
|
|
|
383
383
|
* ```tsx
|
|
384
384
|
* const { fields } = useFieldOptions("records");
|
|
385
385
|
* // populate + color a picker:
|
|
386
|
-
* <
|
|
387
|
-
* renderOptionContent={(o) => <
|
|
386
|
+
* <Select variant="native" options={fields.status?.options ?? []}
|
|
387
|
+
* renderOptionContent={(o) => <Status option={o} />} />
|
|
388
388
|
* // color a stored value:
|
|
389
|
-
* <
|
|
389
|
+
* <Status option={fields.status?.byKey(readSelect(r.status)[0]?.key ?? "")} />
|
|
390
390
|
* ```
|
|
391
391
|
*/
|
|
392
392
|
export declare function useFieldOptions<K extends keyof AppQueries & string>(alias: K, opts?: FieldOptionsOptions): FieldOptionsState;
|
|
@@ -448,7 +448,7 @@ export declare function usePaginatedQuery<K extends string>(alias: K, ...args: Q
|
|
|
448
448
|
*
|
|
449
449
|
* ```tsx
|
|
450
450
|
* const { total } = useCount("orders", { q }, { filter: unpaidFilter });
|
|
451
|
-
* return <
|
|
451
|
+
* return <Status label={total == null ? "…" : `${total}`} />;
|
|
452
452
|
* ```
|
|
453
453
|
*/
|
|
454
454
|
export declare function useCount<K extends string>(alias: K, ...args: QueryArgs<K, CountOptions<ColumnKeyOf<K>>>): CountState;
|
|
@@ -619,7 +619,7 @@ export interface MembersOptions {
|
|
|
619
619
|
*
|
|
620
620
|
* ```tsx
|
|
621
621
|
* const { members } = useMembers({ group: "grp_..." });
|
|
622
|
-
* // <
|
|
622
|
+
* // <Select variant="native" options={members.map((m) => ({
|
|
623
623
|
* // value: m.id, label: m.name || m.email || m.id, image: m.image,
|
|
624
624
|
* // }))} />
|
|
625
625
|
* ```
|
package/dist/src/hooks.js
CHANGED
|
@@ -536,7 +536,7 @@ export function useAiContext(slot, context) {
|
|
|
536
536
|
*
|
|
537
537
|
* ```tsx
|
|
538
538
|
* const { members } = useMembers({ group: "grp_..." });
|
|
539
|
-
* // <
|
|
539
|
+
* // <Select variant="native" options={members.map((m) => ({
|
|
540
540
|
* // value: m.id, label: m.name || m.email || m.id, image: m.image,
|
|
541
541
|
* // }))} />
|
|
542
542
|
* ```
|
package/dist/src/index.d.ts
CHANGED
|
@@ -4,15 +4,14 @@
|
|
|
4
4
|
* "@lotics/app-sdk"` and ship the resulting bundle via `lotics app deploy`.
|
|
5
5
|
*
|
|
6
6
|
* This SDK is data + RPC only — it deliberately does NOT re-export any
|
|
7
|
-
* `@lotics/ui`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* not raw HTML/CSS. See `docs/apps.md` → "Styling & components".
|
|
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".
|
|
16
15
|
*/
|
|
17
16
|
export { mount } from "./mount.js";
|
|
18
17
|
export type { MountOptions } from "./mount.js";
|
|
@@ -26,6 +25,7 @@ export type { GeofenceZone, GeoCoords, GeofenceOutcome, GeofenceOptions } from "
|
|
|
26
25
|
export { rpc, isEmbedded } from "./rpc.js";
|
|
27
26
|
export type { RpcOp, AiContextValue, AiContextRecordRef } from "./rpc.js";
|
|
28
27
|
export { openExternal } from "./open_external.js";
|
|
28
|
+
export { openApp } from "./open_app.js";
|
|
29
29
|
export { askAi, type AskAiArgs } from "./ask_ai.js";
|
|
30
30
|
export { downloadFile } from "./download.js";
|
|
31
31
|
export { readMembers } from "./members.js";
|
package/dist/src/index.js
CHANGED
|
@@ -4,15 +4,14 @@
|
|
|
4
4
|
* "@lotics/app-sdk"` and ship the resulting bundle via `lotics app deploy`.
|
|
5
5
|
*
|
|
6
6
|
* This SDK is data + RPC only — it deliberately does NOT re-export any
|
|
7
|
-
* `@lotics/ui`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* not raw HTML/CSS. See `docs/apps.md` → "Styling & components".
|
|
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".
|
|
16
15
|
*/
|
|
17
16
|
export { mount } from "./mount.js";
|
|
18
17
|
export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useCount, useFieldOptions, useFileUpload, useAttachments, useMembers, useAgentRun, useAgentRuns, useAiContext, buildChoiceOutput, } from "./hooks.js";
|
|
@@ -21,6 +20,7 @@ export { useViewer } from "./viewer.js";
|
|
|
21
20
|
export { requestGeofencedLocation, isWithinZone } from "./geolocation.js";
|
|
22
21
|
export { rpc, isEmbedded } from "./rpc.js";
|
|
23
22
|
export { openExternal } from "./open_external.js";
|
|
23
|
+
export { openApp } from "./open_app.js";
|
|
24
24
|
export { askAi } from "./ask_ai.js";
|
|
25
25
|
export { downloadFile } from "./download.js";
|
|
26
26
|
export { readMembers } from "./members.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open a record's screen in the sibling app that owns it — the cross-app hop.
|
|
3
|
+
* The host owns app routing and lands the viewer on `route` inside `appId`,
|
|
4
|
+
* same tab, chrome kept. `route` is the target app's own in-app path, exactly
|
|
5
|
+
* as its router declares it (`/` for its register).
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { openApp } from "@lotics/app-sdk";
|
|
9
|
+
* await openApp(PLANNING_APP_ID, `/plan/${record.id}`);
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Standalone (`<slug>.lotics.app`) has no sibling apps, so the call rejects
|
|
13
|
+
* there — gate the control on `isEmbedded()`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function openApp(appId: string, route?: string): Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { rpc } from "./rpc.js";
|
|
2
|
+
/**
|
|
3
|
+
* Open a record's screen in the sibling app that owns it — the cross-app hop.
|
|
4
|
+
* The host owns app routing and lands the viewer on `route` inside `appId`,
|
|
5
|
+
* same tab, chrome kept. `route` is the target app's own in-app path, exactly
|
|
6
|
+
* as its router declares it (`/` for its register).
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { openApp } from "@lotics/app-sdk";
|
|
10
|
+
* await openApp(PLANNING_APP_ID, `/plan/${record.id}`);
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Standalone (`<slug>.lotics.app`) has no sibling apps, so the call rejects
|
|
14
|
+
* there — gate the control on `isEmbedded()`.
|
|
15
|
+
*/
|
|
16
|
+
export function openApp(appId, route = "/") {
|
|
17
|
+
return rpc("openApp", { app_id: appId, route });
|
|
18
|
+
}
|
package/dist/src/rpc.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { type UrlParams, type UrlParamsPatch } from "./url_params.js";
|
|
|
20
20
|
* app → host: { id, op, payload }
|
|
21
21
|
* host → app: { id, type: "result", data } | { id, type: "error", message }
|
|
22
22
|
*/
|
|
23
|
-
export type RpcOp = "query" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "members" | "context" | "openExternal" | "askAi" | "urlState.get" | "urlState.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
|
|
23
|
+
export type RpcOp = "query" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "members" | "context" | "openExternal" | "openApp" | "askAi" | "urlState.get" | "urlState.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
|
|
24
24
|
/** Payload for starting a streaming agent run. */
|
|
25
25
|
export interface AgentRunPayload {
|
|
26
26
|
alias: string;
|
package/dist/src/rpc.js
CHANGED
|
@@ -666,6 +666,10 @@ function rpcStandalone(op, payload) {
|
|
|
666
666
|
return standaloneContext();
|
|
667
667
|
case "openExternal":
|
|
668
668
|
return standaloneOpenExternal(payload);
|
|
669
|
+
case "openApp":
|
|
670
|
+
// A standalone app is one page at its own address: no host routing
|
|
671
|
+
// between apps, no sibling to reach.
|
|
672
|
+
return Promise.reject(new Error("openApp needs the Lotics host: a standalone app has no sibling apps to open. Gate the control on isEmbedded()."));
|
|
669
673
|
case "askAi":
|
|
670
674
|
// The chat surface lives in the Lotics host — a standalone page has
|
|
671
675
|
// nowhere to hand off to.
|
package/dist/src/select.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface ResolvedOption {
|
|
|
18
18
|
* Named palette color token (e.g. `"blue"`, `"emerald"`). Populated by
|
|
19
19
|
* `useFieldOptions` (which reads the field config); absent on options read
|
|
20
20
|
* back from a query CELL via `readSelect` — a cell carries only key + label.
|
|
21
|
-
* Pass the resolved option straight to `@lotics/ui`'s `
|
|
21
|
+
* Pass the resolved option straight to `@lotics/ui`'s `Status`, which
|
|
22
22
|
* degrades a missing/unknown token to a neutral badge.
|
|
23
23
|
*/
|
|
24
24
|
color?: string;
|
package/docs/data_fetching.md
CHANGED
|
@@ -439,7 +439,7 @@ a partial as its period start.
|
|
|
439
439
|
|
|
440
440
|
**`readSelect`.** A query **cell** carries `key` + `label` only — `color` comes from
|
|
441
441
|
`useFieldOptions`, not the cell. An option deleted after the cell was written surfaces as
|
|
442
|
-
`label === key` (the stale state is explicit, never hidden). Render with `@lotics/ui` `
|
|
442
|
+
`label === key` (the stale state is explicit, never hidden). Render with `@lotics/ui` `Status`
|
|
443
443
|
— see [./members_and_options.md](./members_and_options.md).
|
|
444
444
|
|
|
445
445
|
**`readMembers`.** `name` is `null` when the id doesn't resolve in the app's org (e.g. a removed
|
|
@@ -477,7 +477,7 @@ A query cell carries only the options a record actually holds (key + label, no c
|
|
|
477
477
|
**complete** option list of a select column — every option including those in no current row, with
|
|
478
478
|
colors — use **`useFieldOptions`**, and prefer it over deriving options from loaded rows
|
|
479
479
|
(row-derived sets are incomplete until every page loads and carry no colors). Its full contract
|
|
480
|
-
(return shape, `byKey`, `opts.enabled`, freshness), rendering the values (`
|
|
480
|
+
(return shape, `byKey`, `opts.enabled`, freshness), rendering the values (`Status`,
|
|
481
481
|
`MemberChip`, `MemberSelect`), and the member roster (`useMembers`) live in
|
|
482
482
|
[./members_and_options.md](./members_and_options.md).
|
|
483
483
|
|
|
@@ -510,7 +510,7 @@ pieces. Compose these — don't hand-roll search:
|
|
|
510
510
|
|
|
511
511
|
- **`Combobox`** (`@lotics/ui/combobox`) owns the interaction — debounced `onSearchChange`, a
|
|
512
512
|
popover listbox with rich rows (`renderOptionContent`), keyboard navigation, `recentOptions`,
|
|
513
|
-
`allowCustom`. (For a known small list with no search box, `
|
|
513
|
+
`allowCustom`. (For a known small list with no search box, `Select`.)
|
|
514
514
|
- **A parameterized `search` query** — a `from_table` with `search: "{{params.q}}"` over the
|
|
515
515
|
maintained search document: **diacritics- and case-insensitive**, trigram-indexed, and AND-ed
|
|
516
516
|
with the template's `filter` (search within a scope). The full `search` contract is in
|
|
@@ -540,7 +540,7 @@ const [typed, setTyped] = useState(""); // the input's own value, every
|
|
|
540
540
|
const [term, setTerm] = useState(""); // what the server is asked, once typing settles
|
|
541
541
|
const commit = useDebouncedCallback(setTerm, 250);
|
|
542
542
|
|
|
543
|
-
<
|
|
543
|
+
<TextInput type="search" value={typed} onChangeText={(v) => { setTyped(v); commit(v.trim()); }} />;
|
|
544
544
|
|
|
545
545
|
const { rows, loading } = useQuery(
|
|
546
546
|
"searchCustomers",
|
|
@@ -560,8 +560,8 @@ relationship exists.
|
|
|
560
560
|
When the user doesn't know the term — "show me everything, let me narrow it" — build a modal table
|
|
561
561
|
they can browse (numbered pages), search, sort, and filter:
|
|
562
562
|
|
|
563
|
-
- **The screen is app-owned**, composed from `@lotics/ui`: a `Dialog` over `
|
|
564
|
-
pills (`
|
|
563
|
+
- **The screen is app-owned**, composed from `@lotics/ui`: a `Dialog` over a search `TextInput` + filter
|
|
564
|
+
pills (`FilterChip column=`) + `Table` + `Pagination`. It needs both `@lotics/ui` and the SDK (which is
|
|
565
565
|
UI-free), so it lives in the app (e.g. a `record_picker.tsx`) — reuse it for any table by passing
|
|
566
566
|
a different `alias` + column config.
|
|
567
567
|
- **`usePaginatedQuery`** drives it: the page of rows, the `total` for "Page 1 of N" (the built-in
|
package/docs/files.md
CHANGED
|
@@ -19,7 +19,7 @@ discipline in [data fetching](./data_fetching.md).
|
|
|
19
19
|
| Attach to a record | a declared workflow with a `{ type: "file" }` input | the workflow writes the id(s) into a `files` field — the **only** write path |
|
|
20
20
|
| Read back from records | `useQuery` + `readFiles(cell)` | `AppFile[]` — presigned `url`/`thumbnail_url` (24 h) + `size`/`created_at` |
|
|
21
21
|
| Receive a generated document | `useWorkflow` → `WorkflowResult.files` | presigned files auto-extracted from the run |
|
|
22
|
-
| Show it | `@lotics/ui` `FileThumbnail` / `
|
|
22
|
+
| Show it | `@lotics/ui` `FileThumbnail` / `FileThumbnailGrid` / `FileGalleryDialog` | map to `DisplayFile` (see below) |
|
|
23
23
|
| Save browser-built bytes | `downloadFile(filename, data, mimeType?)` | a client-side download (see [runtime](./runtime.md)) |
|
|
24
24
|
|
|
25
25
|
An uploaded file is **inert until a workflow attaches it** — it has an id and serving URLs, but
|
|
@@ -175,10 +175,10 @@ Each `AttachedFile` is `{ id, filename, mime_type, preview_url, status, file_id?
|
|
|
175
175
|
|
|
176
176
|
Wiring to `@lotics/ui`: in a `Composer`, trigger picking from `actionsButton` (via `pickFiles`),
|
|
177
177
|
render the attachment pills with `FileThumbnail` as above, and gate `sendDisabled` on `uploading`.
|
|
178
|
-
For a full add-files *screen* (not a composer pill), map each `AttachedFile` to a
|
|
179
|
-
`FileUpload` entry — ready → `{ status: "complete", id: file_id, file:
|
|
180
|
-
`{ status, id, filename, mimeType: mime_type, previewUrl: preview_url }`
|
|
181
|
-
the uploading/error/retry tiles itself.
|
|
178
|
+
For a full add-files *screen* (not a composer pill), map each `AttachedFile` to a
|
|
179
|
+
`FileThumbnailGrid` `FileUpload` entry — ready → `{ status: "complete", id: file_id, file:
|
|
180
|
+
<DisplayFile> }`, else `{ status, id, filename, mimeType: mime_type, previewUrl: preview_url }`
|
|
181
|
+
— and the grid renders the uploading/error/retry tiles itself.
|
|
182
182
|
|
|
183
183
|
## File cells in query results — `readFiles` and `AppFile`
|
|
184
184
|
|
|
@@ -325,11 +325,10 @@ not viewing). Component contracts live in the `@lotics/ui` reference
|
|
|
325
325
|
For an `AttachedFile` still uploading, map `preview_url` → `url` (there is no server URL yet).
|
|
326
326
|
The app owns this data→UI adapter — the SDK deliberately never imports `@lotics/ui`.
|
|
327
327
|
|
|
328
|
-
- **`FileThumbnail`** — one square tile; `uploading`
|
|
329
|
-
`
|
|
330
|
-
- **`
|
|
331
|
-
|
|
332
|
-
- **`FileGalleryModal`** — the full-screen viewer (filename · counter · actions · close, with
|
|
328
|
+
- **`FileThumbnail`** — one square tile; `uploading` takes the queue status and overlays the
|
|
329
|
+
scrim, the spinner or the retry, images fall back from `previewUrl` to `thumbnailUrl` to `url`.
|
|
330
|
+
- **`FileThumbnailGrid`** — a grid of stored files plus a live `uploads` queue, in one grid.
|
|
331
|
+
- **`FileGalleryDialog`** — the full-screen viewer (filename · counter · actions · close, with
|
|
333
332
|
prev/next + ESC), delegating per-file to **`FilePreview`**, which dispatches by MIME. Wire
|
|
334
333
|
`onFilePress` → a `number | null` `activeIndex`.
|
|
335
334
|
- PDF renders **inline to a canvas** (a nested PDF browsing context is blocked in the sandboxed
|
|
@@ -4,7 +4,7 @@ How an app renders and picks **people** and **select-field options**, plus the *
|
|
|
4
4
|
surface. Covers the two cell readers (`readSelect`, `readMembers`), the two catalog hooks
|
|
5
5
|
(`useFieldOptions`, `useMembers`), the viewer identity hook (`useViewer`), comments
|
|
6
6
|
(`useComments`, `useCommentCounts`), and the `@lotics/ui` components they feed. Read this before
|
|
7
|
-
building an assign picker, a colored
|
|
7
|
+
building an assign picker, a colored `Status` mark, a per-viewer ("my records") screen, or a
|
|
8
8
|
comment thread. Query mechanics live in [queries](./queries.md); the authority model in
|
|
9
9
|
[security](./security.md).
|
|
10
10
|
|
|
@@ -61,7 +61,7 @@ const { fields } = useFieldOptions("orders"); // same alias you query
|
|
|
61
61
|
// are { value, label }, so map the option key to `value`):
|
|
62
62
|
<Select
|
|
63
63
|
options={(fields.status?.options ?? []).map((o) => ({ value: o.key, label: o.label }))}
|
|
64
|
-
renderOptionContent={(o) => <
|
|
64
|
+
renderOptionContent={(o) => <Status option={fields.status?.byKey(o.value)} />}
|
|
65
65
|
value={status} onValueChange={setStatus}
|
|
66
66
|
/>
|
|
67
67
|
```
|
|
@@ -78,7 +78,7 @@ name**, not the field key. Each `FieldOptions`:
|
|
|
78
78
|
| `byKey(key)` | Resolve one option by key; `undefined` for an unknown key (option removed after the cell was written) |
|
|
79
79
|
|
|
80
80
|
- `color` is a named palette token (e.g. `"blue"`, `"emerald"`). Pass the option straight to
|
|
81
|
-
`@lotics/ui`'s `
|
|
81
|
+
`@lotics/ui`'s `Status`; a missing/unrecognized token degrades to a neutral badge.
|
|
82
82
|
- **A column the server can't map to a single source select field is simply absent** from
|
|
83
83
|
`fields` — a UNION output whose arms disagree on the source field, or a computed column. Read
|
|
84
84
|
defensively: `fields.status?.options ?? []`.
|
|
@@ -99,7 +99,7 @@ until an edit drawer opens). State: `{ fields, loading, isValidating, error, ref
|
|
|
99
99
|
|
|
100
100
|
```tsx
|
|
101
101
|
const opt = readSelect(row.status)[0];
|
|
102
|
-
<
|
|
102
|
+
<Status option={opt ? (fields.status?.byKey(opt.key) ?? opt) : null} />
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
`byKey` hit → the configured color. `byKey` miss (option removed post-write) → fall back to the
|
|
@@ -128,12 +128,12 @@ projected `select_member` cell to `Array<{ id, name, email?, image?, groups?, ro
|
|
|
128
128
|
"Admin" beside a name as rank. If the question your screen asks is "who is this person in the
|
|
129
129
|
company", the answer is `groups`.
|
|
130
130
|
- **`joined` is an ISO timestamp of when the membership began.** Render it at whatever precision
|
|
131
|
-
your question needs — `@lotics/ui`'s `
|
|
131
|
+
your question needs — `@lotics/ui`'s `MemberPeek` shows month and year, because "is this
|
|
132
132
|
the new person?" does not want a day. Absent on a public response, and on an id that did not
|
|
133
133
|
resolve: there is no membership to have begun.
|
|
134
134
|
- **`archived: true` marks someone who has LEFT** — omitted otherwise, never `false`. It is the one
|
|
135
135
|
field that is NOT gated (a departed colleague reading as a current assignee is wrong on a public
|
|
136
|
-
app too). Feed it to `inactive` on `MemberChip` / `
|
|
136
|
+
app too). Feed it to `inactive` on `MemberChip` / `MemberPeek`.
|
|
137
137
|
- **An id that no longer resolves** (removed member, id outside the org) comes back with
|
|
138
138
|
`name: null` — an explicit missing state, never an empty string. Render a placeholder.
|
|
139
139
|
- Only ids already present in the projected rows are resolved — a member cell never exposes the
|
|
@@ -256,7 +256,7 @@ client-side up front, and re-checked server-side.
|
|
|
256
256
|
- **The author is always the real signed-in member** — correct attribution, enforced server-side.
|
|
257
257
|
**Warning:** under "View as", comments still author as the real member (the admin), not the
|
|
258
258
|
view-as target — unlike `useViewer` and `is_current_member` scoping, which follow the target.
|
|
259
|
-
- **Edit and delete are author-only**, checked server-side against the viewer. `
|
|
259
|
+
- **Edit and delete are author-only**, checked server-side against the viewer. `CommentThread`'s
|
|
260
260
|
`currentMemberId` prop drives the matching affordance client-side.
|
|
261
261
|
|
|
262
262
|
### Reading & writing
|
|
@@ -277,13 +277,13 @@ renders the panel. State: `{ comments, loading, error, available, createComment,
|
|
|
277
277
|
updateComment, deleteComment, refetch }`.
|
|
278
278
|
|
|
279
279
|
- `comments` — newest first on the wire (server order). Pass the array as-is to `@lotics/ui`'s
|
|
280
|
-
`
|
|
280
|
+
`CommentThread`, which re-sorts oldest-first for display. Each `AppComment`: `{ id, record_id,
|
|
281
281
|
table_id, member_id, content, files, workspace_id, created_at, updated_at }`. Attachments
|
|
282
282
|
(`AppCommentFile`) carry `id` / `filename` / `mime_type` — a file's identity is its `id`, and
|
|
283
283
|
the server re-reads every attachment from storage by that id, so nothing else you hold about a
|
|
284
284
|
file can affect what is stored. The `url` / `thumbnail_url` / `preview_url` fields exist on the type
|
|
285
285
|
but the server does not populate them today — render attachments by name and type (what
|
|
286
|
-
`
|
|
286
|
+
`CommentThread`'s default file row does), never by counting on a fetchable URL.
|
|
287
287
|
- `createComment({ content, file_ids? })` — posts as the viewing member. `file_ids` come from
|
|
288
288
|
`useFileUpload` / `useAttachments` (see [files](./files.md)). A comment must have content or at
|
|
289
289
|
least one file (empty input is a client no-op; the server enforces the same rule). Content max
|
|
@@ -296,7 +296,7 @@ updateComment, deleteComment, refetch }`.
|
|
|
296
296
|
on it.
|
|
297
297
|
- **Limitation:** a comment carries `member_id` only — resolving the author's display name needs a
|
|
298
298
|
member source: the `useMembers` roster (requires the member-access declaration above) or member
|
|
299
|
-
cells in your own data. An unresolvable id should render a fallback (`
|
|
299
|
+
cells in your own data. An unresolvable id should render a fallback (`CommentThread` has an
|
|
300
300
|
`unknownMember` label for exactly this).
|
|
301
301
|
- **Freshness:** SWR-cached, revalidates on focus/reconnect, so another viewer's comment appears on
|
|
302
302
|
the next focus or explicit `refetch()` — not on its own. Comments are the one read that does not
|
|
@@ -320,10 +320,10 @@ directly (full props: `node_modules/@lotics/ui/AGENTS.md` and its `docs/`):
|
|
|
320
320
|
|
|
321
321
|
| Value | Component | Feed it |
|
|
322
322
|
| --- | --- | --- |
|
|
323
|
-
| A select value (stored or picker option) | `
|
|
323
|
+
| A select value (stored or picker option) | `Status` | A `useFieldOptions` option, or `byKey(readSelect(cell)[0]?.key)`; accepts a single option, an array (multi → one badge each), or null (renders nothing). Missing/unknown color → neutral. |
|
|
324
324
|
| A person, inline | `MemberChip` | `name` / `image` from a roster or a cell — both carry it; no image → initials |
|
|
325
325
|
| A member picker | `MemberSelect` | `members={useMembers().members}` — renders each option as a `MemberChip`; `MEMBER_UNASSIGNED` marks its optional "unassigned" option |
|
|
326
|
-
| A comment thread | `
|
|
326
|
+
| A comment thread | `CommentThread` + `CommentComposer` | `useComments` state; `resolveMember` bridges `member_id` → your member source |
|
|
327
327
|
|
|
328
328
|
The SDK never imports `@lotics/ui` — the app owns the (thin) data→UI adapter in each row above.
|
|
329
329
|
|
|
@@ -287,3 +287,4 @@ default `JSON.stringify`) and `max` (default **5**).
|
|
|
287
287
|
| `urlParam`, `UrlParamCodec`, `OptionalUrlParamCodec`, `UrlParams`, `UrlParamValue` | `@lotics/app-sdk` | `dist/src/url_params.d.ts` |
|
|
288
288
|
| `useRecents`, `RecentsApi`, `RecentsOptions` | `@lotics/app-sdk` | `dist/src/use_recents.d.ts` |
|
|
289
289
|
| `isEmbedded` | `@lotics/app-sdk` | `dist/src/rpc.d.ts` |
|
|
290
|
+
| `openApp` | `@lotics/app-sdk` | `dist/src/open_app.d.ts` |
|
package/docs/runtime.md
CHANGED
|
@@ -185,6 +185,7 @@ surfaces:
|
|
|
185
185
|
| `agentRun.get`, `agentRun.cancel` | yes | **no** — the dev forwarder doesn't implement them (`"Unknown RPC op: …"`) | yes |
|
|
186
186
|
| `agentRuns` (session history) | **no** — the product host doesn't implement it either (`"Unknown RPC op: agentRuns"`) | **no** | yes |
|
|
187
187
|
| `askAi` | yes | **no** — `"Unknown RPC op: askAi"` | rejects — `"askAi is only available when the app runs inside Lotics"` |
|
|
188
|
+
| `openApp` | yes — routes to the sibling in the same tab | yes — opens the sibling on the web app in a new tab (one app is served locally) | rejects — `"openApp needs the Lotics host …"` |
|
|
188
189
|
|
|
189
190
|
**Limitation:** Don't build a session-log UI on `useAgentRuns`; keep the log in app
|
|
190
191
|
state from live `useAgentRun` results (see [ai](./ai.md)). Server-side *cancel*
|
|
@@ -226,6 +227,7 @@ semantics are documented:
|
|
|
226
227
|
| `members` | `{ group? }` | `{ members }` | [members & options](./members_and_options.md) |
|
|
227
228
|
| `context` | `{}` | app identity (below) | this doc |
|
|
228
229
|
| `openExternal` | `{ url }` | `void` | this doc |
|
|
230
|
+
| `openApp` | `{ app_id, route }` | `void` | this doc |
|
|
229
231
|
| `askAi` | prompt/files/records seed | `void` | [ai](./ai.md) |
|
|
230
232
|
| `agentRuns` | `{ session_id, limit?, offset? }` | `{ runs }` | [ai](./ai.md) |
|
|
231
233
|
| `agentRun.get` | `{ run_id }` | `{ run }` | [ai](./ai.md) |
|
|
@@ -264,9 +266,41 @@ page (standalone). Opens in a new tab with `noopener,noreferrer`.
|
|
|
264
266
|
- Typical use: opening a workflow-generated file's `url` from
|
|
265
267
|
`WorkflowResult.files[]` (see [mutations](./mutations.md)).
|
|
266
268
|
- **Not a preview mechanism.** To *view* a file inline, use `@lotics/ui`'s
|
|
267
|
-
`FilePreview`/`
|
|
269
|
+
`FilePreview`/`FileGalleryDialog` (`@lotics/ui` ≥ 48 — see [files](./files.md)); `openExternal` is
|
|
268
270
|
"leave the app".
|
|
269
271
|
|
|
272
|
+
## `openApp()` — the cross-app hop
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
import { openApp, isEmbedded } from "@lotics/app-sdk";
|
|
276
|
+
await openApp(PLANNING_APP_ID, `/plan/${record.id}`);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`openApp(appId: string, route?: string): Promise<void>` (`dist/src/open_app.d.ts`).
|
|
280
|
+
A workspace built as several apps around one spine shows another app's record
|
|
281
|
+
read-only with a way THROUGH to the app that owns it; this is the way through.
|
|
282
|
+
The host owns app routing, so it lands the viewer on `route` inside `appId` in
|
|
283
|
+
the **same tab**, chrome kept — `_loc` carries the screen exactly as
|
|
284
|
+
`AppRouter` mirrors it, so the target boots at that record rather than at its
|
|
285
|
+
root. `route` is the target app's own in-app path, exactly as its router
|
|
286
|
+
declares it (`/` for its register); it is the target's to define.
|
|
287
|
+
|
|
288
|
+
- **Checked at the bridge, host-side**: the id by shape (`app_…`), the route as
|
|
289
|
+
an in-app path — starts with `/`, never `//` or a scheme. An app never
|
|
290
|
+
assembles the host's URL itself.
|
|
291
|
+
- **By transport**: embedded routes in place; `lotics app dev` serves one app
|
|
292
|
+
and has no shell to route inside, so it opens the sibling on the web app in a
|
|
293
|
+
new tab; standalone rejects (`openApp needs the Lotics host …`) — gate the
|
|
294
|
+
control on `isEmbedded()`.
|
|
295
|
+
- A sibling's id is this workspace's: a copy of the suite has different ones,
|
|
296
|
+
and the portability gate refuses a concrete `app_` in `src/` and in every
|
|
297
|
+
`.md`. The id reaches the app as data — a field the model binds, a query's
|
|
298
|
+
row — never as a literal.
|
|
299
|
+
- Put it on a control the person presses. A hop on mount pushes the host's
|
|
300
|
+
history without a gesture, and two apps that each do it leave Back nowhere
|
|
301
|
+
to go. A hop to the app's own id is refused — navigate inside an app with
|
|
302
|
+
its own router.
|
|
303
|
+
|
|
270
304
|
## `downloadFile()` — save browser-built bytes
|
|
271
305
|
|
|
272
306
|
```tsx
|
|
@@ -411,14 +445,11 @@ is a transport that wasn't wired.
|
|
|
411
445
|
no dynamic `import()`, no Node built-ins. Don't touch `window` at module top
|
|
412
446
|
level — resolve lazily (test environments import modules before `jsdom` is
|
|
413
447
|
ready).
|
|
414
|
-
- **
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
`@lotics/docx`) build under both. When a shared package must diverge per
|
|
420
|
-
target, use platform files (`x.web.ts` / `x.ts`) plus conditional package
|
|
421
|
-
`exports` — never a runtime `require` or dynamic import.
|
|
448
|
+
- **One module per entry.** A package apps share with the Lotics product
|
|
449
|
+
(`@lotics/ui`, `@lotics/xlsx`, `@lotics/docx`) ships ONE module per entry: no
|
|
450
|
+
platform twins, no per-target `exports` condition. The SDK itself is never
|
|
451
|
+
bundled by the product at all — the host frontend is forbidden from importing
|
|
452
|
+
`@lotics/app-sdk`, enforced by a dependency-direction test.
|
|
422
453
|
- **Self-contained on npm.** The SDK cannot import workspace-private or
|
|
423
454
|
host-only packages (`@lotics/shared`, `@lotics/ui-internal`, the frontend's
|
|
424
455
|
`@/` alias) or any React Native / Expo module — also test-enforced. A helper
|