@testsmith/api-spector 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13983,6 +13983,16 @@ const createCollectionsSlice = (set2, get2) => ({
13983
13983
  if (folder.headers?.length) inheritedHeaders = [...inheritedHeaders, ...folder.headers];
13984
13984
  }
13985
13985
  return { auth: inheritedAuth, headers: inheritedHeaders };
13986
+ },
13987
+ getInheritedVariables: (requestId) => {
13988
+ const state = get2();
13989
+ const colEntry = Object.values(state.collections).find((c) => c.data.requests[requestId]);
13990
+ if (!colEntry) return {};
13991
+ const merged = {};
13992
+ for (const folder of findFolderPath(colEntry.data.rootFolder, requestId)) {
13993
+ if (folder.variables) Object.assign(merged, folder.variables);
13994
+ }
13995
+ return merged;
13986
13996
  }
13987
13997
  });
13988
13998
  function envSlugRelPath(name2) {
@@ -14133,6 +14143,7 @@ const createUiSlice = (set2) => ({
14133
14143
  pinnedResponse: null,
14134
14144
  activeGitDiff: null,
14135
14145
  quickInsertsOpen: true,
14146
+ sendSignal: 0,
14136
14147
  setShowGeneratorPanel: (v) => set2((s) => {
14137
14148
  s.showGeneratorPanel = v;
14138
14149
  }),
@@ -14180,6 +14191,9 @@ const createUiSlice = (set2) => ({
14180
14191
  }),
14181
14192
  setQuickInsertsOpen: (open) => set2((s) => {
14182
14193
  s.quickInsertsOpen = open;
14194
+ }),
14195
+ requestSend: () => set2((s) => {
14196
+ s.sendSignal += 1;
14183
14197
  })
14184
14198
  });
14185
14199
  const useStore = create()(
@@ -35817,11 +35831,17 @@ function FolderSettingsModal({ collectionId, folder, onClose }) {
35817
35831
  const [activeTab, setActiveTab] = reactExports.useState("auth");
35818
35832
  const [auth, setAuth] = reactExports.useState(folder.auth ?? { type: "none" });
35819
35833
  const [headers, setHeaders] = reactExports.useState(folder.headers ?? []);
35834
+ const [varRows, setVarRows] = reactExports.useState(
35835
+ Object.entries(folder.variables ?? {}).map(([key, value]) => ({ key, value, enabled: true }))
35836
+ );
35820
35837
  function patchAuth(patch) {
35821
35838
  setAuth((prev) => ({ ...prev, ...patch }));
35822
35839
  }
35823
35840
  function save() {
35824
- updateFolder(collectionId, folder.id, { auth, headers });
35841
+ const variables = Object.fromEntries(
35842
+ varRows.filter((r) => r.key.trim()).map((r) => [r.key.trim(), r.value])
35843
+ );
35844
+ updateFolder(collectionId, folder.id, { auth, headers, variables });
35825
35845
  onClose();
35826
35846
  }
35827
35847
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
@@ -35836,12 +35856,12 @@ function FolderSettingsModal({ collectionId, folder, onClose }) {
35836
35856
  /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-sm font-semibold", children: "Folder settings" }),
35837
35857
  /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[10px] text-surface-600 mt-0.5", children: [
35838
35858
  folder.name,
35839
- " - auth and headers inherited by all requests in this folder"
35859
+ " - auth, headers and variables inherited by all requests in this folder"
35840
35860
  ] })
35841
35861
  ] }),
35842
35862
  /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "text-surface-400 hover:text-white text-lg leading-none", children: "×" })
35843
35863
  ] }),
35844
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex border-b border-surface-800 px-4 shrink-0", children: ["auth", "headers"].map((t2) => /* @__PURE__ */ jsxRuntimeExports.jsx(
35864
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex border-b border-surface-800 px-4 shrink-0", children: ["auth", "headers", "variables"].map((t2) => /* @__PURE__ */ jsxRuntimeExports.jsx(
35845
35865
  "button",
35846
35866
  {
35847
35867
  onClick: () => setActiveTab(t2),
@@ -35868,7 +35888,23 @@ function FolderSettingsModal({ collectionId, folder, onClose }) {
35868
35888
  valuePlaceholder: "value",
35869
35889
  headerMode: true
35870
35890
  }
35871
- )
35891
+ ),
35892
+ activeTab === "variables" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
35893
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[10px] text-surface-600", children: [
35894
+ "Variables scoped to this folder. They override collection variables and are overridden by an inner folder, the active environment, and script-set values. Reference them anywhere with ",
35895
+ "{{name}}",
35896
+ "."
35897
+ ] }),
35898
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35899
+ KVTable,
35900
+ {
35901
+ rows: varRows,
35902
+ onChange: setVarRows,
35903
+ keyPlaceholder: "VARIABLE_NAME",
35904
+ valuePlaceholder: "value"
35905
+ }
35906
+ )
35907
+ ] })
35872
35908
  ] }),
35873
35909
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 px-4 py-3 border-t border-surface-800 shrink-0", children: [
35874
35910
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -63874,6 +63910,7 @@ function RequestBuilder({ request }) {
63874
63910
  }
63875
63911
  const [editingName, setEditingName] = reactExports.useState(false);
63876
63912
  const [showFuzz, setShowFuzz] = reactExports.useState(false);
