@iota/apps-ui-kit 0.2.1 → 0.5.0

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 (43) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +16 -0
  3. package/dist/index.d.ts +91 -15
  4. package/dist/index.es.js +4536 -4297
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/index.umd.js +52 -52
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/style.css +1 -1
  9. package/package.json +3 -3
  10. package/src/lib/components/atoms/badge/badge.classes.ts +9 -0
  11. package/src/lib/components/atoms/badge/badge.enums.ts +3 -0
  12. package/src/lib/components/atoms/button/button.classes.ts +4 -4
  13. package/src/lib/components/atoms/index.ts +1 -0
  14. package/src/lib/components/atoms/info-box/InfoBox.tsx +1 -1
  15. package/src/lib/components/atoms/info-box/infoBox.classes.ts +7 -2
  16. package/src/lib/components/atoms/info-box/infoBox.enums.ts +2 -0
  17. package/src/lib/components/atoms/snackbar/snackbar.classes.ts +5 -1
  18. package/src/lib/components/atoms/snackbar/snackbar.enums.tsx +2 -0
  19. package/src/lib/components/atoms/toggle/Toggle.tsx +140 -0
  20. package/src/lib/components/atoms/toggle/index.ts +6 -0
  21. package/src/lib/components/atoms/toggle/toggle.classes.ts +41 -0
  22. package/src/lib/components/atoms/toggle/toggle.enums.ts +12 -0
  23. package/src/lib/components/atoms/tooltip/Tooltip.tsx +4 -1
  24. package/src/lib/components/atoms/visual-asset-card/VisualAssetCard.tsx +26 -18
  25. package/src/lib/components/molecules/address/Address.tsx +1 -1
  26. package/src/lib/components/molecules/card/CardAction.tsx +4 -2
  27. package/src/lib/components/molecules/card/CardBody.tsx +12 -1
  28. package/src/lib/components/molecules/card/card.classes.ts +1 -0
  29. package/src/lib/components/molecules/card/card.enums.ts +1 -0
  30. package/src/lib/components/molecules/input/input.classes.ts +2 -2
  31. package/src/lib/components/molecules/search/Search.tsx +39 -5
  32. package/src/lib/components/molecules/table-header-cell/TableHeaderCell.tsx +27 -13
  33. package/src/lib/components/molecules/title/Title.tsx +9 -1
  34. package/src/lib/components/organisms/accordion/Accordion.tsx +3 -5
  35. package/src/lib/components/organisms/dialog/Dialog.tsx +6 -5
  36. package/src/lib/components/organisms/navbar/Navbar.tsx +10 -10
  37. package/src/lib/components/organisms/table/Table.tsx +19 -3
  38. package/src/lib/constants/colors.constants.ts +6 -0
  39. package/src/lib/styles/index.css +13 -0
  40. package/src/lib/tailwind/base.presets.ts +2 -0
  41. package/src/lib/tailwind/plugins.ts +8 -0
  42. package/src/storybook/stories/atoms/Toggle.stories.tsx +59 -0
  43. package/src/storybook/stories/organisms/Navbar.stories.tsx +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iota/apps-ui-kit",
3
- "version": "0.2.1",
3
+ "version": "0.5.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "github.com:iotaledger/iota.git"
@@ -30,7 +30,7 @@
30
30
  "react": "^18.3.1",
31
31
  "react-dom": "^18.3.1",
32
32
  "react-number-format": "^5.2.2",
33
- "@iota/apps-ui-icons": "0.2.1"
33
+ "@iota/apps-ui-icons": "0.3.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@chromatic-com/storybook": "^1.6.0",
@@ -54,7 +54,7 @@
54
54
  "storybook-dark-mode": "^4.0.2",
55
55
  "tailwindcss": "^3.3.3",
56
56
  "typescript": "^5.5.3",
57
- "vite": "^5.3.3",
57
+ "vite": "^5.4.12",
58
58
  "vite-plugin-dts": "^4.3.0",
59
59
  "vite-tsconfig-paths": "^4.2.0"
60
60
  },
