@dbcdk/react-components 0.0.104 → 0.0.105

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.
@@ -13,8 +13,11 @@ var styles__default = /*#__PURE__*/_interopDefault(styles);
13
13
  function uniqSorted(nums) {
14
14
  return Array.from(new Set(nums)).sort((a, b) => a - b);
15
15
  }
16
- function normalizeMultiple(indexes) {
17
- return uniqSorted((indexes != null ? indexes : []).filter((n) => Number.isFinite(n)));
16
+ function normalizeMultiple(indexes, itemCount) {
17
+ if (indexes === "all") {
18
+ return Array.from({ length: itemCount }, (_, i) => i);
19
+ }
20
+ return uniqSorted((indexes != null ? indexes : []).filter((n) => Number.isInteger(n) && n >= 0 && n < itemCount));
18
21
  }
19
22
  function Accordion({
20
23
  items,
@@ -34,35 +37,40 @@ function Accordion({
34
37
  react.useEffect(() => {
35
38
  setHasMounted(true);
36
39
  }, []);
37
- const [internalSingle, setInternalSingle] = react.useState(
38
- mode === "single" ? defaultOpenIndex : null
39
- );
40
+ const [internalSingle, setInternalSingle] = react.useState(() => {
41
+ if (mode !== "single") return null;
42
+ if (defaultOpenIndex !== null && Number.isInteger(defaultOpenIndex) && defaultOpenIndex >= 0 && defaultOpenIndex < items.length) {
43
+ return defaultOpenIndex;
44
+ }
45
+ return null;
46
+ });
40
47
  const [internalMultiple, setInternalMultiple] = react.useState(() => {
41
48
  if (mode !== "multiple") return [];
42
- if (defaultOpenIndexes === "all") return items.map((_, i) => i);
43
- return normalizeMultiple(defaultOpenIndexes);
49
+ return normalizeMultiple(defaultOpenIndexes, items.length);
44
50
  });
45
51
  const currentOpenIndexes = react.useMemo(() => {
46
52
  if (mode === "single") {
47
53
  const current = isControlled ? openIndex != null ? openIndex : null : internalSingle;
48
- return current === null ? [] : [current];
54
+ return current !== null && Number.isInteger(current) && current >= 0 && current < items.length ? [current] : [];
49
55
  }
50
- return isControlled ? normalizeMultiple(openIndexes) : internalMultiple;
51
- }, [mode, isControlled, openIndex, openIndexes, internalSingle, internalMultiple]);
56
+ return isControlled ? normalizeMultiple(openIndexes, items.length) : normalizeMultiple(internalMultiple, items.length);
57
+ }, [mode, isControlled, openIndex, openIndexes, internalSingle, internalMultiple, items.length]);
52
58
  const openSet = react.useMemo(() => new Set(currentOpenIndexes), [currentOpenIndexes]);
53
59
  function commit(nextIndexes) {
54
60
  if (mode === "single") {
55
61
  const next = nextIndexes.length ? nextIndexes[0] : null;
56
- if (isControlled) onOpenIndexChange == null ? void 0 : onOpenIndexChange(next);
57
- else {
62
+ if (isControlled) {
63
+ onOpenIndexChange == null ? void 0 : onOpenIndexChange(next);
64
+ } else {
58
65
  setInternalSingle(next);
59
66
  onOpenIndexChange == null ? void 0 : onOpenIndexChange(next);
60
67
  }
61
68
  return;
62
69
  }
63
- const normalized = uniqSorted(nextIndexes);
64
- if (isControlled) onOpenIndexesChange == null ? void 0 : onOpenIndexesChange(normalized);
65
- else {
70
+ const normalized = normalizeMultiple(nextIndexes, items.length);
71
+ if (isControlled) {
72
+ onOpenIndexesChange == null ? void 0 : onOpenIndexesChange(normalized);
73
+ } else {
66
74
  setInternalMultiple(normalized);
67
75
  onOpenIndexesChange == null ? void 0 : onOpenIndexesChange(normalized);
68
76
  }
@@ -75,27 +83,33 @@ function Accordion({
75
83
  commit(isOpen ? [] : [index]);
76
84
  return;
77
85
  }
78
- if (isOpen) commit(currentOpenIndexes.filter((i) => i !== index));
79
- else commit([...currentOpenIndexes, index]);
86
+ if (isOpen) {
87
+ commit(currentOpenIndexes.filter((i) => i !== index));
88
+ } else {
89
+ commit([...currentOpenIndexes, index]);
90
+ }
80
91
  }
81
92
  return /* @__PURE__ */ jsxRuntime.jsx(
82
93
  "div",
83
94
  {
84
95
  className: [styles__default.default.container, styles__default.default[size], variant !== "default" ? styles__default.default[variant] : ""].filter(Boolean).join(" "),
85
- children: items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(
86
- AccordionRow.AccordionRow,
87
- {
88
- uid,
89
- index: i,
90
- item,
91
- isOpen: openSet.has(i),
92
- onToggle: toggle,
93
- shouldAnimate: hasMounted,
94
- headlineSize: size === "sm" ? 4 : 3,
95
- variant
96
- },
97
- i
98
- ))
96
+ children: items.map((item, i) => {
97
+ var _a;
98
+ return /* @__PURE__ */ jsxRuntime.jsx(
99
+ AccordionRow.AccordionRow,
100
+ {
101
+ uid,
102
+ index: i,
103
+ item,
104
+ isOpen: openSet.has(i),
105
+ onToggle: toggle,
106
+ shouldAnimate: hasMounted,
107
+ headlineSize: size === "sm" ? 4 : 3,
108
+ variant
109
+ },
110
+ (_a = item.id) != null ? _a : i
111
+ );
112
+ })
99
113
  }
100
114
  );
101
115
  }
@@ -1,9 +1,11 @@
1
1
  import type { JSX, ReactNode } from 'react';
2
2
  import type { Severity } from '../../constants/severity.types';
3
3
  export interface AccordionItem {
4
+ id?: string;
4
5
  header: string;
5
6
  subheader?: ReactNode;
6
7
  headerAddition?: ReactNode;
8
+ headerActions?: ReactNode;
7
9
  headerIcon?: ReactNode;
8
10
  severity?: Severity;
9
11
  children: ReactNode;
@@ -12,6 +14,7 @@ export interface AccordionItem {
12
14
  type Size = 'sm' | 'md' | 'lg';
13
15
  type Mode = 'single' | 'multiple';
14
16
  type Variant = 'default' | 'outlined';
17
+ type OpenIndexes = number[] | 'all';
15
18
  export interface AccordionProps {
16
19
  items: AccordionItem[];
17
20
  mode?: Mode;
@@ -19,10 +22,10 @@ export interface AccordionProps {
19
22
  variant?: Variant;
20
23
  /** Uncontrolled defaults */
21
24
  defaultOpenIndex?: number | null;
22
- defaultOpenIndexes?: number[] | 'all';
25
+ defaultOpenIndexes?: OpenIndexes;
23
26
  /** Controlled state */
24
27
  openIndex?: number | null;
25
- openIndexes?: number[];
28
+ openIndexes?: OpenIndexes;
26
29
  /** Change callbacks */
27
30
  onOpenIndexChange?: (index: number | null) => void;
28
31
  onOpenIndexesChange?: (indexes: number[]) => void;
@@ -7,8 +7,11 @@ import { AccordionRow } from './components/AccordionRow';
7
7
  function uniqSorted(nums) {
8
8
  return Array.from(new Set(nums)).sort((a, b) => a - b);
9
9
  }
10
- function normalizeMultiple(indexes) {
11
- return uniqSorted((indexes != null ? indexes : []).filter((n) => Number.isFinite(n)));
10
+ function normalizeMultiple(indexes, itemCount) {
11
+ if (indexes === "all") {
12
+ return Array.from({ length: itemCount }, (_, i) => i);
13
+ }
14
+ return uniqSorted((indexes != null ? indexes : []).filter((n) => Number.isInteger(n) && n >= 0 && n < itemCount));
12
15
  }
13
16
  function Accordion({
14
17
  items,
@@ -28,35 +31,40 @@ function Accordion({
28
31
  useEffect(() => {
29
32
  setHasMounted(true);
30
33
  }, []);
31
- const [internalSingle, setInternalSingle] = useState(
32
- mode === "single" ? defaultOpenIndex : null
33
- );
34
+ const [internalSingle, setInternalSingle] = useState(() => {
35
+ if (mode !== "single") return null;
36
+ if (defaultOpenIndex !== null && Number.isInteger(defaultOpenIndex) && defaultOpenIndex >= 0 && defaultOpenIndex < items.length) {
37
+ return defaultOpenIndex;
38
+ }
39
+ return null;
40
+ });
34
41
  const [internalMultiple, setInternalMultiple] = useState(() => {
35
42
  if (mode !== "multiple") return [];
36
- if (defaultOpenIndexes === "all") return items.map((_, i) => i);
37
- return normalizeMultiple(defaultOpenIndexes);
43
+ return normalizeMultiple(defaultOpenIndexes, items.length);
38
44
  });
39
45
  const currentOpenIndexes = useMemo(() => {
40
46
  if (mode === "single") {
41
47
  const current = isControlled ? openIndex != null ? openIndex : null : internalSingle;
42
- return current === null ? [] : [current];
48
+ return current !== null && Number.isInteger(current) && current >= 0 && current < items.length ? [current] : [];
43
49
  }
44
- return isControlled ? normalizeMultiple(openIndexes) : internalMultiple;
45
- }, [mode, isControlled, openIndex, openIndexes, internalSingle, internalMultiple]);
50
+ return isControlled ? normalizeMultiple(openIndexes, items.length) : normalizeMultiple(internalMultiple, items.length);
51
+ }, [mode, isControlled, openIndex, openIndexes, internalSingle, internalMultiple, items.length]);
46
52
  const openSet = useMemo(() => new Set(currentOpenIndexes), [currentOpenIndexes]);
47
53
  function commit(nextIndexes) {
48
54
  if (mode === "single") {
49
55
  const next = nextIndexes.length ? nextIndexes[0] : null;
50
- if (isControlled) onOpenIndexChange == null ? void 0 : onOpenIndexChange(next);
51
- else {
56
+ if (isControlled) {
57
+ onOpenIndexChange == null ? void 0 : onOpenIndexChange(next);
58
+ } else {
52
59
  setInternalSingle(next);
53
60
  onOpenIndexChange == null ? void 0 : onOpenIndexChange(next);
54
61
  }
55
62
  return;
56
63
  }
57
- const normalized = uniqSorted(nextIndexes);
58
- if (isControlled) onOpenIndexesChange == null ? void 0 : onOpenIndexesChange(normalized);
59
- else {
64
+ const normalized = normalizeMultiple(nextIndexes, items.length);
65
+ if (isControlled) {
66
+ onOpenIndexesChange == null ? void 0 : onOpenIndexesChange(normalized);
67
+ } else {
60
68
  setInternalMultiple(normalized);
61
69
  onOpenIndexesChange == null ? void 0 : onOpenIndexesChange(normalized);
62
70
  }
@@ -69,27 +77,33 @@ function Accordion({
69
77
  commit(isOpen ? [] : [index]);
70
78
  return;
71
79
  }
72
- if (isOpen) commit(currentOpenIndexes.filter((i) => i !== index));
73
- else commit([...currentOpenIndexes, index]);
80
+ if (isOpen) {
81
+ commit(currentOpenIndexes.filter((i) => i !== index));
82
+ } else {
83
+ commit([...currentOpenIndexes, index]);
84
+ }
74
85
  }
75
86
  return /* @__PURE__ */ jsx(
76
87
  "div",
77
88
  {
78
89
  className: [styles.container, styles[size], variant !== "default" ? styles[variant] : ""].filter(Boolean).join(" "),
79
- children: items.map((item, i) => /* @__PURE__ */ jsx(
80
- AccordionRow,
81
- {
82
- uid,
83
- index: i,
84
- item,
85
- isOpen: openSet.has(i),
86
- onToggle: toggle,
87
- shouldAnimate: hasMounted,
88
- headlineSize: size === "sm" ? 4 : 3,
89
- variant
90
- },
91
- i
92
- ))
90
+ children: items.map((item, i) => {
91
+ var _a;
92
+ return /* @__PURE__ */ jsx(
93
+ AccordionRow,
94
+ {
95
+ uid,
96
+ index: i,
97
+ item,
98
+ isOpen: openSet.has(i),
99
+ onToggle: toggle,
100
+ shouldAnimate: hasMounted,
101
+ headlineSize: size === "sm" ? 4 : 3,
102
+ variant
103
+ },
104
+ (_a = item.id) != null ? _a : i
105
+ );
106
+ })
93
107
  }
94
108
  );
95
109
  }
@@ -56,6 +56,7 @@ function AccordionRow({
56
56
  variant = "default"
57
57
  }) {
58
58
  const isDisabled = !!item.disabled;
59
+ const headlineWeight = isOpen ? variant === "outlined" ? 500 : 600 : variant === "outlined" ? 400 : 500;
59
60
  const buttonId = `${uid}-acc-btn-${index}`;
60
61
  const panelId = `${uid}-acc-panel-${index}`;
61
62
  const { innerRef, height, onTransitionEnd } = useCollapsibleHeight(isOpen, shouldAnimate);
@@ -63,38 +64,42 @@ function AccordionRow({
63
64
  "section",
64
65
  {
65
66
  className: `${styles__default.default.item} ${isOpen ? styles__default.default.open : ""} ${isDisabled ? styles__default.default.disabled : ""}`,
67
+ "data-state": isOpen ? "open" : "closed",
66
68
  children: [
67
- /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsxs(
68
- "button",
69
- {
70
- type: "button",
71
- id: buttonId,
72
- className: styles__default.default.trigger,
73
- "aria-expanded": isOpen,
74
- "aria-controls": panelId,
75
- onClick: () => onToggle(index),
76
- disabled: isDisabled,
77
- children: [
78
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles__default.default.title, children: [
79
- item.headerIcon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.icon, children: item.headerIcon }) : null,
80
- /* @__PURE__ */ jsxRuntime.jsx(
81
- Headline.Headline,
82
- {
83
- disableMargin: true,
84
- size: headlineSize,
85
- weight: variant === "outlined" ? 400 : 500,
86
- severity: item.severity,
87
- subheader: item.subheader,
88
- allowWrap: isOpen,
89
- children: item.header
90
- }
91
- ),
92
- item.headerAddition
93
- ] }),
94
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.chevron, "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, {}) })
95
- ]
96
- }
97
- ) }),
69
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.headerRow, children: [
70
+ /* @__PURE__ */ jsxRuntime.jsxs(
71
+ "button",
72
+ {
73
+ type: "button",
74
+ id: buttonId,
75
+ className: styles__default.default.trigger,
76
+ "aria-expanded": isOpen,
77
+ "aria-controls": panelId,
78
+ onClick: () => onToggle(index),
79
+ disabled: isDisabled,
80
+ children: [
81
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.title, children: [
82
+ item.headerIcon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.icon, children: item.headerIcon }) : null,
83
+ /* @__PURE__ */ jsxRuntime.jsx(
84
+ Headline.Headline,
85
+ {
86
+ disableMargin: true,
87
+ size: headlineSize,
88
+ weight: headlineWeight,
89
+ severity: item.severity,
90
+ subheader: item.subheader,
91
+ allowWrap: isOpen,
92
+ children: item.header
93
+ }
94
+ ),
95
+ item.headerAddition ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.headerAddition, children: item.headerAddition }) : null
96
+ ] }),
97
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.chevron, "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, {}) })
98
+ ]
99
+ }
100
+ ),
101
+ item.headerActions ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.headerActions, onClick: (event) => event.stopPropagation(), children: item.headerActions }) : null
102
+ ] }),
98
103
  /* @__PURE__ */ jsxRuntime.jsx(
99
104
  "div",
100
105
  {
@@ -50,6 +50,7 @@ function AccordionRow({
50
50
  variant = "default"
51
51
  }) {
52
52
  const isDisabled = !!item.disabled;
53
+ const headlineWeight = isOpen ? variant === "outlined" ? 500 : 600 : variant === "outlined" ? 400 : 500;
53
54
  const buttonId = `${uid}-acc-btn-${index}`;
54
55
  const panelId = `${uid}-acc-panel-${index}`;
55
56
  const { innerRef, height, onTransitionEnd } = useCollapsibleHeight(isOpen, shouldAnimate);
@@ -57,38 +58,42 @@ function AccordionRow({
57
58
  "section",
58
59
  {
59
60
  className: `${styles.item} ${isOpen ? styles.open : ""} ${isDisabled ? styles.disabled : ""}`,
61
+ "data-state": isOpen ? "open" : "closed",
60
62
  children: [
61
- /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(
62
- "button",
63
- {
64
- type: "button",
65
- id: buttonId,
66
- className: styles.trigger,
67
- "aria-expanded": isOpen,
68
- "aria-controls": panelId,
69
- onClick: () => onToggle(index),
70
- disabled: isDisabled,
71
- children: [
72
- /* @__PURE__ */ jsxs("span", { className: styles.title, children: [
73
- item.headerIcon ? /* @__PURE__ */ jsx("span", { className: styles.icon, children: item.headerIcon }) : null,
74
- /* @__PURE__ */ jsx(
75
- Headline,
76
- {
77
- disableMargin: true,
78
- size: headlineSize,
79
- weight: variant === "outlined" ? 400 : 500,
80
- severity: item.severity,
81
- subheader: item.subheader,
82
- allowWrap: isOpen,
83
- children: item.header
84
- }
85
- ),
86
- item.headerAddition
87
- ] }),
88
- /* @__PURE__ */ jsx("span", { className: styles.chevron, "aria-hidden": "true", children: /* @__PURE__ */ jsx(ChevronDown, {}) })
89
- ]
90
- }
91
- ) }),
63
+ /* @__PURE__ */ jsxs("div", { className: styles.headerRow, children: [
64
+ /* @__PURE__ */ jsxs(
65
+ "button",
66
+ {
67
+ type: "button",
68
+ id: buttonId,
69
+ className: styles.trigger,
70
+ "aria-expanded": isOpen,
71
+ "aria-controls": panelId,
72
+ onClick: () => onToggle(index),
73
+ disabled: isDisabled,
74
+ children: [
75
+ /* @__PURE__ */ jsxs("div", { className: styles.title, children: [
76
+ item.headerIcon ? /* @__PURE__ */ jsx("span", { className: styles.icon, children: item.headerIcon }) : null,
77
+ /* @__PURE__ */ jsx(
78
+ Headline,
79
+ {
80
+ disableMargin: true,
81
+ size: headlineSize,
82
+ weight: headlineWeight,
83
+ severity: item.severity,
84
+ subheader: item.subheader,
85
+ allowWrap: isOpen,
86
+ children: item.header
87
+ }
88
+ ),
89
+ item.headerAddition ? /* @__PURE__ */ jsx("div", { className: styles.headerAddition, children: item.headerAddition }) : null
90
+ ] }),
91
+ /* @__PURE__ */ jsx("span", { className: styles.chevron, "aria-hidden": "true", children: /* @__PURE__ */ jsx(ChevronDown, {}) })
92
+ ]
93
+ }
94
+ ),
95
+ item.headerActions ? /* @__PURE__ */ jsx("div", { className: styles.headerActions, onClick: (event) => event.stopPropagation(), children: item.headerActions }) : null
96
+ ] }),
92
97
  /* @__PURE__ */ jsx(
93
98
  "div",
94
99
  {
@@ -1,23 +1,41 @@
1
+ .headerRow {
2
+ display: flex;
3
+ align-items: stretch;
4
+ width: 100%;
5
+ background: var(--acc-trigger-bg, var(--color-bg-contextual-subtle));
6
+ }
7
+
1
8
  .trigger {
2
9
  all: unset;
3
10
  box-sizing: border-box;
4
-
5
- width: 100%;
6
11
  display: flex;
7
12
  align-items: center;
8
13
  justify-content: space-between;
9
14
  gap: var(--spacing-sm);
15
+ flex: 1 1 auto;
10
16
 
11
17
  cursor: pointer;
12
18
  user-select: none;
13
19
 
14
20
  font-size: var(--acc-font-size);
15
21
  padding: var(--acc-trigger-py) var(--acc-trigger-px);
16
- background: var(--acc-trigger-bg, var(--color-bg-contextual-subtle));
22
+ background: transparent;
17
23
 
18
24
  min-width: 0;
19
25
  }
20
26
 
27
+ .headerActions {
28
+ flex: 0 0 auto;
29
+ display: flex;
30
+ align-items: center;
31
+ padding-block: var(--acc-trigger-py);
32
+ padding-inline: 0 var(--acc-trigger-px);
33
+ }
34
+
35
+ .headerActions > * {
36
+ flex: 0 0 auto;
37
+ }
38
+
21
39
  .trigger:focus-visible {
22
40
  outline: none;
23
41
  box-shadow: var(--focus-ring);
@@ -37,10 +55,18 @@
37
55
  overflow: hidden;
38
56
  }
39
57
 
58
+ .headerAddition {
59
+ display: flex;
60
+ align-items: baseline;
61
+ flex: 1 1 auto;
62
+ min-width: 0;
63
+ }
64
+
40
65
  .icon {
41
66
  flex: 0 0 auto;
42
67
  display: inline-flex;
43
68
  align-items: center;
69
+ align-self: center;
44
70
  }
45
71
 
46
72
  .icon svg {
@@ -88,6 +114,14 @@
88
114
  border-bottom: var(--acc-item-separator, none);
89
115
  }
90
116
 
117
+ .item[data-state='open'] .headerRow {
118
+ background: var(--color-bg-contextual);
119
+ }
120
+
121
+ :global(.outlined) .item[data-state='open'] .headerRow {
122
+ box-shadow: inset 3px 0 0 var(--color-border-selected);
123
+ }
124
+
91
125
  .item:last-child {
92
126
  border-bottom: none;
93
127
  }
@@ -27,17 +27,26 @@ function CollapsibleHeadline({
27
27
  }) {
28
28
  const generatedId = react.useId();
29
29
  const panelId = controls != null ? controls : generatedId;
30
- const [internalExpanded, setInternalExpanded] = react.useState(() => {
31
- if (!storageKey || typeof window === "undefined") return expanded != null ? expanded : true;
32
- const stored = localStorage.getItem(storageKey);
33
- return stored !== null ? stored === "true" : expanded != null ? expanded : true;
34
- });
35
- const isExpanded = storageKey ? internalExpanded : expanded != null ? expanded : false;
30
+ const isControlled = expanded !== void 0;
31
+ const [internalExpanded, setInternalExpanded] = react.useState(false);
32
+ const [hasToggled, setHasToggled] = react.useState(false);
33
+ const persistedExpanded = react.useSyncExternalStore(
34
+ () => () => void 0,
35
+ () => {
36
+ if (isControlled || !storageKey) return false;
37
+ return localStorage.getItem(storageKey) === "true";
38
+ },
39
+ () => false
40
+ );
41
+ const isExpanded = isControlled ? expanded : hasToggled ? internalExpanded : persistedExpanded;
36
42
  const handleToggle = () => {
37
- if (storageKey) {
38
- const next = !internalExpanded;
43
+ const next = !isExpanded;
44
+ if (!isControlled) {
45
+ setHasToggled(true);
39
46
  setInternalExpanded(next);
40
- localStorage.setItem(storageKey, String(next));
47
+ if (storageKey) {
48
+ localStorage.setItem(storageKey, String(next));
49
+ }
41
50
  }
42
51
  onToggle == null ? void 0 : onToggle();
43
52
  };
@@ -3,7 +3,7 @@ import type { HeadlineProps } from './Headline';
3
3
  export interface CollapsibleHeadlineProps extends Omit<HeadlineProps, 'addition' | 'children'> {
4
4
  /** The headline text — always visible. */
5
5
  header: React.ReactNode;
6
- /** Controlled expanded state. Required when `storageKey` is not set. */
6
+ /** Controlled expanded state. When omitted, the component manages its own state. */
7
7
  expanded?: boolean;
8
8
  /** Called when the toggle is clicked. */
9
9
  onToggle?: () => void;
@@ -12,9 +12,9 @@ export interface CollapsibleHeadlineProps extends Omit<HeadlineProps, 'addition'
12
12
  /** Extra content rendered between the headline text and the toggle button. */
13
13
  addition?: React.ReactNode;
14
14
  /**
15
- * When set the component manages its own expanded state, persisted to
16
- * localStorage under this key. `expanded` is used as the initial value
17
- * if nothing is stored yet; `onToggle` is still called after each toggle.
15
+ * When set in uncontrolled mode, the component persists its expanded
16
+ * state to localStorage under this key. The server render still starts
17
+ * collapsed to stay hydration-safe.
18
18
  */
19
19
  storageKey?: string;
20
20
  }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsxs, jsx } from 'react/jsx-runtime';
3
3
  import { ChevronDown } from 'lucide-react';
4
- import { useId, useState } from 'react';
4
+ import { useId, useState, useSyncExternalStore } from 'react';
5
5
  import { Headline } from './Headline';
6
6
  import styles from './Headline.module.css';
7
7
  import { Button } from '../button/Button';
@@ -21,17 +21,26 @@ function CollapsibleHeadline({
21
21
  }) {
22
22
  const generatedId = useId();
23
23
  const panelId = controls != null ? controls : generatedId;
24
- const [internalExpanded, setInternalExpanded] = useState(() => {
25
- if (!storageKey || typeof window === "undefined") return expanded != null ? expanded : true;
26
- const stored = localStorage.getItem(storageKey);
27
- return stored !== null ? stored === "true" : expanded != null ? expanded : true;
28
- });
29
- const isExpanded = storageKey ? internalExpanded : expanded != null ? expanded : false;
24
+ const isControlled = expanded !== void 0;
25
+ const [internalExpanded, setInternalExpanded] = useState(false);
26
+ const [hasToggled, setHasToggled] = useState(false);
27
+ const persistedExpanded = useSyncExternalStore(
28
+ () => () => void 0,
29
+ () => {
30
+ if (isControlled || !storageKey) return false;
31
+ return localStorage.getItem(storageKey) === "true";
32
+ },
33
+ () => false
34
+ );
35
+ const isExpanded = isControlled ? expanded : hasToggled ? internalExpanded : persistedExpanded;
30
36
  const handleToggle = () => {
31
- if (storageKey) {
32
- const next = !internalExpanded;
37
+ const next = !isExpanded;
38
+ if (!isControlled) {
39
+ setHasToggled(true);
33
40
  setInternalExpanded(next);
34
- localStorage.setItem(storageKey, String(next));
41
+ if (storageKey) {
42
+ localStorage.setItem(storageKey, String(next));
43
+ }
35
44
  }
36
45
  onToggle == null ? void 0 : onToggle();
37
46
  };
@@ -19,6 +19,7 @@ function NavBar({
19
19
  logo: logo$1,
20
20
  items = [],
21
21
  productName,
22
+ productNameComponent: ProductNameComponent,
22
23
  addition,
23
24
  activeLink,
24
25
  size
@@ -45,6 +46,17 @@ function NavBar({
45
46
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.label, children: label })
46
47
  ] }) }, id);
47
48
  });
49
+ const productNameContent = productName ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
50
+ /* @__PURE__ */ jsxRuntime.jsx(logo.Logo, {}),
51
+ /* @__PURE__ */ jsxRuntime.jsx(Headline.Headline, { disableMargin: true, size: 1, children: productName })
52
+ ] }) : null;
53
+ const productNameNode = (() => {
54
+ if (!productName || !productNameContent) return null;
55
+ if (ProductNameComponent) {
56
+ return /* @__PURE__ */ jsxRuntime.jsx(ProductNameComponent, { className: styles__default.default.productName, children: productNameContent });
57
+ }
58
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.productName, children: productNameContent });
59
+ })();
48
60
  return /* @__PURE__ */ jsxRuntime.jsx(AppHeader.AppHeader, { size, children: /* @__PURE__ */ jsxRuntime.jsxs(
49
61
  "nav",
50
62
  {
@@ -54,10 +66,7 @@ function NavBar({
54
66
  children: [
55
67
  (logo$1 || productName) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.logoRow, children: [
56
68
  logo$1,
57
- productName && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles__default.default.productName, children: [
58
- /* @__PURE__ */ jsxRuntime.jsx(logo.Logo, {}),
59
- /* @__PURE__ */ jsxRuntime.jsx(Headline.Headline, { disableMargin: true, size: 1, children: productName })
60
- ] })
69
+ productNameNode
61
70
  ] }),
62
71
  (navLinks == null ? void 0 : navLinks.length) > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.navContent, children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: styles__default.default.navItems, role: "list", children: navLinks }) }),
63
72
  addition && !isMobile && /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.addition, children: addition }),
