@dbcdk/react-components 0.0.13 → 0.0.15

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 (52) hide show
  1. package/dist/components/accordion/Accordion.d.ts +2 -1
  2. package/dist/components/accordion/Accordion.js +14 -3
  3. package/dist/components/accordion/Accordion.module.css +1 -1
  4. package/dist/components/accordion/components/AccordionRow.d.ts +2 -1
  5. package/dist/components/accordion/components/AccordionRow.js +8 -6
  6. package/dist/components/accordion/components/AccordionRow.module.css +12 -8
  7. package/dist/components/button/Button.d.ts +1 -0
  8. package/dist/components/button/Button.js +3 -3
  9. package/dist/components/button/Button.module.css +12 -1
  10. package/dist/components/card/Card.module.css +1 -1
  11. package/dist/components/chip/Chip.js +11 -1
  12. package/dist/components/chip/Chip.module.css +92 -30
  13. package/dist/components/circle/Circle.d.ts +1 -1
  14. package/dist/components/circle/Circle.module.css +5 -1
  15. package/dist/components/clear-button/ClearButton.js +1 -1
  16. package/dist/components/clear-button/ClearButton.module.css +3 -0
  17. package/dist/components/code-block/CodeBlock.d.ts +7 -3
  18. package/dist/components/code-block/CodeBlock.js +35 -2
  19. package/dist/components/code-block/CodeBlock.module.css +49 -2
  20. package/dist/components/filter-field/FilterField.d.ts +2 -1
  21. package/dist/components/filter-field/FilterField.js +22 -7
  22. package/dist/components/filtering/chip-multi-toggle/ChipMultiToggle.d.ts +4 -3
  23. package/dist/components/filtering/chip-multi-toggle/ChipMultiToggle.js +3 -2
  24. package/dist/components/forms/checkbox/Checkbox.d.ts +1 -1
  25. package/dist/components/forms/checkbox/Checkbox.module.css +11 -0
  26. package/dist/components/hyperlink/Hyperlink.module.css +0 -1
  27. package/dist/components/overlay/modal/Modal.js +1 -1
  28. package/dist/components/overlay/side-panel/SidePanel.js +1 -1
  29. package/dist/components/page-layout/PageLayout.module.css +0 -2
  30. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.js +1 -1
  31. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.js +1 -1
  32. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.module.css +0 -4
  33. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.d.ts +2 -1
  34. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.js +2 -2
  35. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.module.css +9 -0
  36. package/dist/components/split-pane/SplitPane.js +1 -1
  37. package/dist/components/state-page/StatePage.module.css +1 -1
  38. package/dist/components/table/Table.js +5 -2
  39. package/dist/components/table/Table.module.css +19 -16
  40. package/dist/components/table/components/empty-state/EmptyState.d.ts +7 -22
  41. package/dist/components/table/components/empty-state/EmptyState.js +12 -8
  42. package/dist/components/table/components/empty-state/EmptyState.module.css +2 -14
  43. package/dist/components/tabs/Tabs.js +1 -1
  44. package/dist/components/tabs/Tabs.module.css +1 -1
  45. package/dist/components/toast/Toast.js +1 -1
  46. package/dist/hooks/useTableSelection.d.ts +1 -1
  47. package/dist/hooks/useTableSelection.js +97 -77
  48. package/dist/hooks/useViewportFill.js +12 -0
  49. package/dist/src/styles/styles.css +8 -0
  50. package/dist/styles/styles.css +8 -0
  51. package/dist/styles/themes/dbc/base.css +1 -0
  52. package/package.json +1 -1
@@ -1,58 +1,79 @@
1
1
  'use client';
