api_binding_ui 2.0.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,15 @@
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
+ fromFile2,
6
+ toFile2,
12
7
  createEngine2,
13
- validateRegistry2,
14
- useBindingRuntime
15
- } from "../index-3q1vx729.js";
8
+ validateRegistry2
9
+ } from "../index-jj8twdw9.js";
16
10
 
17
11
  // src/studio/BindingStudio.tsx
18
- import * as React15 from "react";
12
+ import * as React14 from "react";
19
13
 
20
14
  // src/studio/ComponentList.tsx
21
15
  import * as React2 from "react";
@@ -2052,44 +2046,53 @@ function Navigator({ engine, current, onPick }) {
2052
2046
  // src/studio/SettingsPanel.tsx
2053
2047
  import * as React12 from "react";
2054
2048
 
2055
- // src/studio/useSettings.ts
2049
+ // src/studio/useCatalog.ts
2056
2050
  import * as React11 from "react";
2057
2051
  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;
2052
+ function useCatalog(url, provided) {
2053
+ const [state, setState] = React11.useState(() => provided ? { status: "ready", catalog: provided, loadedAt: new Date, source: "provided" } : { status: "loading" });
2054
+ const [nonce, setNonce] = React11.useState(0);
2055
+ const [pasted, setPasted] = React11.useState(null);
2056
+ React11.useEffect(() => {
2057
+ if (pasted) {
2058
+ setState({ status: "ready", catalog: pasted, loadedAt: new Date, source: "pasted" });
2059
+ return;
2060
+ }
2061
+ if (provided) {
2062
+ setState({ status: "ready", catalog: provided, loadedAt: new Date, source: "provided" });
2063
+ return;
2064
+ }
2065
+ if (!url)
2066
+ return;
2067
+ const ctrl = new AbortController;
2068
+ setState((s) => ({ status: "loading", catalog: s.catalog }));
2069
+ loadCatalog2(url, { signal: ctrl.signal }).then((catalog) => setState({ status: "ready", catalog, loadedAt: new Date, source: "url" }), (e) => {
2070
+ if (!ctrl.signal.aborted)
2071
+ setState((s) => ({ status: "error", catalog: s.catalog, error: e instanceof Error ? e.message : String(e) }));
2077
2072
  });
2073
+ return () => ctrl.abort();
2074
+ }, [url, provided, nonce, pasted]);
2075
+ const reload = React11.useCallback(() => {
2076
+ setPasted(null);
2077
+ setNonce((n) => n + 1);
2078
2078
  }, []);
2079
- return [settings, update];
2079
+ const loadPasted = React11.useCallback((text) => new Promise((resolve) => {
2080
+ setTimeout(() => {
2081
+ try {
2082
+ setPasted(catalogFromText2(text, "pasted"));
2083
+ resolve(undefined);
2084
+ } catch (e) {
2085
+ resolve(e instanceof Error ? e.message : String(e));
2086
+ }
2087
+ }, 0);
2088
+ }), []);
2089
+ return [state, reload, loadPasted];
2080
2090
  }
2081
2091
 
2082
2092
  // src/studio/SettingsPanel.tsx
2083
2093
  import { jsx as jsx17, jsxs as jsxs15, Fragment as Fragment9 } from "react/jsx-runtime";
2084
2094
  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
- }
2095
+ var isSource = (v) => v.trim().length > 0 && !/^\s*(javascript|data):/i.test(v);
2093
2096
  function docsSummary(c) {
2094
2097
  if (c.status === "loading")
2095
2098
  return "Loading…";
@@ -2182,14 +2185,12 @@ function PasteDocs({ onPaste, onDone }) {
2182
2185
  ]
2183
2186
  });
2184
2187
  }