63913
+ const [customVerb, setCustomVerb] = reactExports.useState(false);
63877
63914
  const [runHooks, setRunHooks] = reactExports.useState(() => localStorage.getItem("runHooks") !== "false");
63878
63915
  function toggleRunHooks() {
63879
63916
  setRunHooks((prev) => {
@@ -63885,6 +63922,14 @@ function RequestBuilder({ request }) {
63885
63922
  function update(patch) {
63886
63923
  updateRequest(request.id, patch);
63887
63924
  }
63925
+ const sendSignal = useStore((s) => s.sendSignal);
63926
+ const lastSendSignal = reactExports.useRef(sendSignal);
63927
+ reactExports.useEffect(() => {
63928
+ if (sendSignal !== lastSendSignal.current) {
63929
+ lastSendSignal.current = sendSignal;
63930
+ void sendRequest();
63931
+ }
63932
+ }, [sendSignal]);
63888
63933
  async function sendRequest() {
63889
63934
  if (!activeTabId) return;
63890
63935
  setTabSending(activeTabId, true);
@@ -63903,6 +63948,8 @@ function RequestBuilder({ request }) {
63903
63948
  };
63904
63949
  let collectionVars = {
63905
63950
  ...activeCollectionId ? collections[activeCollectionId]?.data.collectionVariables ?? {} : {},
63951
+ // Folder-chain variables sit above collection vars and below session/local.
63952
+ ...useStore.getState().getInheritedVariables(request.id),
63906
63953
  ...sessionVars
63907
63954
  };
63908
63955
  let liveGlobals = { ...globals };
@@ -64113,15 +64160,39 @@ function RequestBuilder({ request }) {
64113
64160
  }
64114
64161
  )
64115
64162
  ] }),
64116
- !isWs && !isSoap && /* @__PURE__ */ jsxRuntimeExports.jsx(
64117
- "select",
64163
+ !isWs && !isSoap && (customVerb ? /* @__PURE__ */ jsxRuntimeExports.jsx(
64164
+ "input",
64118
64165
  {
64166
+ autoFocus: true,
64119
64167
  value: request.method,
64120
- onChange: (e) => update({ method: e.target.value }),
64121
- className: `bg-surface-800 border border-surface-700 rounded px-2 py-1.5 text-xs font-bold focus:outline-none focus:border-blue-500 ${METHOD_COLORS[request.method]}`,
64122
- children: METHODS.map((m) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: m, className: "text-white", children: m }, m))
64168
+ onChange: (e) => update({ method: e.target.value.toUpperCase() }),
64169
+ onBlur: () => setCustomVerb(false),
64170
+ onKeyDown: (e) => {
64171
+ if (e.key === "Enter") {
64172
+ e.preventDefault();
64173
+ setCustomVerb(false);
64174
+ }
64175
+ },
64176
+ placeholder: "VERB",
64177
+ title: "Type any HTTP method",
64178
+ className: "w-24 bg-surface-800 border border-blue-500 rounded px-2 py-1.5 text-xs font-bold uppercase focus:outline-none text-fuchsia-400 placeholder-surface-600"
64123
64179
  }
64124
- ),
64180
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsxs(
64181
+ "select",
64182
+ {
64183
+ value: METHODS.includes(request.method) ? request.method : "__current__",
64184
+ onChange: (e) => {
64185
+ if (e.target.value === "__custom__") setCustomVerb(true);
64186
+ else if (e.target.value !== "__current__") update({ method: e.target.value });
64187
+ },
64188
+ className: `bg-surface-800 border border-surface-700 rounded px-2 py-1.5 text-xs font-bold focus:outline-none focus:border-blue-500 ${METHOD_COLORS[request.method] ?? "text-fuchsia-400"}`,
64189
+ children: [
64190
+ METHODS.map((m) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: m, className: "text-white", children: m }, m)),
64191
+ !METHODS.includes(request.method) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "__current__", className: "text-white", children: request.method }),
64192
+ /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "__custom__", className: "text-white", children: "Custom…" })
64193
+ ]
64194
+ }
64195
+ )),
64125
64196
  isSoap && /* @__PURE__ */ jsxRuntimeExports.jsx(
64126
64197
  "span",
64127
64198
  {
@@ -65297,7 +65368,7 @@ function KVBlock({ label, rows }) {
65297
65368
  ] }, k))
65298
65369
  ] });
65299
65370
  }
65300
- function HistoryTabRow({ entry, onLoad }) {
65371
+ function HistoryTabRow({ entry, onLoad, onResend }) {
65301
65372
  const [open, setOpen] = reactExports.useState(false);
65302
65373
  const reqBody = requestBodyText(entry.request.body);
65303
65374
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border-b border-surface-800", children: [
@@ -65315,7 +65386,8 @@ function HistoryTabRow({ entry, onLoad }) {
65315
65386
  ] }),
65316
65387
  entry.environmentName && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] bg-surface-800 text-surface-400 px-1.5 py-0.5 rounded shrink-0", children: entry.environmentName }),
65317
65388
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] text-surface-500 ml-auto shrink-0", children: new Date(entry.timestamp).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" }) }),
65318
- /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onLoad, title: "Load this response into the viewer", className: "text-[10px] text-blue-400 hover:text-blue-300 shrink-0", children: "load" })
65389
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onLoad, title: "Load this response into the viewer", className: "text-[10px] text-blue-400 hover:text-blue-300 shrink-0", children: "load" }),
65390
+ onResend && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onResend, title: "Send this request again", className: "text-[10px] text-emerald-400 hover:text-emerald-300 shrink-0", children: "resend" })
65319
65391
  ] }),
