@dbcdk/react-components 0.0.79 → 0.0.81
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/forms/input/Input.module.css +30 -58
- package/dist/components/forms/select/Select.js +2 -1
- package/dist/components/forms/select/Select.module.css +19 -0
- package/dist/components/forms/text-area/Textarea.js +1 -1
- package/dist/components/forms/text-area/Textarea.module.css +22 -1
- package/dist/components/json-viewer/JsonViewer.js +2 -21
- package/dist/components/json-viewer/JsonViewer.module.css +2 -7
- package/dist/components/overlay/tooltip/TooltipProvider.js +15 -4
- package/dist/components/overlay/tooltip/useTooltipTrigger.d.ts +3 -1
- package/dist/components/overlay/tooltip/useTooltipTrigger.js +8 -1
- package/dist/components/search-box/SearchBox.js +9 -2
- package/dist/components/sidebar/Sidebar.d.ts +2 -1
- package/dist/components/sidebar/Sidebar.js +2 -2
- package/dist/components/sidebar/components/sidebar-container/SidebarContainer.d.ts +2 -1
- package/dist/components/sidebar/components/sidebar-container/SidebarContainer.js +2 -2
- package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.js +9 -2
- package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.module.css +3 -2
- package/dist/components/sidebar/components/sidenav-filteirng/SidenavFiltering.d.ts +1 -0
- package/dist/components/sidebar/components/sidenav-filteirng/SidenavFiltering.js +10 -5
- package/dist/components/sidebar/providers/SidebarProvider.d.ts +3 -1
- package/dist/components/sidebar/providers/SidebarProvider.js +26 -46
- package/dist/styles/styles.css +6 -1
- package/dist/styles/themes/dbc/dark.css +2 -0
- package/dist/styles/themes/dbc/light.css +2 -0
- package/dist/styles.css +6 -1
- package/dist/utils/text/get-highlighted-segments.d.ts +5 -0
- package/dist/utils/text/get-highlighted-segments.js +46 -0
- package/package.json +1 -1
|
@@ -191,103 +191,70 @@
|
|
|
191
191
|
|
|
192
192
|
.modified {
|
|
193
193
|
background-color: color-mix(in srgb, var(--color-status-warning-bg) 22%, var(--color-bg-surface));
|
|
194
|
-
border-color: color-mix(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
var(--color-border-default)
|
|
198
|
-
);
|
|
199
|
-
box-shadow: inset 4px 0 0 var(--color-status-warning-border);
|
|
194
|
+
border-color: color-mix(in srgb, var(--color-status-warning-border) 45%, var(--color-border-default));
|
|
195
|
+
border-left-color: var(--color-status-warning-border);
|
|
196
|
+
border-left-width: 4px;
|
|
200
197
|
}
|
|
201
198
|
|
|
202
199
|
/* Hover should stay warm, not switch back to the normal border */
|
|
203
200
|
.modified:hover:not([aria-disabled='true']) {
|
|
204
201
|
background-color: color-mix(in srgb, var(--color-status-warning-bg) 28%, var(--color-bg-surface));
|
|
205
|
-
border-color: color-mix(
|
|
206
|
-
|
|
207
|
-
var(--color-status-warning-border) 60%,
|
|
208
|
-
var(--color-border-default)
|
|
209
|
-
);
|
|
202
|
+
border-color: color-mix(in srgb, var(--color-status-warning-border) 60%, var(--color-border-default));
|
|
203
|
+
border-left-color: var(--color-status-warning-border);
|
|
210
204
|
}
|
|
211
205
|
|
|
212
206
|
/* Focus should also stay warm and readable */
|
|
213
207
|
.modified:focus-within:not([aria-disabled='true']) {
|
|
214
208
|
background-color: color-mix(in srgb, var(--color-status-warning-bg) 28%, var(--color-bg-surface));
|
|
215
|
-
border-color: color-mix(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
var(--color-border-default)
|
|
219
|
-
);
|
|
220
|
-
box-shadow:
|
|
221
|
-
inset 4px 0 0 var(--color-status-warning-border),
|
|
222
|
-
inset 0 0 0 1px
|
|
223
|
-
color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
|
|
209
|
+
border-color: color-mix(in srgb, var(--color-status-warning-border) 75%, var(--color-border-default));
|
|
210
|
+
border-left-color: var(--color-status-warning-border);
|
|
211
|
+
box-shadow: inset 0 0 0 1px
|
|
212
|
+
color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
|
|
224
213
|
}
|
|
225
214
|
|
|
226
215
|
/* Variant-specific tweaks when modified */
|
|
227
216
|
.surface.modified {
|
|
228
|
-
box-shadow:
|
|
229
|
-
inset 4px 0 0 var(--color-status-warning-border),
|
|
230
|
-
0 1px 2px rgba(0, 0, 0, 0.03);
|
|
217
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
|
231
218
|
}
|
|
232
219
|
|
|
233
220
|
.surface.modified:hover:not([aria-disabled='true']) {
|
|
234
|
-
box-shadow:
|
|
235
|
-
inset 4px 0 0 var(--color-status-warning-border),
|
|
236
|
-
0 1px 3px rgba(0, 0, 0, 0.05);
|
|
221
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
237
222
|
}
|
|
238
223
|
|
|
239
224
|
.surface.modified:focus-within:not([aria-disabled='true']) {
|
|
240
|
-
box-shadow:
|
|
241
|
-
|
|
242
|
-
inset 0 0 0 1px
|
|
243
|
-
color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
|
|
225
|
+
box-shadow: inset 0 0 0 1px
|
|
226
|
+
color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
|
|
244
227
|
}
|
|
245
228
|
|
|
246
229
|
.subtle.modified {
|
|
247
230
|
background-color: color-mix(in srgb, var(--color-status-warning-bg) 30%, var(--color-bg-toolbar));
|
|
248
231
|
border-color: transparent;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
232
|
+
border-left-color: var(--color-status-warning-border);
|
|
233
|
+
border-left-width: 4px;
|
|
234
|
+
box-shadow: none;
|
|
252
235
|
}
|
|
253
236
|
|
|
254
237
|
.subtle.modified:hover:not([aria-disabled='true']) {
|
|
255
|
-
background-color: color-mix(
|
|
256
|
-
in srgb,
|
|
257
|
-
var(--color-status-warning-bg) 36%,
|
|
258
|
-
var(--color-bg-toolbar-hover)
|
|
259
|
-
);
|
|
238
|
+
background-color: color-mix(in srgb, var(--color-status-warning-bg) 36%, var(--color-bg-toolbar-hover));
|
|
260
239
|
}
|
|
261
240
|
|
|
262
241
|
.subtle.modified:focus-within:not([aria-disabled='true']) {
|
|
263
|
-
border-color: color-mix(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
var(--color-border-default)
|
|
267
|
-
);
|
|
268
|
-
box-shadow:
|
|
269
|
-
inset 4px 0 0 var(--color-status-warning-border),
|
|
270
|
-
inset 0 0 0 1px
|
|
271
|
-
color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
|
|
242
|
+
border-color: color-mix(in srgb, var(--color-status-warning-border) 75%, var(--color-border-default));
|
|
243
|
+
border-left-color: var(--color-status-warning-border);
|
|
244
|
+
box-shadow: inset 0 0 0 1px
|
|
245
|
+
color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
|
|
272
246
|
}
|
|
273
247
|
|
|
274
248
|
.standalone.modified {
|
|
275
|
-
box-shadow:
|
|
276
|
-
inset 4px 0 0 var(--color-status-warning-border),
|
|
277
|
-
var(--shadow-xs),
|
|
278
|
-
var(--shadow-md);
|
|
249
|
+
box-shadow: var(--shadow-xs), var(--shadow-md);
|
|
279
250
|
}
|
|
280
251
|
|
|
281
252
|
.standalone.modified:hover:not([aria-disabled='true']) {
|
|
282
|
-
box-shadow:
|
|
283
|
-
inset 4px 0 0 var(--color-status-warning-border),
|
|
284
|
-
var(--shadow-sm),
|
|
285
|
-
var(--shadow-md);
|
|
253
|
+
box-shadow: var(--shadow-sm), var(--shadow-md);
|
|
286
254
|
}
|
|
287
255
|
|
|
288
256
|
.standalone.modified:focus-within:not([aria-disabled='true']) {
|
|
289
257
|
box-shadow:
|
|
290
|
-
inset 4px 0 0 var(--color-status-warning-border),
|
|
291
258
|
var(--shadow-xs),
|
|
292
259
|
var(--shadow-md),
|
|
293
260
|
inset 0 0 0 1px
|
|
@@ -298,20 +265,25 @@
|
|
|
298
265
|
.embedded.modified {
|
|
299
266
|
background-color: color-mix(in srgb, var(--color-status-warning-bg) 18%, transparent);
|
|
300
267
|
border-color: transparent;
|
|
301
|
-
|
|
268
|
+
border-left-color: var(--color-status-warning-border);
|
|
269
|
+
border-left-width: 3px;
|
|
270
|
+
box-shadow: none;
|
|
302
271
|
}
|
|
303
272
|
|
|
304
273
|
.embedded.modified:hover:not([aria-disabled='true']),
|
|
305
274
|
.embedded.modified:focus-within:not([aria-disabled='true']) {
|
|
306
275
|
background-color: color-mix(in srgb, var(--color-status-warning-bg) 22%, transparent);
|
|
307
276
|
border-color: transparent;
|
|
308
|
-
|
|
277
|
+
border-left-color: var(--color-status-warning-border);
|
|
278
|
+
box-shadow: none;
|
|
309
279
|
}
|
|
310
280
|
|
|
311
281
|
/* Disabled modified state */
|
|
312
282
|
.modified[aria-disabled='true'] {
|
|
313
283
|
background-color: var(--color-disabled-bg);
|
|
314
284
|
border-color: var(--color-disabled-border);
|
|
285
|
+
border-left-color: var(--color-disabled-border);
|
|
286
|
+
border-left-width: var(--border-width-thin);
|
|
315
287
|
box-shadow: none;
|
|
316
288
|
}
|
|
317
289
|
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Check } from 'lucide-react';
|
|
4
4
|
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { useTooltipTrigger } from '../../../components/overlay/tooltip/useTooltipTrigger';
|
|
6
|
+
import styles from './Select.module.css';
|
|
6
7
|
import { Button } from '../../button/Button';
|
|
7
8
|
import { ClearButton } from '../../clear-button/ClearButton';
|
|
8
9
|
import { Menu } from '../../menu/Menu';
|
|
@@ -168,7 +169,7 @@ export function Select({ label, error, helpText, orientation = 'vertical', label
|
|
|
168
169
|
returnFocus: true, trigger: (toggle, icon, isOpen) => (_jsx(Button, { disabled: disabled, ...(tooltipEnabled ? triggerProps : {}), id: controlId, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'select-button', onKeyDown: handleKeyDown, fullWidth: fullWidth, variant: variant, onClick: e => {
|
|
169
170
|
resetActiveToSelected();
|
|
170
171
|
toggle(e);
|
|
171
|
-
}, size: size, type: "button", "data-forminput": true, "aria-haspopup": "listbox", "aria-expanded": !!isOpen, "aria-controls": listboxId, "aria-invalid": Boolean(error) || undefined, "aria-describedby": describedBy, children: _jsxs("span", { className: "dbc-flex dbc-justify-between dbc-items-center dbc-gap-xxs", style: { width: '100%' }, children: [_jsx("span", { children: selected ? selected.label : _jsx("span", { className: "dbc-muted-text", children: placeholder }) }), _jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [onClear && selected && _jsx(ClearButton, { onClick: onClear }), _jsx("span", { style: { color: 'var(--color-fg-subtle)', display: 'inline-flex' }, children: icon })] })] }) })), children: _jsx(Menu, { onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
|
|
172
|
+
}, size: size, type: "button", "data-forminput": true, "aria-haspopup": "listbox", "aria-expanded": !!isOpen, "aria-controls": listboxId, "aria-invalid": Boolean(error) || undefined, "aria-describedby": describedBy, className: modified ? styles.triggerModified : undefined, children: _jsxs("span", { className: "dbc-flex dbc-justify-between dbc-items-center dbc-gap-xxs", style: { width: '100%' }, children: [_jsx("span", { children: selected ? selected.label : _jsx("span", { className: "dbc-muted-text", children: placeholder }) }), _jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [onClear && selected && _jsx(ClearButton, { onClick: onClear }), _jsx("span", { style: { color: 'var(--color-fg-subtle)', display: 'inline-flex' }, children: icon })] })] }) })), children: _jsx(Menu, { onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
|
|
172
173
|
const isSelected = typeof opt.value === 'object' && typeof selectedValue === 'object' && datakey
|
|
173
174
|
? (selectedValue === null || selectedValue === void 0 ? void 0 : selectedValue[datakey]) === opt.value[datakey]
|
|
174
175
|
: opt.value === selectedValue;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.triggerModified {
|
|
2
|
+
background-color: color-mix(in srgb, var(--color-status-warning-bg) 22%, var(--color-bg-surface));
|
|
3
|
+
border-color: color-mix(in srgb, var(--color-status-warning-border) 45%, var(--color-border-default));
|
|
4
|
+
border-left-color: var(--color-status-warning-border);
|
|
5
|
+
border-left-width: 4px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.triggerModified:hover:not([disabled]) {
|
|
9
|
+
background-color: color-mix(in srgb, var(--color-status-warning-bg) 28%, var(--color-bg-surface));
|
|
10
|
+
border-color: color-mix(in srgb, var(--color-status-warning-border) 60%, var(--color-border-default));
|
|
11
|
+
border-left-color: var(--color-status-warning-border);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.triggerModified:focus-visible {
|
|
15
|
+
background-color: color-mix(in srgb, var(--color-status-warning-bg) 28%, var(--color-bg-surface));
|
|
16
|
+
border-color: color-mix(in srgb, var(--color-status-warning-border) 75%, var(--color-border-default));
|
|
17
|
+
border-left-color: var(--color-status-warning-border);
|
|
18
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
|
|
19
|
+
}
|
|
@@ -39,6 +39,6 @@ className, ...rest }) {
|
|
|
39
39
|
placement: tooltipPlacement,
|
|
40
40
|
offset: 8,
|
|
41
41
|
});
|
|
42
|
-
return (_jsx(InputContainer, { modified: modified, label: label, htmlFor: textareaId, error: error, helpText: helpText, helpTextAddition: showCount ? `${value === null || value === void 0 ? void 0 : value.length} tegn i denne boks` : undefined, orientation: orientation, labelWidth: labelWidth, fullWidth: fullWidth, required: required, children: _jsx("div", { className: styles.container, children: _jsx("div", { ...(tooltipEnabled ? triggerProps : {}), children: inputField }) }) }));
|
|
42
|
+
return (_jsx(InputContainer, { modified: modified, label: label, htmlFor: textareaId, error: error, helpText: helpText, helpTextAddition: showCount ? `${value === null || value === void 0 ? void 0 : value.length} tegn i denne boks` : undefined, orientation: orientation, labelWidth: labelWidth, fullWidth: fullWidth, required: required, children: _jsx("div", { className: [styles.container, modified ? styles.modified : ''].filter(Boolean).join(' '), children: _jsx("div", { ...(tooltipEnabled ? triggerProps : {}), children: inputField }) }) }));
|
|
43
43
|
};
|
|
44
44
|
Textarea.displayName = 'Textarea';
|
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
.container {
|
|
2
2
|
flex-grow: 1;
|
|
3
3
|
}
|
|
4
|
+
|
|
5
|
+
.container.modified textarea {
|
|
6
|
+
background-color: color-mix(in srgb, var(--color-status-warning-bg) 22%, var(--color-bg-surface));
|
|
7
|
+
border-color: color-mix(in srgb, var(--color-status-warning-border) 45%, var(--color-border-default));
|
|
8
|
+
border-left-color: var(--color-status-warning-border);
|
|
9
|
+
border-left-width: 4px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.container.modified textarea:hover {
|
|
13
|
+
background-color: color-mix(in srgb, var(--color-status-warning-bg) 28%, var(--color-bg-surface));
|
|
14
|
+
border-color: color-mix(in srgb, var(--color-status-warning-border) 60%, var(--color-border-default));
|
|
15
|
+
border-left-color: var(--color-status-warning-border);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.container.modified textarea:focus-visible {
|
|
19
|
+
background-color: color-mix(in srgb, var(--color-status-warning-bg) 28%, var(--color-bg-surface));
|
|
20
|
+
border-color: color-mix(in srgb, var(--color-status-warning-border) 75%, var(--color-border-default));
|
|
21
|
+
border-left-color: var(--color-status-warning-border);
|
|
22
|
+
box-shadow: inset 0 0 0 1px
|
|
23
|
+
color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
|
|
24
|
+
}
|
|
4
25
|
.container textarea {
|
|
5
26
|
width: 100%;
|
|
6
27
|
padding: var(--spacing-xs);
|
|
7
28
|
border: var(--border-width-thin) solid var(--color-border-default);
|
|
8
|
-
border-radius: var(--border-radius-
|
|
29
|
+
border-radius: var(--border-radius-default);
|
|
9
30
|
font-family: var(--font-family);
|
|
10
31
|
font-size: var(--font-size-sm);
|
|
11
32
|
line-height: var(--line-height-normal);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Check, ChevronDown, ChevronRight, Copy, Search, X } from 'lucide-react';
|
|
4
4
|
import { Fragment, useEffect, useId, useMemo, useState } from 'react';
|
|
5
|
+
import { getHighlightedSegments } from '../../utils/text/get-highlighted-segments';
|
|
5
6
|
import styles from './JsonViewer.module.css';
|
|
6
7
|
function isJsonArray(value) {
|
|
7
8
|
return Array.isArray(value);
|
|
@@ -89,28 +90,8 @@ function collectSearchMatches(value, query, path = [], key) {
|
|
|
89
90
|
visit(value, path, key);
|
|
90
91
|
return matches;
|
|
91
92
|
}
|
|
92
|
-
function getHighlightedSegments(text, query) {
|
|
93
|
-
if (!query)
|
|
94
|
-
return [{ text, matched: false }];
|
|
95
|
-
const lower = text.toLowerCase();
|
|
96
|
-
const segments = [];
|
|
97
|
-
let start = 0;
|
|
98
|
-
while (start < text.length) {
|
|
99
|
-
const index = lower.indexOf(query, start);
|
|
100
|
-
if (index === -1) {
|
|
101
|
-
segments.push({ text: text.slice(start), matched: false });
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
if (index > start) {
|
|
105
|
-
segments.push({ text: text.slice(start, index), matched: false });
|
|
106
|
-
}
|
|
107
|
-
segments.push({ text: text.slice(index, index + query.length), matched: true });
|
|
108
|
-
start = index + query.length;
|
|
109
|
-
}
|
|
110
|
-
return segments.length > 0 ? segments : [{ text, matched: false }];
|
|
111
|
-
}
|
|
112
93
|
function HighlightText({ text, query }) {
|
|
113
|
-
return getHighlightedSegments(text, query).map((segment, index) => segment.matched ? (_jsx("mark", { className:
|
|
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}`)));
|
|
114
95
|
}
|
|
115
96
|
function useClipboardStatus() {
|
|
116
97
|
const [copiedId, setCopiedId] = useState(null);
|
|
@@ -8,6 +8,8 @@
|
|
|
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
10
|
--json-pane-highlight: color-mix(in oklab, var(--color-brand) 68%, var(--dbc-blue-300) 32%);
|
|
11
|
+
--color-highlight-bg: color-mix(in oklab, var(--json-pane-highlight) 58%, transparent);
|
|
12
|
+
--color-highlight-fg: var(--color-fg-inverse);
|
|
11
13
|
--json-pane-key: color-mix(in oklab, var(--color-fg-inverse) 88%, var(--dbc-blue-300) 12%);
|
|
12
14
|
--json-pane-string: color-mix(in oklab, var(--dbc-green-300) 78%, white 22%);
|
|
13
15
|
--json-pane-number: color-mix(in oklab, var(--dbc-amber-400) 76%, white 24%);
|
|
@@ -314,13 +316,6 @@
|
|
|
314
316
|
color: color-mix(in oklab, var(--dbc-green-300) 78%, white 22%);
|
|
315
317
|
}
|
|
316
318
|
|
|
317
|
-
.highlight {
|
|
318
|
-
color: var(--color-fg-inverse);
|
|
319
|
-
background: color-mix(in oklab, var(--json-pane-highlight) 58%, transparent);
|
|
320
|
-
border-radius: var(--border-radius-sm);
|
|
321
|
-
padding-inline: 0.1em;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
319
|
.emptyState {
|
|
325
320
|
padding: var(--spacing-md);
|
|
326
321
|
color: var(--json-pane-muted);
|
|
@@ -5,6 +5,7 @@ import { createPortal } from 'react-dom';
|
|
|
5
5
|
import styles from './Tooltip.module.css';
|
|
6
6
|
export const TooltipContext = createContext(null);
|
|
7
7
|
const VIEWPORT_PADDING = 8;
|
|
8
|
+
const SHOW_DELAY_MS = 500;
|
|
8
9
|
function clamp(n, min, max) {
|
|
9
10
|
return Math.max(min, Math.min(n, max));
|
|
10
11
|
}
|
|
@@ -65,13 +66,23 @@ function shallowEqualActive(a, b) {
|
|
|
65
66
|
}
|
|
66
67
|
export function TooltipProvider({ children }) {
|
|
67
68
|
const [active, setActive] = useState(null);
|
|
69
|
+
const showTimerRef = useRef(null);
|
|
68
70
|
const show = useCallback((next) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
if (showTimerRef.current)
|
|
72
|
+
clearTimeout(showTimerRef.current);
|
|
73
|
+
showTimerRef.current = setTimeout(() => {
|
|
74
|
+
showTimerRef.current = null;
|
|
75
|
+
setActive(curr => {
|
|
76
|
+
const proposed = { ...next, open: true };
|
|
77
|
+
return shallowEqualActive(curr, proposed) ? curr : proposed;
|
|
78
|
+
});
|
|
79
|
+
}, SHOW_DELAY_MS);
|
|
73
80
|
}, []);
|
|
74
81
|
const hide = useCallback((id) => {
|
|
82
|
+
if (showTimerRef.current) {
|
|
83
|
+
clearTimeout(showTimerRef.current);
|
|
84
|
+
showTimerRef.current = null;
|
|
85
|
+
}
|
|
75
86
|
setActive(curr => {
|
|
76
87
|
if (!curr || curr.id !== id)
|
|
77
88
|
return curr;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import type { ReactNode } from 'react';
|
|
2
3
|
import { TooltipPlacement } from './TooltipProvider';
|
|
3
4
|
type UseTooltipOptions = {
|
|
@@ -16,7 +17,8 @@ export declare function useTooltipTrigger(options: UseTooltipOptions): {
|
|
|
16
17
|
ref: (node: HTMLElement | null) => void;
|
|
17
18
|
onPointerEnter: () => void;
|
|
18
19
|
onPointerLeave: () => void;
|
|
19
|
-
|
|
20
|
+
onPointerDown: () => void;
|
|
21
|
+
onFocus: (e: React.FocusEvent) => void;
|
|
20
22
|
onBlur: () => void;
|
|
21
23
|
'aria-describedby'?: string;
|
|
22
24
|
};
|
|
@@ -81,9 +81,15 @@ export function useTooltipTrigger(options) {
|
|
|
81
81
|
return;
|
|
82
82
|
closeTimer.current = window.setTimeout(() => setOpen(false), delayCloseMs);
|
|
83
83
|
};
|
|
84
|
-
const
|
|
84
|
+
const onPointerDown = () => {
|
|
85
85
|
clearTimers();
|
|
86
86
|
if (!isControlled)
|
|
87
|
+
setOpen(false);
|
|
88
|
+
};
|
|
89
|
+
const onFocus = (e) => {
|
|
90
|
+
clearTimers();
|
|
91
|
+
// Only show for keyboard focus — skip pointer clicks and programmatic focus returns (e.g. from modal close)
|
|
92
|
+
if (!isControlled && e.currentTarget.matches(':focus-visible'))
|
|
87
93
|
setTimeout(() => {
|
|
88
94
|
setOpen(true);
|
|
89
95
|
}, MOTION_MS.tooltipOpen);
|
|
@@ -102,6 +108,7 @@ export function useTooltipTrigger(options) {
|
|
|
102
108
|
},
|
|
103
109
|
onPointerEnter,
|
|
104
110
|
onPointerLeave,
|
|
111
|
+
onPointerDown,
|
|
105
112
|
onFocus,
|
|
106
113
|
onBlur,
|
|
107
114
|
'aria-describedby': content ? id : undefined,
|
|
@@ -79,6 +79,12 @@ export const SearchBox = forwardRef(function SearchBoxInner({ inputWidth, maxWid
|
|
|
79
79
|
setActiveIndex(null);
|
|
80
80
|
(_a = popoverRef.current) === null || _a === void 0 ? void 0 : _a.close();
|
|
81
81
|
}
|
|
82
|
+
const handleClear = React.useCallback(() => {
|
|
83
|
+
var _a;
|
|
84
|
+
reset();
|
|
85
|
+
onSearch === null || onSearch === void 0 ? void 0 : onSearch('');
|
|
86
|
+
(_a = internalInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
87
|
+
}, [onSearch]);
|
|
82
88
|
// Reset active index when results change
|
|
83
89
|
useEffect(() => {
|
|
84
90
|
setActiveIndex(null);
|
|
@@ -100,7 +106,7 @@ export const SearchBox = forwardRef(function SearchBoxInner({ inputWidth, maxWid
|
|
|
100
106
|
var _a, _b;
|
|
101
107
|
if (!((_a = popoverRef.current) === null || _a === void 0 ? void 0 : _a.isOpen()))
|
|
102
108
|
(_b = popoverRef.current) === null || _b === void 0 ? void 0 : _b.open();
|
|
103
|
-
}, minWidth: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), width: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), fullWidth: fullWidth, icon: showInputIcon ? _jsx(Search, {}) : undefined, inputSize: inputSize, variant: variant, autoComplete: "off", onButtonClick: onButtonClick, buttonLabel: buttonLabel, buttonIcon: trailingButtonIcon, ...inputProps, onKeyDown: e => {
|
|
109
|
+
}, minWidth: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), width: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), fullWidth: fullWidth, icon: showInputIcon ? _jsx(Search, {}) : undefined, inputSize: inputSize, variant: variant, autoComplete: "off", onClear: handleClear, onButtonClick: onButtonClick, buttonLabel: buttonLabel, buttonIcon: trailingButtonIcon, ...inputProps, onKeyDown: e => {
|
|
104
110
|
var _a;
|
|
105
111
|
if (result === null || result === void 0 ? void 0 : result.length) {
|
|
106
112
|
if (e.key === 'ArrowDown') {
|
|
@@ -132,11 +138,12 @@ export const SearchBox = forwardRef(function SearchBoxInner({ inputWidth, maxWid
|
|
|
132
138
|
return (_jsx("td", { className: styles.suggestionCell, style: { whiteSpace: cell.length < 10 ? 'nowrap' : undefined }, children: cell }, key));
|
|
133
139
|
}) }, index))) }) }) })) : !searchQuery && !loading ? (initialTemplate || _jsx("div", { className: styles.resultContainer, children: "Indtast s\u00F8geord" })) : loading ? (_jsx("table", { style: { width: '100%' }, children: _jsx("tbody", { children: Array.from({ length: 5 }).map((_, index) => (_jsx("tr", { children: resultKeys === null || resultKeys === void 0 ? void 0 : resultKeys.map(key => (_jsx("td", { style: { padding: '8px' }, children: _jsx(SkeletonLoaderItem, { height: 20, width: "100%" }) }, key))) }, index))) }) })) : (_jsx("div", { className: styles.resultContainer, children: noResultText })) }));
|
|
134
140
|
}
|
|
135
|
-
return (_jsx(Input, { ref: internalInputRef, icon: showInputIcon ? _jsx(Search, {}) : undefined, minWidth: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), fullWidth: fullWidth, inputSize: inputSize, variant: variant, onButtonClick: onButtonClick, buttonLabel: buttonLabel, buttonIcon: trailingButtonIcon, ...inputProps, placeholder: (_a = rest.placeholder) !== null && _a !== void 0 ? _a : 'Indtast søgeord' }));
|
|
141
|
+
return (_jsx(Input, { ref: internalInputRef, icon: showInputIcon ? _jsx(Search, {}) : undefined, minWidth: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), fullWidth: fullWidth, inputSize: inputSize, variant: variant, onClear: handleClear, onButtonClick: onButtonClick, buttonLabel: buttonLabel, buttonIcon: trailingButtonIcon, ...inputProps, placeholder: (_a = rest.placeholder) !== null && _a !== void 0 ? _a : 'Indtast søgeord' }));
|
|
136
142
|
}, [
|
|
137
143
|
rest,
|
|
138
144
|
draft,
|
|
139
145
|
handleChange,
|
|
146
|
+
handleClear,
|
|
140
147
|
displayPopover,
|
|
141
148
|
inputWidth,
|
|
142
149
|
inputSize,
|
|
@@ -12,11 +12,12 @@ interface SidebarProps {
|
|
|
12
12
|
version?: string | number;
|
|
13
13
|
hideSearch?: boolean;
|
|
14
14
|
searchPlaceholder?: string;
|
|
15
|
+
showSettings?: boolean;
|
|
15
16
|
footer?: React.ReactNode;
|
|
16
17
|
resizable?: boolean;
|
|
17
18
|
defaultWidth?: number;
|
|
18
19
|
minWidth?: number;
|
|
19
20
|
storageKey?: string;
|
|
20
21
|
}
|
|
21
|
-
export declare function Sidebar({ items, productLogo, activeLink, version, hideSearch, searchPlaceholder, footer, resizable, defaultWidth, minWidth, storageKey, }: SidebarProps): JSX.Element;
|
|
22
|
+
export declare function Sidebar({ items, productLogo, activeLink, version, hideSearch, searchPlaceholder, showSettings, footer, resizable, defaultWidth, minWidth, storageKey, }: SidebarProps): JSX.Element;
|
|
22
23
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { SidebarContainer } from './components/sidebar-container/SidebarContainer';
|
|
3
3
|
import { SidebarProvider } from './providers/SidebarProvider';
|
|
4
|
-
export function Sidebar({ items, productLogo, activeLink, version, hideSearch, searchPlaceholder, footer, resizable, defaultWidth, minWidth, storageKey, }) {
|
|
5
|
-
return (_jsx(SidebarProvider, { items: items, children: _jsx(SidebarContainer, { productLogo: productLogo, activeLink: activeLink, version: version, hideSearch: hideSearch, searchPlaceholder: searchPlaceholder, footer: footer, resizable: resizable, defaultWidth: defaultWidth, minWidth: minWidth, storageKey: storageKey }) }));
|
|
4
|
+
export function Sidebar({ items, productLogo, activeLink, version, hideSearch, searchPlaceholder, showSettings, footer, resizable, defaultWidth, minWidth, storageKey, }) {
|
|
5
|
+
return (_jsx(SidebarProvider, { items: items, children: _jsx(SidebarContainer, { productLogo: productLogo, activeLink: activeLink, version: version, hideSearch: hideSearch, searchPlaceholder: searchPlaceholder, showSettings: showSettings, footer: footer, resizable: resizable, defaultWidth: defaultWidth, minWidth: minWidth, storageKey: storageKey }) }));
|
|
6
6
|
}
|
|
@@ -7,11 +7,12 @@ interface SidebarContainerProps {
|
|
|
7
7
|
version?: string | number;
|
|
8
8
|
hideSearch?: boolean;
|
|
9
9
|
searchPlaceholder?: string;
|
|
10
|
+
showSettings?: boolean;
|
|
10
11
|
footer?: ReactNode;
|
|
11
12
|
resizable?: boolean;
|
|
12
13
|
defaultWidth?: number;
|
|
13
14
|
minWidth?: number;
|
|
14
15
|
storageKey?: string;
|
|
15
16
|
}
|
|
16
|
-
export declare function SidebarContainer({ logo, productLogo, activeLink, version, hideSearch, searchPlaceholder, footer, resizable, defaultWidth, minWidth, storageKey, }: SidebarContainerProps): JSX.Element;
|
|
17
|
+
export declare function SidebarContainer({ logo, productLogo, activeLink, version, hideSearch, searchPlaceholder, showSettings, footer, resizable, defaultWidth, minWidth, storageKey, }: SidebarContainerProps): JSX.Element;
|
|
17
18
|
export {};
|
|
@@ -39,7 +39,7 @@ function removeStoredWidth(key) {
|
|
|
39
39
|
// ignore
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
export function SidebarContainer({ logo, productLogo, activeLink, version, hideSearch, searchPlaceholder = 'Filtrer menu', footer, resizable, defaultWidth = 240, minWidth = 160, storageKey, }) {
|
|
42
|
+
export function SidebarContainer({ logo, productLogo, activeLink, version, hideSearch, searchPlaceholder = 'Filtrer menu', showSettings = false, footer, resizable, defaultWidth = 240, minWidth = 160, storageKey, }) {
|
|
43
43
|
const { isSidebarCollapsed, handleSidebarCollapseChange } = useSidebar();
|
|
44
44
|
// Always start from defaultWidth so server and client render identical HTML.
|
|
45
45
|
// localStorage is read in a useEffect after hydration to avoid SSR mismatch.
|
|
@@ -149,5 +149,5 @@ export function SidebarContainer({ logo, productLogo, activeLink, version, hideS
|
|
|
149
149
|
const containerStyle = useMemo(() => ({
|
|
150
150
|
'--sidebar-width': `${sidebarWidth}px`,
|
|
151
151
|
}), [sidebarWidth]);
|
|
152
|
-
return (_jsxs("div", { ref: containerRef, role: "complementary", className: `${styles.container} ${isSidebarCollapsed ? styles.collapsed : ''} ${isResizing ? styles.resizing : ''}`, style: containerStyle, children: [_jsx("div", { className: styles.header, children: _jsxs("div", { className: styles.productHeader, children: [_jsx("div", { className: styles.productLogo, children: productLogo }), _jsx(Button, { size: "md", variant: "inline", shape: "round", "aria-label": "Collapse sidebar", icon: _jsx(ChevronLeft, { className: isSidebarCollapsed ? styles.collapsedIcon : '' }), onClick: () => handleSidebarCollapseChange(!isSidebarCollapsed) })] }) }), _jsxs("div", { className: styles.content, children: [!hideSearch && (_jsx("div", { className: styles.filter, children: _jsx(SidenavFiltering, { placeholder: searchPlaceholder }) })), _jsx("div", { className: `${styles.links} hideScrollBar`, children: _jsx(SidebarItems, { activeLink: activeLink }) })] }), footer && _jsx("div", { className: styles.footerSlot, children: footer }), _jsxs("div", { className: styles.footer, children: [_jsx("div", { className: styles.companyLogo, children: logo !== null && logo !== void 0 ? logo : _jsx(Logo, {}) }), version && _jsx("div", { className: `${styles.version} dbc-muted-text dbc-sm-text`, children: version })] }), resizable && (_jsx("div", { className: styles.resizeHandle, role: "separator", "aria-label": "Resize sidebar", "aria-orientation": "vertical", "aria-valuemin": Math.round(minWidth), "aria-valuemax": ariaMaxWidth !== undefined ? Math.round(ariaMaxWidth) : undefined, "aria-valuenow": Math.round(sidebarWidth), tabIndex: isSidebarCollapsed ? -1 : 0, onPointerDown: onResizePointerDown, onPointerMove: onResizePointerMove, onPointerUp: endResizeDrag, onPointerCancel: endResizeDrag, onDoubleClick: resetWidth, onKeyDown: onKeyDown }))] }));
|
|
152
|
+
return (_jsxs("div", { ref: containerRef, role: "complementary", className: `${styles.container} ${isSidebarCollapsed ? styles.collapsed : ''} ${isResizing ? styles.resizing : ''}`, style: containerStyle, children: [_jsx("div", { className: styles.header, children: _jsxs("div", { className: styles.productHeader, children: [_jsx("div", { className: styles.productLogo, children: productLogo }), _jsx(Button, { size: "md", variant: "inline", shape: "round", "aria-label": "Collapse sidebar", icon: _jsx(ChevronLeft, { className: isSidebarCollapsed ? styles.collapsedIcon : '' }), onClick: () => handleSidebarCollapseChange(!isSidebarCollapsed) })] }) }), _jsxs("div", { className: styles.content, children: [!hideSearch && (_jsx("div", { className: styles.filter, children: _jsx(SidenavFiltering, { placeholder: searchPlaceholder, showSettings: showSettings }) })), _jsx("div", { className: `${styles.links} hideScrollBar`, children: _jsx(SidebarItems, { activeLink: activeLink }) })] }), footer && _jsx("div", { className: styles.footerSlot, children: footer }), _jsxs("div", { className: styles.footer, children: [_jsx("div", { className: styles.companyLogo, children: logo !== null && logo !== void 0 ? logo : _jsx(Logo, {}) }), version && _jsx("div", { className: `${styles.version} dbc-muted-text dbc-sm-text`, children: version })] }), resizable && (_jsx("div", { className: styles.resizeHandle, role: "separator", "aria-label": "Resize sidebar", "aria-orientation": "vertical", "aria-valuemin": Math.round(minWidth), "aria-valuemax": ariaMaxWidth !== undefined ? Math.round(ariaMaxWidth) : undefined, "aria-valuenow": Math.round(sidebarWidth), tabIndex: isSidebarCollapsed ? -1 : 0, onPointerDown: onResizePointerDown, onPointerMove: onResizePointerMove, onPointerUp: endResizeDrag, onPointerCancel: endResizeDrag, onDoubleClick: resetWidth, onKeyDown: onKeyDown }))] }));
|
|
153
153
|
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { getHighlightedSegments } from '../../../../utils/text/get-highlighted-segments';
|
|
3
5
|
import styles from './SidebarItemContent.module.css';
|
|
4
6
|
import { useSidebar } from '../../providers/SidebarProvider';
|
|
5
7
|
export function SidebarItemContent({ icon, label, suffixIcon, href, iconWidth, disableActiveStyles = false, headerStyle, truncateLabel = false, }) {
|
|
6
|
-
const { activeLink, isSidebarCollapsed } = useSidebar();
|
|
8
|
+
const { activeLink, activeQuery, isSidebarCollapsed, wrapItemText } = useSidebar();
|
|
7
9
|
const iconStyle = iconWidth !== undefined
|
|
8
10
|
? { ['--sidebar-item-icon-width']: iconWidth }
|
|
9
11
|
: undefined;
|
|
10
|
-
|
|
12
|
+
const shouldTruncate = truncateLabel && !wrapItemText && !activeQuery;
|
|
13
|
+
const highlightTerms = activeQuery.trim().split(/\s+/).filter(Boolean);
|
|
14
|
+
const renderedLabel = typeof label === 'string' && highlightTerms.length > 0
|
|
15
|
+
? getHighlightedSegments(label, highlightTerms).map((segment, index) => segment.matched ? (_jsx("mark", { className: "dbc-highlight", children: segment.text }, `${segment.text}-${index}`)) : (_jsx(React.Fragment, { children: segment.text }, `${segment.text}-${index}`)))
|
|
16
|
+
: label;
|
|
17
|
+
return (_jsxs("span", { className: `${styles.container} ${!disableActiveStyles && activeLink === href ? styles.active : ''} ${isSidebarCollapsed ? styles.collapsed : ''} ${headerStyle ? styles.headerStyle : ''}`, children: [_jsxs("span", { className: styles.iconLabel, children: [_jsx("span", { className: styles.icon, style: iconStyle, children: icon }), !isSidebarCollapsed && (_jsx("span", { className: `${styles.label} ${shouldTruncate ? styles.truncate : ''}`, title: shouldTruncate && typeof label === 'string' ? label : undefined, children: renderedLabel }))] }), suffixIcon && !isSidebarCollapsed && _jsx("span", { className: styles.suffixIcon, children: suffixIcon })] }));
|
|
11
18
|
}
|
package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.module.css
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
color: var(--color-fg-muted);
|
|
11
11
|
cursor: pointer;
|
|
12
12
|
position: relative;
|
|
13
|
-
border-radius: var(--border-radius-
|
|
13
|
+
border-radius: var(--border-radius-md);
|
|
14
14
|
user-select: none;
|
|
15
15
|
transition:
|
|
16
16
|
background-color var(--transition-fast) var(--ease-standard),
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
inset-block: 0;
|
|
35
35
|
inline-size: 3px;
|
|
36
36
|
background-color: var(--color-brand);
|
|
37
|
-
border-radius:
|
|
37
|
+
border-start-start-radius: inherit;
|
|
38
|
+
border-end-start-radius: inherit;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
.iconLabel {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { Search } from 'lucide-react';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Search, Settings2 } from 'lucide-react';
|
|
4
4
|
import React from 'react';
|
|
5
5
|
import { Button } from '../../../button/Button';
|
|
6
|
+
import { Menu } from '../../../menu/Menu';
|
|
7
|
+
import { Popover } from '../../../popover/Popover';
|
|
6
8
|
import { SearchBox } from '../../../search-box/SearchBox';
|
|
7
9
|
import { useSidebar } from '../../providers/SidebarProvider';
|
|
8
|
-
const SidenavFiltering = ({ placeholder = 'Filtrer menu' }) => {
|
|
9
|
-
const { activeQuery, setActiveQuery, isSidebarCollapsed, handleSidebarCollapseChange } = useSidebar();
|
|
10
|
+
const SidenavFiltering = ({ placeholder = 'Filtrer menu', showSettings = false, }) => {
|
|
11
|
+
const { activeQuery, setActiveQuery, isSidebarCollapsed, handleSidebarCollapseChange, wrapItemText, setWrapItemText, } = useSidebar();
|
|
10
12
|
const searchBoxRef = React.useRef(null);
|
|
11
13
|
const handleSearch = (value) => {
|
|
12
14
|
setActiveQuery === null || setActiveQuery === void 0 ? void 0 : setActiveQuery(value);
|
|
@@ -20,6 +22,9 @@ const SidenavFiltering = ({ placeholder = 'Filtrer menu' }) => {
|
|
|
20
22
|
}, 50);
|
|
21
23
|
} }));
|
|
22
24
|
}
|
|
23
|
-
return (_jsx(SearchBox, { ref: searchBoxRef, inputWidth: "100%", inputSize: "sm", value: activeQuery !== null && activeQuery !== void 0 ? activeQuery : '', onSearch: handleSearch, placeholder: placeholder }))
|
|
25
|
+
return (_jsxs("div", { style: { display: 'flex', alignItems: 'stretch', gap: 'var(--spacing-xs)' }, children: [_jsx(SearchBox, { ref: searchBoxRef, inputWidth: "100%", inputSize: "sm", value: activeQuery !== null && activeQuery !== void 0 ? activeQuery : '', onSearch: handleSearch, placeholder: placeholder, fullWidth: true }), showSettings ? (_jsx(Popover, { minWidth: "220px", trigger: toggle => (_jsx(Button, { type: "button", size: "sm", variant: "outlined", shape: "round", "aria-label": "\u00C5bn indstillinger for sidemenu", onClick: toggle, icon: _jsx(Settings2, {}) })), children: close => (_jsx(Menu, { children: _jsx(Menu.Item, { active: wrapItemText, children: _jsx("button", { type: "button", onClick: () => {
|
|
26
|
+
setWrapItemText(!wrapItemText);
|
|
27
|
+
close();
|
|
28
|
+
}, children: "Ombryd tekst" }) }) })) })) : null] }));
|
|
24
29
|
};
|
|
25
30
|
export default SidenavFiltering;
|
|
@@ -6,8 +6,10 @@ export type SidebarContextValue = {
|
|
|
6
6
|
expandedItems: Set<string>;
|
|
7
7
|
resetExpandAll: () => void;
|
|
8
8
|
activeQuery: string;
|
|
9
|
+
wrapItemText: boolean;
|
|
9
10
|
areItemsCollapsed: boolean;
|
|
10
11
|
setActiveQuery: (query: string) => void;
|
|
12
|
+
setWrapItemText: (value: boolean) => void;
|
|
11
13
|
triggerExpandAll: () => void;
|
|
12
14
|
setItemsCollapsed: (v: boolean) => void;
|
|
13
15
|
filteredItems?: NavBarItem[];
|
|
@@ -27,6 +29,6 @@ type SidebarProviderProps = {
|
|
|
27
29
|
initialCollapseChildren?: boolean;
|
|
28
30
|
initialSidebarCollapsed?: boolean;
|
|
29
31
|
};
|
|
30
|
-
export declare function SidebarProvider({ children, items, initialCollapsed, initialSidebarCollapsed, }: SidebarProviderProps): JSX.Element;
|
|
32
|
+
export declare function SidebarProvider({ children, items, initialQuery, initialCollapsed, initialSidebarCollapsed, }: SidebarProviderProps): JSX.Element;
|
|
31
33
|
export declare function useSidebar(): SidebarContextValue;
|
|
32
34
|
export {};
|
|
@@ -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) => {
|
|
@@ -55,8 +48,10 @@ const SidebarContext = createContext({
|
|
|
55
48
|
defaultExpanded: null,
|
|
56
49
|
expandedItems: new Set(),
|
|
57
50
|
activeQuery: '',
|
|
51
|
+
wrapItemText: false,
|
|
58
52
|
areItemsCollapsed: false,
|
|
59
53
|
setActiveQuery: () => { },
|
|
54
|
+
setWrapItemText: () => { },
|
|
60
55
|
triggerExpandAll: () => { },
|
|
61
56
|
setItemsCollapsed: () => { },
|
|
62
57
|
resetExpandAll: () => { },
|
|
@@ -71,9 +66,10 @@ const SidebarContext = createContext({
|
|
|
71
66
|
const SIDEBAR_COLLAPSED_STORAGE_KEY = 'sidebar-is-collapsed';
|
|
72
67
|
const SIDEBAR_BREAKPOINT = 1024;
|
|
73
68
|
const getBreakpoint = (width) => width < SIDEBAR_BREAKPOINT ? 'small' : 'large';
|
|
74
|
-
export function SidebarProvider({ children, items, initialCollapsed = false, initialSidebarCollapsed, }) {
|
|
69
|
+
export function SidebarProvider({ children, items, initialQuery, initialCollapsed = false, initialSidebarCollapsed, }) {
|
|
75
70
|
const [defaultExpanded, setDefaultExpanded] = useState(null);
|
|
76
|
-
const [activeQuery, setActiveQuery] = useState('');
|
|
71
|
+
const [activeQuery, setActiveQuery] = useState(initialQuery !== null && initialQuery !== void 0 ? initialQuery : '');
|
|
72
|
+
const [wrapItemText, setWrapItemText] = useState(false);
|
|
77
73
|
const [areItemsCollapsed, setItemsCollapsed] = useState(initialCollapsed);
|
|
78
74
|
const [activeHref, setActiveHref] = useState('');
|
|
79
75
|
// expandedItems is the source of truth for "open groups"
|
|
@@ -84,7 +80,22 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
|
|
|
84
80
|
itemsRef.current = items;
|
|
85
81
|
}, [items]);
|
|
86
82
|
const [isSidebarCollapsed, setSidebarCollapsed] = useState(initialSidebarCollapsed !== null && initialSidebarCollapsed !== void 0 ? initialSidebarCollapsed : false);
|
|
87
|
-
|
|
83
|
+
// Runs once after hydration — safe to read localStorage and window.innerWidth here.
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (initialSidebarCollapsed !== undefined)
|
|
86
|
+
return;
|
|
87
|
+
try {
|
|
88
|
+
const stored = window.localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY);
|
|
89
|
+
if (stored !== null) {
|
|
90
|
+
setSidebarCollapsed(Boolean(JSON.parse(stored))); // eslint-disable-line react-hooks/set-state-in-effect -- intentional: SSR-safe initial read
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// ignore parse failures
|
|
96
|
+
}
|
|
97
|
+
setSidebarCollapsed(getBreakpoint(window.innerWidth) === 'small');
|
|
98
|
+
}, []); // intentionally empty — only runs once after first mount
|
|
88
99
|
const triggerExpandAll = useCallback(() => setDefaultExpanded(true), []);
|
|
89
100
|
const resetExpandAll = useCallback(() => setDefaultExpanded(null), []);
|
|
90
101
|
const setActiveLink = useCallback((href) => setActiveHref(href), []);
|
|
@@ -140,41 +151,6 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
|
|
|
140
151
|
})
|
|
141
152
|
: items;
|
|
142
153
|
}, [items, activeQuery]);
|
|
143
|
-
// Searching should expand all.
|
|
144
|
-
useEffect(() => {
|
|
145
|
-
if (activeQuery)
|
|
146
|
-
triggerExpandAll();
|
|
147
|
-
}, [activeQuery, triggerExpandAll]);
|
|
148
|
-
// Initial collapsed state: explicit prop > localStorage > responsive default.
|
|
149
|
-
useEffect(() => {
|
|
150
|
-
if (typeof window === 'undefined')
|
|
151
|
-
return;
|
|
152
|
-
const currentBreakpoint = getBreakpoint(window.innerWidth);
|
|
153
|
-
if (hasExplicitInitialSidebarCollapsed) {
|
|
154
|
-
const value = initialSidebarCollapsed !== null && initialSidebarCollapsed !== void 0 ? initialSidebarCollapsed : false;
|
|
155
|
-
setSidebarCollapsed(value);
|
|
156
|
-
try {
|
|
157
|
-
window.localStorage.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, JSON.stringify(value));
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
// ignore persist failures
|
|
161
|
-
}
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
try {
|
|
165
|
-
const stored = window.localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY);
|
|
166
|
-
if (stored !== null) {
|
|
167
|
-
const parsed = JSON.parse(stored);
|
|
168
|
-
setSidebarCollapsed(Boolean(parsed));
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
// ignore parse failures
|
|
174
|
-
}
|
|
175
|
-
// Nothing stored -> responsive default (do NOT persist automatic choice)
|
|
176
|
-
setSidebarCollapsed(currentBreakpoint === 'small');
|
|
177
|
-
}, [hasExplicitInitialSidebarCollapsed, initialSidebarCollapsed]);
|
|
178
154
|
const persistCollapsed = useCallback((collapsed) => {
|
|
179
155
|
if (typeof window === 'undefined')
|
|
180
156
|
return;
|
|
@@ -208,12 +184,14 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
|
|
|
208
184
|
return () => window.removeEventListener('resize', onResize);
|
|
209
185
|
}, []);
|
|
210
186
|
const value = useMemo(() => ({
|
|
211
|
-
defaultExpanded,
|
|
187
|
+
defaultExpanded: activeQuery ? true : defaultExpanded,
|
|
212
188
|
expandedItems,
|
|
213
189
|
filteredItems,
|
|
214
190
|
activeQuery,
|
|
191
|
+
wrapItemText,
|
|
215
192
|
areItemsCollapsed,
|
|
216
193
|
setActiveQuery,
|
|
194
|
+
setWrapItemText,
|
|
217
195
|
triggerExpandAll,
|
|
218
196
|
resetExpandAll,
|
|
219
197
|
setItemsCollapsed,
|
|
@@ -229,8 +207,10 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
|
|
|
229
207
|
expandedItems,
|
|
230
208
|
filteredItems,
|
|
231
209
|
activeQuery,
|
|
210
|
+
wrapItemText,
|
|
232
211
|
areItemsCollapsed,
|
|
233
212
|
setActiveQuery,
|
|
213
|
+
setWrapItemText,
|
|
234
214
|
triggerExpandAll,
|
|
235
215
|
resetExpandAll,
|
|
236
216
|
setItemsCollapsed,
|
package/dist/styles/styles.css
CHANGED
|
@@ -122,7 +122,12 @@ body.dbc-app {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
.dbc-highlight {
|
|
125
|
-
|
|
125
|
+
color: var(--color-highlight-fg, inherit);
|
|
126
|
+
background-color: var(--color-highlight-bg, var(--color-status-warning-bg));
|
|
127
|
+
border-radius: var(--border-radius-sm);
|
|
128
|
+
padding-inline: var(--spacing-2xs);
|
|
129
|
+
box-decoration-break: clone;
|
|
130
|
+
-webkit-box-decoration-break: clone;
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
.dbc-muted-text {
|
|
@@ -83,6 +83,8 @@ html[data-theme='dark'] {
|
|
|
83
83
|
/* Selected */
|
|
84
84
|
--color-bg-selected: var(--dbc-blue-100);
|
|
85
85
|
--color-bg-selected-hover: var(--dbc-blue-150);
|
|
86
|
+
--color-highlight-bg: var(--color-bg-selected-hover);
|
|
87
|
+
--color-highlight-fg: var(--color-fg-default);
|
|
86
88
|
|
|
87
89
|
--color-neutral: var(--dbc-neutral-200);
|
|
88
90
|
|
|
@@ -83,6 +83,8 @@ html[data-theme='light'] {
|
|
|
83
83
|
/* Selected */
|
|
84
84
|
--color-bg-selected: var(--dbc-blue-100);
|
|
85
85
|
--color-bg-selected-hover: var(--dbc-blue-150);
|
|
86
|
+
--color-highlight-bg: var(--color-bg-selected-hover);
|
|
87
|
+
--color-highlight-fg: var(--color-fg-default);
|
|
86
88
|
|
|
87
89
|
--color-neutral: var(--dbc-neutral-200);
|
|
88
90
|
|
package/dist/styles.css
CHANGED
|
@@ -122,7 +122,12 @@ body.dbc-app {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
.dbc-highlight {
|
|
125
|
-
|
|
125
|
+
color: var(--color-highlight-fg, inherit);
|
|
126
|
+
background-color: var(--color-highlight-bg, var(--color-status-warning-bg));
|
|
127
|
+
border-radius: var(--border-radius-sm);
|
|
128
|
+
padding-inline: var(--spacing-2xs);
|
|
129
|
+
box-decoration-break: clone;
|
|
130
|
+
-webkit-box-decoration-break: clone;
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
.dbc-muted-text {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function normalizeTerms(query) {
|
|
2
|
+
const terms = Array.isArray(query) ? query : [query];
|
|
3
|
+
return [...new Set(terms.map(term => term.trim().toLowerCase()).filter(Boolean))].sort((a, b) => b.length - a.length);
|
|
4
|
+
}
|
|
5
|
+
export function getHighlightedSegments(text, query) {
|
|
6
|
+
const terms = normalizeTerms(query);
|
|
7
|
+
if (!text || terms.length === 0)
|
|
8
|
+
return [{ text, matched: false }];
|
|
9
|
+
const lower = text.toLowerCase();
|
|
10
|
+
const ranges = [];
|
|
11
|
+
for (const term of terms) {
|
|
12
|
+
let startIndex = 0;
|
|
13
|
+
while (startIndex < lower.length) {
|
|
14
|
+
const matchIndex = lower.indexOf(term, startIndex);
|
|
15
|
+
if (matchIndex === -1)
|
|
16
|
+
break;
|
|
17
|
+
ranges.push({ start: matchIndex, end: matchIndex + term.length });
|
|
18
|
+
startIndex = matchIndex + term.length;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (ranges.length === 0)
|
|
22
|
+
return [{ text, matched: false }];
|
|
23
|
+
ranges.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
24
|
+
const mergedRanges = [];
|
|
25
|
+
for (const range of ranges) {
|
|
26
|
+
const previous = mergedRanges[mergedRanges.length - 1];
|
|
27
|
+
if (!previous || range.start > previous.end) {
|
|
28
|
+
mergedRanges.push({ ...range });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
previous.end = Math.max(previous.end, range.end);
|
|
32
|
+
}
|
|
33
|
+
const segments = [];
|
|
34
|
+
let cursor = 0;
|
|
35
|
+
for (const range of mergedRanges) {
|
|
36
|
+
if (range.start > cursor) {
|
|
37
|
+
segments.push({ text: text.slice(cursor, range.start), matched: false });
|
|
38
|
+
}
|
|
39
|
+
segments.push({ text: text.slice(range.start, range.end), matched: true });
|
|
40
|
+
cursor = range.end;
|
|
41
|
+
}
|
|
42
|
+
if (cursor < text.length) {
|
|
43
|
+
segments.push({ text: text.slice(cursor), matched: false });
|
|
44
|
+
}
|
|
45
|
+
return segments.length > 0 ? segments : [{ text, matched: false }];
|
|
46
|
+
}
|