@dbcdk/react-components 0.0.80 → 0.0.82
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/dist/components/json-viewer/HighlightedText.d.ts +5 -0
- package/dist/components/json-viewer/HighlightedText.js +6 -0
- package/dist/components/json-viewer/JsonNode.d.ts +3 -0
- package/dist/components/json-viewer/JsonNode.js +30 -0
- package/dist/components/json-viewer/JsonViewer.d.ts +2 -11
- package/dist/components/json-viewer/JsonViewer.js +6 -166
- package/dist/components/json-viewer/JsonViewer.module.css +6 -6
- package/dist/components/json-viewer/types.d.ts +33 -0
- package/dist/components/json-viewer/types.js +1 -0
- package/dist/components/json-viewer/useClipboardStatus.d.ts +4 -0
- package/dist/components/json-viewer/useClipboardStatus.js +11 -0
- package/dist/components/json-viewer/utils.d.ts +17 -0
- package/dist/components/json-viewer/utils.js +125 -0
- package/dist/components/sidebar/providers/SidebarProvider.js +9 -17
- package/dist/styles/styles.css +0 -5
- package/dist/styles/themes/dbc/dark.css +6 -0
- package/dist/styles/themes/dbc/light.css +6 -0
- package/dist/styles.css +0 -5
- package/package.json +1 -1
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment } from 'react';
|
|
3
|
+
import { getHighlightedSegments } from '../../utils/text/get-highlighted-segments';
|
|
4
|
+
export function HighlightedText({ text, query }) {
|
|
5
|
+
return getHighlightedSegments(text, query).map((segment, index) => segment.matched ? (_jsx("mark", { className: "dbc-highlight", children: segment.text }, `${segment.text}-${index}`)) : (_jsx(Fragment, { children: segment.text }, `${segment.text}-${index}`)));
|
|
6
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check, ChevronDown, ChevronRight, Copy } from 'lucide-react';
|
|
4
|
+
import { capitalize, formatPrimitive, getNodeId, getPathLabel, getPrimitiveRawValue, getSummary, getValueType, isJsonArray, isJsonObject, } from './utils';
|
|
5
|
+
import { HighlightedText } from './HighlightedText';
|
|
6
|
+
import styles from './JsonViewer.module.css';
|
|
7
|
+
export function JsonNode({ keyName, path, value, depth, defaultExpandedDepth, query, matches, expandedNodeIds, onToggleNode, copiedId, onCopy, }) {
|
|
8
|
+
const nodeId = getNodeId(path);
|
|
9
|
+
const nodeType = getValueType(value);
|
|
10
|
+
const match = matches.get(nodeId);
|
|
11
|
+
const shouldRender = query ? Boolean(match) : true;
|
|
12
|
+
const forceOpen = query ? Boolean(match === null || match === void 0 ? void 0 : match.descendant) : false;
|
|
13
|
+
if (!shouldRender)
|
|
14
|
+
return null;
|
|
15
|
+
if (nodeType !== 'array' && nodeType !== 'object') {
|
|
16
|
+
const primitive = value;
|
|
17
|
+
const valueLabel = formatPrimitive(primitive);
|
|
18
|
+
const rawValue = getPrimitiveRawValue(primitive);
|
|
19
|
+
const copied = copiedId === nodeId;
|
|
20
|
+
return (_jsxs("div", { className: styles.node, style: { ['--json-indent']: depth }, children: [_jsx("div", { className: styles.row, "data-match": (match === null || match === void 0 ? void 0 : match.self) || undefined, children: _jsxs("button", { type: "button", className: styles.valueButton, onClick: () => onCopy(nodeId, rawValue), title: copied ? 'Kopieret' : 'Kopiér værdi', children: [_jsxs("span", { className: styles.rowLabel, children: [keyName !== undefined ? (_jsxs("span", { className: styles.keyChunk, children: [_jsx("span", { className: styles.key, children: _jsx(HighlightedText, { text: keyName, query: query }) }), _jsx("span", { className: styles.punctuation, children: ":" })] })) : (_jsx("span", { className: styles.rootLabel, children: "$" })), _jsx("span", { className: [styles.value, styles[`value${capitalize(nodeType)}`]].join(' '), children: _jsx(HighlightedText, { text: valueLabel, query: query }) })] }), _jsxs("span", { className: styles.copyState, "aria-live": "polite", children: [copied ? _jsx(Check, { "aria-hidden": "true" }) : _jsx(Copy, { "aria-hidden": "true" }), _jsx("span", { children: copied ? 'Kopieret' : 'Kopiér' })] })] }) }), query && !(match === null || match === void 0 ? void 0 : match.self) ? (_jsx("div", { className: styles.pathHint, children: _jsx(HighlightedText, { text: getPathLabel(path), query: query }) })) : null] }));
|
|
21
|
+
}
|
|
22
|
+
const entries = isJsonArray(value)
|
|
23
|
+
? value.map((item, index) => [String(index), item])
|
|
24
|
+
: isJsonObject(value)
|
|
25
|
+
? Object.entries(value)
|
|
26
|
+
: [];
|
|
27
|
+
const summary = getSummary(value);
|
|
28
|
+
const expanded = query || forceOpen ? true : expandedNodeIds.has(nodeId);
|
|
29
|
+
return (_jsxs("div", { className: styles.node, style: { ['--json-indent']: depth }, children: [_jsx("div", { className: styles.row, "data-match": (match === null || match === void 0 ? void 0 : match.self) || undefined, children: _jsxs("button", { type: "button", className: styles.toggle, onClick: () => onToggleNode(nodeId), "aria-expanded": expanded, children: [expanded ? _jsx(ChevronDown, { "aria-hidden": "true" }) : _jsx(ChevronRight, { "aria-hidden": "true" }), _jsxs("span", { className: styles.rowLabel, children: [keyName !== undefined ? (_jsxs("span", { className: styles.keyChunk, children: [_jsx("span", { className: styles.key, children: _jsx(HighlightedText, { text: keyName, query: query }) }), _jsx("span", { className: styles.punctuation, children: ":" })] })) : (_jsx("span", { className: styles.rootLabel, children: "$" })), _jsx("span", { className: styles.bracket, children: isJsonArray(value) ? '[' : '{' }), _jsx("span", { className: styles.summary, children: _jsx(HighlightedText, { text: summary, query: query }) }), _jsx("span", { className: styles.bracket, children: isJsonArray(value) ? ']' : '}' })] })] }) }), query && !(match === null || match === void 0 ? void 0 : match.self) ? (_jsx("div", { className: styles.pathHint, children: _jsx(HighlightedText, { text: getPathLabel(path), query: query }) })) : null, expanded ? (_jsx("div", { className: styles.children, role: depth === 0 ? 'group' : undefined, children: entries.map(([childKey, childValue]) => (_jsx(JsonNode, { keyName: isJsonArray(value) ? `[${childKey}]` : childKey, path: [...path, childKey], value: childValue, depth: depth + 1, defaultExpandedDepth: defaultExpandedDepth, query: query, matches: matches, expandedNodeIds: expandedNodeIds, onToggleNode: onToggleNode, copiedId: copiedId, onCopy: onCopy }, `${nodeId}-${childKey}`))) })) : null] }));
|
|
30
|
+
}
|
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
import type { JSX } from 'react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
defaultExpandedDepth?: number;
|
|
5
|
-
expandAll?: boolean;
|
|
6
|
-
searchDebounceMs?: number;
|
|
7
|
-
helperText?: string;
|
|
8
|
-
searchPlaceholder?: string;
|
|
9
|
-
emptySearchText?: string;
|
|
10
|
-
className?: string;
|
|
11
|
-
maxHeight?: number | string;
|
|
12
|
-
}
|
|
2
|
+
import type { JsonViewerProps } from './types';
|
|
3
|
+
export type { JsonViewerProps } from './types';
|
|
13
4
|
export declare function JsonViewer({ value, defaultExpandedDepth, expandAll, searchDebounceMs, helperText, searchPlaceholder, emptySearchText, className, maxHeight, }: JsonViewerProps): JSX.Element;
|
|
@@ -1,145 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { Search, X } from 'lucide-react';
|
|
4
|
+
import { useEffect, useId, useMemo, useState } from 'react';
|
|
5
|
+
import { JsonNode } from './JsonNode';
|
|
6
6
|
import styles from './JsonViewer.module.css';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
function isJsonObject(value) {
|
|
11
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
12
|
-
}
|
|
13
|
-
function getValueType(value) {
|
|
14
|
-
if (value === null)
|
|
15
|
-
return 'null';
|
|
16
|
-
if (Array.isArray(value))
|
|
17
|
-
return 'array';
|
|
18
|
-
if (typeof value === 'object')
|
|
19
|
-
return 'object';
|
|
20
|
-
if (typeof value === 'string')
|
|
21
|
-
return 'string';
|
|
22
|
-
if (typeof value === 'number')
|
|
23
|
-
return 'number';
|
|
24
|
-
if (typeof value === 'boolean')
|
|
25
|
-
return 'boolean';
|
|
26
|
-
return 'string';
|
|
27
|
-
}
|
|
28
|
-
function formatPrimitive(value) {
|
|
29
|
-
if (value === null)
|
|
30
|
-
return 'null';
|
|
31
|
-
if (typeof value === 'string')
|
|
32
|
-
return JSON.stringify(value);
|
|
33
|
-
return String(value);
|
|
34
|
-
}
|
|
35
|
-
function getPrimitiveRawValue(value) {
|
|
36
|
-
if (value === null)
|
|
37
|
-
return 'null';
|
|
38
|
-
return String(value);
|
|
39
|
-
}
|
|
40
|
-
function getSummary(value) {
|
|
41
|
-
if (isJsonArray(value)) {
|
|
42
|
-
return `${value.length} element${value.length === 1 ? '' : 'er'}`;
|
|
43
|
-
}
|
|
44
|
-
if (!isJsonObject(value))
|
|
45
|
-
return '0 felter';
|
|
46
|
-
const count = Object.keys(value).length;
|
|
47
|
-
return `${count} felt${count === 1 ? '' : 'er'}`;
|
|
48
|
-
}
|
|
49
|
-
function getNodeId(path) {
|
|
50
|
-
return path.length === 0 ? '$' : `$.${path.join('.')}`;
|
|
51
|
-
}
|
|
52
|
-
function getPathLabel(path) {
|
|
53
|
-
return getNodeId(path);
|
|
54
|
-
}
|
|
55
|
-
function normalizeSearch(value) {
|
|
56
|
-
return value.trim().toLowerCase();
|
|
57
|
-
}
|
|
58
|
-
function valueToSearchString(value) {
|
|
59
|
-
return value === null ? 'null' : String(value).toLowerCase();
|
|
60
|
-
}
|
|
61
|
-
function collectSearchMatches(value, query, path = [], key) {
|
|
62
|
-
const matches = new Map();
|
|
63
|
-
const visit = (current, currentPath, currentKey) => {
|
|
64
|
-
const nodeId = getNodeId(currentPath);
|
|
65
|
-
const pathLabel = getPathLabel(currentPath).toLowerCase();
|
|
66
|
-
const keyLabel = (currentKey !== null && currentKey !== void 0 ? currentKey : '').toLowerCase();
|
|
67
|
-
let self = keyLabel.includes(query) || pathLabel.includes(query);
|
|
68
|
-
let descendant = false;
|
|
69
|
-
if (isJsonArray(current)) {
|
|
70
|
-
current.forEach((item, index) => {
|
|
71
|
-
const childMatches = visit(item, [...currentPath, String(index)], String(index));
|
|
72
|
-
descendant = descendant || childMatches;
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
else if (isJsonObject(current)) {
|
|
76
|
-
Object.entries(current).forEach(([childKey, childValue]) => {
|
|
77
|
-
const childMatches = visit(childValue, [...currentPath, childKey], childKey);
|
|
78
|
-
descendant = descendant || childMatches;
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
self = self || valueToSearchString(current).includes(query);
|
|
83
|
-
}
|
|
84
|
-
const any = self || descendant;
|
|
85
|
-
if (any) {
|
|
86
|
-
matches.set(nodeId, { self, descendant });
|
|
87
|
-
}
|
|
88
|
-
return any;
|
|
89
|
-
};
|
|
90
|
-
visit(value, path, key);
|
|
91
|
-
return matches;
|
|
92
|
-
}
|
|
93
|
-
function HighlightText({ text, query }) {
|
|
94
|
-
return getHighlightedSegments(text, query).map((segment, index) => segment.matched ? (_jsx("mark", { className: "dbc-highlight", children: segment.text }, `${segment.text}-${index}`)) : (_jsx(Fragment, { children: segment.text }, `${segment.text}-${index}`)));
|
|
95
|
-
}
|
|
96
|
-
function useClipboardStatus() {
|
|
97
|
-
const [copiedId, setCopiedId] = useState(null);
|
|
98
|
-
useEffect(() => {
|
|
99
|
-
if (!copiedId)
|
|
100
|
-
return;
|
|
101
|
-
const timer = window.setTimeout(() => setCopiedId(null), 1400);
|
|
102
|
-
return () => window.clearTimeout(timer);
|
|
103
|
-
}, [copiedId]);
|
|
104
|
-
return { copiedId, setCopiedId };
|
|
105
|
-
}
|
|
106
|
-
function isJsonValue(value) {
|
|
107
|
-
if (value === null)
|
|
108
|
-
return true;
|
|
109
|
-
if (Array.isArray(value))
|
|
110
|
-
return value.every(isJsonValue);
|
|
111
|
-
if (typeof value === 'object') {
|
|
112
|
-
return Object.values(value).every(isJsonValue);
|
|
113
|
-
}
|
|
114
|
-
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
|
|
115
|
-
}
|
|
116
|
-
function collectExpandableNodeIds(value, path = []) {
|
|
117
|
-
if (!isJsonArray(value) && !isJsonObject(value))
|
|
118
|
-
return [];
|
|
119
|
-
const nodeId = getNodeId(path);
|
|
120
|
-
const childEntries = isJsonArray(value)
|
|
121
|
-
? value.map((item, index) => [String(index), item])
|
|
122
|
-
: Object.entries(value);
|
|
123
|
-
return [
|
|
124
|
-
nodeId,
|
|
125
|
-
...childEntries.flatMap(([childKey, childValue]) => collectExpandableNodeIds(childValue, [...path, childKey])),
|
|
126
|
-
];
|
|
127
|
-
}
|
|
128
|
-
function collectInitiallyExpandedNodeIds(value, defaultExpandedDepth, expandAll, depth = 0, path = []) {
|
|
129
|
-
if (!isJsonArray(value) && !isJsonObject(value))
|
|
130
|
-
return [];
|
|
131
|
-
const nodeId = getNodeId(path);
|
|
132
|
-
const childEntries = isJsonArray(value)
|
|
133
|
-
? value.map((item, index) => [String(index), item])
|
|
134
|
-
: Object.entries(value);
|
|
135
|
-
const includeNode = expandAll || depth < defaultExpandedDepth;
|
|
136
|
-
const nested = childEntries.flatMap(([childKey, childValue]) => collectInitiallyExpandedNodeIds(childValue, defaultExpandedDepth, expandAll, depth + 1, [
|
|
137
|
-
...path,
|
|
138
|
-
childKey,
|
|
139
|
-
]));
|
|
140
|
-
return includeNode ? [nodeId, ...nested] : nested;
|
|
141
|
-
}
|
|
142
|
-
export function JsonViewer({ value, defaultExpandedDepth = 2, expandAll = false, searchDebounceMs = 180, helperText, searchPlaceholder = 'Søg i nøgler, stier og værdier', emptySearchText = 'Ingen matchende noder fundet.', className, maxHeight, }) {
|
|
7
|
+
import { useClipboardStatus } from './useClipboardStatus';
|
|
8
|
+
import { collectExpandableNodeIds, collectInitiallyExpandedNodeIds, collectSearchMatches, isJsonValue, normalizeSearch, } from './utils';
|
|
9
|
+
export function JsonViewer({ value, defaultExpandedDepth = 2, expandAll = false, searchDebounceMs = 800, helperText, searchPlaceholder = 'Søg i nøgler, stier og værdier', emptySearchText = 'Ingen matchende noder fundet.', className, maxHeight, }) {
|
|
143
10
|
const [queryInput, setQueryInput] = useState('');
|
|
144
11
|
const [query, setQuery] = useState('');
|
|
145
12
|
const normalizedQuery = normalizeSearch(query);
|
|
@@ -199,30 +66,3 @@ export function JsonViewer({ value, defaultExpandedDepth = 2, expandAll = false,
|
|
|
199
66
|
? { maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight }
|
|
200
67
|
: undefined, children: hasResults ? (_jsx(JsonNode, { keyName: undefined, path: [], value: safeValue, depth: 0, defaultExpandedDepth: defaultExpandedDepth, query: normalizedQuery, matches: matches, expandedNodeIds: expandedNodeIds, onToggleNode: handleToggleNode, copiedId: copiedId, onCopy: handleCopy })) : (_jsx("div", { className: styles.emptyState, children: emptySearchText })) })] }));
|
|
201
68
|
}
|
|
202
|
-
function JsonNode({ keyName, path, value, depth, defaultExpandedDepth, query, matches, expandedNodeIds, onToggleNode, copiedId, onCopy, }) {
|
|
203
|
-
const nodeId = getNodeId(path);
|
|
204
|
-
const nodeType = getValueType(value);
|
|
205
|
-
const match = matches.get(nodeId);
|
|
206
|
-
const shouldRender = query ? Boolean(match) : true;
|
|
207
|
-
const forceOpen = query ? Boolean(match === null || match === void 0 ? void 0 : match.descendant) : false;
|
|
208
|
-
if (!shouldRender)
|
|
209
|
-
return null;
|
|
210
|
-
if (nodeType !== 'array' && nodeType !== 'object') {
|
|
211
|
-
const primitive = value;
|
|
212
|
-
const valueLabel = formatPrimitive(primitive);
|
|
213
|
-
const rawValue = getPrimitiveRawValue(primitive);
|
|
214
|
-
const copied = copiedId === nodeId;
|
|
215
|
-
return (_jsxs("div", { className: styles.node, style: { ['--json-indent']: depth }, children: [_jsx("div", { className: styles.row, "data-match": (match === null || match === void 0 ? void 0 : match.self) || undefined, children: _jsxs("button", { type: "button", className: styles.valueButton, onClick: () => onCopy(nodeId, rawValue), title: copied ? 'Kopieret' : 'Kopiér værdi', children: [_jsxs("span", { className: styles.rowLabel, children: [keyName !== undefined ? (_jsxs("span", { className: styles.keyChunk, children: [_jsx("span", { className: styles.key, children: _jsx(HighlightText, { text: keyName, query: query }) }), _jsx("span", { className: styles.punctuation, children: ":" })] })) : (_jsx("span", { className: styles.rootLabel, children: "$" })), _jsx("span", { className: [styles.value, styles[`value${capitalize(nodeType)}`]].join(' '), children: _jsx(HighlightText, { text: valueLabel, query: query }) })] }), _jsxs("span", { className: styles.copyState, "aria-live": "polite", children: [copied ? _jsx(Check, { "aria-hidden": "true" }) : _jsx(Copy, { "aria-hidden": "true" }), _jsx("span", { children: copied ? 'Kopieret' : 'Kopiér' })] })] }) }), query && !(match === null || match === void 0 ? void 0 : match.self) ? (_jsx("div", { className: styles.pathHint, children: _jsx(HighlightText, { text: getPathLabel(path), query: query }) })) : null] }));
|
|
216
|
-
}
|
|
217
|
-
const entries = isJsonArray(value)
|
|
218
|
-
? value.map((item, index) => [String(index), item])
|
|
219
|
-
: isJsonObject(value)
|
|
220
|
-
? Object.entries(value)
|
|
221
|
-
: [];
|
|
222
|
-
const summary = getSummary(value);
|
|
223
|
-
const expanded = query || forceOpen ? true : expandedNodeIds.has(nodeId);
|
|
224
|
-
return (_jsxs("div", { className: styles.node, style: { ['--json-indent']: depth }, children: [_jsx("div", { className: styles.row, "data-match": (match === null || match === void 0 ? void 0 : match.self) || undefined, children: _jsxs("button", { type: "button", className: styles.toggle, onClick: () => onToggleNode(nodeId), "aria-expanded": expanded, children: [expanded ? _jsx(ChevronDown, { "aria-hidden": "true" }) : _jsx(ChevronRight, { "aria-hidden": "true" }), _jsxs("span", { className: styles.rowLabel, children: [keyName !== undefined ? (_jsxs("span", { className: styles.keyChunk, children: [_jsx("span", { className: styles.key, children: _jsx(HighlightText, { text: keyName, query: query }) }), _jsx("span", { className: styles.punctuation, children: ":" })] })) : (_jsx("span", { className: styles.rootLabel, children: "$" })), _jsx("span", { className: styles.bracket, children: isJsonArray(value) ? '[' : '{' }), _jsx("span", { className: styles.summary, children: _jsx(HighlightText, { text: summary, query: query }) }), _jsx("span", { className: styles.bracket, children: isJsonArray(value) ? ']' : '}' })] })] }) }), query && !(match === null || match === void 0 ? void 0 : match.self) ? (_jsx("div", { className: styles.pathHint, children: _jsx(HighlightText, { text: getPathLabel(path), query: query }) })) : null, expanded ? (_jsx("div", { className: styles.children, role: depth === 0 ? 'group' : undefined, children: entries.map(([childKey, childValue]) => (_jsx(JsonNode, { keyName: isJsonArray(value) ? `[${childKey}]` : childKey, path: [...path, childKey], value: childValue, depth: depth + 1, defaultExpandedDepth: defaultExpandedDepth, query: query, matches: matches, expandedNodeIds: expandedNodeIds, onToggleNode: onToggleNode, copiedId: copiedId, onCopy: onCopy }, `${nodeId}-${childKey}`))) })) : null] }));
|
|
225
|
-
}
|
|
226
|
-
function capitalize(value) {
|
|
227
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
228
|
-
}
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
--json-pane-subtle: color-mix(in oklab, var(--color-fg-inverse) 44%, transparent);
|
|
8
8
|
--json-pane-hover: color-mix(in oklab, var(--color-fg-inverse) 8%, transparent);
|
|
9
9
|
--json-pane-selected: color-mix(in oklab, var(--color-brand) 24%, transparent);
|
|
10
|
-
--json-pane-highlight: color-mix(in oklab, var(--color-brand) 68%, var(--
|
|
10
|
+
--json-pane-highlight: color-mix(in oklab, var(--color-brand) 68%, var(--color-syntax-accent) 32%);
|
|
11
11
|
--color-highlight-bg: color-mix(in oklab, var(--json-pane-highlight) 58%, transparent);
|
|
12
12
|
--color-highlight-fg: var(--color-fg-inverse);
|
|
13
|
-
--json-pane-key: color-mix(in oklab, var(--color-fg-inverse) 88%, var(--
|
|
14
|
-
--json-pane-string: color-mix(in oklab, var(--
|
|
15
|
-
--json-pane-number: color-mix(in oklab, var(--
|
|
16
|
-
--json-pane-boolean: color-mix(in oklab, var(--
|
|
13
|
+
--json-pane-key: color-mix(in oklab, var(--color-fg-inverse) 88%, var(--color-syntax-accent) 12%);
|
|
14
|
+
--json-pane-string: color-mix(in oklab, var(--color-syntax-string) 78%, white 22%);
|
|
15
|
+
--json-pane-number: color-mix(in oklab, var(--color-syntax-number) 76%, white 24%);
|
|
16
|
+
--json-pane-boolean: color-mix(in oklab, var(--color-syntax-boolean) 74%, white 26%);
|
|
17
17
|
--json-pane-null: color-mix(in oklab, var(--color-fg-inverse) 54%, transparent);
|
|
18
18
|
|
|
19
19
|
display: flex;
|
|
@@ -313,7 +313,7 @@
|
|
|
313
313
|
.valueButton[title='Kopieret'] .copyState {
|
|
314
314
|
opacity: 1;
|
|
315
315
|
transform: translateX(0);
|
|
316
|
-
color: color-mix(in oklab, var(--
|
|
316
|
+
color: color-mix(in oklab, var(--color-syntax-string) 78%, white 22%);
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
.emptyState {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
2
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
3
|
+
[key: string]: JsonValue;
|
|
4
|
+
};
|
|
5
|
+
export type PrimitiveKind = 'string' | 'number' | 'boolean' | 'null';
|
|
6
|
+
export type SearchMatch = {
|
|
7
|
+
self: boolean;
|
|
8
|
+
descendant: boolean;
|
|
9
|
+
};
|
|
10
|
+
export interface JsonViewerProps {
|
|
11
|
+
value: unknown;
|
|
12
|
+
defaultExpandedDepth?: number;
|
|
13
|
+
expandAll?: boolean;
|
|
14
|
+
searchDebounceMs?: number;
|
|
15
|
+
helperText?: string;
|
|
16
|
+
searchPlaceholder?: string;
|
|
17
|
+
emptySearchText?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
maxHeight?: number | string;
|
|
20
|
+
}
|
|
21
|
+
export type JsonNodeProps = {
|
|
22
|
+
keyName?: string;
|
|
23
|
+
path: string[];
|
|
24
|
+
value: JsonValue;
|
|
25
|
+
depth: number;
|
|
26
|
+
defaultExpandedDepth: number;
|
|
27
|
+
query: string;
|
|
28
|
+
matches: Map<string, SearchMatch>;
|
|
29
|
+
expandedNodeIds: Set<string>;
|
|
30
|
+
onToggleNode: (nodeId: string) => void;
|
|
31
|
+
copiedId: string | null;
|
|
32
|
+
onCopy: (nodeId: string, rawValue: string) => void;
|
|
33
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
export function useClipboardStatus() {
|
|
3
|
+
const [copiedId, setCopiedId] = useState(null);
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
if (!copiedId)
|
|
6
|
+
return;
|
|
7
|
+
const timer = window.setTimeout(() => setCopiedId(null), 1400);
|
|
8
|
+
return () => window.clearTimeout(timer);
|
|
9
|
+
}, [copiedId]);
|
|
10
|
+
return { copiedId, setCopiedId };
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { JsonValue, JsonPrimitive, PrimitiveKind, SearchMatch } from './types';
|
|
2
|
+
export declare function isJsonArray(value: JsonValue): value is JsonValue[];
|
|
3
|
+
export declare function isJsonObject(value: JsonValue): value is {
|
|
4
|
+
[key: string]: JsonValue;
|
|
5
|
+
};
|
|
6
|
+
export declare function isJsonValue(value: unknown): value is JsonValue;
|
|
7
|
+
export declare function getValueType(value: unknown): PrimitiveKind | 'array' | 'object';
|
|
8
|
+
export declare function formatPrimitive(value: JsonPrimitive): string;
|
|
9
|
+
export declare function getPrimitiveRawValue(value: JsonPrimitive): string;
|
|
10
|
+
export declare function getSummary(value: JsonValue): string;
|
|
11
|
+
export declare function getNodeId(path: string[]): string;
|
|
12
|
+
export declare function getPathLabel(path: string[]): string;
|
|
13
|
+
export declare function normalizeSearch(value: string): string;
|
|
14
|
+
export declare function capitalize(value: string): string;
|
|
15
|
+
export declare function collectSearchMatches(value: JsonValue, query: string, path?: string[], key?: string): Map<string, SearchMatch>;
|
|
16
|
+
export declare function collectExpandableNodeIds(value: JsonValue, path?: string[]): string[];
|
|
17
|
+
export declare function collectInitiallyExpandedNodeIds(value: JsonValue, defaultExpandedDepth: number, expandAll: boolean, depth?: number, path?: string[]): string[];
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export function isJsonArray(value) {
|
|
2
|
+
return Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
export function isJsonObject(value) {
|
|
5
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
export function isJsonValue(value) {
|
|
8
|
+
if (value === null)
|
|
9
|
+
return true;
|
|
10
|
+
if (Array.isArray(value))
|
|
11
|
+
return value.every(isJsonValue);
|
|
12
|
+
if (typeof value === 'object') {
|
|
13
|
+
return Object.values(value).every(isJsonValue);
|
|
14
|
+
}
|
|
15
|
+
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
|
|
16
|
+
}
|
|
17
|
+
export function getValueType(value) {
|
|
18
|
+
if (value === null)
|
|
19
|
+
return 'null';
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return 'array';
|
|
22
|
+
if (typeof value === 'object')
|
|
23
|
+
return 'object';
|
|
24
|
+
if (typeof value === 'string')
|
|
25
|
+
return 'string';
|
|
26
|
+
if (typeof value === 'number')
|
|
27
|
+
return 'number';
|
|
28
|
+
if (typeof value === 'boolean')
|
|
29
|
+
return 'boolean';
|
|
30
|
+
return 'string';
|
|
31
|
+
}
|
|
32
|
+
export function formatPrimitive(value) {
|
|
33
|
+
if (value === null)
|
|
34
|
+
return 'null';
|
|
35
|
+
if (typeof value === 'string')
|
|
36
|
+
return JSON.stringify(value);
|
|
37
|
+
return String(value);
|
|
38
|
+
}
|
|
39
|
+
export function getPrimitiveRawValue(value) {
|
|
40
|
+
if (value === null)
|
|
41
|
+
return 'null';
|
|
42
|
+
return String(value);
|
|
43
|
+
}
|
|
44
|
+
export function getSummary(value) {
|
|
45
|
+
if (isJsonArray(value)) {
|
|
46
|
+
return `${value.length} element${value.length === 1 ? '' : 'er'}`;
|
|
47
|
+
}
|
|
48
|
+
if (!isJsonObject(value))
|
|
49
|
+
return '0 felter';
|
|
50
|
+
const count = Object.keys(value).length;
|
|
51
|
+
return `${count} felt${count === 1 ? '' : 'er'}`;
|
|
52
|
+
}
|
|
53
|
+
export function getNodeId(path) {
|
|
54
|
+
return path.length === 0 ? '$' : `$.${path.join('.')}`;
|
|
55
|
+
}
|
|
56
|
+
export function getPathLabel(path) {
|
|
57
|
+
return getNodeId(path);
|
|
58
|
+
}
|
|
59
|
+
export function normalizeSearch(value) {
|
|
60
|
+
return value.trim().toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
export function capitalize(value) {
|
|
63
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
64
|
+
}
|
|
65
|
+
function valueToSearchString(value) {
|
|
66
|
+
return value === null ? 'null' : String(value).toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
export function collectSearchMatches(value, query, path = [], key) {
|
|
69
|
+
const matches = new Map();
|
|
70
|
+
const visit = (current, currentPath, currentKey) => {
|
|
71
|
+
const nodeId = getNodeId(currentPath);
|
|
72
|
+
const pathLabel = getPathLabel(currentPath).toLowerCase();
|
|
73
|
+
const keyLabel = (currentKey !== null && currentKey !== void 0 ? currentKey : '').toLowerCase();
|
|
74
|
+
let self = keyLabel.includes(query) || pathLabel.includes(query);
|
|
75
|
+
let descendant = false;
|
|
76
|
+
if (isJsonArray(current)) {
|
|
77
|
+
current.forEach((item, index) => {
|
|
78
|
+
const childMatches = visit(item, [...currentPath, String(index)], String(index));
|
|
79
|
+
descendant = descendant || childMatches;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else if (isJsonObject(current)) {
|
|
83
|
+
Object.entries(current).forEach(([childKey, childValue]) => {
|
|
84
|
+
const childMatches = visit(childValue, [...currentPath, childKey], childKey);
|
|
85
|
+
descendant = descendant || childMatches;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
self = self || valueToSearchString(current).includes(query);
|
|
90
|
+
}
|
|
91
|
+
const any = self || descendant;
|
|
92
|
+
if (any) {
|
|
93
|
+
matches.set(nodeId, { self, descendant });
|
|
94
|
+
}
|
|
95
|
+
return any;
|
|
96
|
+
};
|
|
97
|
+
visit(value, path, key);
|
|
98
|
+
return matches;
|
|
99
|
+
}
|
|
100
|
+
export function collectExpandableNodeIds(value, path = []) {
|
|
101
|
+
if (!isJsonArray(value) && !isJsonObject(value))
|
|
102
|
+
return [];
|
|
103
|
+
const nodeId = getNodeId(path);
|
|
104
|
+
const childEntries = isJsonArray(value)
|
|
105
|
+
? value.map((item, index) => [String(index), item])
|
|
106
|
+
: Object.entries(value);
|
|
107
|
+
return [
|
|
108
|
+
nodeId,
|
|
109
|
+
...childEntries.flatMap(([childKey, childValue]) => collectExpandableNodeIds(childValue, [...path, childKey])),
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
export function collectInitiallyExpandedNodeIds(value, defaultExpandedDepth, expandAll, depth = 0, path = []) {
|
|
113
|
+
if (!isJsonArray(value) && !isJsonObject(value))
|
|
114
|
+
return [];
|
|
115
|
+
const nodeId = getNodeId(path);
|
|
116
|
+
const childEntries = isJsonArray(value)
|
|
117
|
+
? value.map((item, index) => [String(index), item])
|
|
118
|
+
: Object.entries(value);
|
|
119
|
+
const includeNode = expandAll || depth < defaultExpandedDepth;
|
|
120
|
+
const nested = childEntries.flatMap(([childKey, childValue]) => collectInitiallyExpandedNodeIds(childValue, defaultExpandedDepth, expandAll, depth + 1, [
|
|
121
|
+
...path,
|
|
122
|
+
childKey,
|
|
123
|
+
]));
|
|
124
|
+
return includeNode ? [nodeId, ...nested] : nested;
|
|
125
|
+
}
|
|
@@ -2,13 +2,6 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { nestedFiltering } from '../../../utils/arrays/nested-filtering';
|
|
5
|
-
/**
|
|
6
|
-
* Production notes:
|
|
7
|
-
* - No console logging.
|
|
8
|
-
* - Auto-expands the correct expandable chain for the active link (including when the active link
|
|
9
|
-
* points to the expandable parent itself).
|
|
10
|
-
* - Normalizes hrefs (trailing slashes) so comparisons are stable.
|
|
11
|
-
*/
|
|
12
5
|
const hasChildren = (item) => Array.isArray(item.children) && item.children.length > 0;
|
|
13
6
|
const hasHref = (item) => typeof item.href === 'string' && item.href.length > 0;
|
|
14
7
|
const normalizeHref = (href) => {
|
|
@@ -86,24 +79,23 @@ export function SidebarProvider({ children, items, initialQuery, initialCollapse
|
|
|
86
79
|
useEffect(() => {
|
|
87
80
|
itemsRef.current = items;
|
|
88
81
|
}, [items]);
|
|
89
|
-
const [isSidebarCollapsed, setSidebarCollapsed] = useState(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (initialSidebarCollapsed !== undefined)
|
|
93
|
-
return
|
|
94
|
-
}
|
|
82
|
+
const [isSidebarCollapsed, setSidebarCollapsed] = useState(initialSidebarCollapsed !== null && initialSidebarCollapsed !== void 0 ? initialSidebarCollapsed : false);
|
|
83
|
+
// Runs once after hydration — safe to read localStorage and window.innerWidth here.
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (initialSidebarCollapsed !== undefined)
|
|
86
|
+
return;
|
|
95
87
|
try {
|
|
96
88
|
const stored = window.localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY);
|
|
97
89
|
if (stored !== null) {
|
|
98
|
-
|
|
99
|
-
return
|
|
90
|
+
setSidebarCollapsed(Boolean(JSON.parse(stored))); // eslint-disable-line react-hooks/set-state-in-effect -- intentional: SSR-safe initial read
|
|
91
|
+
return;
|
|
100
92
|
}
|
|
101
93
|
}
|
|
102
94
|
catch {
|
|
103
95
|
// ignore parse failures
|
|
104
96
|
}
|
|
105
|
-
|
|
106
|
-
});
|
|
97
|
+
setSidebarCollapsed(getBreakpoint(window.innerWidth) === 'small');
|
|
98
|
+
}, []); // intentionally empty — only runs once after first mount
|
|
107
99
|
const triggerExpandAll = useCallback(() => setDefaultExpanded(true), []);
|
|
108
100
|
const resetExpandAll = useCallback(() => setDefaultExpanded(null), []);
|
|
109
101
|
const setActiveLink = useCallback((href) => setActiveHref(href), []);
|
package/dist/styles/styles.css
CHANGED
|
@@ -137,6 +137,12 @@ html[data-theme='dark'] {
|
|
|
137
137
|
--color-focus-ring: var(--dbc-blue-300);
|
|
138
138
|
--focus-ring: 0 0 0 3px rgba(59, 130, 246, 0.35);
|
|
139
139
|
|
|
140
|
+
/* Syntax (JSON viewer) */
|
|
141
|
+
--color-syntax-accent: var(--dbc-blue-300);
|
|
142
|
+
--color-syntax-string: var(--dbc-green-300);
|
|
143
|
+
--color-syntax-number: var(--dbc-amber-400);
|
|
144
|
+
--color-syntax-boolean: var(--dbc-pink-500);
|
|
145
|
+
|
|
140
146
|
/* ==========================================================================
|
|
141
147
|
* COMPONENT-LEVEL TOKENS (THEME-DEPENDENT COLORS)
|
|
142
148
|
* ======================================================================= */
|
|
@@ -137,6 +137,12 @@ html[data-theme='light'] {
|
|
|
137
137
|
--color-focus-ring: var(--dbc-blue-300);
|
|
138
138
|
--focus-ring: 0 0 0 3px rgba(12, 62, 227, 0.25);
|
|
139
139
|
|
|
140
|
+
/* Syntax (JSON viewer) */
|
|
141
|
+
--color-syntax-accent: var(--dbc-blue-300);
|
|
142
|
+
--color-syntax-string: var(--dbc-green-300);
|
|
143
|
+
--color-syntax-number: var(--dbc-amber-400);
|
|
144
|
+
--color-syntax-boolean: var(--dbc-pink-500);
|
|
145
|
+
|
|
140
146
|
/* ==========================================================================
|
|
141
147
|
* COMPONENT-LEVEL TOKENS (THEME-DEPENDENT COLORS)
|
|
142
148
|
* ======================================================================= */
|
package/dist/styles.css
CHANGED