@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.
@@ -0,0 +1,5 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function HighlightedText({ text, query }: {
3
+ text: string;
4
+ query: string;
5
+ }): ReactNode;
@@ -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,3 @@
1
+ import type { JSX } from 'react';
2
+ import type { JsonNodeProps } from './types';
3
+ export declare function JsonNode({ keyName, path, value, depth, defaultExpandedDepth, query, matches, expandedNodeIds, onToggleNode, copiedId, onCopy, }: JsonNodeProps): JSX.Element | null;
@@ -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
- export interface JsonViewerProps {
3
- value: unknown;
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 { Check, ChevronDown, ChevronRight, Copy, Search, X } from 'lucide-react';
4
- import { Fragment, useEffect, useId, useMemo, useState } from 'react';
5
- import { getHighlightedSegments } from '../../utils/text/get-highlighted-segments';
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
- function isJsonArray(value) {
8
- return Array.isArray(value);
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(--dbc-blue-300) 32%);
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(--dbc-blue-300) 12%);
14
- --json-pane-string: color-mix(in oklab, var(--dbc-green-300) 78%, white 22%);
15
- --json-pane-number: color-mix(in oklab, var(--dbc-amber-400) 76%, white 24%);
16
- --json-pane-boolean: color-mix(in oklab, var(--dbc-pink-500) 74%, white 26%);
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(--dbc-green-300) 78%, white 22%);
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,4 @@
1
+ export declare function useClipboardStatus(): {
2
+ copiedId: string | null;
3
+ setCopiedId: import("react").Dispatch<import("react").SetStateAction<string | null>>;
4
+ };
@@ -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
- if (typeof window === 'undefined')
91
- return initialSidebarCollapsed !== null && initialSidebarCollapsed !== void 0 ? initialSidebarCollapsed : false;
92
- if (initialSidebarCollapsed !== undefined) {
93
- return initialSidebarCollapsed;
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
- const parsed = JSON.parse(stored);
99
- return Boolean(parsed);
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
- return getBreakpoint(window.innerWidth) === 'small';
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), []);
@@ -26,11 +26,6 @@
26
26
  box-sizing: border-box;
27
27
  }
28
28
 
29
- html,
30
- body {
31
- scrollbar-gutter: stable both-edges;
32
- }
33
-
34
29
  body {
35
30
  color: var(--color-fg-default);
36
31
  background-color: var(--color-bg-page);
@@ -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
@@ -26,11 +26,6 @@
26
26
  box-sizing: border-box;
27
27
  }
28
28
 
29
- html,
30
- body {
31
- scrollbar-gutter: stable both-edges;
32
- }
33
-
34
29
  body {
35
30
  color: var(--color-fg-default);
36
31
  background-color: var(--color-bg-page);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.80",
3
+ "version": "0.0.82",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",