65320
65392
  open && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 pb-3 pt-1 flex flex-col gap-3 bg-surface-950/40", children: [
65321
65393
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1.5", children: [
@@ -65356,6 +65428,7 @@ function ResponseViewer() {
65356
65428
  const hookResults = activeTab?.lastHookResults ?? null;
65357
65429
  const requestId = activeTab?.requestId ?? null;
65358
65430
  const setTabResponse = useStore((s) => s.setTabResponse);
65431
+ const requestSend = useStore((s) => s.requestSend);
65359
65432
  const history2 = useStore((s) => s.history);
65360
65433
  const activeEnvironmentId = useStore((s) => s.activeEnvironmentId);
65361
65434
  const environments = useStore((s) => s.environments);
@@ -65379,6 +65452,13 @@ function ResponseViewer() {
65379
65452
  setTab("console");
65380
65453
  }
65381
65454
  }, [scriptResult?.preScriptError, scriptResult?.postScriptError]);
65455
+ reactExports.useEffect(() => {
65456
+ if (response?.error) {
65457
+ if (tab !== "request" && tab !== "history") setTab("error");
65458
+ } else if (tab === "error") {
65459
+ setTab("body");
65460
+ }
65461
+ }, [response]);
65382
65462
  const [diffMode, setDiffMode] = reactExports.useState(false);
65383
65463
  const [showMockModal, setShowMockModal] = reactExports.useState(false);
65384
65464
  const [bodyView, setBodyView] = reactExports.useState("raw");
@@ -65419,12 +65499,6 @@ function ResponseViewer() {
65419
65499
  if (!response) {
65420
65500
  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-full flex items-center justify-center text-surface-400 text-sm", children: "Hit Send to see the response" });
65421
65501
  }
65422
- if (response.error) {
65423
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-full flex flex-col p-4 gap-2", children: [
65424
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-red-400 text-sm font-medium", children: "Request failed" }),
65425
- /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "text-xs text-red-300 whitespace-pre-wrap", children: response.error })
65426
- ] });
65427
- }
65428
65502
  const contentType = response.headers["content-type"] ?? "";
65429
65503
  const isJson = contentType.includes("json");
65430
65504
  const isXml = !isJson && (contentType.includes("xml") || contentType.includes("html"));
@@ -65442,15 +65516,31 @@ function ResponseViewer() {
65442
65516
  const totalCount = scriptResult?.testResults.length ?? 0;
65443
65517
  const consoleCount = scriptResult?.consoleOutput.length ?? 0;
65444
65518
  const hasScriptError = !!(scriptResult?.preScriptError || scriptResult?.postScriptError);
65445
- const tabList = [
65519
+ const historyBadge = requestHistory.length > 0 ? requestHistory.length : void 0;
65520
+ const tabList = response.error ? [
65521
+ { id: "error", label: "Error", error: true },
65522
+ { id: "request", label: "Request" },
65523
+ { id: "history", label: "History", badge: historyBadge }
65524
+ ] : [
65446
65525
  { id: "request", label: "Request" },
65447
65526
  { id: "body", label: "Body", badge: bodyParseError ? "!" : void 0, error: bodyParseError },
65448
65527
  { id: "headers", label: "Headers" },
65449
65528
  { id: "tests", label: "Tests", badge: totalCount > 0 ? `${passedCount}/${totalCount}` : void 0 },
65450
65529
  { id: "console", label: "Console", badge: hasScriptError ? "!" : consoleCount > 0 ? consoleCount : void 0, error: hasScriptError },
65451
- { id: "history", label: "History", badge: requestHistory.length > 0 ? requestHistory.length : void 0 },
65530
+ { id: "history", label: "History", badge: historyBadge },
65452
65531
  { id: "http", label: "HTTP", badge: httpFindings.length > 0 ? httpErrors > 0 ? "!" : httpFindings.length : void 0, error: httpErrors > 0 }
65453
65532
  ];
65533
+ const historyContent = requestHistory.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-surface-500 text-center p-8", children: "No past responses for this request yet. Each send is recorded here." }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col", children: requestHistory.map((entry) => /* @__PURE__ */ jsxRuntimeExports.jsx(
65534
+ HistoryTabRow,
65535
+ {
65536
+ entry,
65537
+ onLoad: () => {
65538
+ if (activeTabId) setTabResponse(activeTabId, entry.response, entry.scriptResult ?? null);
65539
+ },
65540
+ onResend: requestSend
65541
+ },
65542
+ entry.id
65543
+ )) });
65454
65544
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-full flex flex-col", children: [
65455
65545
  hookResults && hookResults.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(HookResultsPanel, { results: hookResults }),
65456
65546
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 px-4 py-1.5 border-b border-surface-800 flex-shrink-0 overflow-x-auto", children: [
@@ -65479,7 +65569,7 @@ function ResponseViewer() {
65479
65569
  },
65480
65570
  t2.id
65481
65571
  )) }),
65482
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ml-auto flex items-center gap-1 shrink-0", children: [
65572
+ !response.error && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ml-auto flex items-center gap-1 shrink-0", children: [
65483
65573
  assertToast.toast && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-emerald-400 font-medium px-1", children: assertToast.toast.msg }),
