@dbcdk/react-components 0.0.104 → 0.0.106

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,20 +13,26 @@ 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,
21
24
  mode = "single",
22
25
  size = "md",
23
26
  variant = "default",
27
+ renderMode = "lazy",
24
28
  defaultOpenIndex = null,
25
29
  defaultOpenIndexes = [],
26
30
  openIndex,
27
31
  openIndexes,
28
32
  onOpenIndexChange,
29
- onOpenIndexesChange
33
+ onOpenIndexesChange,
34
+ className,
35
+ ...rest
30
36
  }) {
31
37
  const uid = react.useId();
32
38
  const isControlled = mode === "single" ? openIndex !== void 0 : openIndexes !== void 0;
@@ -34,35 +40,40 @@ function Accordion({
34
40
  react.useEffect(() => {
35
41
  setHasMounted(true);
36
42
  }, []);
37
- const [internalSingle, setInternalSingle] = react.useState(
38
- mode === "single" ? defaultOpenIndex : null
39
- );
43
+ const [internalSingle, setInternalSingle] = react.useState(() => {
44
+ if (mode !== "single") return null;
45
+ if (defaultOpenIndex !== null && Number.isInteger(defaultOpenIndex) && defaultOpenIndex >= 0 && defaultOpenIndex < items.length) {
46
+ return defaultOpenIndex;
47
+ }
48
+ return null;
49
+ });
40
50
  const [internalMultiple, setInternalMultiple] = react.useState(() => {
41
51
  if (mode !== "multiple") return [];
42
- if (defaultOpenIndexes === "all") return items.map((_, i) => i);
43
- return normalizeMultiple(defaultOpenIndexes);
52
+ return normalizeMultiple(defaultOpenIndexes, items.length);
44
53
  });
45
54
  const currentOpenIndexes = react.useMemo(() => {
46
55
  if (mode === "single") {
47
56
  const current = isControlled ? openIndex != null ? openIndex : null : internalSingle;
48
- return current === null ? [] : [current];
57
+ return current !== null && Number.isInteger(current) && current >= 0 && current < items.length ? [current] : [];
49
58
  }
50
- return isControlled ? normalizeMultiple(openIndexes) : internalMultiple;
51
- }, [mode, isControlled, openIndex, openIndexes, internalSingle, internalMultiple]);
59
+ return isControlled ? normalizeMultiple(openIndexes, items.length) : normalizeMultiple(internalMultiple, items.length);
60
+ }, [mode, isControlled, openIndex, openIndexes, internalSingle, internalMultiple, items.length]);
52
61
  const openSet = react.useMemo(() => new Set(currentOpenIndexes), [currentOpenIndexes]);
53
62
  function commit(nextIndexes) {
54
63
  if (mode === "single") {
55
64
  const next = nextIndexes.length ? nextIndexes[0] : null;
56
- if (isControlled) onOpenIndexChange == null ? void 0 : onOpenIndexChange(next);
57
- else {
65
+ if (isControlled) {
66
+ onOpenIndexChange == null ? void 0 : onOpenIndexChange(next);
67
+ } else {
58
68
  setInternalSingle(next);
59
69
  onOpenIndexChange == null ? void 0 : onOpenIndexChange(next);
60
70
  }
61
71
  return;
62
72
  }
63
- const normalized = uniqSorted(nextIndexes);
64
- if (isControlled) onOpenIndexesChange == null ? void 0 : onOpenIndexesChange(normalized);
65
- else {
73
+ const normalized = normalizeMultiple(nextIndexes, items.length);
74
+ if (isControlled) {
75
+ onOpenIndexesChange == null ? void 0 : onOpenIndexesChange(normalized);
76
+ } else {
66
77
  setInternalMultiple(normalized);
67
78
  onOpenIndexesChange == null ? void 0 : onOpenIndexesChange(normalized);
68
79
  }
@@ -75,27 +86,40 @@ function Accordion({
75
86
  commit(isOpen ? [] : [index]);
76
87
  return;
77
88
  }
78
- if (isOpen) commit(currentOpenIndexes.filter((i) => i !== index));
79
- else commit([...currentOpenIndexes, index]);
89
+ if (isOpen) {
90
+ commit(currentOpenIndexes.filter((i) => i !== index));
91
+ } else {
92
+ commit([...currentOpenIndexes, index]);
93
+ }
80
94
  }
81
95
  return /* @__PURE__ */ jsxRuntime.jsx(
82
96
  "div",
83
97
  {
84
- 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
- ))
98
+ ...rest,
99
+ className: [
100
+ styles__default.default.container,
101
+ styles__default.default[size],
102
+ variant !== "default" ? styles__default.default[variant] : "",
103
+ className
104
+ ].filter(Boolean).join(" "),
105
+ children: items.map((item, i) => {
106
+ var _a;
107
+ return /* @__PURE__ */ jsxRuntime.jsx(
108
+ AccordionRow.AccordionRow,
109
+ {
110
+ uid,
111
+ index: i,
112
+ item,
113
+ isOpen: openSet.has(i),
114
+ onToggle: toggle,
115
+ shouldAnimate: hasMounted,
116
+ headlineSize: size === "sm" ? 4 : 3,
117
+ variant,
118
+ renderMode
119
+ },
120
+ (_a = item.id) != null ? _a : i
121
+ );
122
+ })
99
123
  }
100
124
  );
101
125
  }
@@ -1,9 +1,11 @@
1
- import type { JSX, ReactNode } from 'react';
1
+ import type { HTMLAttributes, JSX, ReactNode } from 'react';
2
2
  import type { Severity } from '../../constants/severity.types';
3
3
  export interface AccordionItem {
4
- header: string;
4
+ id?: string;
5
+ header: ReactNode;
5
6
  subheader?: ReactNode;
6
7
  headerAddition?: ReactNode;
8
+ headerActions?: ReactNode;
7
9
  headerIcon?: ReactNode;
8
10
  severity?: Severity;
9
11
  children: ReactNode;
@@ -12,20 +14,23 @@ export interface AccordionItem {
12
14
  type Size = 'sm' | 'md' | 'lg';
13
15
  type Mode = 'single' | 'multiple';
14
16
  type Variant = 'default' | 'outlined';
15
- export interface AccordionProps {
17
+ type OpenIndexes = number[] | 'all';
18
+ type RenderMode = 'lazy' | 'eager';
19
+ export interface AccordionProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
16
20
  items: AccordionItem[];
17
21
  mode?: Mode;
18
22
  size?: Size;
19
23
  variant?: Variant;
24
+ renderMode?: RenderMode;
20
25
  /** Uncontrolled defaults */
21
26
  defaultOpenIndex?: number | null;
22
- defaultOpenIndexes?: number[] | 'all';
27
+ defaultOpenIndexes?: OpenIndexes;
23
28
  /** Controlled state */
24
29
  openIndex?: number | null;
25
- openIndexes?: number[];
30
+ openIndexes?: OpenIndexes;
26
31
  /** Change callbacks */
27
32
  onOpenIndexChange?: (index: number | null) => void;
28
33
  onOpenIndexesChange?: (indexes: number[]) => void;
29
34
  }
30
- export declare function Accordion({ items, mode, size, variant, defaultOpenIndex, defaultOpenIndexes, openIndex, openIndexes, onOpenIndexChange, onOpenIndexesChange, }: AccordionProps): JSX.Element;
35
+ export declare function Accordion({ items, mode, size, variant, renderMode, defaultOpenIndex, defaultOpenIndexes, openIndex, openIndexes, onOpenIndexChange, onOpenIndexesChange, className, ...rest }: AccordionProps): JSX.Element;
31
36
  export {};
@@ -7,20 +7,26 @@ 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,
15
18
  mode = "single",
16
19
  size = "md",
17
20
  variant = "default",
21
+ renderMode = "lazy",
18
22
  defaultOpenIndex = null,
19
23
  defaultOpenIndexes = [],
20
24
  openIndex,
21
25
  openIndexes,
22
26
  onOpenIndexChange,
23
- onOpenIndexesChange
27
+ onOpenIndexesChange,
28
+ className,
29
+ ...rest
24
30
  }) {
25
31
  const uid = useId();
26
32
  const isControlled = mode === "single" ? openIndex !== void 0 : openIndexes !== void 0;
@@ -28,35 +34,40 @@ function Accordion({
28
34
  useEffect(() => {
29
35
  setHasMounted(true);
30
36
  }, []);
31
- const [internalSingle, setInternalSingle] = useState(
32
- mode === "single" ? defaultOpenIndex : null
33
- );
37
+ const [internalSingle, setInternalSingle] = useState(() => {
38
+ if (mode !== "single") return null;
39
+ if (defaultOpenIndex !== null && Number.isInteger(defaultOpenIndex) && defaultOpenIndex >= 0 && defaultOpenIndex < items.length) {
40
+ return defaultOpenIndex;
41
+ }
42
+ return null;
43
+ });
34
44
  const [internalMultiple, setInternalMultiple] = useState(() => {
35
45
  if (mode !== "multiple") return [];
36
- if (defaultOpenIndexes === "all") return items.map((_, i) => i);
37
- return normalizeMultiple(defaultOpenIndexes);
46
+ return normalizeMultiple(defaultOpenIndexes, items.length);
38
47
  });
39
48
  const currentOpenIndexes = useMemo(() => {
40
49
  if (mode === "single") {
41
50
  const current = isControlled ? openIndex != null ? openIndex : null : internalSingle;
42
- return current === null ? [] : [current];
51
+ return current !== null && Number.isInteger(current) && current >= 0 && current < items.length ? [current] : [];
43
52
  }
44
- return isControlled ? normalizeMultiple(openIndexes) : internalMultiple;
45
- }, [mode, isControlled, openIndex, openIndexes, internalSingle, internalMultiple]);
53
+ return isControlled ? normalizeMultiple(openIndexes, items.length) : normalizeMultiple(internalMultiple, items.length);
54
+ }, [mode, isControlled, openIndex, openIndexes, internalSingle, internalMultiple, items.length]);
46
55
  const openSet = useMemo(() => new Set(currentOpenIndexes), [currentOpenIndexes]);
47
56
  function commit(nextIndexes) {
48
57
  if (mode === "single") {
49
58
  const next = nextIndexes.length ? nextIndexes[0] : null;
50
- if (isControlled) onOpenIndexChange == null ? void 0 : onOpenIndexChange(next);
51
- else {
59
+ if (isControlled) {
60
+ onOpenIndexChange == null ? void 0 : onOpenIndexChange(next);
61
+ } else {
52
62
  setInternalSingle(next);
53
63
  onOpenIndexChange == null ? void 0 : onOpenIndexChange(next);
54
64
  }
55
65
  return;
56
66
  }
57
- const normalized = uniqSorted(nextIndexes);
58
- if (isControlled) onOpenIndexesChange == null ? void 0 : onOpenIndexesChange(normalized);
59
- else {
67
+ const normalized = normalizeMultiple(nextIndexes, items.length);
68
+ if (isControlled) {
69
+ onOpenIndexesChange == null ? void 0 : onOpenIndexesChange(normalized);
70
+ } else {
60
71
  setInternalMultiple(normalized);
61
72
  onOpenIndexesChange == null ? void 0 : onOpenIndexesChange(normalized);
62
73
  }
@@ -69,27 +80,40 @@ function Accordion({
69
80
  commit(isOpen ? [] : [index]);
70
81
  return;
71
82
  }
72
- if (isOpen) commit(currentOpenIndexes.filter((i) => i !== index));
73
- else commit([...currentOpenIndexes, index]);
83
+ if (isOpen) {
84
+ commit(currentOpenIndexes.filter((i) => i !== index));
85
+ } else {
86
+ commit([...currentOpenIndexes, index]);
87
+ }
74
88
  }
75
89
  return /* @__PURE__ */ jsx(
76
90
  "div",
77
91
  {
78
- 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
- ))
92
+ ...rest,
93
+ className: [
94
+ styles.container,
95
+ styles[size],
96
+ variant !== "default" ? styles[variant] : "",
97
+ className
98
+ ].filter(Boolean).join(" "),
99
+ children: items.map((item, i) => {
100
+ var _a;
101
+ return /* @__PURE__ */ jsx(
102
+ AccordionRow,
103
+ {
104
+ uid,
105
+ index: i,
106
+ item,
107
+ isOpen: openSet.has(i),
108
+ onToggle: toggle,
109
+ shouldAnimate: hasMounted,
110
+ headlineSize: size === "sm" ? 4 : 3,
111
+ variant,
112
+ renderMode
113
+ },
114
+ (_a = item.id) != null ? _a : i
115
+ );
116
+ })
93
117
  }
94
118
  );
95
119
  }
@@ -53,48 +53,62 @@ function AccordionRow({
53
53
  onToggle,
54
54
  shouldAnimate = true,
55
55
  headlineSize = 4,
56
- variant = "default"
56
+ variant = "default",
57
+ renderMode = "lazy"
57
58
  }) {
58
59
  const isDisabled = !!item.disabled;
60
+ const headlineWeight = isOpen ? variant === "outlined" ? 500 : 600 : variant === "outlined" ? 400 : 500;
59
61
  const buttonId = `${uid}-acc-btn-${index}`;
60
62
  const panelId = `${uid}-acc-panel-${index}`;
61
63
  const { innerRef, height, onTransitionEnd } = useCollapsibleHeight(isOpen, shouldAnimate);
64
+ const [hasBeenOpened, setHasBeenOpened] = react.useState(isOpen);
65
+ const shouldRenderChildren = renderMode === "eager" || isOpen || hasBeenOpened;
66
+ function handleToggle() {
67
+ if (!isOpen) {
68
+ setHasBeenOpened(true);
69
+ }
70
+ onToggle(index);
71
+ }
62
72
  return /* @__PURE__ */ jsxRuntime.jsxs(
63
73
  "section",
64
74
  {
65
75
  className: `${styles__default.default.item} ${isOpen ? styles__default.default.open : ""} ${isDisabled ? styles__default.default.disabled : ""}`,
76
+ "data-state": isOpen ? "open" : "closed",
66
77
  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
- ) }),
78
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.headerRow, children: [
79
+ /* @__PURE__ */ jsxRuntime.jsxs(
80
+ "button",
81
+ {
82
+ type: "button",
83
+ id: buttonId,
84
+ className: styles__default.default.trigger,
85
+ "aria-expanded": isOpen,
86
+ "aria-controls": panelId,
87
+ onClick: handleToggle,
88
+ disabled: isDisabled,
89
+ children: [
90
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.title, children: [
91
+ item.headerIcon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.icon, children: item.headerIcon }) : null,
92
+ /* @__PURE__ */ jsxRuntime.jsx(
93
+ Headline.Headline,
94
+ {
95
+ disableMargin: true,
96
+ size: headlineSize,
97
+ weight: headlineWeight,
98
+ severity: item.severity,
99
+ subheader: item.subheader,
100
+ allowWrap: isOpen,
101
+ children: item.header
102
+ }
103
+ ),
104
+ item.headerAddition ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.headerAddition, children: item.headerAddition }) : null
105
+ ] }),
106
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.chevron, "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, {}) })
107
+ ]
108
+ }
109
+ ),
110
+ item.headerActions ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.headerActions, onClick: (event) => event.stopPropagation(), children: item.headerActions }) : null
111
+ ] }),
98
112
  /* @__PURE__ */ jsxRuntime.jsx(
99
113
  "div",
100
114
  {
@@ -104,7 +118,7 @@ function AccordionRow({
104
118
  className: `${styles__default.default.panel} ${shouldAnimate ? styles__default.default.animate : styles__default.default.noAnimate}`,
105
119
  style: { height },
106
120
  onTransitionEnd,
107
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: innerRef, className: styles__default.default.content, children: item.children })
121
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: innerRef, className: styles__default.default.content, children: shouldRenderChildren ? item.children : null })
108
122
  }
