api_binding_ui 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,74 +1,80 @@
1
1
  # api_binding_ui
2
2
 
3
- Bind any UI component to documented APIs while a product is being designed, then run those bindings in production.
3
+ Design-time API binding. Register an app's components by id, bind them to documented APIs in a local studio, and generate plain React Query hooks over an [Orval](https://orval.dev) client. Nothing from this package ships in the app.
4
4
 
5
- - **Generic.** The code knows three slot roles (`consume`, `produce`, `trigger`) and nothing else. Component kinds, screens, products and APIs are data.
6
- - **Docs only.** APIs come from the OpenAPI documents. If the docs do not have it, it cannot be bound.
7
- - **API only.** Bindings say which API is called with what data and where results go. Opening, closing and navigation stay in the app.
5
+ Spec: [`spec/FRAMEWORK.md`](spec/FRAMEWORK.md). Agent rules: [`SKILL.md`](SKILL.md).
8
6
 
9
- The full contract is in [`spec/FRAMEWORK.md`](spec/FRAMEWORK.md); the types are in [`src/framework/contract.ts`](src/framework/contract.ts).
7
+ ## Flow
10
8
 
11
- ## Entries
9
+ 1. **Ids.** Every bindable component gets `componentId="<product>.<screen>[.<sub-screen>].<component>"`, e.g. `collections.worklist.add-notes.submit`.
10
+ 2. **Register.** List kinds, screens and components in `bindings.json`.
11
+ 3. **Client.** Run Orval on the same OpenAPI file (see below).
12
+ 4. **Bind.** `npx api-binding studio bindings.json` opens the studio on http://localhost:5180. Edits save to the file.
13
+ 5. **Generate.** `npx api-binding generate bindings.json` writes one hook per screen into `output.dir`.
14
+ 6. **Wire once.** Each screen calls its hook:
12
15
 
13
- | Import | Use | In production |
14
- |---|---|---|
15
- | `api_binding_ui` | Contract types, `catalogFromOpenApi`, the runtime (`BindingProvider`, `BindingScreen`, `useSlot`, `useCell`, `useProduce`, `useTrigger`), validation | Yes |
16
- | `api_binding_ui/studio` + `api_binding_ui/studio.css` | `BindingWidget`, `BindingStudio` | No; import only from design-time code |
16
+ ```tsx
17
+ import { useCollectionsWorklistAddNotesApi } from "@/api/screens";
17
18
 
18
- ## Runtime
19
+ const api = useCollectionsWorklistAddNotesApi({
20
+ notesType: { selected }, subject: { value: subject }, notes: { value: notes },
21
+ context: { selection: selectedRows },
22
+ });
23
+ const options = api.notesType.options.bound ? api.notesType.options.data ?? [] : sampleOptions;
19
24
 
20
- ```tsx
21
- import { BindingProvider, BindingScreen, useSlot, useProduce, useTrigger } from "api_binding_ui";
22
-
23
- <BindingProvider catalog={catalog} manifests={manifests} fetcher={fetcher}>
24
- <BindingScreen id="collections.worklist.add-notes" context={{ selection }}>
25
- <AddNotesForm />
26
- </BindingScreen>
27
- </BindingProvider>;
28
-
29
- function AddNotesForm() {
30
- const notesType = useSlot<{ value: string; label: string }[]>("notesType", "options");
31
- const [subject, setSubject] = useState("");
32
- useProduce("subject", "value", subject);
33
- const submit = useTrigger("submit", "click");
34
- // render notesType.data, call submit.run(), then close the dialog in your own code
35
- }
25
+ <Button componentId="collections.worklist.add-notes.submit" disabled={api.submit.click.running}
26
+ onClick={async () => { try { await api.submit.click.run(); close(); } catch (e) { toast(String(e)); } }} />
36
27
  ```
37
28
 
38
- `fetcher` receives `{ method, path, query, headers, body }` and the catalog operation; base URL, auth and errors stay in the app.
29
+ Rebinding changes only the hook bodies; the hook's types come from the registry, so call sites stay put.
39
30
 
40
- ## Studio
31
+ ## bindings.json
41
32
 
42
- ```tsx
43
- import { BindingWidget } from "api_binding_ui/studio";
44
- import "api_binding_ui/studio.css";
45
-
46
- // once, inside <BindingProvider persistence={…}>, design mode only
47
- <BindingWidget framework={{ descriptors, registry }} />
33
+ ```jsonc
34
+ {
35
+ "version": 3,
36
+ "openapi": "https://apidocs.highradius.com/documentation-ui/index.yaml",
37
+ "output": { "dir": "src/api/screens", "client": "@/api/generated" },
38
+ "kinds": [], // slot descriptors per component kind
39
+ "screens": [], // { id, label, context? }
40
+ "components": [], // { id, kind, label, parent? }
41
+ "bindings": [] // written by the studio
42
+ }
48
43
  ```