65484
65574
  contractToast.toast && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-blue-400 font-medium px-1", children: contractToast.toast.msg }),
65485
65575
  tab === "body" && supportsTree && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex rounded overflow-hidden border border-surface-800 mr-1", children: [
@@ -65541,7 +65631,10 @@ function ResponseViewer() {
65541
65631
  ] })
65542
65632
  ] }),
65543
65633
  showMockModal && /* @__PURE__ */ jsxRuntimeExports.jsx(SaveAsMockModal, { onClose: () => setShowMockModal(false) }),
65544
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 flex flex-col overflow-y-auto", children: diffMode && pinnedResponse ? /* @__PURE__ */ jsxRuntimeExports.jsx(DiffView, { pinned: pinnedResponse, current: response }) : tab === "body" && supportsTree && bodyView === "tree" ? /* @__PURE__ */ jsxRuntimeExports.jsx(
65634
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 flex flex-col overflow-y-auto", children: response.error ? tab === "request" ? /* @__PURE__ */ jsxRuntimeExports.jsx(RequestPanel, { sentRequest }) : tab === "history" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: historyContent }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col p-4 gap-2", children: [
65635
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-red-400 text-sm font-medium", children: "Request failed" }),
65636
+ /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "text-xs text-red-300 whitespace-pre-wrap", children: response.error })
65637
+ ] }) : diffMode && pinnedResponse ? /* @__PURE__ */ jsxRuntimeExports.jsx(DiffView, { pinned: pinnedResponse, current: response }) : tab === "body" && supportsTree && bodyView === "tree" ? /* @__PURE__ */ jsxRuntimeExports.jsx(
65545
65638
  InteractiveBody,
65546
65639
  {
65547
65640
  body: response.body,
@@ -65587,16 +65680,7 @@ function ResponseViewer() {
65587
65680
  ] }),
65588
65681
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-surface-200 mt-1", children: find2.message })
65589
65682
  ] }, i);
