@djangocfg/ui-tools 2.1.289 → 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.
Files changed (97) hide show
  1. package/README.md +14 -3
  2. package/dist/{DocsLayout-YDR7DSMM.cjs → DocsLayout-IKH7BLSU.cjs} +1537 -682
  3. package/dist/DocsLayout-IKH7BLSU.cjs.map +1 -0
  4. package/dist/{DocsLayout-TKJQ5W5E.mjs → DocsLayout-JPXFUKAR.mjs} +1429 -574
  5. package/dist/DocsLayout-JPXFUKAR.mjs.map +1 -0
  6. package/dist/{PrettyCode.client-5GABIN2I.cjs → PrettyCode.client-RPDIE5CH.cjs} +104 -3
  7. package/dist/PrettyCode.client-RPDIE5CH.cjs.map +1 -0
  8. package/dist/{PrettyCode.client-IZTXXYHG.mjs → PrettyCode.client-SPMTQEG4.mjs} +106 -5
  9. package/dist/PrettyCode.client-SPMTQEG4.mjs.map +1 -0
  10. package/dist/{chunk-IULI4XII.cjs → chunk-5Q4UMSWB.cjs} +355 -9
  11. package/dist/chunk-5Q4UMSWB.cjs.map +1 -0
  12. package/dist/{chunk-VZGQC3NG.mjs → chunk-EFWOJPA6.mjs} +349 -9
  13. package/dist/chunk-EFWOJPA6.mjs.map +1 -0
  14. package/dist/index.cjs +10 -10
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +18 -0
  17. package/dist/index.d.ts +18 -0
  18. package/dist/index.mjs +5 -5
  19. package/dist/index.mjs.map +1 -1
  20. package/package.json +18 -14
  21. package/src/components/markdown/MarkdownMessage.tsx +46 -0
  22. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +87 -178
  23. package/src/tools/OpenapiViewer/README.md +114 -6
  24. package/src/tools/OpenapiViewer/components/DocsLayout/ApiIntroSection.tsx +20 -6
  25. package/src/tools/OpenapiViewer/components/DocsLayout/DocsView.tsx +6 -0
  26. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/LanguageTabs.tsx +36 -0
  27. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/index.tsx +56 -0
  28. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/useCodeSnippet.ts +77 -0
  29. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +146 -0
  30. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MethodBadge.tsx +6 -0
  31. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/PathDisplay.tsx +26 -0
  32. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/index.tsx +87 -0
  33. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamGroup.tsx +30 -0
  34. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamRow.tsx +36 -0
  35. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/index.tsx +22 -0
  36. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/RequestBody/index.tsx +33 -0
  37. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +76 -0
  38. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseRow.tsx +80 -0
  39. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +32 -0
  40. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/index.tsx +21 -0
  41. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +106 -0
  42. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +127 -0
  43. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/index.tsx +31 -0
  44. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/types.ts +28 -0
  45. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +87 -0
  46. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/defaults.ts +27 -0
  47. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/index.tsx +45 -0
  48. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/context.tsx +56 -0
  49. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/hooks/useSectionHash.ts +63 -0
  50. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +96 -0
  51. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/index.ts +133 -0
  52. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/selectors.ts +40 -0
  53. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/types.ts +17 -0
  54. package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +8 -2
  55. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/BrandHeader.tsx +48 -0
  56. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/CategoryBlock.tsx +33 -0
  57. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/EndpointRow.tsx +73 -0
  58. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/MethodChips.tsx +43 -0
  59. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SchemaSection.tsx +27 -0
  60. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SearchInput.tsx +45 -0
  61. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SidebarBody.tsx +50 -0
  62. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/Toolbar.tsx +64 -0
  63. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/buildVM.ts +126 -0
  64. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/index.tsx +112 -0
  65. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/types.ts +42 -0
  66. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/useDebouncedValue.ts +14 -0
  67. package/src/tools/OpenapiViewer/components/DocsLayout/SlideInPlayground.tsx +10 -7
  68. package/src/tools/OpenapiViewer/components/DocsLayout/TryItSheet.tsx +9 -6
  69. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +55 -0
  70. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PreviewView.tsx +115 -0
  71. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/RawView.tsx +24 -0
  72. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/StatusBar.tsx +63 -0
  73. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/ViewTabs.tsx +45 -0
  74. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/detectContent.ts +97 -0
  75. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/index.tsx +93 -0
  76. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/types.ts +26 -0
  77. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/useResponseView.ts +62 -0
  78. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +41 -71
  79. package/src/tools/OpenapiViewer/types.ts +10 -0
  80. package/src/tools/OpenapiViewer/utils/codeSamples.ts +287 -0
  81. package/src/tools/OpenapiViewer/utils/index.ts +3 -0
  82. package/src/tools/OpenapiViewer/utils/operationToHar.ts +119 -0
  83. package/src/tools/OpenapiViewer/utils/sampler.ts +72 -0
  84. package/src/tools/PrettyCode/PrettyCode.client.tsx +88 -1
  85. package/src/tools/PrettyCode/PrettyCode.story.tsx +114 -361
  86. package/src/tools/PrettyCode/index.tsx +13 -0
  87. package/src/tools/PrettyCode/lazy.tsx +5 -0
  88. package/src/tools/PrettyCode/registerPrismLanguages.ts +111 -0
  89. package/dist/DocsLayout-TKJQ5W5E.mjs.map +0 -1
  90. package/dist/DocsLayout-YDR7DSMM.cjs.map +0 -1
  91. package/dist/PrettyCode.client-5GABIN2I.cjs.map +0 -1
  92. package/dist/PrettyCode.client-IZTXXYHG.mjs.map +0 -1
  93. package/dist/chunk-IULI4XII.cjs.map +0 -1
  94. package/dist/chunk-VZGQC3NG.mjs.map +0 -1
  95. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc.tsx +0 -273
  96. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar.tsx +0 -439
  97. package/src/tools/OpenapiViewer/components/shared/ResponsePanel.tsx +0 -127
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+
3
+ import React, { createContext, useContext, useMemo } from 'react';
4
+
5
+ /** Identity context for a single EndpointDoc subtree.
6
+ *
7
+ * This is NOT a state container — state lives in the zustand store at
8
+ * ``./store``. The context only carries per-endpoint *identity* values
9
+ * so children (Section, CodeSamples, future ExpandAll button) don't
10
+ * have to re-derive them or receive them as repeated props.
11
+ *
12
+ * Why identity-only:
13
+ * - One zustand singleton handles state for *all* endpoints on the
14
+ * page, keyed by ``endpointId``. Wrapping zustand itself in a
15
+ * Provider would defeat that composite-key design.
16
+ * - Identity (anchor, HTTP method) is invariant inside an endpoint
17
+ * card; deriving it per-child is wasted work and makes the props
18
+ * table look noisier than the logic actually is. */
19
+ interface EndpointDocContextValue {
20
+ /** DOM-safe anchor id for this endpoint — used as the store key
21
+ * prefix (``openSections[`${endpointId}:${sectionId}`]``) and as
22
+ * the URL hash target for shareable links. */
23
+ endpointId: string;
24
+ /** HTTP method (``GET`` / ``POST`` / …). Drives per-method default
25
+ * open state for Section. Kept in context so ``defaultSectionOpen``
26
+ * doesn't need to be threaded as a prop through every subtree. */
27
+ method: string;
28
+ }
29
+
30
+ const EndpointDocContext = createContext<EndpointDocContextValue | null>(null);
31
+
32
+ interface EndpointDocProviderProps extends EndpointDocContextValue {
33
+ children: React.ReactNode;
34
+ }
35
+
36
+ export function EndpointDocProvider({ endpointId, method, children }: EndpointDocProviderProps) {
37
+ // Memoise the value so every re-render of the parent orchestrator
38
+ // doesn't invalidate children reading the context — the common
39
+ // hover/focus state on the header row re-renders EndpointDoc often.
40
+ const value = useMemo(() => ({ endpointId, method }), [endpointId, method]);
41
+ return <EndpointDocContext.Provider value={value}>{children}</EndpointDocContext.Provider>;
42
+ }
43
+
44
+ export function useEndpointDocContext(): EndpointDocContextValue {
45
+ const ctx = useContext(EndpointDocContext);
46
+ if (!ctx) {
47
+ // Treated as a programming error, not a runtime one — if this
48
+ // fires, something rendered EndpointDoc internals outside the
49
+ // provider, which means the store keys will collide or be
50
+ // missing entirely. Loud failure beats silent corruption.
51
+ throw new Error(
52
+ '[OpenapiViewer] useEndpointDocContext must be used inside <EndpointDocProvider>.',
53
+ );
54
+ }
55
+ return ctx;
56
+ }
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+
5
+ import { useEndpointDocStore } from '../store';
6
+ import { ALL_SECTION_IDS, type SectionId } from '../types';
7
+
8
+ /** Parse ``#section=<endpointId>.<sectionId>`` out of a hash string.
9
+ * Returns ``null`` for any other shape (including the plain
10
+ * ``#<endpointId>`` form used elsewhere for scrolling to an endpoint). */
11
+ export function parseSectionHash(hash: string): { endpointId: string; sectionId: SectionId } | null {
12
+ const raw = hash.startsWith('#') ? hash.slice(1) : hash;
13
+ if (!raw.startsWith('section=')) return null;
14
+ const value = raw.slice('section='.length);
15
+ const dot = value.lastIndexOf('.');
16
+ if (dot <= 0 || dot === value.length - 1) return null;
17
+ const endpointId = value.slice(0, dot);
18
+ const sectionIdCandidate = value.slice(dot + 1);
19
+ if (!(ALL_SECTION_IDS as readonly string[]).includes(sectionIdCandidate)) return null;
20
+ return { endpointId, sectionId: sectionIdCandidate as SectionId };
21
+ }
22
+
23
+ /** Build the shareable hash that opens a specific section. */
24
+ export function buildSectionHash(endpointId: string, sectionId: SectionId): string {
25
+ return `section=${endpointId}.${sectionId}`;
26
+ }
27
+
28
+ /** On mount + on hashchange, read ``#section=...``, open that section
29
+ * in the store, and scroll its endpoint into view. Runs once per
30
+ * hash change, not per endpoint render — the effect lives above the
31
+ * per-endpoint level in the tree.
32
+ *
33
+ * Behaviour:
34
+ * 1. Parse hash. If it isn't our ``section=`` form, bail — the
35
+ * browser's own anchor-scrolling handles ``#ep-…`` URLs already.
36
+ * 2. Open the referenced section in the zustand store so its body
37
+ * is visible the moment the user lands.
38
+ * 3. Scroll the endpoint's ``<section id={endpointId}>`` into view
39
+ * on the next frame (after the section body has mounted). */
40
+ export function useSectionHashRouter(): void {
41
+ const setSectionOpen = useEndpointDocStore((s) => s.setSectionOpen);
42
+
43
+ useEffect(() => {
44
+ if (typeof window === 'undefined') return;
45
+
46
+ function apply() {
47
+ const parsed = parseSectionHash(window.location.hash);
48
+ if (!parsed) return;
49
+ setSectionOpen(parsed.endpointId, parsed.sectionId, true);
50
+ // Defer scroll to let the section body render first —
51
+ // scrolling a collapsed row lands on the wrong offset
52
+ // because the body takes vertical space once expanded.
53
+ requestAnimationFrame(() => {
54
+ const el = document.getElementById(parsed.endpointId);
55
+ el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
56
+ });
57
+ }
58
+
59
+ apply();
60
+ window.addEventListener('hashchange', apply);
61
+ return () => window.removeEventListener('hashchange', apply);
62
+ }, [setSectionOpen]);
63
+ }
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import type { ApiEndpoint } from '../../../types';
6
+ import { endpointAnchor } from '../anchor';
7
+ import { CodeSamples } from './CodeSamples';
8
+ import { EndpointDocProvider } from './context';
9
+ import { EndpointHeader } from './Header';
10
+ import { Parameters } from './Parameters';
11
+ import { RequestBody } from './RequestBody';
12
+ import { Responses } from './Responses';
13
+ import { Section } from './Section';
14
+ import type { SectionId } from './types';
15
+
16
+ interface EndpointDocProps {
17
+ endpoint: ApiEndpoint;
18
+ /** Is this endpoint currently loaded in the sticky playground? */
19
+ isLoadedInPlayground: boolean;
20
+ onTryIt: () => void;
21
+ /** Scoping prefix for the anchor, so endpoints from different schemas
22
+ * don't collide on a single page. Falls back to ``endpoint.schemaId``. */
23
+ schemaId?: string | null;
24
+ }
25
+
26
+ /** Card that documents one API endpoint: header + a stack of collapsible
27
+ * sections (parameters, request body, responses, code samples). The
28
+ * component itself is a thin orchestrator — each child folder owns its
29
+ * own rendering concerns and reads the endpoint identity from context. */
30
+ export function EndpointDoc({ endpoint, isLoadedInPlayground, onTryIt, schemaId }: EndpointDocProps) {
31
+ const scopedSchemaId = schemaId ?? endpoint.schemaId ?? null;
32
+ const anchor = endpointAnchor(endpoint, scopedSchemaId);
33
+ const pathParams = endpoint.parameters?.filter((p) => endpoint.path.includes(`{${p.name}}`)) ?? [];
34
+ const queryParams = endpoint.parameters?.filter((p) => !endpoint.path.includes(`{${p.name}}`)) ?? [];
35
+
36
+ const hasParameters = pathParams.length > 0 || queryParams.length > 0;
37
+ const hasResponses = (endpoint.responses?.length ?? 0) > 0;
38
+
39
+ // Collect section ids that actually render so the header's
40
+ // expand/collapse-all toggle only touches visible sections.
41
+ // ``codeSamples`` is always present — it synthesises a snippet
42
+ // even for parameter-less endpoints.
43
+ const presentSections: SectionId[] = [];
44
+ if (hasParameters) presentSections.push('parameters');
45
+ if (endpoint.requestBody) presentSections.push('requestBody');
46
+ presentSections.push('codeSamples');
47
+ if (hasResponses) presentSections.push('responses');
48
+
49
+ return (
50
+ <EndpointDocProvider endpointId={anchor} method={endpoint.method}>
51
+ <section
52
+ id={anchor}
53
+ data-endpoint-anchor={anchor}
54
+ data-schema-id={scopedSchemaId ?? ''}
55
+ className="scroll-mt-24 py-10 first:pt-0"
56
+ >
57
+ <EndpointHeader
58
+ endpoint={endpoint}
59
+ anchor={anchor}
60
+ isLoadedInPlayground={isLoadedInPlayground}
61
+ onTryIt={onTryIt}
62
+ presentSections={presentSections}
63
+ />
64
+
65
+ <div className="mt-8 space-y-5">
66
+ {hasParameters && (
67
+ <Section
68
+ id="parameters"
69
+ title="Parameters"
70
+ badge={pathParams.length + queryParams.length}
71
+ >
72
+ <Parameters pathParams={pathParams} queryParams={queryParams} />
73
+ </Section>
74
+ )}
75
+ {endpoint.requestBody && (
76
+ <Section id="requestBody" title="Request body">
77
+ <RequestBody body={endpoint.requestBody} />
78
+ </Section>
79
+ )}
80
+ <Section id="codeSamples" title="Code samples">
81
+ <CodeSamples endpoint={endpoint} />
82
+ </Section>
83
+ {hasResponses && (
84
+ <Section
85
+ id="responses"
86
+ title="Responses"
87
+ badge={endpoint.responses!.length}
88
+ >
89
+ <Responses responses={endpoint.responses!} />
90
+ </Section>
91
+ )}
92
+ </div>
93
+ </section>
94
+ </EndpointDocProvider>
95
+ );
96
+ }
@@ -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;
@@ -120,7 +120,13 @@ export function SchemaCopyMenu({ schema, endpoints, baseUrl, variant = 'button'
120
120
  </Button>
121
121
  )}
122
122
  </DropdownMenuTrigger>
123
- <DropdownMenuContent align="end" className="w-72">
123
+ <DropdownMenuContent
124
+ side="right"
125
+ align="start"
126
+ sideOffset={6}
127
+ collisionPadding={8}
128
+ className="w-60 max-w-[calc(100vw-16px)]"
129
+ >
124
130
  <DropdownMenuLabel className="text-[10px] uppercase tracking-wider text-muted-foreground/70">
125
131
  Copy schema
126
132
  </DropdownMenuLabel>
@@ -150,7 +156,7 @@ export function SchemaCopyMenu({ schema, endpoints, baseUrl, variant = 'button'
150
156
  </span>
151
157
  ) : null}
152
158
  </div>
153
- <span className="text-[10px] text-muted-foreground/70 leading-snug">
159
+ <span className="text-[10px] text-muted-foreground/70 leading-snug line-clamp-1">
154
160
  {label.hint}
155
161
  </span>
156
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
+ }