@dbcdk/react-components 0.0.18 → 0.0.19

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.
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { JSX, ReactNode } from 'react';
3
- type Variant = 'default' | 'primary' | 'outlined' | 'success';
3
+ type Variant = 'default' | 'primary' | 'outlined' | 'success' | 'info';
4
4
  type Size = 'sm' | 'md' | 'lg';
5
5
  interface CheckboxProps {
6
6
  checked?: boolean;
@@ -66,6 +66,13 @@
66
66
  &:not(:hover) {
67
67
  border-color: transparent;
68
68
  }
69
+ }
70
+
71
+ .info.checked {
72
+ background-color: var(--color-status-info);
73
+ &:not(:hover) {
74
+ border-color: transparent;
75
+ }
69
76
 
70
77
  .icon {
71
78
  color: var(--color-fg-on-brand);
@@ -1,6 +1,6 @@
1
1
  import type { JSX } from 'react';
2
2
  import * as React from 'react';
3
- import { NavBarItem } from '../../../components/nav-bar/NavBar';
3
+ import type { NavBarItem } from '../../../components/nav-bar/NavBar';
4
4
  export type SidebarContextValue = {
5
5
  defaultExpanded: boolean | null;
6
6
  expandedItems: Set<string>;
@@ -2,24 +2,54 @@
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
+ */
5
12
  const hasChildren = (item) => Array.isArray(item.children) && item.children.length > 0;
6
13
  const hasHref = (item) => typeof item.href === 'string' && item.href.length > 0;
7
- const findParentItem = (navItem, items, prevPath = '') => {
8
- for (const currentItem of items) {
9
- // Groups don't contribute to the href path; they just wrap children
10
- const nextPath = hasHref(currentItem) ? `${prevPath}.${currentItem.href}` : prevPath;
11
- if (hasChildren(currentItem)) {
12
- // Direct child match
13
- if (currentItem.children.some(child => hasHref(child) && child.href === navItem)) {
14
- return nextPath;
14
+ const normalizeHref = (href) => {
15
+ if (!href)
16
+ return href;
17
+ // strip trailing slashes except root
18
+ return href.length > 1 ? href.replace(/\/+$/, '') : href;
19
+ };
20
+ /**
21
+ * Returns the chain of "expandable parents" (hrefs) that contain `targetHref`.
22
+ *
23
+ * Behavior:
24
+ * - If targetHref matches an expandable parent item itself, the chain includes it.
25
+ * - If targetHref matches a leaf under expandables, the chain includes expandable ancestors.
26
+ *
27
+ * Expandable parent definition:
28
+ * - any node with children AND an href
29
+ * (If you want to restrict this to a specific type, update isExpandableParent.)
30
+ */
31
+ const findExpandableParentChain = (targetHref, items) => {
32
+ var _a;
33
+ const target = normalizeHref(targetHref);
34
+ const dfs = (nodes, parentExpandables) => {
35
+ for (const node of nodes) {
36
+ const nodeHref = hasHref(node) ? normalizeHref(node.href) : null;
37
+ const isExpandableParent = hasChildren(node) && hasHref(node);
38
+ const nextParents = isExpandableParent && nodeHref ? [...parentExpandables, nodeHref] : parentExpandables;
39
+ // Match this node
40
+ if (nodeHref === target) {
41
+ return isExpandableParent ? nextParents : parentExpandables;
42
+ }
43
+ // Recurse into children
44
+ if (hasChildren(node)) {
45
+ const found = dfs(node.children, nextParents);
46
+ if (found)
47
+ return found;
15
48
  }
16
- // Recurse
17
- const path = findParentItem(navItem, currentItem.children, nextPath);
18
- if (path)
19
- return path;
20
49
  }
21
- }
22
- return '';
50
+ return null;
51
+ };
52
+ return (_a = dfs(items, [])) !== null && _a !== void 0 ? _a : [];
23
53
  };
24
54
  const SidebarContext = createContext({
25
55
  defaultExpanded: null,
@@ -46,10 +76,9 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
46
76
  const [activeQuery, setActiveQuery] = useState('');
47
77
  const [areItemsCollapsed, setItemsCollapsed] = useState(initialCollapsed);
48
78
  const [activeHref, setActiveHref] = useState('');
49
- // expandedItems is now the single source of truth for "open groups"
50
- // (it includes both auto-expanded parents and user-expanded groups)
79
+ // expandedItems is the source of truth for "open groups"
51
80
  const [expandedItems, setExpandedItems] = useState(new Set());
52
- // Track items in a ref to avoid effect loops if parent recreates the items array every render
81
+ // Keep latest items without creating effect loops if parent recreates the array
53
82
  const itemsRef = useRef(items);
54
83
  useEffect(() => {
55
84
  itemsRef.current = items;
@@ -60,40 +89,41 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
60
89
  const resetExpandAll = useCallback(() => setDefaultExpanded(null), []);
61
90
  const setActiveLink = useCallback((href) => setActiveHref(href), []);
62
91
  const expandItem = useCallback((href) => {
92
+ const h = normalizeHref(href);
63
93
  setExpandedItems(prev => {
64
- if (prev.has(href))
94
+ if (prev.has(h))
65
95
  return prev;
66
96
  const next = new Set(prev);
67
- next.add(href);
97
+ next.add(h);
68
98
  return next;
69
99
  });
70
100
  }, []);
71
101
  const collapseItem = useCallback((href) => {
102
+ const h = normalizeHref(href);
72
103
  setExpandedItems(prev => {
73
- if (!prev.has(href))
104
+ if (!prev.has(h))
74
105
  return prev;
75
106
  const next = new Set(prev);
76
- next.delete(href);
107
+ next.delete(h);
77
108
  return next;
78
109
  });
79
110
  }, []);
80
- const isExpanded = useCallback((href) => expandedItems.has(href), [expandedItems]);
81
- // Auto-expand: when active link changes, ensure its parent chain is expanded.
82
- // IMPORTANT: guard so we only set state when we actually add something.
111
+ const isExpanded = useCallback((href) => expandedItems.has(normalizeHref(href)), [expandedItems]);
112
+ // Auto-expand: when active link changes, ensure its expandable parent chain is expanded.
83
113
  useEffect(() => {
84
114
  if (!activeHref)
85
115
  return;
86
116
  const currentItems = itemsRef.current;
87
- const path = findParentItem(activeHref, currentItems);
88
- const parents = path.split('.').filter(Boolean);
117
+ const parents = findExpandableParentChain(activeHref, currentItems);
89
118
  if (parents.length === 0)
90
119
  return;
91
120
  setExpandedItems(prev => {
92
121
  let changed = false;
93
122
  const next = new Set(prev);
94
123
  for (const p of parents) {
95
- if (!next.has(p)) {
96
- next.add(p);
124
+ const norm = normalizeHref(p);
125
+ if (!next.has(norm)) {
126
+ next.add(norm);
97
127
  changed = true;
98
128
  }
99
129
  }
@@ -110,8 +140,7 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
110
140
  })
111
141
  : items;
112
142
  }, [items, activeQuery]);
113
- // Searching should expand all, but do not fight the user forever.
114
- // We just set defaultExpanded=true, and individual components can honor it.
143
+ // Searching should expand all.
115
144
  useEffect(() => {
116
145
  if (activeQuery)
117
146
  triggerExpandAll();
@@ -128,7 +157,7 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
128
157
  window.localStorage.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, JSON.stringify(value));
129
158
  }
130
159
  catch {
131
- console.error('Failed to persist sidebar collapsed state');
160
+ // ignore persist failures
132
161
  }
133
162
  return;
134
163
  }
@@ -141,9 +170,9 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
141
170
  }
142
171
  }
143
172
  catch {
144
- console.error('Failed to parse sidebar collapsed state from storage');
173
+ // ignore parse failures
145
174
  }
146
- // Nothing stored responsive default (but we do NOT persist this automatic choice)
175
+ // Nothing stored -> responsive default (do NOT persist automatic choice)
147
176
  setSidebarCollapsed(currentBreakpoint === 'small');
148
177
  }, [hasExplicitInitialSidebarCollapsed, initialSidebarCollapsed]);
149
178
  const persistCollapsed = useCallback((collapsed) => {
@@ -153,7 +182,7 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
153
182
  window.localStorage.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, JSON.stringify(collapsed));
154
183
  }
155
184
  catch {
156
- console.error('Failed to persist sidebar collapsed state');
185
+ // ignore persist failures
157
186
  }
158
187
  }, []);
159
188
  // Only persist user-triggered changes
@@ -163,7 +192,7 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
163
192
  }, [persistCollapsed]);
164
193
  // Resize behavior:
165
194
  // - only apply auto-collapse when breakpoint changes
166
- // - do NOT persist the automatic change (only user actions persist)
195
+ // - do NOT persist automatic changes (only user actions persist)
167
196
  useEffect(() => {
168
197
  if (typeof window === 'undefined')
169
198
  return;
@@ -4,14 +4,9 @@ export type TableEmptyConfig = {
4
4
  /** Keep it short + system-like */
5
5
  title?: string;
6
6
  description?: ReactNode;
7
- /**
8
- * Optional custom actions (rendered after built-ins).
9
- * Good for injecting extra links/buttons from the table page.
10
- */
11
7
  actions?: ReactNode;
12
8
  onBack?: () => void;
13
9
  onRefresh?: () => void;
14
- /** New: clear filters */
15
10
  onClearFilters?: () => void;
16
11
  showClearFilters?: boolean;
17
12
  clearFiltersLabel?: ReactNode;
@@ -1,18 +1,15 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ArrowLeft } from 'lucide-react';
3
- import { Button } from '../../../../components/button/Button';
4
3
  const defaultEmptyConfig = {
5
4
  enabled: true,
6
5
  title: 'Ingen resultater',
7
6
  description: _jsx("span", { children: "Ingen data at vise." }),
8
7
  showBack: false,
9
8
  showRefresh: false,
10
- // default: show button if handler exists
11
9
  showClearFilters: true,
12
10
  backLabel: (_jsxs(_Fragment, { children: [_jsx(ArrowLeft, { size: 16 }), "Tilbage"] })),
13
11
  refreshLabel: _jsx(_Fragment, { children: "Indl\u00E6s igen" }),
14
12
  clearFiltersLabel: _jsx(_Fragment, { children: "Nulstil alle filtre" }),
15
- // "system-like": less hero, less vertical drama
16
13
  className: 'dbc-flex dbc-flex-column dbc-items-start dbc-justify-start dbc-text-left dbc-gap-sm dbc-py-lg',
17
14
  };
18
15
  export function TableEmptyState({ config }) {
@@ -22,9 +19,5 @@ export function TableEmptyState({ config }) {
22
19
  };
23
20
  if (!merged.enabled)
24
21
  return null;
25
- const showBack = merged.showBack && typeof merged.onBack === 'function';
26
- const showRefresh = merged.showRefresh && typeof merged.onRefresh === 'function';
27
- const showClearFilters = merged.showClearFilters && typeof merged.onClearFilters === 'function';
28
- const hasAnyActions = Boolean(merged.actions) || showBack || showRefresh || showClearFilters;
29
- return (_jsxs("div", { className: merged.className, children: [_jsx("div", { className: "dbc-text-sm", children: merged.description }), hasAnyActions && (_jsxs("div", { className: "dbc-flex dbc-gap-sm dbc-mt-xs", children: [showClearFilters && (_jsx(Button, { type: "button", variant: "secondary", onClick: merged.onClearFilters, children: merged.clearFiltersLabel })), showRefresh && (_jsx(Button, { type: "button", variant: "secondary", onClick: merged.onRefresh, children: merged.refreshLabel })), showBack && (_jsx(Button, { type: "button", variant: "secondary", onClick: merged.onBack, children: merged.backLabel })), merged.actions] }))] }));
22
+ return (_jsxs("div", { className: "dbc-flex dbc-flex-column dbc-gap-md dbc-pt-sm dbc-pb-sm", children: [_jsx("span", { className: "dbc-text-sm dbc-muted-text", children: merged.description }), merged.actions ? _jsx("span", { className: "dbc-flex dbc-gap-sm", children: merged.actions }) : null] }));
30
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",