65590
- }) }) }) : tab === "history" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: requestHistory.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-surface-500 text-center p-8", children: "No past responses for this request yet. Each send is recorded here." }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col", children: requestHistory.map((entry) => /* @__PURE__ */ jsxRuntimeExports.jsx(
65591
- HistoryTabRow,
65592
- {
65593
- entry,
65594
- onLoad: () => {
65595
- if (activeTabId) setTabResponse(activeTabId, entry.response, entry.scriptResult ?? null);
65596
- }
65597
- },
65598
- entry.id
65599
- )) }) }) : null }),
65683
+ }) }) }) : tab === "history" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: historyContent }) : null }),
65600
65684
  headerMenu && /* @__PURE__ */ jsxRuntimeExports.jsx(
65601
65685
  ContextMenu,
65602
65686
  {
@@ -65681,7 +65765,8 @@ const TARGETS = [
65681
65765
  { id: "supertest_js", label: "Supertest JS", description: "Jest + Supertest JavaScript tests" },
65682
65766
  { id: "rest_assured", label: "REST Assured", description: "Java + JUnit 5 + Maven pom.xml" },
65683
65767
  { id: "karate", label: "Karate", description: "Karate feature files + JUnit 5 runner + Maven" },
65684
- { id: "http_file", label: "HTTP file", description: ".http / .rest file (VSCode REST Client / IntelliJ)" }
65768
+ { id: "http_file", label: "HTTP file", description: ".http / .rest file (VSCode REST Client / IntelliJ)" },
65769
+ { id: "curl", label: "cURL", description: "Runnable shell script, one curl command per request" }
65685
65770
  ];
65686
65771
  function GeneratorPanel() {
65687
65772
  const setShowGeneratorPanel = useStore((s) => s.setShowGeneratorPanel);
@@ -65937,6 +66022,7 @@ function HistoryPanel() {
65937
66022
  const activeTabId = useStore((s) => s.activeTabId);
65938
66023
  const setTabResponse = useStore((s) => s.setTabResponse);
65939
66024
  const setActiveRequest = useStore((s) => s.setActiveRequest);
66025
+ const requestSend = useStore((s) => s.requestSend);
65940
66026
  const collections = useStore((s) => s.collections);
65941
66027
  const [selected, setSelected] = reactExports.useState(null);
65942
66028
  const [search, setSearch] = reactExports.useState("");
@@ -65959,8 +66045,8 @@ function HistoryPanel() {
65959
66045
  }
65960
66046
  function open(entry) {
65961
66047
  setSelected(entry);
65962
- const stillExists = Object.values(collections).some((c) => entry.request.id in c.data.requests);
65963
- if (stillExists) {
66048
+ const stillExists2 = Object.values(collections).some((c) => entry.request.id in c.data.requests);
66049
+ if (stillExists2) {
65964
66050
  setActiveRequest(entry.request.id);
65965
66051
  const tabId = useStore.getState().activeTabId;
65966
66052
  if (tabId) setTabResponse(tabId, entry.response, entry.scriptResult ?? null);
@@ -65968,6 +66054,14 @@ function HistoryPanel() {
65968
66054
  setTabResponse(activeTabId, entry.response, entry.scriptResult ?? null);
65969
66055
  }
65970
66056
  }
66057
+ function replay(entry) {
66058
+ setSelected(entry);
66059
+ setActiveRequest(entry.request.id);
66060
+ requestSend();
66061
+ }
66062
+ function stillExists(entry) {
66063
+ return Object.values(collections).some((c) => entry.request.id in c.data.requests);
66064
+ }
65971
66065
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col flex-1 min-h-0", children: [
65972
66066
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-2 py-2 border-b border-surface-800 flex gap-1.5 flex-shrink-0", children: [
65973
66067
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -66012,7 +66106,8 @@ function HistoryPanel() {
66012
66106
  {
66013
66107
  entry,
66014
66108
  isSelected: selected?.id === entry.id,
66015
- onSelect: () => open(entry)
66109
+ onSelect: () => open(entry),
66110
+ onResend: stillExists(entry) ? () => replay(entry) : void 0
66016
66111
  },
66017
66112
  entry.id
66018
66113
  ))
@@ -66023,19 +66118,40 @@ function HistoryPanel() {
66023
66118
  function HistoryRow({
66024
66119
  entry,
66025
66120
  isSelected,
66026
- onSelect
66121
+ onSelect,
66122
+ onResend
66027
66123
  }) {
66028
66124
  const status = entry.response.status;
66029
66125
  const hasError = !!entry.response.error;
66030
66126
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
66031
- "button",
66127
+ "div",
66032
66128
  {
66129
+ role: "button",
66130
+ tabIndex: 0,
66033
66131
  onClick: onSelect,
66034
- className: `w-full text-left px-3 py-2 border-b border-surface-800/50 transition-colors ${isSelected ? "bg-surface-800" : "hover:bg-surface-800/50"}`,
66132
+ onKeyDown: (e) => {
66133
+ if (e.key === "Enter" || e.key === " ") {
66134
+ e.preventDefault();
66135
+ onSelect();
66136
+ }
66137
+ },
66138
+ className: `group w-full text-left px-3 py-2 border-b border-surface-800/50 transition-colors cursor-pointer ${isSelected ? "bg-surface-800" : "hover:bg-surface-800/50"}`,
66035
66139
  children: [
66036
66140
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
66037
66141
  /* @__PURE__ */ jsxRuntimeExports.jsx(MethodBadge, { method: entry.request.method, size: "xs" }),
66038
66142
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 text-xs truncate text-white", children: entry.request.name }),
66143
+ onResend && /* @__PURE__ */ jsxRuntimeExports.jsx(
66144
+ "button",
66145
+ {
66146
+ onClick: (e) => {
66147
+ e.stopPropagation();
66148
+ onResend();
66149
+ },
66150
+ title: "Send this request again",
66151
+ className: "text-[10px] text-emerald-400 hover:text-emerald-300 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity",
66152
+ children: "resend"
66153
+ }
66154
+ ),
66039
66155
  hasError ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-red-400 text-[10px] font-medium shrink-0", children: "ERR" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-[10px] font-bold shrink-0 ${statusColor$2(status)}`, children: status })
66040
66156
  ] }),
66041
66157
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mt-0.5 min-w-0", children: [
@@ -66059,6 +66175,15 @@ function HistoryRow({
66059
66175
  const { electron: electron$f } = window;
66060
66176
  function WelcomeScreen() {
66061
66177
  const { applyWorkspace } = useWorkspaceLoader();
66178
+ const [recents, setRecents] = reactExports.useState([]);
66179
+ const [update, setUpdate] = reactExports.useState(null);
66180
+ reactExports.useEffect(() => {
66181
+ electron$f.getRecentWorkspaces().then(setRecents).catch(() => setRecents([]));
66182
+ electron$f.checkForUpdate().then((info) => {
66183
+ if (info?.updateAvailable) setUpdate(info);
66184
+ }).catch(() => {
66185
+ });
66186
+ }, []);
66062
66187
  async function openWorkspace() {
66063
66188
  const result = await electron$f.openWorkspace();
66064
66189
  if (!result) return;
@@ -66069,7 +66194,25 @@ function WelcomeScreen() {
66069
66194
  if (!result) return;
66070
66195
  await applyWorkspace(result.workspace, result.workspacePath);
66071
66196
  }
66197
+ async function openRecent(path) {
66198
+ const result = await electron$f.openWorkspacePath(path);
66199
+ if (!result) {
66200
+ setRecents((prev) => prev.filter((r) => r.path !== path));
66201
+ return;
66202
+ }
66203
+ await applyWorkspace(result.workspace, result.workspacePath);
66204
+ }
66072
66205
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col items-center justify-center gap-6 text-center p-8", children: [
66206
+ update && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center gap-1 px-4 py-2 rounded-lg border border-blue-800 bg-blue-950/40 text-xs", children: [
66207
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-200", children: [
66208
+ "New version available (v",
66209
+ update.latest,
66210
+ ", you have v",
66211
+ update.current,
66212
+ ")"
66213
+ ] }),
66214
+ /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-[11px] text-blue-300 bg-surface-900 px-2 py-0.5 rounded select-all", children: update.command })
66215
+ ] }),
66073
66216
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
66074
66217
  /* @__PURE__ */ jsxRuntimeExports.jsxs("h1", { className: "text-2xl font-semibold mb-1", children: [
66075
66218
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "var(--wordmark-muted)" }, children: "API" }),
@@ -66089,7 +66232,11 @@ function WelcomeScreen() {
66089
66232
  }
66090
66233
  )
66091
66234
  ] }),
