@djangocfg/ui-tools 2.1.287 → 2.1.289

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.
@@ -2,13 +2,14 @@ import { deduplicateEndpoints, dereferenceSchema, resolveBaseUrl, usePlaygroundC
2
2
  import { JsonTree_default } from './chunk-LFWQ36LJ.mjs';
3
3
  import './chunk-SSUOENAZ.mjs';
4
4
  import { __name } from './chunk-CGILA3WO.mjs';
5
- import React6, { useRef, useMemo, useCallback, useEffect, useState } from 'react';
6
- import { Combobox, Input, Tooltip, TooltipTrigger, TooltipContent, DropdownMenu, DropdownMenuTrigger, Button, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuItem, CopyButton, Switch, Textarea, SidePanel, ResponsiveSheet, ResponsiveSheetContent, ResponsiveSheetHeader, ResponsiveSheetTitle, Skeleton, TooltipProvider } from '@djangocfg/ui-core/components';
7
- import { useMediaQuery } from '@djangocfg/ui-core/hooks';
5
+ import React6, { useRef, useCallback, useEffect, useMemo, useState } from 'react';
6
+ import { groupBy, orderBy, partition, sortBy, keyBy } from 'lodash-es';
7
+ import { Tooltip, TooltipTrigger, TooltipContent, DropdownMenu, DropdownMenuTrigger, Button, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuItem, Combobox, Input, CopyButton, Switch, Textarea, SidePanel, ResponsiveSheet, ResponsiveSheetContent, ResponsiveSheetHeader, ResponsiveSheetTitle, Skeleton, TooltipProvider } from '@djangocfg/ui-core/components';
8
+ import { toast, useMediaQuery } from '@djangocfg/ui-core/hooks';
8
9
  import consola from 'consola';
9
- import { ChevronRight, Search, Sparkles, ChevronDown, Check, Link2, Play, Minus, Plus, RotateCcw, Send, Key, Terminal, Loader2, WifiOff, AlertCircle } from 'lucide-react';
10
+ import { ChevronRight, Sparkles, ChevronDown, Check, Search, Link2, Play, Minus, Plus, RotateCcw, Send, Key, Terminal, Loader2, WifiOff, AlertCircle } from 'lucide-react';
10
11
  import { cn } from '@djangocfg/ui-core/lib';
11
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
12
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
12
13
 
13
14
  function exampleFromSchema(schema, depth = 0) {
14
15
  if (!schema || depth > 8) return null;
@@ -51,7 +52,7 @@ function exampleFromSchema(schema, depth = 0) {
51
52
  }
52
53
  __name(exampleFromSchema, "exampleFromSchema");
53
54
  var HTTP_METHODS = ["get", "post", "put", "patch", "delete"];
54
- var extractEndpoints = /* @__PURE__ */ __name((schema, baseUrl) => {
55
+ var extractEndpoints = /* @__PURE__ */ __name((schema, baseUrl, schemaId) => {
55
56
  const endpoints = [];
56
57
  if (!schema.paths) return [];
57
58
  for (const [path, methods] of Object.entries(schema.paths)) {
@@ -102,7 +103,8 @@ var extractEndpoints = /* @__PURE__ */ __name((schema, baseUrl) => {
102
103
  category,
103
104
  parameters: parameters.length > 0 ? parameters : void 0,
104
105
  requestBody,
105
- responses: responses.length > 0 ? responses : void 0
106
+ responses: responses.length > 0 ? responses : void 0,
107
+ schemaId
106
108
  };
107
109
  endpoints.push(endpoint);
108
110
  }
@@ -128,7 +130,8 @@ var fetchSchema = /* @__PURE__ */ __name(async (url) => {
128
130
  function useOpenApiSchema({
129
131
  schemas,
130
132
  defaultSchemaId,
131
- baseUrl: configBaseUrl
133
+ baseUrl: configBaseUrl,
134
+ preloadAll = false
132
135
  }) {
133
136
  const [loading, setLoading] = useState(true);
134
137
  const [error, setError] = useState(null);
@@ -138,6 +141,7 @@ function useOpenApiSchema({
138
141
  const [loadedSchemas, setLoadedSchemas] = useState(
139
142
  /* @__PURE__ */ new Map()
140
143
  );
144
+ const [loadStates, setLoadStates] = useState(/* @__PURE__ */ new Map());
141
145
  const currentSchema = useMemo(
142
146
  () => schemas.find((s) => s.id === currentSchemaId) || null,
143
147
  [schemas, currentSchemaId]
@@ -159,8 +163,8 @@ function useOpenApiSchema({
159
163
  [currentSchema?.baseUrl, configBaseUrl, currentOpenApiSchema]
160
164
  );
161
165
  const endpoints = useMemo(
162
- () => dereferencedSchema ? extractEndpoints(dereferencedSchema, resolvedBaseUrl) : [],
163
- [dereferencedSchema, resolvedBaseUrl]
166
+ () => dereferencedSchema ? extractEndpoints(dereferencedSchema, resolvedBaseUrl, currentSchemaId) : [],
167
+ [dereferencedSchema, resolvedBaseUrl, currentSchemaId]
164
168
  );
165
169
  const categories = useMemo(() => getCategories(endpoints), [endpoints]);
166
170
  const schemaInfo = useMemo(() => {
@@ -174,6 +178,7 @@ function useOpenApiSchema({
174
178
  };
175
179
  }, [currentOpenApiSchema]);
176
180
  useEffect(() => {
181
+ if (preloadAll) return;
177
182
  if (!currentSchema) return;
178
183
  if (loadedSchemas.has(currentSchema.id)) {
179
184
  setLoading(false);
@@ -190,7 +195,90 @@ function useOpenApiSchema({
190
195
  setError(err instanceof Error ? err.message : "Failed to load schema");
191
196
  setLoading(false);
192
197
  });
193
- }, [currentSchema, loadedSchemas]);
198
+ }, [currentSchema, loadedSchemas, preloadAll]);
199
+ useEffect(() => {
200
+ if (!preloadAll) return;
201
+ if (schemas.length === 0) {
202
+ setLoading(false);
203
+ return;
204
+ }
205
+ let cancelled = false;
206
+ const pending = schemas.filter((s) => !loadedSchemas.has(s.id));
207
+ if (pending.length === 0) {
208
+ setLoading(false);
209
+ return;
210
+ }
211
+ setLoading(true);
212
+ setLoadStates((prev) => {
213
+ const next = new Map(prev);
214
+ for (const s of pending) next.set(s.id, { loading: true, error: null });
215
+ return next;
216
+ });
217
+ Promise.allSettled(
218
+ pending.map(
219
+ (s) => fetchSchema(s.url).then((schema) => ({ id: s.id, name: s.name, schema }))
220
+ )
221
+ ).then((results) => {
222
+ if (cancelled) return;
223
+ setLoadedSchemas((prev) => {
224
+ const next = new Map(prev);
225
+ for (const r of results) {
226
+ if (r.status === "fulfilled") {
227
+ next.set(r.value.id, r.value.schema);
228
+ consola.success(`Schema loaded: ${r.value.name}`);
229
+ }
230
+ }
231
+ return next;
232
+ });
233
+ setLoadStates((prev) => {
234
+ const next = new Map(prev);
235
+ results.forEach((r, i) => {
236
+ const src = pending[i];
237
+ if (r.status === "fulfilled") {
238
+ next.set(src.id, { loading: false, error: null });
239
+ } else {
240
+ const msg = r.reason instanceof Error ? r.reason.message : "Failed to load schema";
241
+ consola.error(`Error loading schema from ${src.url}:`, r.reason);
242
+ next.set(src.id, { loading: false, error: msg });
243
+ }
244
+ });
245
+ return next;
246
+ });
247
+ setLoading(false);
248
+ });
249
+ return () => {
250
+ cancelled = true;
251
+ };
252
+ }, [preloadAll, schemas, loadedSchemas]);
253
+ const schemasData = useMemo(() => {
254
+ if (!preloadAll) return [];
255
+ return schemas.map((src) => {
256
+ const raw = loadedSchemas.get(src.id) ?? null;
257
+ const deref = raw ? dereferenceSchema(raw) : null;
258
+ const resolved = resolveBaseUrl({
259
+ schemaSource: src.baseUrl,
260
+ config: configBaseUrl,
261
+ fromServers: raw?.servers?.[0]?.url
262
+ });
263
+ const info = raw?.info ? {
264
+ title: raw.info.title,
265
+ version: raw.info.version,
266
+ description: raw.info.description,
267
+ servers: raw.servers
268
+ } : null;
269
+ const eps = deref ? extractEndpoints(deref, resolved, src.id) : [];
270
+ const state = loadStates.get(src.id) ?? { loading: !raw, error: null };
271
+ return {
272
+ source: src,
273
+ info,
274
+ rawSchema: raw,
275
+ endpoints: eps,
276
+ resolvedBaseUrl: resolved || void 0,
277
+ loading: state.loading,
278
+ error: state.error
279
+ };
280
+ });
281
+ }, [preloadAll, schemas, loadedSchemas, loadStates, configBaseUrl]);
194
282
  const setCurrentSchema = useCallback((schemaId) => {
195
283
  setCurrentSchemaId(schemaId);
196
284
  }, []);
@@ -227,10 +315,75 @@ function useOpenApiSchema({
227
315
  // convention from the resolver into undefined at the API boundary.
228
316
  resolvedBaseUrl: resolvedBaseUrl || void 0,
229
317
  setCurrentSchema,
230
- refresh
318
+ refresh,
319
+ schemasData
231
320
  };
232
321
  }
233
322
  __name(useOpenApiSchema, "useOpenApiSchema");
323
+ function parseDocsHash(hash) {
324
+ const raw = hash.startsWith("#") ? hash.slice(1) : hash;
325
+ if (!raw) return { schemaId: null, anchor: null };
326
+ const [schemaId = null, ...rest] = raw.split("/");
327
+ const anchor = rest.length > 0 ? rest.join("/") : null;
328
+ return {
329
+ schemaId: schemaId || null,
330
+ anchor: anchor || null
331
+ };
332
+ }
333
+ __name(parseDocsHash, "parseDocsHash");
334
+ function buildDocsHash(schemaId, anchor) {
335
+ if (!schemaId && !anchor) return "";
336
+ if (schemaId && anchor) return `#${schemaId}/${anchor}`;
337
+ if (schemaId) return `#${schemaId}`;
338
+ return anchor ? `#${anchor}` : "";
339
+ }
340
+ __name(buildDocsHash, "buildDocsHash");
341
+ function useDocsUrlSync({
342
+ enabled,
343
+ currentSchemaId,
344
+ activeAnchor,
345
+ onHashTarget
346
+ }) {
347
+ const primedRef = useRef(false);
348
+ const onHashTargetRef = useRef(onHashTarget);
349
+ useEffect(() => {
350
+ onHashTargetRef.current = onHashTarget;
351
+ }, [onHashTarget]);
352
+ useEffect(() => {
353
+ if (!enabled || typeof window === "undefined") return;
354
+ const apply = /* @__PURE__ */ __name(() => {
355
+ onHashTargetRef.current(parseDocsHash(window.location.hash));
356
+ }, "apply");
357
+ apply();
358
+ primedRef.current = true;
359
+ window.addEventListener("hashchange", apply);
360
+ window.addEventListener("popstate", apply);
361
+ return () => {
362
+ window.removeEventListener("hashchange", apply);
363
+ window.removeEventListener("popstate", apply);
364
+ };
365
+ }, [enabled]);
366
+ useEffect(() => {
367
+ if (!enabled || typeof window === "undefined") return;
368
+ if (!primedRef.current) return;
369
+ const next = buildDocsHash(currentSchemaId, activeAnchor);
370
+ const current = window.location.hash;
371
+ if (next === current) return;
372
+ const url = next ? `${window.location.pathname}${window.location.search}${next}` : `${window.location.pathname}${window.location.search}`;
373
+ window.history.replaceState(window.history.state, "", url);
374
+ }, [enabled, currentSchemaId, activeAnchor]);
375
+ const pushTarget = useCallback(
376
+ (schemaId, anchor) => {
377
+ if (!enabled || typeof window === "undefined") return;
378
+ const next = buildDocsHash(schemaId, anchor);
379
+ const url = next ? `${window.location.pathname}${window.location.search}${next}` : `${window.location.pathname}${window.location.search}`;
380
+ window.history.pushState(window.history.state, "", url);
381
+ },
382
+ [enabled]
383
+ );
384
+ return { pushTarget };
385
+ }
386
+ __name(useDocsUrlSync, "useDocsUrlSync");
234
387
  var EMPTY_DRAFT = { parameters: {}, requestBody: "" };
235
388
  function storageKey(schemaId, ep) {
236
389
  if (!schemaId || !ep) return null;
@@ -361,6 +514,18 @@ function EndpointDraftSync({ schemaId }) {
361
514
  return null;
362
515
  }
363
516
  __name(EndpointDraftSync, "EndpointDraftSync");
517
+
518
+ // src/tools/OpenapiViewer/components/DocsLayout/anchor.ts
519
+ function endpointAnchor(ep, schemaId) {
520
+ const slug = ep.path.replace(/^https?:\/\/[^/]+/, "").replace(/[{}]/g, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
521
+ const schemaSlug = schemaId ? `${slugifySchemaId(schemaId)}-` : "";
522
+ return `ep-${schemaSlug}${ep.method.toLowerCase()}-${slug}`;
523
+ }
524
+ __name(endpointAnchor, "endpointAnchor");
525
+ function slugifySchemaId(id) {
526
+ return id.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
527
+ }
528
+ __name(slugifySchemaId, "slugifySchemaId");
364
529
  var METHOD_STYLES = {
365
530
  GET: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/25",
366
531
  POST: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/25",
@@ -456,13 +621,6 @@ function CollapsibleSection({
456
621
  }
457
622
  __name(CollapsibleSection, "CollapsibleSection");
458
623
 
459
- // src/tools/OpenapiViewer/components/DocsLayout/anchor.ts
460
- function endpointAnchor(ep) {
461
- const slug = ep.path.replace(/^https?:\/\/[^/]+/, "").replace(/[{}]/g, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
462
- return `ep-${ep.method.toLowerCase()}-${slug}`;
463
- }
464
- __name(endpointAnchor, "endpointAnchor");
465
-
466
624
  // src/tools/OpenapiViewer/components/DocsLayout/sidebarLabel.ts
467
625
  function longestCommonPrefix(paths) {
468
626
  if (paths.length === 0) return "";
@@ -503,6 +661,8 @@ function sidebarTooltip(ep) {
503
661
  return `${ep.method} ${relativePath2(ep.path)}`;
504
662
  }
505
663
  __name(sidebarTooltip, "sidebarTooltip");
664
+
665
+ // src/tools/OpenapiViewer/components/DocsLayout/grouping.ts
506
666
  var METHOD_ORDER = {
507
667
  GET: 0,
508
668
  POST: 1,
@@ -510,138 +670,25 @@ var METHOD_ORDER = {
510
670
  PATCH: 3,
511
671
  DELETE: 4
512
672
  };
673
+ var methodRank = /* @__PURE__ */ __name((ep) => METHOD_ORDER[ep.method] ?? 99, "methodRank");
513
674
  function groupEndpoints(list) {
514
- const map = /* @__PURE__ */ new Map();
515
- for (const ep of list) {
516
- const arr = map.get(ep.category) ?? [];
517
- arr.push(ep);
518
- map.set(ep.category, arr);
519
- }
520
- const groups = Array.from(map.entries()).map(([category, endpoints]) => ({
675
+ const byCategory = groupBy(list, "category");
676
+ const all = Object.entries(byCategory).map(([category, endpoints]) => ({
521
677
  category,
522
- endpoints: [...endpoints].sort((a, b) => {
523
- const byPath = a.path.localeCompare(b.path);
524
- if (byPath !== 0) return byPath;
525
- return (METHOD_ORDER[a.method] ?? 99) - (METHOD_ORDER[b.method] ?? 99);
526
- }),
678
+ endpoints: orderBy(endpoints, ["path", methodRank], ["asc", "asc"]),
527
679
  commonPrefix: longestCommonPrefix(endpoints.map((e) => e.path))
528
680
  }));
529
- groups.sort((a, b) => {
530
- if (a.category === "Other") return 1;
531
- if (b.category === "Other") return -1;
532
- return a.category.localeCompare(b.category);
533
- });
534
- return groups;
681
+ const [other, named] = partition(all, (g) => g.category === "Other");
682
+ return [...sortBy(named, (g) => g.category.toLowerCase()), ...other];
535
683
  }
536
684
  __name(groupEndpoints, "groupEndpoints");
537
- function DocsSidebar({
538
- info,
539
- endpoints,
540
- schemas,
541
- currentSchemaId,
542
- onSchemaChange,
543
- activeEndpointId,
544
- selectedVersion,
545
- onNavigate
546
- }) {
547
- const [search, setSearch] = useState("");
548
- const [debounced, setDebounced] = useState("");
549
- useEffect(() => {
550
- const id = setTimeout(() => setDebounced(search), 120);
551
- return () => clearTimeout(id);
552
- }, [search]);
553
- const filteredGroups = useMemo(() => {
554
- let list = deduplicateEndpoints(endpoints, selectedVersion);
555
- if (debounced) {
556
- const q = debounced.toLowerCase();
557
- list = list.filter(
558
- (e) => e.summary.toLowerCase().includes(q) || e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q) || e.path.toLowerCase().includes(q)
559
- );
560
- }
561
- return groupEndpoints(list);
562
- }, [endpoints, debounced, selectedVersion]);
563
- const schemaOptions = useMemo(
564
- () => schemas.map((s) => ({ value: s.id, label: s.name })),
565
- [schemas]
566
- );
567
- const hasMultipleSchemas = schemas.length > 1;
568
- const apiTitle = info?.title ?? "API Reference";
569
- return /* @__PURE__ */ jsxs("aside", { className: "flex flex-col min-h-0 border-r bg-muted/10", children: [
570
- /* @__PURE__ */ jsxs("div", { className: "shrink-0 border-b px-4 h-12 flex items-center gap-2", children: [
571
- /* @__PURE__ */ jsx("span", { className: "text-[13px] font-semibold text-foreground truncate", children: apiTitle }),
572
- info?.version && /* @__PURE__ */ jsxs("span", { className: "font-mono text-[10px] text-muted-foreground/70 shrink-0", children: [
573
- "v",
574
- info.version
575
- ] })
576
- ] }),
577
- /* @__PURE__ */ jsxs("div", { className: "shrink-0 border-b px-3 py-3 space-y-2", children: [
578
- hasMultipleSchemas && /* @__PURE__ */ jsx(
579
- Combobox,
580
- {
581
- options: schemaOptions,
582
- value: currentSchemaId ?? "",
583
- onValueChange: (id) => id && onSchemaChange(id),
584
- placeholder: "Select API",
585
- searchPlaceholder: "Search APIs\u2026",
586
- emptyText: "No APIs found",
587
- className: "w-full h-8 text-xs"
588
- }
589
- ),
590
- /* @__PURE__ */ jsxs("div", { className: "relative", children: [
591
- /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/50 pointer-events-none" }),
592
- /* @__PURE__ */ jsx(
593
- Input,
594
- {
595
- placeholder: "Search endpoints\u2026",
596
- value: search,
597
- onChange: (e) => setSearch(e.target.value),
598
- className: "pl-8 h-8 text-xs"
599
- }
600
- )
601
- ] })
602
- ] }),
603
- /* @__PURE__ */ jsx(ScrollArea, { children: filteredGroups.length === 0 ? /* @__PURE__ */ jsx("div", { className: "py-10 px-4 text-center text-xs text-muted-foreground", children: debounced ? `No endpoints match "${debounced}"` : "No endpoints in this schema" }) : /* @__PURE__ */ jsx("nav", { className: "py-2", children: filteredGroups.map((group) => /* @__PURE__ */ jsxs("div", { className: "mb-4 last:mb-2", children: [
604
- /* @__PURE__ */ jsx("div", { className: "px-4 py-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/50 select-none", children: group.category }),
605
- /* @__PURE__ */ jsx("div", { children: group.endpoints.map((ep) => {
606
- const anchor = endpointAnchor(ep);
607
- const isActive = activeEndpointId === anchor;
608
- const label = sidebarLabel(ep, group.commonPrefix);
609
- const tooltip = sidebarTooltip(ep);
610
- const useMono = !ep.summary;
611
- return /* @__PURE__ */ jsxs(Tooltip, { delayDuration: 350, children: [
612
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
613
- "button",
614
- {
615
- onClick: () => onNavigate(anchor),
616
- "aria-current": isActive ? "location" : void 0,
617
- className: cn(
618
- "relative group w-full text-left flex items-center gap-2 pl-4 pr-3 py-1.5 transition-colors",
619
- isActive ? "bg-primary/10 text-foreground" : "hover:bg-muted/40 text-foreground/75 hover:text-foreground"
620
- ),
621
- children: [
622
- isActive && /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded-r bg-primary" }),
623
- /* @__PURE__ */ jsx(MethodBadge, { method: ep.method }),
624
- /* @__PURE__ */ jsx(
625
- "span",
626
- {
627
- className: cn(
628
- "truncate leading-tight flex-1 min-w-0",
629
- useMono ? "font-mono text-[11px]" : "text-[12px]",
630
- isActive && "text-foreground font-medium"
631
- ),
632
- children: label
633
- }
634
- )
635
- ]
636
- }
637
- ) }),
638
- /* @__PURE__ */ jsx(TooltipContent, { side: "right", align: "center", className: "font-mono text-[11px]", children: tooltip })
639
- ] }, `${ep.method}-${ep.path}`);
640
- }) })
641
- ] }, group.category)) }) })
642
- ] });
685
+ function buildSchemaSections(sources, endpointsBySchema) {
686
+ return sources.map((source) => ({
687
+ source,
688
+ groups: groupEndpoints(endpointsBySchema[source.id] ?? [])
689
+ }));
643
690
  }
644
- __name(DocsSidebar, "DocsSidebar");
691
+ __name(buildSchemaSections, "buildSchemaSections");
645
692
  var FLAVOUR_LABELS = {
646
693
  markdown: {
647
694
  title: "Markdown for LLM",
@@ -656,7 +703,7 @@ var FLAVOUR_LABELS = {
656
703
  hint: "Full OpenAPI document with $refs."
657
704
  }
658
705
  };
659
- function SchemaCopyMenu({ schema, endpoints, baseUrl }) {
706
+ function SchemaCopyMenu({ schema, endpoints, baseUrl, variant = "button" }) {
660
707
  const [sizeCache, setSizeCache] = useState({});
661
708
  const [justCopied, setJustCopied] = useState(null);
662
709
  const [open, setOpen] = useState(false);
@@ -674,20 +721,36 @@ function SchemaCopyMenu({ schema, endpoints, baseUrl }) {
674
721
  async (flavour) => {
675
722
  if (!isReady) return;
676
723
  const text = build(flavour);
724
+ const label = FLAVOUR_LABELS[flavour].title;
677
725
  try {
678
726
  await navigator.clipboard.writeText(text);
679
- setSizeCache((prev) => ({ ...prev, [flavour]: formatBytes(text) }));
727
+ const size = formatBytes(text);
728
+ setSizeCache((prev) => ({ ...prev, [flavour]: size }));
680
729
  setJustCopied(flavour);
681
730
  setTimeout(() => setJustCopied(null), 1500);
682
731
  setOpen(false);
683
- } catch {
732
+ toast.success(`Copied ${label}`, { description: size });
733
+ } catch (err) {
734
+ const message = err instanceof Error ? err.message : "Clipboard permission denied";
735
+ toast.error("Copy failed", { description: message });
684
736
  }
685
737
  },
686
738
  [build, isReady]
687
739
  );
688
740
  const flavours = useMemo(() => ["markdown", "compact", "raw"], []);
689
741
  return /* @__PURE__ */ jsxs(DropdownMenu, { open, onOpenChange: setOpen, children: [
690
- /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs", disabled: !isReady, children: [
742
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: variant === "icon" ? /* @__PURE__ */ jsx(
743
+ Button,
744
+ {
745
+ variant: "ghost",
746
+ size: "icon",
747
+ className: "h-7 w-7 shrink-0",
748
+ disabled: !isReady,
749
+ title: "Copy schema for AI",
750
+ "aria-label": "Copy schema for AI",
751
+ children: /* @__PURE__ */ jsx(Sparkles, { className: "h-3.5 w-3.5" })
752
+ }
753
+ ) : /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs", disabled: !isReady, children: [
691
754
  /* @__PURE__ */ jsx(Sparkles, { className: "h-3 w-3" }),
692
755
  "Copy for AI",
693
756
  /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 opacity-60" })
@@ -725,6 +788,296 @@ function SchemaCopyMenu({ schema, endpoints, baseUrl }) {
725
788
  ] });
726
789
  }
727
790
  __name(SchemaCopyMenu, "SchemaCopyMenu");
791
+ var METHOD_FILTERS = ["ALL", "GET", "POST", "PUT", "PATCH", "DELETE"];
792
+ function filterEndpoints(list, query, method) {
793
+ let out = list;
794
+ if (method !== "ALL") {
795
+ out = out.filter((e) => e.method === method);
796
+ }
797
+ if (query) {
798
+ const q = query.toLowerCase();
799
+ out = out.filter(
800
+ (e) => e.summary.toLowerCase().includes(q) || e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q) || e.path.toLowerCase().includes(q)
801
+ );
802
+ }
803
+ return out;
804
+ }
805
+ __name(filterEndpoints, "filterEndpoints");
806
+ function buildCategory(group, activeEndpointId, schemaId, keyPrefix) {
807
+ const rows = group.endpoints.map((ep) => {
808
+ const anchor = endpointAnchor(ep, schemaId ?? ep.schemaId ?? null);
809
+ return {
810
+ key: `${ep.method}-${ep.path}`,
811
+ anchor,
812
+ schemaId: schemaId ?? ep.schemaId ?? null,
813
+ label: sidebarLabel(ep, group.commonPrefix),
814
+ tooltip: sidebarTooltip(ep),
815
+ method: ep.method,
816
+ useMono: !ep.summary,
817
+ isActive: activeEndpointId === anchor
818
+ };
819
+ });
820
+ return {
821
+ key: `${keyPrefix}${group.category}`,
822
+ category: group.category,
823
+ rows
824
+ };
825
+ }
826
+ __name(buildCategory, "buildCategory");
827
+ var emptyTextFor = /* @__PURE__ */ __name((query, method, defaultText) => {
828
+ if (query && method !== "ALL") return `No ${method} endpoints match "${query}"`;
829
+ if (query) return `No endpoints match "${query}"`;
830
+ if (method !== "ALL") return `No ${method} endpoints`;
831
+ return defaultText;
832
+ }, "emptyTextFor");
833
+ function buildFlatVM(endpoints, selectedVersion, query, method, activeEndpointId) {
834
+ const filtered = filterEndpoints(deduplicateEndpoints(endpoints, selectedVersion), query, method);
835
+ const groups = groupEndpoints(filtered);
836
+ return {
837
+ kind: "flat",
838
+ categories: groups.map((g) => buildCategory(g, activeEndpointId, null, "")),
839
+ emptyText: emptyTextFor(query, method, "No endpoints in this schema")
840
+ };
841
+ }
842
+ __name(buildFlatVM, "buildFlatVM");
843
+ function buildSectionsVM(schemas, endpointsBySchema, selectedVersion, query, method, activeEndpointId) {
844
+ const filteredMap = {};
845
+ for (const src of schemas) {
846
+ const raw = endpointsBySchema[src.id] ?? [];
847
+ filteredMap[src.id] = filterEndpoints(deduplicateEndpoints(raw, selectedVersion), query, method);
848
+ }
849
+ const rawSections = buildSchemaSections(schemas, filteredMap);
850
+ const sections = rawSections.filter((s) => s.groups.length > 0).map((s) => ({
851
+ sourceId: s.source.id,
852
+ sourceName: s.source.name,
853
+ categories: s.groups.map((g) => buildCategory(g, activeEndpointId, s.source.id, `${s.source.id}-`))
854
+ }));
855
+ return {
856
+ kind: "sections",
857
+ sections,
858
+ emptyText: emptyTextFor(query, method, "No endpoints in any schema")
859
+ };
860
+ }
861
+ __name(buildSectionsVM, "buildSectionsVM");
862
+ function DocsSidebar({
863
+ info,
864
+ endpoints,
865
+ schemas,
866
+ currentSchemaId,
867
+ onSchemaChange,
868
+ activeEndpointId,
869
+ selectedVersion,
870
+ onNavigate,
871
+ grouping = "selector",
872
+ endpointsBySchema,
873
+ rawSchema,
874
+ resolvedBaseUrl
875
+ }) {
876
+ const [search, setSearch] = useState("");
877
+ const [debounced, setDebounced] = useState("");
878
+ const [methodFilter, setMethodFilter] = useState("ALL");
879
+ useEffect(() => {
880
+ const id = setTimeout(() => setDebounced(search), 120);
881
+ return () => clearTimeout(id);
882
+ }, [search]);
883
+ const body = useMemo(() => {
884
+ if (grouping === "sections") {
885
+ return buildSectionsVM(
886
+ schemas,
887
+ endpointsBySchema ?? {},
888
+ selectedVersion,
889
+ debounced,
890
+ methodFilter,
891
+ activeEndpointId
892
+ );
893
+ }
894
+ return buildFlatVM(endpoints, selectedVersion, debounced, methodFilter, activeEndpointId);
895
+ }, [
896
+ grouping,
897
+ schemas,
898
+ endpointsBySchema,
899
+ endpoints,
900
+ selectedVersion,
901
+ debounced,
902
+ methodFilter,
903
+ activeEndpointId
904
+ ]);
905
+ const schemaOptions = useMemo(
906
+ () => schemas.map((s) => ({ value: s.id, label: s.name })),
907
+ [schemas]
908
+ );
909
+ const hasMultipleSchemas = schemas.length > 1;
910
+ const apiTitle = info?.title ?? "API Reference";
911
+ const showCombobox = grouping === "selector" && hasMultipleSchemas;
912
+ const copyReady = rawSchema !== null && rawSchema !== void 0 && endpoints.length > 0;
913
+ return /* @__PURE__ */ jsxs("aside", { className: "flex flex-col h-full min-h-0 border-r bg-muted/10", children: [
914
+ /* @__PURE__ */ jsxs("div", { className: "shrink-0 border-b px-3 h-12 flex items-center gap-2", children: [
915
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-1 min-w-0", children: [
916
+ /* @__PURE__ */ jsx("span", { className: "text-[13px] font-semibold text-foreground truncate", children: apiTitle }),
917
+ info?.version && /* @__PURE__ */ jsxs("span", { className: "font-mono text-[10px] text-muted-foreground/70 shrink-0", children: [
918
+ "v",
919
+ info.version
920
+ ] })
921
+ ] }),
922
+ copyReady && /* @__PURE__ */ jsx(
923
+ SchemaCopyMenu,
924
+ {
925
+ schema: rawSchema ?? null,
926
+ endpoints,
927
+ baseUrl: resolvedBaseUrl,
928
+ variant: "icon"
929
+ }
930
+ )
931
+ ] }),
932
+ /* @__PURE__ */ jsxs("div", { className: "shrink-0 border-b px-3 py-3 space-y-2", children: [
933
+ showCombobox && /* @__PURE__ */ jsx(
934
+ Combobox,
935
+ {
936
+ options: schemaOptions,
937
+ value: currentSchemaId ?? "",
938
+ onValueChange: (id) => id && onSchemaChange(id),
939
+ placeholder: "Select API",
940
+ searchPlaceholder: "Search APIs\u2026",
941
+ emptyText: "No APIs found",
942
+ className: "w-full h-8 text-xs"
943
+ }
944
+ ),
945
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
946
+ /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/50 pointer-events-none" }),
947
+ /* @__PURE__ */ jsx(
948
+ Input,
949
+ {
950
+ placeholder: "Search endpoints\u2026",
951
+ value: search,
952
+ onChange: (e) => setSearch(e.target.value),
953
+ className: "pl-8 h-8 text-xs"
954
+ }
955
+ )
956
+ ] }),
957
+ /* @__PURE__ */ jsx(MethodChips, { value: methodFilter, onChange: setMethodFilter })
958
+ ] }),
959
+ /* @__PURE__ */ jsx(ScrollArea, { children: /* @__PURE__ */ jsx(SidebarBody, { body, onNavigate }) })
960
+ ] });
961
+ }
962
+ __name(DocsSidebar, "DocsSidebar");
963
+ function MethodChips({
964
+ value,
965
+ onChange
966
+ }) {
967
+ return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 flex-wrap", children: METHOD_FILTERS.map((m) => {
968
+ const active = value === m;
969
+ return /* @__PURE__ */ jsx(
970
+ "button",
971
+ {
972
+ type: "button",
973
+ onClick: () => onChange(m),
974
+ "aria-pressed": active,
975
+ className: cn(
976
+ "px-2 py-0.5 rounded font-mono text-[10px] font-semibold tracking-wide transition-colors border",
977
+ active ? "bg-primary/15 border-primary/40 text-foreground" : "bg-transparent border-border/40 text-muted-foreground hover:text-foreground hover:border-border"
978
+ ),
979
+ children: m
980
+ },
981
+ m
982
+ );
983
+ }) });
984
+ }
985
+ __name(MethodChips, "MethodChips");
986
+ function SidebarBody({ body, onNavigate }) {
987
+ if (body.kind === "flat") {
988
+ if (body.categories.length === 0) {
989
+ return /* @__PURE__ */ jsx("div", { className: "py-10 px-4 text-center text-xs text-muted-foreground", children: body.emptyText });
990
+ }
991
+ return /* @__PURE__ */ jsx("nav", { className: "py-2", children: body.categories.map((cat) => /* @__PURE__ */ jsx(CategoryBlock, { category: cat, onNavigate }, cat.key)) });
992
+ }
993
+ if (body.sections.length === 0) {
994
+ return /* @__PURE__ */ jsx("div", { className: "py-10 px-4 text-center text-xs text-muted-foreground", children: body.emptyText });
995
+ }
996
+ return /* @__PURE__ */ jsx("nav", { className: "py-2", children: body.sections.map((section) => /* @__PURE__ */ jsxs("div", { className: "mb-5 last:mb-2", children: [
997
+ /* @__PURE__ */ jsx("div", { className: "px-4 py-2 sticky top-0 z-[1] bg-muted/30 backdrop-blur-[2px] border-b border-border/30", children: /* @__PURE__ */ jsx("span", { className: "text-[11px] font-bold uppercase tracking-[0.12em] text-foreground/80", children: section.sourceName }) }),
998
+ section.categories.map((cat) => /* @__PURE__ */ jsx(CategoryBlock, { category: cat, onNavigate }, cat.key))
999
+ ] }, section.sourceId)) });
1000
+ }
1001
+ __name(SidebarBody, "SidebarBody");
1002
+ var CategoryBlock = React6.memo(/* @__PURE__ */ __name(function CategoryBlock2({
1003
+ category,
1004
+ onNavigate
1005
+ }) {
1006
+ return /* @__PURE__ */ jsxs("div", { className: "mb-4 last:mb-2", children: [
1007
+ /* @__PURE__ */ jsx("div", { className: "px-4 py-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/50 select-none", children: category.category }),
1008
+ /* @__PURE__ */ jsx("div", { children: category.rows.map((row) => /* @__PURE__ */ jsx(EndpointRow, { row, onNavigate }, row.key)) })
1009
+ ] });
1010
+ }, "CategoryBlock"));
1011
+ var EndpointRow = React6.memo(/* @__PURE__ */ __name(function EndpointRow2({
1012
+ row,
1013
+ onNavigate
1014
+ }) {
1015
+ return /* @__PURE__ */ jsxs(Tooltip, { delayDuration: 350, children: [
1016
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
1017
+ "button",
1018
+ {
1019
+ onClick: () => onNavigate(row.anchor, row.schemaId),
1020
+ "aria-current": row.isActive ? "location" : void 0,
1021
+ className: cn(
1022
+ "relative group w-full text-left flex items-start gap-2 pl-4 pr-3 py-1.5 transition-colors",
1023
+ row.isActive ? "bg-primary/10 text-foreground" : "hover:bg-muted/40 text-foreground/75 hover:text-foreground"
1024
+ ),
1025
+ children: [
1026
+ row.isActive && /* @__PURE__ */ jsx("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded-r bg-primary" }),
1027
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 mt-[1px]", children: /* @__PURE__ */ jsx(MethodBadge, { method: row.method }) }),
1028
+ /* @__PURE__ */ jsx(
1029
+ "span",
1030
+ {
1031
+ className: cn(
1032
+ "line-clamp-2 leading-snug flex-1 min-w-0",
1033
+ row.useMono ? "font-mono text-[11px] break-all" : "text-[12px]",
1034
+ row.isActive && "text-foreground font-medium"
1035
+ ),
1036
+ children: row.label
1037
+ }
1038
+ )
1039
+ ]
1040
+ }
1041
+ ) }),
1042
+ /* @__PURE__ */ jsx(TooltipContent, { side: "right", align: "center", className: "font-mono text-[11px]", children: row.tooltip })
1043
+ ] });
1044
+ }, "EndpointRow"));
1045
+
1046
+ // src/tools/OpenapiViewer/utils/scrollParent.ts
1047
+ function getScrollParent(el) {
1048
+ if (typeof window === "undefined") return null;
1049
+ if (!el) return window;
1050
+ let cur = el.parentElement;
1051
+ while (cur && cur !== document.body && cur !== document.documentElement) {
1052
+ const style = getComputedStyle(cur);
1053
+ const overflowY = style.overflowY;
1054
+ const canScroll = (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") && cur.scrollHeight > cur.clientHeight;
1055
+ if (canScroll) return cur;
1056
+ cur = cur.parentElement;
1057
+ }
1058
+ return window;
1059
+ }
1060
+ __name(getScrollParent, "getScrollParent");
1061
+ function getScrollTop(target) {
1062
+ return target === window ? window.scrollY : target.scrollTop;
1063
+ }
1064
+ __name(getScrollTop, "getScrollTop");
1065
+ function getViewportHeight(target) {
1066
+ return target === window ? window.innerHeight : target.clientHeight;
1067
+ }
1068
+ __name(getViewportHeight, "getViewportHeight");
1069
+ function getTargetTop(target) {
1070
+ return target === window ? 0 : target.getBoundingClientRect().top;
1071
+ }
1072
+ __name(getTargetTop, "getTargetTop");
1073
+ function scrollTargetTo(target, top) {
1074
+ if (target === window) {
1075
+ window.scrollTo({ top, behavior: "smooth" });
1076
+ return;
1077
+ }
1078
+ target.scrollTop = top;
1079
+ }
1080
+ __name(scrollTargetTo, "scrollTargetTo");
728
1081
  function ApiIntroSection({ info, schema, endpoints, resolvedBaseUrl }) {
729
1082
  return /* @__PURE__ */ jsxs("section", { className: "pb-10 mb-10 border-b", children: [
730
1083
  /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [
@@ -828,8 +1181,9 @@ function schemaToFields(schema, prefix = "", depth = 0) {
828
1181
  return rows;
829
1182
  }
830
1183
  __name(schemaToFields, "schemaToFields");
831
- function EndpointDoc({ endpoint, isLoadedInPlayground, onTryIt }) {
832
- const anchor = endpointAnchor(endpoint);
1184
+ function EndpointDoc({ endpoint, isLoadedInPlayground, onTryIt, schemaId }) {
1185
+ const scopedSchemaId = schemaId ?? endpoint.schemaId ?? null;
1186
+ const anchor = endpointAnchor(endpoint, scopedSchemaId);
833
1187
  const pathParams = endpoint.parameters?.filter((p) => endpoint.path.includes(`{${p.name}}`)) ?? [];
834
1188
  const queryParams = endpoint.parameters?.filter((p) => !endpoint.path.includes(`{${p.name}}`)) ?? [];
835
1189
  const [copied, setCopied] = useState(false);
@@ -847,6 +1201,7 @@ function EndpointDoc({ endpoint, isLoadedInPlayground, onTryIt }) {
847
1201
  {
848
1202
  id: anchor,
849
1203
  "data-endpoint-anchor": anchor,
1204
+ "data-schema-id": scopedSchemaId ?? "",
850
1205
  className: "scroll-mt-24 py-10 first:pt-0",
851
1206
  children: [
852
1207
  /* @__PURE__ */ jsxs("header", { className: "space-y-4", children: [
@@ -1007,52 +1362,103 @@ function StatusTag({ code }) {
1007
1362
  ), children: code });
1008
1363
  }
1009
1364
  __name(StatusTag, "StatusTag");
1010
- var DocsView = React6.forwardRef(/* @__PURE__ */ __name(function DocsView2({
1011
- info,
1012
- rawSchema,
1013
- resolvedBaseUrl,
1014
- endpoints,
1015
- selectedVersion,
1016
- loadedEndpoint,
1017
- onTryEndpoint,
1018
- onActiveChange
1019
- }, ref) {
1365
+ var readNavbarOffset = /* @__PURE__ */ __name(() => {
1366
+ if (typeof document === "undefined") return 0;
1367
+ const raw = getComputedStyle(document.documentElement).getPropertyValue("--navbar-height");
1368
+ const parsed = parseInt(raw || "", 10);
1369
+ return Number.isFinite(parsed) ? parsed : 0;
1370
+ }, "readNavbarOffset");
1371
+ var isSameEndpoint = /* @__PURE__ */ __name((a, b) => a !== null && a.method === b.method && a.path === b.path, "isSameEndpoint");
1372
+ function buildEndpointRow(ep, loadedEndpoint, schemaId) {
1373
+ const keySchema = schemaId ? `${schemaId}-` : "";
1374
+ return {
1375
+ key: `${keySchema}${ep.method}-${ep.path}`,
1376
+ endpoint: ep,
1377
+ isLoaded: isSameEndpoint(loadedEndpoint, ep),
1378
+ schemaId
1379
+ };
1380
+ }
1381
+ __name(buildEndpointRow, "buildEndpointRow");
1382
+ function buildSchemaSectionVM(entry, selectedVersion, loadedEndpoint) {
1383
+ const title = entry.info?.title ?? entry.source.name;
1384
+ const version = entry.info?.version ?? null;
1385
+ const description = entry.info?.description ?? null;
1386
+ let state;
1387
+ if (entry.loading) {
1388
+ state = { kind: "loading" };
1389
+ } else if (entry.error) {
1390
+ state = { kind: "error", message: entry.error };
1391
+ } else {
1392
+ const visible = deduplicateEndpoints(entry.endpoints, selectedVersion);
1393
+ state = visible.length === 0 ? { kind: "empty" } : {
1394
+ kind: "ready",
1395
+ rows: visible.map((ep) => buildEndpointRow(ep, loadedEndpoint, entry.source.id))
1396
+ };
1397
+ }
1398
+ return {
1399
+ schemaId: entry.source.id,
1400
+ title,
1401
+ version,
1402
+ description,
1403
+ state,
1404
+ rawSchema: entry.rawSchema,
1405
+ baseUrl: entry.resolvedBaseUrl,
1406
+ allEndpoints: entry.endpoints
1407
+ };
1408
+ }
1409
+ __name(buildSchemaSectionVM, "buildSchemaSectionVM");
1410
+ var DocsView = React6.forwardRef(/* @__PURE__ */ __name(function DocsView2(props, ref) {
1020
1411
  const scrollRef = useRef(null);
1021
- const visibleEndpoints = useMemo(
1022
- () => deduplicateEndpoints(endpoints, selectedVersion),
1023
- [endpoints, selectedVersion]
1024
- );
1025
- const scrollToAnchor = useCallback((anchor) => {
1026
- const root = scrollRef.current;
1027
- if (!root) return;
1028
- const el = root.querySelector(`[data-endpoint-anchor="${anchor}"]`);
1029
- if (!el) return;
1030
- el.scrollIntoView({ behavior: "smooth", block: "start" });
1412
+ const scrollTargetRef = useRef(null);
1413
+ const { onActiveChange } = props;
1414
+ const ensureScrollTarget = useCallback(() => {
1415
+ if (scrollTargetRef.current) return scrollTargetRef.current;
1416
+ if (!scrollRef.current) return null;
1417
+ scrollTargetRef.current = getScrollParent(scrollRef.current);
1418
+ return scrollTargetRef.current;
1031
1419
  }, []);
1420
+ const scrollToAnchor = useCallback(
1421
+ (anchor) => {
1422
+ const root = scrollRef.current;
1423
+ if (!root) return;
1424
+ const el = root.querySelector(`[data-endpoint-anchor="${anchor}"]`);
1425
+ if (!el) return;
1426
+ const target = ensureScrollTarget();
1427
+ if (!target) return;
1428
+ const navbar = readNavbarOffset();
1429
+ const top = el.getBoundingClientRect().top - getTargetTop(target) + getScrollTop(target) - navbar - 8;
1430
+ scrollTargetTo(target, top);
1431
+ },
1432
+ [ensureScrollTarget]
1433
+ );
1032
1434
  React6.useImperativeHandle(ref, () => ({ scrollToAnchor }), [scrollToAnchor]);
1033
1435
  useEffect(() => {
1034
1436
  const root = scrollRef.current;
1035
1437
  if (!root) return;
1438
+ const target = ensureScrollTarget();
1439
+ if (!target) return;
1036
1440
  let rafId = 0;
1037
1441
  let lastActive = null;
1038
1442
  const compute = /* @__PURE__ */ __name(() => {
1039
1443
  rafId = 0;
1040
1444
  const sections = root.querySelectorAll("[data-endpoint-anchor]");
1041
1445
  if (sections.length === 0) return;
1042
- const rootTop = root.getBoundingClientRect().top;
1043
- const threshold = rootTop + root.clientHeight * 0.25;
1446
+ const navbar = readNavbarOffset();
1447
+ const viewportTop = getTargetTop(target);
1448
+ const threshold = viewportTop + navbar + getViewportHeight(target) * 0.25;
1044
1449
  let active = null;
1045
1450
  for (const s of Array.from(sections)) {
1046
1451
  const top = s.getBoundingClientRect().top;
1047
1452
  if (top <= threshold) {
1048
- active = s.dataset.endpointAnchor ?? null;
1453
+ active = s;
1049
1454
  } else {
1050
1455
  break;
1051
1456
  }
1052
1457
  }
1053
- if (active !== lastActive) {
1054
- lastActive = active;
1055
- onActiveChange(active);
1458
+ const anchor = active?.dataset.endpointAnchor ?? null;
1459
+ if (anchor !== lastActive) {
1460
+ lastActive = anchor;
1461
+ onActiveChange(anchor, active?.dataset.schemaId || null);
1056
1462
  }
1057
1463
  }, "compute");
1058
1464
  const onScroll = /* @__PURE__ */ __name(() => {
@@ -1060,13 +1466,39 @@ var DocsView = React6.forwardRef(/* @__PURE__ */ __name(function DocsView2({
1060
1466
  rafId = requestAnimationFrame(compute);
1061
1467
  }, "onScroll");
1062
1468
  compute();
1063
- root.addEventListener("scroll", onScroll, { passive: true });
1469
+ target.addEventListener("scroll", onScroll, { passive: true });
1470
+ window.addEventListener("resize", onScroll, { passive: true });
1064
1471
  return () => {
1065
- root.removeEventListener("scroll", onScroll);
1472
+ target.removeEventListener("scroll", onScroll);
1473
+ window.removeEventListener("resize", onScroll);
1066
1474
  if (rafId) cancelAnimationFrame(rafId);
1067
1475
  };
1068
- }, [visibleEndpoints, onActiveChange]);
1069
- return /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "flex-1 overflow-y-auto min-h-0", children: /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-[860px] px-6 md:px-10 lg:px-14 py-12", children: [
1476
+ }, [onActiveChange, ensureScrollTarget, props]);
1477
+ if (props.grouping === "sections") {
1478
+ return /* @__PURE__ */ jsx(SectionsBody, { scrollRef, ...props });
1479
+ }
1480
+ return /* @__PURE__ */ jsx(SelectorBody, { scrollRef, ...props });
1481
+ }, "DocsView"));
1482
+ function SelectorBody({
1483
+ scrollRef,
1484
+ info,
1485
+ rawSchema,
1486
+ resolvedBaseUrl,
1487
+ endpoints,
1488
+ selectedVersion,
1489
+ loadedEndpoint,
1490
+ onTryEndpoint
1491
+ }) {
1492
+ const visibleEndpoints = useMemo(
1493
+ () => deduplicateEndpoints(endpoints, selectedVersion),
1494
+ [endpoints, selectedVersion]
1495
+ );
1496
+ const rows = useMemo(
1497
+ () => visibleEndpoints.map((ep) => buildEndpointRow(ep, loadedEndpoint, ep.schemaId ?? null)),
1498
+ [visibleEndpoints, loadedEndpoint]
1499
+ );
1500
+ const isEmpty = rows.length === 0;
1501
+ return /* @__PURE__ */ jsx("div", { ref: scrollRef, children: /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-[860px] px-6 md:px-10 lg:px-14 py-12", children: [
1070
1502
  info && /* @__PURE__ */ jsx(
1071
1503
  ApiIntroSection,
1072
1504
  {
@@ -1076,20 +1508,96 @@ var DocsView = React6.forwardRef(/* @__PURE__ */ __name(function DocsView2({
1076
1508
  resolvedBaseUrl
1077
1509
  }
1078
1510
  ),
1079
- visibleEndpoints.length === 0 ? /* @__PURE__ */ jsx("div", { className: "py-16 text-center text-sm text-muted-foreground", children: "No endpoints to display." }) : /* @__PURE__ */ jsx("div", { className: "divide-y divide-border/60", children: visibleEndpoints.map((ep) => {
1080
- const isLoaded = loadedEndpoint?.method === ep.method && loadedEndpoint?.path === ep.path;
1081
- return /* @__PURE__ */ jsx(
1511
+ isEmpty ? /* @__PURE__ */ jsx("div", { className: "py-16 text-center text-sm text-muted-foreground", children: "No endpoints to display." }) : /* @__PURE__ */ jsx("div", { className: "divide-y divide-border/60", children: rows.map((row) => /* @__PURE__ */ jsx(
1512
+ EndpointDoc,
1513
+ {
1514
+ endpoint: row.endpoint,
1515
+ isLoadedInPlayground: row.isLoaded,
1516
+ onTryIt: () => onTryEndpoint(row.endpoint),
1517
+ schemaId: row.schemaId
1518
+ },
1519
+ row.key
1520
+ )) })
1521
+ ] }) });
1522
+ }
1523
+ __name(SelectorBody, "SelectorBody");
1524
+ function SectionsBody({
1525
+ scrollRef,
1526
+ schemasData,
1527
+ selectedVersion,
1528
+ loadedEndpoint,
1529
+ onTryEndpoint
1530
+ }) {
1531
+ const sections = useMemo(
1532
+ () => schemasData.map((e) => buildSchemaSectionVM(e, selectedVersion, loadedEndpoint)),
1533
+ [schemasData, selectedVersion, loadedEndpoint]
1534
+ );
1535
+ return /* @__PURE__ */ jsx("div", { ref: scrollRef, children: /* @__PURE__ */ jsx("div", { className: "mx-auto w-full max-w-[860px] px-6 md:px-10 lg:px-14 py-12 space-y-16", children: sections.map((section) => /* @__PURE__ */ jsx(SchemaSectionView, { section, onTryEndpoint }, section.schemaId)) }) });
1536
+ }
1537
+ __name(SectionsBody, "SectionsBody");
1538
+ var SchemaSectionView = React6.memo(/* @__PURE__ */ __name(function SchemaSectionView2({
1539
+ section,
1540
+ onTryEndpoint
1541
+ }) {
1542
+ const canCopy = section.rawSchema !== null && section.allEndpoints.length > 0;
1543
+ return /* @__PURE__ */ jsxs("section", { "data-schema-anchor": section.schemaId, className: "scroll-mt-20", children: [
1544
+ /* @__PURE__ */ jsxs("header", { className: "mb-8 pb-4 border-b", children: [
1545
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4", children: [
1546
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-3 min-w-0", children: [
1547
+ /* @__PURE__ */ jsx("h2", { className: "text-2xl font-semibold tracking-tight", children: section.title }),
1548
+ section.version && /* @__PURE__ */ jsxs("span", { className: "font-mono text-xs text-muted-foreground/70", children: [
1549
+ "v",
1550
+ section.version
1551
+ ] })
1552
+ ] }),
1553
+ canCopy && /* @__PURE__ */ jsx(
1554
+ SchemaCopyMenu,
1555
+ {
1556
+ schema: section.rawSchema,
1557
+ endpoints: section.allEndpoints,
1558
+ baseUrl: section.baseUrl
1559
+ }
1560
+ )
1561
+ ] }),
1562
+ section.description && /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-muted-foreground whitespace-pre-wrap", children: section.description })
1563
+ ] }),
1564
+ /* @__PURE__ */ jsx(SchemaSectionStateView, { section, onTryEndpoint })
1565
+ ] });
1566
+ }, "SchemaSectionView"));
1567
+ function SchemaSectionStateView({
1568
+ section,
1569
+ onTryEndpoint
1570
+ }) {
1571
+ switch (section.state.kind) {
1572
+ case "loading":
1573
+ return /* @__PURE__ */ jsxs("div", { className: "py-8 text-center text-sm text-muted-foreground", children: [
1574
+ "Loading ",
1575
+ section.title,
1576
+ "\u2026"
1577
+ ] });
1578
+ case "error":
1579
+ return /* @__PURE__ */ jsxs("div", { className: "py-8 text-center text-sm text-destructive", children: [
1580
+ "Failed to load ",
1581
+ section.title,
1582
+ ": ",
1583
+ section.state.message
1584
+ ] });
1585
+ case "empty":
1586
+ return /* @__PURE__ */ jsx("div", { className: "py-8 text-center text-sm text-muted-foreground", children: "No endpoints in this schema." });
1587
+ case "ready":
1588
+ return /* @__PURE__ */ jsx("div", { className: "divide-y divide-border/60", children: section.state.rows.map((row) => /* @__PURE__ */ jsx(
1082
1589
  EndpointDoc,
1083
1590
  {
1084
- endpoint: ep,
1085
- isLoadedInPlayground: isLoaded,
1086
- onTryIt: () => onTryEndpoint(ep)
1591
+ endpoint: row.endpoint,
1592
+ isLoadedInPlayground: row.isLoaded,
1593
+ onTryIt: () => onTryEndpoint(row.endpoint),
1594
+ schemaId: row.schemaId
1087
1595
  },
1088
- `${ep.method}-${ep.path}`
1089
- );
1090
- }) })
1091
- ] }) });
1092
- }, "DocsView"));
1596
+ row.key
1597
+ )) });
1598
+ }
1599
+ }
1600
+ __name(SchemaSectionStateView, "SchemaSectionStateView");
1093
1601
  var MAX_DEPTH2 = 6;
1094
1602
  function defaultForSchema(schema) {
1095
1603
  if (!schema) return null;
@@ -1882,6 +2390,9 @@ var DocsLayout = /* @__PURE__ */ __name(() => {
1882
2390
  const { state, config, setSelectedEndpoint } = usePlaygroundContext();
1883
2391
  const isDesktop = useMediaQuery("(min-width: 1024px)");
1884
2392
  const isMobile = !isDesktop;
2393
+ const grouping = config.schemaGrouping ?? "selector";
2394
+ const preloadAll = grouping === "sections";
2395
+ const urlSyncEnabled = typeof config.urlSync === "boolean" ? config.urlSync : Boolean(config.urlSync?.enabled);
1885
2396
  const {
1886
2397
  endpoints,
1887
2398
  schemaInfo,
@@ -1891,16 +2402,26 @@ var DocsLayout = /* @__PURE__ */ __name(() => {
1891
2402
  error,
1892
2403
  schemas,
1893
2404
  currentSchema,
1894
- setCurrentSchema
2405
+ setCurrentSchema,
2406
+ schemasData
1895
2407
  } = useOpenApiSchema({
1896
2408
  schemas: config.schemas,
1897
2409
  defaultSchemaId: config.defaultSchemaId,
1898
- baseUrl: config.baseUrl
2410
+ baseUrl: config.baseUrl,
2411
+ preloadAll
1899
2412
  });
1900
2413
  const [activeAnchor, setActiveAnchor] = useState(null);
2414
+ const [activeSchemaId, setActiveSchemaId] = useState(null);
1901
2415
  const [sheetOpen, setSheetOpen] = useState(false);
1902
2416
  const docsRef = useRef(null);
1903
2417
  const slideOpen = !isMobile && state.selectedEndpoint !== null;
2418
+ const endpointsBySchema = useMemo(() => {
2419
+ if (grouping !== "sections") return {};
2420
+ const byId = keyBy(schemasData, (e) => e.source.id);
2421
+ const out = {};
2422
+ for (const src of schemas) out[src.id] = byId[src.id]?.endpoints ?? [];
2423
+ return out;
2424
+ }, [grouping, schemasData, schemas]);
1904
2425
  const handleTry = useCallback(
1905
2426
  (ep) => {
1906
2427
  setSelectedEndpoint(ep);
@@ -1911,29 +2432,71 @@ var DocsLayout = /* @__PURE__ */ __name(() => {
1911
2432
  const handleCloseSlide = useCallback(() => {
1912
2433
  setSelectedEndpoint(null);
1913
2434
  }, [setSelectedEndpoint]);
1914
- const handleNavigate = useCallback((anchor) => {
1915
- docsRef.current?.scrollToAnchor(anchor);
2435
+ const handleNavigate = useCallback(
2436
+ (anchor, schemaId) => {
2437
+ if (schemaId && schemaId !== currentSchema?.id && grouping === "selector") {
2438
+ setCurrentSchema(schemaId);
2439
+ requestAnimationFrame(() => {
2440
+ docsRef.current?.scrollToAnchor(anchor);
2441
+ });
2442
+ return;
2443
+ }
2444
+ docsRef.current?.scrollToAnchor(anchor);
2445
+ },
2446
+ [currentSchema?.id, grouping, setCurrentSchema]
2447
+ );
2448
+ const handleActiveChange = useCallback((anchor, schemaId) => {
2449
+ setActiveAnchor(anchor);
2450
+ setActiveSchemaId(schemaId);
1916
2451
  }, []);
1917
- if (loading) {
1918
- return /* @__PURE__ */ jsxs(
1919
- "div",
1920
- {
1921
- className: "grid grid-cols-[260px_1fr] min-h-0 overflow-hidden",
1922
- style: { height: "calc(100dvh - var(--navbar-height, 64px))" },
1923
- children: [
1924
- /* @__PURE__ */ jsx("div", { className: "border-r p-3 space-y-1.5", children: Array.from({ length: 12 }).map((_, i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-full rounded" }, i)) }),
1925
- /* @__PURE__ */ jsxs("div", { className: "p-8 space-y-4", children: [
1926
- /* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-1/2" }),
1927
- /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }),
1928
- /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/4" }),
1929
- /* @__PURE__ */ jsx("div", { className: "mt-8 space-y-6", children: Array.from({ length: 3 }).map((_, i) => /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1930
- /* @__PURE__ */ jsx(Skeleton, { className: "h-6 w-1/3" }),
1931
- /* @__PURE__ */ jsx(Skeleton, { className: "h-20 w-full" })
1932
- ] }, i)) })
1933
- ] })
1934
- ]
2452
+ const effectiveSchemaId = grouping === "sections" ? activeSchemaId : currentSchema?.id ?? null;
2453
+ const handleHashTarget = useCallback(
2454
+ (target) => {
2455
+ if (!target.schemaId && !target.anchor) return;
2456
+ const matched = target.schemaId ? schemas.find((s) => s.id === target.schemaId || slugifySchemaId(s.id) === target.schemaId) : null;
2457
+ const needsSchemaSwitch = matched && grouping === "selector" && matched.id !== currentSchema?.id;
2458
+ if (needsSchemaSwitch) {
2459
+ setCurrentSchema(matched.id);
1935
2460
  }
1936
- );
2461
+ if (target.anchor) {
2462
+ const anchor = target.anchor;
2463
+ if (needsSchemaSwitch) {
2464
+ requestAnimationFrame(() => {
2465
+ docsRef.current?.scrollToAnchor(anchor);
2466
+ });
2467
+ } else {
2468
+ docsRef.current?.scrollToAnchor(anchor);
2469
+ }
2470
+ }
2471
+ },
2472
+ [schemas, grouping, currentSchema?.id, setCurrentSchema]
2473
+ );
2474
+ useDocsUrlSync({
2475
+ enabled: urlSyncEnabled,
2476
+ currentSchemaId: effectiveSchemaId,
2477
+ activeAnchor,
2478
+ onHashTarget: handleHashTarget
2479
+ });
2480
+ if (loading) {
2481
+ return /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[260px_1fr] items-start", children: [
2482
+ /* @__PURE__ */ jsx(
2483
+ "div",
2484
+ {
2485
+ className: "sticky top-[var(--navbar-height,64px)] border-r p-3 space-y-1.5 overflow-y-auto",
2486
+ style: { height: "calc(100dvh - var(--navbar-height, 64px))" },
2487
+ children: Array.from({ length: 12 }).map((_, i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-full rounded" }, i))
2488
+ }
2489
+ ),
2490
+ /* @__PURE__ */ jsxs("div", { className: "p-8 space-y-4", children: [
2491
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-1/2" }),
2492
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }),
2493
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/4" }),
2494
+ /* @__PURE__ */ jsx("div", { className: "mt-8 space-y-6", children: Array.from({ length: 3 }).map((_, i) => /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
2495
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-6 w-1/3" }),
2496
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-20 w-full" })
2497
+ ] }, i)) })
2498
+ ] })
2499
+ ] });
1937
2500
  }
1938
2501
  if (error) {
1939
2502
  return /* @__PURE__ */ jsx(
@@ -1949,40 +2512,44 @@ var DocsLayout = /* @__PURE__ */ __name(() => {
1949
2512
  );
1950
2513
  }
1951
2514
  if (isMobile) {
1952
- return /* @__PURE__ */ jsxs(
2515
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
2516
+ /* @__PURE__ */ jsx(EndpointDraftSync, { schemaId: currentSchema?.id ?? null }),
2517
+ grouping === "sections" ? /* @__PURE__ */ jsx(
2518
+ DocsView,
2519
+ {
2520
+ ref: docsRef,
2521
+ grouping: "sections",
2522
+ schemasData,
2523
+ selectedVersion: state.selectedVersion,
2524
+ loadedEndpoint: state.selectedEndpoint,
2525
+ onTryEndpoint: handleTry,
2526
+ onActiveChange: handleActiveChange
2527
+ }
2528
+ ) : /* @__PURE__ */ jsx(
2529
+ DocsView,
2530
+ {
2531
+ ref: docsRef,
2532
+ info: schemaInfo,
2533
+ rawSchema,
2534
+ resolvedBaseUrl,
2535
+ endpoints,
2536
+ selectedVersion: state.selectedVersion,
2537
+ loadedEndpoint: state.selectedEndpoint,
2538
+ onTryEndpoint: handleTry,
2539
+ onActiveChange: handleActiveChange
2540
+ }
2541
+ ),
2542
+ /* @__PURE__ */ jsx(TryItSheet, { open: sheetOpen, onOpenChange: setSheetOpen })
2543
+ ] });
2544
+ }
2545
+ return /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 350, children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[260px_minmax(0,1fr)] items-start", children: [
2546
+ /* @__PURE__ */ jsx(EndpointDraftSync, { schemaId: currentSchema?.id ?? null }),
2547
+ /* @__PURE__ */ jsx(
1953
2548
  "div",
1954
2549
  {
1955
- className: "flex flex-col overflow-hidden",
2550
+ className: "sticky top-[var(--navbar-height,64px)]",
1956
2551
  style: { height: "calc(100dvh - var(--navbar-height, 64px))" },
1957
- children: [
1958
- /* @__PURE__ */ jsx(EndpointDraftSync, { schemaId: currentSchema?.id ?? null }),
1959
- /* @__PURE__ */ jsx(
1960
- DocsView,
1961
- {
1962
- ref: docsRef,
1963
- info: schemaInfo,
1964
- rawSchema,
1965
- resolvedBaseUrl,
1966
- endpoints,
1967
- selectedVersion: state.selectedVersion,
1968
- loadedEndpoint: state.selectedEndpoint,
1969
- onTryEndpoint: handleTry,
1970
- onActiveChange: setActiveAnchor
1971
- }
1972
- ),
1973
- /* @__PURE__ */ jsx(TryItSheet, { open: sheetOpen, onOpenChange: setSheetOpen })
1974
- ]
1975
- }
1976
- );
1977
- }
1978
- return /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 350, children: /* @__PURE__ */ jsxs(
1979
- "div",
1980
- {
1981
- className: "grid grid-cols-[260px_minmax(0,1fr)] min-h-0 overflow-hidden",
1982
- style: { height: "calc(100dvh - var(--navbar-height, 64px))" },
1983
- children: [
1984
- /* @__PURE__ */ jsx(EndpointDraftSync, { schemaId: currentSchema?.id ?? null }),
1985
- /* @__PURE__ */ jsx(
2552
+ children: /* @__PURE__ */ jsx(
1986
2553
  DocsSidebar,
1987
2554
  {
1988
2555
  info: schemaInfo,
@@ -1992,29 +2559,44 @@ var DocsLayout = /* @__PURE__ */ __name(() => {
1992
2559
  onSchemaChange: setCurrentSchema,
1993
2560
  activeEndpointId: activeAnchor,
1994
2561
  selectedVersion: state.selectedVersion,
1995
- onNavigate: handleNavigate
1996
- }
1997
- ),
1998
- /* @__PURE__ */ jsx(
1999
- DocsView,
2000
- {
2001
- ref: docsRef,
2002
- info: schemaInfo,
2562
+ onNavigate: handleNavigate,
2563
+ grouping,
2564
+ endpointsBySchema,
2003
2565
  rawSchema,
2004
- resolvedBaseUrl,
2005
- endpoints,
2006
- selectedVersion: state.selectedVersion,
2007
- loadedEndpoint: state.selectedEndpoint,
2008
- onTryEndpoint: handleTry,
2009
- onActiveChange: setActiveAnchor
2566
+ resolvedBaseUrl
2010
2567
  }
2011
- ),
2012
- /* @__PURE__ */ jsx(SlideInPlayground, { open: slideOpen, onClose: handleCloseSlide })
2013
- ]
2014
- }
2015
- ) });
2568
+ )
2569
+ }
2570
+ ),
2571
+ grouping === "sections" ? /* @__PURE__ */ jsx(
2572
+ DocsView,
2573
+ {
2574
+ ref: docsRef,
2575
+ grouping: "sections",
2576
+ schemasData,
2577
+ selectedVersion: state.selectedVersion,
2578
+ loadedEndpoint: state.selectedEndpoint,
2579
+ onTryEndpoint: handleTry,
2580
+ onActiveChange: handleActiveChange
2581
+ }
2582
+ ) : /* @__PURE__ */ jsx(
2583
+ DocsView,
2584
+ {
2585
+ ref: docsRef,
2586
+ info: schemaInfo,
2587
+ rawSchema,
2588
+ resolvedBaseUrl,
2589
+ endpoints,
2590
+ selectedVersion: state.selectedVersion,
2591
+ loadedEndpoint: state.selectedEndpoint,
2592
+ onTryEndpoint: handleTry,
2593
+ onActiveChange: handleActiveChange
2594
+ }
2595
+ ),
2596
+ /* @__PURE__ */ jsx(SlideInPlayground, { open: slideOpen, onClose: handleCloseSlide })
2597
+ ] }) });
2016
2598
  }, "DocsLayout");
2017
2599
 
2018
2600
  export { DocsLayout };
2019
- //# sourceMappingURL=DocsLayout-ERETJLLV.mjs.map
2020
- //# sourceMappingURL=DocsLayout-ERETJLLV.mjs.map
2601
+ //# sourceMappingURL=DocsLayout-TKJQ5W5E.mjs.map
2602
+ //# sourceMappingURL=DocsLayout-TKJQ5W5E.mjs.map