@dbcdk/react-components 0.0.179 → 0.0.181

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.
Files changed (29) hide show
  1. package/README.md +4 -3
  2. package/dist/components/clear-button/ClearButton.cjs +7 -2
  3. package/dist/components/clear-button/ClearButton.d.ts +3 -1
  4. package/dist/components/clear-button/ClearButton.js +7 -2
  5. package/dist/components/clear-button/ClearButton.module.css +9 -0
  6. package/dist/components/forms/input/Input.cjs +8 -1
  7. package/dist/components/forms/input/Input.js +8 -1
  8. package/dist/components/forms/multi-select/MultiSelect.cjs +7 -1
  9. package/dist/components/forms/multi-select/MultiSelect.js +7 -1
  10. package/dist/components/forms/select/Select.cjs +7 -1
  11. package/dist/components/forms/select/Select.js +7 -1
  12. package/dist/components/interval-select/IntervalSelect.cjs +7 -1
  13. package/dist/components/interval-select/IntervalSelect.js +7 -1
  14. package/dist/components/json-viewer/JsonViewer.cjs +196 -83
  15. package/dist/components/json-viewer/JsonViewer.d.ts +1 -1
  16. package/dist/components/json-viewer/JsonViewer.js +197 -84
  17. package/dist/components/json-viewer/JsonViewer.module.css +37 -44
  18. package/dist/components/json-viewer/types.d.ts +8 -0
  19. package/dist/components/search-box/SearchBox.cjs +9 -1
  20. package/dist/components/search-box/SearchBox.d.ts +3 -3
  21. package/dist/components/search-box/SearchBox.js +9 -1
  22. package/dist/components/tabs/Tabs.cjs +50 -21
  23. package/dist/components/tabs/Tabs.d.ts +7 -1
  24. package/dist/components/tabs/Tabs.js +50 -21
  25. package/dist/components/tabs/Tabs.module.css +91 -11
  26. package/dist/hooks/useGetActiveTheme.cjs +2 -1
  27. package/dist/hooks/useGetActiveTheme.d.ts +1 -0
  28. package/dist/hooks/useGetActiveTheme.js +2 -1
  29. package/package.json +1 -1
@@ -1,12 +1,17 @@
1
1
  'use client';
2
2
  import { jsxs, jsx } from 'react/jsx-runtime';
3
- import { Search, X } from 'lucide-react';
3
+ import { Search, X, ArrowDown, ArrowUp, ListTree, Braces, Sun, Moon, ClipboardCopy, MoreVertical } from 'lucide-react';
4
4
  import { useState, useId, useEffect, useMemo } from 'react';
5
+ import { Menu } from '../../components/menu/Menu';
6
+ import { Popover } from '../../components/popover/Popover';
7
+ import { usePersistentState } from '../../hooks/usePersistentState';
5
8
  import { JsonNode } from './JsonNode';
6
9
  import styles from './JsonViewer.module.css';
7
10
  import { useClipboardStatus } from './useClipboardStatus';
8
11
  import { normalizeSearch, isJsonValue, collectSearchMatches, collectExpandableNodeIds, collectInitiallyExpandedNodeIds } from './utils';
12
+ import { Button } from '../button/Button';
9
13
 