66092
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-400 text-sm max-w-sm", children: "Local-first API testing with Robot Framework & Playwright code generation. Secrets stay on your machine." })
66235
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-400 text-sm max-w-sm", children: "Local-first API testing with Robot Framework & Playwright code generation. Secrets stay on your machine." }),
66236
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[11px] mt-3", style: { color: "var(--text-muted)" }, children: [
66237
+ "version ",
66238
+ "0.4.1"
66239
+ ] })
66093
66240
  ] }),
66094
66241
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3 w-64", children: [
66095
66242
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -66109,6 +66256,22 @@ function WelcomeScreen() {
66109
66256
  }
66110
66257
  )
66111
66258
  ] }),
66259
+ recents.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1 w-72 text-left", children: [
66260
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] uppercase tracking-wider font-semibold px-1", style: { color: "var(--text-muted)" }, children: "Recent workspaces" }),
66261
+ recents.map((r) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
66262
+ "button",
66263
+ {
66264
+ onClick: () => openRecent(r.path),
66265
+ title: r.path,
66266
+ className: "flex flex-col items-start px-2 py-1.5 rounded hover:bg-surface-800 transition-colors text-left group",
66267
+ children: [
66268
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-surface-200 group-hover:text-white truncate max-w-full", children: r.name }),
66269
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-surface-500 truncate max-w-full", children: r.path })
66270
+ ]
66271
+ },
66272
+ r.path
66273
+ ))
66274
+ ] }),
66112
66275
  /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-surface-400 text-xs max-w-xs", children: [
66113
66276
  "A workspace is a ",
66114
66277
  /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-surface-500", children: ".spector" }),
@@ -67305,6 +67468,230 @@ function DocsGeneratorModal({ onClose }) {
67305
67468
  }
67306
67469
  );
67307
67470
  }