2
- import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ function safeParseIds(raw) {
4
+ if (!raw)
5
+ return null;
6
+ try {
7
+ const parsed = JSON.parse(raw);
8
+ if (!Array.isArray(parsed))
9
+ return null;
10
+ // Allow strings/numbers only; drop anything else
11
+ return parsed.filter(v => typeof v === 'string' || typeof v === 'number');
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ function serializeIds(ids) {
18
+ return JSON.stringify(Array.from(ids));
19
+ }
3
20
  export function useTableSelection({ storageKey, items, getId, initialSelectedIds = new Set(), totalItems, onSelectionChange, selectionMode = 'single', }) {
4
- // Selected IDs are persisted and are your primary lookup structure
5
21
  const [selectedIds, setSelectedIds] = useState(initialSelectedIds);
6
- // Keep the actual selected items keyed by id for O(1) access
7
- const [selectedItemMap, setSelectedItemMap] = useState(() => new Map());
8
- // Build a fast lookup of current items by id (for hydration + reconciliation)
22
+ const [hydrated, setHydrated] = useState(false);
23
+ // Used to avoid unnecessary writes / loops (and helps with storage-event sync)
24
+ const lastWrittenRef = useRef(null);
25
+ // Fast lookup of current items by id
9
26
  const itemsById = useMemo(() => {
10
27
  const m = new Map();
11
28
  for (const item of items)
12
29
  m.set(getId(item), item);
13
30
  return m;
14
31
  }, [items, getId]);
15
- // Hydrate selectedIds from localStorage on mount / storageKey change
32
+ // Hydrate from localStorage on mount / when storageKey changes
16
33
  useEffect(() => {
17
- const stored = window.localStorage.getItem(storageKey);
18
- if (!stored) {
19
- setSelectedIds(initialSelectedIds);
34
+ if (typeof window === 'undefined')
20
35
  return;
21
- }
22
- try {
23
- const parsed = JSON.parse(stored);
36
+ const parsed = safeParseIds(window.localStorage.getItem(storageKey));
37
+ if (parsed) {
24
38
  setSelectedIds(new Set(parsed));
25
39
  }
26
- catch {
40
+ else {
27
41
  setSelectedIds(initialSelectedIds);
28
42
  }
43
+ // Mark hydrated after we’ve decided initial state for this key
44
+ setHydrated(true);
29
45
  // eslint-disable-next-line react-hooks/exhaustive-deps
30
46
  }, [storageKey]);
31
- // Reconcile selectedItemMap whenever selectedIds or items change.
32
- // This makes sure selectedItems stay up to date without scanning the full items list on every render.
47
+ // Keep selection in sync across tabs/windows (optional but usually desired)
33
48
  useEffect(() => {
34
- setSelectedItemMap(prev => {
35
- const next = new Map();
36
- // Keep only ids that still exist in itemsById (if list changed)
37
- for (const id of selectedIds) {
38
- const item = itemsById.get(id);
39
- if (item !== undefined)
40
- next.set(id, item);
41
- }
42
- // If nothing changed, return prev to avoid re-renders
43
- if (next.size === prev.size) {
44
- let same = true;
45
- for (const [id, item] of next) {
46
- if (prev.get(id) !== item) {
47
- same = false;
48
- break;
49
- }
50
- }
51
- if (same)
52
- return prev;
53
- }
54
- return next;
55
- });
49
+ if (typeof window === 'undefined')
50
+ return;
51
+ const onStorage = (e) => {
52
+ if (e.storageArea !== window.localStorage)
53
+ return;
54
+ if (e.key !== storageKey)
55
+ return;
56
+ const parsed = safeParseIds(e.newValue);
57
+ const next = new Set(parsed !== null && parsed !== void 0 ? parsed : Array.from(initialSelectedIds));
58
+ // Avoid setting state if nothing actually changed
59
+ const nextStr = serializeIds(next);
60
+ const curStr = serializeIds(selectedIds);
61
+ if (nextStr !== curStr)
62
+ setSelectedIds(next);
63
+ };
64
+ window.addEventListener('storage', onStorage);
65
+ return () => window.removeEventListener('storage', onStorage);
66
+ // Note: selectedIds is intentionally included to compare current vs next
67
+ }, [storageKey, initialSelectedIds, selectedIds]);
68
+ // Derive selectedItemMap from selectedIds + itemsById (never out of sync)
69
+ const selectedItemMap = useMemo(() => {
70
+ const m = new Map();
71
+ for (const id of selectedIds) {
72
+ const item = itemsById.get(id);
73
+ if (item !== undefined)
74
+ m.set(id, item);
75
+ }
76
+ return m;
56
77
  }, [selectedIds, itemsById]);
57
78
  const selectedItems = useMemo(() => Array.from(selectedItemMap.values()), [selectedItemMap]);
58
79
  const allSelected = useMemo(() => {
@@ -61,59 +82,58 @@ export function useTableSelection({ storageKey, items, getId, initialSelectedIds
61
82
  return totalItems > 0 && selectedIds.size === totalItems;
62
83
  }, [selectedIds, totalItems]);
63
84
  const anySelected = useMemo(() => selectedIds.size > 0, [selectedIds]);
85
+ // Persist + notify (but only after hydration so we don’t clobber stored values)
64
86
  useEffect(() => {
65
- window.localStorage.setItem(storageKey, JSON.stringify(Array.from(selectedIds)));
87
+ if (!hydrated)
88
+ return;
89
+ if (typeof window === 'undefined')
90
+ return;
91
+ const nextStr = serializeIds(selectedIds);
92
+ if (lastWrittenRef.current !== nextStr) {
93
+ window.localStorage.setItem(storageKey, nextStr);
94
+ lastWrittenRef.current = nextStr;
95
+ }
66
96
  onSelectionChange === null || onSelectionChange === void 0 ? void 0 : onSelectionChange({ selectedIds, selectedItems });
67
- }, [selectedIds, selectedItems, storageKey, onSelectionChange]);
68
- const toggleId = useCallback((id) => {
69
- setSelectedIds(prevIds => {
70
- const nextIds = new Set(prevIds);
71
- const isSelected = nextIds.has(id);
72
- if (isSelected) {
73
- nextIds.delete(id);
74
- }
75
- else {
76
- if (selectionMode === 'single')
77
- nextIds.clear();
78
- nextIds.add(id);
97
+ }, [hydrated, selectedIds, selectedItems, storageKey, onSelectionChange]);
98
+ const toggleId = useCallback((id, selected) => {
99
+ setSelectedIds(prev => {
100
+ const next = new Set(prev);
101
+ const isSelected = next.has(id);
102
+ const shouldSelect = selected === undefined ? !isSelected : selected;
103
+ if (selectionMode === 'single') {
104
+ next.clear();
105
+ if (shouldSelect)
106
+ next.add(id);
107
+ return next;
79
108
  }
80
- return nextIds;
81
- });
82
- // Update selectedItemMap in the same interaction for snappy UI
83
- setSelectedItemMap(prevMap => {
84
- const nextMap = new Map(prevMap);
85
- const isSelected = nextMap.has(id);
86
- if (isSelected) {
87
- nextMap.delete(id);
88
- }
89
- else {
90
- if (selectionMode === 'single')
91
- nextMap.clear();
92
- const item = itemsById.get(id);
93
- if (item !== undefined)
94
- nextMap.set(id, item);
95
- }
96
- return nextMap;
109
+ // multiple
110
+ if (shouldSelect)
111
+ next.add(id);
112
+ else
113
+ next.delete(id);
114
+ return next;
97
115
  });
98
- }, [itemsById, selectionMode]);
99
- const toggleItem = useCallback((item) => {
100
- toggleId(getId(item));
101
- }, [toggleId, getId]);
116
+ }, [selectionMode]);
117
+ const toggleItem = useCallback((item) => toggleId(getId(item)), [toggleId, getId]);
102
118
  const clearSelection = useCallback(() => {
103
119
  setSelectedIds(new Set());
104
- setSelectedItemMap(new Map());
105
120
  }, []);
106
121
  const toggleAll = useCallback((selected) => {
107
122
  if (!selected) {
108
123
  clearSelection();
109
124
  return;
110
125
  }
111
- const nextIds = new Set();
112
- for (const item of items) {
113
- nextIds.add(getId(item));
126
+ if (selectionMode === 'single') {
127
+ // Choose the first item (or nothing if empty)
128
+ const first = items[0];
129
+ setSelectedIds(first ? new Set([getId(first)]) : new Set());
130
+ return;
114
131
  }
115
- setSelectedIds(nextIds);
116
- }, [clearSelection, getId, items]);
132
+ const next = new Set();
133
+ for (const item of items)
134
+ next.add(getId(item));
135
+ setSelectedIds(next);
136
+ }, [clearSelection, getId, items, selectionMode]);
117
137
  return {
118
138
  selectedIds,
119
139
  selectedItems,
@@ -41,11 +41,23 @@ export function useViewportFill(ref, { bottomOffset = 0, min = 120, includeMargi
41
41
  ro = new ResizeObserver(onFrame);
42
42
  ro.observe(ref.current);
43
43
  }
44
+ // MutationObserver to detect DOM changes that may affect position
45
+ let mo = null;
46
+ if ('MutationObserver' in window && ref.current) {
47
+ mo = new MutationObserver(onFrame);
48
+ // Observe parent node for subtree changes
49
+ const parent = ref.current.parentNode;
50
+ if (parent) {
51
+ mo.observe(parent, { childList: true, subtree: true });
52
+ }
53
+ }
44
54
  return () => {
45
55
  window.removeEventListener('scroll', onFrame);
46
56
  window.removeEventListener('resize', onFrame);
47
57
  if (ro)
48
58
  ro.disconnect();
59
+ if (mo)
60
+ mo.disconnect();
49
61
  if (raf.current)
50
62
  cancelAnimationFrame(raf.current);
51
63
  };
@@ -118,6 +118,14 @@ body.dbc-app {
118
118
  color: var(--color-fg-subtle);
119
119
  }
120
120
 
121
+ .dbc-small-text {
122
+ font-size: var(--font-size-xs);
123
+ }
124
+
125
+ .dbc-medium-text {
126
+ font-weight: var(--font-weight-medium);
127
+ }
128
+
121
129
  .dbc-table--bordered {
122
130
  width: auto;
123
131
  border: var(--border-width-thin) solid var(--color-border-default);
@@ -118,6 +118,14 @@ body.dbc-app {
118
118
  color: var(--color-fg-subtle);
119
119
  }
120
120
 
121
+ .dbc-small-text {
122
+ font-size: var(--font-size-xs);
123
+ }
124
+
125
+ .dbc-medium-text {
126
+ font-weight: var(--font-weight-medium);
127
+ }
128
+
121
129
  .dbc-table--bordered {
122
130
  width: auto;
123
131
  border: var(--border-width-thin) solid var(--color-border-default);
@@ -38,6 +38,7 @@
38
38
  --icon-size-md: 20px;
39
39
  --icon-size-lg: 24px;
40
40
 
41
+ --component-size-2xs: 8px;
41
42
  --component-size-xxs: 12px;
42
43
  --component-size-xs: 20px;
43
44
  --component-size-sm: 30px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",