@dbcdk/react-components 0.0.14 → 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.
- package/dist/components/accordion/Accordion.d.ts +2 -1
- package/dist/components/accordion/Accordion.js +14 -3
- package/dist/components/accordion/Accordion.module.css +1 -1
- package/dist/components/accordion/components/AccordionRow.d.ts +2 -1
- package/dist/components/accordion/components/AccordionRow.js +8 -6
- package/dist/components/accordion/components/AccordionRow.module.css +12 -8
- package/dist/components/button/Button.d.ts +1 -0
- package/dist/components/button/Button.js +3 -3
- package/dist/components/button/Button.module.css +12 -1
- package/dist/components/card/Card.module.css +1 -1
- package/dist/components/chip/Chip.js +11 -1
- package/dist/components/chip/Chip.module.css +92 -30
- package/dist/components/circle/Circle.d.ts +1 -1
- package/dist/components/circle/Circle.module.css +5 -1
- package/dist/components/clear-button/ClearButton.js +1 -1
- package/dist/components/clear-button/ClearButton.module.css +3 -0
- package/dist/components/code-block/CodeBlock.d.ts +7 -3
- package/dist/components/code-block/CodeBlock.js +35 -2
- package/dist/components/code-block/CodeBlock.module.css +49 -2
- package/dist/components/filter-field/FilterField.d.ts +2 -1
- package/dist/components/filter-field/FilterField.js +2 -2
- package/dist/components/filtering/chip-multi-toggle/ChipMultiToggle.d.ts +4 -3
- package/dist/components/filtering/chip-multi-toggle/ChipMultiToggle.js +3 -2
- package/dist/components/forms/checkbox/Checkbox.d.ts +1 -1
- package/dist/components/forms/checkbox/Checkbox.module.css +11 -0
- package/dist/components/hyperlink/Hyperlink.module.css +0 -1
- package/dist/components/overlay/modal/Modal.js +1 -1
- package/dist/components/overlay/side-panel/SidePanel.js +1 -1
- package/dist/components/page-layout/PageLayout.module.css +0 -2
- package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.js +1 -1
- package/dist/components/sidebar/components/sidebar-container/SidebarContainer.js +1 -1
- package/dist/components/sidebar/components/sidebar-container/SidebarContainer.module.css +0 -4
- package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.d.ts +2 -1
- package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.js +2 -2
- package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.module.css +9 -0
- package/dist/components/split-pane/SplitPane.js +1 -1
- package/dist/components/state-page/StatePage.module.css +1 -1
- package/dist/components/table/Table.js +5 -2
- package/dist/components/table/Table.module.css +19 -16
- package/dist/components/table/components/empty-state/EmptyState.d.ts +7 -22
- package/dist/components/table/components/empty-state/EmptyState.js +12 -8
- package/dist/components/table/components/empty-state/EmptyState.module.css +2 -14
- package/dist/components/tabs/Tabs.js +1 -1
- package/dist/components/tabs/Tabs.module.css +1 -1
- package/dist/components/toast/Toast.js +1 -1
- package/dist/hooks/useTableSelection.d.ts +1 -1
- package/dist/hooks/useTableSelection.js +97 -77
- package/dist/hooks/useViewportFill.js +12 -0
- package/dist/src/styles/styles.css +8 -0
- package/dist/styles/styles.css +8 -0
- package/dist/styles/themes/dbc/base.css +1 -0
- 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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
32
|
+
// Hydrate from localStorage on mount / when storageKey changes
|
|
16
33
|
useEffect(() => {
|
|
17
|
-
|
|
18
|
-
if (!stored) {
|
|
19
|
-
setSelectedIds(initialSelectedIds);
|
|
34
|
+
if (typeof window === 'undefined')
|
|
20
35
|
return;
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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(
|
|
70
|
-
const
|
|
71
|
-
const isSelected =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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);
|
package/dist/styles/styles.css
CHANGED
|
@@ -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);
|