api_binding_ui 1.0.0 → 2.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.
Files changed (75) hide show
  1. package/README.md +74 -384
  2. package/SKILL.md +49 -294
  3. package/dist/framework/contract.d.ts +268 -0
  4. package/dist/framework/engine.d.ts +88 -0
  5. package/dist/framework/expr.d.ts +34 -0
  6. package/dist/framework/loadCatalog.d.ts +6 -0
  7. package/dist/framework/names.d.ts +7 -0
  8. package/dist/framework/openapi.d.ts +7 -0
  9. package/dist/framework/runner.d.ts +33 -0
  10. package/dist/framework/schema.d.ts +22 -0
  11. package/dist/framework/store.d.ts +17 -0
  12. package/dist/framework/validate.d.ts +7 -0
  13. package/dist/index-e9twc25y.js +754 -0
  14. package/dist/index-e9twc25y.js.map +16 -0
  15. package/dist/index.d.ts +11 -26
  16. package/dist/index.js +345 -3790
  17. package/dist/index.js.map +14 -0
  18. package/dist/runtime/BindingProvider.d.ts +42 -0
  19. package/dist/runtime/BindingScreen.d.ts +23 -0
  20. package/dist/runtime/hooks.d.ts +22 -0
  21. package/dist/runtime/screenStore.d.ts +7 -0
  22. package/dist/studio/ApiPicker.d.ts +15 -0
  23. package/dist/studio/BindingStudio.d.ts +21 -0
  24. package/dist/studio/BindingWidget.d.ts +9 -0
  25. package/dist/studio/Collapse.d.ts +6 -0
  26. package/dist/studio/ComponentList.d.ts +10 -0
  27. package/dist/studio/FindAnything.d.ts +7 -0
  28. package/dist/studio/Inspector.d.ts +12 -0
  29. package/dist/studio/Navigator.d.ts +17 -0
  30. package/dist/studio/SettingsPanel.d.ts +15 -0
  31. package/dist/studio/Tip.d.ts +23 -0
  32. package/dist/studio/editors/ConsumeEditor.d.ts +4 -0
  33. package/dist/studio/editors/RequestInputs.d.ts +11 -0
  34. package/dist/studio/editors/ScopedEditor.d.ts +3 -0
  35. package/dist/studio/editors/SlotSection.d.ts +3 -0
  36. package/dist/studio/editors/SourceSelect.d.ts +10 -0
  37. package/dist/studio/editors/StepCard.d.ts +12 -0
  38. package/dist/studio/editors/TriggerEditor.d.ts +4 -0
  39. package/dist/studio/editors/types.d.ts +14 -0
  40. package/dist/studio/icons.d.ts +65 -0
  41. package/dist/studio/index.css +2021 -0
  42. package/dist/studio/index.d.ts +3 -0
  43. package/dist/studio/index.js +2806 -0
  44. package/dist/studio/index.js.map +33 -0
  45. package/dist/studio/useBindings.d.ts +20 -0
  46. package/dist/studio/useCatalog.d.ts +15 -0
  47. package/dist/studio/useGlide.d.ts +11 -0
  48. package/dist/studio/useSettings.d.ts +8 -0
  49. package/package.json +20 -9
  50. package/spec/FRAMEWORK.md +271 -0
  51. package/dist/api-binding-ui.css +0 -244
  52. package/dist/components/ApiBindingWorkspace.d.ts +0 -10
  53. package/dist/components/ApiMappings.d.ts +0 -21
  54. package/dist/components/BindApiButton.d.ts +0 -41
  55. package/dist/components/RequestPreview.d.ts +0 -7
  56. package/dist/components/SimulationDialog.d.ts +0 -8
  57. package/dist/components/WorkspaceContext.d.ts +0 -7
  58. package/dist/components/ui/AppGrid.d.ts +0 -27
  59. package/dist/components/ui/Badge.d.ts +0 -9
  60. package/dist/components/ui/Banner.d.ts +0 -8
  61. package/dist/components/ui/Button.d.ts +0 -31
  62. package/dist/components/ui/Card.d.ts +0 -14
  63. package/dist/components/ui/Dialog.d.ts +0 -61
  64. package/dist/components/ui/Icons.d.ts +0 -21
  65. package/dist/components/ui/Input.d.ts +0 -31
  66. package/dist/core/bindingContext.d.ts +0 -67
  67. package/dist/core/catalog.d.ts +0 -18
  68. package/dist/core/componentId.d.ts +0 -12
  69. package/dist/core/model.d.ts +0 -57
  70. package/dist/core/transport.d.ts +0 -33
  71. package/dist/core/valueCoercion.d.ts +0 -14
  72. package/dist/hooks/useAppliedBinding.d.ts +0 -6
  73. package/dist/index.css +0 -244
  74. package/dist/style.css +0 -244
  75. package/dist/types.d.ts +0 -232
