api_binding_ui 1.1.0 → 2.0.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/README.md +74 -394
- package/SKILL.md +50 -350
- package/dist/framework/contract.d.ts +268 -0
- package/dist/framework/engine.d.ts +88 -0
- package/dist/framework/expr.d.ts +34 -0
- package/dist/framework/loadCatalog.d.ts +6 -0
- package/dist/framework/names.d.ts +7 -0
- package/dist/framework/openapi.d.ts +7 -0
- package/dist/framework/runner.d.ts +36 -0
- package/dist/framework/schema.d.ts +22 -0
- package/dist/framework/store.d.ts +17 -0
- package/dist/framework/validate.d.ts +7 -0
- package/dist/index-3q1vx729.js +759 -0
- package/dist/index-3q1vx729.js.map +16 -0
- package/dist/index.d.ts +11 -29
- package/dist/index.js +348 -11932
- package/dist/index.js.map +14 -0
- package/dist/runtime/BindingProvider.d.ts +42 -0
- package/dist/runtime/BindingScreen.d.ts +23 -0
- package/dist/runtime/hooks.d.ts +22 -0
- package/dist/runtime/screenStore.d.ts +7 -0
- package/dist/studio/ApiPicker.d.ts +15 -0
- package/dist/studio/BindingStudio.d.ts +21 -0
- package/dist/studio/BindingWidget.d.ts +9 -0
- package/dist/studio/Collapse.d.ts +6 -0
- package/dist/studio/ComponentList.d.ts +10 -0
- package/dist/studio/FindAnything.d.ts +7 -0
- package/dist/studio/Inspector.d.ts +12 -0
- package/dist/studio/Navigator.d.ts +17 -0
- package/dist/studio/SettingsPanel.d.ts +15 -0
- package/dist/studio/Tip.d.ts +23 -0
- package/dist/studio/editors/ConsumeEditor.d.ts +4 -0
- package/dist/studio/editors/RequestInputs.d.ts +11 -0
- package/dist/studio/editors/ScopedEditor.d.ts +3 -0
- package/dist/studio/editors/SlotSection.d.ts +3 -0
- package/dist/studio/editors/SourceSelect.d.ts +10 -0
- package/dist/studio/editors/StepCard.d.ts +12 -0
- package/dist/studio/editors/TriggerEditor.d.ts +4 -0
- package/dist/studio/editors/types.d.ts +14 -0
- package/dist/studio/icons.d.ts +65 -0
- package/dist/studio/index.css +2021 -0
- package/dist/studio/index.d.ts +3 -0
- package/dist/studio/index.js +2806 -0
- package/dist/studio/index.js.map +33 -0
- package/dist/studio/useBindings.d.ts +20 -0
- package/dist/studio/useCatalog.d.ts +15 -0
- package/dist/studio/useGlide.d.ts +11 -0
- package/dist/studio/useSettings.d.ts +8 -0
- package/package.json +20 -14
- package/spec/FRAMEWORK.md +271 -0
- package/dist/api-binding-ui.css +0 -248
- package/dist/components/ApiBindingWorkspace.d.ts +0 -10
- package/dist/components/ApiMappings.d.ts +0 -21
- package/dist/components/BindApiButton.d.ts +0 -41
- package/dist/components/RequestPreview.d.ts +0 -7
- package/dist/components/SimulationDialog.d.ts +0 -8
- package/dist/components/ValidationDialog.d.ts +0 -8
- package/dist/components/WorkspaceContext.d.ts +0 -7
- package/dist/components/ui/AppGrid.d.ts +0 -27
- package/dist/components/ui/Badge.d.ts +0 -9
- package/dist/components/ui/Banner.d.ts +0 -8
- package/dist/components/ui/Button.d.ts +0 -31
- package/dist/components/ui/Card.d.ts +0 -14
- package/dist/components/ui/Dialog.d.ts +0 -61
- package/dist/components/ui/Icons.d.ts +0 -21
- package/dist/components/ui/Input.d.ts +0 -31
- package/dist/core/bindingContext.d.ts +0 -67
- package/dist/core/catalog.d.ts +0 -18
- package/dist/core/componentId.d.ts +0 -12
- package/dist/core/model.d.ts +0 -63
- package/dist/core/openapi.d.ts +0 -2
- package/dist/core/transport.d.ts +0 -33
- package/dist/core/validation.d.ts +0 -2
- package/dist/core/valueCoercion.d.ts +0 -17
- package/dist/hooks/useAppliedBinding.d.ts +0 -6
- package/dist/index.css +0 -248
- package/dist/style.css +0 -244
- package/dist/types.d.ts +0 -245
package/SKILL.md
CHANGED
|
@@ -1,350 +1,50 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: api-binding-ui
|
|
3
|
-
description: "Use when
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
read: [],
|
|
52
|
-
write: [{
|
|
53
|
-
component: componentId({ kind: "button", name: "Submit" }),
|
|
54
|
-
api: "/cls/v1/notes",
|
|
55
|
-
method: "POST",
|
|
56
|
-
params: [
|
|
57
|
-
{ param: "noteSubject", in: "body", source: "subject", property: "subject" },
|
|
58
|
-
{ param: "noteDescription", in: "body", source: "notes", property: "notes" },
|
|
59
|
-
{ param: "noteTypeId", in: "body", source: "notesType", property: "notesType" },
|
|
60
|
-
{ param: "customerId", in: "body", source: "worklistUIGrid", property: "worklistUIGrid.customerId" },
|
|
61
|
-
],
|
|
62
|
-
}],
|
|
63
|
-
};
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
This example assumes the real button is Submit, option values are numeric note-type IDs and exactly one customer is selected. Use actual host IDs and labels. `noteType` is a separate string property from `noteTypeId`. Never rename API fields to fit a simulator, substitute a customer ID for a note ID, or fabricate lookup APIs. Multiple selected rows feed comma-joined array values; scalar parameters require one row. The package does not dispatch one live request per row.
|
|
67
|
-
|
|
68
|
-
Match APIs using component labels/names, window and parent entity, operationId, summary, description, tags and schema compatibility. POST search operations are reads. Automatically populate only high-confidence mappings; leave missing or ambiguous sources unresolved with a report. Do not invent IDs, credentials or defaults. Fields use their actual names as sources, parents use `<windowId>.<fieldName>`, and literals use the exported `FREE_TEXT_SOURCE` with the literal in `property`. Preserve `in` to distinguish same-name path/body/query/header parameters.
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## 3. Router Setup Requirements
|
|
73
|
-
|
|
74
|
-
Every application using `api_binding_ui` must declare a route matching `/api-binding/$key`.
|
|
75
|
-
|
|
76
|
-
### TanStack Router (`src/routes/api-binding.$key.tsx`)
|
|
77
|
-
```tsx
|
|
78
|
-
import { createFileRoute } from "@tanstack/react-router";
|
|
79
|
-
import { ApiBindingWorkspace } from "api_binding_ui";
|
|
80
|
-
import "api_binding_ui/style.css";
|
|
81
|
-
|
|
82
|
-
export const Route = createFileRoute("/api-binding/$key")({
|
|
83
|
-
head: () => ({
|
|
84
|
-
meta: [{ title: "API Binding Studio" }],
|
|
85
|
-
}),
|
|
86
|
-
component: ApiBindingRouteView,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
function ApiBindingRouteView() {
|
|
90
|
-
const { key } = Route.useParams();
|
|
91
|
-
return <ApiBindingWorkspace key={key} pageKey={key} />;
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### React Router (`src/routes/ApiBindingRoute.tsx`)
|
|
96
|
-
```tsx
|
|
97
|
-
import { useParams } from "react-router-dom";
|
|
98
|
-
import { ApiBindingWorkspace } from "api_binding_ui";
|
|
99
|
-
import "api_binding_ui/style.css";
|
|
100
|
-
|
|
101
|
-
export function ApiBindingRoute() {
|
|
102
|
-
const { key = "default" } = useParams<{ key: string }>();
|
|
103
|
-
return <ApiBindingWorkspace key={key} pageKey={key} />;
|
|
104
|
-
}
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Register this component explicitly in the existing React Router tree with `<Route path="/api-binding/:key" element={<ApiBindingRoute />} />`; the component alone does not register a route. Do not add a second BrowserRouter. For TanStack, follow the host's existing file/code route conventions. Import package CSS once. The studio opens a named same-origin tab inside the host app; verify popup permissions, direct-route refresh and deployment fallback routing.
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## 4. Host Form Integration Rules
|
|
112
|
-
|
|
113
|
-
When rendering forms, modals, or drawers that will be bound to APIs, AI agents MUST follow these structural rules:
|
|
114
|
-
|
|
115
|
-
### Rule 1: Form Inputs Must Have Identifiers
|
|
116
|
-
Every form control (`<input>`, `<select>`, `<textarea>`, or custom combobox) MUST have a `name` attribute matching the semantic field name (or an `id`, `data-testid`, or `aria-label`).
|
|
117
|
-
- Correct: `<input name="subject" required />`
|
|
118
|
-
- Incorrect: `<input placeholder="Subject" />` (lacks identifier, will be ignored by DOM discovery).
|
|
119
|
-
|
|
120
|
-
### Rule 2: Explicit Labels
|
|
121
|
-
Controls should be paired with standard `<label htmlFor="...">` tags or include `aria-label`. The studio extracts human-readable field labels from these associations.
|
|
122
|
-
|
|
123
|
-
### Rule 3: Pass Dropdown Options via `options` Prop
|
|
124
|
-
Dropdown choices captured in state or constants must be passed to `<BindApiButton />` using the `options` prop:
|
|
125
|
-
```tsx
|
|
126
|
-
const noteTypeOptions = [
|
|
127
|
-
{ value: "1", label: "General Follow-up" },
|
|
128
|
-
{ value: "2", label: "Promise to Pay" },
|
|
129
|
-
];
|
|
130
|
-
|
|
131
|
-
const templateOptions = [
|
|
132
|
-
{
|
|
133
|
-
value: "tmpl-1",
|
|
134
|
-
label: "Payment Reminder",
|
|
135
|
-
subject: "Payment Reminder",
|
|
136
|
-
description: "Followed up with customer regarding pending invoices.",
|
|
137
|
-
},
|
|
138
|
-
];
|
|
139
|
-
|
|
140
|
-
<BindApiButton
|
|
141
|
-
// ...
|
|
142
|
-
options={{
|
|
143
|
-
notesType: noteTypeOptions,
|
|
144
|
-
template: templateOptions,
|
|
145
|
-
}}
|
|
146
|
-
/>
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### Rule 4: Pass Grid Selection when Targeting Grid Rows
|
|
150
|
-
If the action is triggered from or operates on selected table/grid rows:
|
|
151
|
-
```tsx
|
|
152
|
-
<BindApiButton
|
|
153
|
-
// ...
|
|
154
|
-
grid={{
|
|
155
|
-
gridId: "worklistUIGrid",
|
|
156
|
-
rows: selectedRecords,
|
|
157
|
-
}}
|
|
158
|
-
/>
|
|
159
|
-
```
|
|
160
|
-
If the screen does not operate on a grid (such as a standalone dispute creation screen or customer profile editor), omit the `grid` prop. The studio automatically adapts its target context view and simulation engine to direct screen input.
|
|
161
|
-
|
|
162
|
-
---
|
|
163
|
-
|
|
164
|
-
## 5. Complete Implementation Patterns
|
|
165
|
-
|
|
166
|
-
### Pattern A: Modal Dialog Triggered from a Data Grid
|
|
167
|
-
```tsx
|
|
168
|
-
import * as React from "react";
|
|
169
|
-
import { BindApiButton, type ApiDefinition, type MappingFile } from "api_binding_ui";
|
|
170
|
-
|
|
171
|
-
interface CustomerRecord {
|
|
172
|
-
customerId: number;
|
|
173
|
-
customerName: string;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function AddNotesModal({
|
|
177
|
-
open,
|
|
178
|
-
onClose,
|
|
179
|
-
selectedCustomers,
|
|
180
|
-
definitions,
|
|
181
|
-
mappingFile,
|
|
182
|
-
}: {
|
|
183
|
-
open: boolean;
|
|
184
|
-
onClose: () => void;
|
|
185
|
-
selectedCustomers: CustomerRecord[];
|
|
186
|
-
definitions: ApiDefinition[];
|
|
187
|
-
mappingFile: MappingFile;
|
|
188
|
-
}) {
|
|
189
|
-
if (!open) return null;
|
|
190
|
-
|
|
191
|
-
const noteTypeOptions = [
|
|
192
|
-
{ value: "1", label: "General Follow-up" },
|
|
193
|
-
{ value: "2", label: "Promise to Pay" },
|
|
194
|
-
];
|
|
195
|
-
|
|
196
|
-
const templateOptions = [
|
|
197
|
-
{
|
|
198
|
-
value: "tmpl-payment",
|
|
199
|
-
label: "Payment Reminder",
|
|
200
|
-
subject: "Reminder: Invoice due",
|
|
201
|
-
description: "Contacted customer regarding pending balance.",
|
|
202
|
-
},
|
|
203
|
-
];
|
|
204
|
-
|
|
205
|
-
return (
|
|
206
|
-
<div role="dialog" aria-labelledby="dialog-title" className="ds-dialog">
|
|
207
|
-
<h2 id="dialog-title">Add Notes</h2>
|
|
208
|
-
|
|
209
|
-
<div className="form-group">
|
|
210
|
-
<label htmlFor="notesType">Notes Type</label>
|
|
211
|
-
<select id="notesType" name="notesType" required>
|
|
212
|
-
{noteTypeOptions.map((opt) => (
|
|
213
|
-
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
214
|
-
))}
|
|
215
|
-
</select>
|
|
216
|
-
</div>
|
|
217
|
-
|
|
218
|
-
<div className="form-group">
|
|
219
|
-
<label htmlFor="subject">Subject</label>
|
|
220
|
-
<input id="subject" name="subject" required />
|
|
221
|
-
</div>
|
|
222
|
-
|
|
223
|
-
<div className="form-group">
|
|
224
|
-
<label htmlFor="notes">Description</label>
|
|
225
|
-
<textarea id="notes" name="notes" required />
|
|
226
|
-
</div>
|
|
227
|
-
|
|
228
|
-
<div className="button-bar">
|
|
229
|
-
<button type="button" onClick={onClose}>Cancel</button>
|
|
230
|
-
<button type="submit">Submit</button>
|
|
231
|
-
|
|
232
|
-
<BindApiButton
|
|
233
|
-
definitions={definitions}
|
|
234
|
-
mappingFile={mappingFile}
|
|
235
|
-
appId="collections"
|
|
236
|
-
windowId="worklist-add-notes"
|
|
237
|
-
source="Add Notes"
|
|
238
|
-
parent="Worklist Grid"
|
|
239
|
-
grid={{
|
|
240
|
-
gridId: "worklistUIGrid",
|
|
241
|
-
rows: selectedCustomers,
|
|
242
|
-
}}
|
|
243
|
-
options={{
|
|
244
|
-
notesType: noteTypeOptions,
|
|
245
|
-
template: templateOptions,
|
|
246
|
-
}}
|
|
247
|
-
/>
|
|
248
|
-
</div>
|
|
249
|
-
</div>
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
### Pattern B: Standalone Drawer or Form without Grid
|
|
255
|
-
```tsx
|
|
256
|
-
import * as React from "react";
|
|
257
|
-
import { BindApiButton, type ApiDefinition, type MappingFile } from "api_binding_ui";
|
|
258
|
-
|
|
259
|
-
export function DisputeDrawer({ open, onClose, definitions, mappingFile }: { open: boolean; onClose: () => void; definitions: ApiDefinition[]; mappingFile: MappingFile }) {
|
|
260
|
-
if (!open) return null;
|
|
261
|
-
|
|
262
|
-
const disputeReasons = [
|
|
263
|
-
{ value: "DAMAGED", label: "Damaged Goods" },
|
|
264
|
-
{ value: "PRICING", label: "Incorrect Pricing" },
|
|
265
|
-
];
|
|
266
|
-
|
|
267
|
-
return (
|
|
268
|
-
<aside className="drawer-container" aria-label="Create Dispute">
|
|
269
|
-
<h2>Create Dispute</h2>
|
|
270
|
-
|
|
271
|
-
<form id="dispute-form">
|
|
272
|
-
<label htmlFor="reasonCode">Dispute Reason</label>
|
|
273
|
-
<select id="reasonCode" name="reasonCode" required>
|
|
274
|
-
{disputeReasons.map((r) => (
|
|
275
|
-
<option key={r.value} value={r.value}>{r.label}</option>
|
|
276
|
-
))}
|
|
277
|
-
</select>
|
|
278
|
-
|
|
279
|
-
<label htmlFor="amount">Disputed Amount</label>
|
|
280
|
-
<input id="amount" name="amount" type="number" required />
|
|
281
|
-
|
|
282
|
-
<label htmlFor="comments">Comments</label>
|
|
283
|
-
<textarea id="comments" name="comments" />
|
|
284
|
-
|
|
285
|
-
<div className="drawer-actions">
|
|
286
|
-
<button type="button" onClick={onClose}>Cancel</button>
|
|
287
|
-
<button type="submit">Submit Dispute</button>
|
|
288
|
-
|
|
289
|
-
<BindApiButton
|
|
290
|
-
definitions={definitions}
|
|
291
|
-
mappingFile={mappingFile}
|
|
292
|
-
appId="receivables"
|
|
293
|
-
windowId="disputes-create"
|
|
294
|
-
source="Create Dispute"
|
|
295
|
-
parent="Disputes Management"
|
|
296
|
-
options={{
|
|
297
|
-
reasonCode: disputeReasons,
|
|
298
|
-
}}
|
|
299
|
-
/>
|
|
300
|
-
</div>
|
|
301
|
-
</form>
|
|
302
|
-
</aside>
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
---
|
|
308
|
-
|
|
309
|
-
## 6. Listening to Applied Configurations
|
|
310
|
-
|
|
311
|
-
When the user validates requests and applies a binding, the configuration is stored in browser storage. `useAppliedBinding` only observes it: it does not submit requests, populate fields from responses, or write repository files. Preserve existing host API clients/runtime behavior. Live execution is a separate integration task.
|
|
312
|
-
|
|
313
|
-
```tsx
|
|
314
|
-
import * as React from "react";
|
|
315
|
-
import { useAppliedBinding } from "api_binding_ui";
|
|
316
|
-
|
|
317
|
-
export function ScreenComponent() {
|
|
318
|
-
const { isApplied, appliedConfig } = useAppliedBinding(
|
|
319
|
-
"collections.worklistUIGrid.worklist-add-notes"
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
React.useEffect(() => {
|
|
323
|
-
if (isApplied && appliedConfig) {
|
|
324
|
-
// Configuration applied: access appliedConfig.calls or appliedConfig.fields
|
|
325
|
-
}
|
|
326
|
-
}, [isApplied, appliedConfig]);
|
|
327
|
-
}
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
---
|
|
331
|
-
|
|
332
|
-
## 7. Agent Verification Checklist
|
|
333
|
-
|
|
334
|
-
Before completing an integration task, the AI agent must verify:
|
|
335
|
-
1. `name` attributes are assigned to all form controls on the host screen.
|
|
336
|
-
2. The `<BindApiButton />` has accurate `appId`, `windowId`, `source`, and `parent` strings.
|
|
337
|
-
3. Dropdown choices are forwarded in the `options` record.
|
|
338
|
-
4. The application router has an active route at `/api-binding/$key` mounting `<ApiBindingWorkspace />`.
|
|
339
|
-
5. No emojis or informal characters are introduced into the code or documentation.
|
|
340
|
-
6. Every trigger supplies the contract `definitions` and its own proposed/saved `mappingFile`. Existing browser drafts take precedence over seeds; never erase all browser storage to refresh defaults.
|
|
341
|
-
7. Portal/custom controls are scoped to their actual owner using `containerRef`, `containerSelector`, `rootElement` or `formId`. Custom widgets expose their real values through `name`/`data-value` or a complete `components` override; `components` and `buttons` replace, not append to, discovery.
|
|
342
|
-
8. Pass actual dropdown options and live selected rows. Use `notifySelectionUpdate(pageKey, { id: gridId, rows })` when selection changes while the studio is open. Forward `parents`, `kind` and `trigger` through host wrappers.
|
|
343
|
-
9. Validate all configured read/write requests through the generic Validate dialog, including read-only mappings and non-Submit buttons. Offline validation covers request constraints, not network access, authorization, responses or business success. Legacy `simulateSave`/`SimulationDialog` remain note-specific exports and are not the workspace's validation flow.
|
|
344
|
-
10. Config, contract and captured source changes require revalidation; existing draft mappings survive the upgrade but old verification tokens do not authorize Apply. Never manually forge applied storage or signatures.
|
|
345
|
-
11. Do not capture passwords, tokens or secrets in snapshots/options/mappings. Pass an explicit safe component list for sensitive forms and keep authentication in the existing secure client. Browser snapshots, selections and drafts contain captured data and are not shared server-side persistence.
|
|
346
|
-
12. Run host tests/typecheck/build and browser-check every binding trigger, selected-row updates, standalone forms, similar window names, popup restrictions, existing drafts, metadata and desktop/mobile layout. Confirm that opening, previewing and validating send no API request.
|
|
347
|
-
|
|
348
|
-
## Migration From api-binding-studio
|
|
349
|
-
|
|
350
|
-
The older wrapper's `apiInfoList`, `onSave` and `grid.id` are not new-button props. Replace them with `definitions`, `mappingFile` and `grid.gridId`. The package renders its own button: do not nest another Button inside it. A browser Apply does not replace a server-side source-file save handler. Preserve or explicitly report legacy file persistence/runtime behavior rather than claiming it was migrated automatically. Prepare the host changes inside the Lovable app only; do not copy this package's studio code.
|
|
1
|
+
---
|
|
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."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# api_binding_ui: rules for agents
|
|
7
|
+
|
|
8
|
+
Contract: `node_modules/api_binding_ui/spec/FRAMEWORK.md`. Types: `import type { … } from "api_binding_ui"`.
|
|
9
|
+
|
|
10
|
+
## Never
|
|
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.
|
|
19
|
+
|
|
20
|
+
## Always
|
|
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>" }] }
|
|
27
|
+
```
|
|
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.
|
|
44
|
+
|
|
45
|
+
## New screen checklist
|
|
46
|
+
|
|
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
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/** Binding framework contract v1. Semantics: spec/FRAMEWORK.md. */
|
|
2
|
+
/** JSON Schema (2020-12). */
|
|
3
|
+
export type JsonSchema = boolean | {
|
|
4
|
+
[keyword: string]: unknown;
|
|
5
|
+
};
|
|
6
|
+
/** Namespaced component kind, e.g. "acme-ds/grid". */
|
|
7
|
+
export type KindId = string;
|
|
8
|
+
/** Component instance id, unique within its screen. Letters, digits, - and _; no dots. */
|
|
9
|
+
export type BindId = string;
|
|
10
|
+
/** Screen id, e.g. "<product>.<area>.<screen>". */
|
|
11
|
+
export type ScreenId = string;
|
|
12
|
+
/** Expression string; grammar in FRAMEWORK.md §7. */
|
|
13
|
+
export type Expr = string;
|
|
14
|
+
/** RFC 6901 JSON Pointer into a request: "/path/<name>", "/query/<name>", "/header/<name>", "/body/<pointer>". */
|
|
15
|
+
export type RequestPointer = string;
|
|
16
|
+
/** Selector into a response, e.g. "$", "$.items[*]". */
|
|
17
|
+
export type ResponseSelector = string;
|
|
18
|
+
export interface Labelled {
|
|
19
|
+
label: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
}
|
|
22
|
+
/** consume: needs data. produce: exposes data. trigger: emits an event that runs API calls. */
|
|
23
|
+
export type SlotRole = "consume" | "produce" | "trigger";
|
|
24
|
+
export interface SlotDescriptor extends Labelled {
|
|
25
|
+
id: string;
|
|
26
|
+
role: SlotRole;
|
|
27
|
+
/**
|
|
28
|
+
* consume: shape the component needs (per item when `many`).
|
|
29
|
+
* produce: shape the component provides (per item when `many`).
|
|
30
|
+
* trigger: payload the event carries.
|
|
31
|
+
*/
|
|
32
|
+
schema: JsonSchema;
|
|
33
|
+
many?: boolean;
|
|
34
|
+
/** The screen is incomplete until this slot is bound. */
|
|
35
|
+
required?: boolean;
|
|
36
|
+
/** Evaluated per item of a parent slot (e.g. a cell in a row); `$item` is that item. */
|
|
37
|
+
scope?: {
|
|
38
|
+
parentSlot: string;
|
|
39
|
+
};
|
|
40
|
+
/** Produce slots of the same component that may feed the request (paging, sort). */
|
|
41
|
+
feeds?: string[];
|
|
42
|
+
/** Matching hints from the component author. */
|
|
43
|
+
hints?: MatchHints;
|
|
44
|
+
}
|
|
45
|
+
export interface MatchHints {
|
|
46
|
+
/** Words describing the slot, for name matching. */
|
|
47
|
+
keywords?: string[];
|
|
48
|
+
/** Operation intents this slot usually binds to. */
|
|
49
|
+
intents?: string[];
|
|
50
|
+
}
|
|
51
|
+
export interface ChildPolicy {
|
|
52
|
+
/** Kinds allowed as children; omit for any. */
|
|
53
|
+
kinds?: KindId[];
|
|
54
|
+
/** Parent slot children are scoped to (e.g. columns to rows). */
|
|
55
|
+
scopedTo?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface ComponentDescriptor extends Labelled {
|
|
58
|
+
kind: KindId;
|
|
59
|
+
/** Bump on incompatible slot changes; manifests record it. */
|
|
60
|
+
version: number;
|
|
61
|
+
slots: SlotDescriptor[];
|
|
62
|
+
children?: ChildPolicy;
|
|
63
|
+
/** SVG markup. */
|
|
64
|
+
icon?: string;
|
|
65
|
+
}
|
|
66
|
+
export interface ComponentInstance {
|
|
67
|
+
bindId: BindId;
|
|
68
|
+
kind: KindId;
|
|
69
|
+
kindVersion: number;
|
|
70
|
+
label: string;
|
|
71
|
+
/** Parent component's bindId. */
|
|
72
|
+
parent?: BindId;
|
|
73
|
+
/** Display-only configuration; never used for matching. */
|
|
74
|
+
info?: Record<string, string>;
|
|
75
|
+
}
|
|
76
|
+
export interface ContextDeclaration extends Labelled {
|
|
77
|
+
/** Name used in expressions: `$context.<id>`. */
|
|
78
|
+
id: string;
|
|
79
|
+
/** A schema, or another screen's produce slot whose bound shape it follows. */
|
|
80
|
+
shape: {
|
|
81
|
+
schema: JsonSchema;
|
|
82
|
+
} | {
|
|
83
|
+
from: {
|
|
84
|
+
screen: ScreenId;
|
|
85
|
+
component: BindId;
|
|
86
|
+
slot: string;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
many?: boolean;
|
|
90
|
+
}
|
|
91
|
+
export interface ScreenEntry extends Labelled {
|
|
92
|
+
id: ScreenId;
|
|
93
|
+
/** Navigation path; the first segment is the product. */
|
|
94
|
+
path: string[];
|
|
95
|
+
/** Screen this one opens over (dialog, drawer). Grouping only. */
|
|
96
|
+
parent?: ScreenId;
|
|
97
|
+
context: ContextDeclaration[];
|
|
98
|
+
components: ComponentInstance[];
|
|
99
|
+
}
|
|
100
|
+
export interface ScreenRegistry {
|
|
101
|
+
registryVersion: 1;
|
|
102
|
+
screens: ScreenEntry[];
|
|
103
|
+
}
|
|
104
|
+
export interface CatalogParameter {
|
|
105
|
+
pointer: RequestPointer;
|
|
106
|
+
name: string;
|
|
107
|
+
required: boolean;
|
|
108
|
+
schema: JsonSchema;
|
|
109
|
+
description?: string;
|
|
110
|
+
}
|
|
111
|
+
export interface CatalogOperation {
|
|
112
|
+
/** operationId. */
|
|
113
|
+
id: string;
|
|
114
|
+
method: string;
|
|
115
|
+
path: string;
|
|
116
|
+
summary?: string;
|
|
117
|
+
description?: string;
|
|
118
|
+
tags: string[];
|
|
119
|
+
/** Product from the docs' `x-tagGroups`. */
|
|
120
|
+
product?: string;
|
|
121
|
+
/** "read" or "write": `x-binding-intent` if declared, else from the HTTP method. */
|
|
122
|
+
intent?: string;
|
|
123
|
+
parameters: CatalogParameter[];
|
|
124
|
+
body?: {
|
|
125
|
+
required: boolean;
|
|
126
|
+
schema: JsonSchema;
|
|
127
|
+
};
|
|
128
|
+
responses: Array<{
|
|
129
|
+
status: string;
|
|
130
|
+
schema?: JsonSchema;
|
|
131
|
+
}>;
|
|
132
|
+
/** `x-*` extensions as published. */
|
|
133
|
+
extensions: Record<string, unknown>;
|
|
134
|
+
}
|
|
135
|
+
export interface Catalog {
|
|
136
|
+
catalogVersion: 1;
|
|
137
|
+
source: {
|
|
138
|
+
title: string;
|
|
139
|
+
version: string;
|
|
140
|
+
url?: string;
|
|
141
|
+
};
|
|
142
|
+
/** Content hash; manifests pin it. */
|
|
143
|
+
hash: string;
|
|
144
|
+
operations: CatalogOperation[];
|
|
145
|
+
/** Shared schemas referenced by `$ref` inside operations. */
|
|
146
|
+
schemas: Record<string, JsonSchema>;
|
|
147
|
+
}
|
|
148
|
+
export type BindingOrigin = "manual" | "suggested";
|
|
149
|
+
export type BindingState = "draft" | "confirmed";
|
|
150
|
+
interface BindingBase {
|
|
151
|
+
component: BindId;
|
|
152
|
+
slot: string;
|
|
153
|
+
origin: BindingOrigin;
|
|
154
|
+
state: BindingState;
|
|
155
|
+
}
|
|
156
|
+
/** consume slot fed by an operation. */
|
|
157
|
+
export interface OperationConsumeBinding extends BindingBase {
|
|
158
|
+
type: "operation";
|
|
159
|
+
operation: string;
|
|
160
|
+
request: Record<RequestPointer, Expr>;
|
|
161
|
+
/** Where the slot's data is in the response. */
|
|
162
|
+
select: ResponseSelector;
|
|
163
|
+
/** Slot-schema property → expression over `$item` (when the slot declares properties). */
|
|
164
|
+
map?: Record<string, Expr>;
|
|
165
|
+
/** When to load: on open, on first use, or on an interval. */
|
|
166
|
+
load?: "open" | "demand" | {
|
|
167
|
+
everySeconds: number;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** consume slot fed by an expression (row field, another component, context). */
|
|
171
|
+
export interface ExpressionConsumeBinding extends BindingBase {
|
|
172
|
+
type: "expression";
|
|
173
|
+
value: Expr;
|
|
174
|
+
}
|
|
175
|
+
/** One API call of a trigger. UI behaviour (open, close, navigate) stays in the app. */
|
|
176
|
+
export interface Step {
|
|
177
|
+
type: "call";
|
|
178
|
+
id: string;
|
|
179
|
+
operation: string;
|
|
180
|
+
request: Record<RequestPointer, Expr>;
|
|
181
|
+
forEach?: Expr;
|
|
182
|
+
onError: "stop" | "continue";
|
|
183
|
+
}
|
|
184
|
+
/** trigger slot that calls APIs in order. */
|
|
185
|
+
export interface TriggerBinding extends BindingBase {
|
|
186
|
+
type: "steps";
|
|
187
|
+
steps: Step[];
|
|
188
|
+
}
|
|
189
|
+
/** trigger slot reviewed as calling no API. */
|
|
190
|
+
export interface NoApiBinding extends BindingBase {
|
|
191
|
+
type: "none";
|
|
192
|
+
}
|
|
193
|
+
export type SlotBinding = OperationConsumeBinding | ExpressionConsumeBinding | TriggerBinding | NoApiBinding;
|
|
194
|
+
export interface BindingManifest {
|
|
195
|
+
$schema?: string;
|
|
196
|
+
manifestVersion: 1;
|
|
197
|
+
screen: ScreenId;
|
|
198
|
+
catalog: {
|
|
199
|
+
hash: string;
|
|
200
|
+
};
|
|
201
|
+
/** Descriptor versions the bindings were made against. */
|
|
202
|
+
kinds: Record<KindId, number>;
|
|
203
|
+
bindings: SlotBinding[];
|
|
204
|
+
}
|
|
205
|
+
/** A named, typed value transform usable as `expr | name(args)`. */
|
|
206
|
+
export interface TransformDefinition extends Labelled {
|
|
207
|
+
name: string;
|
|
208
|
+
input: JsonSchema;
|
|
209
|
+
output: JsonSchema;
|
|
210
|
+
apply: (value: unknown, ...args: unknown[]) => unknown;
|
|
211
|
+
}
|
|
212
|
+
/** Supplies `$<id>` values (e.g. the signed-in user). Registered by the host app. */
|
|
213
|
+
export interface ContextProvider extends Labelled {
|
|
214
|
+
id: string;
|
|
215
|
+
schema: JsonSchema;
|
|
216
|
+
read: () => unknown;
|
|
217
|
+
}
|
|
218
|
+
export interface MatchInput {
|
|
219
|
+
screen: ScreenEntry;
|
|
220
|
+
component: ComponentInstance;
|
|
221
|
+
descriptor: ComponentDescriptor;
|
|
222
|
+
slot: SlotDescriptor;
|
|
223
|
+
operation: CatalogOperation;
|
|
224
|
+
catalog: Catalog;
|
|
225
|
+
}
|
|
226
|
+
export interface MatchResult {
|
|
227
|
+
/** 0..1 */
|
|
228
|
+
score: number;
|
|
229
|
+
/** Reasons shown in the studio. */
|
|
230
|
+
reasons: string[];
|
|
231
|
+
/** Proposed request/map entries, if the matcher can propose them. */
|
|
232
|
+
proposal?: Partial<Pick<OperationConsumeBinding, "request" | "select" | "map">> | Partial<Pick<TriggerBinding, "steps">>;
|
|
233
|
+
}
|
|
234
|
+
/** Scores one operation for one slot; matchers combine by weight. */
|
|
235
|
+
export interface Matcher {
|
|
236
|
+
id: string;
|
|
237
|
+
weight: number;
|
|
238
|
+
score: (input: MatchInput) => MatchResult | null;
|
|
239
|
+
}
|
|
240
|
+
/** Per-product synonyms, loaded as data. */
|
|
241
|
+
export interface Glossary {
|
|
242
|
+
product: string;
|
|
243
|
+
/** Groups of words that mean the same thing in this product. */
|
|
244
|
+
synonyms: string[][];
|
|
245
|
+
}
|
|
246
|
+
export interface PersistenceAdapter {
|
|
247
|
+
load: (screen: ScreenId) => Promise<BindingManifest | null>;
|
|
248
|
+
save: (manifest: BindingManifest) => Promise<void>;
|
|
249
|
+
}
|
|
250
|
+
export interface FrameworkRegistry {
|
|
251
|
+
registerDescriptor: (descriptor: ComponentDescriptor) => void;
|
|
252
|
+
registerTransform: (transform: TransformDefinition) => void;
|
|
253
|
+
registerContext: (provider: ContextProvider) => void;
|
|
254
|
+
registerMatcher: (matcher: Matcher) => void;
|
|
255
|
+
registerGlossary: (glossary: Glossary) => void;
|
|
256
|
+
setPersistence: (adapter: PersistenceAdapter) => void;
|
|
257
|
+
}
|
|
258
|
+
export type IssueSeverity = "error" | "warning";
|
|
259
|
+
export interface ValidationIssue {
|
|
260
|
+
severity: IssueSeverity;
|
|
261
|
+
screen: ScreenId;
|
|
262
|
+
component?: BindId;
|
|
263
|
+
slot?: string;
|
|
264
|
+
/** Stable machine code, e.g. "slot.required-unbound", "schema.incompatible", "catalog.operation-missing". */
|
|
265
|
+
code: string;
|
|
266
|
+
message: string;
|
|
267
|
+
}
|
|
268
|
+
export {};
|