@djangocfg/ui-nextjs 2.1.3 → 2.1.5
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/package.json +13 -12
- package/src/components/markdown/MarkdownMessage.tsx +11 -26
- package/src/components/sidebar.tsx +3 -1
- package/src/components/ssr-pagination.tsx +14 -30
- package/src/hooks/index.ts +7 -2
- package/src/hooks/useHotkey.ts +103 -0
- package/src/hooks/{useTheme.ts → useResolvedTheme.ts} +29 -18
- package/src/theme/ThemeProvider.tsx +55 -61
- package/src/theme/ThemeToggle.tsx +15 -11
- package/src/theme/index.ts +2 -1
- package/src/tools/JsonTree/index.tsx +10 -20
- package/src/tools/Mermaid/Mermaid.client.tsx +2 -2
- package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +20 -21
- package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +7 -17
- package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +26 -26
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +0 -7
- package/src/tools/OpenapiViewer/types.ts +0 -1
- package/src/tools/PrettyCode/PrettyCode.client.tsx +10 -25
- package/src/tools/VideoPlayer/VideoPlayer.tsx +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-nextjs",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
4
4
|
"description": "Next.js UI component library with Radix UI primitives, Tailwind CSS styling, charts, and form components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -58,10 +58,11 @@
|
|
|
58
58
|
"check": "tsc --noEmit"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@djangocfg/api": "^2.1.
|
|
62
|
-
"@djangocfg/ui-core": "^1.
|
|
61
|
+
"@djangocfg/api": "^2.1.5",
|
|
62
|
+
"@djangocfg/ui-core": "^2.1.5",
|
|
63
63
|
"@types/react": "^19.1.0",
|
|
64
64
|
"@types/react-dom": "^19.1.0",
|
|
65
|
+
"consola": "^3.4.2",
|
|
65
66
|
"lucide-react": "^0.545.0",
|
|
66
67
|
"moment": "^2.30.1",
|
|
67
68
|
"next": "^15.5.7",
|
|
@@ -69,13 +70,9 @@
|
|
|
69
70
|
"react-dom": "^19.1.0",
|
|
70
71
|
"react-hook-form": "7.65.0",
|
|
71
72
|
"tailwindcss": "^4.1.14",
|
|
72
|
-
"zod": "^4.1.13"
|
|
73
|
-
"consola": "^3.4.2"
|
|
73
|
+
"zod": "^4.1.13"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"class-variance-authority": "^0.7.1",
|
|
77
|
-
"libphonenumber-js": "^1.12.24",
|
|
78
|
-
"sonner": "2.0.7",
|
|
79
76
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
80
77
|
"@radix-ui/react-menubar": "^1.1.16",
|
|
81
78
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
@@ -83,26 +80,30 @@
|
|
|
83
80
|
"@rjsf/core": "^6.1.2",
|
|
84
81
|
"@rjsf/utils": "^6.1.2",
|
|
85
82
|
"@rjsf/validator-ajv8": "^6.1.2",
|
|
83
|
+
"@vidstack/react": "next",
|
|
86
84
|
"@web3icons/react": "^4.0.26",
|
|
87
85
|
"chart.js": "^4.5.0",
|
|
86
|
+
"class-variance-authority": "^0.7.1",
|
|
88
87
|
"cytoscape": "^3.33.1",
|
|
89
88
|
"cytoscape-cose-bilkent": "^4.1.0",
|
|
89
|
+
"libphonenumber-js": "^1.12.24",
|
|
90
|
+
"media-icons": "next",
|
|
90
91
|
"mermaid": "^11.12.0",
|
|
91
92
|
"next-themes": "^0.4.6",
|
|
92
93
|
"prism-react-renderer": "^2.4.1",
|
|
93
94
|
"react-chartjs-2": "^5.3.0",
|
|
95
|
+
"react-hotkeys-hook": "^5.2.1",
|
|
94
96
|
"react-json-tree": "^0.20.0",
|
|
95
97
|
"react-lottie-player": "^2.1.0",
|
|
96
98
|
"react-markdown": "10.1.0",
|
|
97
|
-
"remark-gfm": "4.0.1",
|
|
98
99
|
"react-sticky-box": "^2.0.5",
|
|
99
100
|
"recharts": "2.15.4",
|
|
100
|
-
"
|
|
101
|
-
"
|
|
101
|
+
"remark-gfm": "4.0.1",
|
|
102
|
+
"sonner": "2.0.7",
|
|
102
103
|
"vidstack": "next"
|
|
103
104
|
},
|
|
104
105
|
"devDependencies": {
|
|
105
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
106
|
+
"@djangocfg/typescript-config": "^2.1.5",
|
|
106
107
|
"@types/node": "^24.7.2",
|
|
107
108
|
"eslint": "^9.37.0",
|
|
108
109
|
"tailwindcss-animate": "1.0.7",
|
|
@@ -5,10 +5,8 @@ import ReactMarkdown from 'react-markdown';
|
|
|
5
5
|
import remarkGfm from 'remark-gfm';
|
|
6
6
|
import Mermaid from '../../tools/Mermaid';
|
|
7
7
|
import PrettyCode from '../../tools/PrettyCode';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { useTheme } from '../../hooks/useTheme';
|
|
11
|
-
import { Copy } from 'lucide-react';
|
|
8
|
+
import { CopyButton } from '@djangocfg/ui-core/components';
|
|
9
|
+
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
|
|
12
10
|
import type { Components } from 'react-markdown';
|
|
13
11
|
|
|
14
12
|
// Helper function to extract text content from React children
|
|
@@ -52,32 +50,24 @@ interface CodeBlockProps {
|
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
const CodeBlock: React.FC<CodeBlockProps> = ({ code, language, isUser }) => {
|
|
55
|
-
const
|
|
56
|
-
const theme = useTheme();
|
|
57
|
-
|
|
58
|
-
const handleCopy = () => {
|
|
59
|
-
copyToClipboard(code, "Code copied to clipboard!");
|
|
60
|
-
};
|
|
53
|
+
const theme = useResolvedTheme();
|
|
61
54
|
|
|
62
55
|
return (
|
|
63
56
|
<div className="relative group my-3">
|
|
64
57
|
{/* Copy button */}
|
|
65
|
-
<
|
|
58
|
+
<CopyButton
|
|
59
|
+
value={code}
|
|
66
60
|
variant="ghost"
|
|
67
|
-
size="sm"
|
|
68
|
-
onClick={handleCopy}
|
|
69
61
|
className={`
|
|
70
62
|
absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity
|
|
71
|
-
h-8 w-8
|
|
63
|
+
h-8 w-8
|
|
72
64
|
${isUser
|
|
73
65
|
? 'hover:bg-white/20 text-white'
|
|
74
66
|
: 'hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground'
|
|
75
67
|
}
|
|
76
68
|
`}
|
|
77
69
|
title="Copy code"
|
|
78
|
-
|
|
79
|
-
<Copy className="h-4 w-4" />
|
|
80
|
-
</Button>
|
|
70
|
+
/>
|
|
81
71
|
|
|
82
72
|
{/* Code content */}
|
|
83
73
|
<PrettyCode
|
|
@@ -195,24 +185,19 @@ const createMarkdownComponents = (isUser: boolean = false, isCompact: boolean =
|
|
|
195
185
|
console.warn('CodeBlock failed, using fallback:', error);
|
|
196
186
|
return (
|
|
197
187
|
<div className="relative group my-3">
|
|
198
|
-
<
|
|
188
|
+
<CopyButton
|
|
189
|
+
value={codeContent}
|
|
199
190
|
variant="ghost"
|
|
200
|
-
size="sm"
|
|
201
|
-
onClick={() => {
|
|
202
|
-
navigator.clipboard.writeText(codeContent);
|
|
203
|
-
}}
|
|
204
191
|
className={`
|
|
205
192
|
absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity
|
|
206
|
-
h-8 w-8
|
|
193
|
+
h-8 w-8
|
|
207
194
|
${isUser
|
|
208
195
|
? 'hover:bg-white/20 text-white'
|
|
209
196
|
: 'hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground'
|
|
210
197
|
}
|
|
211
198
|
`}
|
|
212
199
|
title="Copy code"
|
|
213
|
-
|
|
214
|
-
<Copy className="h-4 w-4" />
|
|
215
|
-
</Button>
|
|
200
|
+
/>
|
|
216
201
|
<pre className={`
|
|
217
202
|
p-3 rounded text-xs font-mono overflow-x-auto
|
|
218
203
|
${isUser
|
|
@@ -440,7 +440,9 @@ const SidebarFooter = React.forwardRef<
|
|
|
440
440
|
})
|
|
441
441
|
SidebarFooter.displayName = "SidebarFooter"
|
|
442
442
|
|
|
443
|
-
const SidebarSeparator
|
|
443
|
+
const SidebarSeparator: React.ForwardRefExoticComponent<
|
|
444
|
+
React.ComponentProps<typeof Separator> & React.RefAttributes<React.ElementRef<typeof Separator>>
|
|
445
|
+
> = React.forwardRef<
|
|
444
446
|
React.ElementRef<typeof Separator>,
|
|
445
447
|
React.ComponentProps<typeof Separator>
|
|
446
448
|
>(({ className, ...props }, ref) => {
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { usePathname } from 'next/navigation';
|
|
5
|
-
import Link from 'next/link';
|
|
6
5
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
7
6
|
import { useIsMobile } from '@djangocfg/ui-core/hooks';
|
|
8
7
|
import { useQueryParams } from '../hooks';
|
|
@@ -29,7 +28,6 @@ interface SSRPaginationProps {
|
|
|
29
28
|
baseUrl?: string;
|
|
30
29
|
pathname?: string;
|
|
31
30
|
preserveQuery?: boolean;
|
|
32
|
-
prefetch?: boolean;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
export const SSRPagination: React.FC<SSRPaginationProps> = ({
|
|
@@ -45,7 +43,6 @@ export const SSRPagination: React.FC<SSRPaginationProps> = ({
|
|
|
45
43
|
baseUrl,
|
|
46
44
|
pathname: propPathname,
|
|
47
45
|
preserveQuery = true,
|
|
48
|
-
prefetch = true,
|
|
49
46
|
}) => {
|
|
50
47
|
const queryParams = useQueryParams();
|
|
51
48
|
const pathname = usePathname();
|
|
@@ -186,17 +183,10 @@ export const SSRPagination: React.FC<SSRPaginationProps> = ({
|
|
|
186
183
|
<PaginationContent>
|
|
187
184
|
{/* Previous Button */}
|
|
188
185
|
<PaginationItem>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
) : (
|
|
194
|
-
<PaginationPrevious
|
|
195
|
-
href="#"
|
|
196
|
-
className="pointer-events-none opacity-50"
|
|
197
|
-
onClick={(e) => e.preventDefault()}
|
|
198
|
-
/>
|
|
199
|
-
)}
|
|
186
|
+
<PaginationPrevious
|
|
187
|
+
href={actualHasPreviousPage ? getPageUrl(actualCurrentPage - 1) : undefined}
|
|
188
|
+
className={!actualHasPreviousPage ? "pointer-events-none opacity-50" : undefined}
|
|
189
|
+
/>
|
|
200
190
|
</PaginationItem>
|
|
201
191
|
|
|
202
192
|
{/* Page Numbers */}
|
|
@@ -205,28 +195,22 @@ export const SSRPagination: React.FC<SSRPaginationProps> = ({
|
|
|
205
195
|
{page === 'ellipsis' ? (
|
|
206
196
|
<PaginationEllipsis />
|
|
207
197
|
) : (
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
198
|
+
<PaginationLink
|
|
199
|
+
href={getPageUrl(page)}
|
|
200
|
+
isActive={page === actualCurrentPage}
|
|
201
|
+
>
|
|
202
|
+
{page}
|
|
203
|
+
</PaginationLink>
|
|
213
204
|
)}
|
|
214
205
|
</PaginationItem>
|
|
215
206
|
))}
|
|
216
207
|
|
|
217
208
|
{/* Next Button */}
|
|
218
209
|
<PaginationItem>
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
) : (
|
|
224
|
-
<PaginationNext
|
|
225
|
-
href="#"
|
|
226
|
-
className="pointer-events-none opacity-50"
|
|
227
|
-
onClick={(e) => e.preventDefault()}
|
|
228
|
-
/>
|
|
229
|
-
)}
|
|
210
|
+
<PaginationNext
|
|
211
|
+
href={hasNextPage ? getPageUrl(actualCurrentPage + 1) : undefined}
|
|
212
|
+
className={!hasNextPage ? "pointer-events-none opacity-50" : undefined}
|
|
213
|
+
/>
|
|
230
214
|
</PaginationItem>
|
|
231
215
|
</PaginationContent>
|
|
232
216
|
</Pagination>
|
package/src/hooks/index.ts
CHANGED
|
@@ -11,9 +11,14 @@ export * from '@djangocfg/ui-core/hooks';
|
|
|
11
11
|
export { useLocalStorage } from './useLocalStorage';
|
|
12
12
|
export { useSessionStorage } from './useSessionStorage';
|
|
13
13
|
|
|
14
|
-
// Theme hook (
|
|
15
|
-
export {
|
|
14
|
+
// Theme hook (standalone, no provider required)
|
|
15
|
+
export { useResolvedTheme } from './useResolvedTheme';
|
|
16
|
+
export type { ResolvedTheme } from './useResolvedTheme';
|
|
16
17
|
|
|
17
18
|
// Next.js router hooks
|
|
18
19
|
export { useQueryParams } from './useQueryParams';
|
|
19
20
|
export { useCfgRouter } from './useCfgRouter';
|
|
21
|
+
|
|
22
|
+
// Keyboard shortcuts
|
|
23
|
+
export { useHotkey, useHotkeysContext, HotkeysProvider, isHotkeyPressed } from './useHotkey';
|
|
24
|
+
export type { UseHotkeyOptions, HotkeyCallback, Keys } from './useHotkey';
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { RefObject } from 'react';
|
|
4
|
+
import { useHotkeys, Options as HotkeysOptions } from 'react-hotkeys-hook';
|
|
5
|
+
import type { HotkeyCallback, Keys } from 'react-hotkeys-hook';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for the useHotkey hook
|
|
9
|
+
*/
|
|
10
|
+
export interface UseHotkeyOptions extends Omit<HotkeysOptions, 'enabled'> {
|
|
11
|
+
/** Whether the hotkey is enabled (default: true) */
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/** Scope for the hotkey - useful for context-specific shortcuts */
|
|
14
|
+
scope?: string;
|
|
15
|
+
/** Only trigger when focus is within a specific element */
|
|
16
|
+
scopes?: string[];
|
|
17
|
+
/** Prevent default browser behavior */
|
|
18
|
+
preventDefault?: boolean;
|
|
19
|
+
/** Enable in input fields and textareas */
|
|
20
|
+
enableOnFormTags?: boolean | readonly ('input' | 'textarea' | 'select')[];
|
|
21
|
+
/** Enable when contentEditable element is focused */
|
|
22
|
+
enableOnContentEditable?: boolean;
|
|
23
|
+
/** Split key for multiple hotkey combinations (default: ',') */
|
|
24
|
+
splitKey?: string;
|
|
25
|
+
/** Key up/down events */
|
|
26
|
+
keyup?: boolean;
|
|
27
|
+
keydown?: boolean;
|
|
28
|
+
/** Description for the hotkey (useful for help dialogs) */
|
|
29
|
+
description?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Simple wrapper hook for react-hotkeys-hook
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Single key
|
|
37
|
+
* useHotkey('escape', () => closeModal());
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Key combination
|
|
41
|
+
* useHotkey('ctrl+s', (e) => {
|
|
42
|
+
* e.preventDefault();
|
|
43
|
+
* saveDocument();
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Multiple keys (any of them will trigger)
|
|
48
|
+
* useHotkey(['ArrowLeft', '['], () => goToPrevious());
|
|
49
|
+
* useHotkey(['ArrowRight', ']'], () => goToNext());
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // With options
|
|
53
|
+
* useHotkey('/', () => focusSearch(), {
|
|
54
|
+
* preventDefault: true,
|
|
55
|
+
* enableOnFormTags: false,
|
|
56
|
+
* description: 'Focus search input'
|
|
57
|
+
* });
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // Scoped hotkeys
|
|
61
|
+
* useHotkey('delete', () => deleteItem(), { scopes: ['list-view'] });
|
|
62
|
+
*
|
|
63
|
+
* @param keys - Hotkey or array of hotkeys (e.g., 'ctrl+s', 'ArrowLeft', ['[', 'ArrowLeft'])
|
|
64
|
+
* @param callback - Function to call when hotkey is pressed
|
|
65
|
+
* @param options - Configuration options
|
|
66
|
+
* @returns Ref to attach to element for scoped hotkeys
|
|
67
|
+
*/
|
|
68
|
+
export function useHotkey<T extends HTMLElement = HTMLElement>(
|
|
69
|
+
keys: Keys,
|
|
70
|
+
callback: HotkeyCallback,
|
|
71
|
+
options: UseHotkeyOptions = {}
|
|
72
|
+
): RefObject<T | null> {
|
|
73
|
+
const {
|
|
74
|
+
enabled = true,
|
|
75
|
+
preventDefault = false,
|
|
76
|
+
enableOnFormTags = false,
|
|
77
|
+
enableOnContentEditable = false,
|
|
78
|
+
description: _description,
|
|
79
|
+
...restOptions
|
|
80
|
+
} = options;
|
|
81
|
+
|
|
82
|
+
return useHotkeys<T>(
|
|
83
|
+
keys,
|
|
84
|
+
(event, handler) => {
|
|
85
|
+
if (preventDefault) {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
}
|
|
88
|
+
callback(event, handler);
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
enabled,
|
|
92
|
+
enableOnFormTags,
|
|
93
|
+
enableOnContentEditable,
|
|
94
|
+
...restOptions,
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Re-export useful utilities from react-hotkeys-hook
|
|
100
|
+
export { useHotkeysContext, HotkeysProvider, isHotkeyPressed } from 'react-hotkeys-hook';
|
|
101
|
+
|
|
102
|
+
// Re-export types
|
|
103
|
+
export type { HotkeyCallback, Keys } from 'react-hotkeys-hook';
|
|
@@ -2,56 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
|
|
5
|
-
export type
|
|
5
|
+
export type ResolvedTheme = 'light' | 'dark';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Hook to detect
|
|
9
|
-
*
|
|
8
|
+
* Hook to detect the current resolved theme (light or dark)
|
|
9
|
+
*
|
|
10
|
+
* Standalone hook - doesn't require ThemeProvider.
|
|
11
|
+
* Detects theme from:
|
|
12
|
+
* 1. 'dark' class on html element
|
|
13
|
+
* 2. System preference (prefers-color-scheme)
|
|
14
|
+
*
|
|
15
|
+
* For full theme control (setTheme, toggleTheme), use useThemeContext instead.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const theme = useResolvedTheme(); // 'light' | 'dark'
|
|
20
|
+
* ```
|
|
10
21
|
*/
|
|
11
|
-
export const
|
|
12
|
-
const [theme, setTheme] = useState<
|
|
13
|
-
|
|
22
|
+
export const useResolvedTheme = (): ResolvedTheme => {
|
|
23
|
+
const [theme, setTheme] = useState<ResolvedTheme>('light');
|
|
24
|
+
|
|
14
25
|
useEffect(() => {
|
|
15
|
-
const checkTheme = ():
|
|
16
|
-
// Check if dark class is applied to html element
|
|
26
|
+
const checkTheme = (): ResolvedTheme => {
|
|
27
|
+
// Check if dark class is applied to html element
|
|
17
28
|
if (document.documentElement.classList.contains('dark')) {
|
|
18
29
|
return 'dark';
|
|
19
30
|
}
|
|
20
|
-
|
|
31
|
+
|
|
21
32
|
// Check system preference
|
|
22
33
|
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
23
34
|
return 'dark';
|
|
24
35
|
}
|
|
25
|
-
|
|
36
|
+
|
|
26
37
|
return 'light';
|
|
27
38
|
};
|
|
28
|
-
|
|
39
|
+
|
|
29
40
|
// Set initial theme
|
|
30
41
|
setTheme(checkTheme());
|
|
31
|
-
|
|
32
|
-
// Listen for
|
|
42
|
+
|
|
43
|
+
// Listen for class changes on html element
|
|
33
44
|
const observer = new MutationObserver(() => {
|
|
34
45
|
setTheme(checkTheme());
|
|
35
46
|
});
|
|
36
|
-
|
|
47
|
+
|
|
37
48
|
observer.observe(document.documentElement, {
|
|
38
49
|
attributes: true,
|
|
39
50
|
attributeFilter: ['class']
|
|
40
51
|
});
|
|
41
|
-
|
|
52
|
+
|
|
42
53
|
// Listen for system theme changes
|
|
43
54
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
44
55
|
const handleMediaChange = () => {
|
|
45
56
|
setTheme(checkTheme());
|
|
46
57
|
};
|
|
47
|
-
|
|
58
|
+
|
|
48
59
|
mediaQuery.addEventListener('change', handleMediaChange);
|
|
49
|
-
|
|
60
|
+
|
|
50
61
|
return () => {
|
|
51
62
|
observer.disconnect();
|
|
52
63
|
mediaQuery.removeEventListener('change', handleMediaChange);
|
|
53
64
|
};
|
|
54
65
|
}, []);
|
|
55
|
-
|
|
66
|
+
|
|
56
67
|
return theme;
|
|
57
68
|
};
|
|
@@ -1,82 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ThemeProvider - Universal theme management
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Re-exports next-themes ThemeProvider with sensible defaults.
|
|
5
|
+
* Supports light, dark, and system themes with localStorage persistence.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // In app/layout.tsx
|
|
10
|
+
* import { ThemeProvider } from '@djangocfg/ui-nextjs';
|
|
11
|
+
*
|
|
12
|
+
* <ThemeProvider
|
|
13
|
+
* attribute="class"
|
|
14
|
+
* defaultTheme="system"
|
|
15
|
+
* enableSystem
|
|
16
|
+
* >
|
|
17
|
+
* {children}
|
|
18
|
+
* </ThemeProvider>
|
|
19
|
+
* ```
|
|
5
20
|
*/
|
|
6
21
|
|
|
7
22
|
'use client';
|
|
8
23
|
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
13
|
-
// Types
|
|
14
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
type Theme = 'light' | 'dark';
|
|
17
|
-
|
|
18
|
-
interface ThemeContextValue {
|
|
19
|
-
theme: Theme;
|
|
20
|
-
setTheme: (theme: Theme) => void;
|
|
21
|
-
toggleTheme: () => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
25
|
-
// Create Context
|
|
26
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
24
|
+
import { ThemeProvider as NextThemesProvider, useTheme as useNextTheme } from 'next-themes';
|
|
25
|
+
import type { ThemeProviderProps as NextThemesProviderProps } from 'next-themes';
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Provider Component
|
|
32
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
interface ThemeProviderProps {
|
|
35
|
-
children: ReactNode;
|
|
36
|
-
defaultTheme?: Theme;
|
|
37
|
-
storageKey?: string;
|
|
38
|
-
}
|
|
27
|
+
// Re-export types
|
|
28
|
+
export type Theme = 'light' | 'dark' | 'system';
|
|
29
|
+
export type ThemeProviderProps = NextThemesProviderProps;
|
|
39
30
|
|
|
31
|
+
/**
|
|
32
|
+
* ThemeProvider wraps next-themes with sensible defaults
|
|
33
|
+
*/
|
|
40
34
|
export function ThemeProvider({
|
|
41
35
|
children,
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
attribute = 'class',
|
|
37
|
+
defaultTheme = 'system',
|
|
38
|
+
enableSystem = true,
|
|
39
|
+
disableTransitionOnChange = true,
|
|
40
|
+
...props
|
|
44
41
|
}: ThemeProviderProps) {
|
|
45
|
-
|
|
42
|
+
return (
|
|
43
|
+
<NextThemesProvider
|
|
44
|
+
attribute={attribute}
|
|
45
|
+
defaultTheme={defaultTheme}
|
|
46
|
+
enableSystem={enableSystem}
|
|
47
|
+
disableTransitionOnChange={disableTransitionOnChange}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</NextThemesProvider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Hook to access theme context
|
|
57
|
+
* Returns theme, setTheme, resolvedTheme, systemTheme, and themes
|
|
58
|
+
*/
|
|
59
|
+
export function useThemeContext() {
|
|
60
|
+
const { theme, setTheme, resolvedTheme, systemTheme, themes } = useNextTheme();
|
|
52
61
|
|
|
53
62
|
const toggleTheme = () => {
|
|
54
|
-
|
|
63
|
+
// Toggle between light and dark (ignore system)
|
|
64
|
+
const currentResolved = resolvedTheme || 'light';
|
|
65
|
+
setTheme(currentResolved === 'light' ? 'dark' : 'light');
|
|
55
66
|
};
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
theme,
|
|
68
|
+
return {
|
|
69
|
+
theme: theme as Theme | undefined,
|
|
59
70
|
setTheme,
|
|
71
|
+
resolvedTheme: resolvedTheme as 'light' | 'dark' | undefined,
|
|
72
|
+
systemTheme: systemTheme as 'light' | 'dark' | undefined,
|
|
73
|
+
themes,
|
|
60
74
|
toggleTheme,
|
|
61
75
|
};
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<ThemeContext.Provider value={value}>
|
|
65
|
-
{children}
|
|
66
|
-
</ThemeContext.Provider>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
71
|
-
// Custom Hook
|
|
72
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
export function useThemeContext(): ThemeContextValue {
|
|
75
|
-
const context = useContext(ThemeContext);
|
|
76
|
-
|
|
77
|
-
if (context === undefined) {
|
|
78
|
-
throw new Error('useThemeContext must be used within ThemeProvider');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return context;
|
|
82
76
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ThemeToggle - Theme switcher component
|
|
3
3
|
*
|
|
4
|
-
* Switches between light and dark themes
|
|
5
|
-
*
|
|
4
|
+
* Switches between light and dark themes.
|
|
5
|
+
* Must be used within ThemeProvider.
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```tsx
|
|
@@ -20,17 +20,21 @@ import { Button } from '@djangocfg/ui-core/components';
|
|
|
20
20
|
import { useThemeContext } from './ThemeProvider';
|
|
21
21
|
|
|
22
22
|
export function ThemeToggle() {
|
|
23
|
-
const {
|
|
24
|
-
const [
|
|
23
|
+
const { resolvedTheme, toggleTheme } = useThemeContext();
|
|
24
|
+
const [mounted, setMounted] = useState(false);
|
|
25
25
|
|
|
26
|
-
// Prevent hydration mismatch
|
|
26
|
+
// Prevent hydration mismatch
|
|
27
27
|
useEffect(() => {
|
|
28
|
-
|
|
28
|
+
setMounted(true);
|
|
29
29
|
}, []);
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
if (!mounted) {
|
|
32
|
+
return (
|
|
33
|
+
<Button variant="ghost" size="icon" className="h-9 w-9" disabled>
|
|
34
|
+
<Sun className="h-4 w-4" />
|
|
35
|
+
<span className="sr-only">Toggle theme</span>
|
|
36
|
+
</Button>
|
|
37
|
+
);
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
return (
|
|
@@ -39,9 +43,9 @@ export function ThemeToggle() {
|
|
|
39
43
|
size="icon"
|
|
40
44
|
onClick={toggleTheme}
|
|
41
45
|
className="h-9 w-9"
|
|
42
|
-
title={`Switch to ${
|
|
46
|
+
title={`Switch to ${resolvedTheme === 'light' ? 'dark' : 'light'} theme`}
|
|
43
47
|
>
|
|
44
|
-
{
|
|
48
|
+
{resolvedTheme === 'light' ? (
|
|
45
49
|
<Sun className="h-4 w-4" />
|
|
46
50
|
) : (
|
|
47
51
|
<Moon className="h-4 w-4" />
|
package/src/theme/index.ts
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState } from 'react';
|
|
4
4
|
import { CommonExternalProps, JSONTree } from 'react-json-tree';
|
|
5
|
-
import { ChevronDown, ChevronUp,
|
|
6
|
-
import { Button } from '@djangocfg/ui-core/components';
|
|
7
|
-
import { useCopy } from '@djangocfg/ui-core/hooks';
|
|
5
|
+
import { ChevronDown, ChevronUp, Download } from 'lucide-react';
|
|
6
|
+
import { Button, CopyButton } from '@djangocfg/ui-core/components';
|
|
8
7
|
|
|
9
8
|
export type { Language } from 'prism-react-renderer';
|
|
10
9
|
|
|
@@ -43,12 +42,6 @@ const JsonTreeComponent = ({ title, data, config = {}, jsonTreeProps = {} }: Jso
|
|
|
43
42
|
// State for expand/collapse all
|
|
44
43
|
const [expandAll, setExpandAll] = useState<boolean | null>(null);
|
|
45
44
|
const [renderKey, setRenderKey] = useState(0);
|
|
46
|
-
|
|
47
|
-
// Copy hook
|
|
48
|
-
const { copyToClipboard } = useCopy({
|
|
49
|
-
successMessage: "JSON copied to clipboard",
|
|
50
|
-
errorMessage: "Failed to copy JSON"
|
|
51
|
-
});
|
|
52
45
|
|
|
53
46
|
// Default configuration
|
|
54
47
|
const {
|
|
@@ -142,14 +135,11 @@ const JsonTreeComponent = ({ title, data, config = {}, jsonTreeProps = {} }: Jso
|
|
|
142
135
|
return false;
|
|
143
136
|
};
|
|
144
137
|
|
|
145
|
-
//
|
|
146
|
-
const
|
|
147
|
-
const jsonString = JSON.stringify(data, null, 2);
|
|
148
|
-
copyToClipboard(jsonString);
|
|
149
|
-
};
|
|
138
|
+
// JSON string for copy/download
|
|
139
|
+
const jsonString = JSON.stringify(data, null, 2);
|
|
150
140
|
|
|
141
|
+
// Action handlers
|
|
151
142
|
const handleDownload = () => {
|
|
152
|
-
const jsonString = JSON.stringify(data, null, 2);
|
|
153
143
|
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
154
144
|
const url = URL.createObjectURL(blob);
|
|
155
145
|
const a = document.createElement('a');
|
|
@@ -202,15 +192,15 @@ const JsonTreeComponent = ({ title, data, config = {}, jsonTreeProps = {} }: Jso
|
|
|
202
192
|
{/* Action Buttons */}
|
|
203
193
|
{showActionButtons && (
|
|
204
194
|
<>
|
|
205
|
-
<
|
|
195
|
+
<CopyButton
|
|
196
|
+
value={jsonString}
|
|
206
197
|
variant="outline"
|
|
207
198
|
size="sm"
|
|
208
|
-
onClick={handleCopy}
|
|
209
199
|
className="h-8 px-2"
|
|
200
|
+
iconClassName="h-3 w-3"
|
|
210
201
|
>
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
</Button>
|
|
202
|
+
Copy
|
|
203
|
+
</CopyButton>
|
|
214
204
|
<Button
|
|
215
205
|
variant="outline"
|
|
216
206
|
size="sm"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import mermaid from 'mermaid';
|
|
4
4
|
import React, { useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
|
-
import {
|
|
6
|
+
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
|
|
7
7
|
|
|
8
8
|
interface MermaidProps {
|
|
9
9
|
chart: string;
|
|
@@ -48,7 +48,7 @@ const Mermaid: React.FC<MermaidProps> = ({ chart, className = '' }) => {
|
|
|
48
48
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
49
49
|
const [svgContent, setSvgContent] = useState<string>('');
|
|
50
50
|
const [isVertical, setIsVertical] = useState(false);
|
|
51
|
-
const theme =
|
|
51
|
+
const theme = useResolvedTheme();
|
|
52
52
|
|
|
53
53
|
useEffect(() => {
|
|
54
54
|
// Get CSS variables for semantic colors
|
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { Badge,
|
|
5
|
-
import { ChevronDown, Code, Database, FileText, AlertCircle
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
|
+
import { Badge, Card, CardContent, CardHeader, CardTitle, Collapsible, CollapsibleContent, CollapsibleTrigger, CopyButton } from '@djangocfg/ui-core/components';
|
|
5
|
+
import { ChevronDown, Code, Database, FileText, AlertCircle } from 'lucide-react';
|
|
6
6
|
import { usePlaygroundContext } from '../context/PlaygroundContext';
|
|
7
7
|
import { getMethodColor, getStatusColor } from '../utils';
|
|
8
8
|
|
|
9
9
|
export const EndpointInfo: React.FC = () => {
|
|
10
|
-
const { state
|
|
10
|
+
const { state } = usePlaygroundContext();
|
|
11
11
|
const { selectedEndpoint } = state;
|
|
12
12
|
|
|
13
|
+
// Memoize endpoint JSON for copy
|
|
14
|
+
const endpointJson = useMemo(() => {
|
|
15
|
+
if (!selectedEndpoint) return '';
|
|
16
|
+
return JSON.stringify({
|
|
17
|
+
name: selectedEndpoint.name,
|
|
18
|
+
method: selectedEndpoint.method,
|
|
19
|
+
path: selectedEndpoint.path,
|
|
20
|
+
description: selectedEndpoint.description,
|
|
21
|
+
parameters: selectedEndpoint.parameters,
|
|
22
|
+
requestBody: selectedEndpoint.requestBody,
|
|
23
|
+
responses: selectedEndpoint.responses
|
|
24
|
+
}, null, 2);
|
|
25
|
+
}, [selectedEndpoint]);
|
|
26
|
+
|
|
13
27
|
if (!selectedEndpoint) {
|
|
14
28
|
return null;
|
|
15
29
|
}
|
|
@@ -22,28 +36,13 @@ export const EndpointInfo: React.FC = () => {
|
|
|
22
36
|
));
|
|
23
37
|
};
|
|
24
38
|
|
|
25
|
-
const handleCopyEndpoint = () => {
|
|
26
|
-
const endpointDetails = {
|
|
27
|
-
name: selectedEndpoint.name,
|
|
28
|
-
method: selectedEndpoint.method,
|
|
29
|
-
path: selectedEndpoint.path,
|
|
30
|
-
description: selectedEndpoint.description,
|
|
31
|
-
parameters: selectedEndpoint.parameters,
|
|
32
|
-
requestBody: selectedEndpoint.requestBody,
|
|
33
|
-
responses: selectedEndpoint.responses
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
copyToClipboard(JSON.stringify(endpointDetails, null, 2));
|
|
37
|
-
};
|
|
38
|
-
|
|
39
39
|
return (
|
|
40
40
|
<div className="space-y-4">
|
|
41
41
|
<div className="flex items-center justify-between">
|
|
42
42
|
<h2 className="text-lg font-semibold text-foreground">Selected Endpoint</h2>
|
|
43
|
-
<
|
|
44
|
-
<Copy className="h-4 w-4" />
|
|
43
|
+
<CopyButton value={endpointJson} variant="outline" size="sm">
|
|
45
44
|
Copy
|
|
46
|
-
</
|
|
45
|
+
</CopyButton>
|
|
47
46
|
</div>
|
|
48
47
|
|
|
49
48
|
<Card>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useCallback } from 'react';
|
|
4
|
-
import { Button, Card, CardContent, CardHeader, CardTitle, Input,
|
|
5
|
-
import {
|
|
4
|
+
import { Button, Card, CardContent, CardHeader, CardTitle, Input, Textarea, CopyButton } from '@djangocfg/ui-core/components';
|
|
5
|
+
import { Loader2, Key, Send } from 'lucide-react';
|
|
6
6
|
import { usePlaygroundContext } from '../context/PlaygroundContext';
|
|
7
|
-
import { findApiKeyById
|
|
7
|
+
import { findApiKeyById } from '../utils';
|
|
8
8
|
import { isValidJson, parseRequestHeaders } from '../utils';
|
|
9
9
|
import { RequestParametersForm } from './RequestParametersForm';
|
|
10
10
|
import { EndpointInfo } from './EndpointInfo';
|
|
@@ -14,16 +14,8 @@ export const RequestBuilder: React.FC = () => {
|
|
|
14
14
|
const {
|
|
15
15
|
state,
|
|
16
16
|
apiKeys,
|
|
17
|
-
apiKeysLoading,
|
|
18
|
-
setRequestUrl,
|
|
19
|
-
setRequestMethod,
|
|
20
|
-
setRequestHeaders,
|
|
21
17
|
setRequestBody,
|
|
22
|
-
setResponse,
|
|
23
|
-
setLoading,
|
|
24
|
-
setSelectedApiKey,
|
|
25
18
|
setManualApiToken,
|
|
26
|
-
copyToClipboard,
|
|
27
19
|
sendRequest
|
|
28
20
|
} = usePlaygroundContext();
|
|
29
21
|
|
|
@@ -143,15 +135,13 @@ export const RequestBuilder: React.FC = () => {
|
|
|
143
135
|
<CardHeader>
|
|
144
136
|
<div className="flex items-center justify-between">
|
|
145
137
|
<CardTitle className="text-sm text-foreground">cURL Command</CardTitle>
|
|
146
|
-
<
|
|
138
|
+
<CopyButton
|
|
139
|
+
value={curlCommand}
|
|
147
140
|
variant="outline"
|
|
148
141
|
size="sm"
|
|
149
|
-
onClick={() => copyToClipboard(curlCommand)}
|
|
150
|
-
disabled={!curlCommand}
|
|
151
142
|
>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
</Button>
|
|
143
|
+
Copy cURL
|
|
144
|
+
</CopyButton>
|
|
155
145
|
</div>
|
|
156
146
|
</CardHeader>
|
|
157
147
|
<CardContent>
|
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from '@djangocfg/ui-core/components';
|
|
5
|
-
import {
|
|
3
|
+
import React, { useMemo, useCallback } from 'react';
|
|
4
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, CopyButton } from '@djangocfg/ui-core/components';
|
|
5
|
+
import { XCircle, Download } from 'lucide-react';
|
|
6
6
|
import { usePlaygroundContext } from '../context/PlaygroundContext';
|
|
7
7
|
import { getStatusColor } from '../utils';
|
|
8
8
|
import JsonTree from '../../JsonTree';
|
|
9
9
|
|
|
10
10
|
export const ResponseViewer: React.FC = () => {
|
|
11
|
-
const { state
|
|
11
|
+
const { state } = usePlaygroundContext();
|
|
12
12
|
const { response } = state;
|
|
13
13
|
|
|
14
|
+
// Memoize response text for copy/download
|
|
15
|
+
const responseText = useMemo(() => {
|
|
16
|
+
if (!response?.data) return '';
|
|
17
|
+
return typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2);
|
|
18
|
+
}, [response?.data]);
|
|
19
|
+
|
|
20
|
+
const handleDownloadResponse = useCallback(() => {
|
|
21
|
+
if (!responseText) return;
|
|
22
|
+
const blob = new Blob([responseText], { type: 'application/json' });
|
|
23
|
+
const url = URL.createObjectURL(blob);
|
|
24
|
+
const a = document.createElement('a');
|
|
25
|
+
a.href = url;
|
|
26
|
+
a.download = 'response.json';
|
|
27
|
+
document.body.appendChild(a);
|
|
28
|
+
a.click();
|
|
29
|
+
document.body.removeChild(a);
|
|
30
|
+
URL.revokeObjectURL(url);
|
|
31
|
+
}, [responseText]);
|
|
32
|
+
|
|
14
33
|
if (!response) {
|
|
15
34
|
return (
|
|
16
35
|
<div className="space-y-4">
|
|
@@ -26,33 +45,14 @@ export const ResponseViewer: React.FC = () => {
|
|
|
26
45
|
);
|
|
27
46
|
}
|
|
28
47
|
|
|
29
|
-
const handleCopyResponse = () => {
|
|
30
|
-
const responseText = typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2);
|
|
31
|
-
copyToClipboard(responseText);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const handleDownloadResponse = () => {
|
|
35
|
-
const responseText = typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2);
|
|
36
|
-
const blob = new Blob([responseText], { type: 'application/json' });
|
|
37
|
-
const url = URL.createObjectURL(blob);
|
|
38
|
-
const a = document.createElement('a');
|
|
39
|
-
a.href = url;
|
|
40
|
-
a.download = 'response.json';
|
|
41
|
-
document.body.appendChild(a);
|
|
42
|
-
a.click();
|
|
43
|
-
document.body.removeChild(a);
|
|
44
|
-
URL.revokeObjectURL(url);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
48
|
return (
|
|
48
49
|
<div className="space-y-4">
|
|
49
50
|
<div className="flex items-center justify-between">
|
|
50
51
|
<h2 className="text-lg font-semibold text-foreground">Response</h2>
|
|
51
52
|
<div className="flex items-center space-x-2">
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
</Button>
|
|
53
|
+
<CopyButton value={responseText} variant="outline" size="sm">
|
|
54
|
+
Copy
|
|
55
|
+
</CopyButton>
|
|
56
56
|
<Button variant="outline" size="sm" onClick={handleDownloadResponse}>
|
|
57
57
|
<Download className="h-4 w-4" />
|
|
58
58
|
<span className="hidden sm:inline">Download</span>
|
|
@@ -5,7 +5,6 @@ import consola from 'consola';
|
|
|
5
5
|
import { type ApiEndpoint, type ApiResponse, type PlaygroundContextType, type PlaygroundState, type PlaygroundStep, type PlaygroundConfig } from '../types';
|
|
6
6
|
import { getDefaultVersion } from '../utils/versionManager';
|
|
7
7
|
import { parseRequestHeaders, substituteUrlParameters } from '../utils';
|
|
8
|
-
import { useCopy } from '@djangocfg/ui-core/hooks';
|
|
9
8
|
|
|
10
9
|
const createInitialState = (): PlaygroundState => ({
|
|
11
10
|
// Step management
|
|
@@ -59,11 +58,6 @@ export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({ children
|
|
|
59
58
|
const apiKeys = React.useMemo(() => [], []);
|
|
60
59
|
const isLoadingApiKeys = false;
|
|
61
60
|
|
|
62
|
-
const { copyToClipboard } = useCopy({
|
|
63
|
-
successMessage: "cURL command copied to clipboard",
|
|
64
|
-
errorMessage: "Failed to copy cURL command"
|
|
65
|
-
});
|
|
66
|
-
|
|
67
61
|
const updateState = (updates: Partial<PlaygroundState>) => {
|
|
68
62
|
setState((prev) => ({ ...prev, ...updates }));
|
|
69
63
|
};
|
|
@@ -330,7 +324,6 @@ export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({ children
|
|
|
330
324
|
|
|
331
325
|
// Actions
|
|
332
326
|
clearAll,
|
|
333
|
-
copyToClipboard,
|
|
334
327
|
sendRequest,
|
|
335
328
|
};
|
|
336
329
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Highlight, Language, themes } from 'prism-react-renderer';
|
|
4
|
-
import React
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { useTheme } from '../../hooks/useTheme';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { CopyButton } from '@djangocfg/ui-core/components';
|
|
6
|
+
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
|
|
8
7
|
|
|
9
8
|
interface PrettyCodeProps {
|
|
10
9
|
data: string | object;
|
|
@@ -16,12 +15,7 @@ interface PrettyCodeProps {
|
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
const PrettyCode = ({ data, language, className, mode, inline = false, customBg }: PrettyCodeProps) => {
|
|
19
|
-
const detectedTheme =
|
|
20
|
-
const [copied, setCopied] = useState(false);
|
|
21
|
-
const { copyToClipboard } = useCopy({
|
|
22
|
-
successMessage: "Code copied to clipboard",
|
|
23
|
-
errorMessage: "Failed to copy code"
|
|
24
|
-
});
|
|
18
|
+
const detectedTheme = useResolvedTheme();
|
|
25
19
|
|
|
26
20
|
// Use provided mode or fall back to detected theme
|
|
27
21
|
const currentTheme = mode || detectedTheme;
|
|
@@ -32,15 +26,6 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg
|
|
|
32
26
|
|
|
33
27
|
// Convert form object to JSON string with proper formatting
|
|
34
28
|
const contentJson = typeof data === 'string' ? data : JSON.stringify(data || {}, null, 2);
|
|
35
|
-
|
|
36
|
-
// Handle copy
|
|
37
|
-
const handleCopy = async () => {
|
|
38
|
-
const success = await copyToClipboard(contentJson);
|
|
39
|
-
if (success) {
|
|
40
|
-
setCopied(true);
|
|
41
|
-
setTimeout(() => setCopied(false), 2000);
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
29
|
|
|
45
30
|
// Handle empty content
|
|
46
31
|
if (!contentJson || contentJson.trim() === '') {
|
|
@@ -173,13 +158,13 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg
|
|
|
173
158
|
<span className="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-background/80 text-muted-foreground border border-border/50 backdrop-blur-sm">
|
|
174
159
|
{displayLanguage}
|
|
175
160
|
</span>
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
161
|
+
<CopyButton
|
|
162
|
+
value={contentJson}
|
|
163
|
+
variant="ghost"
|
|
164
|
+
className="h-7 w-7 bg-background/80 border border-border/50 backdrop-blur-sm"
|
|
165
|
+
iconClassName="h-3.5 w-3.5"
|
|
179
166
|
title="Copy code"
|
|
180
|
-
|
|
181
|
-
{copied ? <Check className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />}
|
|
182
|
-
</button>
|
|
167
|
+
/>
|
|
183
168
|
</div>
|
|
184
169
|
|
|
185
170
|
<div className="h-full overflow-auto">
|
|
@@ -194,20 +194,20 @@ export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(({
|
|
|
194
194
|
<MediaProvider />
|
|
195
195
|
|
|
196
196
|
{/* Poster with proper aspect ratio handling */}
|
|
197
|
-
{
|
|
197
|
+
{posterUrl && (
|
|
198
198
|
<Poster
|
|
199
199
|
className="vds-poster"
|
|
200
|
-
src={
|
|
200
|
+
src={posterUrl}
|
|
201
201
|
alt={source.title || 'Video poster'}
|
|
202
202
|
style={{ objectFit: 'cover' }}
|
|
203
203
|
/>
|
|
204
204
|
)}
|
|
205
|
-
|
|
205
|
+
|
|
206
206
|
{/* Use Vidstack's built-in default layout */}
|
|
207
207
|
{controls && (
|
|
208
208
|
<DefaultVideoLayout
|
|
209
209
|
icons={defaultLayoutIcons}
|
|
210
|
-
thumbnails={
|
|
210
|
+
thumbnails={posterUrl}
|
|
211
211
|
/>
|
|
212
212
|
)}
|
|
213
213
|
</MediaPlayer>
|