@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.
@@ -1,4 +1,17 @@
1
1
  import { type RouteObject } from "react-router";
2
- export declare function AppRouter({ routes }: {
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;
@@ -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
- export function AppRouter({ routes }) {
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`). `lotics app deploy` ships code and queries only — it never binds agents. The `lotics.agents` map in `package.json` is a **read-only reflection** written by `lotics app pull`; hand-editing it does nothing. 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.)
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). `lotics app deploy` ships code, queries, and capabilities — it never binds
33
- workflows.
34
- The `package.json#lotics.workflows` map is a *pulled reflection* of the live bindings, used
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 `{ workflow_id, inputs?, outputs? }`. `inputs` maps each input name
208
- to a typed declaration; it is authored when the workflow is bound (`set_app_workflow` /
209
- `lotics app workflow set` reads it from `package.json#lotics.workflows.<alias>`), and the
210
- schema the body was verified against is canonical — the manifest reflection cannot silently
211
- weaken it. It drives three things at once: compile-time typing of the `useWorkflow` payload,
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`; `lotics app deploy` never binds workflows) with
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.91.0",
3
+ "version": "0.91.1",
4
4
  "description": "Runtime SDK for Lotics custom-code apps \u2014 typed hooks, postMessage bridge, mount entry point",
5
5
  "type": "module",
6
6
  "exports": {