@dbcdk/react-components 0.0.62 → 0.0.64
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/components/app-header/AppHeader.d.ts +3 -1
- package/dist/components/app-header/AppHeader.js +2 -2
- package/dist/components/app-header/AppHeader.module.css +9 -3
- package/dist/components/button/Button.js +17 -3
- package/dist/components/filter-field/FilterField.module.css +3 -15
- package/dist/components/forms/input/Input.module.css +18 -0
- package/dist/components/forms/typeahead/Typeahead.js +3 -4
- package/dist/components/hyperlink/Hyperlink.js +1 -0
- package/dist/components/menu/Menu.d.ts +4 -0
- package/dist/components/menu/Menu.js +50 -5
- package/dist/components/menu/Menu.module.css +18 -0
- package/dist/components/nav-bar/NavBar.d.ts +4 -1
- package/dist/components/nav-bar/NavBar.js +22 -12
- package/dist/components/nav-bar/NavBar.module.css +101 -1
- package/dist/components/overlay/modal/Modal.js +30 -5
- package/dist/components/page-layout/PageLayout.d.ts +4 -2
- package/dist/components/page-layout/PageLayout.js +12 -5
- package/dist/components/page-layout/PageLayout.module.css +46 -6
- package/dist/components/page-layout/components/layout-footer/LayoutFooter.d.ts +13 -0
- package/dist/components/page-layout/components/layout-footer/LayoutFooter.js +27 -0
- package/dist/components/page-layout/components/layout-footer/LayoutFooter.module.css +87 -0
- package/dist/components/page-layout/components/page-layout-hero/PageLayoutHero.d.ts +2 -1
- package/dist/components/page-layout/components/page-layout-hero/PageLayoutHero.js +9 -2
- package/dist/components/search-box/SearchBox.d.ts +3 -0
- package/dist/components/search-box/SearchBox.js +50 -9
- package/dist/components/search-box/SearchBox.module.css +11 -9
- package/dist/hooks/useDeviceSize.d.ts +2 -0
- package/dist/hooks/useDeviceSize.js +32 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/src/styles/styles.css +5 -0
- package/dist/styles/styles.css +5 -0
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ReactNode, JSX } from 'react';
|
|
2
|
+
export type AppHeaderSize = 'md' | 'lg';
|
|
2
3
|
interface AppHeaderProps {
|
|
3
4
|
children: ReactNode;
|
|
5
|
+
size?: AppHeaderSize;
|
|
4
6
|
}
|
|
5
|
-
export declare function AppHeader({ children }: AppHeaderProps): JSX.Element;
|
|
7
|
+
export declare function AppHeader({ children, size }: AppHeaderProps): JSX.Element;
|
|
6
8
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import styles from './AppHeader.module.css';
|
|
3
|
-
export function AppHeader({ children }) {
|
|
4
|
-
return (_jsx("header", { children: _jsx("div", { className: styles.container, children: children }) }));
|
|
3
|
+
export function AppHeader({ children, size = 'md' }) {
|
|
4
|
+
return (_jsx("header", { className: styles.header, children: _jsx("div", { className: styles.container, "data-size": size, children: children }) }));
|
|
5
5
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
.header {
|
|
2
|
+
inline-size: 100%;
|
|
3
|
+
background-color: var(--color-bg-surface);
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
.container {
|
|
2
7
|
/* layout */
|
|
3
8
|
display: flex;
|
|
@@ -11,13 +16,14 @@
|
|
|
11
16
|
box-sizing: border-box;
|
|
12
17
|
|
|
13
18
|
/* chrome */
|
|
14
|
-
background-color: var(--color-bg-surface);
|
|
15
19
|
color: var(--color-fg-default);
|
|
16
|
-
border-block-end: var(--border-width-thin) solid var(--color-border-default);
|
|
17
20
|
|
|
18
21
|
/* density-aware vertical rhythm */
|
|
19
22
|
padding-block: calc(var(--control-padding-y) + var(--density));
|
|
20
|
-
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.container[data-size='lg'] {
|
|
26
|
+
min-block-size: 80px;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
/* Optional content wrapper */
|
|
@@ -20,9 +20,14 @@ function mergeRefs(...refs) {
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
export const Button = React.forwardRef(function Button({ variant = 'outlined', shape = 'default', size = 'md', fullWidth, icon, children, loading, active, spinIcon, tooltip, tooltipPlacement = 'top', isLink, type = 'button', ...rest }, ref) {
|
|
23
|
+
var _a;
|
|
23
24
|
const { className: userClassName, ...buttonProps } = rest;
|
|
24
25
|
const computedClassName = cx(styles.button, styles[variant], styles[size], fullWidth ? styles.fullWidth : '', active ? styles.active : '', loading ? styles.loading : '', shape !== 'default' ? styles[shape] : '', userClassName);
|
|
25
26
|
const tooltipEnabled = Boolean(tooltip);
|
|
27
|
+
const childRef = isLink && React.isValidElement(children)
|
|
28
|
+
? ((_a = children.ref) !== null && _a !== void 0 ? _a : null)
|
|
29
|
+
: null;
|
|
30
|
+
const mergedRef = React.useMemo(() => mergeRefs(childRef, ref), [childRef, ref]);
|
|
26
31
|
// Tooltip anchored to the actual clickable element (button or link element)
|
|
27
32
|
const { triggerProps, id: tooltipId } = useTooltipTrigger({
|
|
28
33
|
content: tooltipEnabled ? tooltip : null,
|
|
@@ -42,13 +47,22 @@ export const Button = React.forwardRef(function Button({ variant = 'outlined', s
|
|
|
42
47
|
if (isLink && React.isValidElement(children)) {
|
|
43
48
|
// If this is a link-style button, we need to attach tooltip handlers + ref to the child.
|
|
44
49
|
const childClassName = typeof children.props.className === 'string' ? children.props.className : '';
|
|
45
|
-
const
|
|
50
|
+
const { disabled, onClick, ...linkButtonProps } = buttonProps;
|
|
51
|
+
const handleClick = e => {
|
|
52
|
+
if (disabled) {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
onClick === null || onClick === void 0 ? void 0 : onClick(e);
|
|
57
|
+
};
|
|
46
58
|
buttonEl = React.cloneElement(children, {
|
|
47
|
-
...
|
|
48
|
-
ref:
|
|
59
|
+
...linkButtonProps,
|
|
60
|
+
ref: mergedRef,
|
|
49
61
|
className: cx(childClassName, computedClassName, styles.buttonLink),
|
|
50
62
|
...(tooltipEnabled ? triggerProps : {}),
|
|
51
63
|
'aria-describedby': describedBy,
|
|
64
|
+
'aria-disabled': disabled ? 'true' : undefined,
|
|
65
|
+
onClick: handleClick,
|
|
52
66
|
children: (_jsxs(_Fragment, { children: [icon && _jsx("span", { className: cx(styles.icon, spinIcon ? 'spin' : ''), children: icon }), children.props.children, loading && (_jsx("span", { style: { display: 'flex', opacity: 0.5 }, className: "spin", children: _jsx(LoaderCircle, {}) }))] })),
|
|
53
67
|
});
|
|
54
68
|
}
|
|
@@ -37,11 +37,7 @@
|
|
|
37
37
|
background: color-mix(in srgb, var(--color-bg-selected) 45%, var(--color-bg-surface));
|
|
38
38
|
border-color: var(--color-border-selected);
|
|
39
39
|
box-shadow: var(--shadow-sm);
|
|
40
|
-
--filter-operator-bg: color-mix(
|
|
41
|
-
in srgb,
|
|
42
|
-
var(--color-bg-selected) 45%,
|
|
43
|
-
var(--color-bg-surface)
|
|
44
|
-
);
|
|
40
|
+
--filter-operator-bg: color-mix(in srgb, var(--color-bg-selected) 45%, var(--color-bg-surface));
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
.filterField.outlined {
|
|
@@ -59,11 +55,7 @@
|
|
|
59
55
|
background: color-mix(in srgb, var(--color-bg-selected) 38%, var(--color-bg-surface));
|
|
60
56
|
border-color: var(--color-border-selected);
|
|
61
57
|
box-shadow: none;
|
|
62
|
-
--filter-operator-bg: color-mix(
|
|
63
|
-
in srgb,
|
|
64
|
-
var(--color-bg-selected) 38%,
|
|
65
|
-
var(--color-bg-surface)
|
|
66
|
-
);
|
|
58
|
+
--filter-operator-bg: color-mix(in srgb, var(--color-bg-selected) 38%, var(--color-bg-surface));
|
|
67
59
|
}
|
|
68
60
|
|
|
69
61
|
.filterField.subtle {
|
|
@@ -83,11 +75,7 @@
|
|
|
83
75
|
}
|
|
84
76
|
|
|
85
77
|
.filterField.subtle.active {
|
|
86
|
-
background: color-mix(
|
|
87
|
-
in srgb,
|
|
88
|
-
var(--color-bg-selected) 55%,
|
|
89
|
-
var(--color-bg-surface-strong)
|
|
90
|
-
);
|
|
78
|
+
background: color-mix(in srgb, var(--color-bg-selected) 55%, var(--color-bg-surface-strong));
|
|
91
79
|
border-color: var(--color-border-selected);
|
|
92
80
|
box-shadow: inset 0 0 0 1px transparent;
|
|
93
81
|
--filter-operator-bg: color-mix(
|
|
@@ -168,6 +168,10 @@
|
|
|
168
168
|
box-shadow: var(--shadow-xs), var(--shadow-md);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
.standalone .input {
|
|
172
|
+
padding-inline: var(--spacing-md);
|
|
173
|
+
}
|
|
174
|
+
|
|
171
175
|
.standalone:hover:not([aria-disabled='true']) {
|
|
172
176
|
border-color: var(--color-border-strong);
|
|
173
177
|
box-shadow: var(--shadow-sm), var(--shadow-md);
|
|
@@ -393,6 +397,20 @@
|
|
|
393
397
|
z-index: 2;
|
|
394
398
|
}
|
|
395
399
|
|
|
400
|
+
/* Standalone variant: pill-shaped trailing button to match the field */
|
|
401
|
+
.withButton:has(.standalone) .trailingButton {
|
|
402
|
+
border-top-right-radius: var(--border-radius-rounded);
|
|
403
|
+
border-bottom-right-radius: var(--border-radius-rounded);
|
|
404
|
+
border-left-color: var(--color-border-default);
|
|
405
|
+
background-color: var(--color-bg-surface);
|
|
406
|
+
box-shadow: var(--shadow-xs), var(--shadow-md);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.withButton:has(.standalone) .trailingButton:hover {
|
|
410
|
+
border-color: var(--color-border-strong);
|
|
411
|
+
box-shadow: var(--shadow-sm), var(--shadow-md);
|
|
412
|
+
}
|
|
413
|
+
|
|
396
414
|
/* Date/time picker indicator (WebKit) */
|
|
397
415
|
.input[type='datetime-local']::-webkit-calendar-picker-indicator {
|
|
398
416
|
filter: invert(0.7);
|
|
@@ -294,10 +294,9 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
|
|
|
294
294
|
(_c = focusables[nextIndex]) === null || _c === void 0 ? void 0 : _c.focus();
|
|
295
295
|
return;
|
|
296
296
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
297
|
+
// single mode: Tab always closes the dropdown and lets focus move naturally
|
|
298
|
+
setOpen(false);
|
|
299
|
+
setActiveIndex(-1);
|
|
301
300
|
}, [open, getFocusableElements, mode]);
|
|
302
301
|
const commitSelection = (option) => {
|
|
303
302
|
var _a, _b;
|
|
@@ -7,6 +7,8 @@ export interface MenuProps extends React.HTMLAttributes<HTMLUListElement> {
|
|
|
7
7
|
* - for Select/listbox usage, pass itemRole="option" and role="listbox" on Menu.
|
|
8
8
|
*/
|
|
9
9
|
itemRole?: 'menuitem' | 'option';
|
|
10
|
+
/** Adds a gap of --spacing-xs between items */
|
|
11
|
+
gap?: boolean;
|
|
10
12
|
}
|
|
11
13
|
export type MenuSeparatorProps = React.LiHTMLAttributes<HTMLLIElement>;
|
|
12
14
|
export interface MenuItemProps extends React.LiHTMLAttributes<HTMLLIElement> {
|
|
@@ -19,6 +21,8 @@ export interface MenuItemProps extends React.LiHTMLAttributes<HTMLLIElement> {
|
|
|
19
21
|
* If not set, Menu's `itemRole` is used.
|
|
20
22
|
*/
|
|
21
23
|
itemRole?: 'menuitem' | 'option';
|
|
24
|
+
/** Adds a rounded border around the item */
|
|
25
|
+
variant?: 'default' | 'bordered';
|
|
22
26
|
}
|
|
23
27
|
export interface MenuCheckItemProps extends Omit<React.LiHTMLAttributes<HTMLLIElement>, 'onChange'> {
|
|
24
28
|
label: React.ReactNode;
|
|
@@ -4,13 +4,54 @@ import * as React from 'react';
|
|
|
4
4
|
import styles from './Menu.module.css';
|
|
5
5
|
import { Checkbox } from '../forms/checkbox/Checkbox';
|
|
6
6
|
import { RadioButton } from '../forms/radio-buttons/RadioButton';
|
|
7
|
-
const
|
|
7
|
+
const INTERACTIVE_SELECTOR = 'a:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"]):not([aria-disabled="true"]), [role="menuitem"]:not([aria-disabled="true"]), [role="option"]:not([aria-disabled="true"])';
|
|
8
|
+
function getMenuItems(el) {
|
|
9
|
+
return Array.from(el.querySelectorAll(INTERACTIVE_SELECTOR));
|
|
10
|
+
}
|
|
11
|
+
const MenuBase = React.forwardRef(({ children, className, itemRole = 'menuitem', gap, onKeyDown, ...props }, ref) => {
|
|
12
|
+
const internalRef = React.useRef(null);
|
|
13
|
+
const handleKeyDown = (e) => {
|
|
14
|
+
const ul = internalRef.current;
|
|
15
|
+
if (!ul)
|
|
16
|
+
return;
|
|
17
|
+
const items = getMenuItems(ul);
|
|
18
|
+
if (items.length === 0)
|
|
19
|
+
return;
|
|
20
|
+
const focused = document.activeElement;
|
|
21
|
+
const currentIndex = items.indexOf(focused);
|
|
22
|
+
let nextIndex = null;
|
|
23
|
+
if (e.key === 'ArrowDown') {
|
|
24
|
+
nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
25
|
+
}
|
|
26
|
+
else if (e.key === 'ArrowUp') {
|
|
27
|
+
nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
28
|
+
}
|
|
29
|
+
else if (e.key === 'Home') {
|
|
30
|
+
nextIndex = 0;
|
|
31
|
+
}
|
|
32
|
+
else if (e.key === 'End') {
|
|
33
|
+
nextIndex = items.length - 1;
|
|
34
|
+
}
|
|
35
|
+
if (nextIndex !== null) {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
items[nextIndex].focus();
|
|
38
|
+
}
|
|
39
|
+
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(e);
|
|
40
|
+
};
|
|
41
|
+
return (_jsx("ul", { ref: node => {
|
|
42
|
+
internalRef.current = node;
|
|
43
|
+
if (typeof ref === 'function')
|
|
44
|
+
ref(node);
|
|
45
|
+
else if (ref)
|
|
46
|
+
ref.current = node;
|
|
47
|
+
}, role: "menu", "data-itemrole": itemRole, className: [styles.container, gap ? styles.gap : '', className].filter(Boolean).join(' '), onKeyDown: handleKeyDown, ...props, children: children }));
|
|
48
|
+
});
|
|
8
49
|
MenuBase.displayName = 'Menu';
|
|
9
50
|
const isInteractiveEl = (el) => React.isValidElement(el) &&
|
|
10
51
|
(typeof el.type === 'string' ? el.type === 'a' || el.type === 'button' : true);
|
|
11
52
|
function applyMenuItemPropsToElement(child, opts) {
|
|
12
53
|
var _a, _b, _c, _d;
|
|
13
|
-
const { active, selected, disabled, role, tabIndex =
|
|
54
|
+
const { active, selected, disabled, role, tabIndex = 0, className } = opts;
|
|
14
55
|
const childClass = [styles.item, active ? styles.active : '', selected ? styles.selected : '']
|
|
15
56
|
.filter(Boolean)
|
|
16
57
|
.join(' ');
|
|
@@ -44,16 +85,20 @@ function applyMenuItemPropsToElement(child, opts) {
|
|
|
44
85
|
disabled,
|
|
45
86
|
});
|
|
46
87
|
}
|
|
47
|
-
const MenuItem = React.forwardRef(({ children, active, selected, disabled, className, itemRole, ...liProps }, ref) => {
|
|
88
|
+
const MenuItem = React.forwardRef(({ children, active, selected, disabled, className, itemRole, variant, ...liProps }, ref) => {
|
|
48
89
|
// If caller sets itemRole prop, use it; otherwise attempt to inherit from parent Menu via data attr.
|
|
49
90
|
// (We can’t reliably read parent props here without context; simplest is: caller passes itemRole on Menu.Item when needed.)
|
|
50
91
|
const resolvedRole = itemRole !== null && itemRole !== void 0 ? itemRole : 'menuitem';
|
|
92
|
+
const isBordered = variant === 'bordered';
|
|
93
|
+
const rowClass = [styles.row, isBordered ? styles.rowBordered : '', className]
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.join(' ');
|
|
51
96
|
if (isInteractiveEl(children)) {
|
|
52
97
|
const child = children;
|
|
53
|
-
return (_jsx("li", { ref: ref, role: "none", className:
|
|
98
|
+
return (_jsx("li", { ref: ref, role: "none", className: rowClass, ...liProps, children: applyMenuItemPropsToElement(child, { active, selected, disabled, role: resolvedRole }) }));
|
|
54
99
|
}
|
|
55
100
|
// Fallback: wrap non-interactive children in a <button>
|
|
56
|
-
return (_jsx("li", { ref: ref, role: "none", className:
|
|
101
|
+
return (_jsx("li", { ref: ref, role: "none", className: rowClass, ...liProps, children: _jsx("button", { role: resolvedRole, tabIndex: 0, "aria-selected": selected || undefined, "aria-disabled": disabled || undefined, className: [
|
|
57
102
|
styles.interactive,
|
|
58
103
|
styles.item,
|
|
59
104
|
active ? styles.active : '',
|
|
@@ -173,3 +173,21 @@
|
|
|
173
173
|
opacity: 0.8;
|
|
174
174
|
border-radius: 999px;
|
|
175
175
|
}
|
|
176
|
+
|
|
177
|
+
/* Gap between items */
|
|
178
|
+
.gap {
|
|
179
|
+
gap: var(--spacing-xs);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Bordered item variant */
|
|
183
|
+
.rowBordered {
|
|
184
|
+
border: 1px solid var(--color-border-default);
|
|
185
|
+
border-radius: var(--border-radius-default);
|
|
186
|
+
overflow: hidden;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Inside a bordered row, remove inner border-radius so hover bg fills the full area */
|
|
190
|
+
.rowBordered .interactive,
|
|
191
|
+
.rowBordered .interactiveChild {
|
|
192
|
+
border-radius: 0;
|
|
193
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ElementType, ReactNode, JSX } from 'react';
|
|
2
|
+
import { type AppHeaderSize } from '../app-header/AppHeader';
|
|
2
3
|
export type NavBarItem = NavBarLinkItem | NavBarExpandableItem | NavBarGroupItem;
|
|
3
4
|
type NavBarBase = {
|
|
4
5
|
label: string;
|
|
@@ -33,6 +34,8 @@ interface NavBarProps {
|
|
|
33
34
|
items: NavBarLinkItem[];
|
|
34
35
|
productName?: string;
|
|
35
36
|
addition?: ReactNode;
|
|
37
|
+
activeLink?: string;
|
|
38
|
+
size?: AppHeaderSize;
|
|
36
39
|
}
|
|
37
|
-
export declare function NavBar({ logo, items, productName, addition }: NavBarProps): JSX.Element;
|
|
40
|
+
export declare function NavBar({ logo, items, productName, addition, activeLink, size, }: NavBarProps): JSX.Element;
|
|
38
41
|
export {};
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Menu as MenuIcon, X } from 'lucide-react';
|
|
4
|
+
import { useRef, useState } from 'react';
|
|
3
5
|
import styles from './NavBar.module.css';
|
|
4
6
|
import { Logo } from '../../assets/logo';
|
|
7
|
+
import { useDeviceSize } from '../../hooks/useDeviceSize';
|
|
5
8
|
import { AppHeader } from '../app-header/AppHeader';
|
|
6
9
|
import { Headline } from '../headline/Headline';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
import { Popover } from '../popover/Popover';
|
|
11
|
+
export function NavBar({ logo, items, productName, addition, activeLink, size, }) {
|
|
12
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
13
|
+
const deviceSize = useDeviceSize();
|
|
14
|
+
const isMobile = deviceSize === 'mobile';
|
|
15
|
+
const navRef = useRef(null);
|
|
16
|
+
const navLinks = items === null || items === void 0 ? void 0 : items.filter(i => i.enabled !== false).map((item, id) => {
|
|
17
|
+
const { component: Component, label, icon, href, active, external } = item;
|
|
18
|
+
const isActive = activeLink ? href === activeLink : Boolean(active);
|
|
19
|
+
const linkClass = [styles.link, isActive ? styles.active : ''].filter(Boolean).join(' ');
|
|
20
|
+
const commonProps = {
|
|
21
|
+
className: linkClass,
|
|
22
|
+
href,
|
|
23
|
+
...(isActive ? { 'aria-current': 'page' } : {}),
|
|
24
|
+
...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {}),
|
|
25
|
+
};
|
|
26
|
+
return (_jsx("li", { className: styles.navItem, role: "listitem", children: Component ? (_jsxs(Component, { ...commonProps, children: [icon, _jsx("span", { className: styles.label, children: label })] })) : (_jsxs("a", { ...commonProps, children: [icon, _jsx("span", { className: styles.label, children: label })] })) }, id));
|
|
27
|
+
});
|
|
28
|
+
return (_jsx(AppHeader, { size: size, children: _jsxs("nav", { ref: navRef, className: styles.container, "aria-label": productName ? `${productName} navigation` : 'Main navigation', children: [(logo || productName) && (_jsxs("div", { className: styles.logoRow, children: [logo, productName && (_jsxs("span", { className: styles.productName, children: [_jsx(Logo, {}), _jsx(Headline, { disableMargin: true, size: 1, children: productName })] }))] })), (navLinks === null || navLinks === void 0 ? void 0 : navLinks.length) > 0 && (_jsx("div", { className: styles.navContent, children: _jsx("ul", { className: styles.navItems, role: "list", children: navLinks }) })), addition && !isMobile && _jsx("div", { className: styles.addition, children: addition }), isMobile && (_jsx(Popover, { open: mobileOpen, onOpenChange: setMobileOpen, matchTriggerWidth: true, fullWidth: false, autoFocusContent: true, anchorRef: navRef, trigger: (toggle, _icon, open) => (_jsx("button", { type: "button", className: styles.burgerButton, "aria-label": open ? 'Close navigation menu' : 'Open navigation menu', "aria-expanded": open, onClick: toggle, children: open ? _jsx(X, { size: 20 }) : _jsx(MenuIcon, { size: 20 }) })), children: close => (_jsxs("div", { className: styles.mobileMenu, children: [_jsx("ul", { className: styles.mobileNavItems, role: "list", onClick: close, children: navLinks }), addition && _jsx("div", { className: styles.mobileAddition, children: addition })] })) }))] }) }));
|
|
19
29
|
}
|
|
@@ -7,6 +7,99 @@
|
|
|
7
7
|
flex-grow: 1;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
.logoRow {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
gap: var(--spacing-xs);
|
|
14
|
+
flex-shrink: 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.navContent {
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: var(--spacing-md);
|
|
21
|
+
min-inline-size: 0;
|
|
22
|
+
flex: 1 1 auto;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Burger: hidden on desktop, visible on mobile */
|
|
26
|
+
.burger {
|
|
27
|
+
display: none;
|
|
28
|
+
margin-inline-start: auto;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.burgerButton {
|
|
32
|
+
display: inline-flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
inline-size: var(--component-size-md);
|
|
36
|
+
block-size: var(--component-size-md);
|
|
37
|
+
border: none;
|
|
38
|
+
background: transparent;
|
|
39
|
+
color: var(--color-fg-default);
|
|
40
|
+
border-radius: var(--border-radius-default);
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
padding: 0;
|
|
43
|
+
transition: background-color var(--transition-fast) var(--ease-standard);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.burgerButton:hover {
|
|
47
|
+
background-color: var(--color-bg-hover-subtle);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.burgerButton:focus-visible {
|
|
51
|
+
outline: none;
|
|
52
|
+
box-shadow: var(--focus-ring);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Mobile dropdown content */
|
|
56
|
+
.mobileMenu {
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
gap: var(--spacing-xs);
|
|
60
|
+
padding: var(--spacing-xs);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.mobileNavItems {
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
list-style: none;
|
|
67
|
+
margin: 0;
|
|
68
|
+
padding: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.mobileAddition {
|
|
72
|
+
border-top: var(--border-width-thin) solid var(--color-border-subtle);
|
|
73
|
+
padding-top: var(--spacing-xs);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@media (max-width: 640px) {
|
|
77
|
+
.container {
|
|
78
|
+
flex-wrap: nowrap;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.logoRow {
|
|
83
|
+
flex: 1 1 auto;
|
|
84
|
+
min-inline-size: 0;
|
|
85
|
+
overflow: hidden;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.navContent {
|
|
89
|
+
display: none;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.addition {
|
|
93
|
+
display: none;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.burger {
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-shrink: 0;
|
|
99
|
+
margin-inline-start: 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
10
103
|
.productName {
|
|
11
104
|
display: inline-flex;
|
|
12
105
|
align-items: center;
|
|
@@ -23,13 +116,20 @@
|
|
|
23
116
|
display: flex;
|
|
24
117
|
align-items: center;
|
|
25
118
|
gap: var(--spacing-lg);
|
|
26
|
-
inline-size: 100%;
|
|
27
119
|
min-inline-size: 0;
|
|
28
120
|
list-style: none;
|
|
29
121
|
margin: 0;
|
|
30
122
|
padding: 0;
|
|
31
123
|
}
|
|
32
124
|
|
|
125
|
+
.addition {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
flex: 1 1 auto;
|
|
129
|
+
min-inline-size: 0;
|
|
130
|
+
margin-inline-start: auto;
|
|
131
|
+
}
|
|
132
|
+
|
|
33
133
|
.navItem {
|
|
34
134
|
list-style: none;
|
|
35
135
|
min-inline-size: 0;
|
|
@@ -19,6 +19,15 @@ export function Modal({ isOpen, onRequestClose, header, content, children, prima
|
|
|
19
19
|
useEffect(() => {
|
|
20
20
|
setMounted(true);
|
|
21
21
|
}, []);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!isOpen)
|
|
24
|
+
return;
|
|
25
|
+
const previous = document.body.style.overflow;
|
|
26
|
+
document.body.style.overflow = 'hidden';
|
|
27
|
+
return () => {
|
|
28
|
+
document.body.style.overflow = previous;
|
|
29
|
+
};
|
|
30
|
+
}, [isOpen]);
|
|
22
31
|
// Track open transition so we only autofocus once per open
|
|
23
32
|
const wasOpenRef = useRef(false);
|
|
24
33
|
useEffect(() => {
|
|
@@ -32,7 +41,7 @@ export function Modal({ isOpen, onRequestClose, header, content, children, prima
|
|
|
32
41
|
lastActiveElementRef.current = document.activeElement;
|
|
33
42
|
const dialog = dialogRef.current;
|
|
34
43
|
if (dialog) {
|
|
35
|
-
const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
44
|
+
const focusableSelectors = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
36
45
|
const focusable = dialog.querySelectorAll(focusableSelectors);
|
|
37
46
|
if (focusable.length > 0) {
|
|
38
47
|
const preferred = (_a = dialog.querySelector('input, select, textarea')) !== null && _a !== void 0 ? _a : focusable[0];
|
|
@@ -43,35 +52,51 @@ export function Modal({ isOpen, onRequestClose, header, content, children, prima
|
|
|
43
52
|
}
|
|
44
53
|
}
|
|
45
54
|
}
|
|
55
|
+
const focusableSelectors = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
56
|
+
const getFocusable = () => { var _a, _b; return (_b = (_a = dialogRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll(focusableSelectors)) !== null && _b !== void 0 ? _b : []; };
|
|
46
57
|
const handleKeyDown = (event) => {
|
|
47
58
|
if (event.key === 'Escape') {
|
|
48
59
|
onRequestCloseRef.current();
|
|
49
60
|
return;
|
|
50
61
|
}
|
|
51
62
|
if (event.key === 'Tab' && dialogRef.current) {
|
|
52
|
-
const
|
|
53
|
-
const focusable = dialogRef.current.querySelectorAll(focusableSelectors);
|
|
63
|
+
const focusable = getFocusable();
|
|
54
64
|
if (focusable.length === 0)
|
|
55
65
|
return;
|
|
56
66
|
const first = focusable[0];
|
|
57
67
|
const last = focusable[focusable.length - 1];
|
|
58
68
|
if (event.shiftKey) {
|
|
59
|
-
if (document.activeElement === first
|
|
69
|
+
if (document.activeElement === first ||
|
|
70
|
+
!dialogRef.current.contains(document.activeElement)) {
|
|
60
71
|
event.preventDefault();
|
|
61
72
|
last.focus();
|
|
62
73
|
}
|
|
63
74
|
}
|
|
64
75
|
else {
|
|
65
|
-
if (document.activeElement === last
|
|
76
|
+
if (document.activeElement === last ||
|
|
77
|
+
!dialogRef.current.contains(document.activeElement)) {
|
|
66
78
|
event.preventDefault();
|
|
67
79
|
first.focus();
|
|
68
80
|
}
|
|
69
81
|
}
|
|
70
82
|
}
|
|
71
83
|
};
|
|
84
|
+
// Redirect focus back into the dialog if it escapes (e.g. via mouse click on overlay)
|
|
85
|
+
const handleFocusIn = (event) => {
|
|
86
|
+
const dialog = dialogRef.current;
|
|
87
|
+
if (!dialog || dialog.contains(event.target))
|
|
88
|
+
return;
|
|
89
|
+
const focusable = getFocusable();
|
|
90
|
+
if (focusable.length > 0)
|
|
91
|
+
focusable[0].focus();
|
|
92
|
+
else
|
|
93
|
+
dialog.focus();
|
|
94
|
+
};
|
|
72
95
|
document.addEventListener('keydown', handleKeyDown);
|
|
96
|
+
document.addEventListener('focusin', handleFocusIn);
|
|
73
97
|
return () => {
|
|
74
98
|
document.removeEventListener('keydown', handleKeyDown);
|
|
99
|
+
document.removeEventListener('focusin', handleFocusIn);
|
|
75
100
|
if (lastActiveElementRef.current) {
|
|
76
101
|
lastActiveElementRef.current.focus();
|
|
77
102
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
|
2
2
|
import { type PageLayoutHeroProps } from './components/page-layout-hero/PageLayoutHero';
|
|
3
3
|
type Orientation = 'vertical' | 'horizontal';
|
|
4
|
+
export type PageLayoutMaxWidth = boolean | 'sm' | 'md';
|
|
4
5
|
export interface PageLayoutProps extends PropsWithChildren {
|
|
5
6
|
/**
|
|
6
7
|
* If true, PageLayout becomes a self-contained app shell (100vh) and
|
|
@@ -11,14 +12,15 @@ export interface PageLayoutProps extends PropsWithChildren {
|
|
|
11
12
|
orientation?: Orientation;
|
|
12
13
|
}
|
|
13
14
|
export interface PageLayoutHeaderProps {
|
|
14
|
-
maxWidth?:
|
|
15
|
+
maxWidth?: PageLayoutMaxWidth;
|
|
15
16
|
children: ReactNode;
|
|
16
17
|
}
|
|
17
18
|
export interface PageLayoutContentProps {
|
|
18
|
-
maxWidth?:
|
|
19
|
+
maxWidth?: PageLayoutMaxWidth;
|
|
19
20
|
children: ReactNode;
|
|
20
21
|
}
|
|
21
22
|
export interface PageLayoutFooterProps {
|
|
23
|
+
maxWidth?: PageLayoutMaxWidth;
|
|
22
24
|
children: ReactNode;
|
|
23
25
|
}
|
|
24
26
|
export interface PageLayoutSidebarProps {
|
|
@@ -2,6 +2,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { Children, isValidElement } from 'react';
|
|
3
3
|
import { PageLayoutHero, } from './components/page-layout-hero/PageLayoutHero';
|
|
4
4
|
import styles from './PageLayout.module.css';
|
|
5
|
+
function getMaxWidthClass(value, styles) {
|
|
6
|
+
if (!value)
|
|
7
|
+
return '';
|
|
8
|
+
if (value === 'sm')
|
|
9
|
+
return styles.maxWidthSm;
|
|
10
|
+
return styles.maxWidthMd;
|
|
11
|
+
}
|
|
5
12
|
function getSlotName(el) {
|
|
6
13
|
var _a;
|
|
7
14
|
const t = el.type;
|
|
@@ -34,15 +41,15 @@ const PageLayoutSidebar = ({ children, }) => {
|
|
|
34
41
|
};
|
|
35
42
|
PageLayoutSidebar.__PAGE_LAYOUT_SLOT__ = 'Sidebar';
|
|
36
43
|
const PageLayoutHeader = ({ maxWidth = false, children, }) => {
|
|
37
|
-
return (_jsx("div", { className: styles.headerContainer, children: _jsx("div", { className: `${styles.headerContent} ${maxWidth
|
|
44
|
+
return (_jsx("div", { className: styles.headerContainer, children: _jsx("div", { className: `${styles.headerContent} ${getMaxWidthClass(maxWidth, styles)}`, children: children }) }));
|
|
38
45
|
};
|
|
39
46
|
PageLayoutHeader.__PAGE_LAYOUT_SLOT__ = 'Header';
|
|
40
47
|
const PageLayoutContent = ({ maxWidth = false, children, }) => {
|
|
41
|
-
return (_jsx("div", { className: `${styles.contentInner} ${maxWidth
|
|
48
|
+
return (_jsx("div", { className: `${styles.contentInner} ${getMaxWidthClass(maxWidth, styles)}`, children: children }));
|
|
42
49
|
};
|
|
43
50
|
PageLayoutContent.__PAGE_LAYOUT_SLOT__ = 'Content';
|
|
44
|
-
const PageLayoutFooter = ({ children, }) => {
|
|
45
|
-
return _jsx(
|
|
51
|
+
const PageLayoutFooter = ({ maxWidth = false, children, }) => {
|
|
52
|
+
return (_jsx("div", { className: `${styles.footerContent} ${getMaxWidthClass(maxWidth, styles)}`, children: children }));
|
|
46
53
|
};
|
|
47
54
|
PageLayoutFooter.__PAGE_LAYOUT_SLOT__ = 'Footer';
|
|
48
55
|
PageLayoutHero.__PAGE_LAYOUT_SLOT__ = 'Hero';
|
|
@@ -50,7 +57,7 @@ const PageLayoutBase = ({ children, containScrolling = false, orientation = 'ver
|
|
|
50
57
|
var _a, _b;
|
|
51
58
|
const { slots, rest } = splitSlots(children);
|
|
52
59
|
// If no explicit <PageLayout.Content>, we’ll treat remaining children as content.
|
|
53
|
-
const content = (_a = slots.Content) !== null && _a !== void 0 ? _a : (rest.length ? _jsx(PageLayoutContent, { maxWidth:
|
|
60
|
+
const content = (_a = slots.Content) !== null && _a !== void 0 ? _a : (rest.length ? _jsx(PageLayoutContent, { maxWidth: "md", children: rest }) : undefined);
|
|
54
61
|
const rootClass = [
|
|
55
62
|
styles.root,
|
|
56
63
|
orientation === 'vertical' ? styles.vertical : styles.horizontal,
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
.documentScrolling {
|
|
20
|
-
min-height:
|
|
20
|
+
min-height: 100vh;
|
|
21
|
+
min-height: 100dvh;
|
|
21
22
|
overflow: visible;
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -71,6 +72,7 @@
|
|
|
71
72
|
.header {
|
|
72
73
|
min-width: 0;
|
|
73
74
|
background: var(--color-bg-surface);
|
|
75
|
+
border-bottom: var(--border-width-thin) solid var(--color-border-default);
|
|
74
76
|
flex: 0 0 auto;
|
|
75
77
|
}
|
|
76
78
|
|
|
@@ -115,16 +117,18 @@
|
|
|
115
117
|
flex: 1 1 auto; /* take remaining space inside mainScroll */
|
|
116
118
|
display: flex; /* lets contentInner stretch */
|
|
117
119
|
flex-direction: column;
|
|
120
|
+
align-items: center;
|
|
118
121
|
|
|
119
122
|
background: var(--color-bg-surface);
|
|
120
|
-
padding: var(--spacing-md);
|
|
123
|
+
padding: var(--spacing-lg) var(--spacing-md);
|
|
121
124
|
overflow: visible;
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
.footer {
|
|
125
128
|
min-width: 0;
|
|
126
|
-
background: var(--color-bg-surface);
|
|
127
|
-
|
|
129
|
+
background: var(--color-bg-surface-subtle);
|
|
130
|
+
display: flex;
|
|
131
|
+
justify-content: center;
|
|
128
132
|
|
|
129
133
|
/* When there is extra space (content is short), this pushes footer to the bottom.
|
|
130
134
|
When content is long, footer follows content normally and is reached by scrolling.
|
|
@@ -138,6 +142,7 @@
|
|
|
138
142
|
display: flex;
|
|
139
143
|
justify-content: center;
|
|
140
144
|
width: 100%;
|
|
145
|
+
padding-inline: var(--spacing-md);
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
.headerContent {
|
|
@@ -145,19 +150,54 @@
|
|
|
145
150
|
box-sizing: border-box;
|
|
146
151
|
}
|
|
147
152
|
|
|
148
|
-
.
|
|
153
|
+
.footerContent {
|
|
154
|
+
width: 100%;
|
|
155
|
+
box-sizing: border-box;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.maxWidthMd {
|
|
149
159
|
max-width: 1600px;
|
|
150
160
|
margin-inline: auto;
|
|
151
161
|
width: 100%;
|
|
152
162
|
box-sizing: border-box;
|
|
153
163
|
}
|
|
154
164
|
|
|
165
|
+
.maxWidthSm {
|
|
166
|
+
margin-inline: auto;
|
|
167
|
+
width: 100%;
|
|
168
|
+
box-sizing: border-box;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@media (min-width: 640px) {
|
|
172
|
+
.maxWidthSm {
|
|
173
|
+
max-width: 640px;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@media (min-width: 768px) {
|
|
178
|
+
.maxWidthSm {
|
|
179
|
+
max-width: 668px;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@media (min-width: 1024px) {
|
|
184
|
+
.maxWidthSm {
|
|
185
|
+
max-width: 924px;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@media (min-width: 1280px) {
|
|
190
|
+
.maxWidthSm {
|
|
191
|
+
max-width: 1180px;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
155
195
|
/* Content slot inner wrapper (so maxWidth works without interfering with scroll) */
|
|
156
196
|
.contentInner {
|
|
157
197
|
display: flex;
|
|
158
198
|
flex-direction: column;
|
|
159
199
|
gap: var(--spacing-xl);
|
|
160
|
-
|
|
200
|
+
width: 100%;
|
|
161
201
|
box-sizing: border-box;
|
|
162
202
|
min-width: 0;
|
|
163
203
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { JSX, ReactElement } from 'react';
|
|
2
|
+
export interface LayoutFooterLink {
|
|
3
|
+
label: string;
|
|
4
|
+
href: string;
|
|
5
|
+
external?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface LayoutFooterProps {
|
|
8
|
+
links?: LayoutFooterLink[];
|
|
9
|
+
metaParts?: string[];
|
|
10
|
+
/** Extra links rendered before the default links. Pass framework link elements (e.g. Next.js <Link>). */
|
|
11
|
+
extraLinks?: ReactElement[];
|
|
12
|
+
}
|
|
13
|
+
export declare function LayoutFooter({ links, metaParts, extraLinks, }: LayoutFooterProps): JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Logo } from '../../../../assets/logo';
|
|
4
|
+
import { Hyperlink } from '../../../../components/hyperlink/Hyperlink';
|
|
5
|
+
import styles from './LayoutFooter.module.css';
|
|
6
|
+
const DEFAULT_META_PARTS = [
|
|
7
|
+
'Tempovej 7-11',
|
|
8
|
+
'DK-2750 Ballerup',
|
|
9
|
+
'+45 44 86 77 11',
|
|
10
|
+
`© ${new Date().getFullYear()} DBC DIGITAL A/S`,
|
|
11
|
+
];
|
|
12
|
+
const DEFAULT_LINKS = [
|
|
13
|
+
{
|
|
14
|
+
label: 'Kundeservice',
|
|
15
|
+
href: 'https://kundeservice.dbc.dk',
|
|
16
|
+
external: true,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
label: 'Cookies',
|
|
20
|
+
href: '/cookies',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
export function LayoutFooter({ links = DEFAULT_LINKS, metaParts = DEFAULT_META_PARTS, extraLinks, }) {
|
|
24
|
+
return (_jsx("footer", { className: styles.footer, children: _jsxs("div", { className: styles.inner, children: [_jsxs("div", { className: styles.brand, children: [_jsx("div", { className: styles.logoRow, children: _jsx(Logo, {}) }), _jsx("address", { className: styles.meta, children: metaParts.map(part => (_jsx("span", { className: styles.part, children: part }, part))) })] }), _jsxs("nav", { className: styles.links, "aria-label": "Footer navigation", children: [extraLinks &&
|
|
25
|
+
extraLinks.length > 0 &&
|
|
26
|
+
(extraLinks === null || extraLinks === void 0 ? void 0 : extraLinks.map((link, index) => _jsx("span", { children: link }, index))), links.map(link => (_jsx("span", { children: _jsx(Hyperlink, { href: link.href, ...(link.external ? { target: '_blank', rel: 'noopener noreferrer' } : {}), children: link.label }) }, link.label)))] })] }) }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
.footer {
|
|
2
|
+
inline-size: 100%;
|
|
3
|
+
background: var(--color-bg-surface-subtle);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.inner {
|
|
7
|
+
inline-size: 100%;
|
|
8
|
+
max-inline-size: var(--container-xl);
|
|
9
|
+
margin-inline: auto;
|
|
10
|
+
padding-block: var(--spacing-lg);
|
|
11
|
+
padding-inline: var(--spacing-md);
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: row;
|
|
14
|
+
align-items: flex-start;
|
|
15
|
+
gap: var(--spacing-2xl);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.brand {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: var(--spacing-xs);
|
|
22
|
+
flex-shrink: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.logoRow {
|
|
26
|
+
flex-shrink: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.logoRow svg {
|
|
30
|
+
height: 24px;
|
|
31
|
+
width: auto;
|
|
32
|
+
color: var(--color-brand);
|
|
33
|
+
display: block;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.meta {
|
|
37
|
+
font-style: normal;
|
|
38
|
+
margin: 0;
|
|
39
|
+
color: var(--color-fg-subtle);
|
|
40
|
+
line-height: var(--line-height-tight);
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
gap: 1px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.part {
|
|
47
|
+
white-space: nowrap;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.links {
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
align-items: flex-start;
|
|
54
|
+
gap: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.linkGroup {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
align-items: flex-start;
|
|
61
|
+
gap: var(--spacing-2xs);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.linkGroup + .linkGroup {
|
|
65
|
+
margin-block-start: var(--spacing-sm);
|
|
66
|
+
padding-inline: 0;
|
|
67
|
+
border-inline-start: none;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.linkItem {
|
|
71
|
+
white-space: nowrap;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@media (max-width: 640px) {
|
|
75
|
+
.inner {
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: var(--spacing-md);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.linkGroup {
|
|
81
|
+
padding-inline: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.linkGroup:first-child {
|
|
85
|
+
padding-inline-start: 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import type { PageLayoutMaxWidth } from '../../PageLayout';
|
|
2
3
|
export interface PageLayoutHeroProps {
|
|
3
4
|
children?: React.ReactNode;
|
|
4
5
|
link?: (children: React.ReactNode) => React.ReactNode;
|
|
5
6
|
image?: React.ReactNode;
|
|
6
7
|
headline?: string | React.ReactNode;
|
|
7
8
|
metaHeadline?: string | React.ReactNode;
|
|
8
|
-
maxWidth?:
|
|
9
|
+
maxWidth?: PageLayoutMaxWidth;
|
|
9
10
|
textBgColor?: string;
|
|
10
11
|
}
|
|
11
12
|
export declare const PageLayoutHero: React.FC<PageLayoutHeroProps>;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import heroStyles from './PageLayoutHero.module.css';
|
|
3
3
|
import layoutStyles from '../../PageLayout.module.css';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
function getMaxWidthClass(value) {
|
|
5
|
+
if (!value)
|
|
6
|
+
return '';
|
|
7
|
+
if (value === 'sm')
|
|
8
|
+
return layoutStyles.maxWidthSm;
|
|
9
|
+
return layoutStyles.maxWidthMd;
|
|
10
|
+
}
|
|
11
|
+
export const PageLayoutHero = ({ children, link, metaHeadline, headline, image, maxWidth = 'md', textBgColor = 'var(--color-primary-900)', }) => {
|
|
12
|
+
const content = (_jsx("div", { className: `${heroStyles.heroContainer} ${getMaxWidthClass(maxWidth)}`, children: _jsxs("div", { className: heroStyles.splitWrapper, children: [_jsx("div", { className: heroStyles.imageColumn, children: image }), _jsx("div", { className: heroStyles.textColumn, style: { backgroundColor: textBgColor }, children: _jsxs("div", { className: heroStyles.textInner, children: [metaHeadline && _jsx("div", { className: heroStyles.metaHeadline, children: metaHeadline }), headline && (_jsx("div", { className: heroStyles.headline, children: _jsx("h2", { children: headline }) })), children] }) })] }) }));
|
|
6
13
|
return link ? _jsx(_Fragment, { children: link(content) }) : _jsx(_Fragment, { children: content });
|
|
7
14
|
};
|
|
@@ -18,7 +18,10 @@ type SearchBoxProps<T extends Record<string, unknown>> = {
|
|
|
18
18
|
popoverMinWidth?: string;
|
|
19
19
|
loading?: boolean;
|
|
20
20
|
enableHotkey?: boolean;
|
|
21
|
+
fullWidth?: boolean;
|
|
21
22
|
onButtonClick?: () => void;
|
|
23
|
+
buttonLabel?: string;
|
|
24
|
+
buttonIcon?: React.ReactNode;
|
|
22
25
|
} & React.InputHTMLAttributes<HTMLInputElement>;
|
|
23
26
|
type SearchBoxComponent = <T extends Record<string, unknown>>(props: SearchBoxProps<T> & React.RefAttributes<HTMLInputElement>) => React.ReactElement | null;
|
|
24
27
|
export declare const SearchBox: SearchBoxComponent;
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { Search } from 'lucide-react';
|
|
4
4
|
import React, { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
-
import { Button } from '../../components/button/Button';
|
|
6
5
|
import { Input } from '../../components/forms/input/Input';
|
|
7
6
|
import { Menu } from '../../components/menu/Menu';
|
|
8
7
|
import { Popover } from '../../components/popover/Popover';
|
|
9
8
|
import { SkeletonLoaderItem } from '../../components/skeleton-loader/skeleton-loader-item/SkeletonLoaderItem';
|
|
10
9
|
import styles from './SearchBox.module.css';
|
|
11
|
-
export const SearchBox = forwardRef(function SearchBoxInner({ inputWidth, inputSize, variant, result, debounce = true, debounceMs = 800, onSearch, onSelect, displayPopover, resultKeys, resultTemplate, initialTemplate, popoverMinWidth = '500px', noResultText = 'Ingen resultater', loading, enableHotkey = true, onButtonClick, value, onChange, ...rest }, ref) {
|
|
10
|
+
export const SearchBox = forwardRef(function SearchBoxInner({ inputWidth, inputSize, variant, result, debounce = true, debounceMs = 800, onSearch, onSelect, displayPopover, resultKeys, resultTemplate, initialTemplate, popoverMinWidth = '500px', noResultText = 'Ingen resultater', loading, enableHotkey = true, onButtonClick, buttonLabel, buttonIcon, fullWidth = false, value, onChange, ...rest }, ref) {
|
|
12
11
|
const isControlled = value !== undefined;
|
|
13
12
|
// What the user sees immediately in the textbox
|
|
14
13
|
const [draft, setDraft] = useState(() => (isControlled ? String(value !== null && value !== void 0 ? value : '') : ''));
|
|
15
14
|
// Used only for UI state ("Indtast søgeord" vs results/no results)
|
|
16
15
|
const [searchQuery, setSearchQuery] = useState('');
|
|
16
|
+
const [activeIndex, setActiveIndex] = useState(null);
|
|
17
17
|
const popoverRef = useRef(null);
|
|
18
18
|
const internalInputRef = useRef(null);
|
|
19
19
|
// Forward ref to parent
|
|
@@ -76,8 +76,13 @@ export const SearchBox = forwardRef(function SearchBoxInner({ inputWidth, inputS
|
|
|
76
76
|
var _a;
|
|
77
77
|
setDraft(''); // always clear UI immediately
|
|
78
78
|
setSearchQuery('');
|
|
79
|
+
setActiveIndex(null);
|
|
79
80
|
(_a = popoverRef.current) === null || _a === void 0 ? void 0 : _a.close();
|
|
80
81
|
}
|
|
82
|
+
// Reset active index when results change
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
setActiveIndex(null);
|
|
85
|
+
}, [result]);
|
|
81
86
|
// Props for the input are now created inside useMemo to avoid unnecessary dependency changes
|
|
82
87
|
const inputField = useMemo(() => {
|
|
83
88
|
var _a;
|
|
@@ -86,17 +91,48 @@ export const SearchBox = forwardRef(function SearchBoxInner({ inputWidth, inputS
|
|
|
86
91
|
value: draft,
|
|
87
92
|
onChange: handleChange,
|
|
88
93
|
};
|
|
94
|
+
const showInputIcon = !onButtonClick || !!buttonLabel || !!buttonIcon;
|
|
95
|
+
const trailingButtonIcon = onButtonClick && !buttonLabel && !buttonIcon ? _jsx(Search, {}) : buttonIcon;
|
|
89
96
|
if (displayPopover) {
|
|
90
|
-
return (_jsx(Popover, { ref: popoverRef, minWidth: popoverMinWidth, trigger: event => {
|
|
97
|
+
return (_jsx(Popover, { ref: popoverRef, minWidth: popoverMinWidth, fullWidth: fullWidth, trigger: event => {
|
|
91
98
|
var _a;
|
|
92
|
-
return (_jsx(Input, { ref: internalInputRef, onFocusCapture: event, onClick:
|
|
93
|
-
|
|
99
|
+
return (_jsx(Input, { ref: internalInputRef, onFocusCapture: event, onClick: () => {
|
|
100
|
+
var _a, _b;
|
|
101
|
+
if (!((_a = popoverRef.current) === null || _a === void 0 ? void 0 : _a.isOpen()))
|
|
102
|
+
(_b = popoverRef.current) === null || _b === void 0 ? void 0 : _b.open();
|
|
103
|
+
}, minWidth: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), width: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), fullWidth: fullWidth, icon: showInputIcon ? _jsx(Search, {}) : undefined, inputSize: inputSize, variant: variant, autoComplete: "off", onButtonClick: onButtonClick, buttonLabel: buttonLabel, buttonIcon: trailingButtonIcon, ...inputProps, onKeyDown: e => {
|
|
104
|
+
var _a;
|
|
105
|
+
if (result === null || result === void 0 ? void 0 : result.length) {
|
|
106
|
+
if (e.key === 'ArrowDown') {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
setActiveIndex(prev => prev === null || prev === result.length - 1 ? 0 : prev + 1);
|
|
109
|
+
}
|
|
110
|
+
else if (e.key === 'ArrowUp') {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
setActiveIndex(prev => prev === null || prev === 0 ? result.length - 1 : prev - 1);
|
|
113
|
+
}
|
|
114
|
+
else if (e.key === 'Enter') {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
if (activeIndex !== null) {
|
|
117
|
+
handleSelect(result[activeIndex]);
|
|
118
|
+
}
|
|
119
|
+
else if (onButtonClick) {
|
|
120
|
+
onButtonClick();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (e.key === 'Escape') {
|
|
124
|
+
reset();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
(_a = inputProps.onKeyDown) === null || _a === void 0 ? void 0 : _a.call(inputProps, e);
|
|
128
|
+
}, placeholder: (_a = rest.placeholder) !== null && _a !== void 0 ? _a : 'Indtast søgeord' }));
|
|
129
|
+
}, children: resultTemplate ? (resultTemplate) : (result === null || result === void 0 ? void 0 : result.length) ? (_jsx(Menu, { children: _jsx("table", { className: styles.suggestionTable, children: _jsx("tbody", { children: result.map((item, index) => (_jsx("tr", { onClick: () => handleSelect(item), role: "button", tabIndex: 0, className: `${styles.suggestionRow}${index === activeIndex ? ` ${styles.suggestionRowActive}` : ''}`, children: resultKeys === null || resultKeys === void 0 ? void 0 : resultKeys.map(key => {
|
|
94
130
|
const raw = item[key];
|
|
95
131
|
const cell = raw != null ? String(raw) : '';
|
|
96
|
-
return (_jsx("td", { style: { whiteSpace: cell.length < 10 ? 'nowrap' : undefined }, children: cell }, key));
|
|
132
|
+
return (_jsx("td", { className: styles.suggestionCell, style: { whiteSpace: cell.length < 10 ? 'nowrap' : undefined }, children: cell }, key));
|
|
97
133
|
}) }, index))) }) }) })) : !searchQuery && !loading ? (initialTemplate || _jsx("div", { className: styles.resultContainer, children: "Indtast s\u00F8geord" })) : loading ? (_jsx("table", { style: { width: '100%' }, children: _jsx("tbody", { children: Array.from({ length: 5 }).map((_, index) => (_jsx("tr", { children: resultKeys === null || resultKeys === void 0 ? void 0 : resultKeys.map(key => (_jsx("td", { style: { padding: '8px' }, children: _jsx(SkeletonLoaderItem, { height: 20, width: "100%" }) }, key))) }, index))) }) })) : (_jsx("div", { className: styles.resultContainer, children: noResultText })) }));
|
|
98
134
|
}
|
|
99
|
-
return (_jsx(Input, { ref: internalInputRef, icon: _jsx(Search, {}), minWidth: inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px', inputSize: inputSize, variant: variant, ...inputProps, placeholder: (_a = rest.placeholder) !== null && _a !== void 0 ? _a : 'Indtast søgeord' }));
|
|
135
|
+
return (_jsx(Input, { ref: internalInputRef, icon: showInputIcon ? _jsx(Search, {}) : undefined, minWidth: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), fullWidth: fullWidth, inputSize: inputSize, variant: variant, onButtonClick: onButtonClick, buttonLabel: buttonLabel, buttonIcon: trailingButtonIcon, ...inputProps, placeholder: (_a = rest.placeholder) !== null && _a !== void 0 ? _a : 'Indtast søgeord' }));
|
|
100
136
|
}, [
|
|
101
137
|
rest,
|
|
102
138
|
draft,
|
|
@@ -114,6 +150,11 @@ export const SearchBox = forwardRef(function SearchBoxInner({ inputWidth, inputS
|
|
|
114
150
|
noResultText,
|
|
115
151
|
resultKeys,
|
|
116
152
|
handleSelect,
|
|
153
|
+
activeIndex,
|
|
154
|
+
onButtonClick,
|
|
155
|
+
buttonLabel,
|
|
156
|
+
buttonIcon,
|
|
157
|
+
fullWidth,
|
|
117
158
|
]);
|
|
118
|
-
return
|
|
159
|
+
return _jsx("div", { style: fullWidth ? { width: '100%' } : undefined, children: inputField });
|
|
119
160
|
});
|
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
padding: var(--spacing-sm);
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
.
|
|
6
|
-
display: inline-flex;
|
|
7
|
-
align-items: center;
|
|
5
|
+
.suggestionTable {
|
|
8
6
|
border-collapse: collapse;
|
|
7
|
+
width: 100%;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
.
|
|
12
|
-
|
|
13
|
-
border-top-right-radius: 0;
|
|
14
|
-
border-bottom-right-radius: 0;
|
|
10
|
+
.suggestionRow {
|
|
11
|
+
cursor: pointer;
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
.
|
|
18
|
-
|
|
14
|
+
.suggestionRow:hover > .suggestionCell,
|
|
15
|
+
.suggestionRowActive > .suggestionCell {
|
|
16
|
+
background-color: var(--table-row-bg-hover);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.suggestionCell {
|
|
20
|
+
padding: var(--spacing-xs) var(--spacing-sm);
|
|
19
21
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
// Aligned with --bp-sm (640px), --bp-md (768px), --bp-lg (1024px) from base.css
|
|
4
|
+
const BREAKPOINTS = {
|
|
5
|
+
tablet: 640,
|
|
6
|
+
desktop: 1024,
|
|
7
|
+
};
|
|
8
|
+
function getDeviceSize(width) {
|
|
9
|
+
if (width < BREAKPOINTS.tablet)
|
|
10
|
+
return 'mobile';
|
|
11
|
+
if (width < BREAKPOINTS.desktop)
|
|
12
|
+
return 'tablet';
|
|
13
|
+
return 'desktop';
|
|
14
|
+
}
|
|
15
|
+
export function useDeviceSize() {
|
|
16
|
+
const [deviceSize, setDeviceSize] = useState(() => {
|
|
17
|
+
if (typeof window === 'undefined')
|
|
18
|
+
return 'desktop';
|
|
19
|
+
return getDeviceSize(window.innerWidth);
|
|
20
|
+
});
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const mediaQueries = [
|
|
23
|
+
window.matchMedia(`(max-width: ${BREAKPOINTS.tablet - 1}px)`),
|
|
24
|
+
window.matchMedia(`(min-width: ${BREAKPOINTS.tablet}px) and (max-width: ${BREAKPOINTS.desktop - 1}px)`),
|
|
25
|
+
window.matchMedia(`(min-width: ${BREAKPOINTS.desktop}px)`),
|
|
26
|
+
];
|
|
27
|
+
const update = () => setDeviceSize(getDeviceSize(window.innerWidth));
|
|
28
|
+
mediaQueries.forEach(mq => mq.addEventListener('change', update));
|
|
29
|
+
return () => mediaQueries.forEach(mq => mq.removeEventListener('change', update));
|
|
30
|
+
}, []);
|
|
31
|
+
return deviceSize;
|
|
32
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './components/user-display/UserDisplay';
|
|
|
8
8
|
export * from './components/tabs/Tabs';
|
|
9
9
|
export * from './components/headline/Headline';
|
|
10
10
|
export * from './components/page-layout/PageLayout';
|
|
11
|
+
export * from './components/page-layout/components/layout-footer/LayoutFooter';
|
|
11
12
|
export * from './components/forms/input/Input';
|
|
12
13
|
export * from './components/search-box/SearchBox';
|
|
13
14
|
export * from './hooks/useTheme';
|
|
@@ -65,3 +66,4 @@ export * from './components/accordion/Accordion';
|
|
|
65
66
|
export * from './components/state-page/StatePage';
|
|
66
67
|
export * from './components/sticky-footer-layout/StickyFooterLayout';
|
|
67
68
|
export * from './components/forms/typeahead/Typeahead';
|
|
69
|
+
export * from './hooks/useDeviceSize';
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export * from './components/user-display/UserDisplay';
|
|
|
8
8
|
export * from './components/tabs/Tabs';
|
|
9
9
|
export * from './components/headline/Headline';
|
|
10
10
|
export * from './components/page-layout/PageLayout';
|
|
11
|
+
export * from './components/page-layout/components/layout-footer/LayoutFooter';
|
|
11
12
|
export * from './components/forms/input/Input';
|
|
12
13
|
export * from './components/search-box/SearchBox';
|
|
13
14
|
export * from './hooks/useTheme';
|
|
@@ -65,3 +66,4 @@ export * from './components/accordion/Accordion';
|
|
|
65
66
|
export * from './components/state-page/StatePage';
|
|
66
67
|
export * from './components/sticky-footer-layout/StickyFooterLayout';
|
|
67
68
|
export * from './components/forms/typeahead/Typeahead';
|
|
69
|
+
export * from './hooks/useDeviceSize';
|
package/dist/styles/styles.css
CHANGED