@djangocfg/ui-tools 2.1.318 → 2.1.319

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/{DocsLayout-ESVQZO3V.mjs → DocsLayout-CTJINVBM.mjs} +235 -267
  2. package/dist/DocsLayout-CTJINVBM.mjs.map +1 -0
  3. package/dist/{DocsLayout-KUPDWJ3G.cjs → DocsLayout-XLDB6CJ2.cjs} +273 -305
  4. package/dist/DocsLayout-XLDB6CJ2.cjs.map +1 -0
  5. package/dist/{chunk-GBLQTHWT.mjs → chunk-62Y65TGK.mjs} +5 -4
  6. package/dist/chunk-62Y65TGK.mjs.map +1 -0
  7. package/dist/{chunk-S44PW6NK.cjs → chunk-TKSFZHCG.cjs} +5 -4
  8. package/dist/chunk-TKSFZHCG.cjs.map +1 -0
  9. package/dist/index.cjs +10 -10
  10. package/dist/index.mjs +4 -4
  11. package/package.json +6 -6
  12. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +30 -0
  13. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +35 -50
  14. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/index.tsx +49 -22
  15. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/index.tsx +1 -1
  16. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/index.ts +10 -11
  17. package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +25 -5
  18. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/BrandHeader.tsx +18 -33
  19. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/Toolbar.tsx +40 -24
  20. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/index.tsx +8 -14
  21. package/src/tools/OpenapiViewer/components/DocsLayout/sidebarLabel.ts +1 -4
  22. package/src/tools/OpenapiViewer/utils/operationToHar.ts +2 -1
  23. package/src/tools/OpenapiViewer/utils/url.ts +9 -2
  24. package/dist/DocsLayout-ESVQZO3V.mjs.map +0 -1
  25. package/dist/DocsLayout-KUPDWJ3G.cjs.map +0 -1
  26. package/dist/chunk-GBLQTHWT.mjs.map +0 -1
  27. package/dist/chunk-S44PW6NK.cjs.map +0 -1
  28. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/MethodChips.tsx +0 -43
@@ -4,7 +4,6 @@ import {
4
4
  Check,
5
5
  ChevronsDownUp,
6
6
  ChevronsUpDown,
7
- FileCode2,
8
7
  Link2,
9
8
  } from 'lucide-react';
10
9
  import React, { useCallback, useMemo, useState } from 'react';
@@ -23,7 +22,6 @@ import type { SectionId } from '../types';
23
22
 
24
23
  interface MetaActionsProps {
25
24
  anchor: string;
26
- endpointMarkdown: string;
27
25
  /** Sections present on this endpoint — expand/collapse acts only
28
26
  * on visible rows, never on catalogue items the card doesn't render. */
29
27
  presentSections: readonly SectionId[];
@@ -63,19 +61,22 @@ function IconButton({ label, onClick, children, active }: IconButtonProps) {
63
61
  );
64
62
  }
65
63
 
