@dbcdk/react-components 0.0.10 → 0.0.12
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/card/Card.d.ts +21 -3
- package/dist/components/card/Card.js +17 -2
- package/dist/components/card/Card.module.css +59 -0
- package/dist/components/circle/Circle.d.ts +2 -1
- package/dist/components/circle/Circle.js +2 -2
- package/dist/components/circle/Circle.module.css +6 -2
- package/dist/components/code-block/CodeBlock.js +1 -1
- package/dist/components/code-block/CodeBlock.module.css +30 -17
- package/dist/components/copy-button/CopyButton.d.ts +1 -0
- package/dist/components/copy-button/CopyButton.js +10 -2
- package/dist/components/filter-field/FilterField.js +16 -11
- package/dist/components/filter-field/FilterField.module.css +133 -12
- package/dist/components/forms/checkbox/Checkbox.d.ts +2 -2
- package/dist/components/forms/checkbox-group/CheckboxGroup.js +1 -1
- package/dist/components/forms/checkbox-group/CheckboxGroup.module.css +1 -1
- package/dist/components/forms/input/Input.js +1 -1
- package/dist/components/forms/input/Input.module.css +1 -0
- package/dist/components/forms/input-container/InputContainer.module.css +1 -1
- package/dist/components/hyperlink/Hyperlink.d.ts +19 -7
- package/dist/components/hyperlink/Hyperlink.js +35 -11
- package/dist/components/hyperlink/Hyperlink.module.css +50 -2
- package/dist/components/menu/Menu.d.ts +32 -0
- package/dist/components/menu/Menu.js +73 -13
- package/dist/components/menu/Menu.module.css +72 -4
- package/dist/components/overlay/modal/Modal.module.css +2 -2
- package/dist/components/overlay/side-panel/SidePanel.js +17 -0
- package/dist/components/overlay/side-panel/SidePanel.module.css +0 -2
- package/dist/components/overlay/tooltip/useTooltipTrigger.js +4 -2
- package/dist/components/popover/Popover.js +1 -1
- package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.js +22 -18
- package/dist/components/sidebar/providers/SidebarProvider.d.ts +4 -1
- package/dist/components/sidebar/providers/SidebarProvider.js +66 -18
- package/dist/components/split-button/SplitButton.d.ts +1 -1
- package/dist/components/split-button/SplitButton.js +3 -1
- package/dist/components/split-button/SplitButton.module.css +4 -4
- package/dist/components/state-page/StatePage.module.css +1 -1
- package/dist/components/table/Table.d.ts +9 -4
- package/dist/components/table/Table.js +3 -6
- package/dist/components/table/Table.module.css +18 -5
- package/dist/components/table/components/table-settings/TableSettings.d.ts +13 -3
- package/dist/components/table/components/table-settings/TableSettings.js +55 -4
- package/dist/components/table/tanstack.d.ts +12 -1
- package/dist/components/table/tanstack.js +75 -23
- package/dist/hooks/useTableSettings.d.ts +23 -4
- package/dist/hooks/useTableSettings.js +64 -17
- package/dist/src/styles/styles.css +38 -22
- package/dist/styles/animation.d.ts +5 -0
- package/dist/styles/animation.js +5 -0
- package/dist/styles/styles.css +38 -22
- package/dist/utils/localStorage.utils.d.ts +19 -0
- package/dist/utils/localStorage.utils.js +78 -0
- package/package.json +1 -1
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import styles from './Hyperlink.module.css';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
function cx(...parts) {
|
|
5
|
+
return parts.filter(Boolean).join(' ');
|
|
6
|
+
}
|
|
7
|
+
function renderInner(children, icon) {
|
|
8
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.content, children: children }), icon && _jsx("span", { className: styles.icon, children: icon })] }));
|
|
9
|
+
}
|
|
10
|
+
export function Hyperlink(props) {
|
|
11
|
+
var _a;
|
|
12
|
+
const { children, icon, className, asChild, as = 'a', variant = 'primary', inline = true, ...rest } = props;
|
|
13
|
+
const linkClassName = cx(styles.link, className, variant === 'secondary' ? styles.secondary : styles.primary, inline ? '' : styles.block);
|
|
14
|
+
if (asChild) {
|
|
15
|
+
const child = React.Children.only(children);
|
|
16
|
+
if (!React.isValidElement(child)) {
|
|
17
|
+
throw new Error('Hyperlink with asChild expects a single valid React element as its child.');
|
|
18
|
+
}
|
|
19
|
+
const childProps = (_a = child.props) !== null && _a !== void 0 ? _a : {};
|
|
20
|
+
return React.cloneElement(child, {
|
|
21
|
+
...childProps,
|
|
22
|
+
...rest,
|
|
23
|
+
className: cx(childProps.className, linkClassName),
|
|
24
|
+
children: renderInner(childProps.children, icon),
|
|
25
|
+
onClick: (e) => {
|
|
26
|
+
e.stopPropagation();
|
|
27
|
+
if (childProps.onClick) {
|
|
28
|
+
childProps.onClick(e);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (as === 'button') {
|
|
34
|
+
// (Optional) guardrail: avoid accidentally passing href to a button
|
|
35
|
+
// const { href, ...buttonRest } = rest as any
|
|
36
|
+
return (_jsx("button", { type: "button", className: linkClassName, ...rest, children: renderInner(children, icon) }));
|
|
37
|
+
}
|
|
38
|
+
return (_jsx("a", { onClick: e => e.stopPropagation(), className: linkClassName, ...rest, children: renderInner(children, icon) }));
|
|
15
39
|
}
|
|
@@ -2,13 +2,61 @@
|
|
|
2
2
|
display: inline-flex;
|
|
3
3
|
gap: var(--spacing-xs);
|
|
4
4
|
position: relative;
|
|
5
|
+
font-weight: normal;
|
|
6
|
+
background: none;
|
|
7
|
+
border: none;
|
|
8
|
+
padding: 0;
|
|
5
9
|
text-decoration: none;
|
|
6
|
-
color: var(--color-brand);
|
|
7
10
|
font-size: inherit;
|
|
11
|
+
font-family: inherit;
|
|
12
|
+
cursor: pointer;
|
|
13
|
+
color: var(--color-brand);
|
|
14
|
+
line-height: inherit;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.link.secondary {
|
|
18
|
+
color: var(--color-fg-default);
|
|
8
19
|
}
|
|
9
20
|
|
|
10
21
|
.link:hover {
|
|
11
|
-
|
|
22
|
+
color: var(--color-brand);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.link.primary {
|
|
26
|
+
position: relative;
|
|
27
|
+
color: var(--color-brand);
|
|
28
|
+
text-decoration: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.link.block {
|
|
32
|
+
background: var(--color-bg-contextual-subtle);
|
|
33
|
+
display: inline-block;
|
|
34
|
+
padding: var(--spacing-xs);
|
|
35
|
+
&:hover {
|
|
36
|
+
background-color: var(--color-bg-contextual);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.link::after {
|
|
41
|
+
content: '';
|
|
42
|
+
position: absolute;
|
|
43
|
+
left: 0;
|
|
44
|
+
bottom: -2px;
|
|
45
|
+
width: 100%;
|
|
46
|
+
height: 1px;
|
|
47
|
+
background-color: currentColor;
|
|
48
|
+
transform: scaleX(0);
|
|
49
|
+
transform-origin: left;
|
|
50
|
+
transition: transform 100ms ease;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.link:hover::after {
|
|
54
|
+
transform: scaleX(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.link:focus-visible {
|
|
58
|
+
outline: 2px solid var(--color-brand);
|
|
59
|
+
outline-offset: 2px;
|
|
12
60
|
}
|
|
13
61
|
|
|
14
62
|
.icon {
|
|
@@ -2,11 +2,43 @@ import * as React from 'react';
|
|
|
2
2
|
export interface MenuProps extends React.HTMLAttributes<HTMLUListElement> {
|
|
3
3
|
children: React.ReactNode;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Use when you need a visual divider inside a Menu.
|
|
7
|
+
* Renders a non-interactive <li role="separator" />
|
|
8
|
+
*/
|
|
9
|
+
export type MenuSeparatorProps = React.LiHTMLAttributes<HTMLLIElement>;
|
|
5
10
|
export interface MenuItemProps extends React.LiHTMLAttributes<HTMLLIElement> {
|
|
6
11
|
children: React.ReactNode;
|
|
7
12
|
active?: boolean;
|
|
8
13
|
disabled?: boolean;
|
|
9
14
|
}
|
|
15
|
+
export interface MenuCheckItemProps extends Omit<React.LiHTMLAttributes<HTMLLIElement>, 'onChange'> {
|
|
16
|
+
label: React.ReactNode;
|
|
17
|
+
checked: boolean;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
20
|
+
}
|
|
21
|
+
export interface MenuRadioItemProps extends Omit<React.LiHTMLAttributes<HTMLLIElement>, 'onChange'> {
|
|
22
|
+
/**
|
|
23
|
+
* Shared group name for the radio items in this menu section.
|
|
24
|
+
* (Required by your RadioButton component)
|
|
25
|
+
*/
|
|
26
|
+
name: string;
|
|
27
|
+
/**
|
|
28
|
+
* This option's value.
|
|
29
|
+
*/
|
|
30
|
+
value: string;
|
|
31
|
+
/**
|
|
32
|
+
* Whether this radio option is selected.
|
|
33
|
+
*/
|
|
34
|
+
checked: boolean;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
label: string;
|
|
37
|
+
onValueChange?: (value: string) => void;
|
|
38
|
+
}
|
|
10
39
|
export declare const Menu: React.FC<MenuProps> & {
|
|
11
40
|
Item: React.FC<MenuItemProps>;
|
|
41
|
+
CheckItem: React.FC<MenuCheckItemProps>;
|
|
42
|
+
RadioItem: React.FC<MenuRadioItemProps>;
|
|
43
|
+
Separator: React.FC<MenuSeparatorProps>;
|
|
12
44
|
};
|
|
@@ -2,28 +2,88 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import styles from './Menu.module.css';
|
|
5
|
+
import { Checkbox } from '../forms/checkbox/Checkbox';
|
|
6
|
+
import { RadioButton } from '../forms/radio-buttons/RadioButton';
|
|
5
7
|
const MenuBase = React.forwardRef(({ children, className, ...props }, ref) => (_jsx("ul", { ref: ref, role: "menu", className: [styles.container, className].filter(Boolean).join(' '), ...props, children: children })));
|
|
6
8
|
MenuBase.displayName = 'Menu';
|
|
7
9
|
const isInteractiveEl = (el) => React.isValidElement(el) &&
|
|
8
|
-
(typeof el.type === 'string' ? el.type === 'a' || el.type === 'button' : true);
|
|
10
|
+
(typeof el.type === 'string' ? el.type === 'a' || el.type === 'button' : true);
|
|
11
|
+
/**
|
|
12
|
+
* Apply menu styles not only to the interactive element itself,
|
|
13
|
+
* but also to immediate children (covers cases where the direct child is a <div>
|
|
14
|
+
* wrapping a button-like thing, or components that render a wrapper).
|
|
15
|
+
*/
|
|
16
|
+
function applyMenuItemPropsToElement(child, opts) {
|
|
17
|
+
const { active, disabled, role = 'menuitem', tabIndex = -1, className } = opts;
|
|
18
|
+
const childClass = [styles.item, active ? styles.active : ''].filter(Boolean).join(' ');
|
|
19
|
+
// Always apply styling to the immediate child
|
|
20
|
+
const nextImmediate = React.cloneElement(child, {
|
|
21
|
+
className: [child.props.className, styles.interactiveChild, className]
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.join(' '),
|
|
24
|
+
});
|
|
25
|
+
// If the immediate child is already interactive, we can apply full a11y+styles there
|
|
26
|
+
if (typeof child.type === 'string' && (child.type === 'a' || child.type === 'button')) {
|
|
27
|
+
return React.cloneElement(child, {
|
|
28
|
+
role,
|
|
29
|
+
tabIndex,
|
|
30
|
+
'aria-selected': active || undefined,
|
|
31
|
+
'aria-disabled': disabled || undefined,
|
|
32
|
+
className: [child.props.className, styles.interactive, childClass, className]
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.join(' '),
|
|
35
|
+
...(child.type === 'button' ? { disabled } : {}),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// For custom components, we *assume* they forward props.
|
|
39
|
+
return React.cloneElement(nextImmediate, {
|
|
40
|
+
role,
|
|
41
|
+
tabIndex,
|
|
42
|
+
'aria-selected': active || undefined,
|
|
43
|
+
'aria-disabled': disabled || undefined,
|
|
44
|
+
className: [nextImmediate.props.className, styles.interactive, childClass]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(' '),
|
|
47
|
+
disabled,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
9
50
|
const MenuItem = React.forwardRef(({ children, active, disabled, className, ...liProps }, ref) => {
|
|
10
|
-
// If child is interactive (a/button/NextLink), clone it and style it.
|
|
11
51
|
if (isInteractiveEl(children)) {
|
|
12
52
|
const child = children;
|
|
13
|
-
|
|
14
|
-
return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: React.cloneElement(child, {
|
|
15
|
-
role: 'menuitem',
|
|
16
|
-
tabIndex: -1,
|
|
17
|
-
'aria-selected': active || undefined,
|
|
18
|
-
'aria-disabled': disabled || undefined,
|
|
19
|
-
className: [child.props.className, styles.interactive, childClass]
|
|
20
|
-
.filter(Boolean)
|
|
21
|
-
.join(' '),
|
|
22
|
-
}) }));
|
|
53
|
+
return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: applyMenuItemPropsToElement(child, { active, disabled }) }));
|
|
23
54
|
}
|
|
55
|
+
// Fallback: we wrap non-interactive children in a <button>
|
|
24
56
|
return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: _jsx("button", { role: "menuitem", tabIndex: -1, "aria-selected": active || undefined, "aria-disabled": disabled || undefined, className: [styles.interactive, styles.item, active ? styles.active : '']
|
|
25
57
|
.filter(Boolean)
|
|
26
58
|
.join(' '), type: "button", disabled: disabled, children: children }) }));
|
|
27
59
|
});
|
|
28
60
|
MenuItem.displayName = 'Menu.Item';
|
|
29
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Menu checkbox row that uses your Checkbox component.
|
|
63
|
+
* Note: this is a *control inside a menu*, so we lean on your Checkbox a11y (`role="checkbox"`)
|
|
64
|
+
* rather than forcing `menuitemcheckbox` roles.
|
|
65
|
+
*/
|
|
66
|
+
const MenuCheckItem = React.forwardRef(({ label, checked, disabled, onCheckedChange, className, ...liProps }, ref) => {
|
|
67
|
+
return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: _jsx("div", { className: styles.interactiveChild, children: _jsx(Checkbox, { noContainer: true, checked: checked, disabled: disabled, label: label,
|
|
68
|
+
// Your Checkbox emits (checked, mouseEvent)
|
|
69
|
+
onChange: (next, _e) => onCheckedChange === null || onCheckedChange === void 0 ? void 0 : onCheckedChange(next) }) }) }));
|
|
70
|
+
});
|
|
71
|
+
MenuCheckItem.displayName = 'Menu.CheckItem';
|
|
72
|
+
/**
|
|
73
|
+
* Menu radio row that uses your RadioButton component.
|
|
74
|
+
* Same note as above: we keep your native radio semantics.
|
|
75
|
+
*/
|
|
76
|
+
const MenuRadioItem = React.forwardRef(({ name, value, checked, disabled, label, onValueChange, className, ...liProps }, ref) => {
|
|
77
|
+
return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: _jsx("div", { className: styles.interactiveChild, children: _jsx(RadioButton, { noContainer: true, name: name, value: value, checked: checked, disabled: disabled, label: label,
|
|
78
|
+
// Your RadioButton emits (value, changeEvent)
|
|
79
|
+
onChange: (v, _e) => onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(v) }) }) }));
|
|
80
|
+
});
|
|
81
|
+
MenuRadioItem.displayName = 'Menu.RadioItem';
|
|
82
|
+
const MenuSeparator = React.forwardRef(({ className, ...props }, ref) => (_jsx("li", { ref: ref, role: "separator", className: [styles.separator, className].filter(Boolean).join(' '), ...props })));
|
|
83
|
+
MenuSeparator.displayName = 'Menu.Separator';
|
|
84
|
+
export const Menu = Object.assign(MenuBase, {
|
|
85
|
+
Item: MenuItem,
|
|
86
|
+
CheckItem: MenuCheckItem,
|
|
87
|
+
RadioItem: MenuRadioItem,
|
|
88
|
+
Separator: MenuSeparator,
|
|
89
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* Menu.module.css */
|
|
1
2
|
.container {
|
|
2
3
|
list-style: none;
|
|
3
4
|
margin: 0;
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
display: contents;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/* Applied to actual interactive elements (button/a/custom that forwards className) */
|
|
20
22
|
.interactive {
|
|
21
23
|
display: flex;
|
|
22
24
|
align-items: center;
|
|
@@ -45,29 +47,95 @@
|
|
|
45
47
|
color var(--transition-fast) var(--ease-standard);
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
/*
|
|
51
|
+
Applied to the immediate child of <li> even if it's NOT an interactive element (e.g. a <div>)
|
|
52
|
+
so that menu row styling still works for components that render a wrapper.
|
|
53
|
+
*/
|
|
54
|
+
.interactiveChild {
|
|
55
|
+
display: block;
|
|
56
|
+
inline-size: 100%;
|
|
57
|
+
border-radius: var(--border-radius-sm);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* NEW: make wrapper-children (Checkbox/Radio) look/space like menu rows */
|
|
61
|
+
.row > .interactiveChild {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
inline-size: 100%;
|
|
65
|
+
padding-block: calc(var(--spacing-xxs) + var(--density-comfortable));
|
|
66
|
+
padding-inline: var(--spacing-md);
|
|
67
|
+
border-radius: var(--border-radius-sm);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* NEW: let Checkbox/Radio consume full width so the hover area feels right */
|
|
71
|
+
.row > .interactiveChild > * {
|
|
72
|
+
inline-size: 100%;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* NEW: add consistent gap between control and label inside Checkbox/Radio
|
|
76
|
+
Both components use a root element with className={styles.container}.
|
|
77
|
+
Because they're CSS modules, we must target it with :global(.container). */
|
|
78
|
+
.row :global(.container) {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: var(--spacing-sm);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Hover: support both cases (interactive element, or wrapper child) */
|
|
85
|
+
.interactive:hover,
|
|
86
|
+
.row:hover > .interactiveChild {
|
|
49
87
|
background-color: var(--color-bg-hover-subtle);
|
|
50
88
|
}
|
|
51
89
|
|
|
90
|
+
/* Focus ring: support both cases */
|
|
52
91
|
.interactive:focus-visible {
|
|
53
92
|
outline: none;
|
|
54
93
|
box-shadow: var(--focus-ring);
|
|
55
94
|
}
|
|
56
95
|
|
|
96
|
+
/* If wrapper contains a focusable element, show ring when any child is focused */
|
|
97
|
+
.row:focus-within > .interactiveChild {
|
|
98
|
+
outline: none;
|
|
99
|
+
box-shadow: var(--focus-ring);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Selected/active (legacy + item variant) */
|
|
57
103
|
.active,
|
|
58
|
-
.interactive[aria-selected='true']
|
|
104
|
+
.interactive[aria-selected='true'],
|
|
105
|
+
.row > .interactiveChild.active,
|
|
106
|
+
.row > .interactiveChild[aria-selected='true'] {
|
|
59
107
|
background-color: var(--color-bg-selected);
|
|
60
108
|
color: var(--color-fg-default);
|
|
61
109
|
}
|
|
62
110
|
|
|
111
|
+
/* Checked (legacy support; kept in case any interactive element still uses aria-checked) */
|
|
112
|
+
.interactive[aria-checked='true'],
|
|
113
|
+
.row > .interactiveChild[aria-checked='true'] {
|
|
114
|
+
background-color: var(--color-bg-selected);
|
|
115
|
+
color: var(--color-fg-default);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Disabled: support both cases */
|
|
63
119
|
.interactive[aria-disabled='true'],
|
|
64
|
-
.interactive:disabled
|
|
120
|
+
.interactive:disabled,
|
|
121
|
+
.row > .interactiveChild[aria-disabled='true'] {
|
|
65
122
|
color: var(--color-disabled-fg);
|
|
66
123
|
cursor: not-allowed;
|
|
67
124
|
pointer-events: none;
|
|
68
125
|
}
|
|
69
126
|
|
|
70
|
-
|
|
127
|
+
/* Icons inside either interactive element or wrapper */
|
|
128
|
+
.interactive svg,
|
|
129
|
+
.interactiveChild svg {
|
|
71
130
|
inline-size: var(--icon-size-md);
|
|
72
131
|
block-size: var(--icon-size-md);
|
|
73
132
|
}
|
|
133
|
+
|
|
134
|
+
/* Visual separator row (used by <Menu.Separator />) */
|
|
135
|
+
.separator {
|
|
136
|
+
block-size: 1px;
|
|
137
|
+
margin-block: var(--spacing-2xs);
|
|
138
|
+
background: var(--color-border-subtle);
|
|
139
|
+
opacity: 0.8;
|
|
140
|
+
border-radius: 999px;
|
|
141
|
+
}
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
background: var(--color-bg-surface);
|
|
16
16
|
border-radius: var(--border-radius-lg);
|
|
17
17
|
min-width: 320px;
|
|
18
|
-
max-width:
|
|
18
|
+
max-width: 700px;
|
|
19
19
|
max-height: calc(100vh - (2 * var(--spacing-md)));
|
|
20
20
|
display: flex;
|
|
21
21
|
flex-direction: column;
|
|
22
22
|
box-shadow: var(--shadow-lg);
|
|
23
23
|
font-family: var(--font-family);
|
|
24
|
-
min-width:
|
|
24
|
+
min-width: 500px;
|
|
25
25
|
color: var(--color-fg-default);
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState, } from 'react';
|
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { Button } from '../../../components/button/Button';
|
|
6
6
|
import { Headline } from '../../../components/headline/Headline';
|
|
7
|
+
import { MOTION_MS } from '../../../styles/animation';
|
|
7
8
|
import styles from './SidePanel.module.css';
|
|
8
9
|
export function SidePanel({ isOpen, onClose, children, header, headerAddition, actions, showBackdrop = true, severity, showHeaderMarker = true, width = '400px', details, detailsHeader = 'Output', detailsWidth = '420px', onCloseDetails, detailsHeaderAddition, ...props }) {
|
|
9
10
|
const [mounted, setMounted] = useState(false);
|
|
@@ -16,6 +17,21 @@ export function SidePanel({ isOpen, onClose, children, header, headerAddition, a
|
|
|
16
17
|
if (isOpen)
|
|
17
18
|
setShouldRender(true);
|
|
18
19
|
}, [isOpen]);
|
|
20
|
+
// Close on ESC key
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!isOpen)
|
|
23
|
+
return;
|
|
24
|
+
const handleKeyDown = (e) => {
|
|
25
|
+
if (e.key === 'Escape') {
|
|
26
|
+
e.stopPropagation();
|
|
27
|
+
onClose();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
31
|
+
return () => {
|
|
32
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
33
|
+
};
|
|
34
|
+
}, [isOpen, onClose]);
|
|
19
35
|
// Two-phase OPEN/CLOSE class toggle (lets CSS transitions kick in reliably)
|
|
20
36
|
useEffect(() => {
|
|
21
37
|
if (!shouldRender)
|
|
@@ -56,6 +72,7 @@ export function SidePanel({ isOpen, onClose, children, header, headerAddition, a
|
|
|
56
72
|
} })), _jsxs("div", { ref: panelRef, ...props, className: `${styles.sidePanel} ${isActive ? styles.open : ''} ${hasDetails ? styles.withDetails : styles.noDetails}`, style: {
|
|
57
73
|
'--side-panel-width': width,
|
|
58
74
|
'--details-width': detailsWidth,
|
|
75
|
+
'--panel-dur': MOTION_MS.panelSlide + 'ms',
|
|
59
76
|
}, "data-cy": "details-panel", role: "dialog", "aria-modal": "true", children: [hasDetails ? (_jsxs("aside", { className: styles.detailsCol, "data-cy": "details-panel-details", children: [_jsxs("div", { className: styles.detailsHeader, children: [_jsx("div", { className: styles.detailsTitle, children: detailsHeader }), _jsxs("div", { className: styles.detailsHeaderActions, children: [detailsHeaderAddition, onCloseDetails ? (_jsx(Button, { type: "button", size: "sm", variant: "outlined", onClick: e => {
|
|
60
77
|
e.stopPropagation();
|
|
61
78
|
onCloseDetails();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useContext, useEffect, useId, useRef, useState } from 'react';
|
|
2
|
+
import { MOTION_MS } from '../../../styles/animation';
|
|
2
3
|
import { TooltipContext } from './TooltipProvider';
|
|
3
4
|
export function useTooltipTrigger(options) {
|
|
4
5
|
const ctx = useContext(TooltipContext);
|
|
@@ -45,7 +46,6 @@ export function useTooltipTrigger(options) {
|
|
|
45
46
|
}
|
|
46
47
|
show();
|
|
47
48
|
}, [isOpen, show, hide]);
|
|
48
|
-
// ✅ Only call update if THIS tooltip is the active one AND something changed
|
|
49
49
|
useEffect(() => {
|
|
50
50
|
var _a;
|
|
51
51
|
if (!isOpen)
|
|
@@ -84,7 +84,9 @@ export function useTooltipTrigger(options) {
|
|
|
84
84
|
const onFocus = () => {
|
|
85
85
|
clearTimers();
|
|
86
86
|
if (!isControlled)
|
|
87
|
-
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
setOpen(true);
|
|
89
|
+
}, MOTION_MS.tooltipOpen);
|
|
88
90
|
};
|
|
89
91
|
const onBlur = () => {
|
|
90
92
|
clearTimers();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
|
4
|
-
import { createPortal } from 'react-dom';
|
|
5
4
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react';
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
6
6
|
import styles from './Popover.module.css';
|
|
7
7
|
export const Popover = forwardRef(function Popover({ trigger: Trigger, children, minWidth = '200px', matchTriggerWidth = true, viewportPadding = 8, edgeBuffer = 100, dataCy, }, ref) {
|
|
8
8
|
const [pos, setPos] = useState({ top: 0, left: 0, width: 0, visible: false });
|
|
@@ -1,52 +1,57 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { ChevronDown } from 'lucide-react';
|
|
4
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
5
5
|
import styles from './ExpandableSidebarItem.module.css';
|
|
6
6
|
import { Button } from '../../../button/Button';
|
|
7
7
|
import { useSidebar } from '../../providers/SidebarProvider';
|
|
8
|
+
import { ExpandableSidebarItem as ExpandableChild } from '../expandable-sidebar-item/ExpandableSidebarItem';
|
|
8
9
|
import { SidebarItemContent } from '../sidebar-item-content/SidebarItemContent';
|
|
9
10
|
import { SidebarItem } from '../SidebarItem';
|
|
10
|
-
import { ExpandableSidebarItem as ExpandableChild } from '../expandable-sidebar-item/ExpandableSidebarItem';
|
|
11
11
|
const isGroup = (item) => item.type === 'group';
|
|
12
12
|
const isExpandable = (item) => item.type === 'expandable';
|
|
13
13
|
export function ExpandableSidebarItem({ items, label, icon, component: Component, href, }) {
|
|
14
|
-
const { defaultExpanded, resetExpandAll, isSidebarCollapsed, handleSidebarCollapseChange,
|
|
15
|
-
|
|
14
|
+
const { defaultExpanded, resetExpandAll, isSidebarCollapsed, handleSidebarCollapseChange, expandItem, collapseItem, isExpanded, } = useSidebar();
|
|
15
|
+
// Local-only state for animation coordination
|
|
16
16
|
const [closing, setClosing] = useState(false);
|
|
17
17
|
const [ready, setReady] = useState(false);
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
setReady(true);
|
|
20
20
|
}, []);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
}, [expandedItems, href]);
|
|
21
|
+
// Single source of truth: expanded comes from provider state
|
|
22
|
+
const expanded = useMemo(() => isExpanded(href), [href, isExpanded]);
|
|
23
|
+
// Expand-all behavior (e.g. search)
|
|
26
24
|
useEffect(() => {
|
|
27
25
|
if (defaultExpanded === null)
|
|
28
26
|
return;
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
if (defaultExpanded)
|
|
28
|
+
expandItem(href);
|
|
29
|
+
else
|
|
30
|
+
collapseItem(href);
|
|
31
|
+
}, [defaultExpanded, expandItem, collapseItem, href]);
|
|
31
32
|
const handleAnimationEnd = useCallback(() => {
|
|
32
|
-
if (ready)
|
|
33
|
-
|
|
33
|
+
if (!ready)
|
|
34
|
+
return;
|
|
35
|
+
if (closing) {
|
|
36
|
+
// After collapse animation, commit closed state
|
|
37
|
+
collapseItem(href);
|
|
34
38
|
setClosing(false);
|
|
35
39
|
}
|
|
36
|
-
}, [closing, ready]);
|
|
40
|
+
}, [closing, ready, collapseItem, href]);
|
|
37
41
|
const toggleAccordion = useCallback((e, onlyExpand = false) => {
|
|
38
42
|
e === null || e === void 0 ? void 0 : e.preventDefault();
|
|
39
43
|
e === null || e === void 0 ? void 0 : e.stopPropagation();
|
|
40
44
|
resetExpandAll();
|
|
41
|
-
handleSidebarCollapseChange
|
|
45
|
+
handleSidebarCollapseChange(false);
|
|
42
46
|
if (!expanded) {
|
|
43
|
-
|
|
47
|
+
expandItem(href);
|
|
44
48
|
return;
|
|
45
49
|
}
|
|
46
50
|
if (!isSidebarCollapsed && !onlyExpand) {
|
|
51
|
+
// Start collapse animation; state commit happens onAnimationEnd
|
|
47
52
|
setClosing(true);
|
|
48
53
|
}
|
|
49
|
-
}, [expanded, handleSidebarCollapseChange, isSidebarCollapsed, resetExpandAll]);
|
|
54
|
+
}, [expanded, expandItem, href, handleSidebarCollapseChange, isSidebarCollapsed, resetExpandAll]);
|
|
50
55
|
const renderNavItem = (item, key) => {
|
|
51
56
|
var _a, _b;
|
|
52
57
|
if (isGroup(item)) {
|
|
@@ -55,7 +60,6 @@ export function ExpandableSidebarItem({ items, label, icon, component: Component
|
|
|
55
60
|
if (isExpandable(item)) {
|
|
56
61
|
return (_jsx(ExpandableChild, { items: (_b = item.children) !== null && _b !== void 0 ? _b : [], label: item.label, icon: item.icon, href: item.href, component: item.component }, key));
|
|
57
62
|
}
|
|
58
|
-
// Default item (type 'item' or undefined)
|
|
59
63
|
return (_jsx(SidebarItem, { component: item.component, label: item.label, icon: item.icon, href: item.href }, key));
|
|
60
64
|
};
|
|
61
65
|
return (_jsxs("div", { className: `${styles.container} ${expanded ? styles.expanded : ''}`, children: [_jsx(Component, { onClick: () => toggleAccordion(undefined, true), children: _jsx(SidebarItemContent, { icon: icon, label: label, href: href, disableActiveStyles: expanded, suffixIcon: isSidebarCollapsed ? null : (_jsx(Button, { variant: "outlined", onClick: toggleAccordion, children: _jsx(ChevronDown, { className: `${styles.chevron} ${expanded ? styles.chevronExpanded : ''}` }) })) }) }), expanded && !isSidebarCollapsed && (_jsx("div", { onAnimationEnd: handleAnimationEnd, className: `${styles.childrenContainer} ${closing ? 'animate--collapse' : ''} ${expanded ? 'animate--expand' : 'visually-hidden'}`, children: items.map((item, idx) => renderNavItem(item, `${href}-${idx}`)) }))] }));
|
|
@@ -3,7 +3,7 @@ import * as React from 'react';
|
|
|
3
3
|
import { NavBarItem } from '../../../components/nav-bar/NavBar';
|
|
4
4
|
export type SidebarContextValue = {
|
|
5
5
|
defaultExpanded: boolean | null;
|
|
6
|
-
expandedItems: Set<string
|
|
6
|
+
expandedItems: Set<string>;
|
|
7
7
|
resetExpandAll: () => void;
|
|
8
8
|
activeQuery: string;
|
|
9
9
|
areItemsCollapsed: boolean;
|
|
@@ -13,6 +13,9 @@ export type SidebarContextValue = {
|
|
|
13
13
|
filteredItems?: NavBarItem[];
|
|
14
14
|
activeLink?: string;
|
|
15
15
|
setActiveLink: (href: string) => void;
|
|
16
|
+
expandItem: (href: string) => void;
|
|
17
|
+
collapseItem: (href: string) => void;
|
|
18
|
+
isExpanded: (href: string) => boolean;
|
|
16
19
|
isSidebarCollapsed: boolean;
|
|
17
20
|
handleSidebarCollapseChange: (collapsed: boolean) => void;
|
|
18
21
|
};
|