@@ -31,13 +31,14 @@ export type NavBarGroupItem = NavBarBase & {
31
31
  type: 'group';
32
32
  children: NavBarItem[];
33
33
  };
34
- interface NavBarProps {
34
+ export interface NavBarProps {
35
35
  logo?: ReactNode;
36
36
  items?: NavBarLinkItem[];
37
37
  productName?: string;
38
+ productNameComponent?: ElementType<any>;
38
39
  addition?: ReactNode;
39
40
  activeLink?: string;
40
41
  size?: AppHeaderSize;
41
42
  }
42
- export declare function NavBar({ logo, items, productName, addition, activeLink, size, }: NavBarProps): JSX.Element;
43
+ export declare function NavBar({ logo, items, productName, productNameComponent: ProductNameComponent, addition, activeLink, size, }: NavBarProps): JSX.Element;
43
44
  export {};
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
  import { X, Menu } from 'lucide-react';
4
4
  import { useState, useRef } from 'react';
5
5
  import styles from './NavBar.module.css';
@@ -13,6 +13,7 @@ function NavBar({
13
13
  logo,
14
14
  items = [],
15
15
  productName,
16
+ productNameComponent: ProductNameComponent,
16
17
  addition,
17
18
  activeLink,
18
19
  size
@@ -39,6 +40,17 @@ function NavBar({
39
40
  /* @__PURE__ */ jsx("span", { className: styles.label, children: label })
40
41
  ] }) }, id);