49
44
 
50
- A floating button opens the studio over the app on the screen being shown. Edits apply when it closes.
51
-
52
- The studio reads every product's APIs from the published docs (default `https://apidocs.highradius.com/documentation-ui/index.yaml`, JSON or YAML). Each user can change the URL in **Settings** (top right), which also downloads and imports bindings as JSON for committing with the app. Pass `docsUrl` to change the default, or `framework.catalog` to skip loading.
45
+ `openapi` is a URL or a path relative to the file; `output.dir` is relative to the file; `output.client` is the import specifier of the Orval output as the app sees it.
53
46
 
54
- ## Layout
47
+ ## Orval
55
48
 
49
+ ```ts
50
+ // orval.config.ts
51
+ export default {
52
+ api: {
53
+ input: "<same OpenAPI URL or file>",
54
+ output: { target: "src/api/generated/index.ts", client: "fetch", mode: "single", baseUrl: "<API base URL>" },
55
+ },
56
+ };
56
57
  ```
57
- src/
58
- framework/ pure TypeScript: contract, OpenAPI → catalog, schema, names, engine, validation,
59
- expressions, request runner, persistence
60
- runtime/ React runtime: provider, screen scope, hooks
61
- studio/ design-time UI: shell, navigator, search, component list, inspector, slot editors
62
- dev/ sample data and the local harness (not published)
63
- spec/ FRAMEWORK.md (the contract) and contract.ts (re-export)
64
- ```
58
+
59
+ The generated hooks call Orval's functions (named after each `operationId`) and unwrap `{ data, status }`; a non-2xx status throws. A `client: "react-query"` setup with `httpClient: "fetch"` works the same, since it exports the same functions.
60
+
61
+ ## CLI
62
+
63
+ | Command | Does |
64
+ |---|---|
65
+ | `api-binding studio [file] [--port 5180]` | Local studio. Reads and writes the file; fetches the docs server-side (no CORS). Accepts requests from localhost only. |
66
+ | `api-binding generate [file]` | Validates, then writes the hooks. Any error: nothing is written. Removes only files it generated. |
67
+ | `api-binding generate [file] --check` | Writes nothing; exits 1 when the hooks are out of date. For CI. |
68
+ | `api-binding generate [file] --src src` | Also lists registered ids missing from the source, and `componentId` literals not registered. |
65
69
 
66
70
  ## Develop
67
71
 
68
- ```
72
+ ```sh
69
73
  bun install
70
- bun run dev # studio with sample data on http://localhost:5174
74
+ bun run dev # builds the studio and serves it on src/dev/sample/bindings.json
71
75
  bun test
72
76
  bun run typecheck
73
- bun run build
77
+ bun run build # library, CLI (dist/cli.js) and studio app (dist/app)
74
78
  ```
79
+
80
+ Vite's esbuild binary is blocked on some managed machines, so the build uses Bun.
package/SKILL.md CHANGED
@@ -1,50 +1,45 @@
1
1
  ---
2
2
  name: api-binding-ui
3
- description: "Use when adding, changing or wiring screens in an app that depends on api_binding_ui: registry entries, BindingScreen, slot hooks, the design-mode BindingWidget."
3
+ description: "Use when adding, changing or wiring screens in an app whose API calls are designed with api_binding_ui: componentId props, bindings.json, generated screen hooks (src/api/screens), the Orval client."
4
4
  ---
5
5
 
6
6
  # api_binding_ui: rules for agents
7
7
 
8
- Contract: `node_modules/api_binding_ui/spec/FRAMEWORK.md`. Types: `import type { … } from "api_binding_ui"`.
8
+ Spec: `node_modules/api_binding_ui/spec/FRAMEWORK.md`. The app never imports `api_binding_ui`; it imports the generated hooks.
9
9
 
10
10
  ## Never
11
11
 
12
- - Call APIs from screens (`fetch`, axios, generated clients, mock hooks).
13
- - Add new hardcoded option lists, rows or values that an API should provide.
14
- - Put opening, closing, navigation, toasts or reloads in bindings. They are app code.
15
- - Invent APIs. Only operations in the catalog (generated from the OpenAPI docs) exist.
16
- - Edit the catalog or `openapi/*.json` by hand.
17
- - Import `api_binding_ui/studio` from production code paths. Load it with `React.lazy` behind the design-mode flag.
18
- - Add component kinds, product names or field names to framework code.
12
+ - Import `api_binding_ui` from app code, or add a binding runtime, provider or manifest to the app.
13
+ - Edit files under the Orval output or `src/api/screens`. They start with `// @generated`; regenerate instead.
14
+ - Call APIs from screens other than through the generated screen hook.
15
+ - Add hardcoded option lists, rows or values an API should provide.
16
+ - Put opening, closing, navigation, toasts or reloads into bindings. They are app code.
17
+ - Rename or reuse a `componentId`. Renaming orphans its bindings.
18
+ - Invent APIs. Only operations in the OpenAPI docs exist.
19
19
 
