@dbcdk/react-components 0.0.37 → 0.0.39

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,7 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { TextWrap } from 'lucide-react';
3
- import { isValidElement } from 'react';
4
- import { useMemo, useState } from 'react';
3
+ import { isValidElement, useMemo, useState } from 'react';
5
4
  import styles from './CodeBlock.module.css';
6
5
  import { Button } from '../button/Button';
7
6
  import { CopyButton } from '../copy-button/CopyButton';
@@ -11,12 +11,33 @@ export function CopyButton(props) {
11
11
  const [copied, setCopied] = useState(false);
12
12
  const handleCopy = async () => {
13
13
  try {
14
+ if (!window.isSecureContext || !navigator.clipboard) {
15
+ throw new Error('Clipboard API unavailable');
16
+ }
14
17
  await navigator.clipboard.writeText(text);
15
18
  setCopied(true);
16
19
  setTimeout(() => setCopied(false), 1000);
17
20
  }
18
21
  catch (err) {
19
- console.error('Failed to copy: ', err);
22
+ console.error('Failed to copy:', err);
23
+ try {
24
+ const textarea = document.createElement('textarea');
25
+ textarea.value = text;
26
+ textarea.setAttribute('readonly', '');
27
+ textarea.style.position = 'fixed';
28
+ textarea.style.left = '-9999px';
29
+ document.body.appendChild(textarea);
30
+ textarea.select();
31
+ const success = document.execCommand('copy');
32
+ document.body.removeChild(textarea);
33
+ if (success) {
34
+ setCopied(true);
35
+ setTimeout(() => setCopied(false), 1000);
36
+ }
37
+ }
38
+ catch (fallbackErr) {
39
+ console.error('Fallback copy failed:', fallbackErr);
40
+ }
20
41
  }
21
42
  };
22
43
  if (props.style === 'link') {
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Checkbox } from '../../../components/forms/checkbox/Checkbox';
3
3
  import { RadioButton } from '../../../components/forms/radio-buttons/RadioButton';
4
- import { SeverityBgColor } from '../../../constants/severity';
4
+ import { SeverityBorderColor } from '../../../constants/severity';
5
5
  import { useTableRowInteractions } from '../hooks/useTableRowInteractions';
6
6
  import { cx } from '../table.classes';
7
7
  import styles from '../Table.module.css';
@@ -21,7 +21,7 @@ export function TableRow({ row, rowId, columns, selectedRows, hasSelection, sele
21
21
  });
22
22
  return (_jsxs("tr", { className: cx(styles.row, onRowClick && styles.clickableRow, isSelected && styles.selectedRow, rowSeverity && styles.severity), style: {
23
23
  ['--row-severity-color']: rowSeverity
24
- ? SeverityBgColor[rowSeverity]
24
+ ? SeverityBorderColor[rowSeverity]
25
25
  : undefined,
26
26
  }, tabIndex: onRowClick ? 0 : -1, onKeyDown: handleRowKeyDown, onMouseEnter: () => onRowMouseEnter === null || onRowMouseEnter === void 0 ? void 0 : onRowMouseEnter(row), onClick: handleRowClick, children: [hasSelection ? (_jsx("td", { className: cx(styles.cell, styles.selectionCell), "data-selection-control": "true", children: _jsx("div", { className: styles.selectionHitArea, "data-selection-control": "true", onClick: e => {
27
27
  if (e.target !== e.currentTarget)
@@ -1,3 +1,4 @@
1
1
  import type { Severity } from '../constants/severity.types';
2
2
  export declare const SeverityBgColor: Record<Severity, string>;
3
+ export declare const SeverityBorderColor: Record<Severity, string>;
3
4
  export declare const SeverityTextColor: Record<Severity, string>;
@@ -6,6 +6,14 @@ export const SeverityBgColor = {
6
6
  info: 'var(--color-status-info)',
7
7
  warning: 'var(--color-status-warning)',
8
8
  };
9
+ export const SeverityBorderColor = {
10
+ neutral: 'var(--color-neutral-strong)',
11
+ brand: 'var(--color-brand)',
12
+ success: 'var(--color-status-success-border)',
13
+ error: 'var(--color-status-error-border)',
14
+ info: 'var(--color-status-info-border)',
15
+ warning: 'var(--color-status-warning-border)',
16
+ };
9
17
  export const SeverityTextColor = {
10
18
  neutral: 'var(--color-neutral-strong-fg)',
11
19
  brand: 'var(--color-fg-on-brand)',
@@ -1,6 +1,6 @@
1
1
  type Id = string | number;
2
2
  interface Props<T> {
3
- storageKey: string;
3
+ storageKey?: string;
4
4
  items: T[];
5
5
  getId: (item: T) => Id;
6
6
  initialSelectedIds?: Set<Id>;
@@ -8,15 +8,17 @@ interface Props<T> {
8
8
  selectedIds: Set<Id>;
9
9
  selectedItems: T[];
10
10
  }) => void;
11
- totalItems?: number;
12
11
  selectionMode?: 'single' | 'multiple';
12
+ pruneToItems?: boolean;
13
+ storage?: 'local' | 'session';
13
14
  }
14
- export declare function useTableSelection<T>({ storageKey, items, getId, initialSelectedIds, totalItems, onSelectionChange, selectionMode, }: Props<T>): {
15
+ export declare function useTableSelection<T>({ storageKey, items, getId, initialSelectedIds, onSelectionChange, selectionMode, pruneToItems, storage, }: Props<T>): {
15
16
  selectedIds: Set<Id>;
16
17
  selectedItems: T[];
17
18
  selectedItemMap: Map<Id, T>;
18
19
  toggleItem: (item: T) => void;
19
20
  toggleId: (id: Id, selected?: boolean) => void;
21
+ selectOnly: (id: Id) => void;
20
22
  clearSelection: () => void;
21
23
  allSelected: boolean;
22
24
  anySelected: boolean;
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ const EMPTY_IDS = new Set();
3
4
  function safeParseIds(raw) {
4
5
  if (!raw)
5
6
  return null;
@@ -7,8 +8,7 @@ function safeParseIds(raw) {
7
8
  const parsed = JSON.parse(raw);
8
9
  if (!Array.isArray(parsed))
9
10
  return null;
10
- // Allow strings/numbers only; drop anything else
11
- return parsed.filter(v => typeof v === 'string' || typeof v === 'number');
11
+ return parsed.filter((v) => typeof v === 'string' || typeof v === 'number');
12
12
  }
13
13
  catch {
14
14
  return null;
@@ -17,84 +17,91 @@ function safeParseIds(raw) {
17
17
  function serializeIds(ids) {
18
18
  return JSON.stringify(Array.from(ids));
19
19
  }
20
- export function useTableSelection({ storageKey, items, getId, initialSelectedIds = new Set(), totalItems, onSelectionChange, selectionMode = 'single', }) {
21
- const [selectedIds, setSelectedIds] = useState(initialSelectedIds);
20
+ function areSetsEqual(a, b) {
21
+ if (a.size !== b.size)
22
+ return false;
23
+ for (const value of a) {
24
+ if (!b.has(value))
25
+ return false;
26
+ }
27
+ return true;
28
+ }
29
+ export function useTableSelection({ storageKey, items, getId, initialSelectedIds, onSelectionChange, selectionMode = 'single', pruneToItems = false, storage = 'session', }) {
30
+ const resolvedInitialSelectedIds = initialSelectedIds !== null && initialSelectedIds !== void 0 ? initialSelectedIds : EMPTY_IDS;
31
+ const [selectedIds, setSelectedIds] = useState(resolvedInitialSelectedIds);
22
32
  const [hydrated, setHydrated] = useState(false);
23
- // Used to avoid unnecessary writes / loops (and helps with storage-event sync)
24
33
  const lastWrittenRef = useRef(null);
25
- // Fast lookup of current items by id
26
34
  const itemsById = useMemo(() => {
27
- const m = new Map();
28
- for (const item of items)
29
- m.set(getId(item), item);
30
- return m;
35
+ const map = new Map();
36
+ for (const item of items) {
37
+ map.set(getId(item), item);
38
+ }
39
+ return map;
31
40
  }, [items, getId]);
32
- // Hydrate from localStorage on mount / when storageKey changes
33
41
  useEffect(() => {
34
42
  if (typeof window === 'undefined')
35
43
  return;
36
- const parsed = safeParseIds(window.localStorage.getItem(storageKey));
37
- if (parsed) {
38
- setSelectedIds(new Set(parsed));
39
- }
40
- else {
41
- setSelectedIds(initialSelectedIds);
44
+ const storageApi = storage === 'local' ? window.localStorage : window.sessionStorage;
45
+ if (!storageKey) {
46
+ setSelectedIds(prev => areSetsEqual(prev, resolvedInitialSelectedIds) ? prev : new Set(resolvedInitialSelectedIds));
47
+ setHydrated(true);
48
+ lastWrittenRef.current = null;
49
+ return;
42
50
  }
43
- // Mark hydrated after we’ve decided initial state for this key
51
+ const parsed = safeParseIds(storageApi.getItem(storageKey));
52
+ const next = new Set(parsed !== null && parsed !== void 0 ? parsed : Array.from(resolvedInitialSelectedIds));
53
+ setSelectedIds(prev => (areSetsEqual(prev, next) ? prev : next));
54
+ lastWrittenRef.current = serializeIds(next);
44
55
  setHydrated(true);
45
- // eslint-disable-next-line react-hooks/exhaustive-deps
46
- }, [storageKey]);
47
- // Keep selection in sync across tabs/windows (optional but usually desired)
56
+ }, [storage, storageKey, resolvedInitialSelectedIds]);
48
57
  useEffect(() => {
49
- if (typeof window === 'undefined')
58
+ if (!pruneToItems)
50
59
  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)
60
+ const visibleIds = new Set(Array.from(itemsById.keys()));
61
+ setSelectedIds(prev => {
62
+ if (prev.size === 0)
63
+ return prev;
64
+ if (visibleIds.size === 0)
65
+ return prev;
66
+ const next = new Set();
67
+ for (const id of prev) {
68
+ if (visibleIds.has(id))
69
+ next.add(id);
70
+ }
71
+ return areSetsEqual(prev, next) ? prev : next;
72
+ });
73
+ }, [pruneToItems, itemsById]);
69
74
  const selectedItemMap = useMemo(() => {
70
- const m = new Map();
75
+ const map = new Map();
71
76
  for (const id of selectedIds) {
72
77
  const item = itemsById.get(id);
73
78
  if (item !== undefined)
74
- m.set(id, item);
79
+ map.set(id, item);
75
80
  }
76
- return m;
81
+ return map;
77
82
  }, [selectedIds, itemsById]);
78
83
  const selectedItems = useMemo(() => Array.from(selectedItemMap.values()), [selectedItemMap]);
79
84
  const allSelected = useMemo(() => {
80
- if (typeof totalItems !== 'number')
85
+ if (items.length === 0)
81
86
  return false;
82
- return totalItems > 0 && selectedIds.size === totalItems;
83
- }, [selectedIds, totalItems]);
87
+ return items.every(item => selectedIds.has(getId(item)));
88
+ }, [items, selectedIds, getId]);
84
89
  const anySelected = useMemo(() => selectedIds.size > 0, [selectedIds]);
85
- // Persist + notify (but only after hydration so we don’t clobber stored values)
86
90
  useEffect(() => {
87
91
  if (!hydrated)
88
92
  return;
89
93
  if (typeof window === 'undefined')
90
94
  return;
91
- const nextStr = serializeIds(selectedIds);
92
- if (lastWrittenRef.current !== nextStr) {
93
- window.localStorage.setItem(storageKey, nextStr);
94
- lastWrittenRef.current = nextStr;
95
+ if (storageKey) {
96
+ const storageApi = storage === 'local' ? window.localStorage : window.sessionStorage;
97
+ const nextStr = serializeIds(selectedIds);
98
+ if (lastWrittenRef.current !== nextStr) {
99
+ storageApi.setItem(storageKey, nextStr);
100
+ lastWrittenRef.current = nextStr;
101
+ }
95
102
  }
96
103
  onSelectionChange === null || onSelectionChange === void 0 ? void 0 : onSelectionChange({ selectedIds, selectedItems });
97
- }, [hydrated, selectedIds, selectedItems, storageKey, onSelectionChange]);
104
+ }, [hydrated, onSelectionChange, selectedIds, selectedItems, storage, storageKey]);
98
105
  const toggleId = useCallback((id, selected) => {
99
106
  setSelectedIds(prev => {
100
107
  const next = new Set(prev);
@@ -104,19 +111,26 @@ export function useTableSelection({ storageKey, items, getId, initialSelectedIds
104
111
  next.clear();
105
112
  if (shouldSelect)
106
113
  next.add(id);
107
- return next;
114
+ return areSetsEqual(prev, next) ? prev : next;
108
115
  }
109
- // multiple
110
116
  if (shouldSelect)
111
117
  next.add(id);
112
118
  else
113
119
  next.delete(id);
114
- return next;
120
+ return areSetsEqual(prev, next) ? prev : next;
115
121
  });
116
122
  }, [selectionMode]);
117
- const toggleItem = useCallback((item) => toggleId(getId(item)), [toggleId, getId]);
123
+ const toggleItem = useCallback((item) => {
124
+ toggleId(getId(item));
125
+ }, [toggleId, getId]);
126
+ const selectOnly = useCallback((id) => {
127
+ setSelectedIds(prev => {
128
+ const next = new Set([id]);
129
+ return areSetsEqual(prev, next) ? prev : next;
130
+ });
131
+ }, []);
118
132
  const clearSelection = useCallback(() => {
119
- setSelectedIds(new Set());
133
+ setSelectedIds(prev => (prev.size === 0 ? prev : new Set()));
120
134
  }, []);
121
135
  const toggleAll = useCallback((selected) => {
122
136
  if (!selected) {
@@ -124,15 +138,16 @@ export function useTableSelection({ storageKey, items, getId, initialSelectedIds
124
138
  return;
125
139
  }
126
140
  if (selectionMode === 'single') {
127
- // Choose the first item (or nothing if empty)
128
141
  const first = items[0];
129
- setSelectedIds(first ? new Set([getId(first)]) : new Set());
142
+ const next = first ? new Set([getId(first)]) : new Set();
143
+ setSelectedIds(prev => (areSetsEqual(prev, next) ? prev : next));
130
144
  return;
131
145
  }
132
146
  const next = new Set();
133
- for (const item of items)
147
+ for (const item of items) {
134
148
  next.add(getId(item));
135
- setSelectedIds(next);
149
+ }
150
+ setSelectedIds(prev => (areSetsEqual(prev, next) ? prev : next));
136
151
  }, [clearSelection, getId, items, selectionMode]);
137
152
  return {
138
153
  selectedIds,
@@ -140,6 +155,7 @@ export function useTableSelection({ storageKey, items, getId, initialSelectedIds
140
155
  selectedItemMap,
141
156
  toggleItem,
142
157
  toggleId,
158
+ selectOnly,
143
159
  clearSelection,
144
160
  allSelected,
145
161
  anySelected,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",