67471
+ const SKIP_WITH_ARG = /* @__PURE__ */ new Set([
67472
+ "--connect-timeout",
67473
+ "--max-time",
67474
+ "-m",
67475
+ "--retry",
67476
+ "--resolve",
67477
+ "--cacert",
67478
+ "--cert",
67479
+ "--key",
67480
+ "--proxy",
67481
+ "-x",
67482
+ "--output",
67483
+ "-o",
67484
+ "--write-out",
67485
+ "-w"
67486
+ ]);
67487
+ function tokenize(input) {
67488
+ const tokens = [];
67489
+ let cur2 = "";
67490
+ let inTok = false;
67491
+ let i = 0;
67492
+ const n = input.length;
67493
+ while (i < n) {
67494
+ const c = input[i];
67495
+ if (c === "\\" && (input[i + 1] === "\n" || input[i + 1] === "\r")) {
67496
+ i += input[i + 1] === "\r" && input[i + 2] === "\n" ? 3 : 2;
67497
+ continue;
67498
+ }
67499
+ if (c === "'") {
67500
+ inTok = true;
67501
+ i++;
67502
+ while (i < n && input[i] !== "'") cur2 += input[i++];
67503
+ i++;
67504
+ continue;
67505
+ }
67506
+ if (c === '"') {
67507
+ inTok = true;
67508
+ i++;
67509
+ while (i < n && input[i] !== '"') {
67510
+ if (input[i] === "\\" && i + 1 < n && `"\\$\``.includes(input[i + 1])) {
67511
+ cur2 += input[i + 1];
67512
+ i += 2;
67513
+ } else cur2 += input[i++];
67514
+ }
67515
+ i++;
67516
+ continue;
67517
+ }
67518
+ if (/\s/.test(c)) {
67519
+ if (inTok) {
67520
+ tokens.push(cur2);
67521
+ cur2 = "";
67522
+ inTok = false;
67523
+ }
67524
+ i++;
67525
+ continue;
67526
+ }
67527
+ if (c === "\\" && i + 1 < n) {
67528
+ cur2 += input[i + 1];
67529
+ i += 2;
67530
+ inTok = true;
67531
+ continue;
67532
+ }
67533
+ cur2 += c;
67534
+ inTok = true;
67535
+ i++;
67536
+ }
67537
+ if (inTok) tokens.push(cur2);
67538
+ return tokens;
67539
+ }
67540
+ function looksJson(s) {
67541
+ const t2 = s.trim();
67542
+ if (!(t2.startsWith("{") || t2.startsWith("["))) return false;
67543
+ try {
67544
+ JSON.parse(t2);
67545
+ return true;
67546
+ } catch {
67547
+ return false;
67548
+ }
67549
+ }
67550
+ function decodeBasic(b642) {
67551
+ try {
67552
+ const decoded = typeof atob === "function" ? atob(b642) : Buffer.from(b642, "base64").toString("utf8");
67553
+ const idx = decoded.indexOf(":");
67554
+ if (idx === -1) return null;
67555
+ return { username: decoded.slice(0, idx), password: decoded.slice(idx + 1) };
67556
+ } catch {
67557
+ return null;
67558
+ }
67559
+ }
67560
+ function parseCurl(command2) {
67561
+ const tokens = tokenize(command2.trim());
67562
+ if (tokens[0] === "curl") tokens.shift();
67563
+ let method = null;
67564
+ let url = "";
67565
+ let user = null;
67566
+ let forceGet = false;
67567
+ const headers = [];
67568
+ const dataParts = [];
67569
+ const formParts = [];
67570
+ const next = (i) => tokens[i + 1] ?? "";
67571
+ for (let i = 0; i < tokens.length; i++) {
67572
+ const t2 = tokens[i];
67573
+ switch (t2) {
67574
+ case "-X":
67575
+ case "--request":
67576
+ method = next(i);
67577
+ i++;
67578
+ break;
67579
+ case "-H":
67580
+ case "--header": {
67581
+ const raw = next(i);
67582
+ i++;
67583
+ const idx = raw.indexOf(":");
67584
+ if (idx > 0) headers.push({ key: raw.slice(0, idx).trim(), value: raw.slice(idx + 1).trim(), enabled: true });
67585
+ break;
67586
+ }
67587
+ case "-u":
67588
+ case "--user":
67589
+ user = next(i);
67590
+ i++;
67591
+ break;
67592
+ case "-d":
67593
+ case "--data":
67594
+ case "--data-raw":
67595
+ case "--data-ascii":
67596
+ case "--data-binary":
67597
+ dataParts.push(next(i));
67598
+ i++;
67599
+ break;
67600
+ case "--data-urlencode":
67601
+ dataParts.push(next(i));
67602
+ i++;
67603
+ break;
67604
+ case "-F":
67605
+ case "--form":
67606
+ formParts.push(next(i));
67607
+ i++;
67608
+ break;
67609
+ case "-b":
67610
+ case "--cookie":
67611
+ headers.push({ key: "Cookie", value: next(i), enabled: true });
67612
+ i++;
67613
+ break;
67614
+ case "-A":
67615
+ case "--user-agent":
67616
+ headers.push({ key: "User-Agent", value: next(i), enabled: true });
67617
+ i++;
67618
+ break;
67619
+ case "-e":
67620
+ case "--referer":
67621
+ headers.push({ key: "Referer", value: next(i), enabled: true });
67622
+ i++;
67623
+ break;
67624
+ case "--url":
67625
+ url = next(i);
67626
+ i++;
67627
+ break;
67628
+ case "-G":
67629
+ case "--get":
67630
+ forceGet = true;
67631
+ break;
67632
+ case "-I":
67633
+ case "--head":
67634
+ method = method ?? "HEAD";
67635
+ break;
67636
+ default:
67637
+ if (SKIP_WITH_ARG.has(t2)) {
67638
+ i++;
67639
+ break;
67640
+ }
67641
+ if (t2.startsWith("-")) break;
67642
+ if (!url) url = t2;
67643
+ break;
67644
+ }
67645
+ }
67646
+ if (!url) throw new Error("No URL found in the curl command.");
67647
+ const hasData = dataParts.length > 0;
67648
+ const isForm = formParts.length > 0;
67649
+ const resolvedMethod = (forceGet ? "GET" : method ?? (hasData || isForm ? "POST" : "GET")).toUpperCase();
67650
+ let auth = { type: "none" };
67651
+ if (user) {
67652
+ const idx = user.indexOf(":");
67653
+ auth = { type: "basic", username: idx === -1 ? user : user.slice(0, idx), password: idx === -1 ? "" : user.slice(idx + 1) };
67654
+ } else {
67655
+ const authIdx = headers.findIndex((h) => h.key.toLowerCase() === "authorization");
67656
+ if (authIdx !== -1) {
67657
+ const v = headers[authIdx].value;
67658
+ if (/^Bearer\s+/i.test(v)) {
67659
+ auth = { type: "bearer", token: v.replace(/^Bearer\s+/i, "") };
67660
+ headers.splice(authIdx, 1);
67661
+ } else if (/^Basic\s+/i.test(v)) {
67662
+ const creds = decodeBasic(v.replace(/^Basic\s+/i, "").trim());
67663
+ if (creds) {
67664
+ auth = { type: "basic", ...creds };
67665
+ headers.splice(authIdx, 1);
67666
+ }
67667
+ }
67668
+ }
67669
+ }
67670
+ let body = { mode: "none" };
67671
+ if (isForm) {
67672
+ body = {
67673
+ mode: "form",
67674
+ form: formParts.map((kv) => {
67675
+ const idx = kv.indexOf("=");
67676
+ return { key: idx === -1 ? kv : kv.slice(0, idx), value: idx === -1 ? "" : kv.slice(idx + 1), enabled: true };
67677
+ })
67678
+ };
67679
+ } else if (hasData) {
67680
+ const data = dataParts.join("&");
67681
+ const ct = headers.find((h) => h.key.toLowerCase() === "content-type")?.value;
67682
+ if (ct && ct.includes("json") || looksJson(data)) body = { mode: "json", json: data };
67683
+ else body = { mode: "raw", raw: data, rawContentType: ct ?? "application/x-www-form-urlencoded" };
67684
+ }
67685
+ let name2;
67686
+ try {
67687
+ const u = new URL(url.replace(/\{\{[^}]+\}\}/g, "x"));
67688
+ name2 = `${resolvedMethod} ${u.pathname === "/" ? u.hostname : u.pathname}`;
67689
+ } catch {
67690
+ const cleaned = url.replace(/^[a-z]+:\/\//i, "").replace(/\?.*$/, "");
67691
+ name2 = `${resolvedMethod} ${cleaned}`.trim();
67692
+ }
67693
+ return { name: name2, method: resolvedMethod, url, headers, params: [], auth, body };
67694
+ }
67308
67695
  const { electron: electron$9 } = window;