20
20
  ## Always
21
21
 
22
- 1. **Registry entry per screen** (`src/bindings/registry.ts`):
23
- ```ts
24
- { id: "<product>.<area>.<screen>", label, path: ["<Product>", ...groups], parent?: "<screen id>",
25
- context: [{ id, label, many?, shape: { schema } | { from: { screen, component, slot } } }],
26
- components: [{ bindId, kind, kindVersion: 1, label, parent?: "<bindId>" }] }
22
+ 1. **componentId on every bindable component**: data, input and action components.
23
+ - Format: `<product>.<screen>[.<sub-screen>…].<component>[.<part>…]`, lowercase kebab-case segments joined by dots. Examples: `collections.worklist.grid`, `collections.worklist.grid.column.customer-name`, `collections.worklist.grid.row.add-notes`, `collections.worklist.add-notes.notes-type`.
24
+ - A dialog or drawer is a sub-screen: its id extends the id of the screen it opens over.
25
+ - A component id never equals a screen id.
26
+ - Components built from config (grid columns, row actions) build the id from the grid's id and the config item's id: `` `${gridId}.column.${kebab(col.id)}` ``.
27
+ 2. **Register** every new screen and component in `bindings.json` (`screens`, `components`), with its kind and the label users see.
28
+ 3. **One hook call per screen:**
29
+ ```tsx
30
+ const api = use<Screen>Api({ <component>: { <produceSlot>: value }, context: { <id>: value } });
27
31
  ```
28
- - `path[0]` is the product. A dialog or drawer is its own screen with `parent`.
29
- - `bindId` is stable, unique per screen and independent of the label: `notesType`, not `select1`. Letters, digits, `-` and `_` only; never a dot (`grid-search-status`, not `grid.search.status`).
30
- - `label` is the text the user sees ("Call list"), never the id.
31
- - `kind` must exist in `src/bindings/descriptors.ts`. Grid columns and row actions set `parent` to the grid's bindId.
32
- 2. **Wrap the screen:** `<BindingScreen id="<screen id>" context={{ <context id>: value }}>`. Views kept mounted while hidden (inactive tabs) are wrapped in `<BindingActive value={isVisible}>`.
33
- 3. **Wire every component through its slots:**
34
-
35
- | Slot role | Hook | Use |
36
- |---|---|---|
37
- | consume | `const s = useSlot<T>(bindId, slot)` | render by `s.status`: `unbound` / `waiting` / `loading` / `ready` / `error`; data in `s.data`; `s.reload()` |
38
- | consume, per row | `const cell = useCell<T>(bindId, slot)` | `cell(row)` |
39
- | produce | `useProduce(bindId, slot, value)` | publish every value the component exposes; dropdowns publish the whole selected option object as `selected` |
40
- | trigger | `const t = useTrigger(bindId, slot)` | `if (t.bound) { const out = await t.run(item?); if (out.every(o => o.ok)) { /* app: close, toast, reload */ } else { /* show out.find(o => !o.ok).error */ } }` |
41
-
42
- 4. **Unbound keeps what the screen has today:** `status === "unbound"` renders the screen's existing content (its sample data during design); `ready` replaces it with API data. `t.bound === false` keeps the existing behaviour.
43
- 5. **No per-screen Bind API buttons.** One `<BindingWidget>` near the app root opens the studio on whichever `BindingScreen` is showing.
32
+ - Pass every value the screen's components produce: input values, the selected option object (whole object, as `selected`), grid page, sort and selection.
33
+ - Pass what the screen was opened with as `context`.
34
+ 4. **Render by `bound`:**
35
+ - `Data`: `api.x.slot.bound ? api.x.slot.data : <the screen's own value>`, with `loading` and `error`.
36
+ - `Cell`: `api.col.value.bound ? api.col.value.read(row) : row.field`.
37
+ - `Action`: `await api.btn.click.run(payload?)`. It rejects on failure. After success, the app decides what happens next (close, toast, reload).
38
+ 5. After changing `bindings.json` or the docs: `npx api-binding generate bindings.json --src src`, then typecheck. `tsc` errors show the call sites to update.
44
39
 
45
40
  ## New screen checklist
46
41
 
47
- - [ ] Registry entry with every component; `parent` for dialogs; `context` for inputs
48
- - [ ] `<BindingScreen>` around the screen
49
- - [ ] Every component wired with the hooks; no API calls or static data left
50
- - [ ] Screen opens in the studio from the floating button, in design mode
42
+ - [ ] `componentId` on every component, in the format above
43
+ - [ ] Screen and components registered in `bindings.json`
44
+ - [ ] Hooks regenerated; screen calls `use<Screen>Api(state)` once and passes every produced value
45
+ - [ ] Unbound slots keep the screen's existing content