api_binding_ui 2.0.2 → 3.1.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 (39) hide show
  1. package/README.md +68 -48
  2. package/SKILL.md +29 -34
  3. package/dist/app/index-4mgcb4wd.js +367 -0
  4. package/dist/app/index-aaxy714b.css +1 -0
  5. package/dist/app/index.html +7 -0
  6. package/dist/cli.js +1006 -0
  7. package/dist/codegen/generate.d.ts +22 -0
  8. package/dist/codegen/helpers.d.ts +2 -0
  9. package/dist/framework/contract.d.ts +38 -23
  10. package/dist/framework/expr.d.ts +26 -27
  11. package/dist/framework/file.d.ts +28 -0
  12. package/dist/framework/store.d.ts +3 -15
  13. package/dist/{index-3q1vx729.js → index-jj8twdw9.js} +158 -173
  14. package/dist/index-jj8twdw9.js.map +16 -0
  15. package/dist/index.d.ts +6 -8
  16. package/dist/index.js +461 -298
  17. package/dist/index.js.map +6 -8
  18. package/dist/overlay.js +24 -0
  19. package/dist/server/handler.d.ts +18 -0
  20. package/dist/studio/BindingStudio.d.ts +8 -11
  21. package/dist/studio/SettingsPanel.d.ts +3 -7
  22. package/dist/studio/index.css +0 -149
  23. package/dist/studio/index.d.ts +1 -2
  24. package/dist/studio/index.js +231 -438
  25. package/dist/studio/index.js.map +8 -10
  26. package/dist/studio/useBindings.d.ts +7 -5
  27. package/dist/studio/useCatalog.d.ts +2 -0
  28. package/dist/vite/plugin.d.ts +36 -0
  29. package/dist/vite.js +201 -0
  30. package/package.json +68 -52
  31. package/spec/FRAMEWORK.md +107 -201
  32. package/dist/framework/runner.d.ts +0 -36
  33. package/dist/index-3q1vx729.js.map +0 -16
  34. package/dist/runtime/BindingProvider.d.ts +0 -42
  35. package/dist/runtime/BindingScreen.d.ts +0 -23
  36. package/dist/runtime/hooks.d.ts +0 -22
  37. package/dist/runtime/screenStore.d.ts +0 -7
  38. package/dist/studio/BindingWidget.d.ts +0 -9
  39. package/dist/studio/useSettings.d.ts +0 -8
@@ -1,21 +1,16 @@
1
1
  import {
2
- catalogFromOpenApi2,
3
- localStorageAdapter2,
4
- toManifest2,
5
- markDirty,
6
- savePending,
7
- loadAll,
8
- putBinding,
9
- dropBinding,
10
- parseManifests,
2
+ catalogFromText2,
3
+ loadCatalog2,
11
4
  humanize,
5
+ screenOf2,
6
+ fromFile2,
7
+ toFile2,
12
8
  createEngine2,
13
- validateRegistry2,
14
- useBindingRuntime
15
- } from "../index-3q1vx729.js";
9
+ validateRegistry2
10
+ } from "../index-jj8twdw9.js";
16
11
 
17
12
  // src/studio/BindingStudio.tsx
18
- import * as React15 from "react";
13
+ import * as React14 from "react";
19
14
 
20
15
  // src/studio/ComponentList.tsx
21
16
  import * as React2 from "react";
@@ -2052,44 +2047,53 @@ function Navigator({ engine, current, onPick }) {
2052
2047
  // src/studio/SettingsPanel.tsx
2053
2048
  import * as React12 from "react";
2054
2049
 
2055
- // src/studio/useSettings.ts
2050
+ // src/studio/useCatalog.ts
2056
2051
  import * as React11 from "react";
2057
2052
  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;
2053
+ function useCatalog(url, provided) {
2054
+ const [state, setState] = React11.useState(() => provided ? { status: "ready", catalog: provided, loadedAt: new Date, source: "provided" } : { status: "loading" });
2055
+ const [nonce, setNonce] = React11.useState(0);
2056
+ const [pasted, setPasted] = React11.useState(null);
2057
+ React11.useEffect(() => {
2058
+ if (pasted) {
2059
+ setState({ status: "ready", catalog: pasted, loadedAt: new Date, source: "pasted" });
2060
+ return;
2061
+ }
2062
+ if (provided) {
2063
+ setState({ status: "ready", catalog: provided, loadedAt: new Date, source: "provided" });
2064
+ return;
2065
+ }
2066
+ if (!url)
2067
+ return;
2068
+ const ctrl = new AbortController;
2069
+ setState((s) => ({ status: "loading", catalog: s.catalog }));
2070
+ loadCatalog2(url, { signal: ctrl.signal }).then((catalog) => setState({ status: "ready", catalog, loadedAt: new Date, source: "url" }), (e) => {
2071
+ if (!ctrl.signal.aborted)
2072
+ setState((s) => ({ status: "error", catalog: s.catalog, error: e instanceof Error ? e.message : String(e) }));
2077
2073
  });
2074
+ return () => ctrl.abort();
2075
+ }, [url, provided, nonce, pasted]);
2076
+ const reload = React11.useCallback(() => {
2077
+ setPasted(null);
2078
+ setNonce((n) => n + 1);
2078
2079
  }, []);
2079
- return [settings, update];
2080
+ const loadPasted = React11.useCallback((text) => new Promise((resolve) => {
2081
+ setTimeout(() => {
2082
+ try {
2083
+ setPasted(catalogFromText2(text, "pasted"));
2084
+ resolve(undefined);
2085
+ } catch (e) {
2086
+ resolve(e instanceof Error ? e.message : String(e));
2087
+ }
2088
+ }, 0);
2089
+ }), []);
2090
+ return [state, reload, loadPasted];
2080
2091
  }
2081
2092
 
2082
2093
  // src/studio/SettingsPanel.tsx
2083
2094
  import { jsx as jsx17, jsxs as jsxs15, Fragment as Fragment9 } from "react/jsx-runtime";
2084
2095
  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
- }
2096
+ var isSource = (v) => v.trim().length > 0 && !/^\s*(javascript|data):/i.test(v);
2093
2097
  function docsSummary(c) {
2094
2098
  if (c.status === "loading")
2095
2099
  return "Loading…";
@@ -2182,14 +2186,12 @@ function PasteDocs({ onPaste, onDone }) {
2182
2186
  ]
2183
2187
  });
