@iota/apps-ui-kit 0.10.1 → 0.12.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.10.1",
3
+ "version": "0.12.0",
4
4
  "sideEffects": false,
5
5
  "repository": "https://github.com/iotaledger/ts-packages",
6
6
  "license": "Apache-2.0",
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import cx from 'classnames';
5
+ import type { ReactNode } from 'react';
5
6
  import { BadgeSize, BadgeType } from './badge.enums';
6
7
  import { BACKGROUND_COLORS, BORDER_COLORS, TEXT_COLORS } from './badge.classes';
7
8
 
@@ -18,9 +19,18 @@ interface BadgeProps {
18
19
  * The size of the badge
19
20
  */
20
21
  size?: BadgeSize;
22
+ /**
23
+ * An icon rendered before the label.
24
+ */
25
+ icon?: ReactNode;
21
26
  }
22
27
 
23
- export function Badge({ type, label, size = BadgeSize.Medium }: BadgeProps): React.JSX.Element {
28
+ export function Badge({
29
+ type,
30
+ label,
31
+ size = BadgeSize.Medium,
32
+ icon,
33
+ }: BadgeProps): React.JSX.Element {
24
34
  const backgroundClasses = BACKGROUND_COLORS[type];
25
35
  const textClasses = TEXT_COLORS[type];
26
36
  const isSmall = size === BadgeSize.Small;
@@ -31,7 +41,8 @@ export function Badge({ type, label, size = BadgeSize.Medium }: BadgeProps): Rea
31
41
  return (
32
42
  <div
33
43
  className={cx(
34
- 'inline-flex items-center space-x-2 rounded-full disabled:opacity-30',
44
+ 'inline-flex items-center rounded-full disabled:opacity-30',
45
+ icon ? 'gap-xxs' : 'space-x-2',
35
46
  { border: !isSmall },
36
47
  { [BORDER_COLORS[type]]: !isSmall },
37
48
  { 'badge-ring-outlined': isSmall && isOutlined },
@@ -39,6 +50,11 @@ export function Badge({ type, label, size = BadgeSize.Medium }: BadgeProps): Rea
39
50
  labelClasses,
40
51
  )}
41
52
  >
53
+ {icon && (
54
+ <span className={cx('flex h-4 w-4 items-center justify-center', textClasses)}>
55
+ {icon}
56
+ </span>
57
+ )}
42
58
  <span className={cx(textSizeClass, textClasses)}>{label}</span>
43
59
  </div>
44
60
  );
@@ -26,7 +26,7 @@ export interface ButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonE
26
26
  /**
27
27
  * The text of the button.
28
28
  */
29
- text?: string;
29
+ text?: React.ReactNode;
30
30
  /**
31
31
  The icon of the button
32
32
  */
@@ -98,7 +98,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
98
98
  <span
99
99
  onClick={() => inputRef.current?.click()}
100
100
  className={cx(
101
- 'checkbox-base checkbox-state checkbox-icon checkbox-icon-hidden',
101
+ 'checkbox-base checkbox-state checkbox-icon checkbox-icon-hidden shrink-0',
102
102
  'checkbox-border-default',
103
103
  'peer-[&:is(:checked,:indeterminate)]:checkbox-border-checked',
104
104
  'peer-[&:is(:checked,:indeterminate)]:checkbox-bg-checked',
@@ -119,7 +119,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
119
119
 
120
120
  function LabelText({ label, name }: Pick<CheckboxProps, 'label' | 'name'>) {
121
121
  return (
122
- <label htmlFor={name} className="checkbox-label checkbox-label-disabled">
122
+ <label htmlFor={name} className="checkbox-label checkbox-label-disabled flex-1">
123
123
  {label}
124
124
  </label>
125
125
  );
@@ -41,7 +41,9 @@ export function InfoBox({
41
41
  return (
42
42
  <div
43
43
  className={cx('flex flex-row items-start gap-4 py-xs pr-lg', backgroundClass, {
44
- 'rounded-lg pl-xs': style === InfoBoxStyle.Elevated,
44
+ 'rounded-lg': style === InfoBoxStyle.Elevated,
45
+ 'pl-xs': style === InfoBoxStyle.Elevated && !!icon,
46
+ 'pl-lg': style === InfoBoxStyle.Elevated && !icon,
45
47
  })}
46
48
  >
47
49
  {icon && (
@@ -13,7 +13,11 @@ interface KeyValueProps {
13
13
  /**
14
14
  * The key of the KeyValue.
15
15
  */
16
- keyText: string;
16
+ keyText: ReactNode;
17
+ /**
18
+ * An icon shown next to the key text (optional).
19
+ */
20
+ keyIcon?: ReactNode;
17
21
  /**
18
22
  * The value of the KeyValue.
19
23
  */
@@ -29,7 +33,7 @@ interface KeyValueProps {
29
33
  /**
30
34
  * The supporting label of the KeyValue (optional).
31
35
  */
32
- supportingLabel?: string;
36
+ supportingLabel?: string | ReactNode;
33
37
  /**
34
38
  * The size of the value (optional).
35
39
  */
@@ -58,6 +62,11 @@ interface KeyValueProps {
58
62
  * Reverse the KeyValue (optional).
59
63
  */
60
64
  isReverse?: boolean;
65
+ /**
66
+ * Use a receipt-style row with a dotted leader and right-aligned value.
67
+ * This is opt-in so existing KeyValueInfo layouts remain unchanged.
68
+ */
69
+ layout?: 'default' | 'receipt';
61
70
  /**
62
71
  * Text shown on value hover.
63
72
  */
@@ -66,6 +75,7 @@ interface KeyValueProps {
66
75
 
67
76
  export function KeyValueInfo({
68
77
  keyText,
78
+ keyIcon,
69
79
  value,
70
80
  tooltipPosition,
71
81
  tooltipText,
@@ -77,9 +87,11 @@ export function KeyValueInfo({
77
87
  onCopyError,
78
88
  fullwidth,
79
89
  isReverse = false,
90
+ layout = 'default',
80
91
  valueHoverTitle,
81
92
  }: KeyValueProps): React.JSX.Element {
82
93
  const flexDirectionClass = isReverse ? 'flex-row-reverse' : 'flex-row';
94
+ const isReceiptLayout = layout === 'receipt';
83
95
  async function handleCopyClick(event: React.MouseEvent<HTMLButtonElement>) {
84
96
  if (!navigator.clipboard) {
85
97
  return;
@@ -102,32 +114,44 @@ export function KeyValueInfo({
102
114
  'flex w-full items-baseline gap-xs py-xxs font-inter',
103
115
  flexDirectionClass,
104
116
  {
105
- 'justify-between': fullwidth,
117
+ 'flex-wrap justify-between': fullwidth,
106
118
  },
107
119
  )}
108
120
  >
109
121
  <div
110
122
  className={cx('flex shrink-0 flex-row items-center gap-x-0.5', {
111
- 'w-1/4': !fullwidth,
123
+ 'w-1/4': !fullwidth && !isReceiptLayout,
112
124
  })}
113
125
  >
114
- <span className="key-value-key-text-color text-body-md">{keyText}</span>
126
+ {keyIcon}
127
+ <span className="key-value-key-text-color break-normal text-body-md">
128
+ {keyText}
129
+ </span>
115
130
  {tooltipText && (
116
131
  <Tooltip text={tooltipText} position={tooltipPosition}>
117
132
  <Info className="key-supporting-text-color" />
118
133
  </Tooltip>
119
134
  )}
120
135
  </div>
136
+ {isReceiptLayout && !fullwidth && (
137
+ <span
138
+ aria-hidden="true"
139
+ className="mb-1 min-w-[0.5rem] flex-1 border-b border-dotted border-iota-neutral-80 opacity-60 dark:border-iota-neutral-20"
140
+ />
141
+ )}
121
142
  <div
122
- className={cx('flex flex-row items-baseline gap-1 break-all', {
123
- 'w-3/4': !fullwidth,
143
+ className={cx('flex min-w-0 flex-row items-baseline gap-1 break-all', {
144
+ 'w-3/4': !fullwidth && !isReceiptLayout,
145
+ 'max-w-[60%] justify-end text-right': !fullwidth && isReceiptLayout,
146
+ 'flex-wrap': fullwidth,
147
+ 'justify-end text-right': fullwidth && isReceiptLayout,
124
148
  truncate: isTruncated,
125
149
  })}
126
150
  >
127
151
  <span
128
152
  title={valueHoverTitle}
129
153
  className={cx(
130
- 'key-value-hover-text-color',
154
+ 'key-value-hover-text-color min-w-0',
131
155
  size === ValueSize.Medium ? 'text-body-lg' : 'text-body-md',
132
156
  { truncate: isTruncated },
133
157
  )}
@@ -49,51 +49,78 @@ export function Tooltip({
49
49
  closeTimer.current = setTimeout(() => setVisible(false), closeDelay);
50
50
  };
51
51
 
52
- useLayoutEffect(() => {
53
- if (!visible) return;
54
-
55
- const rect = triggerRef.current?.getBoundingClientRect();
56
- if (!rect) return;
57
-
58
- const pos = {
52
+ const computePosition = (rect: DOMRect) =>
53
+ ({
59
54
  [TooltipPosition.Top]: {
60
55
  top: rect.top - offset,
61
56
  left: rect.left + rect.width / 2,
62
57
  transform: 'translate(-50%, -100%)',
58
+ anchorX: 0.5,
59
+ anchorY: 1,
63
60
  },
64
61
  [TooltipPosition.Bottom]: {
65
62
  top: rect.bottom + offset,
66
63
  left: rect.left + rect.width / 2,
67
64
  transform: 'translate(-50%, 0)',
65
+ anchorX: 0.5,
66
+ anchorY: 0,
68
67
  },
69
68
  [TooltipPosition.Left]: {
70
69
  top: rect.top + rect.height / 2,
71
70
  left: rect.left - offset,
72
71
  transform: 'translate(-100%, -50%)',
72
+ anchorX: 1,
73
+ anchorY: 0.5,
73
74
  },
74
75
  [TooltipPosition.Right]: {
75
76
  top: rect.top + rect.height / 2,
76
77
  left: rect.right + offset,
77
78
  transform: 'translate(0, -50%)',
79
+ anchorX: 0,
80
+ anchorY: 0.5,
78
81
  },
79
- }[position];
82
+ })[position];
83
+
84
+ const clampDelta = (boxStart: number, size: number, viewportSize: number, margin: number) => {
85
+ const boxEnd = boxStart + size;
86
+ if (boxEnd > viewportSize - margin) return viewportSize - margin - boxEnd;
87
+ if (boxStart < margin) return margin - boxStart;
88
+ return 0;
89
+ };
90
+
91
+ useLayoutEffect(() => {
92
+ if (!visible) return;
93
+
94
+ const rect = triggerRef.current?.getBoundingClientRect();
95
+ const tooltipEl = tooltipRef.current;
96
+ if (!rect || !tooltipEl) return;
97
+
98
+ const pos = computePosition(rect);
99
+ tooltipEl.style.transform = pos.transform;
100
+
101
+ const { width, height } = tooltipEl.getBoundingClientRect();
102
+ const margin = 8;
103
+
104
+ const boxLeft = pos.left - width * pos.anchorX;
105
+ const boxTop = pos.top - height * pos.anchorY;
106
+
107
+ const deltaX = clampDelta(boxLeft, width, window.innerWidth, margin);
108
+ const deltaY = clampDelta(boxTop, height, window.innerHeight, margin);
80
109
 
81
- setCoords({ top: pos.top, left: pos.left });
82
- tooltipRef.current!.style.transform = pos.transform;
110
+ setCoords({ top: pos.top + deltaY, left: pos.left + deltaX });
83
111
  }, [visible, position, offset]);
84
112
 
85
113
  useLayoutEffect(() => {
86
114
  if (!visible) return;
87
- const update = () => {
88
- const rect = triggerRef.current?.getBoundingClientRect();
89
- if (!rect) return;
90
- setCoords((prev) => ({ ...prev, left: rect.left + rect.width / 2 }));
115
+ const dismiss = () => {
116
+ clearTimers();
117
+ setVisible(false);
91
118
  };
92
- window.addEventListener('scroll', update, true);
93
- window.addEventListener('resize', update);
119
+ window.addEventListener('scroll', dismiss, true);
120
+ window.addEventListener('resize', dismiss);
94
121
  return () => {
95
- window.removeEventListener('scroll', update, true);
96
- window.removeEventListener('resize', update);
122
+ window.removeEventListener('scroll', dismiss, true);
123
+ window.removeEventListener('resize', dismiss);
97
124
  };
98
125
  }, [visible]);
99
126
 
@@ -6,8 +6,8 @@ import { CardActionType } from './card.enums';
6
6
  import { Button, ButtonSize, ButtonType } from '@/components/atoms/button';
7
7
 
8
8
  export type CardActionProps = {
9
- title?: string;
10
- subtitle?: string;
9
+ title?: React.ReactNode;
10
+ subtitle?: string | React.ReactNode;
11
11
  type: CardActionType;
12
12
  onClick?: () => void;
13
13
  icon?: React.ReactNode;
@@ -39,7 +39,7 @@ export function CardBody({
39
39
  })}
40
40
  >
41
41
  <div
42
- className={cx('card-body-title-color font-inter text-title-md', {
42
+ className={cx('card-body-title-color text-start font-inter text-title-md', {
43
43
  truncate: isTextTruncated,
44
44
  })}
45
45
  >
@@ -61,7 +61,7 @@ export function CardBody({
61
61
  </div>
62
62
  {subtitle && (
63
63
  <div
64
- className={cx('card-body-subtitle-color font-inter text-body-md', {
64
+ className={cx('card-body-subtitle-color text-start font-inter text-body-md', {
65
65
  truncate: isTextTruncated,
66
66
  })}
67
67
  >
@@ -28,7 +28,7 @@ interface DisplayStatsProps {
28
28
  /**
29
29
  * The tooltip text.
30
30
  */
31
- tooltipText?: string;
31
+ tooltipText?: ReactNode;
32
32
  /**
33
33
  * The value of the stats.
34
34
  */
@@ -123,7 +123,7 @@ export function DisplayStats({
123
123
  {icon && <span className="display-stats-icon-color">{icon}</span>}
124
124
  </div>
125
125
  <div className="flex w-full flex-row items-baseline gap-xxs">
126
- <span className={cx('break-all', valueClass)}>{value}</span>
126
+ <span className={cx('min-w-0 break-all', valueClass)}>{value}</span>
127
127
  {supportingLabel && (
128
128
  <span className={cx('opacity-40', supportingLabelTextClass)}>
129
129
  {supportingLabel}
@@ -31,7 +31,7 @@ export interface BaseInputProps extends InputWrapperProps {
31
31
  /**
32
32
  * Amount counter that is shown at the side of the caption text.
33
33
  */
34
- amountCounter?: string | number;
34
+ amountCounter?: React.ReactNode;
35
35
  /**
36
36
  * Trailing element that is shown after the input
37
37
  */
@@ -47,7 +47,7 @@ export interface BaseInputProps extends InputWrapperProps {
47
47
  /**
48
48
  * Text that is shown below the value of the input.
49
49
  */
50
- supportingValue?: string | null;
50
+ supportingValue?: React.ReactNode;
51
51
  /**
52
52
  * Default value of the input
53
53
  */
@@ -5,6 +5,7 @@ import cx from 'classnames';
5
5
  import { SecondaryText } from '@/components/atoms/secondary-text';
6
6
  import { LABEL_CLASSES } from './input.classes';
7
7
  import { createElement } from 'react';
8
+ import type { ReactNode } from 'react';
8
9
 
9
10
  export enum LabelHtmlTag {
10
11
  Label = 'label',
@@ -19,7 +20,7 @@ export interface InputWrapperProps {
19
20
  /**
20
21
  * Shows a caption with the text below the input.
21
22
  */
22
- caption?: string;
23
+ caption?: ReactNode;
23
24
  /**
24
25
  * Error Message. Overrides the caption.
25
26
  */
@@ -27,7 +28,7 @@ export interface InputWrapperProps {
27
28
  /**
28
29
  * Amount counter that is shown at the side of the caption text.
29
30
  */
30
- amountCounter?: string | number;
31
+ amountCounter?: React.ReactNode;
31
32
  /**
32
33
  * Is the input required
33
34
  */
@@ -17,15 +17,21 @@ interface TableCellBaseProps {
17
17
  * Whether the cell content should be centered.
18
18
  */
19
19
  isContentCentered?: boolean;
20
+ /**
21
+ * The number of columns the cell should span.
22
+ */
23
+ colSpan?: number;
20
24
  }
21
25
 
22
26
  export function TableCellBase({
23
27
  children,
24
28
  hasLastBorderNoneClass,
25
29
  isContentCentered,
30
+ colSpan,
26
31
  }: PropsWithChildren<TableCellBaseProps>) {
27
32
  return (
28
33
  <td
34
+ colSpan={colSpan}
29
35
  className={cx(
30
36
  'table-cell-border-color h-14 border-b px-md',
31
37
  { 'last:border-none': hasLastBorderNoneClass },
@@ -60,6 +60,8 @@ export interface TableHeaderCellProps extends React.AriaAttributes {
60
60
  export function TableHeaderCell({
61
61
  label,
62
62
  columnKey,
63
+ actionLeft,
64
+ actionRight,
63
65
  hasSort,
64
66
  hasCheckbox,
65
67
  isChecked,
@@ -136,15 +138,19 @@ export function TableHeaderCell({
136
138
  onCheckedChange={onCheckboxChange}
137
139
  />
138
140
  ) : (
139
- <span
140
- className={cx({
141
- 'text-left': !isContentCentered,
142
- })}
143
- >
144
- {label}
145
- </span>
141
+ <>
142
+ {actionLeft}
143
+ <span
144
+ className={cx({
145
+ 'text-left': !isContentCentered,
146
+ })}
147
+ >
148
+ {label}
149
+ </span>
150
+ </>
146
151
  )}
147
152
  {sortElement}
153
+ {actionRight}
148
154
  </div>
149
155
  </th>
150
156
  );
@@ -30,6 +30,13 @@ export interface AccordionHeaderProps {
30
30
  * Flag for hiding border.
31
31
  */
32
32
  hideBorder?: boolean;
33
+
34
+ /**
35
+ * Disable the header state layer while an interactive child is active.
36
+ * This keeps the parent hover treatment from competing with child links
37
+ * and controls.
38
+ */
39
+ disableStateLayer?: boolean;
33
40
  }
34
41
 
35
42
  interface AccordionContentProps {
@@ -44,6 +51,7 @@ export function AccordionHeader({
44
51
  children,
45
52
  isExpanded,
46
53
  hideArrow,
54
+ disableStateLayer = false,
47
55
  }: PropsWithChildren<AccordionHeaderProps>) {
48
56
  return (
49
57
  <div
@@ -51,7 +59,8 @@ export function AccordionHeader({
51
59
  role="button"
52
60
  aria-expanded={isExpanded}
53
61
  className={cx(
54
- 'state-layer relative flex cursor-pointer items-center justify-between gap-md overflow-hidden rounded-xl py-sm--rs',
62
+ 'relative flex cursor-pointer items-center justify-between gap-md overflow-hidden rounded-xl py-sm--rs',
63
+ !disableStateLayer && 'state-layer',
55
64
  {
56
65
  'pr-md--rs': !hideArrow,
57
66
  },
@@ -177,9 +177,19 @@ export function TableHeader({ children }: PropsWithChildren): JSX.Element {
177
177
  export function TableRow({
178
178
  children,
179
179
  leading,
180
- }: PropsWithChildren<{ leading?: React.ReactNode }>): JSX.Element {
180
+ onClick,
181
+ }: PropsWithChildren<{
182
+ leading?: React.ReactNode;
183
+ onClick?: React.MouseEventHandler<HTMLTableRowElement>;
184
+ }>): JSX.Element {
181
185
  return (
182
- <tr>
186
+ <tr
187
+ onClick={onClick}
188
+ className={cx({
189
+ 'cursor-pointer hover:bg-shader-neutral-light-8 dark:hover:bg-shader-neutral-dark-8':
190
+ onClick,
191
+ })}
192
+ >
183
193
  {leading}
184
194
  {children}
185
195
  </tr>