@djangocfg/ui-nextjs 1.4.45
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/LICENSE +21 -0
- package/README.md +152 -0
- package/package.json +110 -0
- package/src/animations/AnimatedBackground.tsx +645 -0
- package/src/animations/index.ts +2 -0
- package/src/blocks/ArticleCard.tsx +94 -0
- package/src/blocks/ArticleList.tsx +95 -0
- package/src/blocks/CTASection.tsx +136 -0
- package/src/blocks/FeatureSection.tsx +104 -0
- package/src/blocks/Hero.tsx +102 -0
- package/src/blocks/NewsletterSection.tsx +119 -0
- package/src/blocks/StatsSection.tsx +103 -0
- package/src/blocks/SuperHero.tsx +328 -0
- package/src/blocks/TestimonialSection.tsx +122 -0
- package/src/blocks/index.ts +9 -0
- package/src/components/README.md +2018 -0
- package/src/components/breadcrumb-navigation.tsx +127 -0
- package/src/components/breadcrumb.tsx +132 -0
- package/src/components/button-download.tsx +275 -0
- package/src/components/dropdown-menu.tsx +219 -0
- package/src/components/index.ts +86 -0
- package/src/components/markdown/MarkdownMessage.tsx +338 -0
- package/src/components/markdown/index.ts +5 -0
- package/src/components/menubar.tsx +274 -0
- package/src/components/multi-select-pro/async.tsx +608 -0
- package/src/components/multi-select-pro/helpers.tsx +84 -0
- package/src/components/multi-select-pro/index.tsx +622 -0
- package/src/components/navigation-menu.tsx +153 -0
- package/src/components/pagination-static.tsx +348 -0
- package/src/components/pagination.tsx +138 -0
- package/src/components/phone-input.tsx +276 -0
- package/src/components/sidebar.tsx +866 -0
- package/src/components/sonner.tsx +31 -0
- package/src/components/ssr-pagination.tsx +237 -0
- package/src/hooks/index.ts +19 -0
- package/src/hooks/useCfgRouter.ts +153 -0
- package/src/hooks/useLocalStorage.ts +221 -0
- package/src/hooks/useQueryParams.ts +73 -0
- package/src/hooks/useSessionStorage.ts +188 -0
- package/src/hooks/useTheme.ts +57 -0
- package/src/index.ts +24 -0
- package/src/lib/index.ts +2 -0
- package/src/styles/index.css +2 -0
- package/src/theme/ForceTheme.tsx +115 -0
- package/src/theme/ThemeProvider.tsx +82 -0
- package/src/theme/ThemeToggle.tsx +52 -0
- package/src/theme/index.ts +3 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +199 -0
- package/src/tools/JsonForm/examples/BotConfigExample.tsx +245 -0
- package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +157 -0
- package/src/tools/JsonForm/index.ts +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +73 -0
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +106 -0
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +34 -0
- package/src/tools/JsonForm/templates/FieldTemplate.tsx +61 -0
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +43 -0
- package/src/tools/JsonForm/templates/index.ts +12 -0
- package/src/tools/JsonForm/types.ts +83 -0
- package/src/tools/JsonForm/utils.ts +212 -0
- package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +36 -0
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +88 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +100 -0
- package/src/tools/JsonForm/widgets/SwitchWidget.tsx +34 -0
- package/src/tools/JsonForm/widgets/TextWidget.tsx +95 -0
- package/src/tools/JsonForm/widgets/index.ts +12 -0
- package/src/tools/JsonTree/index.tsx +252 -0
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +212 -0
- package/src/tools/LottiePlayer/index.tsx +54 -0
- package/src/tools/LottiePlayer/types.ts +108 -0
- package/src/tools/LottiePlayer/useLottie.ts +163 -0
- package/src/tools/Mermaid/Mermaid.client.tsx +341 -0
- package/src/tools/Mermaid/index.tsx +40 -0
- package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +144 -0
- package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +255 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +123 -0
- package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +98 -0
- package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +164 -0
- package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
- package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +169 -0
- package/src/tools/OpenapiViewer/components/VersionSelector.tsx +64 -0
- package/src/tools/OpenapiViewer/components/index.ts +14 -0
- package/src/tools/OpenapiViewer/constants.ts +39 -0
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +338 -0
- package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
- package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +203 -0
- package/src/tools/OpenapiViewer/index.tsx +36 -0
- package/src/tools/OpenapiViewer/types.ts +152 -0
- package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
- package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
- package/src/tools/OpenapiViewer/utils/index.ts +9 -0
- package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +217 -0
- package/src/tools/PrettyCode/index.tsx +43 -0
- package/src/tools/VideoPlayer/README.md +239 -0
- package/src/tools/VideoPlayer/VideoControls.tsx +138 -0
- package/src/tools/VideoPlayer/VideoPlayer.tsx +230 -0
- package/src/tools/VideoPlayer/index.ts +9 -0
- package/src/tools/VideoPlayer/types.ts +62 -0
- package/src/tools/index.ts +43 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useCallback } from 'react';
|
|
4
|
+
import { WidgetProps } from '@rjsf/utils';
|
|
5
|
+
import { Input } from '@djangocfg/ui-core/components';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Number input widget for JSON Schema Form
|
|
9
|
+
* Handles integer and number fields
|
|
10
|
+
*/
|
|
11
|
+
export function NumberWidget(props: WidgetProps) {
|
|
12
|
+
const {
|
|
13
|
+
id,
|
|
14
|
+
placeholder,
|
|
15
|
+
required,
|
|
16
|
+
disabled,
|
|
17
|
+
readonly,
|
|
18
|
+
autofocus,
|
|
19
|
+
value,
|
|
20
|
+
onChange,
|
|
21
|
+
onBlur,
|
|
22
|
+
onFocus,
|
|
23
|
+
options,
|
|
24
|
+
schema,
|
|
25
|
+
rawErrors,
|
|
26
|
+
} = props;
|
|
27
|
+
|
|
28
|
+
// Memoize widget configuration
|
|
29
|
+
const config = useMemo(() => ({
|
|
30
|
+
isInteger: schema.type === 'integer',
|
|
31
|
+
step: schema.type === 'integer' ? '1' : 'any',
|
|
32
|
+
min: schema.minimum,
|
|
33
|
+
max: schema.maximum,
|
|
34
|
+
emptyValue: options?.emptyValue,
|
|
35
|
+
}), [schema, options]);
|
|
36
|
+
|
|
37
|
+
// Ensure value is valid number or empty string
|
|
38
|
+
const safeValue = useMemo(() => {
|
|
39
|
+
if (value === null || value === undefined || value === '') return '';
|
|
40
|
+
if (typeof value === 'number' && !isNaN(value)) return value;
|
|
41
|
+
return '';
|
|
42
|
+
}, [value]);
|
|
43
|
+
|
|
44
|
+
const hasError = useMemo(() => {
|
|
45
|
+
return rawErrors && rawErrors.length > 0;
|
|
46
|
+
}, [rawErrors]);
|
|
47
|
+
|
|
48
|
+
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
|
49
|
+
const newValue = event.target.value;
|
|
50
|
+
if (newValue === '') {
|
|
51
|
+
onChange(config.emptyValue);
|
|
52
|
+
} else {
|
|
53
|
+
const parsedValue = config.isInteger
|
|
54
|
+
? parseInt(newValue, 10)
|
|
55
|
+
: parseFloat(newValue);
|
|
56
|
+
|
|
57
|
+
onChange(isNaN(parsedValue) ? config.emptyValue : parsedValue);
|
|
58
|
+
}
|
|
59
|
+
}, [onChange, config]);
|
|
60
|
+
|
|
61
|
+
const handleBlur = useCallback((event: React.FocusEvent<HTMLInputElement>) => {
|
|
62
|
+
onBlur(id, event.target.value);
|
|
63
|
+
}, [id, onBlur]);
|
|
64
|
+
|
|
65
|
+
const handleFocus = useCallback((event: React.FocusEvent<HTMLInputElement>) => {
|
|
66
|
+
onFocus(id, event.target.value);
|
|
67
|
+
}, [id, onFocus]);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Input
|
|
71
|
+
id={id}
|
|
72
|
+
type="number"
|
|
73
|
+
placeholder={placeholder}
|
|
74
|
+
disabled={disabled}
|
|
75
|
+
readOnly={readonly}
|
|
76
|
+
autoFocus={autofocus}
|
|
77
|
+
value={safeValue}
|
|
78
|
+
required={required}
|
|
79
|
+
onChange={handleChange}
|
|
80
|
+
onBlur={handleBlur}
|
|
81
|
+
onFocus={handleFocus}
|
|
82
|
+
step={config.step}
|
|
83
|
+
min={config.min}
|
|
84
|
+
max={config.max}
|
|
85
|
+
className={hasError ? 'border-destructive' : ''}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useCallback } from 'react';
|
|
4
|
+
import { WidgetProps } from '@rjsf/utils';
|
|
5
|
+
import {
|
|
6
|
+
Select,
|
|
7
|
+
SelectContent,
|
|
8
|
+
SelectItem,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
} from '@djangocfg/ui-core/components';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Select dropdown widget for JSON Schema Form
|
|
15
|
+
* Handles enum fields
|
|
16
|
+
*/
|
|
17
|
+
export function SelectWidget(props: WidgetProps) {
|
|
18
|
+
const {
|
|
19
|
+
id,
|
|
20
|
+
options,
|
|
21
|
+
value,
|
|
22
|
+
required,
|
|
23
|
+
disabled,
|
|
24
|
+
readonly,
|
|
25
|
+
autofocus,
|
|
26
|
+
onChange,
|
|
27
|
+
onBlur,
|
|
28
|
+
onFocus,
|
|
29
|
+
placeholder,
|
|
30
|
+
rawErrors,
|
|
31
|
+
} = props;
|
|
32
|
+
|
|
33
|
+
// Safely extract and validate enum options
|
|
34
|
+
const enumOptions = useMemo(() => {
|
|
35
|
+
const opts = options?.enumOptions;
|
|
36
|
+
if (!Array.isArray(opts)) return [];
|
|
37
|
+
return opts.filter(opt => opt && (opt.value !== undefined));
|
|
38
|
+
}, [options]);
|
|
39
|
+
|
|
40
|
+
const hasError = useMemo(() => {
|
|
41
|
+
return rawErrors && rawErrors.length > 0;
|
|
42
|
+
}, [rawErrors]);
|
|
43
|
+
|
|
44
|
+
// Ensure value is always a string
|
|
45
|
+
const safeValue = useMemo(() => {
|
|
46
|
+
if (value === null || value === undefined) return '';
|
|
47
|
+
return String(value);
|
|
48
|
+
}, [value]);
|
|
49
|
+
|
|
50
|
+
const handleChange = useCallback((newValue: string) => {
|
|
51
|
+
onChange(newValue);
|
|
52
|
+
onBlur(id, newValue);
|
|
53
|
+
}, [onChange, onBlur, id]);
|
|
54
|
+
|
|
55
|
+
// If no enum options, render a text input instead
|
|
56
|
+
// This handles cases like anyOf/oneOf where RJSF might incorrectly use SelectWidget
|
|
57
|
+
if (enumOptions.length === 0) {
|
|
58
|
+
return (
|
|
59
|
+
<input
|
|
60
|
+
id={id}
|
|
61
|
+
type="text"
|
|
62
|
+
value={safeValue}
|
|
63
|
+
onChange={(e) => onChange(e.target.value)}
|
|
64
|
+
onBlur={(e) => onBlur(id, e.target.value)}
|
|
65
|
+
onFocus={(e) => onFocus(id, e.target.value)}
|
|
66
|
+
disabled={disabled || readonly}
|
|
67
|
+
readOnly={readonly}
|
|
68
|
+
className={`flex h-10 w-full rounded-md border ${
|
|
69
|
+
hasError ? 'border-destructive' : 'border-input'
|
|
70
|
+
} bg-transparent px-3 py-2 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm`}
|
|
71
|
+
placeholder={placeholder}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Select
|
|
78
|
+
value={safeValue}
|
|
79
|
+
onValueChange={handleChange}
|
|
80
|
+
disabled={disabled || readonly}
|
|
81
|
+
required={required}
|
|
82
|
+
>
|
|
83
|
+
<SelectTrigger
|
|
84
|
+
id={id}
|
|
85
|
+
className={hasError ? 'border-destructive' : ''}
|
|
86
|
+
autoFocus={autofocus}
|
|
87
|
+
onFocus={() => onFocus(id, value)}
|
|
88
|
+
>
|
|
89
|
+
<SelectValue placeholder={placeholder || 'Select an option'} />
|
|
90
|
+
</SelectTrigger>
|
|
91
|
+
<SelectContent>
|
|
92
|
+
{enumOptions.map((option: any) => (
|
|
93
|
+
<SelectItem key={String(option.value)} value={String(option.value)}>
|
|
94
|
+
{option.label || String(option.value)}
|
|
95
|
+
</SelectItem>
|
|
96
|
+
))}
|
|
97
|
+
</SelectContent>
|
|
98
|
+
</Select>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { WidgetProps } from '@rjsf/utils';
|
|
3
|
+
import { Switch } from '@djangocfg/ui-core/components';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Switch toggle widget for JSON Schema Form
|
|
7
|
+
* Alternative boolean input
|
|
8
|
+
*/
|
|
9
|
+
export function SwitchWidget(props: WidgetProps) {
|
|
10
|
+
const {
|
|
11
|
+
id,
|
|
12
|
+
value,
|
|
13
|
+
disabled,
|
|
14
|
+
readonly,
|
|
15
|
+
onChange,
|
|
16
|
+
onBlur,
|
|
17
|
+
onFocus,
|
|
18
|
+
} = props;
|
|
19
|
+
|
|
20
|
+
const handleChange = (checked: boolean) => {
|
|
21
|
+
onChange(checked);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Switch
|
|
26
|
+
id={id}
|
|
27
|
+
checked={value || false}
|
|
28
|
+
disabled={disabled || readonly}
|
|
29
|
+
onCheckedChange={handleChange}
|
|
30
|
+
onBlur={() => onBlur(id, value)}
|
|
31
|
+
onFocus={() => onFocus(id, value)}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useCallback } from 'react';
|
|
4
|
+
import { WidgetProps } from '@rjsf/utils';
|
|
5
|
+
import { Input } from '@djangocfg/ui-core/components';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Text input widget for JSON Schema Form
|
|
9
|
+
* Handles string fields with optional textarea for multiline
|
|
10
|
+
*/
|
|
11
|
+
export function TextWidget(props: WidgetProps) {
|
|
12
|
+
const {
|
|
13
|
+
id,
|
|
14
|
+
placeholder,
|
|
15
|
+
required,
|
|
16
|
+
disabled,
|
|
17
|
+
readonly,
|
|
18
|
+
autofocus,
|
|
19
|
+
value,
|
|
20
|
+
onChange,
|
|
21
|
+
onBlur,
|
|
22
|
+
onFocus,
|
|
23
|
+
options,
|
|
24
|
+
schema,
|
|
25
|
+
rawErrors,
|
|
26
|
+
} = props;
|
|
27
|
+
|
|
28
|
+
// Memoize widget configuration
|
|
29
|
+
const config = useMemo(() => ({
|
|
30
|
+
isTextarea: options?.widget === 'textarea',
|
|
31
|
+
rows: options?.rows || 3,
|
|
32
|
+
emptyValue: options?.emptyValue,
|
|
33
|
+
}), [options]);
|
|
34
|
+
|
|
35
|
+
// Ensure value is always a string
|
|
36
|
+
const safeValue = useMemo(() => {
|
|
37
|
+
if (value === null || value === undefined) return '';
|
|
38
|
+
return String(value);
|
|
39
|
+
}, [value]);
|
|
40
|
+
|
|
41
|
+
const hasError = useMemo(() => {
|
|
42
|
+
return rawErrors && rawErrors.length > 0;
|
|
43
|
+
}, [rawErrors]);
|
|
44
|
+
|
|
45
|
+
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
46
|
+
const newValue = event.target.value;
|
|
47
|
+
onChange(newValue === '' ? config.emptyValue : newValue);
|
|
48
|
+
}, [onChange, config.emptyValue]);
|
|
49
|
+
|
|
50
|
+
const handleBlur = useCallback((event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
51
|
+
onBlur(id, event.target.value);
|
|
52
|
+
}, [id, onBlur]);
|
|
53
|
+
|
|
54
|
+
const handleFocus = useCallback((event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
55
|
+
onFocus(id, event.target.value);
|
|
56
|
+
}, [id, onFocus]);
|
|
57
|
+
|
|
58
|
+
if (config.isTextarea) {
|
|
59
|
+
return (
|
|
60
|
+
<textarea
|
|
61
|
+
id={id}
|
|
62
|
+
className={`flex min-h-[80px] w-full rounded-md border ${
|
|
63
|
+
hasError ? 'border-destructive' : 'border-input'
|
|
64
|
+
} bg-transparent px-3 py-2 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm`}
|
|
65
|
+
placeholder={placeholder}
|
|
66
|
+
disabled={disabled || readonly}
|
|
67
|
+
readOnly={readonly}
|
|
68
|
+
autoFocus={autofocus}
|
|
69
|
+
value={safeValue}
|
|
70
|
+
required={required}
|
|
71
|
+
onChange={handleChange}
|
|
72
|
+
onBlur={handleBlur}
|
|
73
|
+
onFocus={handleFocus}
|
|
74
|
+
rows={config.rows}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Input
|
|
81
|
+
id={id}
|
|
82
|
+
type="text"
|
|
83
|
+
placeholder={placeholder}
|
|
84
|
+
disabled={disabled}
|
|
85
|
+
readOnly={readonly}
|
|
86
|
+
autoFocus={autofocus}
|
|
87
|
+
value={safeValue}
|
|
88
|
+
required={required}
|
|
89
|
+
onChange={handleChange}
|
|
90
|
+
onBlur={handleBlur}
|
|
91
|
+
onFocus={handleFocus}
|
|
92
|
+
className={hasError ? 'border-destructive' : ''}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom widgets for JSON Schema Form
|
|
3
|
+
*
|
|
4
|
+
* Each widget is a React component that renders a specific form input type
|
|
5
|
+
* using components from @djangocfg/ui
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { TextWidget } from './TextWidget';
|
|
9
|
+
export { NumberWidget } from './NumberWidget';
|
|
10
|
+
export { CheckboxWidget } from './CheckboxWidget';
|
|
11
|
+
export { SelectWidget } from './SelectWidget';
|
|
12
|
+
export { SwitchWidget } from './SwitchWidget';
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { CommonExternalProps, JSONTree } from 'react-json-tree';
|
|
5
|
+
import { ChevronDown, ChevronUp, Copy, Download } from 'lucide-react';
|
|
6
|
+
import { Button } from '@djangocfg/ui-core/components';
|
|
7
|
+
import { useCopy } from '@djangocfg/ui-core/hooks';
|
|
8
|
+
|
|
9
|
+
export type { Language } from 'prism-react-renderer';
|
|
10
|
+
|
|
11
|
+
export interface JsonTreeConfig {
|
|
12
|
+
/** Maximum depth to expand automatically (default: 2) */
|
|
13
|
+
maxAutoExpandDepth?: number;
|
|
14
|
+
/** Maximum items in array to auto-expand (default: 10) */
|
|
15
|
+
maxAutoExpandArrayItems?: number;
|
|
16
|
+
/** Maximum object keys to auto-expand (default: 5) */
|
|
17
|
+
maxAutoExpandObjectKeys?: number;
|
|
18
|
+
/** Maximum string length before truncation (default: 200) */
|
|
19
|
+
maxStringLength?: number;
|
|
20
|
+
/** Collection limit for performance (default: 50) */
|
|
21
|
+
collectionLimit?: number;
|
|
22
|
+
/** Whether to show collection info (array length, object keys count) */
|
|
23
|
+
showCollectionInfo?: boolean;
|
|
24
|
+
/** Whether to show expand/collapse all buttons */
|
|
25
|
+
showExpandControls?: boolean;
|
|
26
|
+
/** Whether to show copy/download buttons */
|
|
27
|
+
showActionButtons?: boolean;
|
|
28
|
+
/** Custom CSS classes for the container */
|
|
29
|
+
className?: string;
|
|
30
|
+
/** Whether to preserve object key order (default: true) */
|
|
31
|
+
preserveKeyOrder?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface JsonTreeComponentProps {
|
|
35
|
+
title?: string;
|
|
36
|
+
data: unknown;
|
|
37
|
+
config?: JsonTreeConfig;
|
|
38
|
+
/** Override for react-json-tree props */
|
|
39
|
+
jsonTreeProps?: Partial<CommonExternalProps>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const JsonTreeComponent = ({ title, data, config = {}, jsonTreeProps = {} }: JsonTreeComponentProps) => {
|
|
43
|
+
// State for expand/collapse all
|
|
44
|
+
const [expandAll, setExpandAll] = useState<boolean | null>(null);
|
|
45
|
+
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
|
+
|
|
53
|
+
// Default configuration
|
|
54
|
+
const {
|
|
55
|
+
maxAutoExpandDepth = 2,
|
|
56
|
+
maxAutoExpandArrayItems = 10,
|
|
57
|
+
maxAutoExpandObjectKeys = 5,
|
|
58
|
+
maxStringLength = 200,
|
|
59
|
+
collectionLimit = 50,
|
|
60
|
+
showCollectionInfo = true,
|
|
61
|
+
showExpandControls = true,
|
|
62
|
+
showActionButtons = true,
|
|
63
|
+
className = '',
|
|
64
|
+
preserveKeyOrder = true,
|
|
65
|
+
} = config;
|
|
66
|
+
|
|
67
|
+
// JSON Tree theme optimized for dark theme
|
|
68
|
+
const jsonTreeTheme = {
|
|
69
|
+
scheme: 'djangocfg-dark',
|
|
70
|
+
base00: 'transparent', // Background
|
|
71
|
+
base01: '#1a1a1a', // Lighter background
|
|
72
|
+
base02: '#2a2a2a', // Selection background
|
|
73
|
+
base03: '#6b7280', // Comments, invisibles
|
|
74
|
+
base04: '#9ca3af', // Dark foreground
|
|
75
|
+
base05: '#e5e7eb', // Default foreground
|
|
76
|
+
base06: '#f3f4f6', // Light foreground
|
|
77
|
+
base07: '#ffffff', // Lightest foreground
|
|
78
|
+
base08: '#ef4444', // Red - for null, undefined
|
|
79
|
+
base09: '#f97316', // Orange - for numbers
|
|
80
|
+
base0A: '#eab308', // Yellow - for strings
|
|
81
|
+
base0B: '#22c55e', // Green - for booleans (true)
|
|
82
|
+
base0C: '#06b6d4', // Cyan - for dates, regex
|
|
83
|
+
base0D: '#3b82f6', // Blue - for keys
|
|
84
|
+
base0E: '#a855f7', // Purple - for functions
|
|
85
|
+
base0F: '#f43f5e', // Pink - for deprecations
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Smart expansion logic
|
|
89
|
+
const shouldExpandNodeInitially = (keyPath: readonly (string | number)[], nodeData: unknown, level: number) => {
|
|
90
|
+
// If user explicitly clicked "Expand All", expand everything
|
|
91
|
+
if (expandAll === true) return true;
|
|
92
|
+
|
|
93
|
+
// If user explicitly clicked "Collapse All", collapse everything
|
|
94
|
+
if (expandAll === false) return false;
|
|
95
|
+
|
|
96
|
+
// Default auto-expansion (expandAll === null)
|
|
97
|
+
// Always expand up to maxAutoExpandDepth
|
|
98
|
+
if (level <= maxAutoExpandDepth) return true;
|
|
99
|
+
|
|
100
|
+
// For arrays, expand if they have less than maxAutoExpandArrayItems
|
|
101
|
+
if (Array.isArray(nodeData) && nodeData.length <= maxAutoExpandArrayItems) return true;
|
|
102
|
+
|
|
103
|
+
// For objects, expand if they have less than maxAutoExpandObjectKeys
|
|
104
|
+
if (nodeData && typeof nodeData === 'object' && !Array.isArray(nodeData)) {
|
|
105
|
+
const keys = Object.keys(nodeData);
|
|
106
|
+
return keys.length <= maxAutoExpandObjectKeys;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return false;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Collection info display
|
|
113
|
+
const getItemString = showCollectionInfo
|
|
114
|
+
? (nodeType: string, nodeData: unknown) => {
|
|
115
|
+
if (nodeType === 'Array') {
|
|
116
|
+
const length = Array.isArray(nodeData) ? nodeData.length : 0;
|
|
117
|
+
return length > 0 ? <span className="text-muted-foreground text-sm">({length} items)</span> : null;
|
|
118
|
+
}
|
|
119
|
+
if (nodeType === 'Object') {
|
|
120
|
+
const keys = nodeData && typeof nodeData === 'object' ? Object.keys(nodeData) : [];
|
|
121
|
+
return keys.length > 0 ? <span className="text-muted-foreground text-sm">({keys.length} keys)</span> : null;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
: () => null;
|
|
126
|
+
|
|
127
|
+
// Value processing for better display
|
|
128
|
+
const postprocessValue = (value: unknown) => {
|
|
129
|
+
// Truncate very long strings
|
|
130
|
+
if (typeof value === 'string' && value.length > maxStringLength) {
|
|
131
|
+
return value.substring(0, maxStringLength) + '... (truncated)';
|
|
132
|
+
}
|
|
133
|
+
return value;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Custom node detection for special formatting
|
|
137
|
+
const isCustomNode = (value: unknown) => {
|
|
138
|
+
// Mark URLs as custom nodes for special styling
|
|
139
|
+
if (typeof value === 'string' && (value.startsWith('http://') || value.startsWith('https://'))) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Action handlers
|
|
146
|
+
const handleCopy = () => {
|
|
147
|
+
const jsonString = JSON.stringify(data, null, 2);
|
|
148
|
+
copyToClipboard(jsonString);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handleDownload = () => {
|
|
152
|
+
const jsonString = JSON.stringify(data, null, 2);
|
|
153
|
+
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
154
|
+
const url = URL.createObjectURL(blob);
|
|
155
|
+
const a = document.createElement('a');
|
|
156
|
+
a.href = url;
|
|
157
|
+
a.download = 'data.json';
|
|
158
|
+
document.body.appendChild(a);
|
|
159
|
+
a.click();
|
|
160
|
+
document.body.removeChild(a);
|
|
161
|
+
URL.revokeObjectURL(url);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div className={`relative border border-border rounded-sm h-full overflow-hidden ${className}`}>
|
|
166
|
+
{/* Header with title and controls */}
|
|
167
|
+
{(title || showExpandControls || showActionButtons) && (
|
|
168
|
+
<div className="p-4 border-b border-border bg-muted/50 rounded-t-sm">
|
|
169
|
+
<div className="flex items-center justify-between">
|
|
170
|
+
{title && (
|
|
171
|
+
<h6 className="text-lg font-semibold text-foreground">{title}</h6>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{(showExpandControls || showActionButtons) && (
|
|
175
|
+
<div className="flex items-center space-x-2">
|
|
176
|
+
{/* Expand/Collapse Controls */}
|
|
177
|
+
{showExpandControls && (
|
|
178
|
+
<Button
|
|
179
|
+
variant={expandAll === true ? "default" : "outline"}
|
|
180
|
+
size="sm"
|
|
181
|
+
onClick={() => {
|
|
182
|
+
const newState = expandAll === true ? false : true;
|
|
183
|
+
setExpandAll(newState);
|
|
184
|
+
setRenderKey(prev => prev + 1);
|
|
185
|
+
}}
|
|
186
|
+
className="h-8 px-2"
|
|
187
|
+
>
|
|
188
|
+
{expandAll === true ? (
|
|
189
|
+
<>
|
|
190
|
+
<ChevronUp className="h-3 w-3" />
|
|
191
|
+
<span className="ml-1 text-xs">Collapse All</span>
|
|
192
|
+
</>
|
|
193
|
+
) : (
|
|
194
|
+
<>
|
|
195
|
+
<ChevronDown className="h-3 w-3" />
|
|
196
|
+
<span className="ml-1 text-xs">Expand All</span>
|
|
197
|
+
</>
|
|
198
|
+
)}
|
|
199
|
+
</Button>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{/* Action Buttons */}
|
|
203
|
+
{showActionButtons && (
|
|
204
|
+
<>
|
|
205
|
+
<Button
|
|
206
|
+
variant="outline"
|
|
207
|
+
size="sm"
|
|
208
|
+
onClick={handleCopy}
|
|
209
|
+
className="h-8 px-2"
|
|
210
|
+
>
|
|
211
|
+
<Copy className="h-3 w-3" />
|
|
212
|
+
<span className="ml-1 text-xs hidden sm:inline">Copy</span>
|
|
213
|
+
</Button>
|
|
214
|
+
<Button
|
|
215
|
+
variant="outline"
|
|
216
|
+
size="sm"
|
|
217
|
+
onClick={handleDownload}
|
|
218
|
+
className="h-8 px-2"
|
|
219
|
+
>
|
|
220
|
+
<Download className="h-3 w-3" />
|
|
221
|
+
<span className="ml-1 text-xs hidden sm:inline">Download</span>
|
|
222
|
+
</Button>
|
|
223
|
+
</>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{/* JSON Tree Content */}
|
|
232
|
+
<div className="h-full overflow-auto p-4">
|
|
233
|
+
<JSONTree
|
|
234
|
+
key={renderKey} // Force re-render when expand/collapse state changes
|
|
235
|
+
data={data}
|
|
236
|
+
theme={jsonTreeTheme}
|
|
237
|
+
invertTheme={false}
|
|
238
|
+
hideRoot={true}
|
|
239
|
+
shouldExpandNodeInitially={shouldExpandNodeInitially}
|
|
240
|
+
getItemString={getItemString}
|
|
241
|
+
postprocessValue={postprocessValue}
|
|
242
|
+
isCustomNode={isCustomNode}
|
|
243
|
+
collectionLimit={collectionLimit}
|
|
244
|
+
sortObjectKeys={!preserveKeyOrder}
|
|
245
|
+
{...jsonTreeProps}
|
|
246
|
+
/>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export default JsonTreeComponent;
|