66
- /** Inline meta-row actions: copy link · copy markdown · expand/collapse
67
- * all. Actions are always visible (corporate tool pattern) but sized
68
- * down so the path on the next line stays the visual focal point. */
69
- export function MetaActions({ anchor, endpointMarkdown, presentSections }: MetaActionsProps) {
64
+ /** Inline meta-row actions: copy link · expand/collapse all. Actions
65
+ * are always visible (corporate tool pattern) but sized down so the
66
+ * path on the next line stays the visual focal point. The
67
+ * AI-friendly "AI Copy" used to live here as another icon — it was
68
+ * invisible at that size, so it now sits next to "Try it" as a
69
+ * proper labelled button. */
70
+ export function MetaActions({ anchor, presentSections }: MetaActionsProps) {
70
71
  const { endpointId } = useEndpointDocContext();
71
72
  const expandAll = useEndpointDocStore((s) => s.expandAll);
72
73
  const collapseAll = useEndpointDocStore((s) => s.collapseAll);
73
74
  const openSections = useEndpointDocStore((s) => s.openSections);
74
75
 
75
- const [justCopied, setJustCopied] = useState<'link' | 'md' | null>(null);
76
- const flash = useCallback((which: 'link' | 'md') => {
77
- setJustCopied(which);
78
- setTimeout(() => setJustCopied(null), 1200);
76
+ const [linkCopied, setLinkCopied] = useState(false);
77
+ const flashLink = useCallback(() => {
78
+ setLinkCopied(true);
79
+ setTimeout(() => setLinkCopied(false), 1200);
79
80
  }, []);
80
81
 
81
82
  const mostlyOpen = useMemo(() => {
@@ -90,56 +91,40 @@ export function MetaActions({ anchor, endpointMarkdown, presentSections }: MetaA
90
91
  const copyLink = useCallback(() => {
91
92
  if (typeof window === 'undefined') return;
92
93
  const url = `${window.location.origin}${window.location.pathname}#${anchor}`;
93
- void navigator.clipboard?.writeText(url).then(() => flash('link'));
94
- }, [anchor, flash]);
95
-
96
- const copyMarkdown = useCallback(() => {
97
- if (typeof window === 'undefined') return;
98
- void navigator.clipboard?.writeText(endpointMarkdown).then(() => flash('md'));
99
- }, [endpointMarkdown, flash]);
94
+ void navigator.clipboard?.writeText(url).then(flashLink);
95
+ }, [anchor, flashLink]);
100
96
 
101
97
  const toggleAll = useCallback(() => {
102
98
  if (mostlyOpen) collapseAll(endpointId, presentSections);
103
99
  else expandAll(endpointId, presentSections);
104
100
  }, [mostlyOpen, collapseAll, expandAll, endpointId, presentSections]);
105
101
 
102
+ const linkLabel = linkCopied ? 'Copied!' : 'Copy link to endpoint';
103
+ const linkIcon = linkCopied ? (
104
+ <Check className="h-3.5 w-3.5" />
105
+ ) : (
106
+ <Link2 className="h-3.5 w-3.5" />
107
+ );
108
+ const showToggleAll = presentSections.length >= 2;
109
+ const toggleAllLabel = mostlyOpen ? 'Collapse all sections' : 'Expand all sections';
110
+ const toggleAllIcon = mostlyOpen ? (
111
+ <ChevronsDownUp className="h-3.5 w-3.5" />
112
+ ) : (
113
+ <ChevronsUpDown className="h-3.5 w-3.5" />
114
+ );
115
+ const toggleAllNode = showToggleAll ? (
116
+ <IconButton label={toggleAllLabel} onClick={toggleAll}>
117
+ {toggleAllIcon}
118
+ </IconButton>
119
+ ) : null;
120
+
106
121
  return (
107
122
  <SafeTooltipProvider delayDuration={200}>
108
123
  <div className="flex items-center gap-0.5">
109
- <IconButton
110
- label={justCopied === 'link' ? 'Copied!' : 'Copy link to endpoint'}
111
- onClick={copyLink}
112
- active={justCopied === 'link'}
113
- >
114
- {justCopied === 'link' ? (
115
- <Check className="h-3.5 w-3.5" />
116
- ) : (
117
- <Link2 className="h-3.5 w-3.5" />
118
- )}
119
- </IconButton>
120
- <IconButton
121
- label={justCopied === 'md' ? 'Copied!' : 'Copy as Markdown (for AI)'}
122
- onClick={copyMarkdown}
123
- active={justCopied === 'md'}
124
- >
125
- {justCopied === 'md' ? (
126
- <Check className="h-3.5 w-3.5" />
127
- ) : (
128
- <FileCode2 className="h-3.5 w-3.5" />
129
- )}
124
+ <IconButton label={linkLabel} onClick={copyLink} active={linkCopied}>
125
+ {linkIcon}
130
126
  </IconButton>
131
- {presentSections.length >= 2 && (
132
- <IconButton
133
- label={mostlyOpen ? 'Collapse all sections' : 'Expand all sections'}
134
- onClick={toggleAll}
135
- >
136
- {mostlyOpen ? (
137
- <ChevronsDownUp className="h-3.5 w-3.5" />
138
- ) : (
139
- <ChevronsUpDown className="h-3.5 w-3.5" />
140
- )}
141
- </IconButton>
142
- )}
127
+ {toggleAllNode}
143
128
  </div>
144
129
  </SafeTooltipProvider>
145
130
  );
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { Play } from 'lucide-react';
4
- import React, { useMemo } from 'react';
3
+ import { Check, Play, Sparkles } from 'lucide-react';
4
+ import React, { useCallback, useMemo, useState } from 'react';
5
5
 
6
6
  import { Button } from '@djangocfg/ui-core/components';
7
7
 
@@ -43,29 +43,60 @@ export function EndpointHeader({
43
43
  // reference changes, not on unrelated re-renders of the subtree.
44
44
  const endpointMd = useMemo(() => endpointToMarkdown(endpoint), [endpoint]);
45
45
 
46
+ const [aiCopied, setAiCopied] = useState(false);
47
+ const onAiCopy = useCallback(() => {
48
+ if (typeof window === 'undefined') return;
49
+ void navigator.clipboard?.writeText(endpointMd).then(() => {
50
+ setAiCopied(true);
51
+ setTimeout(() => setAiCopied(false), 1200);
52
+ });
53
+ }, [endpointMd]);
54
+
55
+ const tryItLabel = isLoadedInPlayground ? 'Loaded' : 'Try it';
56
+ const aiCopyIcon = aiCopied ? (
57
+ <Check className="h-3 w-3" />
58
+ ) : (
59
+ <Sparkles className="h-3 w-3" />
60
+ );
61
+ const aiCopyLabel = aiCopied ? 'Copied' : 'AI Copy';
62
+ const descriptionNode = endpoint.description ? (
63
+ <div className="text-muted-foreground text-sm">
64
+ <MarkdownMessage content={endpoint.description} />
65
+ </div>
66
+ ) : null;
67
+
46
68
  return (
47
69
  <header className="space-y-3">
48
70
  {/* Row 1 — meta strip. Badge + inline icon actions on the
49
- left, primary CTA on the right. Kept tight (24px tall)
71
+ left, primary CTAs on the right. Kept tight (24px tall)
50
72
  so it doesn't visually compete with the path row below. */}
51
73
  <div className="flex items-center gap-3 flex-wrap">
52
74
  <div className="flex items-center gap-2 min-w-0">
53
75
  <MethodBadge method={endpoint.method} />
54
- <MetaActions
55
- anchor={anchor}
56
- endpointMarkdown={endpointMd}
57
- presentSections={presentSections}
58
- />
76
+ <MetaActions anchor={anchor} presentSections={presentSections} />
77
+ </div>
78
+ <div className="ml-auto flex items-center gap-2">
79
+ <Button
80
+ size="sm"
81
+ variant="secondary"
82
+ onClick={onAiCopy}
83
+ title="Copy endpoint as Markdown for AI"
84
+ aria-label="Copy endpoint as Markdown for AI"
85
+ className="h-7 text-xs gap-1.5 px-2.5"
86
+ >
87
+ {aiCopyIcon}
88
+ {aiCopyLabel}
89
+ </Button>
90
+ <Button
91
+ size="sm"
92
+ variant={isLoadedInPlayground ? 'secondary' : 'default'}
93
+ onClick={onTryIt}
94
+ className="h-7 text-xs gap-1.5 px-2.5"
95
+ >
96
+ <Play className="h-3 w-3" />
97
+ {tryItLabel}
98
+ </Button>
59
99
  </div>
60
- <Button
61
- size="sm"
62
- variant={isLoadedInPlayground ? 'secondary' : 'default'}
63
- onClick={onTryIt}
64
- className="ml-auto h-7 text-xs gap-1.5 px-2.5"
65
- >
66
- <Play className="h-3 w-3" />
67
- {isLoadedInPlayground ? 'Loaded' : 'Try it'}
68
- </Button>
69
100
  </div>
70
101
 
71
102
  {/* Row 2 — path as the visual focal point. Larger and more
@@ -77,11 +108,7 @@ export function EndpointHeader({
77
108
 
78
109
  {/* Row 3 — description, aligned to the left edge under the
79
110
  path. Text-sm so it reads as subtitle, not body copy. */}
80
- {endpoint.description && (
81
- <div className="text-muted-foreground text-sm">
82
- <MarkdownMessage content={endpoint.description} />
83
- </div>
84
- )}
111
+ {descriptionNode}
85
112
  </header>
86
113
  );
87
114
  }
@@ -37,7 +37,7 @@ export function Section({ id, title, badge, children }: SectionProps) {
37
37
  title={title}
38
38
  badge={badge}
39
39
  open={open}
40
- onToggle={() => toggleSection(endpointId, id)}
40
+ onToggle={() => toggleSection(endpointId, id, defaultOpen)}
41
41
  />
42
42
  {open && <div>{children}</div>}
43
43
  </div>
@@ -26,7 +26,13 @@ export interface EndpointDocState {
26
26
  }
27
27
 
28
28
  export interface EndpointDocActions {
29
- toggleSection: (endpointId: string, sectionId: SectionId) => void;
29
+ /** Flip the section's open state. ``defaultOpen`` is the value the
30
+ * ``Section`` component currently shows when the user has no
31
+ * explicit override yet — passing it in lets the first click
32
+ * always invert what's actually on screen, instead of always
33
+ * setting ``false`` (which silently no-ops when the section was
34
+ * closed by default). */
35
+ toggleSection: (endpointId: string, sectionId: SectionId, defaultOpen: boolean) => void;
30
36
  setSectionOpen: (endpointId: string, sectionId: SectionId, open: boolean) => void;
31
37
  setCodeTab: (endpointId: string, tab: CodeSampleTargetId) => void;
32
38
  /** Bulk ops — "expand all" / "collapse all" on a single endpoint.
@@ -51,22 +57,15 @@ export const useEndpointDocStore = create<EndpointDocStore>()(
51
57
  (set) => ({
52
58
  ...initialState,
53
59
 
54
- toggleSection: (endpointId, sectionId) =>
60
+ toggleSection: (endpointId, sectionId, defaultOpen) =>
55
61
  set((state) => {
56
62
  const key = sectionKey(endpointId, sectionId);
57
63
  const current = state.openSections[key];
64
+ const visible = current === undefined ? defaultOpen : current;
58
65
  return {
59
66
  openSections: {
60
67
  ...state.openSections,
61
- // If there's no explicit override yet, the user's
62
- // first click means "flip from the default". We
63
- // assume the default was ``true`` for the most
64
- // common case (bodies/responses) and ``false``
65
- // otherwise; the Section component tracks this
66
- // via its ``defaultOpen`` prop and never calls
67
- // toggle on sections whose defaults match the
68
- // next state.
69
- [key]: current === undefined ? false : !current,
68
+ [key]: !visible,
70
69
  },
71
70
  };
72
71
  }),
@@ -48,8 +48,14 @@ interface SchemaCopyMenuProps {
48
48
  /** Trigger appearance.
49
49
  * - ``button`` (default) — labelled pill with icon + chevron.
50
50
  * - ``icon`` — square ghost button, used in tight spots like the
51
- * sidebar header where there is no room for "Copy for AI". */
52
- variant?: 'button' | 'icon';
51
+ * sidebar header where there is no room for "Copy for AI".
52
+ * - ``footer`` full-width secondary CTA, designed to sit at the
53
+ * bottom of the sidebar. Menu opens upward so it stays inside
54
+ * the panel. */
55
+ variant?: 'button' | 'icon' | 'footer';
56
+ /** Where the dropdown content opens. Defaults to ``right`` for the
57
+ * inline triggers; ``footer`` overrides to ``top``. */
58
+ side?: 'right' | 'top' | 'bottom' | 'left';
53
59
  }
54
60
 
55
61
  /**
@@ -58,7 +64,7 @@ interface SchemaCopyMenuProps {
58
64
  * dereferencing + stringifying a large schema can be non-trivial — sizes
59
65
  * are displayed after the first successful copy, via a tiny cache.
60
66
  */
61
- export function SchemaCopyMenu({ schema, endpoints, baseUrl, variant = 'button' }: SchemaCopyMenuProps) {
67
+ export function SchemaCopyMenu({ schema, endpoints, baseUrl, variant = 'button', side }: SchemaCopyMenuProps) {
62
68
  const [sizeCache, setSizeCache] = useState<Partial<Record<Flavour, string>>>({});
63
69
  const [justCopied, setJustCopied] = useState<Flavour | null>(null);
64
70
  const [open, setOpen] = useState(false);
@@ -98,6 +104,9 @@ export function SchemaCopyMenu({ schema, endpoints, baseUrl, variant = 'button'
98
104
 
99
105
  const flavours = useMemo<Flavour[]>(() => ['markdown', 'compact', 'raw'], []);
100
106
 
107
+ const resolvedSide = side ?? (variant === 'footer' ? 'top' : 'right');
108
+ const resolvedAlign = variant === 'footer' ? 'center' : 'start';
109
+
101
110
  return (
102
111
  <DropdownMenu open={open} onOpenChange={setOpen}>
103
112
  <DropdownMenuTrigger asChild>
@@ -112,6 +121,17 @@ export function SchemaCopyMenu({ schema, endpoints, baseUrl, variant = 'button'
112
121
  >
113
122
  <Sparkles className="h-3.5 w-3.5" />
114
123
  </Button>
124
+ ) : variant === 'footer' ? (
125
+ <Button
126
+ variant="secondary"
127
+ size="sm"
128
+ className="w-full justify-center gap-1.5 text-xs"
129
+ disabled={!isReady}
130
+ >
131
+ <Sparkles className="h-3 w-3" />
132
+ Copy schema for AI
133
+ <ChevronDown className="h-3 w-3 opacity-60" />
134
+ </Button>
115
135
  ) : (
116
136
  <Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs" disabled={!isReady}>
117
137
  <Sparkles className="h-3 w-3" />
@@ -121,8 +141,8 @@ export function SchemaCopyMenu({ schema, endpoints, baseUrl, variant = 'button'
121
141
  )}
122
142
  </DropdownMenuTrigger>
123
143
  <DropdownMenuContent
124
- side="right"
125
- align="start"
144
+ side={resolvedSide}
145
+ align={resolvedAlign}
126
146
  sideOffset={6}
127
147
  collisionPadding={8}
128
148
  className="w-60 max-w-[calc(100vw-16px)]"
@@ -1,48 +1,33 @@
1
1
  'use client';
2
2
 
3
- import type { ApiEndpoint, OpenApiInfo, OpenApiSchema } from '../../../types';
4
- import { SchemaCopyMenu } from '../SchemaCopyMenu';
3
+ import type { OpenApiInfo } from '../../../types';
5
4
 
6
5
  interface BrandHeaderProps {
7
6
  info: OpenApiInfo | null;
8
- /** Used only by ``SchemaCopyMenu`` — displayed label comes from ``info``. */
9
- endpoints: ApiEndpoint[];
10
- rawSchema?: OpenApiSchema | null;
11
- resolvedBaseUrl?: string;
12
7
  }
13
8
 
14
9
  /** Topmost row of the sidebar: API title on the left, tiny version
15
- * tag below it, and the Copy-for-AI menu on the right. The version
16
- * used to sit inline with the title and fought it for space on narrow
17
- * panels stacking them vertically keeps the title at a readable
18
- * size and the version as quiet metadata. */
19
- export function BrandHeader({ info, endpoints, rawSchema, resolvedBaseUrl }: BrandHeaderProps) {
10
+ * tag below it. The Copy-for-AI menu used to live here as a cramped
11
+ * icon button; it now sits as a full-width secondary CTA in the
12
+ * sidebar footer where it's actually visible. */
13
+ export function BrandHeader({ info }: BrandHeaderProps) {
20
14
  const apiTitle = info?.title ?? 'API Reference';
21
- const copyReady = rawSchema !== null && rawSchema !== undefined && endpoints.length > 0;
15
+ const versionLabel = info?.version ? `v${info.version}` : null;
16
+ const versionNode = versionLabel ? (
17
+ <div className="font-mono text-[10px] text-muted-foreground/60 leading-tight mt-0.5">
18
+ {versionLabel}
19
+ </div>
20
+ ) : null;
22
21
 
23
22
  return (
24
- <div className="shrink-0 border-b px-3 py-2.5 flex items-start gap-2">
25
- <div className="flex-1 min-w-0">
26
- <div
27
- className="text-[13px] font-semibold text-foreground leading-tight truncate"
28
- title={apiTitle}
29
- >
30
- {apiTitle}
31
- </div>
32
- {info?.version && (
33
- <div className="font-mono text-[10px] text-muted-foreground/60 leading-tight mt-0.5">
34
- v{info.version}
35
- </div>
36
- )}
23
+ <div className="shrink-0 border-b px-3 py-2.5">
24
+ <div
25
+ className="text-[13px] font-semibold text-foreground leading-tight truncate"
26
+ title={apiTitle}
27
+ >
28
+ {apiTitle}
37
29
  </div>
38
- {copyReady && (
39
- <SchemaCopyMenu
40
- schema={rawSchema ?? null}
41
- endpoints={endpoints}
42
- baseUrl={resolvedBaseUrl}
43
- variant="icon"
44
- />
45
- )}
30
+ {versionNode}
46
31
  </div>
47
32
  );
48
33
  }
@@ -4,10 +4,9 @@ import React from 'react';
4
4
 
5
5
  import { Combobox } from '@djangocfg/ui-core/components';
6
6
 
7
- import type { SchemaSource } from '../../../types';
8
- import { MethodChips } from './MethodChips';
7
+ import type { ApiEndpoint, OpenApiSchema, SchemaSource } from '../../../types';
8
+ import { SchemaCopyMenu } from '../SchemaCopyMenu';
9
9
  import { SearchInput } from './SearchInput';
10
- import type { MethodFilter } from './types';
11
10
 
12
11
  interface ToolbarProps {
13
12
  schemas: SchemaSource[];
@@ -18,17 +17,21 @@ interface ToolbarProps {
18
17
  search: string;
19
18
  onSearchChange: (v: string) => void;
20
19
 
21
- methodFilter: MethodFilter;
22
- onMethodFilterChange: (v: MethodFilter) => void;
20
+ /** Active-schema endpoints + raw document — drive the Copy-for-AI
21
+ * CTA that sits under the search input. ``null``/empty disables it. */
22
+ endpoints: ApiEndpoint[];
23
+ rawSchema?: OpenApiSchema | null;
24
+ resolvedBaseUrl?: string;
23
25
  }
24
26
 
25
27
  /** Filter / control panel of the sidebar. Groups the schema selector,
26
- * search box, and HTTP method chips into a single visually cohesive
27
- * block so they read as "one toolbar" rather than three separate
28
- * affordances stacked on top of each other.
28
+ * search box, and the Copy-for-AI CTA into a single visually cohesive
29
+ * block so they read as "one toolbar" rather than separate affordances
30
+ * stacked on top of each other.
29
31
  *
30
- * Each row is optional in the sense that we only render the schema
31
- * selector when there are multiple schemas to choose between. */
32
+ * The Copy CTA sits directly under the search input it's a quiet
33
+ * secondary action, full-width, and previously hid behind a tiny
34
+ * icon in the brand row where users couldn't see it. */
32
35
  export function Toolbar({
33
36
  schemas,
34
37
  currentSchemaId,
@@ -36,29 +39,42 @@ export function Toolbar({
36
39
  showSchemaSelector,
37
40
  search,
38
41
  onSearchChange,
39
- methodFilter,
40
- onMethodFilterChange,
42
+ endpoints,
43
+ rawSchema,
44
+ resolvedBaseUrl,
41
45
  }: ToolbarProps) {
42
46
  const schemaOptions = React.useMemo(
43
47
  () => schemas.map((s) => ({ value: s.id, label: s.name })),
44
48
  [schemas],
45
49
  );
46
50
 
51
+ const copyReady = rawSchema !== null && rawSchema !== undefined && endpoints.length > 0;
52
+ const schemaSelectorNode = showSchemaSelector ? (
53
+ <Combobox
54
+ options={schemaOptions}
55
+ value={currentSchemaId ?? ''}
56
+ onValueChange={(id) => id && onSchemaChange(id)}
57
+ placeholder="Select API"
58
+ searchPlaceholder="Search APIs…"
59
+ emptyText="No APIs found"
60
+ className="w-full h-8 text-xs"
61
+ />
62
+ ) : null;
63
+ const copyMenuNode = copyReady ? (
64
+ <SchemaCopyMenu
65
+ schema={rawSchema ?? null}
66
+ endpoints={endpoints}
67
+ baseUrl={resolvedBaseUrl}
68
+ variant="footer"
69
+ side="bottom"
70
+ />
71
+ ) : null;
72
+
47
73
  return (
48
74
  <div className="shrink-0 border-b px-3 py-2.5 space-y-2">
49
- {showSchemaSelector && (
50
- <Combobox
51
- options={schemaOptions}
52
- value={currentSchemaId ?? ''}
53
- onValueChange={(id) => id && onSchemaChange(id)}
54
- placeholder="Select API"
55
- searchPlaceholder="Search APIs…"
56
- emptyText="No APIs found"
57
- className="w-full h-8 text-xs"
58
- />
59
- )}
75
+ {schemaSelectorNode}
60
76
  <SearchInput value={search} onChange={onSearchChange} />
61
- <MethodChips value={methodFilter} onChange={onMethodFilterChange} />
77
+ {copyMenuNode}
62
78
  </div>
63
79
  );
64
80
  }
@@ -8,7 +8,7 @@ import { BrandHeader } from './BrandHeader';
8
8
  import { buildFlatVM, buildSectionsVM } from './buildVM';
9
9
  import { SidebarBody } from './SidebarBody';
10
10
  import { Toolbar } from './Toolbar';
11
- import type { MethodFilter, SidebarBodyVM } from './types';
11
+ import type { SidebarBodyVM } from './types';
12
12
  import { useDebouncedValue } from './useDebouncedValue';
13
13
 
14
14
  export interface DocsSidebarProps {
@@ -28,7 +28,7 @@ export interface DocsSidebarProps {
28
28
  * schema id. The sidebar renders them as two-level sections. */
29
29
  endpointsBySchema?: Record<string, ApiEndpoint[]>;
30
30
  /** Raw active schema + resolved base URL — used by the Copy-for-AI
31
- * dropdown in the brand row. ``null`` disables the button. */
31
+ * CTA in the toolbar. ``null`` hides the button. */
32
32
  rawSchema?: OpenApiSchema | null;
33
33
  resolvedBaseUrl?: string;
34
34
  }
@@ -51,7 +51,6 @@ export function DocsSidebar({
51
51
  resolvedBaseUrl,
52
52
  }: DocsSidebarProps) {
53
53
  const [search, setSearch] = useState('');
54
- const [methodFilter, setMethodFilter] = useState<MethodFilter>('ALL');
55
54
  const debouncedSearch = useDebouncedValue(search);
56
55
 
57
56
  const body = useMemo<SidebarBodyVM>(() => {
@@ -61,7 +60,7 @@ export function DocsSidebar({
61
60
  endpointsBySchema ?? {},
62
61
  selectedVersion,
63
62
  debouncedSearch,
64
- methodFilter,
63
+ 'ALL',
65
64
  activeEndpointId,
66
65
  );
67
66
  }
@@ -69,7 +68,7 @@ export function DocsSidebar({
69
68
  endpoints,
70
69
  selectedVersion,
71
70
  debouncedSearch,
72
- methodFilter,
71
+ 'ALL',
73
72
  activeEndpointId,
74
73
  );
75
74
  }, [
@@ -79,7 +78,6 @@ export function DocsSidebar({
79
78
  endpoints,
80
79
  selectedVersion,
81
80
  debouncedSearch,
82
- methodFilter,
83
81
  activeEndpointId,
84
82
  ]);
85
83
 
@@ -88,12 +86,7 @@ export function DocsSidebar({
88
86
 
89
87
  return (
90
88
  <aside className="flex flex-col h-full min-h-0 border-r bg-muted/10">
91
- <BrandHeader
92
- info={info}
93
- endpoints={endpoints}
94
- rawSchema={rawSchema}
95
- resolvedBaseUrl={resolvedBaseUrl}
96
- />
89
+ <BrandHeader info={info} />
97
90
  <Toolbar
98
91
  schemas={schemas}
99
92
  currentSchemaId={currentSchemaId}
@@ -101,8 +94,9 @@ export function DocsSidebar({
101
94
  showSchemaSelector={showSchemaSelector}
102
95
  search={search}
103
96
  onSearchChange={setSearch}
104
- methodFilter={methodFilter}
105
- onMethodFilterChange={setMethodFilter}
97
+ endpoints={endpoints}
98
+ rawSchema={rawSchema}
99
+ resolvedBaseUrl={resolvedBaseUrl}
106
100
  />
107
101
  <ScrollArea>
108
102
  <SidebarBody body={body} onNavigate={onNavigate} />
@@ -1,4 +1,5 @@
1
1
  import type { ApiEndpoint } from '../../types';
2
+ import { relativePath } from '../../utils/url';
2
3
 
3
4
  /**
4
5
  * Given a list of full endpoint paths, return the longest ``/``-aligned
@@ -50,10 +51,6 @@ export function sidebarLabel(ep: ApiEndpoint, groupCommonPrefix: string): string
50
51
  return relativePath(ep.path);
51
52
  }
52
53
 
53
- function relativePath(full: string): string {
54
- try { return new URL(full).pathname; } catch { return full; }
55
- }
56
-
57
54
  /** Tooltip text: always the definitive ``METHOD relative/path``. */
58
55
  export function sidebarTooltip(ep: ApiEndpoint): string {
59
56
  return `${ep.method} ${relativePath(ep.path)}`;
@@ -64,7 +64,8 @@ function buildUrl(
64
64
 
65
65
  let path = endpoint.path;
66
66
  for (const name of pathParamNames) {
67
- const value = parameters[name] ?? `{${name}}`;
67
+ const value = parameters[name];
68
+ if (value === undefined || value === '') continue;
68
69
  path = path.replaceAll(`{${name}}`, encodeURIComponent(value));
69
70
  }
70
71
 
@@ -118,9 +118,16 @@ export function resolveAbsolute(url: string): string {
118
118
  return url;
119
119
  }
120
120
 
121
- /** Pull just the path out of any URL (absolute or relative). */
121
+ /** Pull just the path out of any URL (absolute or relative). The
122
+ * ``URL`` parser percent-encodes path segments, which turns OpenAPI
123
+ * template braces (``{id}``) into ``%7Bid%7D``. We restore those so
124
+ * unfilled templates render as the readable form authors wrote. */
122
125
  export function relativePath(url: string): string {
123
- try { return new URL(url).pathname; } catch { return url; }
126
+ try {
127
+ return new URL(url).pathname.replace(/%7B/gi, '{').replace(/%7D/gi, '}');
128
+ } catch {
129
+ return url;
130
+ }
124
131
  }
125
132
 
126
133
  /**