@@ -7,18 +7,27 @@ export const BACKGROUND_COLORS = {
7
7
  [BadgeType.PrimarySolid]: 'bg-primary-30',
8
8
  [BadgeType.Neutral]: 'bg-neutral-92 dark:bg-neutral-12',
9
9
  [BadgeType.PrimarySoft]: 'bg-primary-90 dark:bg-primary-10',
10
+ [BadgeType.Success]: 'bg-tertiary-90 dark:bg-tertiary-10',
11
+ [BadgeType.Warning]: 'bg-warning-90 dark:bg-warning-20',
12
+ [BadgeType.Error]: 'bg-error-90 dark:bg-error-10',
10
13
  };
11
14
 
12
15
  export const TEXT_COLORS: Record<BadgeType, string> = {
13
16
  [BadgeType.PrimarySolid]: 'text-primary-100',
14
17
  [BadgeType.Neutral]: 'text-neutral-10 dark:text-neutral-92',
15
18
  [BadgeType.PrimarySoft]: 'text-primary-20 dark:text-primary-90',
19
+ [BadgeType.Success]: 'text-tertiary-20 dark:text-tertiary-90',
20
+ [BadgeType.Warning]: 'text-warning-10 dark:text-warning-90',
21
+ [BadgeType.Error]: 'text-error-20 dark:text-error-90',
16
22
  };
17
23
 
18
24
  export const BORDER_COLORS: Record<BadgeType, string> = {
19
25
  [BadgeType.PrimarySolid]: 'border-primary-30',
20
26
  [BadgeType.Neutral]: 'border-neutral-92 dark:border-neutral-12',
21
27
  [BadgeType.PrimarySoft]: 'border-primary-90 dark:border-primary-10',
28
+ [BadgeType.Success]: 'border-tertiary-90 dark:border-tertiary-10',
29
+ [BadgeType.Warning]: 'border-warning-90 dark:border-warning-20',
30
+ [BadgeType.Error]: 'border-error-90 dark:border-error-10',
22
31
  };
23
32
 
24
33
  export const BADGE_TEXT_CLASS = 'text-label-md';
@@ -5,4 +5,7 @@ export enum BadgeType {
5
5
  Neutral = 'neutral',
6
6
  PrimarySolid = 'primary-solid',
7
7
  PrimarySoft = 'primary-fill',
8
+ Success = 'success',
9
+ Warning = 'warning',
10
+ Error = 'error',
8
11
  }
@@ -18,7 +18,7 @@ export const BACKGROUND_COLORS: Record<ButtonType, string> = {
18
18
  [ButtonType.Secondary]: 'bg-neutral-90 dark:bg-neutral-20',
19
19
  [ButtonType.Ghost]: 'bg-transparent',
20
20
  [ButtonType.Outlined]: 'bg-transparent border border-neutral-50',
21
- [ButtonType.Destructive]: 'bg-error-90',
21
+ [ButtonType.Destructive]: 'bg-error-90 dark:bg-error-20',
22
22
  };
23
23
 