2185
- function SettingsPanel({ docsUrl, onDocsUrl, catalog, onReload, onPaste, manifests, onImport, onClose }) {
2188
+ function SettingsPanel({ docsUrl, onDocsUrl, catalog, onReload, onPaste, onClose }) {
2186
2189
  const [draft, setDraft] = React12.useState(docsUrl);
2187
2190
  const [pasting, setPasting] = React12.useState(false);
2188
2191
  const usingPaste = catalog.status === "ready" && catalog.source === "pasted";
2189
- const [importMsg, setImportMsg] = React12.useState(null);
2190
2192
  const panel = React12.useRef(null);
2191
- const file = React12.useRef(null);
2192
- const valid = isUrl(draft);
2193
+ const valid = isSource(draft);
2193
2194
  const dirty = draft.trim() !== docsUrl;
2194
2195
  React12.useEffect(() => {
2195
2196
  const key = (e) => {
@@ -2218,223 +2219,144 @@ function SettingsPanel({ docsUrl, onDocsUrl, catalog, onReload, onPaste, manifes
2218
2219
  else
2219
2220
  onReload();
2220
2221
  };
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", {
2222
+ return /* @__PURE__ */ jsx17("div", {
2245
2223
  className: "abs-settings abs-pop",
2246
2224
  ref: panel,
2247
2225
  role: "dialog",
2248
2226
  "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,
2227
+ children: /* @__PURE__ */ jsxs15("form", {
2228
+ className: "abs-set-sec",
2229
+ onSubmit: apply,
2230
+ children: [
2231
+ /* @__PURE__ */ jsx17("label", {
2232
+ className: "abs-set-h",
2233
+ htmlFor: "abs-docs-url",
2234
+ children: "API docs"
2235
+ }),
2236
+ /* @__PURE__ */ jsx17("p", {
2237
+ className: "abs-set-d",
2238
+ children: "The OpenAPI file every API is read from: a URL, or a path next to the bindings file."
2239
+ }),
2240
+ /* @__PURE__ */ jsxs15("div", {
2241
+ className: "abs-set-row",
2242
+ children: [
2243
+ /* @__PURE__ */ jsx17("input", {
2244
+ id: "abs-docs-url",
2245
+ name: "docsUrl",
2246
+ type: "text",
2247
+ inputMode: "url",
2248
+ autoComplete: "off",
2249
+ spellCheck: false,
2250
+ className: "abs-input" + (valid ? "" : " is-invalid"),
2251
+ value: draft,
2252
+ onChange: (e) => setDraft(e.target.value),
2253
+ "aria-invalid": !valid,
2254
+ "aria-describedby": "abs-docs-status"
2255
+ }),
2256
+ /* @__PURE__ */ jsx17("button", {
2257
+ type: "submit",
2258
+ className: "abs-btn abs-btn-primary",
2259
+ disabled: !valid || catalog.status === "loading",
2260
+ children: dirty ? "Use" : /* @__PURE__ */ jsxs15(Fragment9, {
2312
2261
  children: [
2313
2262
  /* @__PURE__ */ jsx17(Icon, {
2314
2263
  name: "refresh",
2315
- size: 14
2264
+ size: 14,
2265
+ className: catalog.status === "loading" ? "abs-spin" : ""
2316
2266
  }),
2317
- "Use the URL again"
2267
+ "Reload"
2318
2268
  ]
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
2269
  })
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
- ]
2270
+ })
2271
+ ]
2272
+ }),
2273
+ /* @__PURE__ */ jsx17("p", {
2274
+ id: "abs-docs-status",
2275
+ className: "abs-set-status" + (catalog.status === "error" ? " is-error" : ""),
2276
+ "aria-live": "polite",
2277
+ children: valid ? docsSummary(catalog) : "Enter a URL or a path, for example " + DEFAULT_DOCS_URL
2278
+ }),
2279
+ pasting ? /* @__PURE__ */ jsx17(PasteDocs, {
2280
+ onPaste,
2281
+ onDone: () => setPasting(false)
2282
+ }) : /* @__PURE__ */ jsxs15("div", {
2283
+ className: "abs-set-links",
2284
+ children: [
2285
+ usingPaste ? /* @__PURE__ */ jsxs15("button", {
2286
+ type: "button",
2287
+ className: "abs-link",
2288
+ onClick: onReload,
2289
+ children: [
2290
+ /* @__PURE__ */ jsx17(Icon, {
2291
+ name: "refresh",
2292
+ size: 14
2293
+ }),
2294
+ "Use the URL again"
2295
+ ]
2296
+ }) : /* @__PURE__ */ jsxs15("button", {
2297
+ type: "button",
2298
+ className: "abs-link",
2299
+ onClick: () => setPasting(true),
2300
+ children: [
2301
+ /* @__PURE__ */ jsx17(Icon, {
2302
+ name: "upload",
2303
+ size: 14
2304
+ }),
2305
+ "Paste the file instead"
2306
+ ]
2307
+ }),
2308
+ docsUrl !== DEFAULT_DOCS_URL ? /* @__PURE__ */ jsx17("button", {
2309
+ type: "button",
2310
+ className: "abs-link",
2311
+ onClick: () => {
2312
+ setDraft(DEFAULT_DOCS_URL);
2313
+ onDocsUrl(DEFAULT_DOCS_URL);
2314
+ },
2315
+ children: "Use the default URL"
2316
+ }) : null
2317
+ ]
2318
+ })
2319
+ ]
2320
+ })
2399
2321
  });
2400
2322
  }
2401
2323
 
2402
2324
  // src/studio/useBindings.ts
2403
2325
  import * as React13 from "react";
2404
- function useBindings(fw, adapter) {
2405
- const [index, setIndex] = React13.useState({});
2406
- const [loaded, setLoaded] = React13.useState(false);
2326
+
2327
+ // src/framework/store.ts
2328
+ function putBinding(idx, screen, b) {
2329
+ const list = (idx[screen] ?? []).filter((x) => !(x.component === b.component && x.slot === b.slot));
2330
+ return { ...idx, [screen]: [...list, b] };
2331
+ }
2332
+ function dropBinding(idx, screen, component, slot) {
2333
+ return { ...idx, [screen]: (idx[screen] ?? []).filter((x) => !(x.component === component && x.slot === slot)) };
2334
+ }
2335
+
2336
+ // src/studio/useBindings.ts
2337
+ function useBindings(initial, save) {
2338
+ const [index, setIndex] = React13.useState(initial);
2407
2339
  const [status, setStatus] = React13.useState("idle");
2408
2340
  const [removed, setRemoved] = React13.useState(null);
2409
- const pending = React13.useRef(new Map);
2341
+ const edits = React13.useRef(0);
2410
2342
  const latest = React13.useRef(index);
2411
2343
  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
2344
  const flush = React13.useCallback(async () => {
2430
- if (!pending.current.size || !fw)
2345
+ const at = edits.current;
2346
+ if (!at)
2431
2347
  return;
2432
2348
  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]);
2349
+ try {
2350
+ await save(latest.current);
2351
+ if (edits.current === at)
2352
+ edits.current = 0;
2353
+ setStatus(edits.current ? "saving" : "saved");
2354
+ } catch {
2355
+ setStatus("failed");
2356
+ }
2357
+ }, [save]);
2436
2358
  React13.useEffect(() => {
2437
- if (!pending.current.size)
2359
+ if (!edits.current)
2438
2360
  return;
2439
2361
  setStatus("saving");
2440
2362
  const t = setTimeout(flush, 400);
@@ -2445,21 +2367,21 @@ function useBindings(fw, adapter) {
2445
2367
  }, [flush]);
2446
2368
  React13.useEffect(() => {
2447
2369
  const warn = (e) => {
2448
- if (pending.current.size)
2370
+ if (edits.current)
2449
2371
  e.preventDefault();
2450
2372
  };
2451
2373
  window.addEventListener("beforeunload", warn);
2452
2374
  return () => window.removeEventListener("beforeunload", warn);
2453
2375
  }, []);
2454
2376
  const put = React13.useCallback((screen, b) => {
2455
- markDirty(pending.current, screen);
2377
+ edits.current++;
2456
2378
  setIndex((i) => putBinding(i, screen, b));
2457
2379
  }, []);
2458
2380
  const drop = React13.useCallback((screen, component, slot) => {
2459
2381
  const binding = latest.current[screen]?.find((x) => x.component === component && x.slot === slot);
2460
2382
  if (binding)
2461
2383
  setRemoved({ screen, binding });
2462
- markDirty(pending.current, screen);
2384
+ edits.current++;
2463
2385
  setIndex((i) => dropBinding(i, screen, component, slot));
2464
2386
  }, []);
2465
2387
  const undo = React13.useCallback(() => {
@@ -2468,95 +2390,12 @@ function useBindings(fw, adapter) {
2468
2390
  setRemoved(null);
2469
2391
  }, [put, removed]);
2470
2392
  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];
2393
+ return { index, loaded: true, status, removed, put, drop, undo, dismiss, retry: flush };
2556
2394
  }
2557
2395
 
2558
2396
  // src/studio/BindingStudio.tsx
2559
2397
  import { jsx as jsx18, jsxs as jsxs16, Fragment as Fragment10 } from "react/jsx-runtime";
2398
+ var same = (s) => s;
2560
2399
  function SaveState({ status, onRetry }) {
2561
2400
  return /* @__PURE__ */ jsx18("span", {
2562
2401
  className: "abs-saved is-" + status,
@@ -2592,7 +2431,7 @@ function SaveState({ status, onRetry }) {
2592
2431
  });
2593
2432
  }
2594
2433
  function UndoToast({ removed, label, onUndo, onDismiss }) {
2595
- React15.useEffect(() => {
2434
+ React14.useEffect(() => {
2596
2435
  const t = setTimeout(onDismiss, 6000);
2597
2436
  return () => clearTimeout(t);
2598
2437
  }, [removed, onDismiss]);
@@ -2632,15 +2471,45 @@ var EMPTY_HINT = /* @__PURE__ */ jsxs16("aside", {
2632
2471
  })
2633
2472
  ]
2634
2473
  });
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]);
2474
+ function BindingStudio({ store, docsUrl = same, initialScreen, title = "Bind API", onClose }) {
2475
+ const [file, setFile] = React14.useState(null);
2476
+ const [fileError, setFileError] = React14.useState();
2477
+ const latest = React14.useRef({ file: null, index: {} });
2478
+ React14.useEffect(() => {
2479
+ let live = true;
2480
+ store.load().then((f) => {
2481
+ if (live) {
2482
+ latest.current.file = f;
2483
+ setFile(f);
2484
+ }
2485
+ }, (e) => live && setFileError(e instanceof Error ? e.message : String(e)));
2486
+ return () => {
2487
+ live = false;
2488
+ };
2489
+ }, [store]);
2490
+ const [catalogState, reloadCatalog, loadPasted] = useCatalog(file ? docsUrl(file.openapi) : "");
2491
+ const [settingsOpen, setSettingsOpen] = React14.useState(false);
2492
+ const closeSettings = React14.useCallback(() => setSettingsOpen(false), []);
2641
2493
  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);
2494
+ const model = React14.useMemo(() => {
2495
+ if (!file || !catalog)
2496
+ return null;
2497
+ const { fw, index } = fromFile2(file, catalog);
2498
+ return { fw: { ...fw, registry: validateRegistry2(fw).registry }, index };
2499
+ }, [file?.kinds, file?.screens, file?.components, catalog]);
2500
+ const save = React14.useCallback(async (index) => {
2501
+ latest.current.index = index;
2502
+ if (latest.current.file)
2503
+ await store.save(toFile2(latest.current.file, index));
2504
+ }, [store]);
2505
+ const setOpenapi = (openapi) => {
2506
+ const f = latest.current.file;
2507
+ if (!f)
2508
+ return;
2509
+ latest.current.file = { ...f, openapi };
2510
+ setFile(latest.current.file);
2511
+ store.save(toFile2(latest.current.file, Object.keys(latest.current.index).length ? latest.current.index : model?.index ?? {}));
2512
+ };
2644
2513
  const settingsButton = /* @__PURE__ */ jsxs16("div", {
2645
2514
  className: "abs-settings-wrap",
2646
2515
  children: [
@@ -2669,19 +2538,18 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2669
2538
  name: "close"
2670
2539
  })
2671
2540
  }) : null,
2672
- settingsOpen ? /* @__PURE__ */ jsx18(SettingsPanel, {
2673
- docsUrl: settings.docsUrl,
2674
- onDocsUrl: (url) => updateSettings({ docsUrl: url }),
2541
+ settingsOpen && file ? /* @__PURE__ */ jsx18(SettingsPanel, {
2542
+ docsUrl: file.openapi,
2543
+ onDocsUrl: setOpenapi,
2675
2544
  catalog: catalogState,
2676
2545
  onReload: reloadCatalog,
2677
2546
  onPaste: loadPasted,
2678
- manifests: () => fw ? Object.entries(binds.index).map(([s, b]) => toManifest2(fw, s, b)) : [],
2679
- onImport: binds.importManifests,
2680
2547
  onClose: closeSettings
2681
2548
  }) : null
2682
2549
  ]
2683
2550
  });
2684
- if (!fw) {
2551
+ if (!model) {
2552
+ const error = fileError ?? (catalogState.status === "error" ? catalogState.error : undefined);
2685
2553
  return /* @__PURE__ */ jsxs16("div", {
2686
2554
  className: "abs-root",
2687
2555
  children: [
@@ -2698,7 +2566,7 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2698
2566
  }),
2699
2567
  /* @__PURE__ */ jsx18("div", {
2700
2568
  className: "abs-state-page",
2701
- children: catalogState.status === "error" ? /* @__PURE__ */ jsxs16(Fragment10, {
2569
+ children: error ? /* @__PURE__ */ jsxs16(Fragment10, {
2702
2570
  children: [
2703
2571
  /* @__PURE__ */ jsx18("span", {
2704
2572
  className: "abs-empty-ico is-bad",
@@ -2708,12 +2576,12 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2708
2576
  })
2709
2577
  }),
2710
2578
  /* @__PURE__ */ jsx18("b", {
2711
- children: "The API docs did not load"
2579
+ children: fileError ? "The bindings file did not load" : "The API docs did not load"
2712
2580
  }),
2713
2581
  /* @__PURE__ */ jsx18("span", {
2714
- children: catalogState.error
2582
+ children: error
2715
2583
  }),
2716
- /* @__PURE__ */ jsxs16("div", {
2584
+ fileError ? null : /* @__PURE__ */ jsxs16("div", {
2717
2585
  className: "abs-set-row",
2718
2586
  children: [
2719
2587
  /* @__PURE__ */ jsxs16("button", {
@@ -2745,7 +2613,7 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2745
2613
  }),
2746
2614
  /* @__PURE__ */ jsx18("span", {
2747
2615
  "aria-live": "polite",
2748
- children: "Loading API docs…"
2616
+ children: file ? "Loading API docs…" : "Loading bindings…"
2749
2617
  })
2750
2618
  ]
2751
2619
  })
@@ -2754,26 +2622,27 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2754
2622
  });
2755
2623
  }
2756
2624
  return /* @__PURE__ */ jsx18(Workbench, {
2757
- fw,
2758
- binds,
2625
+ fw: model.fw,
2626
+ initial: model.index,
2627
+ save,
2759
2628
  title,
2760
2629
  initialScreen,
2761
2630
  settingsButton
2762
2631
  });
2763
2632
  }
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]);
2633
+ function Workbench({ fw, initial, save, title, initialScreen, settingsButton }) {
2634
+ const { index, loaded, status, removed, put, drop, undo, dismiss, retry } = useBindings(initial, save);
2635
+ const [place, go] = React14.useState({ screen: initialScreen });
2636
+ const engine = React14.useMemo(() => createEngine2(fw, index), [fw, index]);
2768
2637
  const screen = place.screen && engine.SCR.get(place.screen) || fw.registry.screens[0];
2769
2638
  const comp = screen && place.component ? engine.compOf(screen.id, place.component) : undefined;
2770
2639
  const screenId = screen?.id;
2771
- const select = React15.useCallback((component) => go({ screen: screenId, component }), [go, screenId]);
2772
- const putHere = React15.useCallback((b) => {
2640
+ const select = React14.useCallback((component) => go({ screen: screenId, component }), [go, screenId]);
2641
+ const putHere = React14.useCallback((b) => {
2773
2642
  if (screenId)
2774
2643
  put(screenId, b);
2775
2644
  }, [put, screenId]);
2776
- const dropHere = React15.useCallback((c, s) => {
2645
+ const dropHere = React14.useCallback((c, s) => {
2777
2646
  if (screenId)
2778
2647
  drop(screenId, c, s);
2779
2648
  }, [drop, screenId]);
@@ -2861,87 +2730,9 @@ function Workbench({ fw, binds, title, initialScreen, settingsButton }) {
2861
2730
  ]
2862
2731
  });
2863
2732
  }
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
2733
  export {
2942
- BindingStudio,
2943
- BindingWidget
2734
+ BindingStudio
2944
2735
  };
2945
2736
 
2946
- //# debugId=C512BDFB06A7F43E64756E2164756E21
2737
+ //# debugId=5CC922DF10302EB664756E2164756E21
2947
2738
  //# sourceMappingURL=index.js.map