@djangocfg/ui-tools 2.1.287 → 2.1.290
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.
- package/README.md +14 -3
- package/dist/DocsLayout-IKH7BLSU.cjs +3464 -0
- package/dist/DocsLayout-IKH7BLSU.cjs.map +1 -0
- package/dist/DocsLayout-JPXFUKAR.mjs +3457 -0
- package/dist/DocsLayout-JPXFUKAR.mjs.map +1 -0
- package/dist/{PrettyCode.client-5GABIN2I.cjs → PrettyCode.client-RPDIE5CH.cjs} +104 -3
- package/dist/PrettyCode.client-RPDIE5CH.cjs.map +1 -0
- package/dist/{PrettyCode.client-IZTXXYHG.mjs → PrettyCode.client-SPMTQEG4.mjs} +106 -5
- package/dist/PrettyCode.client-SPMTQEG4.mjs.map +1 -0
- package/dist/{chunk-IULI4XII.cjs → chunk-5Q4UMSWB.cjs} +355 -9
- package/dist/chunk-5Q4UMSWB.cjs.map +1 -0
- package/dist/{chunk-VZGQC3NG.mjs → chunk-EFWOJPA6.mjs} +349 -9
- package/dist/chunk-EFWOJPA6.mjs.map +1 -0
- package/dist/index.cjs +10 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.mjs +5 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +21 -14
- package/src/components/markdown/MarkdownMessage.tsx +46 -0
- package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +93 -157
- package/src/tools/OpenapiViewer/README.md +114 -6
- package/src/tools/OpenapiViewer/components/DocsLayout/ApiIntroSection.tsx +20 -6
- package/src/tools/OpenapiViewer/components/DocsLayout/DocsView.tsx +331 -53
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/LanguageTabs.tsx +36 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/index.tsx +56 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/useCodeSnippet.ts +77 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +146 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MethodBadge.tsx +6 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/PathDisplay.tsx +26 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/index.tsx +87 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamGroup.tsx +30 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamRow.tsx +36 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/index.tsx +22 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/RequestBody/index.tsx +33 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +76 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseRow.tsx +80 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +32 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/index.tsx +21 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +106 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +127 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/index.tsx +31 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/types.ts +28 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +87 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/defaults.ts +27 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/index.tsx +45 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/context.tsx +56 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/hooks/useSectionHash.ts +63 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +96 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/index.ts +133 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/selectors.ts +40 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/types.ts +17 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +40 -11
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/BrandHeader.tsx +48 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/CategoryBlock.tsx +33 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/EndpointRow.tsx +73 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/MethodChips.tsx +43 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SchemaSection.tsx +27 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SearchInput.tsx +45 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SidebarBody.tsx +50 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/Toolbar.tsx +64 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/buildVM.ts +126 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/index.tsx +112 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/types.ts +42 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/useDebouncedValue.ts +14 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/SlideInPlayground.tsx +10 -7
- package/src/tools/OpenapiViewer/components/DocsLayout/TryItSheet.tsx +9 -6
- package/src/tools/OpenapiViewer/components/DocsLayout/anchor.ts +19 -2
- package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +38 -21
- package/src/tools/OpenapiViewer/components/DocsLayout/index.tsx +168 -50
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +55 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PreviewView.tsx +115 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/RawView.tsx +24 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/StatusBar.tsx +63 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/ViewTabs.tsx +45 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/detectContent.ts +97 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/index.tsx +93 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/types.ts +26 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/useResponseView.ts +62 -0
- package/src/tools/OpenapiViewer/hooks/index.ts +3 -1
- package/src/tools/OpenapiViewer/hooks/useDocsUrlSync.ts +119 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +164 -74
- package/src/tools/OpenapiViewer/types.ts +46 -1
- package/src/tools/OpenapiViewer/utils/codeSamples.ts +287 -0
- package/src/tools/OpenapiViewer/utils/index.ts +3 -0
- package/src/tools/OpenapiViewer/utils/operationToHar.ts +119 -0
- package/src/tools/OpenapiViewer/utils/sampler.ts +72 -0
- package/src/tools/OpenapiViewer/utils/scrollParent.ts +68 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +88 -1
- package/src/tools/PrettyCode/PrettyCode.story.tsx +114 -361
- package/src/tools/PrettyCode/index.tsx +13 -0
- package/src/tools/PrettyCode/lazy.tsx +5 -0
- package/src/tools/PrettyCode/registerPrismLanguages.ts +111 -0
- package/dist/DocsLayout-BCVU6TTX.cjs +0 -2027
- package/dist/DocsLayout-BCVU6TTX.cjs.map +0 -1
- package/dist/DocsLayout-ERETJLLV.mjs +0 -2020
- package/dist/DocsLayout-ERETJLLV.mjs.map +0 -1
- package/dist/PrettyCode.client-5GABIN2I.cjs.map +0 -1
- package/dist/PrettyCode.client-IZTXXYHG.mjs.map +0 -1
- package/dist/chunk-IULI4XII.cjs.map +0 -1
- package/dist/chunk-VZGQC3NG.mjs.map +0 -1
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc.tsx +0 -268
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar.tsx +0 -211
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel.tsx +0 -127
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { create } from 'zustand';
|
|
4
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
5
|
+
|
|
6
|
+
import type { CodeSampleTargetId } from '../../../../utils/codeSamples';
|
|
7
|
+
import type { SectionId } from '../types';
|
|
8
|
+
|
|
9
|
+
/** Composite key so the same section id (``responses``) can have
|
|
10
|
+
* distinct state per endpoint on the page. We don't use Map because
|
|
11
|
+
* zustand+persist serialises state to JSON and Map survival there
|
|
12
|
+
* requires extra replacer/reviver boilerplate for no real gain. */
|
|
13
|
+
export const sectionKey = (endpointId: string, sectionId: SectionId): string =>
|
|
14
|
+
`${endpointId}:${sectionId}`;
|
|
15
|
+
|
|
16
|
+
export interface EndpointDocState {
|
|
17
|
+
/** ``${endpointId}:${sectionId}`` → open? — ``undefined`` means
|
|
18
|
+
* "use the component's ``defaultOpen``"; the store only stores
|
|
19
|
+
* user-driven overrides so defaults can change later without
|
|
20
|
+
* stale persisted state overriding them. */
|
|
21
|
+
openSections: Record<string, boolean>;
|
|
22
|
+
/** Active code-sample tab per endpoint. Keyed on endpoint id only;
|
|
23
|
+
* users tend to pick one language and stick with it across the
|
|
24
|
+
* whole page, so sharing between endpoints is acceptable UX. */
|
|
25
|
+
activeCodeTab: Record<string, CodeSampleTargetId>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface EndpointDocActions {
|
|
29
|
+
toggleSection: (endpointId: string, sectionId: SectionId) => void;
|
|
30
|
+
setSectionOpen: (endpointId: string, sectionId: SectionId, open: boolean) => void;
|
|
31
|
+
setCodeTab: (endpointId: string, tab: CodeSampleTargetId) => void;
|
|
32
|
+
/** Bulk ops — "expand all" / "collapse all" on a single endpoint.
|
|
33
|
+
* The component calls these from an action button in the header. */
|
|
34
|
+
expandAll: (endpointId: string, sectionIds: readonly SectionId[]) => void;
|
|
35
|
+
collapseAll: (endpointId: string, sectionIds: readonly SectionId[]) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type EndpointDocStore = EndpointDocState & EndpointDocActions;
|
|
39
|
+
|
|
40
|
+
const initialState: EndpointDocState = {
|
|
41
|
+
openSections: {},
|
|
42
|
+
activeCodeTab: {},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Zustand store with sessionStorage persistence. Using sessionStorage
|
|
46
|
+
* (not localStorage) so closing the tab clears state — the viewer is
|
|
47
|
+
* often embedded in a dashboard and persisting forever would surprise
|
|
48
|
+
* users who switched schemas. */
|
|
49
|
+
export const useEndpointDocStore = create<EndpointDocStore>()(
|
|
50
|
+
persist(
|
|
51
|
+
(set) => ({
|
|
52
|
+
...initialState,
|
|
53
|
+
|
|
54
|
+
toggleSection: (endpointId, sectionId) =>
|
|
55
|
+
set((state) => {
|
|
56
|
+
const key = sectionKey(endpointId, sectionId);
|
|
57
|
+
const current = state.openSections[key];
|
|
58
|
+
return {
|
|
59
|
+
openSections: {
|
|
60
|
+
...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,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}),
|
|
73
|
+
|
|
74
|
+
setSectionOpen: (endpointId, sectionId, open) =>
|
|
75
|
+
set((state) => ({
|
|
76
|
+
openSections: {
|
|
77
|
+
...state.openSections,
|
|
78
|
+
[sectionKey(endpointId, sectionId)]: open,
|
|
79
|
+
},
|
|
80
|
+
})),
|
|
81
|
+
|
|
82
|
+
setCodeTab: (endpointId, tab) =>
|
|
83
|
+
set((state) => ({
|
|
84
|
+
activeCodeTab: {
|
|
85
|
+
...state.activeCodeTab,
|
|
86
|
+
[endpointId]: tab,
|
|
87
|
+
},
|
|
88
|
+
})),
|
|
89
|
+
|
|
90
|
+
expandAll: (endpointId, sectionIds) =>
|
|
91
|
+
set((state) => {
|
|
92
|
+
const next = { ...state.openSections };
|
|
93
|
+
for (const sid of sectionIds) {
|
|
94
|
+
next[sectionKey(endpointId, sid)] = true;
|
|
95
|
+
}
|
|
96
|
+
return { openSections: next };
|
|
97
|
+
}),
|
|
98
|
+
|
|
99
|
+
collapseAll: (endpointId, sectionIds) =>
|
|
100
|
+
set((state) => {
|
|
101
|
+
const next = { ...state.openSections };
|
|
102
|
+
for (const sid of sectionIds) {
|
|
103
|
+
next[sectionKey(endpointId, sid)] = false;
|
|
104
|
+
}
|
|
105
|
+
return { openSections: next };
|
|
106
|
+
}),
|
|
107
|
+
}),
|
|
108
|
+
{
|
|
109
|
+
name: 'openapi-viewer:endpoint-doc',
|
|
110
|
+
storage: createJSONStorage(() => {
|
|
111
|
+
// Guard for SSR / non-browser environments — zustand's
|
|
112
|
+
// persist middleware calls storage.getItem synchronously
|
|
113
|
+
// during hydration, and ``sessionStorage`` is undefined
|
|
114
|
+
// there. Returning a noop keeps SSR snapshots stable.
|
|
115
|
+
if (typeof window === 'undefined') {
|
|
116
|
+
return {
|
|
117
|
+
getItem: () => null,
|
|
118
|
+
setItem: () => {},
|
|
119
|
+
removeItem: () => {},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return window.sessionStorage;
|
|
123
|
+
}),
|
|
124
|
+
// Only persist user overrides, not the functions. Zustand
|
|
125
|
+
// serialises everything by default and logs a warning on
|
|
126
|
+
// non-serialisable values; partialize keeps the payload lean.
|
|
127
|
+
partialize: (state) => ({
|
|
128
|
+
openSections: state.openSections,
|
|
129
|
+
activeCodeTab: state.activeCodeTab,
|
|
130
|
+
}),
|
|
131
|
+
},
|
|
132
|
+
),
|
|
133
|
+
);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { CodeSampleTargetId } from '../../../../utils/codeSamples';
|
|
4
|
+
import type { SectionId } from '../types';
|
|
5
|
+
import { sectionKey, useEndpointDocStore } from './index';
|
|
6
|
+
|
|
7
|
+
/** Read whether a section is open, falling back to ``defaultOpen`` when
|
|
8
|
+
* the user hasn't explicitly toggled it. Returning a raw boolean makes
|
|
9
|
+
* the caller's conditional rendering trivial. */
|
|
10
|
+
export function useIsSectionOpen(
|
|
11
|
+
endpointId: string,
|
|
12
|
+
sectionId: SectionId,
|
|
13
|
+
defaultOpen: boolean,
|
|
14
|
+
): boolean {
|
|
15
|
+
return useEndpointDocStore((s) => {
|
|
16
|
+
const explicit = s.openSections[sectionKey(endpointId, sectionId)];
|
|
17
|
+
return explicit === undefined ? defaultOpen : explicit;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Read the active code-sample tab for this endpoint, falling back to
|
|
22
|
+
* a shared default (``curl``). Falls back at read time rather than at
|
|
23
|
+
* write time so changing the default later doesn't require store
|
|
24
|
+
* migration. */
|
|
25
|
+
export function useActiveCodeTab(
|
|
26
|
+
endpointId: string,
|
|
27
|
+
fallback: CodeSampleTargetId = 'curl',
|
|
28
|
+
): CodeSampleTargetId {
|
|
29
|
+
return useEndpointDocStore((s) => s.activeCodeTab[endpointId] ?? fallback);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useEndpointDocActions() {
|
|
33
|
+
return useEndpointDocStore((s) => ({
|
|
34
|
+
toggleSection: s.toggleSection,
|
|
35
|
+
setSectionOpen: s.setSectionOpen,
|
|
36
|
+
setCodeTab: s.setCodeTab,
|
|
37
|
+
expandAll: s.expandAll,
|
|
38
|
+
collapseAll: s.collapseAll,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local types for the EndpointDoc folder. Wider OpenAPI types live in
|
|
3
|
+
* ``../../../types``; this module only declares what's specific to the
|
|
4
|
+
* docs-view rendering (section identifiers, tab ids, etc.).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Sections a single endpoint card can contain. Section identity is
|
|
8
|
+
* stable because zustand stores open/close state keyed by it, and the
|
|
9
|
+
* URL hash router references sections by the same id. */
|
|
10
|
+
export type SectionId = 'parameters' | 'requestBody' | 'responses' | 'codeSamples';
|
|
11
|
+
|
|
12
|
+
export const ALL_SECTION_IDS: readonly SectionId[] = [
|
|
13
|
+
'parameters',
|
|
14
|
+
'requestBody',
|
|
15
|
+
'responses',
|
|
16
|
+
'codeSamples',
|
|
17
|
+
] as const;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
DropdownMenuSeparator,
|
|
13
13
|
DropdownMenuTrigger,
|
|
14
14
|
} from '@djangocfg/ui-core/components';
|
|
15
|
+
import { toast } from '@djangocfg/ui-core/hooks';
|
|
15
16
|
|
|
16
17
|
import type { ApiEndpoint, OpenApiSchema } from '../../types';
|
|
17
18
|
import {
|
|
@@ -44,6 +45,11 @@ interface SchemaCopyMenuProps {
|
|
|
44
45
|
/** Resolved base URL that gets embedded into the copy so the AI
|
|
45
46
|
* receives working URLs, not the ones originally in ``schema.servers``. */
|
|
46
47
|
baseUrl?: string;
|
|
48
|
+
/** Trigger appearance.
|
|
49
|
+
* - ``button`` (default) — labelled pill with icon + chevron.
|
|
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';
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
/**
|
|
@@ -52,7 +58,7 @@ interface SchemaCopyMenuProps {
|
|
|
52
58
|
* dereferencing + stringifying a large schema can be non-trivial — sizes
|
|
53
59
|
* are displayed after the first successful copy, via a tiny cache.
|
|
54
60
|
*/
|
|
55
|
-
export function SchemaCopyMenu({ schema, endpoints, baseUrl }: SchemaCopyMenuProps) {
|
|
61
|
+
export function SchemaCopyMenu({ schema, endpoints, baseUrl, variant = 'button' }: SchemaCopyMenuProps) {
|
|
56
62
|
const [sizeCache, setSizeCache] = useState<Partial<Record<Flavour, string>>>({});
|
|
57
63
|
const [justCopied, setJustCopied] = useState<Flavour | null>(null);
|
|
58
64
|
const [open, setOpen] = useState(false);
|
|
@@ -73,14 +79,18 @@ export function SchemaCopyMenu({ schema, endpoints, baseUrl }: SchemaCopyMenuPro
|
|
|
73
79
|
async (flavour: Flavour) => {
|
|
74
80
|
if (!isReady) return;
|
|
75
81
|
const text = build(flavour);
|
|
82
|
+
const label = FLAVOUR_LABELS[flavour].title;
|
|
76
83
|
try {
|
|
77
84
|
await navigator.clipboard.writeText(text);
|
|
78
|
-
|
|
85
|
+
const size = formatBytes(text);
|
|
86
|
+
setSizeCache((prev) => ({ ...prev, [flavour]: size }));
|
|
79
87
|
setJustCopied(flavour);
|
|
80
88
|
setTimeout(() => setJustCopied(null), 1500);
|
|
81
89
|
setOpen(false);
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
toast.success(`Copied ${label}`, { description: size });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
const message = err instanceof Error ? err.message : 'Clipboard permission denied';
|
|
93
|
+
toast.error('Copy failed', { description: message });
|
|
84
94
|
}
|
|
85
95
|
},
|
|
86
96
|
[build, isReady],
|
|
@@ -91,13 +101,32 @@ export function SchemaCopyMenu({ schema, endpoints, baseUrl }: SchemaCopyMenuPro
|
|
|
91
101
|
return (
|
|
92
102
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
|
93
103
|
<DropdownMenuTrigger asChild>
|
|
94
|
-
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
{variant === 'icon' ? (
|
|
105
|
+
<Button
|
|
106
|
+
variant="ghost"
|
|
107
|
+
size="icon"
|
|
108
|
+
className="h-7 w-7 shrink-0"
|
|
109
|
+
disabled={!isReady}
|
|
110
|
+
title="Copy schema for AI"
|
|
111
|
+
aria-label="Copy schema for AI"
|
|
112
|
+
>
|
|
113
|
+
<Sparkles className="h-3.5 w-3.5" />
|
|
114
|
+
</Button>
|
|
115
|
+
) : (
|
|
116
|
+
<Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs" disabled={!isReady}>
|
|
117
|
+
<Sparkles className="h-3 w-3" />
|
|
118
|
+
Copy for AI
|
|
119
|
+
<ChevronDown className="h-3 w-3 opacity-60" />
|
|
120
|
+
</Button>
|
|
121
|
+
)}
|
|
99
122
|
</DropdownMenuTrigger>
|
|
100
|
-
<DropdownMenuContent
|
|
123
|
+
<DropdownMenuContent
|
|
124
|
+
side="right"
|
|
125
|
+
align="start"
|
|
126
|
+
sideOffset={6}
|
|
127
|
+
collisionPadding={8}
|
|
128
|
+
className="w-60 max-w-[calc(100vw-16px)]"
|
|
129
|
+
>
|
|
101
130
|
<DropdownMenuLabel className="text-[10px] uppercase tracking-wider text-muted-foreground/70">
|
|
102
131
|
Copy schema
|
|
103
132
|
</DropdownMenuLabel>
|
|
@@ -127,7 +156,7 @@ export function SchemaCopyMenu({ schema, endpoints, baseUrl }: SchemaCopyMenuPro
|
|
|
127
156
|
</span>
|
|
128
157
|
) : null}
|
|
129
158
|
</div>
|
|
130
|
-
<span className="text-[10px] text-muted-foreground/70 leading-snug">
|
|
159
|
+
<span className="text-[10px] text-muted-foreground/70 leading-snug line-clamp-1">
|
|
131
160
|
{label.hint}
|
|
132
161
|
</span>
|
|
133
162
|
</DropdownMenuItem>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ApiEndpoint, OpenApiInfo, OpenApiSchema } from '../../../types';
|
|
4
|
+
import { SchemaCopyMenu } from '../SchemaCopyMenu';
|
|
5
|
+
|
|
6
|
+
interface BrandHeaderProps {
|
|
7
|
+
info: OpenApiInfo | null;
|
|
8
|
+
/** Used only by ``SchemaCopyMenu`` — displayed label comes from ``info``. */
|
|
9
|
+
endpoints: ApiEndpoint[];
|
|
10
|
+
rawSchema?: OpenApiSchema | null;
|
|
11
|
+
resolvedBaseUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 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) {
|
|
20
|
+
const apiTitle = info?.title ?? 'API Reference';
|
|
21
|
+
const copyReady = rawSchema !== null && rawSchema !== undefined && endpoints.length > 0;
|
|
22
|
+
|
|
23
|
+
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
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
{copyReady && (
|
|
39
|
+
<SchemaCopyMenu
|
|
40
|
+
schema={rawSchema ?? null}
|
|
41
|
+
endpoints={endpoints}
|
|
42
|
+
baseUrl={resolvedBaseUrl}
|
|
43
|
+
variant="icon"
|
|
44
|
+
/>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { EndpointRow } from './EndpointRow';
|
|
6
|
+
import type { CategoryVM, NavigateFn } from './types';
|
|
7
|
+
|
|
8
|
+
interface CategoryBlockProps {
|
|
9
|
+
category: CategoryVM;
|
|
10
|
+
onNavigate: NavigateFn;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A labelled group of endpoint rows. The category header is
|
|
14
|
+
* deliberately tiny (text-[10px] tracking-wider) — it's a divider, not
|
|
15
|
+
* a call to attention. Users who are reading the list want to see the
|
|
16
|
+
* endpoint names, not the category prose. */
|
|
17
|
+
export const CategoryBlock = React.memo(function CategoryBlock({
|
|
18
|
+
category,
|
|
19
|
+
onNavigate,
|
|
20
|
+
}: CategoryBlockProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="mb-2.5 last:mb-1">
|
|
23
|
+
<div className="px-3 pt-3 pb-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/50 select-none">
|
|
24
|
+
{category.category}
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
{category.rows.map((row) => (
|
|
28
|
+
<EndpointRow key={row.key} row={row} onNavigate={onNavigate} />
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@djangocfg/ui-core/components';
|
|
6
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
7
|
+
|
|
8
|
+
import { MethodBadge } from '../../shared/ui';
|
|
9
|
+
import type { EndpointRowVM, NavigateFn } from './types';
|
|
10
|
+
|
|
11
|
+
interface EndpointRowProps {
|
|
12
|
+
row: EndpointRowVM;
|
|
13
|
+
onNavigate: NavigateFn;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** One endpoint in the sidebar list. Layout decisions worth calling out:
|
|
17
|
+
* - Fixed 52px badge column via CSS grid so ``GET`` / ``POST`` /
|
|
18
|
+
* ``PATCH`` all take the same horizontal slot and labels align
|
|
19
|
+
* vertically on the left edge. Without this the list looks ragged
|
|
20
|
+
* because ``POST`` is wider than ``GET`` and each label starts at
|
|
21
|
+
* a different x offset.
|
|
22
|
+
* - ``items-baseline`` so the method badge sits on the same visual
|
|
23
|
+
* line as the label text, not in its vertical centre.
|
|
24
|
+
* - ``py-1`` (not the old ``py-1.5``) for higher list density.
|
|
25
|
+
* - Trailing full-stops are trimmed from the label. OpenAPI
|
|
26
|
+
* summaries often end in ``.`` which looks like noise in a list.
|
|
27
|
+
* - Active state is a single left-edge accent bar + soft tint — no
|
|
28
|
+
* big filled background so the method badge still carries the
|
|
29
|
+
* colour semantics of the row. */
|
|
30
|
+
export const EndpointRow = React.memo(function EndpointRow({
|
|
31
|
+
row,
|
|
32
|
+
onNavigate,
|
|
33
|
+
}: EndpointRowProps) {
|
|
34
|
+
// Strip a trailing full-stop — OpenAPI ``summary`` routinely ends
|
|
35
|
+
// in ``.`` which looks like punctuation noise when stacked in a list.
|
|
36
|
+
const displayLabel = row.label.replace(/\.$/, '');
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Tooltip delayDuration={350}>
|
|
40
|
+
<TooltipTrigger asChild>
|
|
41
|
+
<button
|
|
42
|
+
onClick={() => onNavigate(row.anchor, row.schemaId)}
|
|
43
|
+
aria-current={row.isActive ? 'location' : undefined}
|
|
44
|
+
className={cn(
|
|
45
|
+
'relative w-full text-left grid grid-cols-[52px_minmax(0,1fr)] items-baseline gap-2 pl-3 pr-3 py-1 transition-colors',
|
|
46
|
+
row.isActive
|
|
47
|
+
? 'bg-primary/10 text-foreground'
|
|
48
|
+
: 'hover:bg-muted/40 text-foreground/75 hover:text-foreground',
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
{row.isActive && (
|
|
52
|
+
<span className="absolute left-0 top-1 bottom-1 w-0.5 rounded-r bg-primary" />
|
|
53
|
+
)}
|
|
54
|
+
<span className="justify-self-start">
|
|
55
|
+
<MethodBadge method={row.method} />
|
|
56
|
+
</span>
|
|
57
|
+
<span
|
|
58
|
+
className={cn(
|
|
59
|
+
'line-clamp-2 leading-snug min-w-0',
|
|
60
|
+
row.useMono ? 'font-mono text-[11px] break-all' : 'text-[12px]',
|
|
61
|
+
row.isActive && 'text-foreground font-medium',
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
{displayLabel}
|
|
65
|
+
</span>
|
|
66
|
+
</button>
|
|
67
|
+
</TooltipTrigger>
|
|
68
|
+
<TooltipContent side="right" align="center" className="font-mono text-[11px]">
|
|
69
|
+
{row.tooltip}
|
|
70
|
+
</TooltipContent>
|
|
71
|
+
</Tooltip>
|
|
72
|
+
);
|
|
73
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
4
|
+
|
|
5
|
+
import { METHOD_FILTERS, type MethodFilter } from './types';
|
|
6
|
+
|
|
7
|
+
interface MethodChipsProps {
|
|
8
|
+
value: MethodFilter;
|
|
9
|
+
onChange: (v: MethodFilter) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Horizontal filter strip. Scrolls horizontally on narrow widths
|
|
13
|
+
* rather than wrapping to a second line — the toolbar stays a fixed
|
|
14
|
+
* height so the endpoint list below gets every pixel it can.
|
|
15
|
+
*
|
|
16
|
+
* Active chips use a solid fill (not the outlined-with-tint style
|
|
17
|
+
* from the old design) so "this filter is on" reads at a glance
|
|
18
|
+
* without the eye having to resolve subtle border colour shifts. */
|
|
19
|
+
export function MethodChips({ value, onChange }: MethodChipsProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex items-center gap-1 overflow-x-auto -mx-1 px-1 pb-px">
|
|
22
|
+
{METHOD_FILTERS.map((m) => {
|
|
23
|
+
const active = value === m;
|
|
24
|
+
return (
|
|
25
|
+
<button
|
|
26
|
+
key={m}
|
|
27
|
+
type="button"
|
|
28
|
+
onClick={() => onChange(m)}
|
|
29
|
+
aria-pressed={active}
|
|
30
|
+
className={cn(
|
|
31
|
+
'shrink-0 px-2 h-6 rounded font-mono text-[10px] font-semibold tracking-wide uppercase transition-colors',
|
|
32
|
+
active
|
|
33
|
+
? 'bg-foreground text-background'
|
|
34
|
+
: 'text-muted-foreground/70 hover:text-foreground hover:bg-muted',
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
{m}
|
|
38
|
+
</button>
|
|
39
|
+
);
|
|
40
|
+
})}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CategoryBlock } from './CategoryBlock';
|
|
4
|
+
import type { NavigateFn, SchemaSectionVM } from './types';
|
|
5
|
+
|
|
6
|
+
interface SchemaSectionProps {
|
|
7
|
+
section: SchemaSectionVM;
|
|
8
|
+
onNavigate: NavigateFn;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** One schema block inside ``sections`` grouping mode. Renders a
|
|
12
|
+
* sticky header with the schema name so users can tell which API
|
|
13
|
+
* they're looking at while scrolling, then the categories below. */
|
|
14
|
+
export function SchemaSection({ section, onNavigate }: SchemaSectionProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="mb-4 last:mb-2">
|
|
17
|
+
<div className="px-3 py-1.5 sticky top-0 z-[1] bg-background/95 backdrop-blur-[2px] border-b border-border/40">
|
|
18
|
+
<span className="text-[11px] font-bold uppercase tracking-[0.12em] text-foreground/80">
|
|
19
|
+
{section.sourceName}
|
|
20
|
+
</span>
|
|
21
|
+
</div>
|
|
22
|
+
{section.categories.map((cat) => (
|
|
23
|
+
<CategoryBlock key={cat.key} category={cat} onNavigate={onNavigate} />
|
|
24
|
+
))}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Search, X } from 'lucide-react';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
import { Input } from '@djangocfg/ui-core/components';
|
|
7
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
|
+
|
|
9
|
+
interface SearchInputProps {
|
|
10
|
+
value: string;
|
|
11
|
+
onChange: (v: string) => void;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Search field with a leading icon and a trailing clear button. The
|
|
16
|
+
* clear button only appears when there's something to clear so the
|
|
17
|
+
* idle state reads as a single clean input rather than "icon + input
|
|
18
|
+
* + mystery button". */
|
|
19
|
+
export function SearchInput({ value, onChange, placeholder }: SearchInputProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="relative">
|
|
22
|
+
<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" />
|
|
23
|
+
<Input
|
|
24
|
+
placeholder={placeholder ?? 'Search endpoints…'}
|
|
25
|
+
value={value}
|
|
26
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
|
|
27
|
+
className="pl-8 pr-7 h-8 text-xs"
|
|
28
|
+
/>
|
|
29
|
+
{value && (
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
onClick={() => onChange('')}
|
|
33
|
+
aria-label="Clear search"
|
|
34
|
+
className={cn(
|
|
35
|
+
'absolute right-1.5 top-1/2 -translate-y-1/2 h-5 w-5 rounded',
|
|
36
|
+
'inline-flex items-center justify-center',
|
|
37
|
+
'text-muted-foreground/50 hover:text-foreground hover:bg-muted transition-colors',
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
<X className="h-3 w-3" />
|
|
41
|
+
</button>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CategoryBlock } from './CategoryBlock';
|
|
4
|
+
import { SchemaSection } from './SchemaSection';
|
|
5
|
+
import type { NavigateFn, SidebarBodyVM } from './types';
|
|
6
|
+
|
|
7
|
+
interface SidebarBodyProps {
|
|
8
|
+
body: SidebarBodyVM;
|
|
9
|
+
onNavigate: NavigateFn;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Scrollable body of the sidebar. Picks the rendering strategy based
|
|
13
|
+
* on ``body.kind`` — flat category list for single-schema mode,
|
|
14
|
+
* nested schema sections for multi-schema mode — and falls back to
|
|
15
|
+
* a centred empty-state message when the filter pipeline yields no
|
|
16
|
+
* rows. */
|
|
17
|
+
export function SidebarBody({ body, onNavigate }: SidebarBodyProps) {
|
|
18
|
+
if (body.kind === 'flat') {
|
|
19
|
+
if (body.categories.length === 0) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="py-10 px-4 text-center text-xs text-muted-foreground">
|
|
22
|
+
{body.emptyText}
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return (
|
|
27
|
+
<nav className="py-1.5">
|
|
28
|
+
{body.categories.map((cat) => (
|
|
29
|
+
<CategoryBlock key={cat.key} category={cat} onNavigate={onNavigate} />
|
|
30
|
+
))}
|
|
31
|
+
</nav>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (body.sections.length === 0) {
|
|
36
|
+
return (
|
|
37
|
+
<div className="py-10 px-4 text-center text-xs text-muted-foreground">
|
|
38
|
+
{body.emptyText}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<nav className="py-1.5">
|
|
45
|
+
{body.sections.map((section) => (
|
|
46
|
+
<SchemaSection key={section.sourceId} section={section} onNavigate={onNavigate} />
|
|
47
|
+
))}
|
|
48
|
+
</nav>
|
|
49
|
+
);
|
|
50
|
+
}
|