109
123
  )
110
124
  ]
@@ -9,5 +9,6 @@ export type AccordionRowProps = {
9
9
  shouldAnimate?: boolean;
10
10
  headlineSize?: 1 | 2 | 3 | 4 | 5 | 6;
11
11
  variant?: 'default' | 'outlined';
12
+ renderMode?: 'lazy' | 'eager';
12
13
  };
13
- export declare function AccordionRow({ uid, index, item, isOpen, onToggle, shouldAnimate, headlineSize, variant, }: AccordionRowProps): JSX.Element;
14
+ export declare function AccordionRow({ uid, index, item, isOpen, onToggle, shouldAnimate, headlineSize, variant, renderMode, }: AccordionRowProps): JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { ChevronDown } from 'lucide-react';
3
- import { useRef, useState, useLayoutEffect } from 'react';
3
+ import { useState, useRef, useLayoutEffect } from 'react';
4
4
  import { Headline } from '../../../components/headline/Headline';
5
5
  import styles from './AccordionRow.module.css';
6
6
 
@@ -47,48 +47,62 @@ function AccordionRow({
47
47
  onToggle,
48
48
  shouldAnimate = true,
49
49
  headlineSize = 4,
50
- variant = "default"
50
+ variant = "default",
51
+ renderMode = "lazy"
51
52
  }) {
52
53
  const isDisabled = !!item.disabled;
54
+ const headlineWeight = isOpen ? variant === "outlined" ? 500 : 600 : variant === "outlined" ? 400 : 500;
53
55
  const buttonId = `${uid}-acc-btn-${index}`;
54
56
  const panelId = `${uid}-acc-panel-${index}`;
55
57
  const { innerRef, height, onTransitionEnd } = useCollapsibleHeight(isOpen, shouldAnimate);
58
+ const [hasBeenOpened, setHasBeenOpened] = useState(isOpen);
59
+ const shouldRenderChildren = renderMode === "eager" || isOpen || hasBeenOpened;
60
+ function handleToggle() {
61
+ if (!isOpen) {
62
+ setHasBeenOpened(true);
63
+ }
64
+ onToggle(index);
65
+ }
56
66
  return /* @__PURE__ */ jsxs(
57
67
  "section",
58
68
  {
59
69
  className: `${styles.item} ${isOpen ? styles.open : ""} ${isDisabled ? styles.disabled : ""}`,
70
+ "data-state": isOpen ? "open" : "closed",
60
71
  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
- ) }),
72
+ /* @__PURE__ */ jsxs("div", { className: styles.headerRow, children: [
73
+ /* @__PURE__ */ jsxs(
74
+ "button",
75
+ {
76
+ type: "button",
77
+ id: buttonId,
78
+ className: styles.trigger,
79
+ "aria-expanded": isOpen,
80
+ "aria-controls": panelId,
81
+ onClick: handleToggle,
82
+ disabled: isDisabled,
83
+ children: [
84
+ /* @__PURE__ */ jsxs("div", { className: styles.title, children: [
85
+ item.headerIcon ? /* @__PURE__ */ jsx("span", { className: styles.icon, children: item.headerIcon }) : null,
86
+ /* @__PURE__ */ jsx(
87
+ Headline,
88
+ {
89
+ disableMargin: true,
90
+ size: headlineSize,
91
+ weight: headlineWeight,
92
+ severity: item.severity,
93
+ subheader: item.subheader,
94
+ allowWrap: isOpen,
95
+ children: item.header
96
+ }
97
+ ),
98
+ item.headerAddition ? /* @__PURE__ */ jsx("div", { className: styles.headerAddition, children: item.headerAddition }) : null
99
+ ] }),
100
+ /* @__PURE__ */ jsx("span", { className: styles.chevron, "aria-hidden": "true", children: /* @__PURE__ */ jsx(ChevronDown, {}) })
101
+ ]
102
+ }
103
+ ),
104
+ item.headerActions ? /* @__PURE__ */ jsx("div", { className: styles.headerActions, onClick: (event) => event.stopPropagation(), children: item.headerActions }) : null
105
+ ] }),
92
106
  /* @__PURE__ */ jsx(
93
107
  "div",
94
108
  {
@@ -98,7 +112,7 @@ function AccordionRow({
98
112
  className: `${styles.panel} ${shouldAnimate ? styles.animate : styles.noAnimate}`,
99
113
  style: { height },
100
114
  onTransitionEnd,
101
- children: /* @__PURE__ */ jsx("div", { ref: innerRef, className: styles.content, children: item.children })
115
+ children: /* @__PURE__ */ jsx("div", { ref: innerRef, className: styles.content, children: shouldRenderChildren ? item.children : null })
102
116
  }
103
117
  )
104
118
  ]
@@ -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.106",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",