14
+ const COPY_ALL_ID = "__json-viewer-copy-all__";
10
15
  function JsonViewer({
11
16
  value,
12
17
  defaultExpandedDepth = 2,
@@ -16,13 +21,28 @@ function JsonViewer({
16
21
  searchPlaceholder = "S\xF8g i n\xF8gler, stier og v\xE6rdier",
17
22
  emptySearchText = "Ingen matchende noder fundet.",
18
23
  className,
19
- maxHeight
24
+ maxHeight,
25
+ appearance = "dark",
26
+ storageKey
20
27
  }) {
21
28
  const [queryInput, setQueryInput] = useState("");
22
29
  const [query, setQuery] = useState("");
23
30
  const normalizedQuery = normalizeSearch(query);
24
31
  const inputId = useId();
25
32
  const { copiedId, setCopiedId } = useClipboardStatus();
33
+ const { value: currentAppearance, setValue: setCurrentAppearance } = usePersistentState({
34
+ initialValue: appearance,
35
+ storageKey: storageKey ? `${storageKey}:appearance` : void 0,
36
+ validate: (stored) => stored === "dark" || stored === "light"
37
+ });
38
+ const { value: viewMode, setValue: setViewMode } = usePersistentState({
39
+ initialValue: "tree",
40
+ storageKey: storageKey ? `${storageKey}:viewMode` : void 0,
41
+ validate: (stored) => stored === "tree" || stored === "raw"
42
+ });
43
+ const handleToggleAppearance = () => {
44
+ setCurrentAppearance((current) => current === "dark" ? "light" : "dark");
45
+ };
26
46
  useEffect(() => {
27
47
  const timeoutId = window.setTimeout(() => {
28
48
  setQuery(queryInput);
@@ -47,6 +67,7 @@ function JsonViewer({
47
67
  () => new Set(collectInitiallyExpandedNodeIds(safeValue, defaultExpandedDepth, expandAll))
48
68
  );
49
69
  const hasResults = normalizedQuery ? matches.size > 0 : true;
70
+ const rawJson = useMemo(() => JSON.stringify(safeValue, null, 2), [safeValue]);
50
71
  const handleCopy = async (nodeId, rawValue) => {
51
72
  try {
52
73
  await navigator.clipboard.writeText(rawValue);
@@ -55,6 +76,7 @@ function JsonViewer({
55
76
  setCopiedId(null);
56
77
  }
57
78
  };
79
+ const handleCopyAll = () => handleCopy(COPY_ALL_ID, rawJson);
58
80
  const handleExpandAll = () => {
59
81
  setExpandedNodeIds(new Set(allExpandableNodeIds));
60
82
  };
@@ -69,90 +91,181 @@ function JsonViewer({
69
91
  return next;
70
92
  });
71
93
  };
72
- return /* @__PURE__ */ jsxs("section", { className: [styles.viewer, className].filter(Boolean).join(" "), children: [
73
- /* @__PURE__ */ jsxs("div", { className: styles.toolbar, children: [
74
- /* @__PURE__ */ jsxs("div", { className: styles.toolbarTop, children: [
75
- /* @__PURE__ */ jsxs("label", { htmlFor: inputId, className: styles.searchField, children: [
76
- /* @__PURE__ */ jsx(Search, { className: styles.searchIcon, "aria-hidden": "true" }),
77
- /* @__PURE__ */ jsx(
78
- "input",
79
- {
80
- id: inputId,
81
- type: "search",
82
- value: queryInput,
83
- onChange: (event) => setQueryInput(event.target.value),
84
- placeholder: searchPlaceholder,
85
- className: styles.searchInput
86
- }
87
- ),
88
- queryInput ? /* @__PURE__ */ jsx(
89
- "button",
90
- {
91
- type: "button",
92
- className: styles.clearButton,
93
- "aria-label": "Ryd s\xF8gning",
94
- onClick: () => {
95
- setQueryInput("");
96
- setQuery("");
97
- },
98
- children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
99
- }
100
- ) : null
101
- ] }),
102
- /* @__PURE__ */ jsxs("div", { className: styles.actions, children: [
103
- /* @__PURE__ */ jsx(
104
- "button",
105
- {
106
- type: "button",
107
- className: styles.toolbarButton,
108
- onClick: handleExpandAll,
109
- disabled: Boolean(normalizedQuery),
110
- children: "Fold alle ud"
111
- }
112
- ),
113
- /* @__PURE__ */ jsx(
114
- "button",
115
- {
116
- type: "button",
117
- className: styles.toolbarButton,
118
- onClick: handleCollapseAll,
119
- disabled: Boolean(normalizedQuery),
120
- children: "Fold alle sammen"
121
- }
122
- )
123
- ] })
124
- ] }),
125
- /* @__PURE__ */ jsxs("div", { className: styles.metaRow, children: [
126
- helperText ? /* @__PURE__ */ jsx("span", { className: styles.helperText, children: helperText }) : /* @__PURE__ */ jsx("span", {}),
127
- /* @__PURE__ */ jsx("span", { className: styles.statusText, children: normalizedQuery ? `${matches.size} matchende node${matches.size === 1 ? "" : "r"}` : "" })
128
- ] })
129
- ] }),
130
- /* @__PURE__ */ jsx(
131
- "div",
132
- {
133
- className: styles.treePane,
134
- role: "tree",
135
- "aria-label": "JSON-visning",
136
- style: maxHeight !== void 0 ? { maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight } : void 0,
137
- children: hasResults ? /* @__PURE__ */ jsx(
138
- JsonNode,
94
+ return /* @__PURE__ */ jsxs(
95
+ "section",
96
+ {
97
+ className: [styles.viewer, className].filter(Boolean).join(" "),
98
+ "data-appearance": currentAppearance,
99
+ children: [
100
+ /* @__PURE__ */ jsxs(
101
+ "div",
139
102
  {
140
- keyName: void 0,
141
- path: [],
142
- value: safeValue,
143
- depth: 0,
144
- defaultExpandedDepth,
145
- query: normalizedQuery,
146
- matches,
147
- expandedNodeIds,
148
- onToggleNode: handleToggleNode,
149
- copiedId,
150
- onCopy: handleCopy
103
+ className: styles.toolbar,
104
+ "data-surface": currentAppearance === "dark" ? "inverse" : void 0,
105
+ children: [
106
+ /* @__PURE__ */ jsxs("div", { className: styles.toolbarTop, children: [
107
+ viewMode === "tree" ? /* @__PURE__ */ jsxs("label", { htmlFor: inputId, className: styles.searchField, children: [
108
+ /* @__PURE__ */ jsx(Search, { className: styles.searchIcon, "aria-hidden": "true" }),
109
+ /* @__PURE__ */ jsx(
110
+ "input",
111
+ {
112
+ id: inputId,
113
+ type: "search",
114
+ value: queryInput,
115
+ onChange: (event) => setQueryInput(event.target.value),
116
+ placeholder: searchPlaceholder,
117
+ className: styles.searchInput
118
+ }
119
+ ),
120
+ queryInput ? /* @__PURE__ */ jsx(
121
+ "button",
122
+ {
123
+ type: "button",
124
+ className: styles.clearButton,
125
+ "aria-label": "Ryd s\xF8gning",
126
+ onClick: () => {
127
+ setQueryInput("");
128
+ setQuery("");
129
+ },
130
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
131
+ }
132
+ ) : null
133
+ ] }) : /* @__PURE__ */ jsx("span", {}),
134
+ /* @__PURE__ */ jsx("div", { className: styles.actions, children: /* @__PURE__ */ jsx(
135
+ Popover,
136
+ {
137
+ matchTriggerWidth: false,
138
+ minWidth: "200px",
139
+ trigger: (toggle, _icon, isOpen) => /* @__PURE__ */ jsx(
140
+ Button,
141
+ {
142
+ variant: "inline",
143
+ "aria-label": "Flere handlinger",
144
+ "aria-haspopup": "menu",
145
+ "aria-expanded": isOpen ? true : false,
146
+ onClick: toggle,
147
+ children: /* @__PURE__ */ jsx(MoreVertical, { "aria-hidden": "true" })
148
+ }
149
+ ),
150
+ children: (close) => /* @__PURE__ */ jsxs(Menu, { children: [
151
+ /* @__PURE__ */ jsx(Menu.Item, { disabled: viewMode === "raw" || Boolean(normalizedQuery), children: /* @__PURE__ */ jsxs(
152
+ "button",
153
+ {
154
+ type: "button",
155
+ onClick: () => {
156
+ handleExpandAll();
157
+ close();
158
+ },
159
+ children: [
160
+ /* @__PURE__ */ jsx(ArrowDown, { "aria-hidden": "true" }),
161
+ /* @__PURE__ */ jsx("span", { children: "Fold alle ud" })
162
+ ]
163
+ }
164
+ ) }),
165
+ /* @__PURE__ */ jsx(Menu.Item, { disabled: viewMode === "raw" || Boolean(normalizedQuery), children: /* @__PURE__ */ jsxs(
166
+ "button",
167
+ {
168
+ type: "button",
169
+ onClick: () => {
170
+ handleCollapseAll();
171
+ close();
172
+ },
173
+ children: [
174
+ /* @__PURE__ */ jsx(ArrowUp, { "aria-hidden": "true" }),
175
+ /* @__PURE__ */ jsx("span", { children: "Fold alle sammen" })
176
+ ]
177
+ }
178
+ ) }),
179
+ /* @__PURE__ */ jsx(Menu.Separator, {}),
180
+ /* @__PURE__ */ jsx(Menu.Item, { children: /* @__PURE__ */ jsxs(
181
+ "button",
182
+ {
183
+ type: "button",
184
+ onClick: () => {
185
+ setViewMode((mode) => mode === "raw" ? "tree" : "raw");
186
+ close();
187
+ },
188
+ children: [
189
+ viewMode === "raw" ? /* @__PURE__ */ jsx(ListTree, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Braces, { "aria-hidden": "true" }),
190
+ /* @__PURE__ */ jsx("span", { children: viewMode === "raw" ? "Vis tr\xE6" : "Vis raw" })
191
+ ]
192
+ }
193
+ ) }),
194
+ /* @__PURE__ */ jsx(Menu.Item, { children: /* @__PURE__ */ jsxs(
195
+ "button",
196
+ {
197
+ type: "button",
198
+ onClick: () => {
199
+ handleToggleAppearance();
200
+ close();
201
+ },
202
+ children: [
203
+ currentAppearance === "dark" ? /* @__PURE__ */ jsx(Sun, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Moon, { "aria-hidden": "true" }),
204
+ /* @__PURE__ */ jsx("span", { children: currentAppearance === "dark" ? "Lys visning" : "M\xF8rk visning" })
205
+ ]
206
+ }
207
+ ) }),
208
+ /* @__PURE__ */ jsx(Menu.Separator, {}),
209
+ /* @__PURE__ */ jsx(Menu.Item, { children: /* @__PURE__ */ jsxs(
210
+ "button",
211
+ {
212
+ type: "button",
213
+ onClick: () => {
214
+ handleCopyAll();
215
+ close();
216
+ },
217
+ children: [
218
+ /* @__PURE__ */ jsx(ClipboardCopy, { "aria-hidden": "true" }),
219
+ /* @__PURE__ */ jsx("span", { children: "Kopi\xE9r JSON" })
220
+ ]
221
+ }
222
+ ) })
223
+ ] })
224
+ }
225
+ ) })
226
+ ] }),
227
+ helperText || viewMode === "tree" && normalizedQuery ? /* @__PURE__ */ jsxs("div", { className: styles.metaRow, children: [
228
+ helperText ? /* @__PURE__ */ jsx("span", { className: styles.helperText, children: helperText }) : /* @__PURE__ */ jsx("span", {}),
229
+ /* @__PURE__ */ jsx("span", { className: styles.statusText, children: viewMode === "tree" && normalizedQuery ? `${matches.size} matchende node${matches.size === 1 ? "" : "r"}` : "" })
230
+ ] }) : null
231
+ ]
151
232
  }
152
- ) : /* @__PURE__ */ jsx("div", { className: styles.emptyState, children: emptySearchText })
153
- }
154
- )
155
- ] });
233
+ ),
234
+ viewMode === "raw" ? /* @__PURE__ */ jsx(
235
+ "pre",
236
+ {
237
+ className: styles.rawPane,
238
+ style: maxHeight !== void 0 ? { maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight } : void 0,
239
+ children: rawJson
240
+ }
241
+ ) : /* @__PURE__ */ jsx(
242
+ "div",
243
+ {
244
+ className: styles.treePane,
245
+ role: "tree",
246
+ "aria-label": "JSON-visning",
247
+ style: maxHeight !== void 0 ? { maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight } : void 0,
248
+ children: hasResults ? /* @__PURE__ */ jsx(
249
+ JsonNode,
250
+ {
251
+ keyName: void 0,
252
+ path: [],
253
+ value: safeValue,
254
+ depth: 0,
255
+ defaultExpandedDepth,
256
+ query: normalizedQuery,
257
+ matches,
258
+ expandedNodeIds,
259
+ onToggleNode: handleToggleNode,
260
+ copiedId,
261
+ onCopy: handleCopy
262
+ }
263
+ ) : /* @__PURE__ */ jsx("div", { className: styles.emptyState, children: emptySearchText })
264
+ }
265
+ )
266
+ ]
267
+ }
268
+ );
156
269
  }
157
270
 
158
271
  export { JsonViewer };
@@ -22,16 +22,35 @@
22
22
 
23
23
  display: flex;
24
24
  flex-direction: column;
25
- gap: var(--spacing-sm);
25
+ gap: var(--spacing-xs);
26
26
  background: var(--json-pane-bg);
27
27
  color: var(--json-pane-text);
28
28
  border: var(--border-width-thin) solid var(--json-pane-border);
29
- border-radius: var(--border-radius-xl);
29
+ border-radius: var(--border-radius-default);
30
30
  box-shadow: var(--shadow-md);
31
- padding: var(--spacing-sm);
31
+ padding: var(--spacing-xs);
32
32
  font-family: var(--font-family-mono);
33
33
  }
34
34
 
35
+ .viewer[data-appearance='light'] {
36
+ --json-pane-bg: var(--color-bg-surface);
37
+ --json-pane-border: var(--color-border-default);
38
+ --json-pane-border-strong: var(--color-border-strong);
39
+ --json-pane-text: var(--color-fg-default);
40
+ --json-pane-muted: var(--color-fg-muted);
41
+ --json-pane-subtle: var(--color-fg-subtle);
42
+ --json-pane-hover: var(--color-bg-hover-subtle);
43
+ --json-pane-selected: color-mix(in oklab, var(--color-brand) 16%, transparent);
44
+ --color-highlight-bg: color-mix(in oklab, var(--json-pane-highlight) 32%, transparent);
45
+ --color-highlight-fg: var(--color-fg-default);
46
+ --json-pane-key: color-mix(in oklab, var(--color-fg-default) 88%, var(--color-syntax-accent) 12%);
47
+ --json-pane-string: color-mix(in oklab, var(--color-syntax-string) 70%, black 30%);
48
+ --json-pane-number: color-mix(in oklab, var(--color-syntax-number) 65%, black 35%);
49
+ --json-pane-boolean: color-mix(in oklab, var(--color-syntax-boolean) 70%, black 30%);
50
+ --json-pane-null: var(--color-fg-subtle);
51
+ box-shadow: none;
52
+ }
53
+
35
54
  .toolbar {
36
55
  display: grid;
37
56
  gap: var(--spacing-xs);
@@ -52,7 +71,7 @@
52
71
  min-height: var(--component-size-md);
53
72
  padding-inline: var(--spacing-sm);
54
73
  border: var(--border-width-thin) solid var(--json-pane-border);
55
- border-radius: var(--border-radius-lg);
74
+ border-radius: var(--border-radius-sm);
56
75
  background: transparent;
57
76
  }
58
77
 
@@ -114,31 +133,6 @@
114
133
  gap: var(--spacing-xs);
115
134
  }
116
135
 
117
- .toolbarButton {
118
- min-block-size: var(--component-size-sm);
119
- padding-inline: var(--spacing-sm);
120
- border: var(--border-width-thin) solid var(--json-pane-border);
121
- border-radius: var(--border-radius-md);
122
- background: transparent;
123
- color: var(--json-pane-text);
124
- font: inherit;
125
- cursor: pointer;
126
- }
127
-
128
- .toolbarButton:hover:not(:disabled) {
129
- background: var(--json-pane-hover);
130
- }
131
-
132
- .toolbarButton:focus-visible {
133
- outline: none;
134
- box-shadow: var(--focus-ring);
135
- }
136
-
137
- .toolbarButton:disabled {
138
- opacity: 0.45;
139
- cursor: default;
140
- }
141
-
142
136
  .helperText,
143
137
  .statusText {
144
138
  min-inline-size: 0;
@@ -153,6 +147,17 @@
153
147
  overflow: auto;
154
148
  }
155
149
 
150
+ .rawPane {
151
+ margin: 0;
152
+ padding: var(--spacing-xs);
153
+ overflow: auto;
154
+ white-space: pre-wrap;
155
+ word-break: break-word;
156
+ color: var(--json-pane-text);
157
+ font-family: inherit;
158
+ font-size: inherit;
159
+ }
160
+
156
161
  .node {
157
162
  display: flex;
158
163
  flex-direction: column;
@@ -168,7 +173,7 @@
168
173
  display: flex;
169
174
  align-items: flex-start;
170
175
  min-inline-size: 0;
171
- border-radius: var(--border-radius-md);
176
+ border-radius: var(--border-radius-default);
172
177
  }
173
178
 
174
179
  .row[data-match='true'] {
@@ -186,7 +191,7 @@
186
191
  padding-inline: var(--spacing-xs);
187
192
  background: transparent;
188
193
  border: 0;
189
- border-radius: var(--border-radius-md);
194
+ border-radius: var(--border-radius-default);
190
195
  color: inherit;
191
196
  text-align: left;
192
197
  cursor: pointer;
@@ -256,18 +261,6 @@
256
261
  display: flex;
257
262
  flex-direction: column;
258
263
  gap: 1px;
259
- position: relative;
260
- }
261
-
262
- .children::before {
263
- content: '';
264
- position: absolute;
265
- left: calc(var(--spacing-md) - 2px);
266
- top: 0;
267
- bottom: 0;
268
- width: 1px;
269
- background: color-mix(in oklab, var(--color-fg-inverse) 8%, transparent);
270
- pointer-events: none;
271
264
  }
272
265
 
273
266
  .value {
@@ -317,7 +310,7 @@
317
310
  .valueButton[title='Kopieret'] .copyState {
318
311
  opacity: 1;
319
312
  transform: translateX(0);
320
- color: color-mix(in oklab, var(--color-syntax-string) 78%, white 22%);
313
+ color: var(--json-pane-string);
321
314
  }
322
315
 
323
316
  .emptyState {
@@ -7,6 +7,7 @@ export type SearchMatch = {
7
7
  self: boolean;
8
8
  descendant: boolean;
9
9
  };
10
+ export type JsonViewerAppearance = 'dark' | 'light';
10
11
  export interface JsonViewerProps {
11
12
  value: unknown;
12
13
  defaultExpandedDepth?: number;
@@ -17,6 +18,13 @@ export interface JsonViewerProps {
17
18
  emptySearchText?: string;
18
19
  className?: string;
19
20
  maxHeight?: number | string;
21
+ /** Initial appearance. The user can toggle it via the kebab menu; defaults to 'dark'. */
22
+ appearance?: JsonViewerAppearance;
23
+ /**
24
+ * When set, the user's appearance choice is persisted to localStorage
25
+ * under `${storageKey}:appearance`.
26
+ */
27
+ storageKey?: string;
20
28
  }
21
29
  export type JsonNodeProps = {
22
30
  keyName?: string;
@@ -49,6 +49,7 @@ const SearchBox = react.forwardRef(function SearchBoxInner({
49
49
  const popoverRef = react.useRef(null);
50
50
  const internalInputRef = react.useRef(null);
51
51
  const onSearchRef = react.useRef(onSearch);
52
+ const skipNextSearch = react.useRef(false);
52
53
  react.useEffect(() => {
53
54
  if (typeof ref === "function") {
54
55
  ref(internalInputRef.current);
@@ -59,13 +60,20 @@ const SearchBox = react.forwardRef(function SearchBoxInner({
59
60
  react.useEffect(() => {
60
61
  if (!isControlled) return;
61
62
  const next = String(value != null ? value : "");
62
- if (next !== draft) setDraft(next);
63
+ if (next !== draft) {
64
+ skipNextSearch.current = true;
65
+ setDraft(next);
66
+ }
63
67
  }, [value]);
64
68
  react.useEffect(() => {
65
69
  onSearchRef.current = onSearch;
66
70
  }, [onSearch]);
67
71
  react.useEffect(() => {
68
72
  if (!onSearchRef.current) return;
73
+ if (skipNextSearch.current) {
74
+ skipNextSearch.current = false;
75
+ return;
76
+ }
69
77
  if (!debounce) {
70
78
  onSearchRef.current(draft);
71
79
  return;
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
- import { InputVariant } from '../../components/forms/input/Input';
2
+ import { type InputProps, InputVariant } from '../../components/forms/input/Input';
3
3
  import { Size } from '../../types/sizes.types';
4
- export type SearchBoxProps<T extends Record<string, unknown>> = {
4
+ export type SearchBoxProps<T extends Record<string, unknown>> = Omit<InputProps, 'icon' | 'inputSize' | 'minWidth' | 'maxWidth' | 'width' | 'variant' | 'onClear' | 'onButtonClick' | 'buttonLabel' | 'buttonIcon' | 'buttonAriaLabel'> & {
5
5
  inputWidth?: string | number;
6
6
  maxWidth?: string | number;
7
7
  inputSize?: Exclude<Size, 'xl'>;
@@ -24,7 +24,7 @@ export type SearchBoxProps<T extends Record<string, unknown>> = {
24
24
  buttonLabel?: string;
25
25
  buttonIcon?: React.ReactNode;
26
26
  buttonAriaLabel?: string;
27
- } & React.InputHTMLAttributes<HTMLInputElement>;
27
+ };
28
28
  type SearchBoxComponent = <T extends Record<string, unknown>>(props: SearchBoxProps<T> & React.RefAttributes<HTMLInputElement>) => React.ReactElement | null;
29
29
  export declare const SearchBox: SearchBoxComponent;
30
30
  export {};
@@ -43,6 +43,7 @@ const SearchBox = forwardRef(function SearchBoxInner({
43
43
  const popoverRef = useRef(null);
44
44
  const internalInputRef = useRef(null);
45
45
  const onSearchRef = useRef(onSearch);
46
+ const skipNextSearch = useRef(false);
46
47
  useEffect(() => {
47
48
  if (typeof ref === "function") {
48
49
  ref(internalInputRef.current);
@@ -53,13 +54,20 @@ const SearchBox = forwardRef(function SearchBoxInner({
53
54
  useEffect(() => {
54
55
  if (!isControlled) return;
55
56
  const next = String(value != null ? value : "");
56
- if (next !== draft) setDraft(next);
57
+ if (next !== draft) {
58
+ skipNextSearch.current = true;
59
+ setDraft(next);
60
+ }
57
61
  }, [value]);
58
62
  useEffect(() => {
59
63
  onSearchRef.current = onSearch;
60
64
  }, [onSearch]);
61
65
  useEffect(() => {
62
66
  if (!onSearchRef.current) return;
67
+ if (skipNextSearch.current) {
68
+ skipNextSearch.current = false;
69
+ return;
70
+ }
63
71
  if (!debounce) {
64
72
  onSearchRef.current(draft);
65
73
  return;
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
+ var lucideReact = require('lucide-react');
4
5
  var react = require('react');
5
6
  var usePersistentState = require('../../hooks/usePersistentState');
6
7
  var styles = require('./Tabs.module.css');
@@ -37,6 +38,7 @@ function normalizeFromChildren(children) {
37
38
  disabled: props.disabled,
38
39
  hidden: props.hidden,
39
40
  badge: props.badge,
41
+ removable: props.removable,
40
42
  content: props.children
41
43
  });
42
44
  });
@@ -52,6 +54,8 @@ function Tabs({
52
54
  defaultValue,
53
55
  storageKey,
54
56
  onValueChange,
57
+ onRemoveTab,
58
+ onAddTab,
55
59
  addition,
56
60
  loading = false,
57
61
  children
@@ -144,31 +148,56 @@ function Tabs({
144
148
  }
145
149
  ) : null,
146
150
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${styles__default.default.tabs} ${styles__default.default[variant]} ${panelStyle ? styles__default.default.panelStyle : ""}`, children: [
147
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.tabList, role: "tablist", "aria-label": header != null ? header : "Tabs", children: visibleTabs.map((tab, index) => {
148
- const selected = index === activeIndex;
149
- const tabDomId = `${uid}-tab-${String(tab.id)}`;
150
- const panelDomId = `${uid}-panel-${String(tab.id)}`;
151
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${styles__default.default.tab} ${selected ? styles__default.default.active : ""}`, children: /* @__PURE__ */ jsxRuntime.jsxs(
151
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.tabList, children: [
152
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.tabButtons, role: "tablist", "aria-label": header != null ? header : "Tabs", children: visibleTabs.map((tab, index) => {
153
+ const selected = index === activeIndex;
154
+ const tabDomId = `${uid}-tab-${String(tab.id)}`;
155
+ const panelDomId = `${uid}-panel-${String(tab.id)}`;
156
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${styles__default.default.tab} ${selected ? styles__default.default.active : ""}`, children: [
157
+ /* @__PURE__ */ jsxRuntime.jsxs(
158
+ "button",
159
+ {
160
+ id: tabDomId,
161
+ type: "button",
162
+ className: styles__default.default.tabButton,
163
+ role: "tab",
164
+ "aria-selected": selected,
165
+ "aria-controls": panelDomId,
166
+ tabIndex: selected ? 0 : -1,
167
+ disabled: tab.disabled,
168
+ onClick: () => setValue(tab.id),
169
+ onKeyDown: (e) => onKeyDownTab(e, index),
170
+ children: [
171
+ tab.headerIcon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.icon, children: tab.headerIcon }) : null,
172
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.label, children: tab.header }),
173
+ tab.badge ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.badge, children: /* @__PURE__ */ jsxRuntime.jsx(Chip.Chip, { size: "sm", children: tab.badge.toLocaleString("da-DK") }) }) : null
174
+ ]
175
+ }
176
+ ),
177
+ tab.removable && onRemoveTab ? /* @__PURE__ */ jsxRuntime.jsx(
178
+ "button",
179
+ {
180
+ type: "button",
181
+ className: styles__default.default.removeTabButton,
182
+ "aria-label": `Fjern fane ${tab.header}`,
183
+ disabled: tab.disabled,
184
+ onClick: () => onRemoveTab(tab.id, tab, index),
185
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true" })
186
+ }
187
+ ) : null
188
+ ] }, tab.id);
189
+ }) }),
190
+ onAddTab ? /* @__PURE__ */ jsxRuntime.jsx(
152
191
  "button",
153
192
  {
154
- id: tabDomId,
155
193
  type: "button",
156
- className: styles__default.default.tabButton,
157
- role: "tab",
158
- "aria-selected": selected,
159
- "aria-controls": panelDomId,
160
- tabIndex: selected ? 0 : -1,
161
- disabled: tab.disabled,
162
- onClick: () => setValue(tab.id),
163
- onKeyDown: (e) => onKeyDownTab(e, index),
164
- children: [
165
- tab.headerIcon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.icon, children: tab.headerIcon }) : null,
166
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.label, children: tab.header }),
167
- tab.badge ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.badge, children: /* @__PURE__ */ jsxRuntime.jsx(Chip.Chip, { size: "sm", children: tab.badge.toLocaleString("da-DK") }) }) : null
168
- ]
194
+ className: styles__default.default.addTabButton,
195
+ onClick: onAddTab,
196
+ "aria-label": "Tilf\xF8j fane",
197
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { "aria-hidden": "true" })
169
198
  }
170
- ) }, tab.id);
171
- }) }),
199
+ ) : null
200
+ ] }),
172
201
  /* @__PURE__ */ jsxRuntime.jsx(
173
202
  "div",
174
203
  {