@dbcdk/react-components 0.0.133 → 0.0.135

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/client.cjs CHANGED
@@ -19,6 +19,7 @@ var Footer = require('./components/page-layout/components/footer/Footer');
19
19
  var Input = require('./components/forms/input/Input');
20
20
  var SearchBox = require('./components/search-box/SearchBox');
21
21
  var useTheme = require('./hooks/useTheme');
22
+ var usePersistentState = require('./hooks/usePersistentState');
22
23
  var Chip = require('./components/chip/Chip');
23
24
  var Panel = require('./components/panel/Panel');
24
25
  var Card = require('./components/card/Card');
@@ -199,6 +200,12 @@ Object.keys(useTheme).forEach(function (k) {
199
200
  get: function () { return useTheme[k]; }
200
201
  });
201
202
  });
203
+ Object.keys(usePersistentState).forEach(function (k) {
204
+ if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
205
+ enumerable: true,
206
+ get: function () { return usePersistentState[k]; }
207
+ });
208
+ });
202
209
  Object.keys(Chip).forEach(function (k) {
203
210
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
204
211
  enumerable: true,
package/dist/client.d.ts CHANGED
@@ -16,6 +16,7 @@ export * from './components/page-layout/components/footer/Footer';
16
16
  export * from './components/forms/input/Input';
17
17
  export * from './components/search-box/SearchBox';
18
18
  export * from './hooks/useTheme';
19
+ export * from './hooks/usePersistentState';
19
20
  export * from './components/chip/Chip';
20
21
  export * from './components/panel/Panel';
21
22
  export * from './components/card/Card';
package/dist/client.js CHANGED
@@ -17,6 +17,7 @@ export * from './components/page-layout/components/footer/Footer';
17
17
  export * from './components/forms/input/Input';
18
18
  export * from './components/search-box/SearchBox';
19
19
  export * from './hooks/useTheme';
20
+ export * from './hooks/usePersistentState';
20
21
  export * from './components/chip/Chip';
21
22
  export * from './components/panel/Panel';
22
23
  export * from './components/card/Card';
@@ -4,6 +4,7 @@
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
  var lucideReact = require('lucide-react');
6
6
  var react = require('react');
7
+ var usePersistentState = require('../../hooks/usePersistentState');
7
8
  var Headline = require('./Headline');
8
9
  var styles = require('./Headline.module.css');
9
10
  var Button = require('../button/Button');
@@ -29,26 +30,17 @@ function CollapsibleHeadline({
29
30
  const generatedId = react.useId();
30
31
  const panelId = controls != null ? controls : generatedId;
31
32
  const isControlled = expanded !== void 0;
32
- const [internalExpanded, setInternalExpanded] = react.useState(defaultExpanded);
33
- const [hasToggled, setHasToggled] = react.useState(false);
34
- const persistedExpanded = react.useSyncExternalStore(
35
- () => () => void 0,
36
- () => {
37
- if (isControlled || !storageKey) return defaultExpanded;
38
- const stored = localStorage.getItem(storageKey);
39
- return stored !== null ? stored === "true" : defaultExpanded;
40
- },
41
- () => defaultExpanded
42
- );
43
- const isExpanded = isControlled ? expanded : hasToggled ? internalExpanded : persistedExpanded;
33
+ const { value: internalExpanded, setValue: setInternalExpanded } = usePersistentState.usePersistentState({
34
+ initialValue: defaultExpanded,
35
+ storageKey,
36
+ isControlled,
37
+ validate: (stored) => typeof stored === "boolean"
38
+ });
39
+ const isExpanded = isControlled ? expanded : internalExpanded;
44
40
  const handleToggle = () => {
45
41
  const next = !isExpanded;
46
42
  if (!isControlled) {
47
- setHasToggled(true);
48
43
  setInternalExpanded(next);
49
- if (storageKey) {
50
- localStorage.setItem(storageKey, String(next));
51
- }
52
44
  }
53
45
  onToggle == null ? void 0 : onToggle();
54
46
  };
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
  import { jsxs, jsx } from 'react/jsx-runtime';
3
3
  import { ChevronDown } from 'lucide-react';
4
- import { useId, useState, useSyncExternalStore } from 'react';
4
+ import { useId } from 'react';
5
+ import { usePersistentState } from '../../hooks/usePersistentState';
5
6
  import { Headline } from './Headline';
6
7
  import styles from './Headline.module.css';
7
8
  import { Button } from '../button/Button';
@@ -23,26 +24,17 @@ function CollapsibleHeadline({
23
24
  const generatedId = useId();
24
25
  const panelId = controls != null ? controls : generatedId;
25
26
  const isControlled = expanded !== void 0;
26
- const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
27
- const [hasToggled, setHasToggled] = useState(false);
28
- const persistedExpanded = useSyncExternalStore(
29
- () => () => void 0,
30
- () => {
31
- if (isControlled || !storageKey) return defaultExpanded;
32
- const stored = localStorage.getItem(storageKey);
33
- return stored !== null ? stored === "true" : defaultExpanded;
34
- },
35
- () => defaultExpanded
36
- );
37
- const isExpanded = isControlled ? expanded : hasToggled ? internalExpanded : persistedExpanded;
27
+ const { value: internalExpanded, setValue: setInternalExpanded } = usePersistentState({
28
+ initialValue: defaultExpanded,
29
+ storageKey,
30
+ isControlled,
31
+ validate: (stored) => typeof stored === "boolean"
32
+ });
33
+ const isExpanded = isControlled ? expanded : internalExpanded;
38
34
  const handleToggle = () => {
39
35
  const next = !isExpanded;
40
36
  if (!isControlled) {
41
- setHasToggled(true);
42
37
  setInternalExpanded(next);
43
- if (storageKey) {
44
- localStorage.setItem(storageKey, String(next));
45
- }
46
38
  }
47
39
  onToggle == null ? void 0 : onToggle();
48
40
  };
@@ -36,19 +36,24 @@ function ExpandableSidebarItem({
36
36
  isExpanded
37
37
  } = SidebarProvider.useSidebar();
38
38
  const [closing, setClosing] = react.useState(false);
39
- const ready = true;
40
39
  const expanded = react.useMemo(() => isExpanded(href), [href, isExpanded]);
41
40
  react.useEffect(() => {
42
41
  if (defaultExpanded === null) return;
43
42
  if (defaultExpanded) expandItem(href);
44
43
  else collapseItem(href);
45
44
  }, [defaultExpanded, expandItem, collapseItem, href]);
45
+ const commitCollapse = react.useCallback(() => {
46
+ collapseItem(href);
47
+ setClosing(false);
48
+ }, [collapseItem, href]);
46
49
  const handleAnimationEnd = react.useCallback(() => {
47
- if (closing) {
48
- collapseItem(href);
49
- setClosing(false);
50
- }
51
- }, [closing, ready, collapseItem, href]);
50
+ if (closing) commitCollapse();
51
+ }, [closing, commitCollapse]);
52
+ react.useEffect(() => {
53
+ if (!closing) return;
54
+ const timer = setTimeout(commitCollapse, 250);
55
+ return () => clearTimeout(timer);
56
+ }, [closing, commitCollapse]);
52
57
  const toggleAccordion = react.useCallback(
53
58
  (e, onlyExpand = false) => {
54
59
  e == null ? void 0 : e.preventDefault();
@@ -101,29 +106,42 @@ function ExpandableSidebarItem({
101
106
  );
102
107
  };
103
108
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${styles__default.default.container}`, children: [
104
- /* @__PURE__ */ jsxRuntime.jsx(Component, { onClick: () => toggleAccordion(void 0, true), children: /* @__PURE__ */ jsxRuntime.jsx(
105
- SidebarItemContent.SidebarItemContent,
106
- {
107
- headerStyle: expanded,
108
- icon,
109
- iconWidth,
110
- label,
111
- href,
112
- disableActiveStyles: expanded,
113
- truncateLabel,
114
- suffixIcon: isSidebarCollapsed ? null : /* @__PURE__ */ jsxRuntime.jsx(Button.Button, { variant: "outlined", onClick: toggleAccordion, children: /* @__PURE__ */ jsxRuntime.jsx(
115
- lucideReact.ChevronDown,
116
- {
117
- className: `${styles__default.default.chevron} ${expanded ? styles__default.default.chevronExpanded : ""}`
118
- }
119
- ) })
120
- }
121
- ) }),
109
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.itemRow, children: [
110
+ /* @__PURE__ */ jsxRuntime.jsx(Component, { onClick: () => toggleAccordion(void 0, true), children: /* @__PURE__ */ jsxRuntime.jsx(
111
+ SidebarItemContent.SidebarItemContent,
112
+ {
113
+ headerStyle: expanded,
114
+ icon,
115
+ iconWidth,
116
+ label,
117
+ href,
118
+ disableActiveStyles: expanded,
119
+ truncateLabel
120
+ }
121
+ ) }),
122
+ !isSidebarCollapsed && /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.toggleButton, children: /* @__PURE__ */ jsxRuntime.jsx(
123
+ Button.Button,
124
+ {
125
+ variant: "outlined",
126
+ "aria-label": expanded ? `Collapse ${label}` : `Expand ${label}`,
127
+ "aria-expanded": expanded,
128
+ "aria-controls": `${href}-children`,
129
+ onClick: toggleAccordion,
130
+ children: /* @__PURE__ */ jsxRuntime.jsx(
131
+ lucideReact.ChevronDown,
132
+ {
133
+ className: `${styles__default.default.chevron} ${expanded ? styles__default.default.chevronExpanded : ""}`
134
+ }
135
+ )
136
+ }
137
+ ) })
138
+ ] }),
122
139
  expanded && !isSidebarCollapsed && /* @__PURE__ */ jsxRuntime.jsx(
123
140
  "div",
124
141
  {
142
+ id: `${href}-children`,
125
143
  onAnimationEnd: handleAnimationEnd,
126
- className: `${styles__default.default.childrenContainer} ${closing ? "animate--collapse" : ""} ${expanded ? "animate--expand" : "visually-hidden"}`,
144
+ className: `${styles__default.default.childrenContainer} ${closing ? "animate--collapse" : expanded ? "animate--expand" : "visually-hidden"}`,
127
145
  children: items.map((item, idx) => renderNavItem(item, `${href}-${idx}`))
128
146
  }
129
147
  )
@@ -30,19 +30,24 @@ function ExpandableSidebarItem({
30
30
  isExpanded
31
31
  } = useSidebar();
32
32
  const [closing, setClosing] = useState(false);
33
- const ready = true;
34
33
  const expanded = useMemo(() => isExpanded(href), [href, isExpanded]);
35
34
  useEffect(() => {
36
35
  if (defaultExpanded === null) return;
37
36
  if (defaultExpanded) expandItem(href);
38
37
  else collapseItem(href);
39
38
  }, [defaultExpanded, expandItem, collapseItem, href]);
39
+ const commitCollapse = useCallback(() => {
40
+ collapseItem(href);
41
+ setClosing(false);
42
+ }, [collapseItem, href]);
40
43
  const handleAnimationEnd = useCallback(() => {
41
- if (closing) {
42
- collapseItem(href);
43
- setClosing(false);
44
- }
45
- }, [closing, ready, collapseItem, href]);
44
+ if (closing) commitCollapse();
45
+ }, [closing, commitCollapse]);
46
+ useEffect(() => {
47
+ if (!closing) return;
48
+ const timer = setTimeout(commitCollapse, 250);
49
+ return () => clearTimeout(timer);
50
+ }, [closing, commitCollapse]);
46
51
  const toggleAccordion = useCallback(
47
52
  (e, onlyExpand = false) => {
48
53
  e == null ? void 0 : e.preventDefault();
@@ -95,29 +100,42 @@ function ExpandableSidebarItem({
95
100
  );
96
101
  };
97
102
  return /* @__PURE__ */ jsxs("div", { className: `${styles.container}`, children: [
98
- /* @__PURE__ */ jsx(Component, { onClick: () => toggleAccordion(void 0, true), children: /* @__PURE__ */ jsx(
99
- SidebarItemContent,
100
- {
101
- headerStyle: expanded,
102
- icon,
103
- iconWidth,
104
- label,
105
- href,
106
- disableActiveStyles: expanded,
107
- truncateLabel,
108
- suffixIcon: isSidebarCollapsed ? null : /* @__PURE__ */ jsx(Button, { variant: "outlined", onClick: toggleAccordion, children: /* @__PURE__ */ jsx(
109
- ChevronDown,
110
- {
111
- className: `${styles.chevron} ${expanded ? styles.chevronExpanded : ""}`
112
- }
113
- ) })
114
- }
115
- ) }),
103
+ /* @__PURE__ */ jsxs("div", { className: styles.itemRow, children: [
104
+ /* @__PURE__ */ jsx(Component, { onClick: () => toggleAccordion(void 0, true), children: /* @__PURE__ */ jsx(
105
+ SidebarItemContent,
106
+ {
107
+ headerStyle: expanded,
108
+ icon,
109
+ iconWidth,
110
+ label,
111
+ href,
112
+ disableActiveStyles: expanded,
113
+ truncateLabel
114
+ }
115
+ ) }),
116
+ !isSidebarCollapsed && /* @__PURE__ */ jsx("div", { className: styles.toggleButton, children: /* @__PURE__ */ jsx(
117
+ Button,
118
+ {
119
+ variant: "outlined",
120
+ "aria-label": expanded ? `Collapse ${label}` : `Expand ${label}`,
121
+ "aria-expanded": expanded,
122
+ "aria-controls": `${href}-children`,
123
+ onClick: toggleAccordion,
124
+ children: /* @__PURE__ */ jsx(
125
+ ChevronDown,
126
+ {
127
+ className: `${styles.chevron} ${expanded ? styles.chevronExpanded : ""}`
128
+ }
129
+ )
130
+ }
131
+ ) })
132
+ ] }),
116
133
  expanded && !isSidebarCollapsed && /* @__PURE__ */ jsx(
117
134
  "div",
118
135
  {
136
+ id: `${href}-children`,
119
137
  onAnimationEnd: handleAnimationEnd,
120
- className: `${styles.childrenContainer} ${closing ? "animate--collapse" : ""} ${expanded ? "animate--expand" : "visually-hidden"}`,
138
+ className: `${styles.childrenContainer} ${closing ? "animate--collapse" : expanded ? "animate--expand" : "visually-hidden"}`,
121
139
  children: items.map((item, idx) => renderNavItem(item, `${href}-${idx}`))
122
140
  }
123
141
  )
@@ -5,6 +5,20 @@
5
5
  border-radius: var(--border-radius-default);
6
6
  }
7
7
 
8
+ .itemRow {
9
+ display: grid;
10
+ grid-template-columns: minmax(0, 1fr) auto;
11
+ align-items: center;
12
+ column-gap: var(--spacing-xs);
13
+ }
14
+
15
+ .toggleButton {
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ flex: 0 0 auto;
20
+ }
21
+
8
22
  .container button {
9
23
  color: var(--color-text);
10
24
  min-block-size: 20px !important;
@@ -91,7 +91,6 @@
91
91
  font-size: var(--font-size-xs);
92
92
  text-transform: uppercase;
93
93
  letter-spacing: 0.04em;
94
- transition: 0.15s ease-in-out;
95
94
  }
96
95
 
97
96
  .label {
@@ -2,6 +2,7 @@
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var react = require('react');
5
+ var usePersistentState = require('../../hooks/usePersistentState');
5
6
  var styles = require('./Tabs.module.css');
6
7
  var Chip = require('../chip/Chip');
7
8
  var Headline = require('../headline/Headline');
@@ -19,6 +20,9 @@ function getFirstEnabledId(items) {
19
20
  var _a;
20
21
  return (_a = items.find((t) => !t.hidden && !t.disabled)) == null ? void 0 : _a.id;
21
22
  }
23
+ function isTabId(value) {
24
+ return typeof value === "string" || typeof value === "number";
25
+ }
22
26
  function normalizeFromChildren(children) {
23
27
  const items = [];
24
28
  react.Children.forEach(children, (child) => {
@@ -46,6 +50,7 @@ function Tabs({
46
50
  tabs,
47
51
  value,
48
52
  defaultValue,
53
+ storageKey,
49
54
  onValueChange,
50
55
  addition,
51
56
  disableTopPadding,
@@ -59,8 +64,16 @@ function Tabs({
59
64
  }, [tabs, children]);
60
65
  const visibleTabs = react.useMemo(() => sourceTabs.filter((t) => !t.hidden), [sourceTabs]);
61
66
  const isControlled = value !== void 0;
62
- const [internalValue, setInternalValue] = react.useState(() => {
63
- return defaultValue != null ? defaultValue : getFirstEnabledId(visibleTabs);
67
+ const fallbackValue = defaultValue != null ? defaultValue : getFirstEnabledId(visibleTabs);
68
+ const validateStoredTabId = react.useCallback(
69
+ (stored) => isTabId(stored) && visibleTabs.some((t) => t.id === stored && !t.disabled),
70
+ [visibleTabs]
71
+ );
72
+ const { value: internalValue, setValue: setInternalValue } = usePersistentState.usePersistentState({
73
+ initialValue: fallbackValue,
74
+ storageKey,
75
+ isControlled,
76
+ validate: validateStoredTabId
64
77
  });
65
78
  const currentValue = isControlled ? value : internalValue;
66
79
  const activeIndex = react.useMemo(() => {
@@ -77,7 +90,7 @@ function Tabs({
77
90
  if (!isControlled) setInternalValue(nextId);
78
91
  onValueChange == null ? void 0 : onValueChange(nextId, tab, idx);
79
92
  },
80
- [visibleTabs, isControlled, onValueChange]
93
+ [visibleTabs, isControlled, onValueChange, setInternalValue]
81
94
  );
82
95
  react.useEffect(() => {
83
96
  if (!visibleTabs.length) return;
@@ -21,6 +21,11 @@ export interface TabsProps {
21
21
  value?: TabId;
22
22
  /** Uncontrolled initial */
23
23
  defaultValue?: TabId;
24
+ /**
25
+ * Persist the active tab id in localStorage when used uncontrolled.
26
+ * Ignored in controlled mode.
27
+ */
28
+ storageKey?: string;
24
29
  onValueChange?: (id: TabId, tab: TabItem, index: number) => void;
25
30
  addition?: ReactNode;
26
31
  disableTopPadding?: boolean;
@@ -41,7 +46,7 @@ type SlotName = 'Item';
41
46
  type TabsItemComponent = ((props: TabsItemProps) => JSX.Element) & {
42
47
  __TABS_SLOT__?: SlotName;
43
48
  };
44
- export declare function Tabs({ header, subheader, variant, panelStyle, tabs, value, defaultValue, onValueChange, addition, disableTopPadding, loading, children, }: TabsProps): JSX.Element;
49
+ export declare function Tabs({ header, subheader, variant, panelStyle, tabs, value, defaultValue, storageKey, onValueChange, addition, disableTopPadding, loading, children, }: TabsProps): JSX.Element;
45
50
  export declare namespace Tabs {
46
51
  var Item: TabsItemComponent;
47
52
  }
@@ -1,5 +1,6 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import { useId, useMemo, useState, useCallback, useEffect, Children, isValidElement } from 'react';
2
+ import { useId, useMemo, useCallback, useEffect, Children, isValidElement } from 'react';
3
+ import { usePersistentState } from '../../hooks/usePersistentState';
3
4
  import styles from './Tabs.module.css';
4
5
  import { Chip } from '../chip/Chip';
5
6
  import { Headline } from '../headline/Headline';
@@ -13,6 +14,9 @@ function getFirstEnabledId(items) {
13
14
  var _a;
14
15
  return (_a = items.find((t) => !t.hidden && !t.disabled)) == null ? void 0 : _a.id;
15
16
  }
17
+ function isTabId(value) {
18
+ return typeof value === "string" || typeof value === "number";
19
+ }
16
20
  function normalizeFromChildren(children) {
17
21
  const items = [];
18
22
  Children.forEach(children, (child) => {
@@ -40,6 +44,7 @@ function Tabs({
40
44
  tabs,
41
45
  value,
42
46
  defaultValue,
47
+ storageKey,
43
48
  onValueChange,
44
49
  addition,
45
50
  disableTopPadding,
@@ -53,8 +58,16 @@ function Tabs({
53
58
  }, [tabs, children]);
54
59
  const visibleTabs = useMemo(() => sourceTabs.filter((t) => !t.hidden), [sourceTabs]);
55
60
  const isControlled = value !== void 0;
56
- const [internalValue, setInternalValue] = useState(() => {
57
- return defaultValue != null ? defaultValue : getFirstEnabledId(visibleTabs);
61
+ const fallbackValue = defaultValue != null ? defaultValue : getFirstEnabledId(visibleTabs);
62
+ const validateStoredTabId = useCallback(
63
+ (stored) => isTabId(stored) && visibleTabs.some((t) => t.id === stored && !t.disabled),
64
+ [visibleTabs]
65
+ );
66
+ const { value: internalValue, setValue: setInternalValue } = usePersistentState({
67
+ initialValue: fallbackValue,
68
+ storageKey,
69
+ isControlled,
70
+ validate: validateStoredTabId
58
71
  });
59
72
  const currentValue = isControlled ? value : internalValue;
60
73
  const activeIndex = useMemo(() => {
@@ -71,7 +84,7 @@ function Tabs({
71
84
  if (!isControlled) setInternalValue(nextId);
72
85
  onValueChange == null ? void 0 : onValueChange(nextId, tab, idx);
73
86
  },
74
- [visibleTabs, isControlled, onValueChange]
87
+ [visibleTabs, isControlled, onValueChange, setInternalValue]
75
88
  );
76
89
  useEffect(() => {
77
90
  if (!visibleTabs.length) return;
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ var react = require('react');
5
+ var localStorage_utils = require('../utils/localStorage.utils');
6
+
7
+ function usePersistentState({
8
+ initialValue,
9
+ storageKey,
10
+ isControlled = false,
11
+ validate
12
+ }) {
13
+ const shouldHydrate = !isControlled && Boolean(storageKey);
14
+ const [didHydrate, setDidHydrate] = react.useState(() => !shouldHydrate);
15
+ const [value, setValue] = react.useState(initialValue);
16
+ const hydrated = !shouldHydrate || didHydrate;
17
+ const validateRef = react.useRef(validate);
18
+ react.useEffect(() => {
19
+ validateRef.current = validate;
20
+ }, [validate]);
21
+ react.useEffect(() => {
22
+ if (!storageKey || !shouldHydrate) return;
23
+ const stored = localStorage_utils.readLocalStorage(storageKey);
24
+ const currentValidate = validateRef.current;
25
+ const nextValue = currentValidate == null || currentValidate(stored) ? stored : initialValue;
26
+ setValue(nextValue);
27
+ setDidHydrate(true);
28
+ }, [initialValue, shouldHydrate, storageKey]);
29
+ react.useEffect(() => {
30
+ if (isControlled || !storageKey) return;
31
+ if (!hydrated) return;
32
+ localStorage_utils.writeLocalStorage(storageKey, value);
33
+ }, [hydrated, isControlled, storageKey, value]);
34
+ return {
35
+ value,
36
+ setValue,
37
+ hydrated
38
+ };
39
+ }
40
+
41
+ exports.usePersistentState = usePersistentState;
@@ -0,0 +1,13 @@
1
+ import { Dispatch, SetStateAction } from 'react';
2
+ export interface UsePersistentStateOptions<T> {
3
+ initialValue: T;
4
+ storageKey?: string;
5
+ isControlled?: boolean;
6
+ validate?: (value: unknown) => value is T;
7
+ }
8
+ export interface UsePersistentStateResult<T> {
9
+ value: T;
10
+ setValue: Dispatch<SetStateAction<T>>;
11
+ hydrated: boolean;
12
+ }
13
+ export declare function usePersistentState<T>({ initialValue, storageKey, isControlled, validate, }: UsePersistentStateOptions<T>): UsePersistentStateResult<T>;
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+ import { useState, useRef, useEffect } from 'react';
3
+ import { readLocalStorage, writeLocalStorage } from '../utils/localStorage.utils';
4
+
5
+ function usePersistentState({
6
+ initialValue,
7
+ storageKey,
8
+ isControlled = false,
9
+ validate
10
+ }) {
11
+ const shouldHydrate = !isControlled && Boolean(storageKey);
12
+ const [didHydrate, setDidHydrate] = useState(() => !shouldHydrate);
13
+ const [value, setValue] = useState(initialValue);
14
+ const hydrated = !shouldHydrate || didHydrate;
15
+ const validateRef = useRef(validate);
16
+ useEffect(() => {
17
+ validateRef.current = validate;
18
+ }, [validate]);
19
+ useEffect(() => {
20
+ if (!storageKey || !shouldHydrate) return;
21
+ const stored = readLocalStorage(storageKey);
22
+ const currentValidate = validateRef.current;
23
+ const nextValue = currentValidate == null || currentValidate(stored) ? stored : initialValue;
24
+ setValue(nextValue);
25
+ setDidHydrate(true);
26
+ }, [initialValue, shouldHydrate, storageKey]);
27
+ useEffect(() => {
28
+ if (isControlled || !storageKey) return;
29
+ if (!hydrated) return;
30
+ writeLocalStorage(storageKey, value);
31
+ }, [hydrated, isControlled, storageKey, value]);
32
+ return {
33
+ value,
34
+ setValue,
35
+ hydrated
36
+ };
37
+ }
38
+
39
+ export { usePersistentState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.133",
3
+ "version": "0.0.135",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",