@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.
Files changed (52) hide show
  1. package/dist/components/card/Card.d.ts +21 -3
  2. package/dist/components/card/Card.js +17 -2
  3. package/dist/components/card/Card.module.css +59 -0
  4. package/dist/components/circle/Circle.d.ts +2 -1
  5. package/dist/components/circle/Circle.js +2 -2
  6. package/dist/components/circle/Circle.module.css +6 -2
  7. package/dist/components/code-block/CodeBlock.js +1 -1
  8. package/dist/components/code-block/CodeBlock.module.css +30 -17
  9. package/dist/components/copy-button/CopyButton.d.ts +1 -0
  10. package/dist/components/copy-button/CopyButton.js +10 -2
  11. package/dist/components/filter-field/FilterField.js +16 -11
  12. package/dist/components/filter-field/FilterField.module.css +133 -12
  13. package/dist/components/forms/checkbox/Checkbox.d.ts +2 -2
  14. package/dist/components/forms/checkbox-group/CheckboxGroup.js +1 -1
  15. package/dist/components/forms/checkbox-group/CheckboxGroup.module.css +1 -1
  16. package/dist/components/forms/input/Input.js +1 -1
  17. package/dist/components/forms/input/Input.module.css +1 -0
  18. package/dist/components/forms/input-container/InputContainer.module.css +1 -1
  19. package/dist/components/hyperlink/Hyperlink.d.ts +19 -7
  20. package/dist/components/hyperlink/Hyperlink.js +35 -11
  21. package/dist/components/hyperlink/Hyperlink.module.css +50 -2
  22. package/dist/components/menu/Menu.d.ts +32 -0
  23. package/dist/components/menu/Menu.js +73 -13
  24. package/dist/components/menu/Menu.module.css +72 -4
  25. package/dist/components/overlay/modal/Modal.module.css +2 -2
  26. package/dist/components/overlay/side-panel/SidePanel.js +17 -0
  27. package/dist/components/overlay/side-panel/SidePanel.module.css +0 -2
  28. package/dist/components/overlay/tooltip/useTooltipTrigger.js +4 -2
  29. package/dist/components/popover/Popover.js +1 -1
  30. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.js +22 -18
  31. package/dist/components/sidebar/providers/SidebarProvider.d.ts +4 -1
  32. package/dist/components/sidebar/providers/SidebarProvider.js +66 -18
  33. package/dist/components/split-button/SplitButton.d.ts +1 -1
  34. package/dist/components/split-button/SplitButton.js +3 -1
  35. package/dist/components/split-button/SplitButton.module.css +4 -4
  36. package/dist/components/state-page/StatePage.module.css +1 -1
  37. package/dist/components/table/Table.d.ts +9 -4
  38. package/dist/components/table/Table.js +3 -6
  39. package/dist/components/table/Table.module.css +18 -5
  40. package/dist/components/table/components/table-settings/TableSettings.d.ts +13 -3
  41. package/dist/components/table/components/table-settings/TableSettings.js +55 -4
  42. package/dist/components/table/tanstack.d.ts +12 -1
  43. package/dist/components/table/tanstack.js +75 -23
  44. package/dist/hooks/useTableSettings.d.ts +23 -4
  45. package/dist/hooks/useTableSettings.js +64 -17
  46. package/dist/src/styles/styles.css +38 -22
  47. package/dist/styles/animation.d.ts +5 -0
  48. package/dist/styles/animation.js +5 -0
  49. package/dist/styles/styles.css +38 -22
  50. package/dist/utils/localStorage.utils.d.ts +19 -0
  51. package/dist/utils/localStorage.utils.js +78 -0
  52. 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
- export function Hyperlink({ component, icon }) {
5
- const originalProps = component.props;
6
- return React.cloneElement(component, {
7
- ...originalProps,
8
- className: styles.link,
9
- onClick: (e) => {
10
- var _a;
11
- e.stopPropagation();
12
- (_a = originalProps === null || originalProps === void 0 ? void 0 : originalProps.onClick) === null || _a === void 0 ? void 0 : _a.call(originalProps, e);
13
- },
14
- }, _jsxs(_Fragment, { children: [_jsx("span", { className: styles.content, children: originalProps.children }), icon && _jsx("span", { className: styles.icon, children: icon })] }));
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
- text-decoration: underline;
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); // allow custom/Next Link components
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
- const childClass = [styles.item, active ? styles.active : ''].filter(Boolean).join(' ');
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
- export const Menu = Object.assign(MenuBase, { Item: MenuItem });
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
- .interactive:hover {
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
- .interactive svg {
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: 540px;
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: 400px;
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,8 +1,6 @@
1
1
  .sidePanel {
2
2
  --col-pad: var(--spacing-md);
3
3
 
4
- /* Dial these for “feel” */
5
- --panel-dur: 220ms;
6
4
  --panel-ease: cubic-bezier(0.22, 1, 0.36, 1); /* smooth spring-ish without overshoot */
7
5
 
8
6
  /* Shadow + edge */
@@ -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
- setOpen(true);
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, expandedItems, } = useSidebar();
15
- const [expanded, setExpanded] = useState(false);
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
- useEffect(() => {
22
- if (expandedItems.has(href)) {
23
- setExpanded(true);
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
- setExpanded(defaultExpanded);
30
- }, [defaultExpanded]);
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
- setExpanded(!closing);
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 === null || handleSidebarCollapseChange === void 0 ? void 0 : handleSidebarCollapseChange(false);
45
+ handleSidebarCollapseChange(false);
42
46
  if (!expanded) {
43
- setExpanded(true);
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 | undefined>;
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
  };