@iota/apps-ui-kit 0.12.0 → 0.13.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iota/apps-ui-kit",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "sideEffects": false,
5
5
  "repository": "https://github.com/iotaledger/ts-packages",
6
6
  "license": "Apache-2.0",
@@ -27,7 +27,7 @@
27
27
  "classnames": "^2.5.1",
28
28
  "lodash.merge": "^4.6.2",
29
29
  "react-number-format": "^5.2.2",
30
- "@iota/apps-ui-icons": "0.7.5"
30
+ "@iota/apps-ui-icons": "0.7.6"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": "^18 || ^19",
@@ -58,12 +58,6 @@
58
58
  "vite-plugin-dts": "^4.5.4"
59
59
  },
60
60
  "scripts": {
61
- "prettier:check": "prettier -c --ignore-unknown --ignore-path=../../.prettierignore --ignore-path=.prettierignore .",
62
- "prettier:fix": "prettier -w --ignore-unknown --ignore-path=../../.prettierignore --ignore-path=.prettierignore .",
63
- "eslint:check": "eslint --max-warnings=0 .",
64
- "eslint:fix": "pnpm run eslint:check --fix",
65
- "lint": "pnpm run eslint:check && pnpm run prettier:check",
66
- "lint:fix": "pnpm run eslint:fix && pnpm run prettier:fix",
67
61
  "storybook": "storybook dev -p 6006",
68
62
  "build-storybook": "storybook build",
69
63
  "build-package": "tsc --project tsconfig.package.json && vite build",
@@ -33,9 +33,8 @@ export function Divider({
33
33
  lineHeight = DEFAULT_LINE_HEIGHT,
34
34
  }: DividerProps): React.JSX.Element {
35
35
  // Set height and width of divider line based on type
36
- const lineStyle = {
37
- ...(type === DividerType.Horizontal ? { height: lineHeight } : { width: lineHeight }),
38
- };
36
+ const lineStyle =
37
+ type === DividerType.Horizontal ? { height: lineHeight } : { width: lineHeight };
39
38
 
40
39
  let dividerSize = DIVIDER_FULL_WIDTH[type];
41
40
  if (width && type === DividerType.Horizontal) {
@@ -59,7 +59,7 @@ export function InfoBox({
59
59
  {icon}
60
60
  </span>
61
61
  )}
62
- <div className="flex flex-col gap-1">
62
+ <div className="flex min-w-0 flex-col gap-1 [overflow-wrap:anywhere]">
63
63
  {title && <span className="infobox-text-title text-title-sm">{title}</span>}
64
64
  {supportingText && (
65
65
  <span className="infobox-supporting-text text-body-sm">{supportingText}</span>
@@ -119,12 +119,12 @@ export function KeyValueInfo({
119
119
  )}
120
120
  >
121
121
  <div
122
- className={cx('flex shrink-0 flex-row items-center gap-x-0.5', {
122
+ className={cx('flex max-w-full shrink-0 flex-row items-center gap-x-0.5', {
123
123
  'w-1/4': !fullwidth && !isReceiptLayout,
124
124
  })}
125
125
  >
126
126
  {keyIcon}
127
- <span className="key-value-key-text-color break-normal text-body-md">
127
+ <span className="key-value-key-text-color min-w-0 break-words text-body-md">
128
128
  {keyText}
129
129
  </span>
130
130
  {tooltipText && (
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import { useRef, useState, useLayoutEffect } from 'react';
5
- import type { PropsWithChildren, ReactNode } from 'react';
5
+ import type { MouseEvent, PropsWithChildren, ReactNode } from 'react';
6
6
  import { createPortal } from 'react-dom';
7
7
  import cx from 'classnames';
8
8
  import { TooltipPosition } from './tooltip.enums';
@@ -16,6 +16,13 @@ interface TooltipProps {
16
16
  closeDelay?: number;
17
17
  }
18
18
 
19
+ const OPPOSITE_POSITION: Record<TooltipPosition, TooltipPosition> = {
20
+ [TooltipPosition.Top]: TooltipPosition.Bottom,
21
+ [TooltipPosition.Bottom]: TooltipPosition.Top,
22
+ [TooltipPosition.Left]: TooltipPosition.Right,
23
+ [TooltipPosition.Right]: TooltipPosition.Left,
24
+ };
25
+
19
26
  export function Tooltip({
20
27
  text,
21
28
  position = TooltipPosition.Top,
@@ -29,7 +36,7 @@ export function Tooltip({
29
36
  const tooltipRef = useRef<HTMLDivElement>(null);
30
37
 
31
38
  const [visible, setVisible] = useState(false);
32
- const [coords, setCoords] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
39
+ const [coords, setCoords] = useState({ top: 0, left: 0, overlapsTrigger: false });
33
40
 
34
41
  const openTimer = useRef<ReturnType<typeof setTimeout>>();
35
42
  const closeTimer = useRef<ReturnType<typeof setTimeout>>();
@@ -49,7 +56,7 @@ export function Tooltip({
49
56
  closeTimer.current = setTimeout(() => setVisible(false), closeDelay);
50
57
  };
51
58
 
52
- const computePosition = (rect: DOMRect) =>
59
+ const computePosition = (rect: DOMRect, placement: TooltipPosition) =>
53
60
  ({
54
61
  [TooltipPosition.Top]: {
55
62
  top: rect.top - offset,
@@ -79,7 +86,7 @@ export function Tooltip({
79
86
  anchorX: 0,
80
87
  anchorY: 0.5,
81
88
  },
82
- })[position];
89
+ })[placement];
83
90
 
84
91
  const clampDelta = (boxStart: number, size: number, viewportSize: number, margin: number) => {
85
92
  const boxEnd = boxStart + size;
@@ -95,11 +102,23 @@ export function Tooltip({
95
102
  const tooltipEl = tooltipRef.current;
96
103
  if (!rect || !tooltipEl) return;
97
104
 
98
- const pos = computePosition(rect);
99
- tooltipEl.style.transform = pos.transform;
100
-
101
105
  const { width, height } = tooltipEl.getBoundingClientRect();
102
106
  const margin = 8;
107
+ const isVertical = position === TooltipPosition.Top || position === TooltipPosition.Bottom;
108
+
109
+ const fitsMainAxis = (pos: ReturnType<typeof computePosition>) =>
110
+ isVertical
111
+ ? clampDelta(pos.top - height * pos.anchorY, height, window.innerHeight, margin) ===
112
+ 0
113
+ : clampDelta(pos.left - width * pos.anchorX, width, window.innerWidth, margin) ===
114
+ 0;
115
+
116
+ let pos = computePosition(rect, position);
117
+ if (!fitsMainAxis(pos)) {
118
+ const flipped = computePosition(rect, OPPOSITE_POSITION[position]);
119
+ if (fitsMainAxis(flipped)) pos = flipped;
120
+ }
121
+ tooltipEl.style.transform = pos.transform;
103
122
 
104
123
  const boxLeft = pos.left - width * pos.anchorX;
105
124
  const boxTop = pos.top - height * pos.anchorY;
@@ -107,7 +126,15 @@ export function Tooltip({
107
126
  const deltaX = clampDelta(boxLeft, width, window.innerWidth, margin);
108
127
  const deltaY = clampDelta(boxTop, height, window.innerHeight, margin);
109
128
 
110
- setCoords({ top: pos.top + deltaY, left: pos.left + deltaX });
129
+ const left = boxLeft + deltaX;
130
+ const top = boxTop + deltaY;
131
+ const overlapsTrigger =
132
+ left < rect.right &&
133
+ left + width > rect.left &&
134
+ top < rect.bottom &&
135
+ top + height > rect.top;
136
+
137
+ setCoords({ top: pos.top + deltaY, left: pos.left + deltaX, overlapsTrigger });
111
138
  }, [visible, position, offset]);
112
139
 
113
140
  useLayoutEffect(() => {
@@ -124,6 +151,15 @@ export function Tooltip({
124
151
  };
125
152
  }, [visible]);
126
153
 
154
+ // Keeps a link or button wrapping the trigger from acting on clicks meant to open the tooltip.
155
+ const handleTriggerClick = (event: MouseEvent<HTMLDivElement>) => {
156
+ const interactive = (event.target as Element).closest('a, button');
157
+ if (!interactive || triggerRef.current?.contains(interactive)) return;
158
+ event.preventDefault();
159
+ event.stopPropagation();
160
+ open();
161
+ };
162
+
127
163
  // z-[9999999999]: needed because we must exceed the popup’s ≈2 147 483 647 z-index;
128
164
  // otherwise the tooltip renders but stays invisible inside a Chrome extension
129
165
  const base = 'z-[9999999999] w-max rounded p-xs tooltip-bg tooltip-text-color';
@@ -137,6 +173,7 @@ export function Tooltip({
137
173
  onFocus={open}
138
174
  onMouseLeave={close}
139
175
  onBlur={close}
176
+ onClick={handleTriggerClick}
140
177
  >
141
178
  {children}
142
179
  </div>
@@ -153,7 +190,11 @@ export function Tooltip({
153
190
  transition: 'opacity .15s ease',
154
191
  opacity: 1,
155
192
  }}
156
- className={cx(base, maxWidth)}
193
+ className={cx(
194
+ base,
195
+ maxWidth,
196
+ coords.overlapsTrigger && 'pointer-events-none',
197
+ )}
157
198
  onMouseEnter={open}
158
199
  onMouseLeave={close}
159
200
  >
@@ -51,7 +51,7 @@ export function CardAction({
51
51
  <div className="card-action-title-color font-inter text-label-md">{title}</div>
52
52
  )}
53
53
  {subtitle && (
54
- <div className="card-action-subtitle-color font-inter text-label-sm">
54
+ <div className="card-action-subtitle-color flex flex-row justify-end font-inter text-label-sm">
55
55
  {subtitle}
56
56
  </div>
57
57
  )}
@@ -6,7 +6,7 @@ import { Tooltip } from '@/components/atoms/tooltip';
6
6
  import type { TooltipPosition } from '@/components/atoms/tooltip';
7
7
 
8
8
  export type CardBodyProps = {
9
- title: string;
9
+ title: ReactNode;
10
10
  subtitle?: string | ReactNode;
11
11
  clickableAction?: React.ReactNode;
12
12
  icon?: React.ReactNode;
@@ -17,8 +17,7 @@ export type SelectOption =
17
17
  | { id: string; label: React.ReactNode };
18
18
 
19
19
  interface SelectProps
20
- extends Pick<React.HTMLProps<HTMLSelectElement>, 'disabled'>,
21
- React.AriaAttributes {
20
+ extends Pick<React.HTMLProps<HTMLSelectElement>, 'disabled'>, React.AriaAttributes {
22
21
  /**
23
22
  * The selected option value.
24
23
  */
@@ -8,6 +8,7 @@ import type { ButtonProps } from '@/components/atoms';
8
8
  import { Button, ButtonSize, ButtonType, Checkbox } from '@/components/atoms';
9
9
  import { TableCellBase, TableHeaderCell } from '@/components/molecules';
10
10
  import { ArrowLeft, DoubleArrowLeft, ArrowRight, DoubleArrowRight } from '@iota/apps-ui-icons';
11
+ import { INTERACTIVE_ELEMENT_SELECTOR } from '@/lib/constants';
11
12
 
12
13
  export interface TablePaginationOptions {
13
14
  /**
@@ -147,7 +148,7 @@ export function Table({
147
148
  />
148
149
  </>
149
150
  )}
150
- {action && action}
151
+ {action}
151
152
  </div>
152
153
  )}
153
154
  {supportingLabel || pageSizeSelector ? (
@@ -174,19 +175,36 @@ export function TableHeader({ children }: PropsWithChildren): JSX.Element {
174
175
  return <thead>{children}</thead>;
175
176
  }
176
177
 
178
+ function isRowClickIgnored(event: React.MouseEvent<HTMLTableRowElement>): boolean {
179
+ const interactiveElement =
180
+ event.target instanceof Element && event.target.closest(INTERACTIVE_ELEMENT_SELECTOR);
181
+ if (interactiveElement && event.currentTarget.contains(interactiveElement)) return true;
182
+ return !!window.getSelection()?.toString();
183
+ }
184
+
177
185
  export function TableRow({
178
186
  children,
179
187
  leading,
180
188
  onClick,
189
+ onAuxClick,
181
190
  }: PropsWithChildren<{
182
191
  leading?: React.ReactNode;
183
192
  onClick?: React.MouseEventHandler<HTMLTableRowElement>;
193
+ onAuxClick?: React.MouseEventHandler<HTMLTableRowElement>;
184
194
  }>): JSX.Element {
195
+ function withRowClickGuard(handler?: React.MouseEventHandler<HTMLTableRowElement>) {
196
+ if (!handler) return undefined;
197
+ return (event: React.MouseEvent<HTMLTableRowElement>) => {
198
+ if (!isRowClickIgnored(event)) handler(event);
199
+ };
200
+ }
201
+
185
202
  return (
186
203
  <tr
187
- onClick={onClick}
204
+ onClick={withRowClickGuard(onClick)}
205
+ onAuxClick={withRowClickGuard(onAuxClick)}
188
206
  className={cx({
189
- 'cursor-pointer hover:bg-shader-neutral-light-8 dark:hover:bg-shader-neutral-dark-8':
207
+ 'cursor-pointer row-hover:bg-shader-neutral-light-8 dark:row-hover:bg-shader-neutral-dark-8 inner-link:-mx-xxs inner-link:rounded inner-link:px-xxs inner-link:transition-colors inner-link-hover:bg-iota-primary-90/30 inner-link-hover:ring-1 inner-link-hover:ring-iota-primary-70/60 dark:inner-link-hover:bg-iota-primary-70/30':
190
208
  onClick,
191
209
  })}
192
210
  >
@@ -2,3 +2,4 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  export * from './colors.constants';
5
+ export * from './interactive.constants';
@@ -0,0 +1,19 @@
1
+ // Copyright (c) 2026 IOTA Stiftung
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ export const INTERACTIVE_ELEMENT_SELECTOR = [
5
+ 'a',
6
+ 'button',
7
+ 'input',
8
+ 'select',
9
+ 'textarea',
10
+ 'label',
11
+ '[role="button"]',
12
+ '[role="checkbox"]',
13
+ '[role="link"]',
14
+ '[role="switch"]',
15
+ ].join(',');
16
+
17
+ export const ROW_LINK_ATTRIBUTE = 'data-row-link';
18
+
19
+ export const ROW_LINK_PROPS = { [ROW_LINK_ATTRIBUTE]: true } as const;
@@ -40,8 +40,9 @@
40
40
  }
41
41
 
42
42
  :root.names {
43
- --roboto-flex-variation-settings: 'GRAD' 0, 'XOPQ' 96, 'XTRA' 468, 'YOPQ' 79, 'YTAS' 750,
44
- 'YTDE' -203, 'YTFI' 738, 'YTLC' 514, 'YTUC' 640, 'slnt' 0, 'wdth' 130;
43
+ --roboto-flex-variation-settings:
44
+ 'GRAD' 0, 'XOPQ' 96, 'XTRA' 468, 'YOPQ' 79, 'YTAS' 750, 'YTDE' -203, 'YTFI' 738,
45
+ 'YTLC' 514, 'YTUC' 640, 'slnt' 0, 'wdth' 130;
45
46
 
46
47
  .text-display-lg,
47
48
  .text-display-md,
@@ -16,13 +16,13 @@ import {
16
16
  generateVariableSpacing,
17
17
  } from './constants';
18
18
  import { THEMED_CUSTOM_COLORS } from './constants/customColors.constants';
19
- import { firefoxPlugin, namesVariant } from './plugins';
19
+ import { firefoxPlugin, namesVariant, tableRowVariants } from './plugins';
20
20
  import { NAMES_GRADIENTS } from './constants/gradients.constants';
21
21
 
22
22
  export const BASE_CONFIG: Config = {
23
23
  content: ['./src/**/*.{html,js,jsx,ts,tsx}'],
24
24
  darkMode: 'selector',
25
- plugins: [firefoxPlugin, namesVariant],
25
+ plugins: [firefoxPlugin, namesVariant, tableRowVariants],
26
26
  theme: {
27
27
  extend: {
28
28
  fontSize: {
@@ -2,6 +2,10 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import plugin from 'tailwindcss/plugin';
5
+ import {
6
+ INTERACTIVE_ELEMENT_SELECTOR,
7
+ ROW_LINK_ATTRIBUTE,
8
+ } from '../constants/interactive.constants';
5
9
 
6
10
  export const firefoxPlugin = plugin(({ addVariant }) => {
7
11
  addVariant('firefox', '@-moz-document url-prefix()');
@@ -10,3 +14,13 @@ export const firefoxPlugin = plugin(({ addVariant }) => {
10
14
  export const namesVariant = plugin(({ addVariant }) => {
11
15
  addVariant('names', '&:is(.names *)');
12
16
  });
17
+
18
+ export const tableRowVariants = plugin(({ addVariant }) => {
19
+ const innerLink = `a:not([${ROW_LINK_ATTRIBUTE}])`;
20
+ addVariant(
21
+ 'row-hover',
22
+ `&:hover:not(:has(:is(${INTERACTIVE_ELEMENT_SELECTOR}):not([${ROW_LINK_ATTRIBUTE}]):hover))`,
23
+ );
24
+ addVariant('inner-link', `& ${innerLink}`);
25
+ addVariant('inner-link-hover', `& ${innerLink}:hover`);
26
+ });
@@ -12,7 +12,7 @@ const meta: Meta<typeof Panel> = {
12
12
  <Panel {...props}>
13
13
  <div className="flex flex-col items-start gap-2">
14
14
  <Title title="Title" subtitle="subtitle" />
15
- <div className=" px-md--rs pb-sm--rs">
15
+ <div className="px-md--rs pb-sm--rs">
16
16
  <Address text="0x0d7...3f34" isCopyable />
17
17
  <Address text="0x0d7...3f35" isCopyable />
18
18
  <Address text="0x0d7...3f36" isCopyable />