67309
67696
  const OPTIONS = [
67310
67697
  { id: "postman", label: "Postman", description: "Collection v2.1 JSON" },
@@ -67312,7 +67699,8 @@ const OPTIONS = [
67312
67699
  { id: "insomnia", label: "Insomnia", description: "Export v4 JSON" },
67313
67700
  { id: "bruno", label: "Bruno", description: "bruno.json collection file" },
67314
67701
  { id: "http", label: "HTTP file", description: ".http / .rest (REST Client)" },
67315
- { id: "spector", label: "API Spector", description: "An existing .spector / .json collection" }
67702
+ { id: "spector", label: "API Spector", description: "An existing .spector / .json collection" },
67703
+ { id: "curl", label: "cURL", description: "Paste a curl command" }
67316
67704
  ];
67317
67705
  function listEndpoints(col) {
67318
67706
  const result = [];
@@ -67336,6 +67724,7 @@ function ImportModal({ onImport, onClose }) {
67336
67724
  const markCollectionClean = useStore((s) => s.markCollectionClean);
67337
67725
  const [selected, setSelected] = reactExports.useState(null);
67338
67726
  const [url, setUrl] = reactExports.useState("");
67727
+ const [curlText, setCurlText] = reactExports.useState("");
67339
67728
  const [loading, setLoading] = reactExports.useState(false);
67340
67729
  const [error2, setError] = reactExports.useState(null);
67341
67730
  const urlInputRef = reactExports.useRef(null);
@@ -67414,6 +67803,37 @@ function ImportModal({ onImport, onClose }) {
67414
67803
  setLoading(false);
67415
67804
  }
67416
67805
  }
67806
+ function importCurl() {
67807
+ const text = curlText.trim();
67808
+ if (!text) return;
67809
+ setError(null);
67810
+ try {
67811
+ const parsed = parseCurl(text);
67812
+ const reqId = v4();
67813
+ const col = {
67814
+ version: "1.0",
67815
+ id: v4(),
67816
+ name: parsed.name || "Imported request",
67817
+ description: "",
67818
+ rootFolder: { id: v4(), name: "root", description: "", folders: [], requestIds: [reqId] },
67819
+ requests: {
67820
+ [reqId]: {
67821
+ id: reqId,
67822
+ name: parsed.name,
67823
+ method: parsed.method,
67824
+ url: parsed.url,
67825
+ headers: parsed.headers,
67826
+ params: parsed.params,
67827
+ auth: parsed.auth,
67828
+ body: parsed.body
67829
+ }
67830
+ }
67831
+ };
67832
+ enterPreview(col);
67833
+ } catch (err) {
67834
+ setError(err instanceof Error ? err.message : String(err));
67835
+ }
67836
+ }
67417
67837
  function toggleId(id2) {
67418
67838
  setChosenIds((prev) => {
67419
67839
  const next = new Set(prev);
@@ -67548,7 +67968,7 @@ function ImportModal({ onImport, onClose }) {
67548
67968
  children: [
67549
67969
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [
67550
67970
  /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-sm font-semibold text-surface-100", children: [
67551
- "Import OpenAPI - ",
67971
+ "Import - ",
67552
67972
  previewCol.name
67553
67973
  ] }),
67554
67974
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -67800,6 +68220,24 @@ function ImportModal({ onImport, onClose }) {
67800
68220
  )
67801
68221
  ] })
67802
68222
  ] }),
68223
+ selected === "curl" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
68224
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-surface-500 uppercase tracking-wider font-medium", children: "Paste a curl command" }),
68225
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
68226
+ "textarea",
68227
+ {
68228
+ value: curlText,
68229
+ onChange: (e) => {
68230
+ setCurlText(e.target.value);
68231
+ setError(null);
68232
+ },
68233
+ placeholder: `curl -X POST https://api.example.com/users \\
68234
+ -H 'Content-Type: application/json' \\
68235
+ -d '{"name":"Ada"}'`,
68236
+ rows: 6,
68237
+ className: "w-full text-xs bg-surface-800 border border-surface-700 rounded px-2.5 py-1.5 focus:outline-none focus:border-blue-500 placeholder-surface-600 font-mono resize-y"
68238
+ }
68239
+ )
68240
+ ] }),
67803
68241
  error2 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-red-400", children: error2 }),
67804
68242
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2 pt-1", children: [
67805
68243
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -67810,7 +68248,15 @@ function ImportModal({ onImport, onClose }) {
67810
68248
  children: "Cancel"
67811
68249
  }
67812
68250
  ),
67813
- /* @__PURE__ */ jsxRuntimeExports.jsx(
68251
+ selected === "curl" ? /* @__PURE__ */ jsxRuntimeExports.jsx(
68252
+ "button",
68253
+ {
68254
+ disabled: !curlText.trim() || loading,
68255
+ onClick: importCurl,
68256
+ className: "px-3 py-1.5 text-xs bg-blue-700 hover:bg-blue-600 disabled:bg-surface-800 disabled:text-surface-600 rounded transition-colors",
68257
+ children: "Parse"
68258
+ }
68259
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
67814
68260
  "button",
67815
68261
  {
67816
68262
  disabled: !selected || loading,
@@ -72134,7 +72580,7 @@ function App() {
72134
72580
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#6aa3c8" }, children: "Spector" }),
72135
72581
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 text-[10px] font-normal opacity-50", children: [
72136
72582
  "v",
72137
- "0.4.0"
72583
+ "0.4.1"
72138
72584
  ] }),
72139
72585
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mx-2 opacity-30", children: "·" }),
72140
72586
  /* @__PURE__ */ jsxRuntimeExports.jsx(