api_binding_ui 2.0.1 → 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,58 +2046,151 @@ 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…";
2096
2099
  if (c.status === "error")
2097
2100
  return c.error;
2098
2101
  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)}`;
2102
+ const where = c.source === "pasted" ? "Pasted file · " : "";
2103
+ return `${where}${products.size || 1} ${products.size === 1 ? "product" : "products"} · ${c.catalog.operations.length} ${c.catalog.operations.length === 1 ? "API" : "APIs"} · loaded ${TIME.format(c.loadedAt)}`;
2100
2104
  }
2101
- function SettingsPanel({ docsUrl, onDocsUrl, catalog, onReload, manifests, onImport, onClose }) {
2105
+ var SIZE = new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 });
2106
+ var kb = (chars) => chars < 1e6 ? `${SIZE.format(chars / 1000)} KB` : `${SIZE.format(chars / 1e6)} MB`;
2107
+ function PasteDocs({ onPaste, onDone }) {
2108
+ const area = React12.useRef(null);
2109
+ const [size, setSize] = React12.useState(0);
2110
+ const [busy, setBusy] = React12.useState(false);
2111
+ const [error, setError] = React12.useState();
2112
+ const load = async () => {
2113
+ const text = area.current?.value ?? "";
2114
+ setBusy(true);
2115
+ setError(undefined);
2116
+ const problem = await onPaste(text);
2117
+ setBusy(false);
2118
+ if (problem) {
2119
+ setError(problem);
2120
+ return;
2121
+ }
2122
+ if (area.current)
2123
+ area.current.value = "";
2124
+ onDone();
2125
+ };
2126
+ return /* @__PURE__ */ jsxs15("div", {
2127
+ className: "abs-paste",
2128
+ children: [
2129
+ /* @__PURE__ */ jsx17("textarea", {
2130
+ ref: area,
2131
+ id: "abs-docs-paste",
2132
+ className: "abs-input abs-paste-area",
2133
+ rows: 7,
2134
+ wrap: "off",
2135
+ spellCheck: false,
2136
+ autoCorrect: "off",
2137
+ autoCapitalize: "off",
2138
+ autoComplete: "off",
2139
+ autoFocus: true,
2140
+ placeholder: "Paste the contents of index.yaml (or the JSON version)",
2141
+ "aria-label": "OpenAPI document",
2142
+ "aria-describedby": "abs-paste-note",
2143
+ "aria-invalid": !!error,
2144
+ onInput: (e) => setSize(e.currentTarget.value.length)
2145
+ }),
2146
+ /* @__PURE__ */ jsxs15("div", {
2147
+ className: "abs-set-row",
2148
+ children: [
2149
+ /* @__PURE__ */ jsx17("button", {
2150
+ type: "button",
2151
+ className: "abs-btn abs-btn-primary",
2152
+ onClick: () => void load(),
2153
+ disabled: busy || size === 0,
2154
+ children: busy ? /* @__PURE__ */ jsxs15(Fragment9, {
2155
+ children: [
2156
+ /* @__PURE__ */ jsx17(Icon, {
2157
+ name: "refresh",
2158
+ size: 14,
2159
+ className: "abs-spin"
2160
+ }),
2161
+ "Reading…"
2162
+ ]
2163
+ }) : "Load"
2164
+ }),
2165
+ /* @__PURE__ */ jsx17("button", {
2166
+ type: "button",
2167
+ className: "abs-btn abs-btn-ghost",
2168
+ onClick: onDone,
2169
+ disabled: busy,
2170
+ children: "Cancel"
2171
+ }),
2172
+ /* @__PURE__ */ jsx17("span", {
2173
+ className: "abs-paste-size",
2174
+ "aria-live": "polite",
2175
+ children: size ? kb(size) : ""
2176
+ })
2177
+ ]
2178
+ }),
2179
+ /* @__PURE__ */ jsx17("p", {
2180
+ id: "abs-paste-note",
2181
+ className: "abs-set-status" + (error ? " is-error" : ""),
2182
+ role: error ? "alert" : undefined,
2183
+ children: error ?? "Used for this session only. Nothing is saved; the URL is used again next time."
2184
+ })
2185
+ ]
2186
+ });
2187
+ }
2188
+ function SettingsPanel({ docsUrl, onDocsUrl, catalog, onReload, onPaste, onClose }) {
2102
2189
  const [draft, setDraft] = React12.useState(docsUrl);
2103
- const [importMsg, setImportMsg] = React12.useState(null);
2190
+ const [pasting, setPasting] = React12.useState(false);
2191
+ const usingPaste = catalog.status === "ready" && catalog.source === "pasted";
2104
2192
  const panel = React12.useRef(null);
2105
- const file = React12.useRef(null);
2106
- const valid = isUrl(draft);
2193
+ const valid = isSource(draft);
2107
2194
  const dirty = draft.trim() !== docsUrl;
2108
2195
  React12.useEffect(() => {
2109
2196
  const key = (e) => {
@@ -2132,192 +2219,144 @@ function SettingsPanel({ docsUrl, onDocsUrl, catalog, onReload, manifests, onImp
2132
2219
  else
2133
2220
  onReload();
2134
2221
  };
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", {
2222
+ return /* @__PURE__ */ jsx17("div", {
2159
2223
  className: "abs-settings abs-pop",
2160
2224
  ref: panel,
2161
2225
  role: "dialog",
2162
2226
  "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(),
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, {
2257
2261
  children: [
2258
2262
  /* @__PURE__ */ jsx17(Icon, {
2259
- name: "upload",
2260
- size: 14
2263
+ name: "refresh",
2264
+ size: 14,
2265
+ className: catalog.status === "loading" ? "abs-spin" : ""
2261
2266
  }),
2262
- "Import…"
2267
+ "Reload"
2263
2268
  ]
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
2269
  })
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
- ]
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
+ })
2282
2321
  });
2283
2322
  }
2284
2323
 
2285
2324
  // src/studio/useBindings.ts
2286
2325
  import * as React13 from "react";
2287
- function useBindings(fw, adapter) {
2288
- const [index, setIndex] = React13.useState({});
2289
- 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);
2290
2339
  const [status, setStatus] = React13.useState("idle");
2291
2340
  const [removed, setRemoved] = React13.useState(null);
2292
- const pending = React13.useRef(new Map);
2341
+ const edits = React13.useRef(0);
2293
2342
  const latest = React13.useRef(index);
2294
2343
  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
2344
  const flush = React13.useCallback(async () => {
2313
- if (!pending.current.size || !fw)
2345
+ const at = edits.current;
2346
+ if (!at)
2314
2347
  return;
2315
2348
  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]);
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]);
2319
2358
  React13.useEffect(() => {
2320
- if (!pending.current.size)
2359
+ if (!edits.current)
2321
2360
  return;
2322
2361
  setStatus("saving");
2323
2362
  const t = setTimeout(flush, 400);
@@ -2328,21 +2367,21 @@ function useBindings(fw, adapter) {
2328
2367
  }, [flush]);
2329
2368
  React13.useEffect(() => {
2330
2369
  const warn = (e) => {
2331
- if (pending.current.size)
2370
+ if (edits.current)
2332
2371
  e.preventDefault();
2333
2372
  };
2334
2373
  window.addEventListener("beforeunload", warn);
2335
2374
  return () => window.removeEventListener("beforeunload", warn);
2336
2375
  }, []);
2337
2376
  const put = React13.useCallback((screen, b) => {
2338
- markDirty(pending.current, screen);
2377
+ edits.current++;
2339
2378
  setIndex((i) => putBinding(i, screen, b));
2340
2379
  }, []);
2341
2380
  const drop = React13.useCallback((screen, component, slot) => {
2342
2381
  const binding = latest.current[screen]?.find((x) => x.component === component && x.slot === slot);
2343
2382
  if (binding)
2344
2383
  setRemoved({ screen, binding });
2345
- markDirty(pending.current, screen);
2384
+ edits.current++;
2346
2385
  setIndex((i) => dropBinding(i, screen, component, slot));
2347
2386
  }, []);
2348
2387
  const undo = React13.useCallback(() => {
@@ -2351,72 +2390,12 @@ function useBindings(fw, adapter) {
2351
2390
  setRemoved(null);
2352
2391
  }, [put, removed]);
2353
2392
  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];
2393
+ return { index, loaded: true, status, removed, put, drop, undo, dismiss, retry: flush };
2416
2394
  }
2417
2395
 
2418
2396
  // src/studio/BindingStudio.tsx
2419
2397
  import { jsx as jsx18, jsxs as jsxs16, Fragment as Fragment10 } from "react/jsx-runtime";
2398
+ var same = (s) => s;
2420
2399
  function SaveState({ status, onRetry }) {
2421
2400
  return /* @__PURE__ */ jsx18("span", {
2422
2401
  className: "abs-saved is-" + status,
@@ -2452,7 +2431,7 @@ function SaveState({ status, onRetry }) {
2452
2431
  });
2453
2432
  }
2454
2433
  function UndoToast({ removed, label, onUndo, onDismiss }) {
2455
- React15.useEffect(() => {
2434
+ React14.useEffect(() => {
2456
2435
  const t = setTimeout(onDismiss, 6000);
2457
2436
  return () => clearTimeout(t);
2458
2437
  }, [removed, onDismiss]);
@@ -2492,15 +2471,45 @@ var EMPTY_HINT = /* @__PURE__ */ jsxs16("aside", {
2492
2471
  })
2493
2472
  ]
2494
2473
  });
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]);
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), []);
2501
2493
  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);
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
+ };
2504
2513
  const settingsButton = /* @__PURE__ */ jsxs16("div", {
2505
2514
  className: "abs-settings-wrap",
2506
2515
  children: [
@@ -2529,18 +2538,18 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2529
2538
  name: "close"
2530
2539
  })
2531
2540
  }) : null,
2532
- settingsOpen ? /* @__PURE__ */ jsx18(SettingsPanel, {
2533
- docsUrl: settings.docsUrl,
2534
- onDocsUrl: (url) => updateSettings({ docsUrl: url }),
2541
+ settingsOpen && file ? /* @__PURE__ */ jsx18(SettingsPanel, {
2542
+ docsUrl: file.openapi,
2543
+ onDocsUrl: setOpenapi,
2535
2544
  catalog: catalogState,
2536
2545
  onReload: reloadCatalog,
2537
- manifests: () => fw ? Object.entries(binds.index).map(([s, b]) => toManifest2(fw, s, b)) : [],
2538
- onImport: binds.importManifests,
2546
+ onPaste: loadPasted,
2539
2547
  onClose: closeSettings
2540
2548
  }) : null
2541
2549
  ]
2542
2550
  });
2543
- if (!fw) {
2551
+ if (!model) {
2552
+ const error = fileError ?? (catalogState.status === "error" ? catalogState.error : undefined);
2544
2553
  return /* @__PURE__ */ jsxs16("div", {
2545
2554
  className: "abs-root",
2546
2555
  children: [
@@ -2557,7 +2566,7 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2557
2566
  }),
2558
2567
  /* @__PURE__ */ jsx18("div", {
2559
2568
  className: "abs-state-page",
2560
- children: catalogState.status === "error" ? /* @__PURE__ */ jsxs16(Fragment10, {
2569
+ children: error ? /* @__PURE__ */ jsxs16(Fragment10, {
2561
2570
  children: [
2562
2571
  /* @__PURE__ */ jsx18("span", {
2563
2572
  className: "abs-empty-ico is-bad",
@@ -2567,12 +2576,12 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2567
2576
  })
2568
2577
  }),
2569
2578
  /* @__PURE__ */ jsx18("b", {
2570
- children: "The API docs did not load"
2579
+ children: fileError ? "The bindings file did not load" : "The API docs did not load"
2571
2580
  }),
2572
2581
  /* @__PURE__ */ jsx18("span", {
2573
- children: catalogState.error
2582
+ children: error
2574
2583
  }),
2575
- /* @__PURE__ */ jsxs16("div", {
2584
+ fileError ? null : /* @__PURE__ */ jsxs16("div", {
2576
2585
  className: "abs-set-row",
2577
2586
  children: [
2578
2587
  /* @__PURE__ */ jsxs16("button", {
@@ -2591,7 +2600,7 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2591
2600
  type: "button",
2592
2601
  className: "abs-btn",
2593
2602
  onClick: () => setSettingsOpen(true),
2594
- children: "Change the URL"
2603
+ children: "Change the URL or paste the file"
2595
2604
  })
2596
2605
  ]
2597
2606
  })
@@ -2604,7 +2613,7 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2604
2613
  }),
2605
2614
  /* @__PURE__ */ jsx18("span", {
2606
2615
  "aria-live": "polite",
2607
- children: "Loading API docs…"
2616
+ children: file ? "Loading API docs…" : "Loading bindings…"
2608
2617
  })
2609
2618
  ]
2610
2619
  })
@@ -2613,26 +2622,27 @@ function BindingStudio({ framework, docsUrl = DEFAULT_DOCS_URL, persistence, ini
2613
2622
  });
2614
2623
  }
2615
2624
  return /* @__PURE__ */ jsx18(Workbench, {
2616
- fw,
2617
- binds,
2625
+ fw: model.fw,
2626
+ initial: model.index,
2627
+ save,
2618
2628
  title,
2619
2629
  initialScreen,
2620
2630
  settingsButton
2621
2631
  });
2622
2632
  }
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]);
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]);
2627
2637
  const screen = place.screen && engine.SCR.get(place.screen) || fw.registry.screens[0];
2628
2638
  const comp = screen && place.component ? engine.compOf(screen.id, place.component) : undefined;
2629
2639
  const screenId = screen?.id;
2630
- const select = React15.useCallback((component) => go({ screen: screenId, component }), [go, screenId]);
2631
- const putHere = React15.useCallback((b) => {
2640
+ const select = React14.useCallback((component) => go({ screen: screenId, component }), [go, screenId]);
2641
+ const putHere = React14.useCallback((b) => {
2632
2642
  if (screenId)
2633
2643
  put(screenId, b);
2634
2644
  }, [put, screenId]);
2635
- const dropHere = React15.useCallback((c, s) => {
2645
+ const dropHere = React14.useCallback((c, s) => {
2636
2646
  if (screenId)
2637
2647
  drop(screenId, c, s);
2638
2648
  }, [drop, screenId]);
@@ -2720,87 +2730,9 @@ function Workbench({ fw, binds, title, initialScreen, settingsButton }) {
2720
2730
  ]
2721
2731
  });
2722
2732
  }
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
2733
  export {
2801
- BindingStudio,
2802
- BindingWidget
2734
+ BindingStudio
2803
2735
  };
2804
2736
 
2805
- //# debugId=34079C9B227D68B264756E2164756E21
2737
+ //# debugId=5CC922DF10302EB664756E2164756E21
2806
2738
  //# sourceMappingURL=index.js.map