41
42
  });
43
+ const productNameContent = productName ? /* @__PURE__ */ jsxs(Fragment, { children: [
44
+ /* @__PURE__ */ jsx(Logo, {}),
45
+ /* @__PURE__ */ jsx(Headline, { disableMargin: true, size: 1, children: productName })
46
+ ] }) : null;
47
+ const productNameNode = (() => {
48
+ if (!productName || !productNameContent) return null;
49
+ if (ProductNameComponent) {
50
+ return /* @__PURE__ */ jsx(ProductNameComponent, { className: styles.productName, children: productNameContent });
51
+ }
52
+ return /* @__PURE__ */ jsx("span", { className: styles.productName, children: productNameContent });
53
+ })();
42
54
  return /* @__PURE__ */ jsx(AppHeader, { size, children: /* @__PURE__ */ jsxs(
43
55
  "nav",
44
56
  {
@@ -48,10 +60,7 @@ function NavBar({
48
60
  children: [
49
61
  (logo || productName) && /* @__PURE__ */ jsxs("div", { className: styles.logoRow, children: [
50
62
  logo,
51
- productName && /* @__PURE__ */ jsxs("span", { className: styles.productName, children: [
52
- /* @__PURE__ */ jsx(Logo, {}),
53
- /* @__PURE__ */ jsx(Headline, { disableMargin: true, size: 1, children: productName })
54
- ] })
63
+ productNameNode
55
64
  ] }),
56
65
  (navLinks == null ? void 0 : navLinks.length) > 0 && /* @__PURE__ */ jsx("div", { className: styles.navContent, children: /* @__PURE__ */ jsx("ul", { className: styles.navItems, role: "list", children: navLinks }) }),
57
66
  addition && !isMobile && /* @__PURE__ */ jsx("div", { className: styles.addition, children: addition }),
@@ -112,7 +112,7 @@
112
112
  text-decoration: none;
113
113
  }
114
114
  .productName svg {
115
- inline-size: var(--icon-size-md);
115
+ inline-size: auto;
116
116
  block-size: var(--icon-size-md);
117
117
  }
118
118
 
@@ -38,7 +38,7 @@
38
38
  display: flex;
39
39
  align-items: flex-start;
40
40
  padding: var(--spacing-lg) 0;
41
- padding-block-end: var(--spacing-lg);
41
+ padding-block-end: var(--spacing-md);
42
42
  gap: var(--spacing-md);
43
43
  min-width: 0;
44
44
  flex: 0 0 auto;
@@ -21,7 +21,8 @@ function SplitPane({
21
21
  direction = "horizontal",
22
22
  showDivider = "hover",
23
23
  gutterSize = 16,
24
- storageKey
24
+ storageKey,
25
+ fillViewport = false
25
26
  }) {
26
27
  return /* @__PURE__ */ jsxRuntime.jsx(
27
28
  SplitPaneContext.SplitPaneProvider,
@@ -31,14 +32,23 @@ function SplitPane({
31
32
  minPrimarySize,
32
33
  minSecondarySize,
33
34
  storageKey,
34
- children: /* @__PURE__ */ jsxRuntime.jsx(SplitPaneContainer, { showDivider, gutterSize, children })
35
+ children: /* @__PURE__ */ jsxRuntime.jsx(
36
+ SplitPaneContainer,
37
+ {
38
+ showDivider,
39
+ gutterSize,
40
+ fillViewport,
41
+ children
42
+ }
43
+ )
35
44
  }
36
45
  );
37
46
  }
38
47
  function SplitPaneContainer({
39
48
  children,
40
49
  showDivider,
41
- gutterSize
50
+ gutterSize,
51
+ fillViewport
42
52
  }) {
43
53
  const { direction, primarySize, containerRef } = SplitPaneContext.useSplitPaneContext();
44
54
  const style = react.useMemo(
@@ -55,6 +65,7 @@ function SplitPaneContainer({
55
65
  className: styles__default.default.container,
56
66
  "data-direction": direction,
57
67
  "data-divider": showDivider,
68
+ "data-fill-viewport": fillViewport,
58
69
  style,
59
70
  children
60
71
  }
@@ -9,8 +9,9 @@ export interface SplitPaneProps {
9
9
  showDivider?: 'hover' | 'always' | 'never';
10
10
  gutterSize?: number;
11
11
  storageKey?: string;
12
+ fillViewport?: boolean;
12
13
  }
13
- export declare function SplitPane({ children, initialPrimarySize, minPrimarySize, minSecondarySize, direction, showDivider, gutterSize, storageKey, }: SplitPaneProps): JSX.Element;
14
+ export declare function SplitPane({ children, initialPrimarySize, minPrimarySize, minSecondarySize, direction, showDivider, gutterSize, storageKey, fillViewport, }: SplitPaneProps): JSX.Element;
14
15
  export declare function SplitPanePrimary({ children }: {
15
16
  children: ReactNode;
16
17
  }): JSX.Element;
@@ -15,7 +15,8 @@ function SplitPane({
15
15
  direction = "horizontal",
16
16
  showDivider = "hover",
17
17
  gutterSize = 16,
18
- storageKey
18
+ storageKey,
19
+ fillViewport = false
19
20
  }) {
20
21
  return /* @__PURE__ */ jsx(
21
22
  SplitPaneProvider,
@@ -25,14 +26,23 @@ function SplitPane({
25
26
  minPrimarySize,
26
27
  minSecondarySize,
27
28
  storageKey,
28
- children: /* @__PURE__ */ jsx(SplitPaneContainer, { showDivider, gutterSize, children })
29
+ children: /* @__PURE__ */ jsx(
30
+ SplitPaneContainer,
31
+ {
32
+ showDivider,
33
+ gutterSize,
34
+ fillViewport,
35
+ children
36
+ }
37
+ )
29
38
  }
30
39
  );
31
40
  }
32
41
  function SplitPaneContainer({
33
42
  children,
34
43
  showDivider,
35
- gutterSize
44
+ gutterSize,
45
+ fillViewport
36
46
  }) {
37
47
  const { direction, primarySize, containerRef } = useSplitPaneContext();
38
48
  const style = useMemo(
@@ -49,6 +59,7 @@ function SplitPaneContainer({
49
59
  className: styles.container,
50
60
  "data-direction": direction,
51
61
  "data-divider": showDivider,
62
+ "data-fill-viewport": fillViewport,
52
63
  style,
53
64
  children
54
65
  }
@@ -6,6 +6,13 @@
6
6
  min-block-size: 0;
7
7
  }
8
8
 
9
+ .container[data-fill-viewport='true'] {
10
+ block-size: 100%;
11
+ max-block-size: 100%;
12
+ height: 100%;
13
+ max-height: 100%;
14
+ }
15
+
9
16
  .container[data-direction='horizontal'] {
10
17
  flex-direction: row;
11
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.104",
3
+ "version": "0.0.105",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",