api_binding_ui 3.2.0 → 3.3.2

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.
@@ -7,7 +7,9 @@
7
7
  *
8
8
  * Serves the studio under /__binding/ and injects a floating button into the app's HTML pages
9
9
  * (SPA and server-rendered alike). Every save regenerates the screen hooks, so Vite reloads the
10
- * screen with the new binding. `vite build` never runs it (apply: "serve").
10
+ * screen with the new binding. On start it generates the API client (Orval, when the app has
11
+ * orval.config.ts) and the screen hooks, so a project needs no committed generated code.
12
+ * `vite build` never runs it (apply: "serve").
11
13
  */
12
14
  import type { IncomingMessage, ServerResponse } from "node:http";
13
15
  export interface BindingStudioOptions {
@@ -39,11 +41,13 @@ export interface VitePluginLike {
39
41
  watcher?: {
40
42
  on: (event: "change", fn: (p: string) => void) => void;
41
43
  };
42
- }) => void;
44
+ }) => Promise<void>;
43
45
  }
44
46
  /** Adds the overlay script before the last </body> of HTML chunks; leaves everything else as it is. */
45
47
  export declare function injectOverlay(res: ServerResponse, tag: string): void;
46
48
  /** Regenerates the hooks after a save; logs the outcome, since the file is already saved either way. */
47
49
  export declare function regenerate(file: string, log?: (line: string) => void): Promise<void>;
50
+ /** Runs the app's Orval config, so the client the hooks import exists. Orval is the app's dependency, not this package's. */
51
+ export declare function generateClient(root: string, log?: (line: string) => void): Promise<void>;
48
52
  export declare function bindingStudio(options?: BindingStudioOptions): VitePluginLike;
49
53
  export {};
