@dbcdk/react-components 0.0.89 → 0.0.90

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.
@@ -4,14 +4,16 @@ import styles from './Avatar.module.css';
4
4
  import { SeverityBgColor, SeverityTextColor } from '../../constants/severity';
5
5
  import { sizes } from '../../constants/sizes';
6
6
  export function Avatar({ image, imgSrc, imgAlt, fullName, color = 'brand', button, size = 'md', className, fullWidth = false, photographerCredit, ...rest }) {
7
- const text = fullName
8
- ? fullName
9
- .trim()
10
- .split(/\s+/)
11
- .map(name => name.charAt(0))
12
- .join('')
13
- .toUpperCase()
14
- : '';
7
+ const text = (() => {
8
+ if (!fullName)
9
+ return '';
10
+ const parts = fullName.trim().split(/\s+/);
11
+ if (parts.length === 1) {
12
+ return parts[0].charAt(0).toUpperCase();
13
+ }
14
+ // Use first letter of first and last part only
15
+ return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
16
+ })();
15
17
  const styleVars = {
16
18
  '--bg': SeverityBgColor[color],
17
19
  '--text': SeverityTextColor[color],
@@ -31,9 +33,13 @@ export function Avatar({ image, imgSrc, imgAlt, fullName, color = 'brand', butto
31
33
  if (imgSrc) {
32
34
  return _jsx("img", { className: styles.image, src: imgSrc, alt: imgAlt });
33
35
  }
34
- return (_jsx("span", { className: styles.avatar, "aria-hidden": "true", children: text }));
36
+ const avatarSizeClass = [styles.avatar];
37
+ if (size === 'xs')
38
+ avatarSizeClass.push(styles['size-xs']);
39
+ if (size === 'sm')
40
+ avatarSizeClass.push(styles['size-sm']);
41
+ return (_jsx("span", { className: avatarSizeClass.join(' '), "aria-hidden": "true", children: text }));
35
42
  };
36
- // Keep button behavior as before (no circular credit)
37
43
  if (button) {
38
44
  return (_jsx("button", { type: "button", ...rest, className: [styles.container, styles.button, className].filter(Boolean).join(' '), style: styleVars, children: renderImage() }));
39
45
  }
@@ -4,64 +4,85 @@
4
4
  max-inline-size: var(--component-size-xl);
5
5
  aspect-ratio: 1 / 1;
6
6
  border-radius: var(--border-radius-round);
7
- background-color: var(--bg);
8
- color: var(--text);
9
7
  display: inline-flex;
10
8
  align-items: center;
11
9
  justify-content: center;
12
- font-weight: var(--font-weight-semibold);
10
+ font-weight: var(--font-weight-normal);
13
11
  overflow: hidden;
14
12
  }
15
13
 
16
14
  .container.button {
17
15
  cursor: pointer;
18
16
  border: 0;
19
- background: var(--bg);
20
- transition: opacity var(--transition-fast) var(--ease-standard);
17
+ padding: 0;
18
+ background: transparent; /* important */
19
+ transition:
20
+ background-color var(--transition-fast) var(--ease-standard),
21
+ opacity var(--transition-fast) var(--ease-standard);
22
+ }
23
+
24
+ .container.button:hover .avatar {
25
+ background-color: color-mix(in srgb, var(--bg) 25%, var(--color-bg-surface));
21
26
  }
22
27
 
23
- .container.button:hover {
28
+ .container.button:active {
24
29
  opacity: 0.8;
25
30
  }
26
31
 
27
32
  .avatar {
28
- display: flex;
33
+ inline-size: 80%;
34
+ block-size: 80%;
35
+ display: inline-flex;
29
36
  align-items: center;
30
37
  justify-content: center;
38
+ border-radius: 50%;
39
+
40
+ /* moved here */
41
+ background-color: color-mix(in srgb, var(--bg) 20%, var(--color-bg-surface));
42
+ color: var(--bg);
43
+
31
44
  font-size: var(--font-size-sm);
32
45
  line-height: 1;
33
46
  text-transform: uppercase;
34
47
  }
35
48
 
49
+ .avatar.size-xs,
50
+ .avatar.size-sm {
51
+ font-size: var(--font-size-xs);
52
+ font-weight: var(--font-weight-normal);
53
+ }
54
+
36
55
  .image {
37
56
  inline-size: 100%;
38
57
  block-size: 100%;
39
58
  object-fit: cover;
40
59
  display: block;
60
+ border-radius: 50%; /* important if image fills avatar */
41
61
  }
42
62
 
43
63
  .imageSlot {
44
64
  display: contents;
45
65
  }
46
66
 
47
- /* Wrapper for non-button avatar so we can overlay SVG */
48
67
  .wrapper {
49
68
  position: relative;
50
69
  display: inline-block;
70
+ inline-size: var(--size);
71
+ block-size: var(--size);
72
+ max-inline-size: var(--component-size-xl);
73
+ aspect-ratio: 1 / 1;
51
74
  color: var(--text);
52
75
  }
53
76
 
54
- /* Circular photographer credit text overlay */
55
77
  .creditText {
56
78
  position: absolute;
57
79
  inset: 0;
58
- width: 100%;
59
- height: 100%;
80
+ inline-size: 100%;
81
+ block-size: 100%;
60
82
  pointer-events: none;
61
83
  fill: currentColor;
62
84
  }
63
85
 
64
- /* Make sure the font is large enough and clearly visible */
65
86
  .creditText text {
66
87
  font-size: 11px;
67
88
  font-weight: 600;
@@ -91,6 +91,7 @@
91
91
  /* Body */
92
92
  .body {
93
93
  min-width: 0;
94
+ flex: 1 1 auto;
94
95
  }
95
96
 
96
97
  /* Actions */
@@ -3,17 +3,19 @@ export type GridGap = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
3
3
  export interface GridProps {
4
4
  children?: ReactNode;
5
5
  gap?: GridGap;
6
+ /** Stretch direct children so cards/items in the same row get equal height */
7
+ equalHeight?: boolean;
6
8
  className?: string;
7
9
  style?: CSSProperties;
8
10
  }
9
- export declare function Grid({ children, gap, className, style }: GridProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function Grid({ children, gap, equalHeight, className, style }: GridProps): import("react/jsx-runtime").JSX.Element;
10
12
  export interface GridItemProps {
11
13
  children?: ReactNode;
12
- /** Default span (mobile-first, 1–12) */
14
+ /** Default span: <768px */
13
15
  base?: number;
14
- /** Override at ≥768px */
16
+ /** Medium span: ≥768px */
15
17
  md?: number;
16
- /** Override at1024px */
18
+ /** Large span:1200px */
17
19
  lg?: number;
18
20
  className?: string;
19
21
  style?: CSSProperties;
@@ -8,8 +8,10 @@ const GAP_TOKEN = {
8
8
  lg: 'var(--spacing-lg)',
9
9
  xl: 'var(--spacing-xl)',
10
10
  };
11
- export function Grid({ children, gap = 'md', className, style }) {
12
- return (_jsx("div", { className: [styles.grid, className].filter(Boolean).join(' '), style: { gap: GAP_TOKEN[gap], ...style }, children: children }));
11
+ export function Grid({ children, gap = 'md', equalHeight = false, className, style }) {
12
+ return (_jsx("div", { className: [styles.grid, equalHeight && styles.equalHeight, className]
13
+ .filter(Boolean)
14
+ .join(' '), style: { gap: GAP_TOKEN[gap], ...style }, children: children }));
13
15
  }
14
16
  export function GridItem({ children, base = 12, md, lg, className, style }) {
15
17
  return (_jsx("div", { className: [styles.item, className].filter(Boolean).join(' '), style: {
@@ -1,10 +1,25 @@
1
1
  .grid {
2
2
  display: grid;
3
3
  grid-template-columns: repeat(12, minmax(0, 1fr));
4
+ align-items: start;
5
+ }
6
+
7
+ .equalHeight {
8
+ align-items: stretch;
4
9
  }
5
10
 
6
11
  .item {
7
12
  grid-column: span var(--span-base, 12);
13
+ min-inline-size: 0;
14
+ }
15
+
16
+ .equalHeight .item {
17
+ display: flex;
18
+ }
19
+
20
+ .equalHeight .item > * {
21
+ flex: 1;
22
+ min-inline-size: 0;
8
23
  }
9
24
 
10
25
  @media (min-width: 768px) {
@@ -13,7 +28,7 @@
13
28
  }
14
29
  }
15
30
 
16
- @media (min-width: 1024px) {
31
+ @media (min-width: 1200px) {
17
32
  .item {
18
33
  grid-column: span var(--span-lg, var(--span-md, var(--span-base, 12)));
19
34
  }
@@ -0,0 +1,21 @@
1
+ import React, { PropsWithChildren } from 'react';
2
+ import type { HeadlineProps } from './Headline';
3
+ export interface CollapsibleHeadlineProps extends Omit<HeadlineProps, 'addition' | 'children'> {
4
+ /** The headline text — always visible. */
5
+ header: React.ReactNode;
6
+ /** Controlled expanded state. Required when `storageKey` is not set. */
7
+ expanded?: boolean;
8
+ /** Called when the toggle is clicked. */
9
+ onToggle?: () => void;
10
+ /** id of the panel element, used for aria-controls. Generated automatically if omitted. */
11
+ controls?: string;
12
+ /** Extra content rendered between the headline text and the toggle button. */
13
+ addition?: React.ReactNode;
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.
18
+ */
19
+ storageKey?: string;
20
+ }
21
+ export declare function CollapsibleHeadline({ header, expanded, onToggle, controls, addition, storageKey, children, size, variant, weight, ...headlineProps }: PropsWithChildren<CollapsibleHeadlineProps>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronDown } from 'lucide-react';
4
+ import { useId, useState } from 'react';
5
+ import { Headline } from './Headline';
6
+ import styles from './Headline.module.css';
7
+ import { Button } from '../button/Button';
8
+ export function CollapsibleHeadline({ header, expanded, onToggle, controls, addition, storageKey, children, size = 2, variant = 'muted', weight = 400, ...headlineProps }) {
9
+ const generatedId = useId();
10
+ const panelId = controls !== null && controls !== void 0 ? controls : generatedId;
11
+ const [internalExpanded, setInternalExpanded] = useState(() => {
12
+ if (!storageKey || typeof window === 'undefined')
13
+ return expanded !== null && expanded !== void 0 ? expanded : false;
14
+ const stored = localStorage.getItem(storageKey);
15
+ return stored !== null ? stored === 'true' : (expanded !== null && expanded !== void 0 ? expanded : false);
16
+ });
17
+ const isExpanded = storageKey ? internalExpanded : (expanded !== null && expanded !== void 0 ? expanded : false);
18
+ const handleToggle = () => {
19
+ if (storageKey) {
20
+ const next = !internalExpanded;
21
+ setInternalExpanded(next);
22
+ localStorage.setItem(storageKey, String(next));
23
+ }
24
+ onToggle === null || onToggle === void 0 ? void 0 : onToggle();
25
+ };
26
+ return (_jsxs("div", { className: styles.collapsibleRoot, children: [_jsxs(Headline, { ...headlineProps, variant: variant, weight: weight, size: size, addition: addition, children: [header, _jsx(Button, { shape: "round", type: "button", variant: "inline", "aria-expanded": isExpanded, "aria-controls": panelId, onClick: handleToggle, children: _jsx(ChevronDown, { "aria-hidden": true, className: [styles.toggleChevron, isExpanded ? styles.toggleChevronExpanded : '']
27
+ .filter(Boolean)
28
+ .join(' ') }) })] }), isExpanded && (_jsx("div", { id: panelId, className: styles.collapsiblePanel, children: children }))] }));
29
+ }
@@ -1,17 +1,19 @@
1
1
  import React, { PropsWithChildren } from 'react';
2
2
  import { Severity } from '../../constants/severity.types';
3
3
  type HeadlineTone = 'dark' | 'light';
4
- interface HeadlineProps extends React.AriaAttributes {
4
+ type HeadlineVariant = 'default' | 'muted';
5
+ export interface HeadlineProps extends React.AriaAttributes {
5
6
  size?: 1 | 2 | 3 | 4 | 5 | 6;
6
7
  marker?: boolean;
7
8
  disableMargin?: boolean;
8
9
  severity?: Severity;
9
- weight?: 500 | 600 | 700;
10
+ weight?: 400 | 500 | 600 | 700;
10
11
  subheader?: React.ReactNode;
11
12
  addition?: React.ReactNode;
12
13
  icon?: React.ReactNode;
13
14
  allowWrap?: boolean;
14
15
  tone?: HeadlineTone;
16
+ variant?: HeadlineVariant;
15
17
  }
16
- export declare function Headline({ size, marker, disableMargin, children, severity, weight, subheader, addition, icon, tone, allowWrap, }: PropsWithChildren<HeadlineProps>): React.JSX.Element;
18
+ export declare function Headline({ size, marker, disableMargin, children, severity, weight, subheader, addition, icon, tone, variant, allowWrap, }: PropsWithChildren<HeadlineProps>): React.JSX.Element;
17
19
  export {};
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import styles from './Headline.module.css';
4
4
  import { SeverityBgColor } from '../../constants/severity';
5
5
  import { Icon } from '../icon/Icon';
6
- export function Headline({ size = 2, marker, disableMargin, children, severity, weight = 600, subheader, addition, icon, tone, allowWrap = true, }) {
6
+ export function Headline({ size = 2, marker, disableMargin, children, severity, weight = 600, subheader, addition, icon, tone, variant, allowWrap = true, }) {
7
7
  const Tag = `h${size}`;
8
8
  const containerClassName = [styles.headlineContainer, tone ? styles[`tone-${tone}`] : '']
9
9
  .filter(Boolean)
@@ -12,6 +12,7 @@ export function Headline({ size = 2, marker, disableMargin, children, severity,
12
12
  styles.headline,
13
13
  disableMargin ? styles.noMargin : '',
14
14
  marker ? styles.marker : '',
15
+ variant === 'muted' ? styles['variant-muted'] : '',
15
16
  ]
16
17
  .filter(Boolean)
17
18
  .join(' ');
@@ -11,6 +11,7 @@
11
11
  flex-direction: column;
12
12
  flex: 1 1 auto;
13
13
  min-width: 0;
14
+ gap: var(--spacing-xxs);
14
15
  }
15
16
 
16
17
  .headlineRow {
@@ -29,7 +30,7 @@
29
30
  margin-block: var(--spacing-xs);
30
31
 
31
32
  /* Typography */
32
- font-weight: var(--font-weight-medium, 500);
33
+ font-weight: var(--font-weight, 600);
33
34
  letter-spacing: var(--letter-spacing-tight);
34
35
  line-height: var(--line-height-tight);
35
36
  color: inherit;
@@ -39,6 +40,56 @@
39
40
  min-width: 0; /* required for truncation inside flex */
40
41
  }
41
42
 
43
+ /* CollapsibleHeadline wrapper */
44
+ .collapsibleRoot {
45
+ display: flex;
46
+ flex-direction: column;
47
+ }
48
+
49
+ .collapsiblePanel {
50
+ display: contents;
51
+ }
52
+
53
+ /* CollapsibleHeadline toggle button */
54
+ .toggleButton {
55
+ all: unset;
56
+ box-sizing: border-box;
57
+ display: inline-flex;
58
+ align-items: center;
59
+ justify-content: center;
60
+ flex: 0 0 auto;
61
+ padding: var(--spacing-2xs);
62
+ border-radius: var(--border-radius-sm);
63
+ cursor: pointer;
64
+ color: var(--color-fg-subtle);
65
+ transition: color var(--transition-fast) var(--ease-standard);
66
+ }
67
+
68
+ .toggleButton:hover {
69
+ color: var(--color-fg-default);
70
+ }
71
+
72
+ .toggleButton:focus-visible {
73
+ outline: none;
74
+ box-shadow: var(--focus-ring);
75
+ }
76
+
77
+ .toggleChevron {
78
+ display: block;
79
+ width: var(--icon-size-md);
80
+ height: var(--icon-size-md);
81
+ transition: transform var(--transition-normal) var(--ease-standard);
82
+ }
83
+
84
+ .toggleChevronExpanded {
85
+ transform: rotate(180deg);
86
+ }
87
+
88
+ /* Variant overrides */
89
+ .variant-muted {
90
+ color: var(--color-fg-subtle);
91
+ }
92
+
42
93
  /* Tone overrides */
43
94
  .tone-dark .headline {
44
95
  color: var(--color-fg-default);
@@ -72,9 +123,9 @@
72
123
 
73
124
  /* Subheader */
74
125
  .subheader {
75
- color: var(--color-fg-muted);
76
- font-size: var(--font-size-md);
126
+ font-size: var(--font-size-sm);
77
127
  margin-block-start: calc(var(--spacing-2xs) * -1);
128
+ color: var(--color-fg-subtle);
78
129
  line-height: var(--line-height-normal);
79
130
  }
80
131
 
@@ -83,7 +134,6 @@
83
134
  flex: 1 1 auto;
84
135
  min-width: 0;
85
136
  align-items: center;
86
- justify-content: flex-end;
87
137
  }
88
138
 
89
139
  /* Icon */
@@ -109,4 +159,7 @@
109
159
  .wrap {
110
160
  white-space: normal;
111
161
  overflow: visible;
162
+ display: flex;
163
+ align-items: center;
164
+ gap: var(--spacing-xxs);
112
165
  }
@@ -219,7 +219,7 @@ export const Popover = forwardRef(function Popover({ trigger: Trigger, children,
219
219
  return;
220
220
  (_b = (_a = triggerElRef.current) === null || _a === void 0 ? void 0 : _a.focus) === null || _b === void 0 ? void 0 : _b.call(_a);
221
221
  }, [isOpen, returnFocus]);
222
- const icon = isOpen ? _jsx(ChevronUp, { size: 20 }) : _jsx(ChevronDown, { size: 20 });
222
+ const icon = isOpen ? (_jsx(ChevronUp, { className: "dbc-muted-text", size: 20 })) : (_jsx(ChevronDown, { className: "dbc-muted-text", size: 20 }));
223
223
  const setOverlayRef = React.useCallback((node) => {
224
224
  assignRef(overlayRef, node);
225
225
  }, [overlayRef]);
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export * from './components/icon/Icon';
9
9
  export * from './components/user-display/UserDisplay';
10
10
  export * from './components/tabs/Tabs';
11
11
  export * from './components/headline/Headline';
12
+ export * from './components/headline/CollapsibleHeadline';
12
13
  export * from './components/page-layout/PageLayout';
13
14
  export * from './components/page-layout/components/layout-footer/LayoutFooter';
14
15
  export * from './components/forms/input/Input';
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export * from './components/icon/Icon';
9
9
  export * from './components/user-display/UserDisplay';
10
10
  export * from './components/tabs/Tabs';
11
11
  export * from './components/headline/Headline';
12
+ export * from './components/headline/CollapsibleHeadline';
12
13
  export * from './components/page-layout/PageLayout';
13
14
  export * from './components/page-layout/components/layout-footer/LayoutFooter';
14
15
  export * from './components/forms/input/Input';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.89",
3
+ "version": "0.0.90",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",