@@ -0,0 +1,2806 @@
1
+ import {
2
+ catalogFromOpenApi2,
3
+ localStorageAdapter2,
4
+ toManifest2,
5
+ markDirty,
6
+ savePending,
7
+ loadAll,
8
+ putBinding,
9
+ dropBinding,
10
+ parseManifests,
11
+ humanize,
12
+ createEngine2,
13
+ validateRegistry2,
14
+ useBindingRuntime
15
+ } from "../index-e9twc25y.js";
16
+
17
+ // src/studio/BindingStudio.tsx
18
+ import * as React15 from "react";
19
+
20
+ // src/studio/ComponentList.tsx
21
+ import * as React2 from "react";
22
+
23
+ // src/studio/Collapse.tsx
24
+ import { jsx } from "react/jsx-runtime";
25
+ function Collapse({ open, children }) {
26
+ return /* @__PURE__ */ jsx("div", {
27
+ className: "abs-collapse" + (open ? " is-open" : ""),
28
+ "aria-hidden": !open,
29
+ children: /* @__PURE__ */ jsx("div", {
30
+ className: "abs-collapse-in",
31
+ children
32
+ })
33
+ });
34
+ }
35
+
36
+ // src/studio/icons.tsx
37
+ import { jsx as jsx2, jsxs, Fragment } from "react/jsx-runtime";
38
+ var paths = {
39
+ search: /* @__PURE__ */ jsxs(Fragment, {
40
+ children: [
41
+ /* @__PURE__ */ jsx2("circle", {
42
+ cx: "7.25",
43
+ cy: "7.25",
44
+ r: "4.5"
45
+ }),
46
+ /* @__PURE__ */ jsx2("path", {
47
+ d: "m13.5 13.5-2.9-2.9"
48
+ })
49
+ ]
50
+ }),
51
+ check: /* @__PURE__ */ jsx2("path", {
52
+ d: "m3.5 8.25 3 3 6-6.5"
53
+ }),
54
+ bound: /* @__PURE__ */ jsxs(Fragment, {
55
+ children: [
56
+ /* @__PURE__ */ jsx2("circle", {
57
+ cx: "8",
58
+ cy: "8",
59
+ r: "6"
60
+ }),
61
+ /* @__PURE__ */ jsx2("path", {
62
+ d: "m5.6 8.1 1.7 1.7 3.2-3.4"
63
+ })
64
+ ]
65
+ }),
66
+ suggested: /* @__PURE__ */ jsxs(Fragment, {
67
+ children: [
68
+ /* @__PURE__ */ jsx2("circle", {
69
+ cx: "8",
70
+ cy: "8",
71
+ r: "6"
72
+ }),
73
+ /* @__PURE__ */ jsx2("circle", {
74
+ cx: "8",
75
+ cy: "8",
76
+ r: "2",
77
+ fill: "currentColor",
78
+ stroke: "none"
79
+ })
80
+ ]
81
+ }),
82
+ partial: /* @__PURE__ */ jsxs(Fragment, {
83
+ children: [
84
+ /* @__PURE__ */ jsx2("circle", {
85
+ cx: "8",
86
+ cy: "8",
87
+ r: "6"
88
+ }),
89
+ /* @__PURE__ */ jsx2("path", {
90
+ d: "M8 2a6 6 0 0 1 0 12z",
91
+ fill: "currentColor",
92
+ stroke: "none"
93
+ })
94
+ ]
95
+ }),
96
+ unbound: /* @__PURE__ */ jsx2("circle", {
97
+ cx: "8",
98
+ cy: "8",
99
+ r: "6",
100
+ strokeDasharray: "2.2 2.4"
101
+ }),
102
+ data: /* @__PURE__ */ jsxs(Fragment, {
103
+ children: [
104
+ /* @__PURE__ */ jsx2("ellipse", {
105
+ cx: "8",
106
+ cy: "4",
107
+ rx: "5",
108
+ ry: "1.75"
109
+ }),
110
+ /* @__PURE__ */ jsx2("path", {
111
+ d: "M3 4v8c0 .97 2.24 1.75 5 1.75s5-.78 5-1.75V4M3 8c0 .97 2.24 1.75 5 1.75S13 8.97 13 8"
112
+ })
113
+ ]
114
+ }),
115
+ action: /* @__PURE__ */ jsx2("path", {
116
+ d: "M8.75 1.75 3.75 9h4.5l-1 5.25 5-7.25h-4.5z"
117
+ }),
118
+ row: /* @__PURE__ */ jsxs(Fragment, {
119
+ children: [
120
+ /* @__PURE__ */ jsx2("rect", {
121
+ x: "2",
122
+ y: "3",
123
+ width: "12",
124
+ height: "10",
125
+ rx: "2"
126
+ }),
127
+ /* @__PURE__ */ jsx2("path", {
128
+ d: "M2 7h12"
129
+ })
130
+ ]
131
+ }),
132
+ plus: /* @__PURE__ */ jsx2("path", {
133
+ d: "M8 3.25v9.5M3.25 8h9.5"
134
+ }),
135
+ chevronRight: /* @__PURE__ */ jsx2("path", {
136
+ d: "m6.25 3.75 4.25 4.25-4.25 4.25"
137
+ }),
138
+ chevronDown: /* @__PURE__ */ jsx2("path", {
139
+ d: "m3.75 6.25 4.25 4.25 4.25-4.25"
140
+ }),
141
+ close: /* @__PURE__ */ jsx2("path", {
142
+ d: "m4.25 4.25 7.5 7.5M11.75 4.25l-7.5 7.5"
143
+ }),
144
+ screen: /* @__PURE__ */ jsxs(Fragment, {
145
+ children: [
146
+ /* @__PURE__ */ jsx2("rect", {
147
+ x: "1.75",
148
+ y: "2.75",
149
+ width: "12.5",
150
+ height: "10.5",
151
+ rx: "2"
152
+ }),
153
+ /* @__PURE__ */ jsx2("path", {
154
+ d: "M1.75 6h12.5"
155
+ })
156
+ ]
157
+ }),
158
+ dialog: /* @__PURE__ */ jsxs(Fragment, {
159
+ children: [
160
+ /* @__PURE__ */ jsx2("rect", {
161
+ x: "1.75",
162
+ y: "2.75",
163
+ width: "12.5",
164
+ height: "10.5",
165
+ rx: "2"
166
+ }),
167
+ /* @__PURE__ */ jsx2("rect", {
168
+ x: "4.75",
169
+ y: "6",
170
+ width: "6.5",
171
+ height: "4.5",
172
+ rx: "1"
173
+ })
174
+ ]
175
+ }),
176
+ api: /* @__PURE__ */ jsx2("path", {
177
+ d: "M5.25 4.5 1.75 8l3.5 3.5M10.75 4.5 14.25 8l-3.5 3.5M9.25 3l-2.5 10"
178
+ }),
179
+ trash: /* @__PURE__ */ jsx2("path", {
180
+ d: "M2.75 4.25h10.5M6.25 4.25v-1.5h3.5v1.5M4.25 4.25l.6 8.5a1 1 0 0 0 1 .9h4.3a1 1 0 0 0 1-.9l.6-8.5"
181
+ }),
182
+ link: /* @__PURE__ */ jsx2("path", {
183
+ d: "M6.75 9.25a2.75 2.75 0 0 0 3.9 0l2-2a2.75 2.75 0 0 0-3.9-3.9l-.6.6M9.25 6.75a2.75 2.75 0 0 0-3.9 0l-2 2a2.75 2.75 0 0 0 3.9 3.9l.6-.6"
184
+ }),
185
+ repeat: /* @__PURE__ */ jsxs(Fragment, {
186
+ children: [
187
+ /* @__PURE__ */ jsx2("path", {
188
+ d: "m11.25 1.75 2 2-2 2"
189
+ }),
190
+ /* @__PURE__ */ jsx2("path", {
191
+ d: "M2.75 7.5v-.75a3 3 0 0 1 3-3h7.5M4.75 14.25l-2-2 2-2"
192
+ }),
193
+ /* @__PURE__ */ jsx2("path", {
194
+ d: "M13.25 8.5v.75a3 3 0 0 1-3 3h-7.5"
195
+ })
196
+ ]
197
+ }),
198
+ arrowLeft: /* @__PURE__ */ jsx2("path", {
199
+ d: "M13 8H3m4-4L3 8l4 4"
200
+ }),
201
+ component: /* @__PURE__ */ jsx2("rect", {
202
+ x: "2.5",
203
+ y: "2.5",
204
+ width: "11",
205
+ height: "11",
206
+ rx: "2"
207
+ }),
208
+ pointer: /* @__PURE__ */ jsx2("path", {
209
+ d: "m3.25 3.25 3.5 9.5 1.5-4 4-1.5zM8.75 8.75l3.5 3.5"
210
+ }),
211
+ refresh: /* @__PURE__ */ jsx2("path", {
212
+ d: "M13.25 8A5.25 5.25 0 1 1 11.7 4.3M13.25 2.5v2.75H10.5"
213
+ }),
214
+ list: /* @__PURE__ */ jsx2("path", {
215
+ d: "M5.75 4h7.5M5.75 8h7.5M5.75 12h7.5M2.75 4h.01M2.75 8h.01M2.75 12h.01"
216
+ }),
217
+ todo: /* @__PURE__ */ jsxs(Fragment, {
218
+ children: [
219
+ /* @__PURE__ */ jsx2("circle", {
220
+ cx: "8",
221
+ cy: "8",
222
+ r: "6",
223
+ strokeDasharray: "2.2 2.4"
224
+ }),
225
+ /* @__PURE__ */ jsx2("path", {
226
+ d: "M8 5.5V8l1.75 1.25"
227
+ })
228
+ ]
229
+ }),
230
+ target: /* @__PURE__ */ jsxs(Fragment, {
231
+ children: [
232
+ /* @__PURE__ */ jsx2("circle", {
233
+ cx: "8",
234
+ cy: "8",
235
+ r: "6"
236
+ }),
237
+ /* @__PURE__ */ jsx2("circle", {
238
+ cx: "8",
239
+ cy: "8",
240
+ r: "3"
241
+ }),
242
+ /* @__PURE__ */ jsx2("circle", {
243
+ cx: "8",
244
+ cy: "8",
245
+ r: ".6",
246
+ fill: "currentColor"
247
+ })
248
+ ]
249
+ }),
250
+ apply: /* @__PURE__ */ jsx2("path", {
251
+ d: "M12.75 3.25v3.5a2 2 0 0 1-2 2h-7.5m3-3-3 3 3 3"
252
+ }),
253
+ info: /* @__PURE__ */ jsxs(Fragment, {
254
+ children: [
255
+ /* @__PURE__ */ jsx2("circle", {
256
+ cx: "8",
257
+ cy: "8",
258
+ r: "6"
259
+ }),
260
+ /* @__PURE__ */ jsx2("path", {
261
+ d: "M8 7.25v3.5M8 5.25h.01"
262
+ })
263
+ ]
264
+ }),
265
+ settings: /* @__PURE__ */ jsxs(Fragment, {
266
+ children: [
267
+ /* @__PURE__ */ jsx2("path", {
268
+ d: "M6.6 1.75h2.8l.4 1.7 1.3.75 1.65-.55 1.4 2.43-1.25 1.17v1.5l1.25 1.17-1.4 2.43-1.65-.55-1.3.75-.4 1.7H6.6l-.4-1.7-1.3-.75-1.65.55-1.4-2.43 1.25-1.17v-1.5L1.85 6.08l1.4-2.43 1.65.55 1.3-.75z"
269
+ }),
270
+ /* @__PURE__ */ jsx2("circle", {
271
+ cx: "8",
272
+ cy: "8",
273
+ r: "2"
274
+ })
275
+ ]
276
+ }),
277
+ download: /* @__PURE__ */ jsx2("path", {
278
+ d: "M8 2.75v7.5m-3-3 3 3 3-3M2.75 11.75v.5a1 1 0 0 0 1 1h8.5a1 1 0 0 0 1-1v-.5"
279
+ }),
280
+ upload: /* @__PURE__ */ jsx2("path", {
281
+ d: "M8 10.25v-7.5m-3 3 3-3 3 3M2.75 11.75v.5a1 1 0 0 0 1 1h8.5a1 1 0 0 0 1-1v-.5"
282
+ }),
283
+ alert: /* @__PURE__ */ jsxs(Fragment, {
284
+ children: [
285
+ /* @__PURE__ */ jsx2("circle", {
286
+ cx: "8",
287
+ cy: "8",
288
+ r: "6"
289
+ }),
290
+ /* @__PURE__ */ jsx2("path", {
291
+ d: "M8 5v3.25M8 10.75h.01"
292
+ })
293
+ ]
294
+ })
295
+ };
296
+ function Icon({ name, size = 16, className }) {
297
+ return /* @__PURE__ */ jsx2("svg", {
298
+ className: "abs-ico " + (className ?? ""),
299
+ width: size,
300
+ height: size,
301
+ viewBox: "0 0 16 16",
302
+ fill: "none",
303
+ stroke: "currentColor",
304
+ strokeWidth: 1.5,
305
+ strokeLinecap: "round",
306
+ strokeLinejoin: "round",
307
+ "aria-hidden": "true",
308
+ focusable: "false",
309
+ children: paths[name]
310
+ });
311
+ }
312
+ function KindIcon({ svg }) {
313
+ if (svg && /^\s*<svg[\s>]/i.test(svg)) {
314
+ const svgDoc = /xmlns=/.test(svg) ? svg : svg.replace(/<svg/i, '<svg xmlns="http://www.w3.org/2000/svg"');
315
+ const url = `url("data:image/svg+xml,${encodeURIComponent(svgDoc)}")`;
316
+ return /* @__PURE__ */ jsx2("span", {
317
+ className: "abs-kico",
318
+ "aria-hidden": "true",
319
+ children: /* @__PURE__ */ jsx2("span", {
320
+ className: "abs-kico-m",
321
+ style: { maskImage: url, WebkitMaskImage: url }
322
+ })
323
+ });
324
+ }
325
+ return /* @__PURE__ */ jsx2("span", {
326
+ className: "abs-kico",
327
+ "aria-hidden": "true",
328
+ children: /* @__PURE__ */ jsx2(Icon, {
329
+ name: "component"
330
+ })
331
+ });
332
+ }
333
+ var STATE_LABEL = { bound: "Connected", suggested: "Needs review", partial: "Partly connected", unbound: "Not connected" };
334
+ var stateLabel = (s) => STATE_LABEL[s];
335
+ function StateIcon({ state, size = 16 }) {
336
+ return /* @__PURE__ */ jsxs("span", {
337
+ className: `abs-state abs-state-${state}`,
338
+ title: STATE_LABEL[state],
339
+ children: [
340
+ /* @__PURE__ */ jsx2(Icon, {
341
+ name: state,
342
+ size
343
+ }),
344
+ /* @__PURE__ */ jsx2("span", {
345
+ className: "abs-sr",
346
+ children: STATE_LABEL[state]
347
+ })
348
+ ]
349
+ });
350
+ }
351
+ function Ring({ done, total, size = 18, stroke = 2.5 }) {
352
+ const f = total ? done / total : 0;
353
+ if (size <= 16) {
354
+ const pr = size / 4 - 0.25, pc = 2 * Math.PI * pr, h = size / 2;
355
+ return /* @__PURE__ */ jsxs("svg", {
356
+ className: "abs-ring abs-pie" + (total && done === total ? " is-done" : ""),
357
+ width: size,
358
+ height: size,
359
+ viewBox: `0 0 ${size} ${size}`,
360
+ "aria-hidden": "true",
361
+ children: [
362
+ /* @__PURE__ */ jsx2("circle", {
363
+ cx: h,
364
+ cy: h,
365
+ r: h - 0.75,
366
+ strokeWidth: 1.5,
367
+ className: "abs-ring-bg"
368
+ }),
369
+ /* @__PURE__ */ jsx2("circle", {
370
+ cx: h,
371
+ cy: h,
372
+ r: pr,
373
+ strokeWidth: pr * 2,
374
+ className: "abs-ring-fg",
375
+ strokeDasharray: pc,
376
+ strokeDashoffset: pc * (1 - f),
377
+ transform: `rotate(-90 ${h} ${h})`
378
+ })
379
+ ]
380
+ });
381
+ }
382
+ const r = (size - stroke) / 2, c = 2 * Math.PI * r;
383
+ return /* @__PURE__ */ jsxs("svg", {
384
+ className: "abs-ring" + (total && done === total ? " is-done" : ""),
385
+ width: size,
386
+ height: size,
387
+ viewBox: `0 0 ${size} ${size}`,
388
+ "aria-hidden": "true",
389
+ children: [
390
+ /* @__PURE__ */ jsx2("circle", {
391
+ cx: size / 2,
392
+ cy: size / 2,
393
+ r,
394
+ strokeWidth: stroke,
395
+ className: "abs-ring-bg"
396
+ }),
397
+ /* @__PURE__ */ jsx2("circle", {
398
+ cx: size / 2,
399
+ cy: size / 2,
400
+ r,
401
+ strokeWidth: stroke,
402
+ className: "abs-ring-fg",
403
+ strokeDasharray: c,
404
+ strokeDashoffset: c * (1 - f),
405
+ transform: `rotate(-90 ${size / 2} ${size / 2})`
406
+ })
407
+ ]
408
+ });
409
+ }
410
+
411
+ // src/studio/useGlide.tsx
412
+ import * as React from "react";
413
+ import { jsx as jsx3 } from "react/jsx-runtime";
414
+ function useGlide(active) {
415
+ const ref = React.useRef(null);
416
+ const bar = React.useRef(null);
417
+ const moveTo = React.useCallback((el) => {
418
+ const box = ref.current, b = bar.current;
419
+ if (!box || !b)
420
+ return;
421
+ if (!el) {
422
+ b.style.opacity = "0";
423
+ return;
424
+ }
425
+ const r = el.getBoundingClientRect(), c = box.getBoundingClientRect();
426
+ const first = b.style.opacity !== "1";
427
+ b.style.transition = first ? "opacity 120ms ease" : "";
428
+ b.style.transform = `translate(${r.left - c.left + box.scrollLeft}px, ${r.top - c.top + box.scrollTop}px)`;
429
+ b.style.width = `${r.width}px`;
430
+ b.style.height = `${r.height}px`;
431
+ b.style.opacity = "1";
432
+ }, []);
433
+ React.useEffect(() => {
434
+ if (active !== undefined)
435
+ moveTo(active);
436
+ }, [active, moveTo]);
437
+ const handlers = React.useMemo(() => ({
438
+ onPointerOver: (e) => {
439
+ if (active !== undefined)
440
+ return;
441
+ moveTo(e.target.closest("[data-glide]"));
442
+ },
443
+ onPointerLeave: () => {
444
+ if (active === undefined)
445
+ moveTo(null);
446
+ }
447
+ }), [active, moveTo]);
448
+ const indicator = /* @__PURE__ */ jsx3("span", {
449
+ ref: bar,
450
+ className: "abs-glide",
451
+ "aria-hidden": "true"
452
+ });
453
+ return { ref, handlers, indicator, moveTo };
454
+ }
455
+
456
+ // src/studio/ComponentList.tsx
457
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
458
+ function ComponentList({ engine, screen, selected, onSelect }) {
459
+ const [todo, setTodo] = React2.useState(false);
460
+ const [open, setOpen] = React2.useState({});
461
+ const all = screen.components.filter((c) => engine.bindable(engine.descOf(c)).length);
462
+ const byParent = new Map;
463
+ all.forEach((c) => {
464
+ if (c.parent)
465
+ byParent.set(c.parent, [...byParent.get(c.parent) ?? [], c]);
466
+ });
467
+ const kids = (id) => byParent.get(id) ?? [];
468
+ const t = engine.tally([screen]);
469
+ const pending = (c) => engine.compState(screen.id, c) !== "bound";
470
+ const visible = (c) => !todo || pending(c) || kids(c.bindId).some(visible);
471
+ const glide = useGlide();
472
+ const row = (c, depth) => {
473
+ const ch = kids(c.bindId).filter(visible);
474
+ const isOpen = open[c.bindId] ?? true;
475
+ const d = engine.descOf(c);
476
+ return /* @__PURE__ */ jsxs2("div", {
477
+ role: "listitem",
478
+ children: [
479
+ /* @__PURE__ */ jsxs2("div", {
480
+ "data-glide": true,
481
+ className: "abs-comp" + (c.bindId === selected ? " is-on" : ""),
482
+ style: { paddingLeft: 8 + depth * 24 },
483
+ children: [
484
+ ch.length ? /* @__PURE__ */ jsx4("button", {
485
+ type: "button",
486
+ className: "abs-iconbtn abs-twist",
487
+ onClick: () => setOpen((o) => ({ ...o, [c.bindId]: !isOpen })),
488
+ "aria-label": isOpen ? "Collapse" : "Expand",
489
+ "aria-expanded": isOpen,
490
+ children: /* @__PURE__ */ jsx4(Icon, {
491
+ name: "chevronRight",
492
+ size: 14,
493
+ className: isOpen ? "abs-rot90" : ""
494
+ })
495
+ }) : /* @__PURE__ */ jsx4("span", {
496
+ className: "abs-twist"
497
+ }),
498
+ /* @__PURE__ */ jsxs2("button", {
499
+ type: "button",
500
+ className: "abs-comp-main",
501
+ onClick: () => onSelect(c.bindId),
502
+ "aria-current": c.bindId === selected ? "true" : undefined,
503
+ children: [
504
+ /* @__PURE__ */ jsx4(KindIcon, {
505
+ svg: d.icon
506
+ }),
507
+ /* @__PURE__ */ jsx4("span", {
508
+ className: "abs-comp-l",
509
+ children: c.label
510
+ }),
511
+ /* @__PURE__ */ jsxs2("span", {
512
+ className: "abs-comp-k",
513
+ children: [
514
+ d.label,
515
+ ch.length ? ` · ${ch.length}` : ""
516
+ ]
517
+ }),
518
+ /* @__PURE__ */ jsx4(StateIcon, {
519
+ state: engine.compState(screen.id, c)
520
+ })
521
+ ]
522
+ })
523
+ ]
524
+ }),
525
+ ch.length > 0 && /* @__PURE__ */ jsx4(Collapse, {
526
+ open: isOpen,
527
+ children: ch.map((k) => row(k, depth + 1))
528
+ })
529
+ ]
530
+ }, c.bindId);
531
+ };
532
+ const todoCount = all.filter(pending).length;
533
+ return /* @__PURE__ */ jsxs2("div", {
534
+ className: "abs-center abs-scroll",
535
+ children: [
536
+ /* @__PURE__ */ jsxs2("header", {
537
+ className: "abs-screen-h",
538
+ children: [
539
+ /* @__PURE__ */ jsxs2("div", {
540
+ className: "abs-screen-t",
541
+ children: [
542
+ /* @__PURE__ */ jsx4("div", {
543
+ className: "abs-crumbs",
544
+ children: [...screen.path, ...screen.parent ? [engine.SCR.get(screen.parent)?.label] : []].join(" / ")
545
+ }),
546
+ /* @__PURE__ */ jsx4("h1", {
547
+ children: screen.label
548
+ })
549
+ ]
550
+ }),
551
+ /* @__PURE__ */ jsxs2("div", {
552
+ className: "abs-progress",
553
+ children: [
554
+ /* @__PURE__ */ jsx4(Ring, {
555
+ done: t.bound,
556
+ total: t.total,
557
+ size: 36,
558
+ stroke: 3.5
559
+ }),
560
+ /* @__PURE__ */ jsxs2("div", {
561
+ children: [
562
+ /* @__PURE__ */ jsxs2("b", {
563
+ children: [
564
+ t.bound,
565
+ /* @__PURE__ */ jsxs2("span", {
566
+ children: [
567
+ "/",
568
+ t.total
569
+ ]
570
+ })
571
+ ]
572
+ }),
573
+ /* @__PURE__ */ jsx4("small", {
574
+ children: "connected"
575
+ })
576
+ ]
577
+ })
578
+ ]
579
+ })
580
+ ]
581
+ }),
582
+ /* @__PURE__ */ jsxs2("div", {
583
+ className: "abs-seg",
584
+ role: "group",
585
+ "aria-label": "Show",
586
+ children: [
587
+ /* @__PURE__ */ jsx4("span", {
588
+ className: "abs-seg-thumb",
589
+ style: { transform: `translateX(${todo ? 100 : 0}%)` },
590
+ "aria-hidden": "true"
591
+ }),
592
+ /* @__PURE__ */ jsxs2("button", {
593
+ type: "button",
594
+ className: !todo ? "is-on" : "",
595
+ onClick: () => setTodo(false),
596
+ "aria-pressed": !todo,
597
+ children: [
598
+ /* @__PURE__ */ jsx4(Icon, {
599
+ name: "list",
600
+ size: 14
601
+ }),
602
+ "All",
603
+ /* @__PURE__ */ jsx4("span", {
604
+ children: all.length
605
+ })
606
+ ]
607
+ }),
608
+ /* @__PURE__ */ jsxs2("button", {
609
+ type: "button",
610
+ className: todo ? "is-on" : "",
611
+ onClick: () => setTodo(true),
612
+ "aria-pressed": todo,
613
+ children: [
614
+ /* @__PURE__ */ jsx4(Icon, {
615
+ name: "todo",
616
+ size: 14
617
+ }),
618
+ "To do",
619
+ /* @__PURE__ */ jsx4("span", {
620
+ children: todoCount
621
+ })
622
+ ]
623
+ })
624
+ ]
625
+ }),
626
+ /* @__PURE__ */ jsxs2("div", {
627
+ className: "abs-comps abs-glide-box",
628
+ role: "list",
629
+ ref: glide.ref,
630
+ ...glide.handlers,
631
+ children: [
632
+ glide.indicator,
633
+ all.filter((c) => !c.parent && visible(c)).map((c) => row(c, 0)),
634
+ todo && !todoCount && /* @__PURE__ */ jsxs2("div", {
635
+ className: "abs-done",
636
+ children: [
637
+ /* @__PURE__ */ jsx4(Icon, {
638
+ name: "bound",
639
+ size: 24
640
+ }),
641
+ /* @__PURE__ */ jsx4("b", {
642
+ children: "Everything here is connected"
643
+ })
644
+ ]
645
+ })
646
+ ]
647
+ })
648
+ ]
649
+ });
650
+ }
651
+
652
+ // src/studio/FindAnything.tsx
653
+ import * as React3 from "react";
654
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
655
+ function FindAnything({ engine, onGo }) {
656
+ const [q, setQ] = React3.useState("");
657
+ const [open, setOpen] = React3.useState(false);
658
+ const [active, setActive] = React3.useState(0);
659
+ const input = React3.useRef(null);
660
+ const box = React3.useRef(null);
661
+ const glide = useGlide(null);
662
+ const list = glide.ref;
663
+ const index = React3.useMemo(() => engine.fw.registry.screens.flatMap((screen) => [
664
+ { hit: { screen }, text: [screen.label, ...screen.path].join(" ").toLowerCase() },
665
+ ...screen.components.filter((c) => engine.bindable(engine.descOf(c)).length).map((comp) => ({
666
+ hit: { screen, comp },
667
+ text: [comp.label, engine.descOf(comp).label, screen.label].join(" ").toLowerCase()
668
+ }))
669
+ ]), [engine.fw]);
670
+ const hits = React3.useMemo(() => {
671
+ const words = q.trim().toLowerCase().split(/\s+/).filter(Boolean);
672
+ if (!words.length)
673
+ return [];
674
+ return index.filter((x) => words.every((w) => x.text.includes(w))).sort((a, b) => Number(!a.text.startsWith(words[0])) - Number(!b.text.startsWith(words[0]))).slice(0, 40).map((x) => x.hit);
675
+ }, [q, index]);
676
+ React3.useEffect(() => {
677
+ const key = (e) => {
678
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
679
+ e.preventDefault();
680
+ input.current?.focus();
681
+ setOpen(true);
682
+ }
683
+ };
684
+ const away = (e) => {
685
+ if (!box.current?.contains(e.target))
686
+ setOpen(false);
687
+ };
688
+ window.addEventListener("keydown", key);
689
+ document.addEventListener("pointerdown", away);
690
+ return () => {
691
+ window.removeEventListener("keydown", key);
692
+ document.removeEventListener("pointerdown", away);
693
+ };
694
+ }, []);
695
+ React3.useEffect(() => setActive(0), [q]);
696
+ React3.useEffect(() => {
697
+ const el = list.current?.querySelector(`[data-i="${active}"]`) ?? null;
698
+ el?.scrollIntoView({ block: "nearest" });
699
+ glide.moveTo(el);
700
+ }, [active, hits, list, glide]);
701
+ const go = (h) => {
702
+ onGo(h.screen.id, h.comp?.bindId);
703
+ setOpen(false);
704
+ setQ("");
705
+ input.current?.blur();
706
+ };
707
+ const onKey = (e) => {
708
+ if (e.key === "ArrowDown") {
709
+ e.preventDefault();
710
+ setActive((a) => Math.min(hits.length - 1, a + 1));
711
+ } else if (e.key === "ArrowUp") {
712
+ e.preventDefault();
713
+ setActive((a) => Math.max(0, a - 1));
714
+ } else if (e.key === "Enter" && hits[active])
715
+ go(hits[active]);
716
+ else if (e.key === "Escape" && (show || q)) {
717
+ e.preventDefault();
718
+ setOpen(false);
719
+ setQ("");
720
+ }
721
+ };
722
+ const show = open && q.trim().length > 0;
723
+ return /* @__PURE__ */ jsxs3("div", {
724
+ className: "abs-find",
725
+ ref: box,
726
+ children: [
727
+ /* @__PURE__ */ jsxs3("div", {
728
+ className: "abs-find-box",
729
+ children: [
730
+ /* @__PURE__ */ jsx5(Icon, {
731
+ name: "search",
732
+ className: "abs-c-muted"
733
+ }),
734
+ /* @__PURE__ */ jsx5("input", {
735
+ ref: input,
736
+ value: q,
737
+ onChange: (e) => {
738
+ setQ(e.target.value);
739
+ setOpen(true);
740
+ },
741
+ onFocus: () => setOpen(true),
742
+ onKeyDown: onKey,
743
+ placeholder: "Find a component or screen",
744
+ "aria-label": "Find a component or screen",
745
+ role: "combobox",
746
+ "aria-expanded": show,
747
+ "aria-controls": "abs-find-list"
748
+ }),
749
+ /* @__PURE__ */ jsx5("kbd", {
750
+ children: "Ctrl K"
751
+ })
752
+ ]
753
+ }),
754
+ /* @__PURE__ */ jsxs3("div", {
755
+ className: "abs-pop abs-find-pop abs-glide-box" + (show ? " is-open" : ""),
756
+ role: "listbox",
757
+ id: "abs-find-list",
758
+ ref: list,
759
+ children: [
760
+ glide.indicator,
761
+ show && !hits.length ? /* @__PURE__ */ jsxs3("div", {
762
+ className: "abs-empty-sm",
763
+ children: [
764
+ "Nothing matches “",
765
+ q,
766
+ "”. Try a screen or component name."
767
+ ]
768
+ }) : null,
769
+ show ? hits.map((h, i) => {
770
+ const d = h.comp && engine.descOf(h.comp);
771
+ return /* @__PURE__ */ jsxs3("div", {
772
+ "data-i": i,
773
+ role: "option",
774
+ "aria-selected": i === active,
775
+ className: "abs-opt" + (i === active ? " is-active" : ""),
776
+ onPointerEnter: () => setActive(i),
777
+ onPointerDown: (e) => {
778
+ e.preventDefault();
779
+ go(h);
780
+ },
781
+ children: [
782
+ h.comp ? /* @__PURE__ */ jsx5(KindIcon, {
783
+ svg: d?.icon
784
+ }) : /* @__PURE__ */ jsx5("span", {
785
+ className: "abs-kico",
786
+ children: /* @__PURE__ */ jsx5(Icon, {
787
+ name: h.screen.parent ? "dialog" : "screen"
788
+ })
789
+ }),
790
+ /* @__PURE__ */ jsxs3("span", {
791
+ className: "abs-opt-t",
792
+ children: [
793
+ /* @__PURE__ */ jsx5("span", {
794
+ children: h.comp?.label ?? h.screen.label
795
+ }),
796
+ /* @__PURE__ */ jsx5("code", {
797
+ children: h.comp ? `${d?.label} · ${h.screen.label}` : h.screen.path.join(" / ")
798
+ })
799
+ ]
800
+ }),
801
+ h.comp ? /* @__PURE__ */ jsx5(StateIcon, {
802
+ state: engine.compState(h.screen.id, h.comp)
803
+ }) : /* @__PURE__ */ jsx5("span", {
804
+ className: "abs-nav-count",
805
+ children: count(engine.tally([h.screen]))
806
+ })
807
+ ]
808
+ }, h.screen.id + (h.comp?.bindId ?? ""));
809
+ }) : null
810
+ ]
811
+ })
812
+ ]
813
+ });
814
+ }
815
+ var count = (t) => `${t.bound}/${t.total}`;
816
+
817
+ // src/studio/Tip.tsx
818
+ import * as React4 from "react";
819
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
820
+ function placeTip(anchor, tip, viewport, gap = 8, margin = 8) {
821
+ const side = anchor.top - gap - tip.height >= margin || anchor.bottom + gap + tip.height > viewport.height - margin ? "top" : "bottom";
822
+ const top = side === "top" ? anchor.top - gap - tip.height : anchor.bottom + gap;
823
+ const centred = anchor.left + anchor.width / 2 - tip.width / 2;
824
+ const left = Math.min(Math.max(centred, margin), Math.max(margin, viewport.width - margin - tip.width));
825
+ return { top: Math.max(margin, top), left, side };
826
+ }
827
+ var SHOW_DELAY = 250;
828
+ function Tip({ text }) {
829
+ const id = React4.useId();
830
+ const btn = React4.useRef(null);
831
+ const body = React4.useRef(null);
832
+ const timer = React4.useRef(undefined);
833
+ const [open, setOpen] = React4.useState(false);
834
+ const show = React4.useCallback((delay) => {
835
+ clearTimeout(timer.current);
836
+ timer.current = setTimeout(() => setOpen(true), delay);
837
+ }, []);
838
+ const hide = React4.useCallback(() => {
839
+ clearTimeout(timer.current);
840
+ setOpen(false);
841
+ }, []);
842
+ React4.useEffect(() => () => clearTimeout(timer.current), []);
843
+ React4.useLayoutEffect(() => {
844
+ const el = body.current, anchor = btn.current;
845
+ if (!open || !el || !anchor)
846
+ return;
847
+ el.showPopover();
848
+ const p = placeTip(anchor.getBoundingClientRect(), el.getBoundingClientRect(), { width: innerWidth, height: innerHeight });
849
+ el.style.top = `${p.top}px`;
850
+ el.style.left = `${p.left}px`;
851
+ el.dataset.side = p.side;
852
+ const onKey = (e) => {
853
+ if (e.key === "Escape") {
854
+ e.preventDefault();
855
+ hide();
856
+ }
857
+ };
858
+ addEventListener("scroll", hide, true);
859
+ addEventListener("resize", hide);
860
+ addEventListener("keydown", onKey);
861
+ return () => {
862
+ removeEventListener("scroll", hide, true);
863
+ removeEventListener("resize", hide);
864
+ removeEventListener("keydown", onKey);
865
+ if (el.matches(":popover-open"))
866
+ el.hidePopover();
867
+ };
868
+ }, [open, hide]);
869
+ return /* @__PURE__ */ jsxs4("span", {
870
+ className: "abs-tip",
871
+ children: [
872
+ /* @__PURE__ */ jsx6("button", {
873
+ ref: btn,
874
+ type: "button",
875
+ className: "abs-tip-btn",
876
+ "aria-describedby": id,
877
+ "aria-label": "More information",
878
+ onClick: (e) => e.stopPropagation(),
879
+ onPointerEnter: () => show(SHOW_DELAY),
880
+ onPointerLeave: hide,
881
+ onFocus: (e) => {
882
+ if (e.currentTarget.matches(":focus-visible"))
883
+ show(0);
884
+ },
885
+ onBlur: hide,
886
+ children: /* @__PURE__ */ jsx6(Icon, {
887
+ name: "info",
888
+ size: 14
889
+ })
890
+ }),
891
+ /* @__PURE__ */ jsx6("span", {
892
+ ref: body,
893
+ role: "tooltip",
894
+ id,
895
+ popover: "manual",
896
+ className: "abs-tip-body",
897
+ children: text
898
+ })
899
+ ]
900
+ });
901
+ }
902
+
903
+ // src/studio/editors/ConsumeEditor.tsx
904
+ import * as React7 from "react";
905
+
906
+ // src/studio/ApiPicker.tsx
907
+ import * as React5 from "react";
908
+ import { jsx as jsx7, jsxs as jsxs5, Fragment as Fragment3 } from "react/jsx-runtime";
909
+ var opTitle = (v) => v.op.summary?.trim() || `${v.op.method} ${v.op.path}`;
910
+ function MethodTag({ method }) {
911
+ return /* @__PURE__ */ jsx7("span", {
912
+ className: `abs-method abs-method-${method.toLowerCase()}`,
913
+ children: method
914
+ });
915
+ }
916
+ function ApiPicker({ engine, ranked, value, onPick, label = "Choose API", autoOpen = false }) {
917
+ const [open, setOpen] = React5.useState(autoOpen);
918
+ const [q, setQ] = React5.useState("");
919
+ const [active, setActive] = React5.useState(0);
920
+ const root = React5.useRef(null);
921
+ const glide = useGlide(null);
922
+ const list = glide.ref;
923
+ const id = React5.useId();
924
+ const top = React5.useMemo(() => open ? ranked().filter((r) => r.score >= 0.4).slice(0, 3) : [], [open, ranked]);
925
+ const items = React5.useMemo(() => {
926
+ if (!open)
927
+ return [];
928
+ const needle = q.trim().toLowerCase();
929
+ const hit = (v) => !needle || [v.op.id, v.op.summary, v.op.path, ...v.op.tags].join(" ").toLowerCase().includes(needle);
930
+ const out = [];
931
+ if (!needle)
932
+ top.forEach((r) => out.push({ v: r.view, best: r, group: "Best matches" }));
933
+ const shown = new Set(out.map((x) => x.v.op.id));
934
+ const groupOf = (v) => [v.op.product, v.op.tags[0] ?? "Other"].filter(Boolean).join(" · ");
935
+ [...engine.OPS].sort((a, b) => groupOf(a).localeCompare(groupOf(b)) || opTitle(a).localeCompare(opTitle(b))).forEach((v) => {
936
+ if (!shown.has(v.op.id) && hit(v))
937
+ out.push({ v, group: groupOf(v) });
938
+ });
939
+ return out;
940
+ }, [open, q, top, engine]);
941
+ React5.useEffect(() => {
942
+ if (!open)
943
+ return;
944
+ const close = (e) => {
945
+ if (!root.current?.contains(e.target))
946
+ setOpen(false);
947
+ };
948
+ document.addEventListener("pointerdown", close);
949
+ return () => document.removeEventListener("pointerdown", close);
950
+ }, [open]);
951
+ React5.useEffect(() => {
952
+ setActive(0);
953
+ }, [q, open]);
954
+ React5.useEffect(() => {
955
+ const el = list.current?.querySelector(`[data-i="${active}"]`) ?? null;
956
+ el?.scrollIntoView({ block: "nearest" });
957
+ glide.moveTo(el);
958
+ }, [active, items, list, glide]);
959
+ const pick = (opId) => {
960
+ onPick(opId);
961
+ setOpen(false);
962
+ setQ("");
963
+ };
964
+ const onKey = (e) => {
965
+ if (e.key === "ArrowDown") {
966
+ e.preventDefault();
967
+ setActive((a) => Math.min(items.length - 1, a + 1));
968
+ } else if (e.key === "ArrowUp") {
969
+ e.preventDefault();
970
+ setActive((a) => Math.max(0, a - 1));
971
+ } else if (e.key === "Enter" && items[active]) {
972
+ e.preventDefault();
973
+ pick(items[active].v.op.id);
974
+ } else if (e.key === "Escape") {
975
+ e.preventDefault();
976
+ setOpen(false);
977
+ }
978
+ };
979
+ const current = value ? engine.OPV.get(value) : undefined;
980
+ let lastGroup = "";
981
+ return /* @__PURE__ */ jsxs5("div", {
982
+ className: "abs-picker",
983
+ ref: root,
984
+ children: [
985
+ current ? /* @__PURE__ */ jsxs5("button", {
986
+ type: "button",
987
+ className: "abs-opcard",
988
+ onClick: () => setOpen((o) => !o),
989
+ "aria-expanded": open,
990
+ children: [
991
+ /* @__PURE__ */ jsx7(MethodTag, {
992
+ method: current.op.method
993
+ }),
994
+ /* @__PURE__ */ jsxs5("span", {
995
+ className: "abs-opcard-t",
996
+ children: [
997
+ /* @__PURE__ */ jsx7("b", {
998
+ children: opTitle(current)
999
+ }),
1000
+ /* @__PURE__ */ jsx7("code", {
1001
+ children: current.op.path
1002
+ })
1003
+ ]
1004
+ }),
1005
+ /* @__PURE__ */ jsx7("span", {
1006
+ className: "abs-link",
1007
+ children: "Change"
1008
+ })
1009
+ ]
1010
+ }) : /* @__PURE__ */ jsxs5("button", {
1011
+ type: "button",
1012
+ className: "abs-btn abs-btn-dashed",
1013
+ onClick: () => setOpen((o) => !o),
1014
+ "aria-expanded": open,
1015
+ children: [
1016
+ /* @__PURE__ */ jsx7(Icon, {
1017
+ name: "api"
1018
+ }),
1019
+ " ",
1020
+ label
1021
+ ]
1022
+ }),
1023
+ open ? /* @__PURE__ */ jsxs5("div", {
1024
+ className: "abs-pop",
1025
+ role: "dialog",
1026
+ "aria-label": "Choose an API",
1027
+ children: [
1028
+ /* @__PURE__ */ jsxs5("div", {
1029
+ className: "abs-pop-search",
1030
+ children: [
1031
+ /* @__PURE__ */ jsx7(Icon, {
1032
+ name: "search"
1033
+ }),
1034
+ /* @__PURE__ */ jsx7("input", {
1035
+ autoFocus: true,
1036
+ value: q,
1037
+ onChange: (e) => setQ(e.target.value),
1038
+ onKeyDown: onKey,
1039
+ placeholder: `Search ${engine.OPS.length} APIs`,
1040
+ role: "combobox",
1041
+ "aria-expanded": "true",
1042
+ "aria-controls": id,
1043
+ "aria-activedescendant": items[active] ? `${id}-${active}` : undefined
1044
+ })
1045
+ ]
1046
+ }),
1047
+ /* @__PURE__ */ jsxs5("div", {
1048
+ className: "abs-pop-list abs-glide-box",
1049
+ ref: list,
1050
+ id,
1051
+ role: "listbox",
1052
+ children: [
1053
+ glide.indicator,
1054
+ items.length === 0 ? /* @__PURE__ */ jsxs5("div", {
1055
+ className: "abs-empty-sm",
1056
+ children: [
1057
+ "No API matches “",
1058
+ q,
1059
+ "”. Search by name, path or tag."
1060
+ ]
1061
+ }) : null,
1062
+ items.map((it, i) => {
1063
+ const head = it.group !== lastGroup ? lastGroup = it.group : null;
1064
+ return /* @__PURE__ */ jsxs5(React5.Fragment, {
1065
+ children: [
1066
+ head ? /* @__PURE__ */ jsx7("div", {
1067
+ className: "abs-pop-group",
1068
+ role: "presentation",
1069
+ children: head === "Best matches" ? /* @__PURE__ */ jsxs5(Fragment3, {
1070
+ children: [
1071
+ /* @__PURE__ */ jsx7(Icon, {
1072
+ name: "target",
1073
+ size: 12
1074
+ }),
1075
+ " Best matches"
1076
+ ]
1077
+ }) : head
1078
+ }) : null,
1079
+ /* @__PURE__ */ jsxs5("div", {
1080
+ id: `${id}-${i}`,
1081
+ "data-i": i,
1082
+ role: "option",
1083
+ "aria-selected": i === active,
1084
+ className: "abs-opt" + (i === active ? " is-active" : "") + (it.v.op.id === value ? " is-current" : ""),
1085
+ onPointerEnter: () => setActive(i),
1086
+ onPointerDown: (e) => {
1087
+ e.preventDefault();
1088
+ pick(it.v.op.id);
1089
+ },
1090
+ children: [
1091
+ /* @__PURE__ */ jsx7(MethodTag, {
1092
+ method: it.v.op.method
1093
+ }),
1094
+ /* @__PURE__ */ jsxs5("span", {
1095
+ className: "abs-opt-t",
1096
+ children: [
1097
+ /* @__PURE__ */ jsx7("span", {
1098
+ children: opTitle(it.v)
1099
+ }),
1100
+ /* @__PURE__ */ jsx7("code", {
1101
+ children: it.v.op.path
1102
+ })
1103
+ ]
1104
+ }),
1105
+ it.v.op.id === value ? /* @__PURE__ */ jsx7(Icon, {
1106
+ name: "check",
1107
+ className: "abs-c-accent"
1108
+ }) : null
1109
+ ]
1110
+ })
1111
+ ]
1112
+ }, it.group + it.v.op.id);
1113
+ })
1114
+ ]
1115
+ }),
1116
+ items[active]?.best?.reasons.length ? /* @__PURE__ */ jsxs5("div", {
1117
+ className: "abs-pop-why",
1118
+ "aria-live": "polite",
1119
+ children: [
1120
+ /* @__PURE__ */ jsx7(Icon, {
1121
+ name: "info",
1122
+ size: 14
1123
+ }),
1124
+ /* @__PURE__ */ jsxs5("span", {
1125
+ children: [
1126
+ /* @__PURE__ */ jsx7("b", {
1127
+ children: "Why suggested:"
1128
+ }),
1129
+ " ",
1130
+ items[active].best.reasons.join(" · ")
1131
+ ]
1132
+ })
1133
+ ]
1134
+ }) : null
1135
+ ]
1136
+ }) : null
1137
+ ]
1138
+ });
1139
+ }
1140
+
1141
+ // src/studio/editors/RequestInputs.tsx
1142
+ import * as React6 from "react";
1143
+
1144
+ // src/studio/editors/types.ts
1145
+ var GROUP_LABEL = { item: "Row", screen: "This screen", context: "Passed in", provider: "App" };
1146
+
1147
+ // src/studio/editors/SourceSelect.tsx
1148
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1149
+ function SourceSelect({ engine, value, type, sources, onChange, id }) {
1150
+ const waiting = sources.filter((s) => s.waitsFor);
1151
+ const fits = sources.filter((s) => s.type !== "list" && !s.waitsFor).map((s) => ({ s, fit: engine.typeFit(type, s.type) })).filter((x) => x.fit.ok);
1152
+ const groups = new Map;
1153
+ fits.forEach((x) => {
1154
+ const g = GROUP_LABEL[x.s.group];
1155
+ groups.set(g, [...groups.get(g) ?? [], x]);
1156
+ });
1157
+ const known = !value || fits.some((x) => x.s.expr + (x.fit.transform ? ` | ${x.fit.transform}()` : "") === value);
1158
+ return /* @__PURE__ */ jsxs6("select", {
1159
+ id,
1160
+ className: "abs-select" + (value ? "" : " is-empty"),
1161
+ value: value ?? "",
1162
+ onChange: (e) => onChange(e.target.value || undefined),
1163
+ children: [
1164
+ /* @__PURE__ */ jsx8("option", {
1165
+ value: "",
1166
+ children: "Not set"
1167
+ }),
1168
+ !known && /* @__PURE__ */ jsx8("option", {
1169
+ value,
1170
+ children: value
1171
+ }),
1172
+ [...groups].map(([g, xs]) => /* @__PURE__ */ jsx8("optgroup", {
1173
+ label: g,
1174
+ children: xs.map(({ s, fit }) => /* @__PURE__ */ jsx8("option", {
1175
+ value: s.expr + (fit.transform ? ` | ${fit.transform}()` : ""),
1176
+ children: s.label
1177
+ }, s.expr))
1178
+ }, g)),
1179
+ waiting.length > 0 && /* @__PURE__ */ jsx8("optgroup", {
1180
+ label: "Connect first",
1181
+ children: waiting.map((s) => /* @__PURE__ */ jsxs6("option", {
1182
+ disabled: true,
1183
+ children: [
1184
+ s.label,
1185
+ ": connect ",
1186
+ s.waitsFor
1187
+ ]
1188
+ }, s.expr))
1189
+ })
1190
+ ]
1191
+ });
1192
+ }
1193
+
1194
+ // src/studio/editors/RequestInputs.tsx
1195
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1196
+ function RequestInputs({ engine, view, request, sources, onChange, idp }) {
1197
+ const req = [...view.req].sort((a, b) => Number(b[2]) - Number(a[2]));
1198
+ const required = req.filter((r) => r[2]), optional = req.filter((r) => !r[2]);
1199
+ const filled = req.filter(([p]) => request[p]).length;
1200
+ const [more, setMore] = React6.useState(false);
1201
+ if (!req.length)
1202
+ return null;
1203
+ const row = ([ptr, type, isReq]) => /* @__PURE__ */ jsxs7("div", {
1204
+ className: "abs-map-row",
1205
+ children: [
1206
+ /* @__PURE__ */ jsxs7("label", {
1207
+ htmlFor: idp + ptr,
1208
+ title: ptr,
1209
+ children: [
1210
+ humanize(ptr),
1211
+ isReq && /* @__PURE__ */ jsx9("span", {
1212
+ className: "abs-req",
1213
+ "aria-label": "required",
1214
+ children: "*"
1215
+ })
1216
+ ]
1217
+ }),
1218
+ /* @__PURE__ */ jsx9(Icon, {
1219
+ name: "arrowLeft",
1220
+ size: 14,
1221
+ className: "abs-c-muted"
1222
+ }),
1223
+ /* @__PURE__ */ jsx9(SourceSelect, {
1224
+ engine,
1225
+ id: idp + ptr,
1226
+ type,
1227
+ value: request[ptr],
1228
+ sources,
1229
+ onChange: (v) => {
1230
+ const r = { ...request };
1231
+ if (v)
1232
+ r[ptr] = v;
1233
+ else
1234
+ delete r[ptr];
1235
+ onChange(r);
1236
+ }
1237
+ })
1238
+ ]
1239
+ }, ptr);
1240
+ const shownOptional = optional.filter(([p]) => more || request[p]);
1241
+ const shown = required.length + shownOptional.length;
1242
+ return /* @__PURE__ */ jsxs7("div", {
1243
+ className: "abs-block",
1244
+ children: [
1245
+ shown > 0 && /* @__PURE__ */ jsxs7("div", {
1246
+ className: "abs-block-h",
1247
+ children: [
1248
+ /* @__PURE__ */ jsx9("span", {
1249
+ children: "Sends"
1250
+ }),
1251
+ /* @__PURE__ */ jsxs7("span", {
1252
+ className: "abs-count",
1253
+ children: [
1254
+ filled,
1255
+ "/",
1256
+ req.length
1257
+ ]
1258
+ })
1259
+ ]
1260
+ }),
1261
+ shown > 0 && /* @__PURE__ */ jsxs7("div", {
1262
+ className: "abs-map",
1263
+ children: [
1264
+ required.map(row),
1265
+ shownOptional.map(row)
1266
+ ]
1267
+ }),
1268
+ optional.length > shownOptional.length && /* @__PURE__ */ jsxs7("button", {
1269
+ type: "button",
1270
+ className: "abs-link abs-more",
1271
+ onClick: () => setMore(true),
1272
+ children: [
1273
+ /* @__PURE__ */ jsx9(Icon, {
1274
+ name: "plus",
1275
+ size: 14
1276
+ }),
1277
+ " ",
1278
+ shown ? `${optional.length - shownOptional.length} more inputs` : `Add inputs (${optional.length} optional)`
1279
+ ]
1280
+ })
1281
+ ]
1282
+ });
1283
+ }
1284
+
1285
+ // src/studio/editors/ConsumeEditor.tsx
1286
+ import { jsx as jsx10, jsxs as jsxs8, Fragment as Fragment4 } from "react/jsx-runtime";
1287
+ function ConsumeEditor(p) {
1288
+ const { engine, screen, comp, slot, put } = p;
1289
+ const b = engine.getB(screen, comp.bindId, slot.id);
1290
+ const shape = engine.shapeOf(screen, comp, slot);
1291
+ const ranked = React7.useCallback(() => engine.rank(screen, comp, slot), [engine, screen, comp, slot]);
1292
+ const srcs = React7.useMemo(() => engine.sources(screen), [engine, screen]);
1293
+ const op = b?.type === "operation" ? b : undefined;
1294
+ const view = op ? engine.OPV.get(op.operation) : undefined;
1295
+ const choose = (opId) => {
1296
+ const main = engine.proposeConsume(screen, comp, slot, opId);
1297
+ if (!main)
1298
+ return;
1299
+ const siblings = op ? [] : engine.siblingFills(screen, comp, slot, opId);
1300
+ [main, ...siblings].forEach(put);
1301
+ };
1302
+ const fieldsAt = engine.fieldsAt;
1303
+ const update = (patch) => op && put({ ...op, ...patch, state: "draft" });
1304
+ const props = shape.fields && !shape.via ? shape.fields : null;
1305
+ const choices = view ? shape.many || props ? [...view.lists.map((l) => [l.select, l.fields.length]), ...view.one ? [["$", 0]] : []] : view.scalars.map(([s]) => [s, 0]) : [];
1306
+ return /* @__PURE__ */ jsxs8(Fragment4, {
1307
+ children: [
1308
+ /* @__PURE__ */ jsx10(ApiPicker, {
1309
+ engine,
1310
+ ranked,
1311
+ value: op?.operation,
1312
+ onPick: choose
1313
+ }),
1314
+ view && op && /* @__PURE__ */ jsxs8(Fragment4, {
1315
+ children: [
1316
+ choices.length > 1 && /* @__PURE__ */ jsxs8("div", {
1317
+ className: "abs-field",
1318
+ children: [
1319
+ /* @__PURE__ */ jsx10("label", {
1320
+ htmlFor: `sel-${comp.bindId}-${slot.id}`,
1321
+ children: shape.many || props ? "Use list" : "Use value"
1322
+ }),
1323
+ /* @__PURE__ */ jsx10("select", {
1324
+ id: `sel-${comp.bindId}-${slot.id}`,
1325
+ className: "abs-select",
1326
+ value: op.select,
1327
+ onChange: (e) => update({ select: e.target.value, map: props ? engine.proposeMap(props, fieldsAt(view, e.target.value)) : undefined }),
1328
+ children: choices.map(([s]) => /* @__PURE__ */ jsx10("option", {
1329
+ value: s,
1330
+ children: s === "$" ? "Whole response" : humanize(s)
1331
+ }, s))
1332
+ })
1333
+ ]
1334
+ }),
1335
+ props && /* @__PURE__ */ jsxs8("div", {
1336
+ className: "abs-block",
1337
+ children: [
1338
+ /* @__PURE__ */ jsx10("div", {
1339
+ className: "abs-block-h",
1340
+ children: /* @__PURE__ */ jsx10("span", {
1341
+ children: "Shows"
1342
+ })
1343
+ }),
1344
+ /* @__PURE__ */ jsx10("div", {
1345
+ className: "abs-map",
1346
+ children: props.map(([f, t]) => /* @__PURE__ */ jsxs8("div", {
1347
+ className: "abs-map-row",
1348
+ children: [
1349
+ /* @__PURE__ */ jsx10("label", {
1350
+ htmlFor: `map-${comp.bindId}-${f}`,
1351
+ children: humanize(f)
1352
+ }),
1353
+ /* @__PURE__ */ jsx10(Icon, {
1354
+ name: "arrowLeft",
1355
+ size: 14,
1356
+ className: "abs-c-muted"
1357
+ }),
1358
+ /* @__PURE__ */ jsxs8("select", {
1359
+ id: `map-${comp.bindId}-${f}`,
1360
+ className: "abs-select" + (op.map?.[f] ? "" : " is-empty"),
1361
+ value: op.map?.[f] ?? "",
1362
+ onChange: (e) => {
1363
+ const m = { ...op.map ?? {} };
1364
+ if (e.target.value)
1365
+ m[f] = e.target.value;
1366
+ else
1367
+ delete m[f];
1368
+ update({ map: m });
1369
+ },
1370
+ children: [
1371
+ /* @__PURE__ */ jsx10("option", {
1372
+ value: "",
1373
+ children: "Not set"
1374
+ }),
1375
+ fieldsAt(view, op.select).map(([ff, ft]) => [ff, engine.typeFit(t, ft)]).filter(([, fit]) => fit.ok).map(([ff, fit]) => /* @__PURE__ */ jsx10("option", {
1376
+ value: `$item.${ff}${fit.transform ? ` | ${fit.transform}()` : ""}`,
1377
+ children: humanize(ff)
1378
+ }, ff))
1379
+ ]
1380
+ })
1381
+ ]
1382
+ }, f))
1383
+ })
1384
+ ]
1385
+ }),
1386
+ /* @__PURE__ */ jsx10(RequestInputs, {
1387
+ engine,
1388
+ view,
1389
+ request: op.request,
1390
+ sources: srcs,
1391
+ onChange: (request) => update({ request }),
1392
+ idp: `req-${comp.bindId}-${slot.id}`
1393
+ }),
1394
+ /* @__PURE__ */ jsxs8("div", {
1395
+ className: "abs-field abs-field-inline",
1396
+ children: [
1397
+ /* @__PURE__ */ jsx10("label", {
1398
+ htmlFor: `load-${comp.bindId}-${slot.id}`,
1399
+ children: "Load"
1400
+ }),
1401
+ /* @__PURE__ */ jsxs8("select", {
1402
+ id: `load-${comp.bindId}-${slot.id}`,
1403
+ className: "abs-select abs-select-sm",
1404
+ value: op.load === "demand" ? "demand" : "open",
1405
+ onChange: (e) => update({ load: e.target.value }),
1406
+ children: [
1407
+ /* @__PURE__ */ jsx10("option", {
1408
+ value: "open",
1409
+ children: "When the screen opens"
1410
+ }),
1411
+ /* @__PURE__ */ jsx10("option", {
1412
+ value: "demand",
1413
+ children: "When first used"
1414
+ })
1415
+ ]
1416
+ })
1417
+ ]
1418
+ })
1419
+ ]
1420
+ })
1421
+ ]
1422
+ });
1423
+ }
1424
+
1425
+ // src/studio/editors/ScopedEditor.tsx
1426
+ import { jsx as jsx11, jsxs as jsxs9, Fragment as Fragment5 } from "react/jsx-runtime";
1427
+ function ScopedEditor({ engine, screen, comp, slot, put, goTo }) {
1428
+ const b = engine.getB(screen, comp.bindId, slot.id);
1429
+ const fields = engine.scopeFields(screen, comp, slot);
1430
+ const parent = comp.parent ? engine.compOf(screen, comp.parent) : undefined;
1431
+ if (!fields?.length) {
1432
+ return /* @__PURE__ */ jsxs9("div", {
1433
+ className: "abs-hint",
1434
+ children: [
1435
+ /* @__PURE__ */ jsx11(Icon, {
1436
+ name: "link"
1437
+ }),
1438
+ /* @__PURE__ */ jsxs9("span", {
1439
+ children: [
1440
+ "Uses data from ",
1441
+ /* @__PURE__ */ jsx11("b", {
1442
+ children: parent?.label
1443
+ }),
1444
+ "."
1445
+ ]
1446
+ }),
1447
+ parent && /* @__PURE__ */ jsx11("button", {
1448
+ type: "button",
1449
+ className: "abs-link",
1450
+ onClick: () => goTo(parent.bindId),
1451
+ children: "Connect it first"
1452
+ })
1453
+ ]
1454
+ });
1455
+ }
1456
+ const type = engine.shapeOf(screen, comp, slot).type ?? "any";
1457
+ const item = fields.map(([f, t]) => ({ expr: `$item.${f}`, label: humanize(f), name: f, type: t, group: "item" }));
1458
+ const suggestions = !b ? engine.rankSources(comp.label, type, item, 0.45).slice(0, 3) : [];
1459
+ const id = `scope-${comp.bindId}-${slot.id}`;
1460
+ return /* @__PURE__ */ jsxs9(Fragment5, {
1461
+ children: [
1462
+ /* @__PURE__ */ jsxs9("div", {
1463
+ className: "abs-field",
1464
+ children: [
1465
+ /* @__PURE__ */ jsxs9("label", {
1466
+ htmlFor: id,
1467
+ children: [
1468
+ "Field from ",
1469
+ parent?.label
1470
+ ]
1471
+ }),
1472
+ /* @__PURE__ */ jsxs9("select", {
1473
+ id,
1474
+ className: "abs-select" + (b ? "" : " is-empty"),
1475
+ value: b?.value ?? "",
1476
+ onChange: (e) => e.target.value && put({ component: comp.bindId, slot: slot.id, type: "expression", value: e.target.value, origin: "manual", state: "draft" }),
1477
+ children: [
1478
+ /* @__PURE__ */ jsx11("option", {
1479
+ value: "",
1480
+ children: "Choose a field"
1481
+ }),
1482
+ item.filter((s) => engine.typeFit(type, s.type).ok).map((s) => /* @__PURE__ */ jsx11("option", {
1483
+ value: s.expr,
1484
+ children: s.label
1485
+ }, s.expr))
1486
+ ]
1487
+ })
1488
+ ]
1489
+ }),
1490
+ suggestions.length > 0 && /* @__PURE__ */ jsx11("div", {
1491
+ className: "abs-suggests",
1492
+ children: suggestions.map((sg) => /* @__PURE__ */ jsxs9("button", {
1493
+ type: "button",
1494
+ className: "abs-suggest",
1495
+ onClick: () => put({ component: comp.bindId, slot: slot.id, type: "expression", value: sg.expr, origin: "suggested", state: "draft" }),
1496
+ children: [
1497
+ /* @__PURE__ */ jsx11(Icon, {
1498
+ name: "apply",
1499
+ size: 14
1500
+ }),
1501
+ " ",
1502
+ sg.source.label
1503
+ ]
1504
+ }, sg.expr))
1505
+ })
1506
+ ]
1507
+ });
1508
+ }
1509
+
1510
+ // src/studio/editors/TriggerEditor.tsx
1511
+ import * as React9 from "react";
1512
+
1513
+ // src/studio/editors/StepCard.tsx
1514
+ import * as React8 from "react";
1515
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1516
+ function StepCard({ engine, screen, comp, slot, step, index, total, onChange, onRemove, onMove }) {
1517
+ const ranked = React8.useCallback(() => engine.rank(screen, comp, slot), [engine, screen, comp, slot]);
1518
+ const mc = engine.manyContext(screen);
1519
+ const view = engine.OPV.get(step.operation);
1520
+ const srcs = React8.useMemo(() => step.forEach ? engine.triggerSources(screen) : engine.sources(screen), [engine, screen, step]);
1521
+ return /* @__PURE__ */ jsxs10("div", {
1522
+ className: "abs-step",
1523
+ children: [
1524
+ /* @__PURE__ */ jsxs10("div", {
1525
+ className: "abs-step-h",
1526
+ children: [
1527
+ /* @__PURE__ */ jsx12("span", {
1528
+ className: "abs-step-n",
1529
+ children: index + 1
1530
+ }),
1531
+ /* @__PURE__ */ jsx12(Icon, {
1532
+ name: "api",
1533
+ className: "abs-c-muted"
1534
+ }),
1535
+ /* @__PURE__ */ jsx12("span", {
1536
+ className: "abs-step-t",
1537
+ children: "Call API"
1538
+ }),
1539
+ /* @__PURE__ */ jsxs10("span", {
1540
+ className: "abs-step-tools",
1541
+ children: [
1542
+ index > 0 && /* @__PURE__ */ jsx12("button", {
1543
+ type: "button",
1544
+ className: "abs-iconbtn",
1545
+ onClick: () => onMove(-1),
1546
+ "aria-label": "Move up",
1547
+ children: /* @__PURE__ */ jsx12(Icon, {
1548
+ name: "chevronDown",
1549
+ className: "abs-rot180"
1550
+ })
1551
+ }),
1552
+ index < total - 1 && /* @__PURE__ */ jsx12("button", {
1553
+ type: "button",
1554
+ className: "abs-iconbtn",
1555
+ onClick: () => onMove(1),
1556
+ "aria-label": "Move down",
1557
+ children: /* @__PURE__ */ jsx12(Icon, {
1558
+ name: "chevronDown"
1559
+ })
1560
+ }),
1561
+ /* @__PURE__ */ jsx12("button", {
1562
+ type: "button",
1563
+ className: "abs-iconbtn",
1564
+ onClick: onRemove,
1565
+ "aria-label": "Remove step",
1566
+ children: /* @__PURE__ */ jsx12(Icon, {
1567
+ name: "trash"
1568
+ })
1569
+ })
1570
+ ]
1571
+ })
1572
+ ]
1573
+ }),
1574
+ view && /* @__PURE__ */ jsxs10("div", {
1575
+ className: "abs-step-b",
1576
+ children: [
1577
+ /* @__PURE__ */ jsx12(ApiPicker, {
1578
+ engine,
1579
+ ranked,
1580
+ value: step.operation,
1581
+ onPick: (id) => onChange({ ...step, operation: id, request: engine.proposeRequest(engine.OPV.get(id), srcs) })
1582
+ }),
1583
+ mc && /* @__PURE__ */ jsxs10("label", {
1584
+ className: "abs-toggle",
1585
+ children: [
1586
+ /* @__PURE__ */ jsx12("input", {
1587
+ type: "checkbox",
1588
+ checked: !!step.forEach,
1589
+ onChange: (e) => onChange({ ...step, forEach: e.target.checked ? `$context.${mc.id}` : undefined })
1590
+ }),
1591
+ /* @__PURE__ */ jsx12("span", {
1592
+ className: "abs-toggle-ui",
1593
+ "aria-hidden": "true"
1594
+ }),
1595
+ /* @__PURE__ */ jsx12(Icon, {
1596
+ name: "repeat",
1597
+ size: 14,
1598
+ className: "abs-c-muted"
1599
+ }),
1600
+ " Run for each ",
1601
+ /* @__PURE__ */ jsx12("span", {
1602
+ className: "abs-toggle-of",
1603
+ children: mc.label
1604
+ })
1605
+ ]
1606
+ }),
1607
+ /* @__PURE__ */ jsx12(RequestInputs, {
1608
+ engine,
1609
+ view,
1610
+ request: step.request,
1611
+ sources: srcs,
1612
+ onChange: (request) => onChange({ ...step, request }),
1613
+ idp: `st-${comp.bindId}-${index}-`
1614
+ })
1615
+ ]
1616
+ })
1617
+ ]
1618
+ });
1619
+ }
1620
+
1621
+ // src/studio/editors/TriggerEditor.tsx
1622
+ import { jsx as jsx13, jsxs as jsxs11, Fragment as Fragment6 } from "react/jsx-runtime";
1623
+ function TriggerEditor(p) {
1624
+ const { engine, screen, comp, slot, put } = p;
1625
+ const b = engine.getB(screen, comp.bindId, slot.id);
1626
+ const steps = b?.type === "steps" ? b.steps : [];
1627
+ const [adding, setAdding] = React9.useState(false);
1628
+ const set = (next) => next.length ? put({ component: comp.bindId, slot: slot.id, type: "steps", steps: next, origin: b?.origin ?? "manual", state: "draft" }) : p.drop();
1629
+ const none = () => put({ component: comp.bindId, slot: slot.id, type: "none", origin: "manual", state: "confirmed" });
1630
+ const mc = engine.manyContext(screen);
1631
+ const ranked = React9.useCallback(() => engine.rank(screen, comp, slot), [engine, screen, comp, slot]);
1632
+ if (b?.type === "none")
1633
+ return /* @__PURE__ */ jsxs11("div", {
1634
+ className: "abs-hint",
1635
+ children: [
1636
+ /* @__PURE__ */ jsx13(Icon, {
1637
+ name: "check"
1638
+ }),
1639
+ /* @__PURE__ */ jsx13("span", {
1640
+ children: "Calls no API."
1641
+ })
1642
+ ]
1643
+ });
1644
+ const addCall = (opId) => {
1645
+ const v = engine.OPV.get(opId);
1646
+ const forEach = mc ? `$context.${mc.id}` : undefined;
1647
+ const next = 1 + Math.max(0, ...steps.map((x) => Number(x.id.replace(/^s/, "")) || 0));
1648
+ set([...steps, { type: "call", id: `s${next}`, operation: opId, request: engine.proposeRequest(v, engine.triggerSources(screen)), forEach, onError: "stop" }]);
1649
+ setAdding(false);
1650
+ };
1651
+ return /* @__PURE__ */ jsxs11("div", {
1652
+ className: "abs-steps",
1653
+ children: [
1654
+ steps.map((st, i) => /* @__PURE__ */ jsx13(StepCard, {
1655
+ ...p,
1656
+ step: st,
1657
+ index: i,
1658
+ total: steps.length,
1659
+ onChange: (x) => set(steps.map((y, j) => j === i ? x : y)),
1660
+ onRemove: () => set(steps.filter((_, j) => j !== i)),
1661
+ onMove: (d) => {
1662
+ const n = [...steps];
1663
+ [n[i], n[i + d]] = [n[i + d], n[i]];
1664
+ set(n);
1665
+ }
1666
+ }, i)),
1667
+ adding ? /* @__PURE__ */ jsxs11("div", {
1668
+ className: "abs-addstep",
1669
+ children: [
1670
+ /* @__PURE__ */ jsx13(ApiPicker, {
1671
+ engine,
1672
+ ranked,
1673
+ onPick: addCall,
1674
+ label: "Call API",
1675
+ autoOpen: true
1676
+ }),
1677
+ /* @__PURE__ */ jsx13("button", {
1678
+ type: "button",
1679
+ className: "abs-link abs-more",
1680
+ onClick: () => setAdding(false),
1681
+ children: "Cancel"
1682
+ })
1683
+ ]
1684
+ }) : /* @__PURE__ */ jsxs11(Fragment6, {
1685
+ children: [
1686
+ /* @__PURE__ */ jsxs11("button", {
1687
+ type: "button",
1688
+ className: "abs-btn abs-btn-dashed",
1689
+ onClick: () => setAdding(true),
1690
+ children: [
1691
+ /* @__PURE__ */ jsx13(Icon, {
1692
+ name: "plus"
1693
+ }),
1694
+ " ",
1695
+ steps.length ? "Call another API" : "Call an API"
1696
+ ]
1697
+ }),
1698
+ !steps.length && /* @__PURE__ */ jsx13("button", {
1699
+ type: "button",
1700
+ className: "abs-link abs-more",
1701
+ onClick: none,
1702
+ children: "No API needed"
1703
+ })
1704
+ ]
1705
+ })
1706
+ ]
1707
+ });
1708
+ }
1709
+
1710
+ // src/studio/editors/SlotSection.tsx
1711
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1712
+ var ROLE_HELP = {
1713
+ consume: "Data this component shows. Pick the API that returns it.",
1714
+ scoped: "A value shown for each row. Pick the field from the row's data.",
1715
+ trigger: "What happens when this is used. Pick the API calls it makes, or mark it as calling none."
1716
+ };
1717
+ function SlotSection(p) {
1718
+ const { engine, screen, comp, slot } = p;
1719
+ const state = engine.slotState(screen, comp.bindId, slot.id);
1720
+ const b = engine.getB(screen, comp.bindId, slot.id);
1721
+ const kind = slot.role === "trigger" ? "trigger" : slot.scope ? "scoped" : "consume";
1722
+ return /* @__PURE__ */ jsxs12("section", {
1723
+ className: `abs-slot abs-slot-${state}`,
1724
+ "aria-labelledby": `slot-${comp.bindId}-${slot.id}`,
1725
+ children: [
1726
+ /* @__PURE__ */ jsxs12("header", {
1727
+ className: "abs-slot-h",
1728
+ children: [
1729
+ /* @__PURE__ */ jsx14(Icon, {
1730
+ name: kind === "trigger" ? "action" : kind === "scoped" ? "row" : "data",
1731
+ className: "abs-c-muted"
1732
+ }),
1733
+ /* @__PURE__ */ jsxs12("h3", {
1734
+ id: `slot-${comp.bindId}-${slot.id}`,
1735
+ children: [
1736
+ slot.label,
1737
+ slot.required ? /* @__PURE__ */ jsx14("span", {
1738
+ className: "abs-req",
1739
+ "aria-label": "required",
1740
+ children: "*"
1741
+ }) : null
1742
+ ]
1743
+ }),
1744
+ /* @__PURE__ */ jsx14(Tip, {
1745
+ text: slot.description ? `${slot.description}. ${ROLE_HELP[kind]}` : ROLE_HELP[kind]
1746
+ }),
1747
+ /* @__PURE__ */ jsxs12("span", {
1748
+ className: `abs-pill abs-pill-${state}`,
1749
+ children: [
1750
+ /* @__PURE__ */ jsx14(StateIcon, {
1751
+ state,
1752
+ size: 13
1753
+ }),
1754
+ stateLabel(state)
1755
+ ]
1756
+ })
1757
+ ]
1758
+ }),
1759
+ /* @__PURE__ */ jsx14("div", {
1760
+ className: "abs-slot-b",
1761
+ children: kind === "trigger" ? /* @__PURE__ */ jsx14(TriggerEditor, {
1762
+ ...p
1763
+ }) : kind === "scoped" ? /* @__PURE__ */ jsx14(ScopedEditor, {
1764
+ ...p
1765
+ }) : /* @__PURE__ */ jsx14(ConsumeEditor, {
1766
+ ...p
1767
+ })
1768
+ }),
1769
+ b ? /* @__PURE__ */ jsxs12("footer", {
1770
+ className: "abs-slot-f" + (state === "suggested" ? "" : " is-plain"),
1771
+ children: [
1772
+ state === "suggested" ? /* @__PURE__ */ jsxs12("button", {
1773
+ type: "button",
1774
+ className: "abs-btn abs-btn-primary",
1775
+ onClick: () => p.put({ ...b, state: "confirmed" }),
1776
+ children: [
1777
+ /* @__PURE__ */ jsx14(Icon, {
1778
+ name: "check"
1779
+ }),
1780
+ " Confirm"
1781
+ ]
1782
+ }) : null,
1783
+ /* @__PURE__ */ jsx14("button", {
1784
+ type: "button",
1785
+ className: "abs-btn abs-btn-ghost",
1786
+ onClick: p.drop,
1787
+ children: state === "suggested" ? "Discard" : b.type === "none" ? "Undo" : "Disconnect"
1788
+ })
1789
+ ]
1790
+ }) : null
1791
+ ]
1792
+ });
1793
+ }
1794
+
1795
+ // src/studio/Inspector.tsx
1796
+ import { jsx as jsx15, jsxs as jsxs13, Fragment as Fragment7 } from "react/jsx-runtime";
1797
+ function Inspector({ engine, screen, comp, put, drop, goTo, onClose }) {
1798
+ const d = engine.descOf(comp);
1799
+ const parent = comp.parent ? engine.compOf(screen.id, comp.parent) : undefined;
1800
+ return /* @__PURE__ */ jsxs13("aside", {
1801
+ className: "abs-insp abs-enter",
1802
+ "aria-label": `${comp.label} settings`,
1803
+ children: [
1804
+ /* @__PURE__ */ jsxs13("header", {
1805
+ className: "abs-insp-h",
1806
+ children: [
1807
+ /* @__PURE__ */ jsx15(KindIcon, {
1808
+ svg: d.icon
1809
+ }),
1810
+ /* @__PURE__ */ jsxs13("div", {
1811
+ children: [
1812
+ /* @__PURE__ */ jsx15("h2", {
1813
+ children: comp.label
1814
+ }),
1815
+ /* @__PURE__ */ jsxs13("span", {
1816
+ children: [
1817
+ d.label,
1818
+ parent && /* @__PURE__ */ jsxs13(Fragment7, {
1819
+ children: [
1820
+ " in ",
1821
+ /* @__PURE__ */ jsx15("button", {
1822
+ type: "button",
1823
+ className: "abs-link",
1824
+ onClick: () => goTo(parent.bindId),
1825
+ children: parent.label
1826
+ })
1827
+ ]
1828
+ })
1829
+ ]
1830
+ })
1831
+ ]
1832
+ }),
1833
+ /* @__PURE__ */ jsx15("button", {
1834
+ type: "button",
1835
+ className: "abs-iconbtn",
1836
+ onClick: onClose,
1837
+ "aria-label": "Close panel",
1838
+ children: /* @__PURE__ */ jsx15(Icon, {
1839
+ name: "close"
1840
+ })
1841
+ })
1842
+ ]
1843
+ }),
1844
+ /* @__PURE__ */ jsx15("div", {
1845
+ className: "abs-insp-b abs-scroll",
1846
+ children: engine.bindable(d).map((sl) => /* @__PURE__ */ jsx15(SlotSection, {
1847
+ engine,
1848
+ screen: screen.id,
1849
+ comp,
1850
+ slot: sl,
1851
+ put,
1852
+ drop: () => drop(comp.bindId, sl.id),
1853
+ goTo
1854
+ }, comp.bindId + sl.id))
1855
+ })
1856
+ ]
1857
+ });
1858
+ }
1859
+
1860
+ // src/studio/Navigator.tsx
1861
+ import * as React10 from "react";
1862
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
1863
+ function buildTree(screens) {
1864
+ const root = { key: "", name: "", nodes: [], screens: [], all: [] };
1865
+ for (const s of screens) {
1866
+ let node = root;
1867
+ node.all.push(s);
1868
+ for (const seg of s.path.length ? s.path : ["Other"]) {
1869
+ const key = node.key + "/" + seg;
1870
+ let next = node.nodes.find((n) => n.key === key);
1871
+ if (!next) {
1872
+ next = { key, name: seg, nodes: [], screens: [], all: [] };
1873
+ node.nodes.push(next);
1874
+ }
1875
+ node = next;
1876
+ node.all.push(s);
1877
+ }
1878
+ if (!s.parent)
1879
+ node.screens.push(s);
1880
+ }
1881
+ return root.nodes;
1882
+ }
1883
+ function Navigator({ engine, current, onPick }) {
1884
+ const [q, setQ] = React10.useState("");
1885
+ const [open, setOpen] = React10.useState({});
1886
+ const screens = engine.fw.registry.screens;
1887
+ const tree = React10.useMemo(() => buildTree(screens), [screens]);
1888
+ const byParent = React10.useMemo(() => {
1889
+ const m = new Map;
1890
+ screens.forEach((s) => {
1891
+ if (s.parent)
1892
+ m.set(s.parent, [...m.get(s.parent) ?? [], s]);
1893
+ });
1894
+ return m;
1895
+ }, [screens]);
1896
+ const needle = q.trim().toLowerCase();
1897
+ const match = (s) => !needle || [s.label, ...s.path].join(" ").toLowerCase().includes(needle);
1898
+ React10.useEffect(() => {
1899
+ const path = screens.find((s) => s.id === current)?.path ?? [];
1900
+ setOpen((o) => {
1901
+ const n = { ...o };
1902
+ path.forEach((_, i) => {
1903
+ n["/" + path.slice(0, i + 1).join("/")] = true;
1904
+ });
1905
+ return n;
1906
+ });
1907
+ }, [current, screens]);
1908
+ const glide = useGlide();
1909
+ const screenRow = (s, depth) => {
1910
+ const kids = byParent.get(s.id) ?? [];
1911
+ if (!(match(s) || kids.some(match)))
1912
+ return null;
1913
+ const t = engine.tally([s]);
1914
+ return /* @__PURE__ */ jsxs14(React10.Fragment, {
1915
+ children: [
1916
+ /* @__PURE__ */ jsxs14("button", {
1917
+ type: "button",
1918
+ "data-glide": true,
1919
+ className: "abs-nav-item" + (s.id === current ? " is-on" : ""),
1920
+ style: { paddingLeft: 12 + depth * 16 },
1921
+ onClick: () => onPick(s.id),
1922
+ "aria-current": s.id === current ? "page" : undefined,
1923
+ "aria-label": `${s.label}, ${t.bound} of ${t.total} connected`,
1924
+ children: [
1925
+ /* @__PURE__ */ jsx16(Icon, {
1926
+ name: s.parent ? "dialog" : "screen"
1927
+ }),
1928
+ /* @__PURE__ */ jsx16("span", {
1929
+ className: "abs-nav-l",
1930
+ children: s.label
1931
+ }),
1932
+ t.total > 0 && t.bound === t.total ? /* @__PURE__ */ jsx16(Icon, {
1933
+ name: "check",
1934
+ size: 14,
1935
+ className: "abs-c-ok"
1936
+ }) : /* @__PURE__ */ jsxs14("span", {
1937
+ className: "abs-nav-count",
1938
+ children: [
1939
+ t.bound,
1940
+ "/",
1941
+ t.total
1942
+ ]
1943
+ })
1944
+ ]
1945
+ }),
1946
+ kids.map((k) => screenRow(k, depth + 1))
1947
+ ]
1948
+ }, s.id);
1949
+ };
1950
+ const group = (n, depth) => {
1951
+ if (needle && !n.all.some(match))
1952
+ return null;
1953
+ const isOpen = !!needle || !!open[n.key];
1954
+ const t = engine.tally(n.all);
1955
+ const holdsCurrent = n.all.some((s) => s.id === current);
1956
+ return /* @__PURE__ */ jsxs14("div", {
1957
+ className: "abs-tree",
1958
+ role: "treeitem",
1959
+ "aria-expanded": isOpen,
1960
+ children: [
1961
+ /* @__PURE__ */ jsxs14("button", {
1962
+ type: "button",
1963
+ "data-glide": true,
1964
+ className: "abs-nav-group-h" + (holdsCurrent ? " has-current" : ""),
1965
+ style: { paddingLeft: 12 + depth * 16 },
1966
+ onClick: () => setOpen((o) => ({ ...o, [n.key]: !isOpen })),
1967
+ "aria-expanded": isOpen,
1968
+ "aria-label": `${n.name}, ${t.bound} of ${t.total} connected`,
1969
+ children: [
1970
+ /* @__PURE__ */ jsx16("span", {
1971
+ className: "abs-nav-l",
1972
+ children: n.name
1973
+ }),
1974
+ /* @__PURE__ */ jsxs14("span", {
1975
+ className: "abs-nav-count",
1976
+ children: [
1977
+ t.bound,
1978
+ "/",
1979
+ t.total
1980
+ ]
1981
+ }),
1982
+ /* @__PURE__ */ jsx16(Icon, {
1983
+ name: "chevronRight",
1984
+ size: 14,
1985
+ className: "abs-chev" + (isOpen ? " abs-rot90" : "")
1986
+ })
1987
+ ]
1988
+ }),
1989
+ /* @__PURE__ */ jsx16(Collapse, {
1990
+ open: isOpen,
1991
+ children: /* @__PURE__ */ jsxs14("div", {
1992
+ role: "group",
1993
+ children: [
1994
+ n.nodes.map((x) => group(x, depth + 1)),
1995
+ n.screens.map((s) => screenRow(s, depth + 1))
1996
+ ]
1997
+ })
1998
+ })
1999
+ ]
2000
+ }, n.key);
2001
+ };
2002
+ return /* @__PURE__ */ jsxs14("nav", {
2003
+ className: "abs-nav",
2004
+ "aria-label": "Screens",
2005
+ children: [
2006
+ /* @__PURE__ */ jsx16("div", {
2007
+ className: "abs-nav-head",
2008
+ children: /* @__PURE__ */ jsxs14("div", {
2009
+ className: "abs-search",
2010
+ children: [
2011
+ /* @__PURE__ */ jsx16(Icon, {
2012
+ name: "search",
2013
+ className: "abs-c-muted",
2014
+ size: 14
2015
+ }),
2016
+ /* @__PURE__ */ jsx16("input", {
2017
+ value: q,
2018
+ onChange: (e) => setQ(e.target.value),
2019
+ placeholder: "Filter screens",
2020
+ "aria-label": "Filter screens"
2021
+ })
2022
+ ]
2023
+ })
2024
+ }),
2025
+ /* @__PURE__ */ jsxs14("div", {
2026
+ className: "abs-nav-scroll abs-scroll abs-glide-box",
2027
+ role: "tree",
2028
+ "aria-label": "Products",
2029
+ ref: glide.ref,
2030
+ ...glide.handlers,
2031
+ children: [
2032
+ glide.indicator,
2033
+ /* @__PURE__ */ jsx16("div", {
2034
+ className: "abs-eyebrow abs-eyebrow-sm",
2035
+ children: "Products"
2036
+ }),
2037
+ tree.map((n) => group(n, 0)),
2038
+ needle && !screens.some(match) ? /* @__PURE__ */ jsxs14("div", {
2039
+ className: "abs-empty-sm",
2040
+ children: [
2041
+ "No screen matches “",
2042
+ q,
2043
+ "”."
2044
+ ]
2045
+ }) : null
2046
+ ]
2047
+ })
2048
+ ]
2049
+ });
2050
+ }
2051
+
2052
+ // src/studio/SettingsPanel.tsx
2053
+ import * as React12 from "react";
2054
+
2055
+ // src/studio/useSettings.ts
2056
+ import * as React11 from "react";
2057
+ var DEFAULT_DOCS_URL = "https://apidocs.highradius.com/documentation-ui/index.yaml";
2058
+ var KEY = "abs.settings.v1";
2059
+ function read(defaults) {
2060
+ try {
2061
+ const raw = globalThis.localStorage?.getItem(KEY);
2062
+ const saved = raw ? JSON.parse(raw) : {};
2063
+ return { docsUrl: typeof saved.docsUrl === "string" && saved.docsUrl ? saved.docsUrl : defaults.docsUrl };
2064
+ } catch {
2065
+ return defaults;
2066
+ }
2067
+ }
2068
+ function useSettings(defaults) {
2069
+ const [settings, setSettings] = React11.useState(() => read(defaults));
2070
+ const update = React11.useCallback((patch) => {
2071
+ setSettings((prev) => {
2072
+ const next = { ...prev, ...patch };
2073
+ try {
2074
+ globalThis.localStorage?.setItem(KEY, JSON.stringify(next));
2075
+ } catch {}
2076
+ return next;
2077
+ });
2078
+ }, []);
2079
+ return [settings, update];
2080
+ }
2081
+
2082
+ // src/studio/SettingsPanel.tsx
2083
+ import { jsx as jsx17, jsxs as jsxs15, Fragment as Fragment9 } from "react/jsx-runtime";
2084
+ var TIME = new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" });
2085
+ function isUrl(v) {
2086
+ try {
2087
+ const u = new URL(v, location.href);
2088
+ return u.protocol === "https:" || u.protocol === "http:" || u.protocol === "blob:";
2089
+ } catch {
2090
+ return false;
2091
+ }
2092
+ }
2093
+ function docsSummary(c) {
2094
+ if (c.status === "loading")
2095
+ return "Loading…";
2096
+ if (c.status === "error")
2097
+ return c.error;
2098
+ const products = new Set(c.catalog.operations.map((o) => o.product).filter(Boolean));
2099
+ return `${products.size || 1} ${products.size === 1 ? "product" : "products"} · ${c.catalog.operations.length} APIs · loaded ${TIME.format(c.loadedAt)}`;
2100
+ }
2101
+ function SettingsPanel({ docsUrl, onDocsUrl, catalog, onReload, manifests, onImport, onClose }) {
2102
+ const [draft, setDraft] = React12.useState(docsUrl);
2103
+ const [importMsg, setImportMsg] = React12.useState(null);
2104
+ const panel = React12.useRef(null);
2105
+ const file = React12.useRef(null);
2106
+ const valid = isUrl(draft);
2107
+ const dirty = draft.trim() !== docsUrl;
2108
+ React12.useEffect(() => {
2109
+ const key = (e) => {
2110
+ if (e.key === "Escape") {
2111
+ e.preventDefault();
2112
+ onClose();
2113
+ }
2114
+ };
2115
+ const away = (e) => {
2116
+ if (!panel.current?.contains(e.target) && !e.target.closest?.(".abs-settings-btn"))
2117
+ onClose();
2118
+ };
2119
+ window.addEventListener("keydown", key);
2120
+ document.addEventListener("pointerdown", away);
2121
+ return () => {
2122
+ window.removeEventListener("keydown", key);
2123
+ document.removeEventListener("pointerdown", away);
2124
+ };
2125
+ }, [onClose]);
2126
+ const apply = (e) => {
2127
+ e.preventDefault();
2128
+ if (!valid)
2129
+ return;
2130
+ if (dirty)
2131
+ onDocsUrl(draft.trim());
2132
+ else
2133
+ onReload();
2134
+ };
2135
+ const download = () => {
2136
+ const blob = new Blob([JSON.stringify(manifests(), null, 2) + `
2137
+ `], { type: "application/json" });
2138
+ const a = document.createElement("a");
2139
+ a.href = URL.createObjectURL(blob);
2140
+ a.download = "bindings.json";
2141
+ a.click();
2142
+ setTimeout(() => URL.revokeObjectURL(a.href), 1000);
2143
+ };
2144
+ const readFile = async (f) => {
2145
+ if (!f)
2146
+ return;
2147
+ try {
2148
+ const list = parseManifests(await f.text());
2149
+ onImport(list);
2150
+ setImportMsg({ ok: true, text: `Imported ${list.length} ${list.length === 1 ? "screen" : "screens"}.` });
2151
+ } catch (err) {
2152
+ setImportMsg({ ok: false, text: err instanceof Error ? err.message : "The file could not be read." });
2153
+ } finally {
2154
+ if (file.current)
2155
+ file.current.value = "";
2156
+ }
2157
+ };
2158
+ return /* @__PURE__ */ jsxs15("div", {
2159
+ className: "abs-settings abs-pop",
2160
+ ref: panel,
2161
+ role: "dialog",
2162
+ "aria-label": "Settings",
2163
+ children: [
2164
+ /* @__PURE__ */ jsxs15("form", {
2165
+ className: "abs-set-sec",
2166
+ onSubmit: apply,
2167
+ children: [
2168
+ /* @__PURE__ */ jsx17("label", {
2169
+ className: "abs-set-h",
2170
+ htmlFor: "abs-docs-url",
2171
+ children: "API docs"
2172
+ }),
2173
+ /* @__PURE__ */ jsx17("p", {
2174
+ className: "abs-set-d",
2175
+ children: "The OpenAPI file every API in the studio is read from."
2176
+ }),
2177
+ /* @__PURE__ */ jsxs15("div", {
2178
+ className: "abs-set-row",
2179
+ children: [
2180
+ /* @__PURE__ */ jsx17("input", {
2181
+ id: "abs-docs-url",
2182
+ name: "docsUrl",
2183
+ type: "url",
2184
+ inputMode: "url",
2185
+ autoComplete: "off",
2186
+ spellCheck: false,
2187
+ className: "abs-input" + (valid ? "" : " is-invalid"),
2188
+ value: draft,
2189
+ onChange: (e) => setDraft(e.target.value),
2190
+ "aria-invalid": !valid,
2191
+ "aria-describedby": "abs-docs-status"
2192
+ }),
2193
+ /* @__PURE__ */ jsx17("button", {
2194
+ type: "submit",
2195
+ className: "abs-btn abs-btn-primary",
2196
+ disabled: !valid || catalog.status === "loading",
2197
+ children: dirty ? "Use" : /* @__PURE__ */ jsxs15(Fragment9, {
2198
+ children: [
2199
+ /* @__PURE__ */ jsx17(Icon, {
2200
+ name: "refresh",
2201
+ size: 14,
2202
+ className: catalog.status === "loading" ? "abs-spin" : ""
2203
+ }),
2204
+ "Reload"
2205
+ ]
2206
+ })
2207
+ })
2208
+ ]
2209
+ }),
2210
+ /* @__PURE__ */ jsx17("p", {
2211
+ id: "abs-docs-status",
2212
+ className: "abs-set-status" + (catalog.status === "error" ? " is-error" : ""),
2213
+ "aria-live": "polite",
2214
+ children: valid ? docsSummary(catalog) : "Enter a full URL, for example " + DEFAULT_DOCS_URL
2215
+ }),
2216
+ docsUrl !== DEFAULT_DOCS_URL ? /* @__PURE__ */ jsx17("button", {
2217
+ type: "button",
2218
+ className: "abs-link abs-more",
2219
+ onClick: () => {
2220
+ setDraft(DEFAULT_DOCS_URL);
2221
+ onDocsUrl(DEFAULT_DOCS_URL);
2222
+ },
2223
+ children: "Use the default URL"
2224
+ }) : null
2225
+ ]
2226
+ }),
2227
+ /* @__PURE__ */ jsxs15("div", {
2228
+ className: "abs-set-sec",
2229
+ children: [
2230
+ /* @__PURE__ */ jsx17("span", {
2231
+ className: "abs-set-h",
2232
+ children: "Bindings"
2233
+ }),
2234
+ /* @__PURE__ */ jsx17("p", {
2235
+ className: "abs-set-d",
2236
+ children: "Download to commit them with the app. Import replaces the screens the file contains."
2237
+ }),
2238
+ /* @__PURE__ */ jsxs15("div", {
2239
+ className: "abs-set-row",
2240
+ children: [
2241
+ /* @__PURE__ */ jsxs15("button", {
2242
+ type: "button",
2243
+ className: "abs-btn",
2244
+ onClick: download,
2245
+ children: [
2246
+ /* @__PURE__ */ jsx17(Icon, {
2247
+ name: "download",
2248
+ size: 14
2249
+ }),
2250
+ "Download"
2251
+ ]
2252
+ }),
2253
+ /* @__PURE__ */ jsxs15("button", {
2254
+ type: "button",
2255
+ className: "abs-btn",
2256
+ onClick: () => file.current?.click(),
2257
+ children: [
2258
+ /* @__PURE__ */ jsx17(Icon, {
2259
+ name: "upload",
2260
+ size: 14
2261
+ }),
2262
+ "Import…"
2263
+ ]
2264
+ }),
2265
+ /* @__PURE__ */ jsx17("input", {
2266
+ ref: file,
2267
+ type: "file",
2268
+ accept: "application/json,.json",
2269
+ hidden: true,
2270
+ onChange: (e) => void readFile(e.target.files?.[0])
2271
+ })
2272
+ ]
2273
+ }),
2274
+ importMsg ? /* @__PURE__ */ jsx17("p", {
2275
+ className: "abs-set-status" + (importMsg.ok ? "" : " is-error"),
2276
+ role: "status",
2277
+ children: importMsg.text
2278
+ }) : null
2279
+ ]
2280
+ })
2281
+ ]
2282
+ });
2283
+ }
2284
+
2285
+ // src/studio/useBindings.ts
2286
+ import * as React13 from "react";
2287
+ function useBindings(fw, adapter) {
2288
+ const [index, setIndex] = React13.useState({});
2289
+ const [loaded, setLoaded] = React13.useState(false);
2290
+ const [status, setStatus] = React13.useState("idle");
2291
+ const [removed, setRemoved] = React13.useState(null);
2292
+ const pending = React13.useRef(new Map);
2293
+ const latest = React13.useRef(index);
2294
+ latest.current = index;
2295
+ const screenIds = fw?.registry.screens.map((s) => s.id).join(`
2296
+ `) ?? "";
2297
+ React13.useEffect(() => {
2298
+ if (!screenIds)
2299
+ return;
2300
+ let live = true;
2301
+ loadAll(adapter, screenIds.split(`
2302
+ `)).then((i) => {
2303
+ if (live) {
2304
+ setIndex(i);
2305
+ setLoaded(true);
2306
+ }
2307
+ });
2308
+ return () => {
2309
+ live = false;
2310
+ };
2311
+ }, [adapter, screenIds]);
2312
+ const flush = React13.useCallback(async () => {
2313
+ if (!pending.current.size || !fw)
2314
+ return;
2315
+ setStatus("saving");
2316
+ const failed = await savePending(adapter, fw, latest.current, pending.current);
2317
+ setStatus(failed.length ? "failed" : pending.current.size ? "saving" : "saved");
2318
+ }, [adapter, fw]);
2319
+ React13.useEffect(() => {
2320
+ if (!pending.current.size)
2321
+ return;
2322
+ setStatus("saving");
2323
+ const t = setTimeout(flush, 400);
2324
+ return () => clearTimeout(t);
2325
+ }, [index, flush]);
2326
+ React13.useEffect(() => () => {
2327
+ flush();
2328
+ }, [flush]);
2329
+ React13.useEffect(() => {
2330
+ const warn = (e) => {
2331
+ if (pending.current.size)
2332
+ e.preventDefault();
2333
+ };
2334
+ window.addEventListener("beforeunload", warn);
2335
+ return () => window.removeEventListener("beforeunload", warn);
2336
+ }, []);
2337
+ const put = React13.useCallback((screen, b) => {
2338
+ markDirty(pending.current, screen);
2339
+ setIndex((i) => putBinding(i, screen, b));
2340
+ }, []);
2341
+ const drop = React13.useCallback((screen, component, slot) => {
2342
+ const binding = latest.current[screen]?.find((x) => x.component === component && x.slot === slot);
2343
+ if (binding)
2344
+ setRemoved({ screen, binding });
2345
+ markDirty(pending.current, screen);
2346
+ setIndex((i) => dropBinding(i, screen, component, slot));
2347
+ }, []);
2348
+ const undo = React13.useCallback(() => {
2349
+ if (removed)
2350
+ put(removed.screen, removed.binding);
2351
+ setRemoved(null);
2352
+ }, [put, removed]);
2353
+ const dismiss = React13.useCallback(() => setRemoved(null), []);
2354
+ const importManifests = React13.useCallback((manifests) => {
2355
+ manifests.forEach((m) => markDirty(pending.current, m.screen));
2356
+ setIndex((i) => ({ ...i, ...Object.fromEntries(manifests.map((m) => [m.screen, m.bindings])) }));
2357
+ }, []);
2358
+ return { index, loaded, status, removed, put, drop, undo, dismiss, importManifests, retry: flush };
2359
+ }
2360
+
2361
+ // src/studio/useCatalog.ts
2362
+ import * as React14 from "react";
2363
+
2364
+ // src/framework/loadCatalog.ts
2365
+ import { parse } from "yaml";
2366
+ class CatalogLoadError extends Error {
2367
+ url;
2368
+ constructor(message, url) {
2369
+ super(message);
2370
+ this.url = url;
2371
+ this.name = "CatalogLoadError";
2372
+ }
2373
+ }
2374
+ async function loadCatalog(url, init) {
2375
+ let res;
2376
+ try {
2377
+ res = await fetch(url, { ...init, headers: { Accept: "application/yaml, application/json, text/yaml, */*", ...init?.headers } });
2378
+ } catch {
2379
+ throw new CatalogLoadError("The docs could not be reached. Check the URL, your network, and that the docs server allows this site (CORS).", url);
2380
+ }
2381
+ if (!res.ok)
2382
+ throw new CatalogLoadError(`The docs server answered ${res.status} ${res.statusText}. Check the URL.`, url);
2383
+ const text = await res.text();
2384
+ let doc;
2385
+ try {
2386
+ doc = text.trimStart().startsWith("{") ? JSON.parse(text) : parse(text);
2387
+ } catch {
2388
+ throw new CatalogLoadError("The file is not valid JSON or YAML.", url);
2389
+ }
2390
+ try {
2391
+ return catalogFromOpenApi2(doc, { url });
2392
+ } catch (e) {
2393
+ throw new CatalogLoadError(e instanceof Error ? e.message : "The file is not an OpenAPI document.", url);
2394
+ }
2395
+ }
2396
+
2397
+ // src/studio/useCatalog.ts
2398
+ function useCatalog(url, provided) {
2399
+ const [state, setState] = React14.useState(() => provided ? { status: "ready", catalog: provided, loadedAt: new Date } : { status: "loading" });
2400
+ const [nonce, setNonce] = React14.useState(0);
2401
+ React14.useEffect(() => {
2402
+ if (provided) {
2403
+ setState({ status: "ready", catalog: provided, loadedAt: new Date });
2404
+ return;
2405
+ }
2406
+ const ctrl = new AbortController;
2407
+ setState((s) => ({ status: "loading", catalog: s.catalog }));
2408
+ loadCatalog(url, { signal: ctrl.signal }).then((catalog) => setState({ status: "ready", catalog, loadedAt: new Date }), (e) => {
2409
+ if (!ctrl.signal.aborted)
2410
+ setState((s) => ({ status: "error", catalog: s.catalog, error: e instanceof Error ? e.message : String(e) }));
2411
+ });
2412
+ return () => ctrl.abort();
2413
+ }, [url, provided, nonce]);
2414
+ const reload = React14.useCallback(() => setNonce((n) => n + 1), []);
2415
+ return [state, reload];
2416
+ }
2417
+
2418
+ // src/studio/BindingStudio.tsx
2419
+ import { jsx as jsx18, jsxs as jsxs16, Fragment as Fragment10 } from "react/jsx-runtime";
2420
+ function SaveState({ status, onRetry }) {
2421
+ return /* @__PURE__ */ jsx18("span", {
2422
+ className: "abs-saved is-" + status,
2423
+ "aria-live": "polite",
2424
+ children: status === "failed" ? /* @__PURE__ */ jsxs16("button", {
2425
+ type: "button",
2426
+ onClick: onRetry,
2427
+ children: [
2428
+ /* @__PURE__ */ jsx18(Icon, {
2429
+ name: "alert",
2430
+ size: 14
2431
+ }),
2432
+ "Not saved · Retry"
2433
+ ]
2434
+ }) : status === "saving" ? /* @__PURE__ */ jsxs16(Fragment10, {
2435
+ children: [
2436
+ /* @__PURE__ */ jsx18(Icon, {
2437
+ name: "refresh",
2438
+ size: 14,
2439
+ className: "abs-spin"
2440
+ }),
2441
+ "Saving…"
2442
+ ]
2443
+ }) : status === "saved" ? /* @__PURE__ */ jsxs16(Fragment10, {
2444
+ children: [
2445
+ /* @__PURE__ */ jsx18(Icon, {
2446
+ name: "check",
2447
+ size: 14
2448
+ }),
2449
+ "Saved"
2450
+ ]
2451
+ }) : null
2452
+ });
2453
+ }
2454
+ function UndoToast({ removed, label, onUndo, onDismiss }) {
2455
+ React15.useEffect(() => {
2456
+ const t = setTimeout(onDismiss, 6000);
2457
+ return () => clearTimeout(t);
2458
+ }, [removed, onDismiss]);
2459
+ return /* @__PURE__ */ jsxs16("div", {
2460
+ className: "abs-toast",
2461
+ role: "status",
2462
+ children: [
2463
+ /* @__PURE__ */ jsxs16("span", {
2464
+ children: [
2465
+ label,
2466
+ " disconnected"
2467
+ ]
2468
+ }),
2469
+ /* @__PURE__ */ jsx18("button", {
2470
+ type: "button",
2471
+ onClick: onUndo,
2472
+ children: "Undo"
2473
+ })
2474
+ ]
2475
+ });
2476
+ }
2477
+ var EMPTY_HINT = /* @__PURE__ */ jsxs16("aside", {
2478
+ className: "abs-insp abs-insp-empty abs-enter",
2479
+ children: [
2480
+ /* @__PURE__ */ jsx18("span", {
2481
+ className: "abs-empty-ico",
2482
+ children: /* @__PURE__ */ jsx18(Icon, {
2483
+ name: "pointer",
2484
+ size: 20
2485
+ })
2486
+ }),
2487
+ /* @__PURE__ */ jsx18("b", {
2488
+ children: "Select a component"
2489
+ }),
2490
+ /* @__PURE__ */ jsx18("span", {
2491
+ children: "Pick something from the list to connect it to an API."
2492
+ })
2493
+ ]
2494
+ });
2495
+ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, initialScreen, title = "Bind API", onClose }) {
2496
+ const [settings, updateSettings] = useSettings({ docsUrl });
2497
+ const [catalogState, reloadCatalog] = useCatalog(settings.docsUrl, framework.catalog);
2498
+ const [settingsOpen, setSettingsOpen] = React15.useState(false);
2499
+ const closeSettings = React15.useCallback(() => setSettingsOpen(false), []);
2500
+ const adapter = React15.useMemo(() => persistence ?? localStorageAdapter2(), [persistence]);
2501
+ const catalog = catalogState.catalog;
2502
+ const fw = React15.useMemo(() => catalog ? { ...framework, catalog, registry: validateRegistry2({ ...framework, catalog }).registry } : null, [framework, catalog]);
2503
+ const binds = useBindings(fw, adapter);
2504
+ const settingsButton = /* @__PURE__ */ jsxs16("div", {
2505
+ className: "abs-settings-wrap",
2506
+ children: [
2507
+ /* @__PURE__ */ jsxs16("button", {
2508
+ type: "button",
2509
+ className: "abs-iconbtn abs-settings-btn" + (settingsOpen ? " is-on" : ""),
2510
+ "aria-label": "Settings",
2511
+ "aria-expanded": settingsOpen,
2512
+ onClick: () => setSettingsOpen((o) => !o),
2513
+ children: [
2514
+ /* @__PURE__ */ jsx18(Icon, {
2515
+ name: "settings"
2516
+ }),
2517
+ catalogState.status === "error" ? /* @__PURE__ */ jsx18("span", {
2518
+ className: "abs-dot-bad",
2519
+ "aria-hidden": "true"
2520
+ }) : null
2521
+ ]
2522
+ }),
2523
+ onClose ? /* @__PURE__ */ jsx18("button", {
2524
+ type: "button",
2525
+ className: "abs-iconbtn",
2526
+ "aria-label": "Close",
2527
+ onClick: onClose,
2528
+ children: /* @__PURE__ */ jsx18(Icon, {
2529
+ name: "close"
2530
+ })
2531
+ }) : null,
2532
+ settingsOpen ? /* @__PURE__ */ jsx18(SettingsPanel, {
2533
+ docsUrl: settings.docsUrl,
2534
+ onDocsUrl: (url) => updateSettings({ docsUrl: url }),
2535
+ catalog: catalogState,
2536
+ onReload: reloadCatalog,
2537
+ manifests: () => fw ? Object.entries(binds.index).map(([s, b]) => toManifest2(fw, s, b)) : [],
2538
+ onImport: binds.importManifests,
2539
+ onClose: closeSettings
2540
+ }) : null
2541
+ ]
2542
+ });
2543
+ if (!fw) {
2544
+ return /* @__PURE__ */ jsxs16("div", {
2545
+ className: "abs-root",
2546
+ children: [
2547
+ /* @__PURE__ */ jsxs16("header", {
2548
+ className: "abs-top",
2549
+ children: [
2550
+ /* @__PURE__ */ jsx18("div", {
2551
+ className: "abs-brand",
2552
+ children: title
2553
+ }),
2554
+ /* @__PURE__ */ jsx18("span", {}),
2555
+ settingsButton
2556
+ ]
2557
+ }),
2558
+ /* @__PURE__ */ jsx18("div", {
2559
+ className: "abs-state-page",
2560
+ children: catalogState.status === "error" ? /* @__PURE__ */ jsxs16(Fragment10, {
2561
+ children: [
2562
+ /* @__PURE__ */ jsx18("span", {
2563
+ className: "abs-empty-ico is-bad",
2564
+ children: /* @__PURE__ */ jsx18(Icon, {
2565
+ name: "alert",
2566
+ size: 20
2567
+ })
2568
+ }),
2569
+ /* @__PURE__ */ jsx18("b", {
2570
+ children: "The API docs did not load"
2571
+ }),
2572
+ /* @__PURE__ */ jsx18("span", {
2573
+ children: catalogState.error
2574
+ }),
2575
+ /* @__PURE__ */ jsxs16("div", {
2576
+ className: "abs-set-row",
2577
+ children: [
2578
+ /* @__PURE__ */ jsxs16("button", {
2579
+ type: "button",
2580
+ className: "abs-btn abs-btn-primary",
2581
+ onClick: reloadCatalog,
2582
+ children: [
2583
+ /* @__PURE__ */ jsx18(Icon, {
2584
+ name: "refresh",
2585
+ size: 14
2586
+ }),
2587
+ "Try again"
2588
+ ]
2589
+ }),
2590
+ /* @__PURE__ */ jsx18("button", {
2591
+ type: "button",
2592
+ className: "abs-btn",
2593
+ onClick: () => setSettingsOpen(true),
2594
+ children: "Change the URL"
2595
+ })
2596
+ ]
2597
+ })
2598
+ ]
2599
+ }) : /* @__PURE__ */ jsxs16(Fragment10, {
2600
+ children: [
2601
+ /* @__PURE__ */ jsx18("span", {
2602
+ className: "abs-loader",
2603
+ "aria-hidden": "true"
2604
+ }),
2605
+ /* @__PURE__ */ jsx18("span", {
2606
+ "aria-live": "polite",
2607
+ children: "Loading API docs…"
2608
+ })
2609
+ ]
2610
+ })
2611
+ })
2612
+ ]
2613
+ });
2614
+ }
2615
+ return /* @__PURE__ */ jsx18(Workbench, {
2616
+ fw,
2617
+ binds,
2618
+ title,
2619
+ initialScreen,
2620
+ settingsButton
2621
+ });
2622
+ }
2623
+ function Workbench({ fw, binds, title, initialScreen, settingsButton }) {
2624
+ const { index, loaded, status, removed, put, drop, undo, dismiss, retry } = binds;
2625
+ const [place, go] = React15.useState({ screen: initialScreen });
2626
+ const engine = React15.useMemo(() => createEngine2(fw, index), [fw, index]);
2627
+ const screen = place.screen && engine.SCR.get(place.screen) || fw.registry.screens[0];
2628
+ const comp = screen && place.component ? engine.compOf(screen.id, place.component) : undefined;
2629
+ const screenId = screen?.id;
2630
+ const select = React15.useCallback((component) => go({ screen: screenId, component }), [go, screenId]);
2631
+ const putHere = React15.useCallback((b) => {
2632
+ if (screenId)
2633
+ put(screenId, b);
2634
+ }, [put, screenId]);
2635
+ const dropHere = React15.useCallback((c, s) => {
2636
+ if (screenId)
2637
+ drop(screenId, c, s);
2638
+ }, [drop, screenId]);
2639
+ if (!screen)
2640
+ return /* @__PURE__ */ jsx18("div", {
2641
+ className: "abs-root",
2642
+ children: /* @__PURE__ */ jsx18("div", {
2643
+ className: "abs-empty",
2644
+ children: "No screens are registered yet."
2645
+ })
2646
+ });
2647
+ const total = engine.tally(fw.registry.screens);
2648
+ const removedLabel = removed ? engine.compOf(removed.screen, removed.binding.component)?.label ?? removed.binding.component : "";
2649
+ return /* @__PURE__ */ jsxs16("div", {
2650
+ className: "abs-root",
2651
+ children: [
2652
+ /* @__PURE__ */ jsxs16("header", {
2653
+ className: "abs-top",
2654
+ children: [
2655
+ /* @__PURE__ */ jsx18("div", {
2656
+ className: "abs-brand",
2657
+ children: title
2658
+ }),
2659
+ /* @__PURE__ */ jsx18(FindAnything, {
2660
+ engine,
2661
+ onGo: (s, c) => go({ screen: s, component: c })
2662
+ }),
2663
+ /* @__PURE__ */ jsxs16("div", {
2664
+ className: "abs-top-meta",
2665
+ children: [
2666
+ /* @__PURE__ */ jsx18(SaveState, {
2667
+ status,
2668
+ onRetry: retry
2669
+ }),
2670
+ /* @__PURE__ */ jsxs16("span", {
2671
+ className: "abs-overall",
2672
+ title: `Components connected across all ${fw.registry.screens.length} screens`,
2673
+ children: [
2674
+ /* @__PURE__ */ jsx18("b", {
2675
+ children: total.bound
2676
+ }),
2677
+ "/",
2678
+ total.total,
2679
+ " connected"
2680
+ ]
2681
+ }),
2682
+ settingsButton
2683
+ ]
2684
+ })
2685
+ ]
2686
+ }),
2687
+ /* @__PURE__ */ jsxs16("div", {
2688
+ className: "abs-body" + (comp ? " has-insp" : ""),
2689
+ children: [
2690
+ /* @__PURE__ */ jsx18(Navigator, {
2691
+ engine,
2692
+ current: screen.id,
2693
+ onPick: (id) => go({ screen: id })
2694
+ }),
2695
+ loaded ? /* @__PURE__ */ jsx18(ComponentList, {
2696
+ engine,
2697
+ screen,
2698
+ selected: comp?.bindId,
2699
+ onSelect: select
2700
+ }, screen.id) : /* @__PURE__ */ jsx18("div", {
2701
+ className: "abs-center"
2702
+ }),
2703
+ comp ? /* @__PURE__ */ jsx18(Inspector, {
2704
+ engine,
2705
+ screen,
2706
+ comp,
2707
+ put: putHere,
2708
+ drop: dropHere,
2709
+ goTo: select,
2710
+ onClose: () => select(undefined)
2711
+ }, screen.id + comp.bindId) : loaded ? EMPTY_HINT : null
2712
+ ]
2713
+ }),
2714
+ removed ? /* @__PURE__ */ jsx18(UndoToast, {
2715
+ removed,
2716
+ label: removedLabel,
2717
+ onUndo: undo,
2718
+ onDismiss: dismiss
2719
+ }) : null
2720
+ ]
2721
+ });
2722
+ }
2723
+ // src/studio/BindingWidget.tsx
2724
+ import * as React16 from "react";
2725
+ import { jsx as jsx19, jsxs as jsxs17, Fragment as Fragment11 } from "react/jsx-runtime";
2726
+ var fallbackStore = localStorageAdapter2();
2727
+ function BindingWidget({ framework, docsUrl, title }) {
2728
+ const runtime = useBindingRuntime();
2729
+ const dialog = React16.useRef(null);
2730
+ const fab = React16.useRef(null);
2731
+ const [state, setState] = React16.useState("closed");
2732
+ const [screen, setScreen] = React16.useState();
2733
+ const open = () => {
2734
+ setScreen(runtime.activeScreen());
2735
+ setState("open");
2736
+ };
2737
+ const requestClose = React16.useCallback(() => setState((s) => s === "open" ? "closing" : s), []);
2738
+ React16.useLayoutEffect(() => {
2739
+ if (state === "open" && !dialog.current?.open)
2740
+ dialog.current?.showModal();
2741
+ }, [state]);
2742
+ const finishClose = (e) => {
2743
+ if (e.target !== dialog.current || state !== "closing")
2744
+ return;
2745
+ dialog.current?.close();
2746
+ setState("closed");
2747
+ runtime.reloadSaved();
2748
+ fab.current?.focus();
2749
+ };
2750
+ return /* @__PURE__ */ jsxs17(Fragment11, {
2751
+ children: [
2752
+ /* @__PURE__ */ jsxs17("button", {
2753
+ ref: fab,
2754
+ type: "button",
2755
+ className: "abs-fab",
2756
+ onClick: open,
2757
+ "aria-haspopup": "dialog",
2758
+ "aria-expanded": state !== "closed",
2759
+ children: [
2760
+ /* @__PURE__ */ jsx19("span", {
2761
+ className: "abs-fab-ico",
2762
+ children: /* @__PURE__ */ jsx19(Icon, {
2763
+ name: "link",
2764
+ size: 18
2765
+ })
2766
+ }),
2767
+ /* @__PURE__ */ jsx19("span", {
2768
+ className: "abs-fab-label",
2769
+ children: /* @__PURE__ */ jsx19("span", {
2770
+ children: title ?? "Bind API"
2771
+ })
2772
+ })
2773
+ ]
2774
+ }),
2775
+ state !== "closed" ? /* @__PURE__ */ jsx19("dialog", {
2776
+ ref: dialog,
2777
+ className: "abs-widget" + (state === "closing" ? " is-closing" : ""),
2778
+ "aria-label": title ?? "Bind API",
2779
+ onCancel: (e) => {
2780
+ e.preventDefault();
2781
+ requestClose();
2782
+ },
2783
+ onClick: (e) => {
2784
+ if (e.target === e.currentTarget)
2785
+ requestClose();
2786
+ },
2787
+ onAnimationEnd: finishClose,
2788
+ children: /* @__PURE__ */ jsx19(BindingStudio, {
2789
+ framework,
2790
+ docsUrl,
2791
+ title,
2792
+ initialScreen: screen,
2793
+ persistence: runtime.persistence ?? fallbackStore,
2794
+ onClose: requestClose
2795
+ })
2796
+ }) : null
2797
+ ]
2798
+ });
2799
+ }
2800
+ export {
2801
+ BindingStudio,
2802
+ BindingWidget
2803
+ };
2804
+
2805
+ //# debugId=34079C9B227D68B264756E2164756E21
2806
+ //# sourceMappingURL=index.js.map