package/dist/vite.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/vite/plugin.ts
2
- import { readFileSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
3
  import path4 from "node:path";
4
4
 
5
5
  // src/server/handler.ts
@@ -744,10 +744,6 @@ function compileSteps(steps, cx, at, ops, imports) {
744
744
  cx.use.add("finish");
745
745
  for (const st of steps) {
746
746
  const op = ops.get(st.operation);
747
- if (!op) {
748
- cx.issue("catalog.operation-missing", `${st.operation} is not in the API docs.`, at);
749
- continue;
750
- }
751
747
  const scope = { item: true, steps: done };
752
748
  const call = compileCall(op, st.request, cx, scope, at);
753
749
  imports.add(call.fn);
@@ -818,6 +814,12 @@ function generate(file, catalog) {
818
814
  issue: (code, message, at) => issues.push({ severity: "error", screen: screen.id, code, message, ...at }),
819
815
  warn: (code, message, at) => issues.push({ severity: "warning", screen: screen.id, code, message, ...at })
820
816
  };
817
+ const gone = (operations, at) => {
818
+ const missing = operations.filter((o) => !ops.has(o));
819
+ for (const o of missing)
820
+ cx.warn("catalog.operation-missing", `${o} is no longer in the API docs; generated unbound until it is rebound.`, at);
821
+ return missing.length > 0;
822
+ };
821
823
  const bindings = new Map;
822
824
  for (const b of index[screen.id] ?? []) {
823
825
  if (!keys.has(b.component)) {
@@ -855,7 +857,7 @@ function generate(file, catalog) {
855
857
  if (s.role === "trigger") {
856
858
  types.add("Action");
857
859
  outT.push(`${prop(s.id)}: Action`);
858
- if (b?.type !== "steps") {
860
+ if (b?.type !== "steps" || gone(b.steps.map((st) => st.operation), at)) {
859
861
  unbound(s.id, "UNBOUND_ACTION");
860
862
  continue;
861
863
  }
@@ -881,13 +883,11 @@ function generate(file, catalog) {
881
883
  out.push(`${prop(s.id)}: { bound: true, data: ${compileExpr(b.value, cx, { item: false }, at)}, loading: false, error: undefined }`);
882
884
  continue;
883
885
  }
884
- const op = b?.type === "operation" ? ops.get(b.operation) : undefined;
885
- if (b?.type === "operation" && !op)
886
- cx.issue("catalog.operation-missing", `${b.operation} is not in the API docs.`, at);
887
- if (b?.type !== "operation" || !op) {
886
+ if (b?.type !== "operation" || gone([b.operation], at)) {
888
887
  unbound(s.id, "UNBOUND_DATA");
889
888
  continue;
890
889
  }
890
+ const op = ops.get(b.operation);
891
891
  const call = compileCall(op, b.request, cx, { item: false }, at);
892
892
  imports.add(call.fn);
893
893
  ["useData", "ok"].forEach((h) => cx.use.add(h));
@@ -1045,8 +1045,7 @@ async function regenerate(file, log = console.log) {
1045
1045
  try {
1046
1046
  const r = await writeScreens(file);
1047
1047
  for (const i of r.issues)
1048
- if (i.severity === "error")
1049
- log(`[api-binding] ${describeIssue(i)}`);
1048
+ log(`[api-binding] ${describeIssue(i)}`);
1050
1049
  if (r.issues.some((i) => i.severity === "error"))
1051
1050
  log("[api-binding] Screen hooks not regenerated: fix the errors above.");
1052
1051
  else if (r.changed.length || r.stale.length)
@@ -1055,10 +1054,23 @@ async function regenerate(file, log = console.log) {
1055
1054
  log(`[api-binding] Screen hooks not regenerated: ${e.message}`);
1056
1055
  }
1057
1056
  }
1057
+ async function generateClient(root, log = console.log) {
1058
+ const config = path4.join(root, "orval.config.ts");
1059
+ if (!existsSync(config))
1060
+ return;
1061
+ try {
1062
+ const orval = "orval";
1063
+ const { generate } = await import(orval);
1064
+ await generate(config, root);
1065
+ log("[api-binding] API client generated.");
1066
+ } catch (e) {
1067
+ log(`[api-binding] API client not generated, so it may lack APIs the hooks call: ${e.message}`);
1068
+ }
1069
+ }
1058
1070
  function bindingStudio(options = {}) {
1059
1071
  const base = (options.base ?? "/__binding").replace(/\/+$/, "");
1060
1072
  const tag = `<script type="module" src="${base}/overlay.js"></script>`;
1061
- let file = "";
1073
+ let file = "", root = "";
1062
1074
  const norm = (s) => process.platform === "win32" ? path4.resolve(s).toLowerCase() : path4.resolve(s);
1063
1075
  const isBindingsFile = (p) => {
1064
1076
  const abs = norm(p), f = norm(file);
@@ -1076,11 +1088,16 @@ function bindingStudio(options = {}) {
1076
1088
  name: "api-binding-studio",
1077
1089
  apply: "serve",
1078
1090
  configResolved(config) {
1079
- file = path4.resolve(config.root, options.file ?? "bindings.json");
1091
+ root = config.root;
1092
+ file = path4.resolve(root, options.file ?? "bindings.json");
1080
1093
  },
1081
1094
  transformIndexHtml: (html) => html.includes(tag) ? html : html.replace(/<\/body>/i, `${tag}</body>`),
1082
1095
  config: () => ({ server: { watch: { ignored: [(p) => isBindingsFile(p)] } } }),
1083
- configureServer(server) {
1096
+ async configureServer(server) {
1097
+ if (existsSync(file)) {
1098
+ await generateClient(root);
1099
+ await regenerate(file);
1100
+ }
1084
1101
  const allowed = (req) => {
1085
1102
  const origin = req.headers.origin;
1086
1103
  return req.method === "GET" || !origin || new URL(origin).host === req.headers.host;
@@ -1108,6 +1125,7 @@ function bindingStudio(options = {}) {
1108
1125
  }
1109
1126
  export {
1110
1127
  bindingStudio,
1128
+ generateClient,
1111
1129
  injectOverlay,
1112
1130
  regenerate
1113
1131
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api_binding_ui",
3
- "version": "3.2.0",
3
+ "version": "3.3.2",
4
4
  "description": "Design-time API binding: register screen components, bind them to documented APIs in a studio, and generate plain React Query hooks over an Orval client. Nothing ships in the app.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/spec/FRAMEWORK.md CHANGED
@@ -1,178 +1,178 @@
1
- # API Binding Framework — v3
2
-
3
- Normative types: [`contract.ts`](../src/framework/contract.ts).
4
-
5
- The framework connects **UI components** to **documented API operations** at design time and turns the result into **plain code**. Binding lives in two files: `registry.json` (what exists: kinds and the screen → component → part tree) and `bindings.json` (how it is wired to the API). After design time the app holds only ordinary React Query hooks over an Orval client; no framework code, runtime or manifest ships.
6
-
7
- ---
8
-
9
- ## 1. Principles
10
-
11
- 1. **Nothing concrete in the framework.** Component kinds, products, operations and fields arrive as data: kinds and the registry in the bindings file, operations from the OpenAPI docs.
12
- 2. **The API docs are the only source of operations.** No local hooks, hardcoded option lists or free-text overrides.
13
- 3. **UI first.** The unit of work is a screen and its components; APIs attach to component slots.
14
- 4. **Two files.** `registry.json` says what exists (kinds, screens, components); `bindings.json` says how it is wired (output settings, bindings). The studio writes only `bindings.json`.
15
- 5. **No trace at run time.** The generator writes plain hooks; the app never imports this package.
16
- 6. **Deterministic.** The same file and docs always produce the same code. Generated files carry an `@generated` mark and are never edited by hand.
17
-
18
- ---
19
-
20
- ## 2. The pieces
21
-
22
- ```
23
- OpenAPI docs ──► Orval ──────────────► src/api/generated (typed client)
24
- │
25
- └──► api-binding studio ◄──► bindings.json (+ registry.json) ──► api-binding generate ──► src/api/screens (one hook per screen)
26
- │
27
- app components ── use<Screen>Api(state) ┘
28
- ```
29
-
30
- | Piece | What it is | Owned by |
31
- |---|---|---|
32
- | Kinds | A component kind's slots, with JSON Schema shapes | Design system, in `registry.json` → `kinds` |
33
- | Screens and components | Every screen and component instance, by id | The app, in `registry.json` (a tree; nesting gives each part its parent) |
34
- | Bindings | Which operation or value feeds each slot | Designers in the studio, reviewed by engineers |
35
- | Orval client | Typed functions per operation | Generated by Orval from the same docs |
36
- | Screen hooks | `use<Screen>Api(state)` per screen | Generated by `api-binding generate` |
37
-
38
- ---
39
-
40
- ## 3. Ids
41
-
42
- One namespace for screens and components, used everywhere: in `componentId` props, in the file and in expressions.
43
-
44
- ```
45
- <product>.<screen>[.<sub-screen>…].<component>[.<part>…]
46
- collections.worklist screen
47
- collections.worklist.add-notes sub-screen (a dialog over the worklist)
48
- collections.worklist.grid component on collections.worklist
49
- collections.worklist.grid.column.customer-name
50
- collections.worklist.grid.row.add-notes
51
- collections.worklist.add-notes.notes-type component on collections.worklist.add-notes
52
- ```
53
-
54
- - Segments are lowercase kebab-case (`[a-z0-9-]`), joined by dots: `^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$`.
55
- - A component belongs to the **longest** registered screen id that prefixes it. A screen's parent is the longest other screen id that prefixes it. Navigation is built from this; there is no `path` or `parent` to maintain.
56
- - A component id may not equal a screen id (it would capture that screen's components).
57
- - Ids are stable: renaming one orphans its bindings. They never depend on labels.
58
- - In the app, each component gets `componentId="<id>"`; design-system components render it as `data-component-id`.
59
-
60
- ---
61
-
62
- ## 4. Kinds and slots
63
-
64
- A kind declares slots with one of three **roles**:
65
-
66
- | Role | Meaning | Bound to |
67
- |---|---|---|
68
- | `consume` | The component needs data | An operation (request + response selection) or an expression |
69
- | `produce` | The component exposes data | Nothing; a source others read |
70
- | `trigger` | The component emits an event | Ordered API call steps, or "no API" |
71
-
72
- - `scope.parentSlot`: the slot is evaluated per item of the parent's slot (a cell in a row, an action on a row); `$item` is that item.
73
- - `feeds`: produce slots of the same component offered as request sources (paging, sorting).
74
- - `"$ref": "#slot/<id>"`: the same shape as that slot after binding (a grid's selection has the shape of its rows).
75
-
76
- ---
77
-
78
- ## 5. The bindings file
79
-
80
- ```jsonc
81
- {
82
- "version": 3,
83
- "openapi": "https://apidocs.example.com/index.yaml", // or a path next to this file
84
- "output": { "dir": "src/api/screens", "client": "@/api/generated" },
85
- "kinds": [ /* ComponentDescriptor */ ],
86
- "screens": [ { "id": "collections.worklist.add-notes", "label": "Add Notes",
87
- "context": [ { "id": "selection", "label": "Selected rows", "many": true,
88
- "shape": { "from": { "screen": "collections.worklist", "component": "collections.worklist.grid", "slot": "selection" } } } ] } ],
89
- "components": [ { "id": "collections.worklist.add-notes.notes-type", "kind": "hr-ds/select", "label": "Notes Type" } ],
90
- "bindings": [ /* SlotBinding */ ]
91
- }
92
- ```
93
-
94
- | Slot role | Binding type | Contents |
95
- |---|---|---|
96
- | consume | `operation` | operation, request mapping (pointer → expression), response `select`, optional `map`, `load` |
97
- | consume | `expression` | one expression |
98
- | trigger | `steps` | ordered `call` steps; a step may run `forEach` over a list; `onError: stop \| continue` |
99
- | trigger | `none` | reviewed: this event calls no API |
100
-
101
- Only `confirmed` bindings are generated; drafts are reported as warnings. Bindings describe API calls only: opening, closing, navigation, toasts and reloads are app code.
102
-
103
- ---
104
-
105
- ## 6. Expressions
106
-
107
- | Form | Resolves to |
108
- |---|---|
109
- | `<componentId>:<slot>[.path]` | A produce slot on the same screen, e.g. `collections.worklist.add-notes.subject:value` |
110
- | `<expr>[*].<path>` | A property of every item, as a list |
111
- | `$item[.path]` | The current item: a scoped slot's item, a `forEach` item, or the trigger's payload |
112
- | `$context.<id>[.path]` | A value declared in the screen's `context` |
113
- | `$steps.<stepId>.response[.path]` | The response of an earlier step |
114
-
115
- `:` ends a component id, so ids can contain dots. Nothing outside this grammar is valid.
116
-
117
- ---
118
-
119
- ## 7. Generated code
120
-
121
- `api-binding generate` writes into `output.dir`:
122
-
123
- - `screen-helpers.ts`: the few helpers the hooks share (React Query only).
124
- - `<screen id>.ts` per screen: `<Screen>State`, `<Screen>Api` and `use<Screen>Api(state)`.
125
- - `index.ts`: re-exports.
126
-
127
- **The signature comes from the registry, the body from the bindings.** `<Screen>State` has one optional entry per component that produces values; `<Screen>Api` has one entry per consume and trigger slot. Changing bindings never changes either type, so call sites stay put. Adding or removing components does, and `tsc` points at every call site to update.
128
-
129
- | Slot | Returns | When unbound |
130
- |---|---|---|
131
- | consume | `Data<T>`: `{ bound, data, loading, error }` | `bound: false`; keep the UI's own value |
132
- | consume, scoped | `Cell<T>`: `{ bound, read(item) }` | `read` returns `undefined` |
133
- | trigger | `Action`: `{ bound, run(payload?), running, error }` | `run` resolves at once |
134
-
135
- - Queries call the Orval function for the operation: path params in path order, then the body, then the query params object, then fetch options (headers). A query is enabled only when every required input has a value; `load: { everySeconds }` polls.
136
- - Actions run steps in order. A failed call rejects `run` (`stop`) or is collected and rejected after the rest run (`continue`).
137
- - Responses are unwrapped from Orval's `{ data, status, headers }`; a non-2xx status throws `ApiError`.
138
-
139
- ---
140
-
141
- ## 8. Suggestions
142
-
143
- The studio ranks **all** operations for each slot and shows the best first; any operation can be chosen. Ranking uses metadata only: shape compatibility, intent, name similarity, glossary synonyms. Request fields are proposed from the values the screen has (produce slots, context, earlier steps, the current item), ranked by name and type. Proposals stay `origin: "suggested"` until a person confirms them.
144
-
145
- ---
146
-
147
- ## 9. Validation
148
-
149
- `api-binding generate` fails, and writes nothing, on any error:
150
-
151
- - an id that breaks the format, is duplicated, or clashes with a screen id; a component under no screen
152
- - an unknown kind, or a parent that is not registered
153
- - an expression reading a slot that is not produced on the screen, an undeclared context, a later step, or `$item` where there is none
154
- - an operation not in the docs, a request input the operation does not have, an unconnected path parameter
155
- - two components whose names become the same TypeScript key
156
-
157
- Warnings: drafts, unconnected required body fields, and with `--src` every registered id missing from the source (and every `componentId` literal not registered). `--check` exits 1 when generated files are out of date.
158
-
159
- ---
160
-
161
- ## 10. Studio rules
162
-
163
- - Runs from the Vite dev plugin (`api_binding_ui/vite`, serve only) or the CLI (`api-binding studio`). Either server reads `registry.json`, reads and writes `bindings.json`, and fetches the docs server-side, so the browser needs no CORS access.
164
- - Under the Vite plugin every save regenerates the screen hooks, so a confirmed binding is live in the running app after the save. Vite does not watch `bindings.json`.
165
- - The dev plugin's floating button opens the studio over the app on the screen shown: the screen most `data-component-id` values the user can see belong to; a component covered by any layer above it does not count. Alt+click opens it on one component. Nothing reaches a build.
166
- - Navigation and component lists come from the ids. Icons come from kinds.
167
- - Three slot editors, one per role, generated from slot schemas. A new kind never needs studio code.
168
- - Shows only what validates.
169
-
170
- ---
171
-
172
- ## 11. Conformance rules for an app
173
-
174
- 1. Every bindable component has a `componentId` in the format above, registered in `registry.json`.
175
- 2. Each screen calls its generated `use<Screen>Api(state)` once, passes what its components produce and what it was opened with, and renders `bound ? data : <its own value>`.
176
- 3. No direct API calls in screens other than through generated hooks; no hardcoded data an API should provide.
177
- 4. Opening, closing, navigation, toasts and reloads stay in the app's UI code.
178
- 5. Orval output and screen hooks are regenerated, never edited by hand.
1
+ # API Binding Framework — v3
2
+
3
+ Normative types: [`contract.ts`](../src/framework/contract.ts).
4
+
5
+ The framework connects **UI components** to **documented API operations** at design time and turns the result into **plain code**. Binding lives in two files: `registry.json` (what exists: kinds and the screen → component → part tree) and `bindings.json` (how it is wired to the API). After design time the app holds only ordinary React Query hooks over an Orval client; no framework code, runtime or manifest ships.
6
+
7
+ ---
8
+
9
+ ## 1. Principles
10
+
11
+ 1. **Nothing concrete in the framework.** Component kinds, products, operations and fields arrive as data: kinds and the registry in the bindings file, operations from the OpenAPI docs.
12
+ 2. **The API docs are the only source of operations.** No local hooks, hardcoded option lists or free-text overrides.
13
+ 3. **UI first.** The unit of work is a screen and its components; APIs attach to component slots.
14
+ 4. **Two files.** `registry.json` says what exists (kinds, screens, components); `bindings.json` says how it is wired (output settings, bindings). The studio writes only `bindings.json`.
15
+ 5. **No trace at run time.** The generator writes plain hooks; the app never imports this package.
16
+ 6. **Deterministic.** The same file and docs always produce the same code. Generated files carry an `@generated` mark and are never edited by hand.
17
+
18
+ ---
19
+
20
+ ## 2. The pieces
21
+
22
+ ```
23
+ OpenAPI docs ──► Orval ──────────────► src/api/generated (typed client)
24
+ │
25
+ └──► api-binding studio ◄──► bindings.json (+ registry.json) ──► api-binding generate ──► src/api/screens (one hook per screen)
26
+ │
27
+ app components ── use<Screen>Api(state) ┘
28
+ ```
29
+
30
+ | Piece | What it is | Owned by |
31
+ |---|---|---|
32
+ | Kinds | A component kind's slots, with JSON Schema shapes | Design system, in `registry.json` → `kinds` |
33
+ | Screens and components | Every screen and component instance, by id | The app, in `registry.json` (a tree; nesting gives each part its parent) |
34
+ | Bindings | Which operation or value feeds each slot | Designers in the studio, reviewed by engineers |
35
+ | Orval client | Typed functions per operation | Generated by Orval from the same docs |
36
+ | Screen hooks | `use<Screen>Api(state)` per screen | Generated by `api-binding generate` |
37
+
38
+ ---
39
+
40
+ ## 3. Ids
41
+
42
+ One namespace for screens and components, used everywhere: in `componentId` props, in the file and in expressions.
43
+
44
+ ```
45
+ <product>.<screen>[.<sub-screen>…].<component>[.<part>…]
46
+ collections.worklist screen
47
+ collections.worklist.add-notes sub-screen (a dialog over the worklist)
48
+ collections.worklist.grid component on collections.worklist
49
+ collections.worklist.grid.column.customer-name
50
+ collections.worklist.grid.row.add-notes
51
+ collections.worklist.add-notes.notes-type component on collections.worklist.add-notes
52
+ ```
53
+
54
+ - Segments are lowercase kebab-case (`[a-z0-9-]`), joined by dots: `^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$`.
55
+ - A component belongs to the **longest** registered screen id that prefixes it. A screen's parent is the longest other screen id that prefixes it. Navigation is built from this; there is no `path` or `parent` to maintain.
56
+ - A component id may not equal a screen id (it would capture that screen's components).
57
+ - Ids are stable: renaming one orphans its bindings. They never depend on labels.
58
+ - In the app, each component gets `componentId="<id>"`; design-system components render it as `data-component-id`.
59
+
60
+ ---
61
+
62
+ ## 4. Kinds and slots
63
+
64
+ A kind declares slots with one of three **roles**:
65
+
66
+ | Role | Meaning | Bound to |
67
+ |---|---|---|
68
+ | `consume` | The component needs data | An operation (request + response selection) or an expression |
69
+ | `produce` | The component exposes data | Nothing; a source others read |
70
+ | `trigger` | The component emits an event | Ordered API call steps, or "no API" |
71
+
72
+ - `scope.parentSlot`: the slot is evaluated per item of the parent's slot (a cell in a row, an action on a row); `$item` is that item.
73
+ - `feeds`: produce slots of the same component offered as request sources (paging, sorting).
74
+ - `"$ref": "#slot/<id>"`: the same shape as that slot after binding (a grid's selection has the shape of its rows).
75
+
76
+ ---
77
+
78
+ ## 5. The bindings file
79
+
80
+ ```jsonc
81
+ {
82
+ "version": 3,
83
+ "openapi": "https://apidocs.example.com/index.yaml", // or a path next to this file
84
+ "output": { "dir": "src/api/screens", "client": "@/api/generated" },
85
+ "kinds": [ /* ComponentDescriptor */ ],
86
+ "screens": [ { "id": "collections.worklist.add-notes", "label": "Add Notes",
87
+ "context": [ { "id": "selection", "label": "Selected rows", "many": true,
88
+ "shape": { "from": { "screen": "collections.worklist", "component": "collections.worklist.grid", "slot": "selection" } } } ] } ],
89
+ "components": [ { "id": "collections.worklist.add-notes.notes-type", "kind": "hr-ds/select", "label": "Notes Type" } ],
90
+ "bindings": [ /* SlotBinding */ ]
91
+ }
92
+ ```
93
+
94
+ | Slot role | Binding type | Contents |
95
+ |---|---|---|
96
+ | consume | `operation` | operation, request mapping (pointer → expression), response `select`, optional `map`, `load` |
97
+ | consume | `expression` | one expression |
98
+ | trigger | `steps` | ordered `call` steps; a step may run `forEach` over a list; `onError: stop \| continue` |
99
+ | trigger | `none` | reviewed: this event calls no API |
100
+
101
+ Only `confirmed` bindings are generated; drafts are reported as warnings. Bindings describe API calls only: opening, closing, navigation, toasts and reloads are app code.
102
+
103
+ ---
104
+
105
+ ## 6. Expressions
106
+
107
+ | Form | Resolves to |
108
+ |---|---|
109
+ | `<componentId>:<slot>[.path]` | A produce slot on the same screen, e.g. `collections.worklist.add-notes.subject:value` |
110
+ | `<expr>[*].<path>` | A property of every item, as a list |
111
+ | `$item[.path]` | The current item: a scoped slot's item, a `forEach` item, or the trigger's payload |
112
+ | `$context.<id>[.path]` | A value declared in the screen's `context` |
113
+ | `$steps.<stepId>.response[.path]` | The response of an earlier step |
114
+
115
+ `:` ends a component id, so ids can contain dots. Nothing outside this grammar is valid.
116
+
117
+ ---
118
+
119
+ ## 7. Generated code
120
+
121
+ `api-binding generate` writes into `output.dir`:
122
+
123
+ - `screen-helpers.ts`: the few helpers the hooks share (React Query only).
124
+ - `<screen id>.ts` per screen: `<Screen>State`, `<Screen>Api` and `use<Screen>Api(state)`.
125
+ - `index.ts`: re-exports.
126
+
127
+ **The signature comes from the registry, the body from the bindings.** `<Screen>State` has one optional entry per component that produces values; `<Screen>Api` has one entry per consume and trigger slot. Changing bindings never changes either type, so call sites stay put. Adding or removing components does, and `tsc` points at every call site to update.
128
+
129
+ | Slot | Returns | When unbound |
130
+ |---|---|---|
131
+ | consume | `Data<T>`: `{ bound, data, loading, error }` | `bound: false`; keep the UI's own value |
132
+ | consume, scoped | `Cell<T>`: `{ bound, read(item) }` | `read` returns `undefined` |
133
+ | trigger | `Action`: `{ bound, run(payload?), running, error }` | `run` resolves at once |
134
+
135
+ - Queries call the Orval function for the operation: path params in path order, then the body, then the query params object, then fetch options (headers). A query is enabled only when every required input has a value; `load: { everySeconds }` polls.
136
+ - Actions run steps in order. A failed call rejects `run` (`stop`) or is collected and rejected after the rest run (`continue`).
137
+ - Responses are unwrapped from Orval's `{ data, status, headers }`; a non-2xx status throws `ApiError`.
138
+
139
+ ---
140
+
141
+ ## 8. Suggestions
142
+
143
+ The studio ranks **all** operations for each slot and shows the best first; any operation can be chosen. Ranking uses metadata only: shape compatibility, intent, name similarity, glossary synonyms. Request fields are proposed from the values the screen has (produce slots, context, earlier steps, the current item), ranked by name and type. Proposals stay `origin: "suggested"` until a person confirms them.
144
+
145
+ ---
146
+
147
+ ## 9. Validation
148
+
149
+ `api-binding generate` fails, and writes nothing, on any error:
150
+
151
+ - an id that breaks the format, is duplicated, or clashes with a screen id; a component under no screen
152
+ - an unknown kind, or a parent that is not registered
153
+ - an expression reading a slot that is not produced on the screen, an undeclared context, a later step, or `$item` where there is none
154
+ - an operation not in the docs, a request input the operation does not have, an unconnected path parameter
155
+ - two components whose names become the same TypeScript key
156
+
157
+ Warnings: drafts, unconnected required body fields, and with `--src` every registered id missing from the source (and every `componentId` literal not registered). `--check` exits 1 when generated files are out of date.
158
+
159
+ ---
160
+
161
+ ## 10. Studio rules
162
+
163
+ - Runs from the Vite dev plugin (`api_binding_ui/vite`, serve only) or the CLI (`api-binding studio`). Either server reads `registry.json`, reads and writes `bindings.json`, and fetches the docs server-side, so the browser needs no CORS access.
164
+ - Under the Vite plugin every save regenerates the screen hooks, so a confirmed binding is live in the running app after the save. Vite does not watch `bindings.json`. On start it also generates the API client (the app's `orval.config.ts`) and the hooks, so a project needs no committed generated code.
165
+ - The dev plugin's floating button opens the studio over the app on the screen shown: the screen most `data-component-id` values the user can see belong to; a component covered by any layer above it does not count. Alt+click opens it on one component. Nothing reaches a build.
166
+ - Navigation and component lists come from the ids. Icons come from kinds.
167
+ - Three slot editors, one per role, generated from slot schemas. A new kind never needs studio code.
168
+ - Shows only what validates.
169
+
170
+ ---
171
+
172
+ ## 11. Conformance rules for an app
173
+
174
+ 1. Every bindable component has a `componentId` in the format above, registered in `registry.json`.
175
+ 2. Each screen calls its generated `use<Screen>Api(state)` once, passes what its components produce and what it was opened with, and renders `bound ? data : <its own value>`.
176
+ 3. No direct API calls in screens other than through generated hooks; no hardcoded data an API should provide.
177
+ 4. Opening, closing, navigation, toasts and reloads stay in the app's UI code.
178
+ 5. Orval output and screen hooks are regenerated, never edited by hand.