24
24
  export const DISABLED_BACKGROUND_COLORS: Record<ButtonType, string> = {
@@ -26,7 +26,7 @@ export const DISABLED_BACKGROUND_COLORS: Record<ButtonType, string> = {
26
26
  [ButtonType.Secondary]: 'bg-neutral-90 dark:bg-neutral-20',
27
27
  [ButtonType.Ghost]: 'bg-transparent',
28
28
  [ButtonType.Outlined]: 'bg-transparent border border-neutral-50',
29
- [ButtonType.Destructive]: 'bg-error-90',
29
+ [ButtonType.Destructive]: 'bg-error-90 dark:bg-error-20',
30
30
  };
31
31
 
32
32
  const DEFAULT_TEXT_COLORS: string = 'text-neutral-10 dark:text-neutral-92';
@@ -36,7 +36,7 @@ export const TEXT_COLORS: Record<ButtonType, string> = {
36
36
  [ButtonType.Secondary]: DEFAULT_TEXT_COLORS,
37
37
  [ButtonType.Ghost]: DEFAULT_TEXT_COLORS,
38
38
  [ButtonType.Outlined]: DEFAULT_TEXT_COLORS,
39
- [ButtonType.Destructive]: 'text-error-20',
39
+ [ButtonType.Destructive]: 'text-error-20 dark:text-error-90',
40
40
  };
41
41
 
42
42
  export const TEXT_CLASSES: Record<ButtonSize, string> = {
@@ -49,5 +49,5 @@ export const TEXT_COLOR_DISABLED: Record<ButtonType, string> = {
49
49
  [ButtonType.Secondary]: DEFAULT_TEXT_COLORS,
50
50
  [ButtonType.Ghost]: DEFAULT_TEXT_COLORS,
51
51
  [ButtonType.Outlined]: DEFAULT_TEXT_COLORS,
52
- [ButtonType.Destructive]: 'text-error-20',
52
+ [ButtonType.Destructive]: 'text-error-20 dark:text-error-90',
53
53
  };
@@ -20,3 +20,4 @@ export * from './visual-asset-card';
20
20
  export * from './loading-indicator';
21
21
  export * from './placeholder';
22
22
  export * from './skeleton';
23
+ export * from './toggle';
@@ -17,7 +17,7 @@ export interface InfoBoxProps {
17
17
  /**
18
18
  * The supporting text of the info box (optional).
19
19
  */
20
- supportingText?: string;
20
+ supportingText?: React.ReactNode;
21
21
  /**
22
22
  * The type of the info box.
23
23
  */
@@ -4,11 +4,16 @@
4
4
  import { InfoBoxType } from './infoBox.enums';
5
5
 
6
6
  export const ICON_COLORS: Record<InfoBoxType, string> = {
7
- [InfoBoxType.Default]: 'bg-neutral-96 dark:bg-neutral-12 text-neutral-10 dark:text-neutral-92',
7
+ [InfoBoxType.Default]: 'bg-primary-90 dark:bg-primary-10 text-primary-20 dark:text-primary-90',
8
8
  [InfoBoxType.Error]: 'bg-error-90 dark:bg-error-10 text-error-20 dark:text-error-90',
9
+ [InfoBoxType.Success]:
10
+ 'bg-tertiary-90 dark:bg-tertiary-10 text-tertiary-20 dark:text-tertiary-90',
11
+ [InfoBoxType.Warning]: 'bg-warning-90 dark:bg-warning-20 text-warning-10 dark:text-warning-90',
9
12
  };
10
13
 
11
14
  export const BACKGROUND_COLORS: Record<InfoBoxType, string> = {
12
- [InfoBoxType.Default]: 'bg-neutral-96 dark:bg-neutral-12',
15
+ [InfoBoxType.Default]: 'bg-primary-90 dark:bg-primary-10',
13
16
  [InfoBoxType.Error]: 'bg-error-90 dark:bg-error-10',
17
+ [InfoBoxType.Success]: 'bg-tertiary-90 dark:bg-tertiary-10',
18
+ [InfoBoxType.Warning]: 'bg-warning-90 dark:bg-warning-20',
14
19
  };
@@ -4,6 +4,8 @@
4
4
  export enum InfoBoxType {
5
5
  Default = 'default',
6
6
  Error = 'error',
7
+ Success = 'success',
8
+ Warning = 'warning',
7
9
  }
8
10
 
9
11
  export enum InfoBoxStyle {
@@ -6,9 +6,13 @@ import { SnackbarType } from './snackbar.enums';
6
6
  export const TEXT_COLOR: Record<SnackbarType, string> = {
7
7
  [SnackbarType.Default]: 'text-neutral-10 dark:text-neutral-92',
8
8
  [SnackbarType.Error]: 'text-error-20 dark:text-error-90',
9
+ [SnackbarType.Warning]: 'text-warning-10 dark:text-warning-90',
10
+ [SnackbarType.Success]: 'text-tertiary-20 dark:text-tertiary-90',
9
11
  };
10
12
 
11
13
  export const BACKGROUND_COLOR: Record<SnackbarType, string> = {
12
- [SnackbarType.Default]: 'bg-neutral-80 dark:bg-neutral-30',
14
+ [SnackbarType.Default]: 'bg-primary-90 dark:bg-primary-10',
13
15
  [SnackbarType.Error]: 'bg-error-90 dark:bg-error-10',
16
+ [SnackbarType.Warning]: 'bg-warning-90 dark:bg-warning-20',
17
+ [SnackbarType.Success]: 'bg-tertiary-90 dark:bg-tertiary-10',
14
18
  };
@@ -4,4 +4,6 @@
4
4
  export enum SnackbarType {
5
5
  Default = 'default',
6
6
  Error = 'error',
7
+ Warning = 'warning',
8
+ Success = 'success',
7
9
  }
@@ -0,0 +1,140 @@
1
+ // Copyright (c) 2025 IOTA Stiftung
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { forwardRef, useRef } from 'react';
5
+ import { ToggleLabelPosition, ToggleSize } from './toggle.enums';
6
+ import {
7
+ TOGGLE,
8
+ TOGGLE_LABEL,
9
+ TOGGLE_SIZE,
10
+ TOGGLE_STATES,
11
+ TOGGLE_THUMB,
12
+ TOGGLE_THUMB_COLOR,
13
+ TOGGLE_THUMB_POSITION,
14
+ TOGGLE_THUMB_SIZE,
15
+ TOGGLE_CONTAINER,
16
+ } from './toggle.classes';
17
+ import cx from 'classnames';
18
+
19
+ interface ToggleProps {
20
+ /**
21
+ * The label for the toggle.
22
+ */
23
+ label?: string | React.ReactNode;
24
+ /**
25
+ * The state of the toggle (on or off).
26
+ */
27
+ isToggled: boolean;
28
+ /**
29
+ * Whether the label should be placed before the toggle.
30
+ */
31
+ labelPosition?: ToggleLabelPosition;
32
+ /**
33
+ * If true, the toggle will be disabled.
34
+ */
35
+ isDisabled?: boolean;
36
+ /**
37
+ * The callback to call when the toggle state changes.
38
+ */
39
+ onChange?: (isToggled: boolean, event: React.ChangeEvent<HTMLInputElement>) => void;
40
+ /**
41
+ * The name and id of the toggle input.
42
+ */
43
+ name?: string;
44
+ /**
45
+ * The size of the toggle.
46
+ */
47
+ size?: ToggleSize;
48
+ /**
49
+ * The 'data-testid' attribute value (used in e2e tests)
50
+ */
51
+ testId?: string;
52
+ }
53
+
54
+ export const Toggle = forwardRef<HTMLInputElement, ToggleProps>(
55
+ (
56
+ {
57
+ label,
58
+ isToggled,
59
+ labelPosition = ToggleLabelPosition.Right,
60
+ isDisabled = false,
61
+ onChange,
62
+ name,
63
+ size = ToggleSize.Default,
64
+ testId,
65
+ }: ToggleProps,
66
+ ref,
67
+ ) => {
68
+ const inputRef = useRef<HTMLInputElement | null>(null);
69
+
70
+ function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
71
+ const newChecked = e.target.checked;
72
+ onChange?.(newChecked, e);
73
+ }
74
+
75
+ function assignRefs(element: HTMLInputElement) {
76
+ if (ref) {
77
+ if (typeof ref === 'function') {
78
+ ref(element);
79
+ } else {
80
+ ref.current = element;
81
+ }
82
+ }
83
+ inputRef.current = element;
84
+ }
85
+
86
+ const toggleClasses = cx(TOGGLE, {
87
+ [TOGGLE_STATES.active]: isToggled && !isDisabled,
88
+ [TOGGLE_STATES.inactive]: !isToggled,
89
+ [TOGGLE_STATES.disabledActive]: isDisabled && isToggled,
90
+ [TOGGLE_STATES.disabled]: isDisabled,
91
+ [TOGGLE_SIZE[size]]: true,
92
+ });
93
+
94
+ const thumbClasses = cx(TOGGLE_THUMB, {
95
+ [TOGGLE_THUMB_POSITION[size].unchecked]: !isToggled,
96
+ [TOGGLE_THUMB_POSITION[size].checked]: isToggled,
97
+ [TOGGLE_THUMB_COLOR.unchecked]: !isToggled,
98
+ [TOGGLE_THUMB_COLOR.checked]: isToggled,
99
+ [TOGGLE_THUMB_SIZE[size]]: true,
100
+ });
101
+
102
+ const containerClasses = cx(TOGGLE_CONTAINER, {
103
+ 'flex-row-reverse': labelPosition === ToggleLabelPosition.Left,
104
+ 'cursor-not-allowed': isDisabled,
105
+ });
106
+ const labelClasses = cx(TOGGLE_LABEL, {
107
+ 'opacity-40': isDisabled && !isToggled,
108
+ });
109
+
110
+ return (
111
+ <div className={containerClasses}>
112
+ <input
113
+ id={name}
114
+ name={name}
115
+ type="checkbox"
116
+ className="sr-only"
117
+ checked={isToggled}
118
+ ref={assignRefs}
119
+ disabled={isDisabled}
120
+ onChange={handleChange}
121
+ data-testid={testId}
122
+ />
123
+
124
+ <span
125
+ role="switch"
126
+ onClick={() => inputRef.current?.click()}
127
+ className={toggleClasses}
128
+ >
129
+ <span className={thumbClasses} />
130
+ </span>
131
+
132
+ {label && (
133
+ <label htmlFor={name} className={labelClasses}>
134
+ {label}
135
+ </label>
136
+ )}
137
+ </div>
138
+ );
139
+ },
140
+ );
@@ -0,0 +1,6 @@
1
+ // Copyright (c) 2025 IOTA Stiftung
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ export { Toggle } from './Toggle';
5
+
6
+ export * from './toggle.enums';
@@ -0,0 +1,41 @@
1
+ // Copyright (c) 2025 IOTA Stiftung
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { ToggleSize } from './toggle.enums';
5
+
6
+ export const TOGGLE = 'relative inline-flex items-center p-xxs border rounded-full cursor-pointer';
7
+
8
+ export const TOGGLE_STATES = {
9
+ active: 'bg-primary-30 border-primary-30',
10
+ inactive: 'bg-primary-100 dark:bg-neutral-6 border-neutral-70 dark:border-neutral-40',
11
+ disabledActive: 'bg-neutral-70 border-neutral-70',
12
+ disabled: 'opacity-40 cursor-not-allowed',
13
+ };
14
+
15
+ export const TOGGLE_SIZE = {
16
+ [ToggleSize.Small]: 'h-5 w-10',
17
+ [ToggleSize.Default]: 'h-6 w-12',
18
+ };
19
+
20
+ export const TOGGLE_THUMB = 'absolute rounded-full transition-all duration-200 ease-in';
21
+ export const TOGGLE_THUMB_POSITION = {
22
+ [ToggleSize.Small]: {
23
+ unchecked: 'left-1',
24
+ checked: 'left-8 -translate-x-full',
25
+ },
26
+ [ToggleSize.Default]: {
27
+ unchecked: 'left-1',
28
+ checked: 'left-10 -translate-x-full',
29
+ },
30
+ };
31
+ export const TOGGLE_THUMB_COLOR = {
32
+ unchecked: 'bg-neutral-60',
33
+ checked: 'bg-white',
34
+ };
35
+ export const TOGGLE_THUMB_SIZE = {
36
+ [ToggleSize.Small]: 'h-3 w-3',
37
+ [ToggleSize.Default]: 'h-4 w-4',
38
+ };
39
+
40
+ export const TOGGLE_CONTAINER = 'inline-flex items-center gap-2';
41
+ export const TOGGLE_LABEL = 'text-label-lg text-neutral-40 dark:text-neutral-60';
@@ -0,0 +1,12 @@
1
+ // Copyright (c) 2025 IOTA Stiftung
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ export enum ToggleSize {
5
+ Small = 'small',
6
+ Default = 'default',
7
+ }
8
+
9
+ export enum ToggleLabelPosition {
10
+ Left = 'left',
11
+ Right = 'right',
12
+ }
@@ -9,11 +9,13 @@ import { TOOLTIP_POSITION } from './tooltip.classes';
9
9
  interface TooltipProps {
10
10
  text: string;
11
11
  position?: TooltipPosition;
12
+ maxWidth?: string;
12
13
  }
13
14
 
14
15
  export function Tooltip({
15
16
  text,
16
17
  position = TooltipPosition.Top,
18
+ maxWidth = 'max-w-[200px]',
17
19
  children,
18
20
  }: PropsWithChildren<TooltipProps>): React.JSX.Element {
19
21
  const tooltipPositionClass = TOOLTIP_POSITION[position];
@@ -22,8 +24,9 @@ export function Tooltip({
22
24
  {children}
23
25
  <div
24
26
  className={cx(
25
- 'absolute z-[999] hidden w-max max-w-[200px] rounded bg-neutral-80 p-xs text-neutral-10 opacity-0 transition-opacity duration-300 group-hover:block group-hover:opacity-100 group-focus:opacity-100 dark:bg-neutral-30 dark:text-neutral-92',
27
+ 'absolute z-[999] hidden w-max rounded bg-neutral-80 p-xs text-neutral-10 opacity-0 transition-opacity duration-300 group-hover:block group-hover:opacity-100 group-focus:opacity-100 dark:bg-neutral-30 dark:text-neutral-92',
26
28
  tooltipPositionClass,
29
+ maxWidth,
27
30
  )}
28
31
  role="tooltip"
29
32
  >
@@ -3,8 +3,9 @@
3
3
 
4
4
  import { VisualAssetType } from './visualAssetCard.enums';
5
5
  import { ButtonUnstyled } from '../button';
6
- import { MoreHoriz } from '@iota/apps-ui-icons';
6
+ import { MoreHoriz, PlaceholderReplace } from '@iota/apps-ui-icons';
7
7
  import cx from 'classnames';
8
+ import { useState } from 'react';
8
9
 
9
10
  export interface VisualAssetCardProps {
10
11
  /**
@@ -51,6 +52,8 @@ export function VisualAssetCard({
51
52
  assetTitle,
52
53
  isHoverable = true,
53
54
  }: VisualAssetCardProps): React.JSX.Element {
55
+ const [imageError, setImageError] = useState(false);
56
+
54
57
  const handleIconClick = (event: React.MouseEvent<HTMLButtonElement>) => {
55
58
  onIconClick?.(event);
56
59
  event?.stopPropagation();
@@ -65,28 +68,33 @@ export function VisualAssetCard({
65
68
  >
66
69
  {assetType === VisualAssetType.Video ? (
67
70
  <video src={assetSrc} className="h-full w-full object-cover" autoPlay loop muted />
71
+ ) : imageError ? (
72
+ <div className="flex h-full w-full items-center justify-center bg-neutral-96 dark:bg-neutral-10">
73
+ <PlaceholderReplace className="h-4 w-4 text-neutral-40 dark:text-neutral-60" />
74
+ </div>
68
75
  ) : (
69
- <img src={assetSrc} alt={altText} className="h-full w-full object-cover" />
76
+ <img
77
+ src={assetSrc}
78
+ alt={altText}
79
+ className="h-full w-full object-cover"
80
+ onError={() => setImageError(true)}
81
+ />
70
82
  )}
71
83
  {isHoverable && (
72
84
  <div className="absolute left-0 top-0 h-full w-full bg-cover bg-center bg-no-repeat group-hover:bg-shader-neutral-light-48 group-hover:transition group-hover:duration-300 group-hover:ease-in-out group-hover:dark:bg-shader-primary-dark-48" />
73
85
  )}
74
- {isHoverable && (
75
- <>
76
- {onIconClick && (
77
- <ButtonUnstyled
78
- className="absolute right-2 top-2 h-9 w-9 cursor-pointer rounded-full p-xs opacity-0 transition-opacity duration-300 group-hover:bg-shader-neutral-light-72 group-hover:opacity-100 [&_svg]:h-5 [&_svg]:w-5 [&_svg]:text-primary-100"
79
- onClick={handleIconClick}
80
- >
81
- {icon}
82
- </ButtonUnstyled>
83
- )}
84
- <div className="absolute bottom-0 flex items-center justify-center p-xs opacity-0 transition-opacity duration-300 group-hover:opacity-100">
85
- {assetTitle && (
86
- <span className="text-title-md text-neutral-100">{assetTitle}</span>
87
- )}
88
- </div>
89
- </>
86
+ {isHoverable && onIconClick && (
87
+ <ButtonUnstyled
88
+ className="absolute right-2 top-2 h-9 w-9 cursor-pointer rounded-full p-xs opacity-0 transition-opacity duration-300 group-hover:bg-shader-neutral-light-72 group-hover:opacity-100 [&_svg]:h-5 [&_svg]:w-5 [&_svg]:text-primary-100"
89
+ onClick={handleIconClick}
90
+ >
91
+ {icon}
92
+ </ButtonUnstyled>
93
+ )}
94
+ {isHoverable && assetTitle && (
95
+ <div className="absolute bottom-0 flex items-center justify-center p-xs opacity-0 transition-opacity duration-300 group-hover:opacity-100">
96
+ <span className="text-title-md text-neutral-100">{assetTitle}</span>
97
+ </div>
90
98
  )}
91
99
  </div>
92
100
  );
@@ -70,7 +70,7 @@ export function Address({
70
70
  function handleOpenClick(event: React.MouseEvent<HTMLButtonElement>) {
71
71
  event?.stopPropagation();
72
72
  if (externalLink) {
73
- const newWindow = window.open(externalLink, '_blank', 'noopener,noreferrer');
73
+ const newWindow = window.open(externalLink, '_blank', 'noopener noreferrer');
74
74
  if (newWindow) newWindow.opener = null;
75
75
  } else {
76
76
  onOpen?.(event);
@@ -27,8 +27,10 @@ export function CardAction({
27
27
  buttonDisabled,
28
28
  }: CardActionProps) {
29
29
  function handleActionClick(event: React.MouseEvent) {
30
- event?.stopPropagation();
31
- onClick?.();
30
+ if (onClick) {
31
+ event.stopPropagation();
32
+ onClick();
33
+ }
32
34
  }
33
35
 
34
36
  if (type === CardActionType.Link) {
@@ -3,6 +3,7 @@
3
3
  import cx from 'classnames';
4
4
 
5
5
  import { ReactNode } from 'react';
6
+ import { Tooltip, TooltipPosition } from '../../atoms';
6
7
 
7
8
  export type CardBodyProps = {
8
9
  title: string;
@@ -10,6 +11,8 @@ export type CardBodyProps = {
10
11
  clickableAction?: React.ReactNode;
11
12
  icon?: React.ReactNode;
12
13
  isTextTruncated?: boolean;
14
+ tooltipText?: string;
15
+ tooltipPosition?: TooltipPosition;
13
16
  };
14
17
 
15
18
  export function CardBody({
@@ -18,6 +21,8 @@ export function CardBody({
18
21
  clickableAction,
19
22
  icon,
20
23
  isTextTruncated,
24
+ tooltipText,
25
+ tooltipPosition,
21
26
  }: CardBodyProps) {
22
27
  const handleActionCardBodyClick = (event: React.MouseEvent) => {
23
28
  event?.stopPropagation();
@@ -40,7 +45,13 @@ export function CardBody({
40
45
  >
41
46
  {title}
42
47
  </div>
43
- {icon && <div className="flex items-center">{icon}</div>}
48
+ {tooltipText ? (
49
+ <Tooltip text={tooltipText} position={tooltipPosition}>
50
+ {icon}
51
+ </Tooltip>
52
+ ) : (
53
+ <div className="flex items-center">{icon}</div>
54
+ )}
44
55
  {clickableAction && (
45
56
  <div onClick={handleActionCardBodyClick} className="flex items-center">
46
57
  {clickableAction}
@@ -15,6 +15,7 @@ export const IMAGE_VARIANT_CLASSES: { [key in ImageShape]: string } = {
15
15
  export const IMAGE_BG_CLASSES: { [key in ImageType]: string } = {
16
16
  [ImageType.Placeholder]: '',
17
17
  [ImageType.BgSolid]: 'bg-neutral-96 dark:bg-neutral-10',
18
+ [ImageType.BgWhite]: 'bg-white dark:bg-neutral-6',
18
19
  [ImageType.BgTransparent]: '',
19
20
  };
20
21
 
@@ -10,6 +10,7 @@ export enum CardType {
10
10
  export enum ImageType {
11
11
  Placeholder = 'placeholder',
12
12
  BgSolid = 'bg-solid',
13
+ BgWhite = 'bg-white',
13
14
  BgTransparent = 'bg-transparent',
14
15
  }
15
16
 
@@ -5,9 +5,9 @@ export const INPUT_CLASSES =
5
5
  'w-full bg-transparent text-body-lg caret-primary-30 dark:caret-primary-80 focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed';
6
6
  export const INPUT_TEXT_CLASSES = 'text-neutral-10 dark:text-neutral-92';
7
7
  export const INPUT_PLACEHOLDER_CLASSES =
8
- ' enabled:placeholder:text-neutral-40/40 dark:placeholder:text-neutral-60/40 enabled:dark:placeholder:text-neutral-60/40';
8
+ 'enabled:placeholder:text-neutral-40/40 dark:placeholder:text-neutral-60/40 enabled:dark:placeholder:text-neutral-60/40';
9
9
  export const BORDER_CLASSES =
10
- 'px-md py-sm rounded-lg border border-neutral-80 group-[.enabled]:cursor-text group-[.errored]:border-error-30 hover:group-[.enabled]:border-neutral-50 dark:border-neutral-60 dark:hover:border-neutral-60 dark:group-[.errored]:border-error-80 [&:has(input:focus)]:border-primary-30 [&:has(input:focus)]:dark:border-primary-80';
10
+ 'px-md py-sm rounded-lg border border-neutral-80 group-[.enabled]:cursor-text group-[.errored]:border-error-30 hover:group-[.enabled]:border-neutral-50 dark:border-neutral-20 dark:hover:border-neutral-60 dark:group-[.errored]:border-error-80 [&:has(input:focus)]:!border-primary-30 [&:has(input:focus)]:dark:!border-primary-80';
11
11
  export const INPUT_NUMBER_CLASSES =
12
12
  '[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none disabled:cursor-not-allowed';
13
13
  export const LABEL_CLASSES =
@@ -54,7 +54,7 @@ export interface SearchProps {
54
54
  /**
55
55
  * Render suggestion.
56
56
  */
57
- renderSuggestion: (suggestion: Suggestion, index: number) => React.ReactNode;
57
+ renderSuggestion?: (suggestion: Suggestion, index: number) => React.ReactNode;
58
58
  }
59
59
 
60
60
  export function Search({
@@ -71,6 +71,7 @@ export function Search({
71
71
  const inputRef = useRef<HTMLInputElement>(null);
72
72
  const suggestionsListRef = useRef<HTMLDivElement>(null);
73
73
  const [isSuggestionsVisible, setIsSuggestionsVisible] = useState(true);
74
+ const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
74
75
 
75
76
  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
76
77
  const value = event.target.value;
@@ -131,9 +132,36 @@ export function Search({
131
132
  onSuggestionClick?.(suggestion);
132
133
  onSearchValueChange('');
133
134
  setIsSuggestionsVisible(false);
135
+ setSelectedIndex(null);
134
136
  inputRef.current?.blur();
135
137
  };
136
138
 
139
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
140
+ if (suggestions && suggestions?.length > 0) {
141
+ if (event.key === 'ArrowDown') {
142
+ event.preventDefault();
143
+ setSelectedIndex((prev) =>
144
+ prev === null || prev >= suggestions.length - 1 ? 0 : prev + 1,
145
+ );
146
+ } else if (event.key === 'ArrowUp') {
147
+ event.preventDefault();
148
+ setSelectedIndex((prev) =>
149
+ prev === null || prev <= 0 ? suggestions.length - 1 : prev - 1,
150
+ );
151
+ } else if (event.key === 'Enter') {
152
+ event.preventDefault();
153
+ if (selectedIndex !== null && suggestions[selectedIndex]) {
154
+ handleOnSuggestionClick(suggestions[selectedIndex]);
155
+ } else if (suggestions.length === 1) {
156
+ handleOnSuggestionClick(suggestions[0]);
157
+ }
158
+ }
159
+ }
160
+ if (onKeyDown) {
161
+ onKeyDown(event);
162
+ }
163
+ };
164
+
137
165
  return (
138
166
  <div className="relative w-full">
139
167
  <div
@@ -148,7 +176,7 @@ export function Search({
148
176
  type="text"
149
177
  value={searchValue}
150
178
  onChange={handleChange}
151
- onKeyDown={onKeyDown}
179
+ onKeyDown={handleKeyDown}
152
180
  onFocus={() => setIsSuggestionsVisible(true)}
153
181
  placeholder={placeholder}
154
182
  className={cx(
@@ -158,7 +186,7 @@ export function Search({
158
186
  />
159
187
  <SearchIcon />
160
188
  </div>
161
- {showSuggestions && (
189
+ {showSuggestions && renderSuggestion && (
162
190
  <div
163
191
  ref={suggestionsListRef}
164
192
  className={cx(
@@ -168,15 +196,21 @@ export function Search({
168
196
  >
169
197
  <Divider width="w-11/12" />
170
198
  {isLoading ? (
171
- <div className=" px-md py-sm">
199
+ <div className="px-md py-sm">
172
200
  <Loader className="animate-spin" />
173
201
  </div>
174
202
  ) : (
175
203
  suggestions.map((suggestion, index) => (
176
204
  <div
177
- className="w-full"
178
205
  key={suggestion.id}
179
206
  onClick={() => handleOnSuggestionClick(suggestion)}
207
+ onMouseEnter={() => setSelectedIndex(index)}
208
+ className={cx(
209
+ 'w-full cursor-pointer px-md py-sm',
210
+ selectedIndex === index
211
+ ? 'bg-shader-primary-dark-16 dark:bg-shader-inverted-dark-16'
212
+ : '',
213
+ )}
180
214
  >
181
215
  {renderSuggestion(suggestion, index)}
182
216
  </div>