@lotics/app-sdk 0.91.0 → 0.91.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/dist/src/router.d.ts +14 -1
- package/dist/src/router.js +69 -4
- package/docs/ai.md +1 -1
- package/docs/mutations.md +11 -10
- package/docs/navigation_and_state.md +22 -0
- package/package.json +1 -1
package/dist/src/router.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { type RouteObject } from "react-router";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The not-found screen's words. The SDK ships no locale, so an app whose reader
|
|
4
|
+
* does not read English passes its own — from `@lotics/ui`'s locale where the app
|
|
5
|
+
* uses the kit, from its own strings otherwise.
|
|
6
|
+
*/
|
|
7
|
+
export interface NotFoundWords {
|
|
8
|
+
/** Names the address that has no screen. Takes the path so the words may put
|
|
9
|
+
* it anywhere the language needs it. */
|
|
10
|
+
message: (path: string) => string;
|
|
11
|
+
/** The label of the link back to the first screen. */
|
|
12
|
+
firstScreen: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function AppRouter({ routes, notFound }: {
|
|
3
15
|
routes: RouteObject[];
|
|
16
|
+
notFound?: NotFoundWords;
|
|
4
17
|
}): import("react").JSX.Element;
|
package/dist/src/router.js
CHANGED
|
@@ -35,9 +35,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
35
35
|
* { path: "/item/:id", element: <Detail /> },
|
|
36
36
|
* ]} />;
|
|
37
37
|
* }
|
|
38
|
+
*
|
|
39
|
+
* An address none of the routes claim renders the SDK's not-found screen rather
|
|
40
|
+
* than nothing — see {@link NotFoundScreen} and {@link withNotFound}. Its words
|
|
41
|
+
* are the `notFound` prop ({@link NotFoundWords}), because an app's reader reads
|
|
42
|
+
* the app's language and the SDK ships no locale.
|
|
38
43
|
*/
|
|
39
44
|
import { useEffect } from "react";
|
|
40
|
-
import { BrowserRouter, useLocation, useRoutes, } from "react-router";
|
|
45
|
+
import { BrowserRouter, Link, useLocation, useRoutes, } from "react-router";
|
|
41
46
|
import { isEmbedded, setUrlParams } from "./rpc.js";
|
|
42
47
|
/** Host query key carrying the app's current screen, so it's shareable and the
|
|
43
48
|
* host can restore it on refresh. */
|
|
@@ -70,10 +75,70 @@ function HostScreenMirror() {
|
|
|
70
75
|
function RoutedRoutes({ routes }) {
|
|
71
76
|
return useRoutes(routes);
|
|
72
77
|
}
|
|
73
|
-
|
|
78
|
+
/*
|
|
79
|
+
* Shaped like the kit's `RegionState` — a centred message and one destination —
|
|
80
|
+
* and written in its tokens with literal fallbacks, so it takes the app's theme
|
|
81
|
+
* where `@lotics/ui/styles.css` is loaded and stays legible where it is not. It
|
|
82
|
+
* cannot BE `RegionState`: the SDK ships no kit component.
|
|
83
|
+
*/
|
|
84
|
+
const NOT_FOUND_ROOT = {
|
|
85
|
+
display: "flex",
|
|
86
|
+
flexDirection: "column",
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
justifyContent: "center",
|
|
89
|
+
gap: "var(--lotics-space-8, 8px)",
|
|
90
|
+
paddingBlock: "var(--lotics-space-48, 48px)",
|
|
91
|
+
paddingInline: "var(--lotics-space-16, 16px)",
|
|
92
|
+
textAlign: "center",
|
|
93
|
+
fontFamily: "var(--font-sans, system-ui, sans-serif)",
|
|
94
|
+
};
|
|
95
|
+
const NOT_FOUND_MESSAGE = {
|
|
96
|
+
margin: 0,
|
|
97
|
+
fontSize: "var(--lotics-text-sm, 14px)",
|
|
98
|
+
color: "var(--lotics-ink-muted, #71717a)",
|
|
99
|
+
};
|
|
100
|
+
const NOT_FOUND_LINK = {
|
|
101
|
+
fontSize: "var(--lotics-text-sm, 14px)",
|
|
102
|
+
color: "var(--lotics-accent, #2563eb)",
|
|
103
|
+
};
|
|
104
|
+
const NOT_FOUND_ENGLISH = {
|
|
105
|
+
message: (path) => `No screen at ${path}`,
|
|
106
|
+
firstScreen: "Go to the first screen",
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* What an app shows at an address none of its routes claim. Without it
|
|
110
|
+
* `useRoutes` matches nothing and the page renders EMPTY — no message and no
|
|
111
|
+
* console error — so a stale link or a typo reads as a crash.
|
|
112
|
+
*/
|
|
113
|
+
function NotFoundScreen({ home, words }) {
|
|
114
|
+
const { pathname } = useLocation();
|
|
115
|
+
return (_jsxs("div", { style: NOT_FOUND_ROOT, children: [
|
|
116
|
+
_jsx("p", { style: NOT_FOUND_MESSAGE, children: words.message(pathname) }), _jsx(Link, { to: home, style: NOT_FOUND_LINK, children: words.firstScreen })
|
|
117
|
+
] }));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The catch-all, appended at EVERY level of the tree: a route with `children`
|
|
121
|
+
* is a layout, and a catch-all among those children is what keeps that layout's
|
|
122
|
+
* shell on screen instead of swapping the whole page for the message. An app
|
|
123
|
+
* that declares its own `*` still wins — react-router ranks equal matches by
|
|
124
|
+
* declaration order and ours is appended last.
|
|
125
|
+
*/
|
|
126
|
+
function withNotFound(routes, element) {
|
|
127
|
+
return [
|
|
128
|
+
...routes.map((route) => route.children === undefined
|
|
129
|
+
? route
|
|
130
|
+
: { ...route, children: withNotFound(route.children, element) }),
|
|
131
|
+
{ path: "*", element },
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
/** The screen the not-found sends the reader back to — the first one declared. */
|
|
135
|
+
function firstScreenPath(routes) {
|
|
136
|
+
const first = routes.find((route) => route.path !== undefined && route.path !== "*");
|
|
137
|
+
return first?.path ?? "/";
|
|
138
|
+
}
|
|
139
|
+
export function AppRouter({ routes, notFound = NOT_FOUND_ENGLISH, }) {
|
|
74
140
|
// `isEmbedded()` reads the `?lotics_host=` the host puts on the iframe src, so
|
|
75
141
|
// it's known synchronously at first render.
|
|
76
142
|
const embedded = isEmbedded();
|
|
77
|
-
return (_jsxs(BrowserRouter, { children: [embedded ? _jsx(HostScreenMirror, {}) : null, _jsx(RoutedRoutes, { routes: routes })
|
|
78
|
-
] }));
|
|
143
|
+
return (_jsxs(BrowserRouter, { children: [embedded ? _jsx(HostScreenMirror, {}) : null, _jsx(RoutedRoutes, { routes: withNotFound(routes, _jsx(NotFoundScreen, { home: firstScreenPath(routes), words: notFound })) })] }));
|
|
79
144
|
}
|
package/docs/ai.md
CHANGED
|
@@ -13,7 +13,7 @@ Don't run a structured extraction through `askAi` (the result is stranded in a c
|
|
|
13
13
|
|
|
14
14
|
## Declared agents — what `useAgentRun` runs
|
|
15
15
|
|
|
16
|
-
An agent is **declared on the app server-side, by alias**, with the `set_app_agent` tool (removed with `remove_app_agent`)
|
|
16
|
+
An agent is **declared on the app server-side, by alias**, with the `set_app_agent` tool (removed with `remove_app_agent`) — the only verb that creates a binding, so no deploy binds an alias the app does not already have. A deploy does push an alias it has: the prose in `src/agents/<alias>.md` and the authored `inputs`/`outputs` in `package.json#lotics.agents.<alias>` go through `set_app_agent` whenever either differs from the live row, before the bundle ships. Every other key of that map is a reflection `lotics app pull` refreshes and no verb sends, so replaying a stale snapshot cannot revert a grant bound elsewhere. Invoking an alias that isn't bound fails with a "no agent alias" error naming `set_app_agent`. (After a deploy, the CLI warns about any manifest alias not bound on the server.)
|
|
17
17
|
|
|
18
18
|
A declaration carries:
|
|
19
19
|
|
package/docs/mutations.md
CHANGED
|
@@ -29,10 +29,9 @@ const result = await createOrder({ customer_id, quantity: 3 });
|
|
|
29
29
|
owner's** authority (never the viewer's — see [security](./security.md) for attribution,
|
|
30
30
|
privilege gates, and public-app semantics).
|
|
31
31
|
- Binding is server-side (`set_app_workflow`, or `lotics app workflow set <alias>` from the
|
|
32
|
-
app project)
|
|
33
|
-
workflows
|
|
34
|
-
|
|
35
|
-
purely to type `useWorkflow` (below); hand-editing it changes nothing on the server.
|
|
32
|
+
app project), and `lotics app deploy` calls it: `package.json#lotics.workflows.<alias>` plus
|
|
33
|
+
`src/workflows/<alias>.ts` ARE the source of a binding, so an alias committed to the repo is
|
|
34
|
+
one the next clone ships. The declaration also types `useWorkflow` (below).
|
|
36
35
|
- Invoking an alias that is not bound resolves with `status: "error"` and a message naming
|
|
37
36
|
the missing binding.
|
|
38
37
|
- Anonymous visitors to a publicly shared app can invoke workflows too; the triggering
|
|
@@ -204,11 +203,13 @@ if (r.status === "success" && r.data) {
|
|
|
204
203
|
|
|
205
204
|
## Declaring workflow inputs
|
|
206
205
|
|
|
207
|
-
An alias's declaration is `{
|
|
208
|
-
|
|
209
|
-
`
|
|
210
|
-
|
|
211
|
-
|
|
206
|
+
An alias's declaration is `{ inputs?, outputs?, workflow_id? }` — `workflow_id` is the server's
|
|
207
|
+
half, stamped by the first bind, so an alias an author has only just written carries none.
|
|
208
|
+
`inputs` maps each input name to a typed declaration; it is authored beside the body and
|
|
209
|
+
travels with it (`set_app_workflow` / `lotics app workflow set` reads it from
|
|
210
|
+
`package.json#lotics.workflows.<alias>`, and a deploy pushes a declaration that has moved), and
|
|
211
|
+
the schema the body was verified against is canonical — a manifest edit that reaches no push
|
|
212
|
+
weakens nothing. It drives three things at once: compile-time typing of the `useWorkflow` payload,
|
|
212
213
|
compile-time typing of `trigger.app_workflow.inputs.*` inside the body, and runtime payload
|
|
213
214
|
validation at the execute boundary.
|
|
214
215
|
|
|
@@ -278,7 +279,7 @@ When the alias declares `inputs`, the server validates the payload before the wo
|
|
|
278
279
|
added after authoring is accepted, a removed one rejected
|
|
279
280
|
(`select input "<path>" value "<v>" is not one of field "<key>"'s current options`) with no
|
|
280
281
|
redeploy. A `field` that doesn't exist or names a non-select field is rejected at bind time
|
|
281
|
-
(`lotics app workflow set` / `set_app_workflow
|
|
282
|
+
(`lotics app workflow set` / `set_app_workflow`, which a deploy calls for a declaration it holds) with
|
|
282
283
|
`select input references field "<key>", which does not exist in this workspace` /
|
|
283
284
|
`… which is a <type> field, not a select`. Prefer `field` for any select backed by a real
|
|
284
285
|
field; keep `options` for a fixed enum the app owns. Populate pickers from `useFieldOptions`
|
|
@@ -63,6 +63,28 @@ routes, and splats all work. Inside the tree, use react-router normally:
|
|
|
63
63
|
(`^7 || ^8` — the canonical package; the `react-router-dom` shim's tree also
|
|
64
64
|
satisfies it) is an **optional peer dependency** — an app that imports the
|
|
65
65
|
router entry must install it itself; nothing else in the SDK needs it.
|
|
66
|
+
- **An address no route claims says so.** `AppRouter` appends a catch-all at
|
|
67
|
+
every level of the tree, so such a path renders a not-found screen — a
|
|
68
|
+
message naming the path and a link to the first route — instead of an empty
|
|
69
|
+
page, and a layout route keeps its shell around that message. Declaring your
|
|
70
|
+
own `{ path: "*" }` replaces it.
|
|
71
|
+
- **The not-found screen speaks the app's language.** Its two strings default
|
|
72
|
+
to English; an app whose reader reads another language passes them as
|
|
73
|
+
`notFound`, next to `routes`:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<AppRouter
|
|
77
|
+
routes={routes}
|
|
78
|
+
notFound={{
|
|
79
|
+
message: (path) => `Không có màn hình ở ${path}`,
|
|
80
|
+
firstScreen: "Về màn hình đầu tiên",
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`message` takes the path so the words may place it where the language needs
|
|
86
|
+
it. Where the app uses `@lotics/ui`, read both from the kit's locale — the
|
|
87
|
+
SDK ships none of its own.
|
|
66
88
|
- **Limitation: element routing only.** `AppRouter` mounts a plain browser
|
|
67
89
|
router, not a react-router *data* router — route `loader`/`action` fields
|
|
68
90
|
are ignored, and `useLoaderData` **throws** ("must be used within a data
|