2184
2188
  }
2185
- function SettingsPanel({ docsUrl, onDocsUrl, catalog, onReload, onPaste, manifests, onImport, onClose }) {
2189
+ function SettingsPanel({ docsUrl, onDocsUrl, catalog, onReload, onPaste, onClose }) {
2186
2190
  const [draft, setDraft] = React12.useState(docsUrl);
2187
2191
  const [pasting, setPasting] = React12.useState(false);
2188
2192
  const usingPaste = catalog.status === "ready" && catalog.source === "pasted";
2189
- const [importMsg, setImportMsg] = React12.useState(null);
2190
2193
  const panel = React12.useRef(null);
2191
- const file = React12.useRef(null);
2192
- const valid = isUrl(draft);
2194
+ const valid = isSource(draft);
2193
2195
  const dirty = draft.trim() !== docsUrl;
2194
2196
  React12.useEffect(() => {
2195
2197
  const key = (e) => {
@@ -2218,223 +2220,144 @@ function SettingsPanel({ docsUrl, onDocsUrl, catalog, onReload, onPaste, manifes
2218
2220
  else
2219
2221
  onReload();
2220
2222
  };
2221
- const download = () => {
2222
- const blob = new Blob([JSON.stringify(manifests(), null, 2) + `
2223
- `], { type: "application/json" });
2224
- const a = document.createElement("a");
2225
- a.href = URL.createObjectURL(blob);
2226
- a.download = "bindings.json";
2227
- a.click();
2228
- setTimeout(() => URL.revokeObjectURL(a.href), 1000);
2229
- };
2230
- const readFile = async (f) => {
2231
- if (!f)
2232
- return;
2233
- try {
2234
- const list = parseManifests(await f.text());
2235
- onImport(list);
2236
- setImportMsg({ ok: true, text: `Imported ${list.length} ${list.length === 1 ? "screen" : "screens"}.` });
2237
- } catch (err) {
2238
- setImportMsg({ ok: false, text: err instanceof Error ? err.message : "The file could not be read." });
2239
- } finally {
2240
- if (file.current)
2241
- file.current.value = "";
2242
- }
2243
- };
2244
- return /* @__PURE__ */ jsxs15("div", {
2223
+ return /* @__PURE__ */ jsx17("div", {
2245
2224
  className: "abs-settings abs-pop",
2246
2225
  ref: panel,
2247
2226
  role: "dialog",
2248
2227
  "aria-label": "Settings",
2249
- children: [
2250
- /* @__PURE__ */ jsxs15("form", {
2251
- className: "abs-set-sec",
2252
- onSubmit: apply,
2253
- children: [
2254
- /* @__PURE__ */ jsx17("label", {
2255
- className: "abs-set-h",
2256
- htmlFor: "abs-docs-url",
2257
- children: "API docs"
2258
- }),
2259
- /* @__PURE__ */ jsx17("p", {
2260
- className: "abs-set-d",
2261
- children: "The OpenAPI file every API in the studio is read from."
2262
- }),
2263
- /* @__PURE__ */ jsxs15("div", {
2264
- className: "abs-set-row",
2265
- children: [
2266
- /* @__PURE__ */ jsx17("input", {
2267
- id: "abs-docs-url",
2268
- name: "docsUrl",
2269
- type: "url",
2270
- inputMode: "url",
2271
- autoComplete: "off",
2272
- spellCheck: false,
2273
- className: "abs-input" + (valid ? "" : " is-invalid"),
2274
- value: draft,
2275
- onChange: (e) => setDraft(e.target.value),
2276
- "aria-invalid": !valid,
2277
- "aria-describedby": "abs-docs-status"
2278
- }),
2279
- /* @__PURE__ */ jsx17("button", {
2280
- type: "submit",
2281
- className: "abs-btn abs-btn-primary",
2282
- disabled: !valid || catalog.status === "loading",
2283
- children: dirty ? "Use" : /* @__PURE__ */ jsxs15(Fragment9, {
2284
- children: [
2285
- /* @__PURE__ */ jsx17(Icon, {
2286
- name: "refresh",
2287
- size: 14,
2288
- className: catalog.status === "loading" ? "abs-spin" : ""
2289
- }),
2290
- "Reload"
2291
- ]
2292
- })
2293
- })
2294
- ]
2295
- }),
2296
- /* @__PURE__ */ jsx17("p", {
2297
- id: "abs-docs-status",
2298
- className: "abs-set-status" + (catalog.status === "error" ? " is-error" : ""),
2299
- "aria-live": "polite",
2300
- children: valid ? docsSummary(catalog) : "Enter a full URL, for example " + DEFAULT_DOCS_URL
2301
- }),
2302
- pasting ? /* @__PURE__ */ jsx17(PasteDocs, {
2303
- onPaste,
2304
- onDone: () => setPasting(false)
2305
- }) : /* @__PURE__ */ jsxs15("div", {
2306
- className: "abs-set-links",
2307
- children: [
2308
- usingPaste ? /* @__PURE__ */ jsxs15("button", {
2309
- type: "button",
2310
- className: "abs-link",
2311
- onClick: onReload,
2228
+ children: /* @__PURE__ */ jsxs15("form", {
2229
+ className: "abs-set-sec",
2230
+ onSubmit: apply,
2231
+ children: [
2232
+ /* @__PURE__ */ jsx17("label", {
2233
+ className: "abs-set-h",
2234
+ htmlFor: "abs-docs-url",
2235
+ children: "API docs"
2236
+ }),
2237
+ /* @__PURE__ */ jsx17("p", {
2238
+ className: "abs-set-d",
2239
+ children: "The OpenAPI file every API is read from: a URL, or a path next to the bindings file."
2240
+ }),
2241
+ /* @__PURE__ */ jsxs15("div", {
2242
+ className: "abs-set-row",
2243
+ children: [
2244
+ /* @__PURE__ */ jsx17("input", {
2245
+ id: "abs-docs-url",
2246
+ name: "docsUrl",
2247
+ type: "text",
2248
+ inputMode: "url",
2249
+ autoComplete: "off",
2250
+ spellCheck: false,
2251
+ className: "abs-input" + (valid ? "" : " is-invalid"),
2252
+ value: draft,
2253
+ onChange: (e) => setDraft(e.target.value),
2254
+ "aria-invalid": !valid,
2255
+ "aria-describedby": "abs-docs-status"
2256
+ }),
2257
+ /* @__PURE__ */ jsx17("button", {
2258
+ type: "submit",
2259
+ className: "abs-btn abs-btn-primary",
2260
+ disabled: !valid || catalog.status === "loading",
2261
+ children: dirty ? "Use" : /* @__PURE__ */ jsxs15(Fragment9, {
2312
2262
  children: [
2313
2263
  /* @__PURE__ */ jsx17(Icon, {
2314
2264
  name: "refresh",
2315
- size: 14
2265
+ size: 14,
2266
+ className: catalog.status === "loading" ? "abs-spin" : ""
2316
2267
  }),
2317
- "Use the URL again"
2268
+ "Reload"
2318
2269
  ]
2319
- }) : /* @__PURE__ */ jsxs15("button", {
2320
- type: "button",
2321
- className: "abs-link",
2322
- onClick: () => setPasting(true),
2323
- children: [
2324
- /* @__PURE__ */ jsx17(Icon, {
2325
- name: "upload",
2326
- size: 14
2327
- }),
2328
- "Paste the file instead"
2329
- ]
2330
- }),
2331
- docsUrl !== DEFAULT_DOCS_URL ? /* @__PURE__ */ jsx17("button", {
2332
- type: "button",
2333
- className: "abs-link",
2334
- onClick: () => {
2335
- setDraft(DEFAULT_DOCS_URL);
2336
- onDocsUrl(DEFAULT_DOCS_URL);
2337
- },
2338
- children: "Use the default URL"
2339
- }) : null
2340
- ]
2341
- })
2342
- ]
2343
- }),
2344
- /* @__PURE__ */ jsxs15("div", {
2345
- className: "abs-set-sec",
2346
- children: [
2347
- /* @__PURE__ */ jsx17("span", {
2348
- className: "abs-set-h",
2349
- children: "Bindings"
2350
- }),
2351
- /* @__PURE__ */ jsx17("p", {
2352
- className: "abs-set-d",
2353
- children: "Download to commit them with the app. Import replaces the screens the file contains."
2354
- }),
2355
- /* @__PURE__ */ jsxs15("div", {
2356
- className: "abs-set-row",
2357
- children: [
2358
- /* @__PURE__ */ jsxs15("button", {
2359
- type: "button",
2360
- className: "abs-btn",
2361
- onClick: download,
2362
- children: [
2363
- /* @__PURE__ */ jsx17(Icon, {
2364
- name: "download",
2365
- size: 14
2366
- }),
2367
- "Download"
2368
- ]
2369
- }),
2370
- /* @__PURE__ */ jsxs15("button", {
2371
- type: "button",
2372
- className: "abs-btn",
2373
- onClick: () => file.current?.click(),
2374
- children: [
2375
- /* @__PURE__ */ jsx17(Icon, {
2376
- name: "upload",
2377
- size: 14
2378
- }),
2379
- "Import…"
2380
- ]
2381
- }),
2382
- /* @__PURE__ */ jsx17("input", {
2383
- ref: file,
2384
- type: "file",
2385
- accept: "application/json,.json",
2386
- hidden: true,
2387
- onChange: (e) => void readFile(e.target.files?.[0])
2388
2270
  })
2389
- ]
2390
- }),
2391
- importMsg ? /* @__PURE__ */ jsx17("p", {
2392
- className: "abs-set-status" + (importMsg.ok ? "" : " is-error"),
2393
- role: "status",
2394
- children: importMsg.text
2395
- }) : null
2396
- ]
2397
- })
2398
- ]
2271
+ })
2272
+ ]
2273
+ }),
2274
+ /* @__PURE__ */ jsx17("p", {
2275
+ id: "abs-docs-status",
2276
+ className: "abs-set-status" + (catalog.status === "error" ? " is-error" : ""),
2277
+ "aria-live": "polite",
2278
+ children: valid ? docsSummary(catalog) : "Enter a URL or a path, for example " + DEFAULT_DOCS_URL
2279
+ }),
2280
+ pasting ? /* @__PURE__ */ jsx17(PasteDocs, {
2281
+ onPaste,
2282
+ onDone: () => setPasting(false)
2283
+ }) : /* @__PURE__ */ jsxs15("div", {
2284
+ className: "abs-set-links",
2285
+ children: [
2286
+ usingPaste ? /* @__PURE__ */ jsxs15("button", {
2287
+ type: "button",
2288
+ className: "abs-link",
2289
+ onClick: onReload,
2290
+ children: [
2291
+ /* @__PURE__ */ jsx17(Icon, {
2292
+ name: "refresh",
2293
+ size: 14
2294
+ }),
2295
+ "Use the URL again"
2296
+ ]
2297
+ }) : /* @__PURE__ */ jsxs15("button", {
2298
+ type: "button",
2299
+ className: "abs-link",
2300
+ onClick: () => setPasting(true),
2301
+ children: [
2302
+ /* @__PURE__ */ jsx17(Icon, {
2303
+ name: "upload",
2304
+ size: 14
2305
+ }),
2306
+ "Paste the file instead"
2307
+ ]
2308
+ }),
2309
+ docsUrl !== DEFAULT_DOCS_URL ? /* @__PURE__ */ jsx17("button", {
2310
+ type: "button",
2311
+ className: "abs-link",
2312
+ onClick: () => {
2313
+ setDraft(DEFAULT_DOCS_URL);
2314
+ onDocsUrl(DEFAULT_DOCS_URL);
2315
+ },
2316
+ children: "Use the default URL"
2317
+ }) : null
2318
+ ]
2319
+ })
2320
+ ]
2321
+ })
2399
2322
  });
2400
2323
  }
2401
2324
 
2402
2325
  // src/studio/useBindings.ts
2403
2326
  import * as React13 from "react";
2404
- function useBindings(fw, adapter) {
2405
- const [index, setIndex] = React13.useState({});
2406
- const [loaded, setLoaded] = React13.useState(false);
2327
+
2328
+ // src/framework/store.ts
2329
+ function putBinding(idx, screen, b) {
2330
+ const list = (idx[screen] ?? []).filter((x) => !(x.component === b.component && x.slot === b.slot));
2331
+ return { ...idx, [screen]: [...list, b] };
2332
+ }
2333
+ function dropBinding(idx, screen, component, slot) {
2334
+ return { ...idx, [screen]: (idx[screen] ?? []).filter((x) => !(x.component === component && x.slot === slot)) };
2335
+ }
2336
+
2337
+ // src/studio/useBindings.ts
2338
+ function useBindings(initial, save) {
2339
+ const [index, setIndex] = React13.useState(initial);
2407
2340
  const [status, setStatus] = React13.useState("idle");
2408
2341
  const [removed, setRemoved] = React13.useState(null);
2409
- const pending = React13.useRef(new Map);
2342
+ const edits = React13.useRef(0);
2410
2343
  const latest = React13.useRef(index);
2411
2344
  latest.current = index;
2412
- const screenIds = fw?.registry.screens.map((s) => s.id).join(`
2413
- `) ?? "";
2414
- React13.useEffect(() => {
2415
- if (!screenIds)
2416
- return;
2417
- let live = true;
2418
- loadAll(adapter, screenIds.split(`
2419
- `)).then((i) => {
2420
- if (live) {
2421
- setIndex(i);
2422
- setLoaded(true);
2423
- }
2424
- });
2425
- return () => {
2426
- live = false;
2427
- };
2428
- }, [adapter, screenIds]);
2429
2345
  const flush = React13.useCallback(async () => {
2430
- if (!pending.current.size || !fw)
2346
+ const at = edits.current;
2347
+ if (!at)
2431
2348
  return;
2432
2349
  setStatus("saving");
2433
- const failed = await savePending(adapter, fw, latest.current, pending.current);
2434
- setStatus(failed.length ? "failed" : pending.current.size ? "saving" : "saved");
2435
- }, [adapter, fw]);
2350
+ try {
2351
+ await save(latest.current);
2352
+ if (edits.current === at)
2353
+ edits.current = 0;
2354
+ setStatus(edits.current ? "saving" : "saved");
2355
+ } catch {
2356
+ setStatus("failed");
2357
+ }
2358
+ }, [save]);
2436
2359
  React13.useEffect(() => {
2437
- if (!pending.current.size)
2360
+ if (!edits.current)
2438
2361
  return;
2439
2362
  setStatus("saving");
2440
2363
  const t = setTimeout(flush, 400);
@@ -2445,21 +2368,21 @@ function useBindings(fw, adapter) {
2445
2368
  }, [flush]);
2446
2369
  React13.useEffect(() => {
2447
2370
  const warn = (e) => {
2448
- if (pending.current.size)
2371
+ if (edits.current)
2449
2372
  e.preventDefault();
2450
2373
  };
2451
2374
  window.addEventListener("beforeunload", warn);
2452
2375
  return () => window.removeEventListener("beforeunload", warn);
2453
2376
  }, []);
2454
2377
  const put = React13.useCallback((screen, b) => {
2455
- markDirty(pending.current, screen);
2378
+ edits.current++;
2456
2379
  setIndex((i) => putBinding(i, screen, b));
2457
2380
  }, []);
2458
2381
  const drop = React13.useCallback((screen, component, slot) => {
2459
2382
  const binding = latest.current[screen]?.find((x) => x.component === component && x.slot === slot);
2460
2383
  if (binding)
2461
2384
  setRemoved({ screen, binding });
2462
- markDirty(pending.current, screen);
2385
+ edits.current++;
2463
2386
  setIndex((i) => dropBinding(i, screen, component, slot));
2464
2387
  }, []);
2465
2388
  const undo = React13.useCallback(() => {
@@ -2468,95 +2391,12 @@ function useBindings(fw, adapter) {
2468
2391
  setRemoved(null);
2469
2392
  }, [put, removed]);
2470
2393
  const dismiss = React13.useCallback(() => setRemoved(null), []);
2471
- const importManifests = React13.useCallback((manifests) => {
2472
- manifests.forEach((m) => markDirty(pending.current, m.screen));
2473
- setIndex((i) => ({ ...i, ...Object.fromEntries(manifests.map((m) => [m.screen, m.bindings])) }));
2474
- }, []);
2475
- return { index, loaded, status, removed, put, drop, undo, dismiss, importManifests, retry: flush };
2476
- }
2477
-
2478
- // src/studio/useCatalog.ts
2479
- import * as React14 from "react";
2480
-
2481
- // src/framework/loadCatalog.ts
2482
- import { parse } from "yaml";
2483
- class CatalogLoadError extends Error {
2484
- source;
2485
- constructor(message, source) {
2486
- super(message);
2487
- this.source = source;
2488
- this.name = "CatalogLoadError";
2489
- }
2490
- }
2491
- function catalogFromText(text, source) {
2492
- const body = text.trim();
2493
- if (!body)
2494
- throw new CatalogLoadError("Nothing to read: the text is empty.", source);
2495
- let doc;
2496
- try {
2497
- doc = body.startsWith("{") ? JSON.parse(body) : parse(body);
2498
- } catch {
2499
- throw new CatalogLoadError("The text is not valid JSON or YAML.", source);
2500
- }
2501
- try {
2502
- return catalogFromOpenApi2(doc, source.includes("://") ? { url: source } : {});
2503
- } catch (e) {
2504
- throw new CatalogLoadError(e instanceof Error ? e.message : "The text is not an OpenAPI document.", source);
2505
- }
2506
- }
2507
- async function loadCatalog(url, init) {
2508
- let res;
2509
- try {
2510
- res = await fetch(url, { ...init, headers: { Accept: "application/yaml, application/json, text/yaml, */*", ...init?.headers } });
2511
- } catch {
2512
- throw new CatalogLoadError("The docs could not be reached. Check the URL, your network, and that the docs server allows this site (CORS).", url);
2513
- }
2514
- if (!res.ok)
2515
- throw new CatalogLoadError(`The docs server answered ${res.status} ${res.statusText}. Check the URL.`, url);
2516
- return catalogFromText(await res.text(), url);
2517
- }
2518
-
2519
- // src/studio/useCatalog.ts
2520
- function useCatalog(url, provided) {
2521
- const [state, setState] = React14.useState(() => provided ? { status: "ready", catalog: provided, loadedAt: new Date, source: "provided" } : { status: "loading" });
2522
- const [nonce, setNonce] = React14.useState(0);
2523
- const [pasted, setPasted] = React14.useState(null);
2524
- React14.useEffect(() => {
2525
- if (pasted) {
2526
- setState({ status: "ready", catalog: pasted, loadedAt: new Date, source: "pasted" });
2527
- return;
2528
- }
2529
- if (provided) {
2530
- setState({ status: "ready", catalog: provided, loadedAt: new Date, source: "provided" });
2531
- return;
2532
- }
2533
- const ctrl = new AbortController;
2534
- setState((s) => ({ status: "loading", catalog: s.catalog }));
2535
- loadCatalog(url, { signal: ctrl.signal }).then((catalog) => setState({ status: "ready", catalog, loadedAt: new Date, source: "url" }), (e) => {
2536
- if (!ctrl.signal.aborted)
2537
- setState((s) => ({ status: "error", catalog: s.catalog, error: e instanceof Error ? e.message : String(e) }));
2538
- });
2539
- return () => ctrl.abort();
2540
- }, [url, provided, nonce, pasted]);
2541
- const reload = React14.useCallback(() => {
2542
- setPasted(null);
2543
- setNonce((n) => n + 1);
2544
- }, []);
2545
- const loadPasted = React14.useCallback((text) => new Promise((resolve) => {
2546
- setTimeout(() => {
2547
- try {
2548
- setPasted(catalogFromText(text, "pasted"));
2549
- resolve(undefined);
2550
- } catch (e) {
2551
- resolve(e instanceof Error ? e.message : String(e));
2552
- }
2553
- }, 0);
2554
- }), []);
2555
- return [state, reload, loadPasted];
2394
+ return { index, loaded: true, status, removed, put, drop, undo, dismiss, retry: flush };
2556
2395
  }
2557
2396
 
2558
2397
  // src/studio/BindingStudio.tsx
2559
2398
  import { jsx as jsx18, jsxs as jsxs16, Fragment as Fragment10 } from "react/jsx-runtime";
2399
+ var same = (s) => s;
2560
2400
  function SaveState({ status, onRetry }) {
2561
2401
  return /* @__PURE__ */ jsx18("span", {
2562
2402
  className: "abs-saved is-" + status,
@@ -2592,7 +2432,7 @@ function SaveState({ status, onRetry }) {
2592
2432
  });
2593
2433
  }
2594
2434
  function UndoToast({ removed, label, onUndo, onDismiss }) {
2595
- React15.useEffect(() => {
2435
+ React14.useEffect(() => {
2596
2436
  const t = setTimeout(onDismiss, 6000);
2597
2437
  return () => clearTimeout(t);
2598
2438
  }, [removed, onDismiss]);
@@ -2632,15 +2472,45 @@ var EMPTY_HINT = /* @__PURE__ */ jsxs16("aside", {
2632
2472
  })
2633
2473
  ]
2634
2474
  });
2635
- function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, initialScreen, title = "Bind API", onClose }) {
2636
- const [settings, updateSettings] = useSettings({ docsUrl });
2637
- const [catalogState, reloadCatalog, loadPasted] = useCatalog(settings.docsUrl, framework.catalog);
2638
- const [settingsOpen, setSettingsOpen] = React15.useState(false);
2639
- const closeSettings = React15.useCallback(() => setSettingsOpen(false), []);
2640
- const adapter = React15.useMemo(() => persistence ?? localStorageAdapter2(), [persistence]);
2475
+ function BindingStudio({ store, docsUrl = same, initialScreen, initialComponent, title = "Bind API", onClose }) {
2476
+ const [file, setFile] = React14.useState(null);
2477
+ const [fileError, setFileError] = React14.useState();
2478
+ const latest = React14.useRef({ file: null, index: {} });
2479
+ React14.useEffect(() => {
2480
+ let live = true;
2481
+ store.load().then((f) => {
2482
+ if (live) {
2483
+ latest.current.file = f;
2484
+ setFile(f);
2485
+ }
2486
+ }, (e) => live && setFileError(e instanceof Error ? e.message : String(e)));
2487
+ return () => {
2488
+ live = false;
2489
+ };
2490
+ }, [store]);
2491
+ const [catalogState, reloadCatalog, loadPasted] = useCatalog(file ? docsUrl(file.openapi) : "");
2492
+ const [settingsOpen, setSettingsOpen] = React14.useState(false);
2493
+ const closeSettings = React14.useCallback(() => setSettingsOpen(false), []);
2641
2494
  const catalog = catalogState.catalog;
2642
- const fw = React15.useMemo(() => catalog ? { ...framework, catalog, registry: validateRegistry2({ ...framework, catalog }).registry } : null, [framework, catalog]);
2643
- const binds = useBindings(fw, adapter);
2495
+ const model = React14.useMemo(() => {
2496
+ if (!file || !catalog)
2497
+ return null;
2498
+ const { fw, index } = fromFile2(file, catalog);
2499
+ return { fw: { ...fw, registry: validateRegistry2(fw).registry }, index };
2500
+ }, [file?.kinds, file?.screens, file?.components, catalog]);
2501
+ const save = React14.useCallback(async (index) => {
2502
+ latest.current.index = index;
2503
+ if (latest.current.file)
2504
+ await store.save(toFile2(latest.current.file, index));
2505
+ }, [store]);
2506
+ const setOpenapi = (openapi) => {
2507
+ const f = latest.current.file;
2508
+ if (!f)
2509
+ return;
2510
+ latest.current.file = { ...f, openapi };
2511
+ setFile(latest.current.file);
2512
+ store.save(toFile2(latest.current.file, Object.keys(latest.current.index).length ? latest.current.index : model?.index ?? {}));
2513
+ };
2644
2514
  const settingsButton = /* @__PURE__ */ jsxs16("div", {
2645
2515
  className: "abs-settings-wrap",
2646
2516
  children: [
@@ -2669,19 +2539,18 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2669
2539
  name: "close"
2670
2540
  })
2671
2541
  }) : null,
2672
- settingsOpen ? /* @__PURE__ */ jsx18(SettingsPanel, {
2673
- docsUrl: settings.docsUrl,
2674
- onDocsUrl: (url) => updateSettings({ docsUrl: url }),
2542
+ settingsOpen && file ? /* @__PURE__ */ jsx18(SettingsPanel, {
2543
+ docsUrl: file.openapi,
2544
+ onDocsUrl: setOpenapi,
2675
2545
  catalog: catalogState,
2676
2546
  onReload: reloadCatalog,
2677
2547
  onPaste: loadPasted,
2678
- manifests: () => fw ? Object.entries(binds.index).map(([s, b]) => toManifest2(fw, s, b)) : [],
2679
- onImport: binds.importManifests,
2680
2548
  onClose: closeSettings
2681
2549
  }) : null
2682
2550
  ]
2683
2551
  });
2684
- if (!fw) {
2552
+ if (!model) {
2553
+ const error = fileError ?? (catalogState.status === "error" ? catalogState.error : undefined);
2685
2554
  return /* @__PURE__ */ jsxs16("div", {
2686
2555
  className: "abs-root",
2687
2556
  children: [
@@ -2698,7 +2567,7 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2698
2567
  }),
2699
2568
  /* @__PURE__ */ jsx18("div", {
2700
2569
  className: "abs-state-page",
2701
- children: catalogState.status === "error" ? /* @__PURE__ */ jsxs16(Fragment10, {
2570
+ children: error ? /* @__PURE__ */ jsxs16(Fragment10, {
2702
2571
  children: [
2703
2572
  /* @__PURE__ */ jsx18("span", {
2704
2573
  className: "abs-empty-ico is-bad",
@@ -2708,12 +2577,12 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2708
2577
  })
2709
2578
  }),
2710
2579
  /* @__PURE__ */ jsx18("b", {
2711
- children: "The API docs did not load"
2580
+ children: fileError ? "The bindings file did not load" : "The API docs did not load"
2712
2581
  }),
2713
2582
  /* @__PURE__ */ jsx18("span", {
2714
- children: catalogState.error
2583
+ children: error
2715
2584
  }),
2716
- /* @__PURE__ */ jsxs16("div", {
2585
+ fileError ? null : /* @__PURE__ */ jsxs16("div", {
2717
2586
  className: "abs-set-row",
2718
2587
  children: [
2719
2588
  /* @__PURE__ */ jsxs16("button", {
@@ -2745,7 +2614,7 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2745
2614
  }),
2746
2615
  /* @__PURE__ */ jsx18("span", {
2747
2616
  "aria-live": "polite",
2748
- children: "Loading API docs…"
2617
+ children: file ? "Loading API docs…" : "Loading bindings…"
2749
2618
  })
2750
2619
  ]
2751
2620
  })
@@ -2754,26 +2623,28 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2754
2623
  });
2755
2624
  }
2756
2625
  return /* @__PURE__ */ jsx18(Workbench, {
2757
- fw,
2758
- binds,
2626
+ fw: model.fw,
2627
+ initial: model.index,
2628
+ save,
2759
2629
  title,
2760
- initialScreen,
2630
+ initialScreen: initialComponent ? screenOf2(initialComponent, model.fw.registry.screens.map((s) => s.id)) ?? initialScreen : initialScreen,
2631
+ initialComponent,
2761
2632
  settingsButton
2762
2633
  });
2763
2634
  }
2764
- function Workbench({ fw, binds, title, initialScreen, settingsButton }) {
2765
- const { index, loaded, status, removed, put, drop, undo, dismiss, retry } = binds;
2766
- const [place, go] = React15.useState({ screen: initialScreen });
2767
- const engine = React15.useMemo(() => createEngine2(fw, index), [fw, index]);
2635
+ function Workbench({ fw, initial, save, title, initialScreen, initialComponent, settingsButton }) {
2636
+ const { index, loaded, status, removed, put, drop, undo, dismiss, retry } = useBindings(initial, save);
2637
+ const [place, go] = React14.useState({ screen: initialScreen, component: initialComponent });
2638
+ const engine = React14.useMemo(() => createEngine2(fw, index), [fw, index]);
2768
2639
  const screen = place.screen && engine.SCR.get(place.screen) || fw.registry.screens[0];
2769
2640
  const comp = screen && place.component ? engine.compOf(screen.id, place.component) : undefined;
2770
2641
  const screenId = screen?.id;
2771
- const select = React15.useCallback((component) => go({ screen: screenId, component }), [go, screenId]);
2772
- const putHere = React15.useCallback((b) => {
2642
+ const select = React14.useCallback((component) => go({ screen: screenId, component }), [go, screenId]);
2643
+ const putHere = React14.useCallback((b) => {
2773
2644
  if (screenId)
2774
2645
  put(screenId, b);
2775
2646
  }, [put, screenId]);
2776
- const dropHere = React15.useCallback((c, s) => {
2647
+ const dropHere = React14.useCallback((c, s) => {
2777
2648
  if (screenId)
2778
2649
  drop(screenId, c, s);
2779
2650
  }, [drop, screenId]);
@@ -2861,87 +2732,9 @@ function Workbench({ fw, binds, title, initialScreen, settingsButton }) {
2861
2732
  ]
2862
2733
  });
2863
2734
  }
2864
- // src/studio/BindingWidget.tsx
2865
- import * as React16 from "react";
2866
- import { jsx as jsx19, jsxs as jsxs17, Fragment as Fragment11 } from "react/jsx-runtime";
2867
- var fallbackStore = localStorageAdapter2();
2868
- function BindingWidget({ framework, docsUrl, title }) {
2869
- const runtime = useBindingRuntime();
2870
- const dialog = React16.useRef(null);
2871
- const fab = React16.useRef(null);
2872
- const [state, setState] = React16.useState("closed");
2873
- const [screen, setScreen] = React16.useState();
2874
- const open = () => {
2875
- setScreen(runtime.activeScreen());
2876
- setState("open");
2877
- };
2878
- const requestClose = React16.useCallback(() => setState((s) => s === "open" ? "closing" : s), []);
2879
- React16.useLayoutEffect(() => {
2880
- if (state === "open" && !dialog.current?.open)
2881
- dialog.current?.showModal();
2882
- }, [state]);
2883
- const finishClose = (e) => {
2884
- if (e.target !== dialog.current || state !== "closing")
2885
- return;
2886
- dialog.current?.close();
2887
- setState("closed");
2888
- runtime.reloadSaved();
2889
- fab.current?.focus();
2890
- };
2891
- return /* @__PURE__ */ jsxs17(Fragment11, {
2892
- children: [
2893
- /* @__PURE__ */ jsxs17("button", {
2894
- ref: fab,
2895
- type: "button",
2896
- className: "abs-fab",
2897
- onClick: open,
2898
- "aria-haspopup": "dialog",
2899
- "aria-expanded": state !== "closed",
2900
- children: [
2901
- /* @__PURE__ */ jsx19("span", {
2902
- className: "abs-fab-ico",
2903
- children: /* @__PURE__ */ jsx19(Icon, {
2904
- name: "link",
2905
- size: 18
2906
- })
2907
- }),
2908
- /* @__PURE__ */ jsx19("span", {
2909
- className: "abs-fab-label",
2910
- children: /* @__PURE__ */ jsx19("span", {
2911
- children: title ?? "Bind API"
2912
- })
2913
- })
2914
- ]
2915
- }),
2916
- state !== "closed" ? /* @__PURE__ */ jsx19("dialog", {
2917
- ref: dialog,
2918
- className: "abs-widget" + (state === "closing" ? " is-closing" : ""),
2919
- "aria-label": title ?? "Bind API",
2920
- onCancel: (e) => {
2921
- e.preventDefault();
2922
- requestClose();
2923
- },
2924
- onClick: (e) => {
2925
- if (e.target === e.currentTarget)
2926
- requestClose();
2927
- },
2928
- onAnimationEnd: finishClose,
2929
- children: /* @__PURE__ */ jsx19(BindingStudio, {
2930
- framework,
2931
- docsUrl,
2932
- title,
2933
- initialScreen: screen,
2934
- persistence: runtime.persistence ?? fallbackStore,
2935
- onClose: requestClose
2936
- })
2937
- }) : null
2938
- ]
2939
- });
2940
- }
2941
2735
  export {
2942
- BindingStudio,
2943
- BindingWidget
2736
+ BindingStudio
2944
2737
  };
2945
2738
 
2946
- //# debugId=C512BDFB06A7F43E64756E2164756E21
2739
+ //# debugId=033A885DE9DD742964756E2164756E21
2947
2740
  //# sourceMappingURL=index.js.map