@trackunit/react-components 2.10.30-alpha-fa4af587a69.0 → 2.11.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/index.cjs.js CHANGED
@@ -27,6 +27,8 @@ var dequal = require('dequal');
27
27
  var defaultTranslations = {
28
28
  "breadcrumb.backButton": "back",
29
29
  "breadcrumb.expandButton": "expand",
30
+ "copyButton.copied": "Copied!",
31
+ "copyButton.copy": "Copy",
30
32
  "moreMenu.buttonLabel": "Show more"
31
33
  };
32
34
 
@@ -1753,7 +1755,6 @@ const cvaButtonSpinner = cssClassVarianceUtilities.cvaMerge([], {
1753
1755
  const cvaButtonPrefixSuffix = cssClassVarianceUtilities.cvaMerge(["flex", "items-center"], {
1754
1756
  variants: {
1755
1757
  size: {
1756
- extraSmall: ["h-3"],
1757
1758
  small: ["h-4"],
1758
1759
  medium: ["h-4"],
1759
1760
  large: ["h-5"],
@@ -1823,6 +1824,14 @@ const cvaButtonPrefixSuffix = cssClassVarianceUtilities.cvaMerge(["flex", "items
1823
1824
  const cvaIconButton = cssClassVarianceUtilities.cvaMerge([], {
1824
1825
  variants: {
1825
1826
  size: {
1827
+ /**
1828
+ * Sized to match a single text line (20px) so the button fits inside
1829
+ * fixed-height contexts like table rows without clipping its hover state.
1830
+ *
1831
+ * `min-h-5` is what makes this an icon-only step: it overrides the
1832
+ * text-height floor `cvaButton` applies for every size on its own scale.
1833
+ */
1834
+ extraSmall: ["p-0.5", "w-5", "h-5", "min-h-5"],
1826
1835
  small: ["p-1.5", "w-7", "h-7"],
1827
1836
  medium: ["p-2", "w-8", "h-8"],
1828
1837
  large: ["p-2.5", "w-10", "h-10"],
@@ -2525,6 +2534,16 @@ function useMergeRefs(refs) {
2525
2534
  }, [refEffect]);
2526
2535
  }
2527
2536
 
2537
+ /**
2538
+ * Maps an icon-only size onto the text-button scale that `Button` understands.
2539
+ *
2540
+ * `extraSmall` has no text-button equivalent — its 20px box comes entirely from
2541
+ * `cvaIconButton`, which overrides padding, width and height. `small` is the step
2542
+ * whose remaining classes (`gap-1`, `text-xs`) already match, so it is what the
2543
+ * icon-only components hand to `Button`.
2544
+ */
2545
+ const toButtonSize = (size) => (size === "extraSmall" ? "small" : size);
2546
+
2528
2547
  /**
2529
2548
  * Buttons are clickable elements that are used to trigger actions. They communicate calls to action to the user and allow users to interact with pages in a variety of ways. The Icon Button is a version of the standard Button component without the text label.
2530
2549
  *
@@ -2569,7 +2588,7 @@ function useMergeRefs(refs) {
2569
2588
  */
2570
2589
  const IconButton = ({ icon, size = "medium", square = true, loading = false, disabled = false, className, title, ariaLabel, "data-testid": dataTestId, ref, ...rest }) => {
2571
2590
  const accessibleLabel = ariaLabel ?? title;
2572
- return (jsxRuntime.jsx(Tooltip, { "data-testid": "iconButton-tooltip", delayed: true, disabled: !title || disabled || loading, interactable: false, label: title, children: jsxRuntime.jsx(Button, { ...rest, ariaLabel: accessibleLabel, className: cvaIconButton({ size: size, className }), "data-testid": dataTestId ? dataTestId : undefined, disabled: disabled || loading, loading: loading, prefix: !loading ? react.cloneElement(icon, { ariaHidden: true }) : undefined, ref: ref, size: size, square: square, title: undefined }) }));
2591
+ return (jsxRuntime.jsx(Tooltip, { "data-testid": "iconButton-tooltip", delayed: true, disabled: !title || disabled || loading, interactable: false, label: title, children: jsxRuntime.jsx(Button, { ...rest, ariaLabel: accessibleLabel, className: cvaIconButton({ size: size, className }), "data-testid": dataTestId ? dataTestId : undefined, disabled: disabled || loading, loading: loading, prefix: !loading ? react.cloneElement(icon, { ariaHidden: true }) : undefined, ref: ref, size: toButtonSize(size), square: square, title: undefined }) }));
2573
2592
  };
2574
2593
  IconButton.displayName = "IconButton";
2575
2594
 
@@ -4042,11 +4061,14 @@ const CopyableText = ({ text, withIcon = true, size = "sm", copyLabel = "Copy",
4042
4061
  * clickable/displayed element, so it can be placed next to plain (non-clickable) text.
4043
4062
  *
4044
4063
  * ### When to use
4045
- * Use CopyButton when the copyable value is displayed as plain text (e.g. in a table cell or
4046
- * row) and only a dedicated icon should trigger the copy, keeping the rest of the row clickable.
4064
+ * - Icon-only (default): the copyable value is displayed as plain text (e.g. in a table cell or
4065
+ * row) and only a dedicated icon should trigger the copy, keeping the rest of the row clickable.
4066
+ * - Labelled (`label` set): copying is the primary action of a panel or section — e.g. an invite
4067
+ * link — and deserves a full button that is itself the click target.
4047
4068
  *
4048
4069
  * ### When not to use
4049
- * If the displayed text itself should be the click target, use `CopyableText` instead.
4070
+ * Do not hand-roll copy affordances (custom icons, click-to-copy text) — this component is the
4071
+ * one copy control; pick icon-only or labelled mode instead.
4050
4072
  *
4051
4073
  * ### Empty values
4052
4074
  * Like `CopyableText`, CopyButton renders nothing when `value` is empty, so consumers never
@@ -4066,21 +4088,48 @@ const CopyableText = ({ text, withIcon = true, size = "sm", copyLabel = "Copy",
4066
4088
  * @param {CopyButtonProps} props - The props for the CopyButton component
4067
4089
  * @returns {ReactElement} CopyButton component
4068
4090
  */
4069
- const CopyButton = ({ value, copyLabel = "Copy", copiedLabel = "Copied!", size = "small", className, style, "data-testid": dataTestId, ref, }) => {
4091
+ const CopyButton = ({ value, copyLabel, copiedLabel, size = "small", label, variant = "ghost-neutral", className, style, "data-testid": dataTestId, onCopy, ref, }) => {
4092
+ const [t] = useTranslation();
4070
4093
  const [showCopied, setShowCopied] = react.useState(false);
4071
4094
  const [, copyToClipboard] = useCopyToClipboard();
4095
+ const resetTimerRef = react.useRef(undefined);
4096
+ react.useEffect(() => () => clearTimeout(resetTimerRef.current), []);
4097
+ // Localized defaults, so call sites don't each re-declare "Copy"/"Copied!"
4098
+ // in their own translation files; the props stay as context-specific overrides.
4099
+ const resolvedCopyLabel = copyLabel ?? t("copyButton.copy");
4100
+ const resolvedCopiedLabel = copiedLabel ?? t("copyButton.copied");
4072
4101
  if (!value) {
4073
4102
  return null;
4074
4103
  }
4075
4104
  const handleCopy = (e) => {
4076
4105
  e.stopPropagation();
4077
4106
  void copyToClipboard(value)
4078
- .then(() => setShowCopied(true))
4079
- .catch(() => undefined);
4107
+ // Rejections (both clipboard mechanisms failed) are intentionally
4108
+ // swallowed; the icon simply doesn't flip. A throwing onCopy is a
4109
+ // consumer bug and surfaces as an unhandled rejection instead.
4110
+ .then(() => {
4111
+ // Flip the icon to a checkmark for a moment, then revert. The timer
4112
+ // owns the copied state so the confirmation survives the pointer
4113
+ // leaving the button, and runs even if onCopy below throws.
4114
+ setShowCopied(true);
4115
+ clearTimeout(resetTimerRef.current);
4116
+ resetTimerRef.current = setTimeout(() => setShowCopied(false), COPIED_FEEDBACK_DURATION_MS);
4117
+ onCopy?.();
4118
+ }, () => undefined);
4080
4119
  };
4081
- return (jsxRuntime.jsx(Tooltip, { delayed: !showCopied, label: showCopied ? copiedLabel : copyLabel, placement: "top", children: jsxRuntime.jsx(Button, { ariaLabel: copyLabel, className: cvaIconButton({ size, className }), "data-testid": dataTestId, onBlur: () => setShowCopied(false), onClick: handleCopy, onMouseLeave: () => setShowCopied(false), prefix: jsxRuntime.jsx(Icon, { ariaHidden: true, name: "Square2Stack", size: size === "large" ? "medium" : "small" }), ref: ref, size: size, square: true, style: style, variant: "ghost-neutral" }) }));
4120
+ const icon = (jsxRuntime.jsx(Icon, { ariaHidden: true, name: showCopied ? "Check" : "Square2Stack", size: size === "large" ? "medium" : "small" }));
4121
+ if (label) {
4122
+ // Labelled mode: a regular (non-square) button whose prefix icon carries
4123
+ // the copied confirmation. `extraSmall` is icon-only, so it collapses to small.
4124
+ // The visible label stays the accessible name (WCAG 2.5.3 Label in Name);
4125
+ // since the copied feedback is otherwise visual-only, it is announced
4126
+ // through the polite live region rendered next to the button.
4127
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Tooltip, { delayed: !showCopied, label: showCopied ? resolvedCopiedLabel : resolvedCopyLabel, placement: "top", children: jsxRuntime.jsx(Button, { className: className, "data-testid": dataTestId, onClick: handleCopy, prefix: icon, ref: ref, size: toButtonSize(size), style: style, variant: variant, children: label }) }), jsxRuntime.jsx("span", { className: "sr-only", role: "status", children: showCopied ? resolvedCopiedLabel : undefined })] }));
4128
+ }
4129
+ return (jsxRuntime.jsx(Tooltip, { delayed: !showCopied, label: showCopied ? resolvedCopiedLabel : resolvedCopyLabel, placement: "top", children: jsxRuntime.jsx(Button, { ariaLabel: showCopied ? resolvedCopiedLabel : resolvedCopyLabel, className: cvaIconButton({ size, className }), "data-testid": dataTestId, onClick: handleCopy, prefix: icon, ref: ref, size: toButtonSize(size), square: true, style: style, variant: variant }) }));
4082
4130
  };
4083
4131
  CopyButton.displayName = "CopyButton";
4132
+ const COPIED_FEEDBACK_DURATION_MS = 2000;
4084
4133
 
4085
4134
  const cvaDetailsList = cssClassVarianceUtilities.cvaMerge(["flex", "w-full", "min-w-0", "items-center", "truncate", "text-xs", "text-neutral-600", "font-medium", "pt-0"], {
4086
4135
  variants: {
package/index.esm.js CHANGED
@@ -26,6 +26,8 @@ import { dequal } from 'dequal';
26
26
  var defaultTranslations = {
27
27
  "breadcrumb.backButton": "back",
28
28
  "breadcrumb.expandButton": "expand",
29
+ "copyButton.copied": "Copied!",
30
+ "copyButton.copy": "Copy",
29
31
  "moreMenu.buttonLabel": "Show more"
30
32
  };
31
33
 
@@ -1752,7 +1754,6 @@ const cvaButtonSpinner = cvaMerge([], {
1752
1754
  const cvaButtonPrefixSuffix = cvaMerge(["flex", "items-center"], {
1753
1755
  variants: {
1754
1756
  size: {
1755
- extraSmall: ["h-3"],
1756
1757
  small: ["h-4"],
1757
1758
  medium: ["h-4"],
1758
1759
  large: ["h-5"],
@@ -1822,6 +1823,14 @@ const cvaButtonPrefixSuffix = cvaMerge(["flex", "items-center"], {
1822
1823
  const cvaIconButton = cvaMerge([], {
1823
1824
  variants: {
1824
1825
  size: {
1826
+ /**
1827
+ * Sized to match a single text line (20px) so the button fits inside
1828
+ * fixed-height contexts like table rows without clipping its hover state.
1829
+ *
1830
+ * `min-h-5` is what makes this an icon-only step: it overrides the
1831
+ * text-height floor `cvaButton` applies for every size on its own scale.
1832
+ */
1833
+ extraSmall: ["p-0.5", "w-5", "h-5", "min-h-5"],
1825
1834
  small: ["p-1.5", "w-7", "h-7"],
1826
1835
  medium: ["p-2", "w-8", "h-8"],
1827
1836
  large: ["p-2.5", "w-10", "h-10"],
@@ -2524,6 +2533,16 @@ function useMergeRefs(refs) {
2524
2533
  }, [refEffect]);
2525
2534
  }
2526
2535
 
2536
+ /**
2537
+ * Maps an icon-only size onto the text-button scale that `Button` understands.
2538
+ *
2539
+ * `extraSmall` has no text-button equivalent — its 20px box comes entirely from
2540
+ * `cvaIconButton`, which overrides padding, width and height. `small` is the step
2541
+ * whose remaining classes (`gap-1`, `text-xs`) already match, so it is what the
2542
+ * icon-only components hand to `Button`.
2543
+ */
2544
+ const toButtonSize = (size) => (size === "extraSmall" ? "small" : size);
2545
+
2527
2546
  /**
2528
2547
  * Buttons are clickable elements that are used to trigger actions. They communicate calls to action to the user and allow users to interact with pages in a variety of ways. The Icon Button is a version of the standard Button component without the text label.
2529
2548
  *
@@ -2568,7 +2587,7 @@ function useMergeRefs(refs) {
2568
2587
  */
2569
2588
  const IconButton = ({ icon, size = "medium", square = true, loading = false, disabled = false, className, title, ariaLabel, "data-testid": dataTestId, ref, ...rest }) => {
2570
2589
  const accessibleLabel = ariaLabel ?? title;
2571
- return (jsx(Tooltip, { "data-testid": "iconButton-tooltip", delayed: true, disabled: !title || disabled || loading, interactable: false, label: title, children: jsx(Button, { ...rest, ariaLabel: accessibleLabel, className: cvaIconButton({ size: size, className }), "data-testid": dataTestId ? dataTestId : undefined, disabled: disabled || loading, loading: loading, prefix: !loading ? cloneElement(icon, { ariaHidden: true }) : undefined, ref: ref, size: size, square: square, title: undefined }) }));
2590
+ return (jsx(Tooltip, { "data-testid": "iconButton-tooltip", delayed: true, disabled: !title || disabled || loading, interactable: false, label: title, children: jsx(Button, { ...rest, ariaLabel: accessibleLabel, className: cvaIconButton({ size: size, className }), "data-testid": dataTestId ? dataTestId : undefined, disabled: disabled || loading, loading: loading, prefix: !loading ? cloneElement(icon, { ariaHidden: true }) : undefined, ref: ref, size: toButtonSize(size), square: square, title: undefined }) }));
2572
2591
  };
2573
2592
  IconButton.displayName = "IconButton";
2574
2593
 
@@ -4041,11 +4060,14 @@ const CopyableText = ({ text, withIcon = true, size = "sm", copyLabel = "Copy",
4041
4060
  * clickable/displayed element, so it can be placed next to plain (non-clickable) text.
4042
4061
  *
4043
4062
  * ### When to use
4044
- * Use CopyButton when the copyable value is displayed as plain text (e.g. in a table cell or
4045
- * row) and only a dedicated icon should trigger the copy, keeping the rest of the row clickable.
4063
+ * - Icon-only (default): the copyable value is displayed as plain text (e.g. in a table cell or
4064
+ * row) and only a dedicated icon should trigger the copy, keeping the rest of the row clickable.
4065
+ * - Labelled (`label` set): copying is the primary action of a panel or section — e.g. an invite
4066
+ * link — and deserves a full button that is itself the click target.
4046
4067
  *
4047
4068
  * ### When not to use
4048
- * If the displayed text itself should be the click target, use `CopyableText` instead.
4069
+ * Do not hand-roll copy affordances (custom icons, click-to-copy text) — this component is the
4070
+ * one copy control; pick icon-only or labelled mode instead.
4049
4071
  *
4050
4072
  * ### Empty values
4051
4073
  * Like `CopyableText`, CopyButton renders nothing when `value` is empty, so consumers never
@@ -4065,21 +4087,48 @@ const CopyableText = ({ text, withIcon = true, size = "sm", copyLabel = "Copy",
4065
4087
  * @param {CopyButtonProps} props - The props for the CopyButton component
4066
4088
  * @returns {ReactElement} CopyButton component
4067
4089
  */
4068
- const CopyButton = ({ value, copyLabel = "Copy", copiedLabel = "Copied!", size = "small", className, style, "data-testid": dataTestId, ref, }) => {
4090
+ const CopyButton = ({ value, copyLabel, copiedLabel, size = "small", label, variant = "ghost-neutral", className, style, "data-testid": dataTestId, onCopy, ref, }) => {
4091
+ const [t] = useTranslation();
4069
4092
  const [showCopied, setShowCopied] = useState(false);
4070
4093
  const [, copyToClipboard] = useCopyToClipboard();
4094
+ const resetTimerRef = useRef(undefined);
4095
+ useEffect(() => () => clearTimeout(resetTimerRef.current), []);
4096
+ // Localized defaults, so call sites don't each re-declare "Copy"/"Copied!"
4097
+ // in their own translation files; the props stay as context-specific overrides.
4098
+ const resolvedCopyLabel = copyLabel ?? t("copyButton.copy");
4099
+ const resolvedCopiedLabel = copiedLabel ?? t("copyButton.copied");
4071
4100
  if (!value) {
4072
4101
  return null;
4073
4102
  }
4074
4103
  const handleCopy = (e) => {
4075
4104
  e.stopPropagation();
4076
4105
  void copyToClipboard(value)
4077
- .then(() => setShowCopied(true))
4078
- .catch(() => undefined);
4106
+ // Rejections (both clipboard mechanisms failed) are intentionally
4107
+ // swallowed; the icon simply doesn't flip. A throwing onCopy is a
4108
+ // consumer bug and surfaces as an unhandled rejection instead.
4109
+ .then(() => {
4110
+ // Flip the icon to a checkmark for a moment, then revert. The timer
4111
+ // owns the copied state so the confirmation survives the pointer
4112
+ // leaving the button, and runs even if onCopy below throws.
4113
+ setShowCopied(true);
4114
+ clearTimeout(resetTimerRef.current);
4115
+ resetTimerRef.current = setTimeout(() => setShowCopied(false), COPIED_FEEDBACK_DURATION_MS);
4116
+ onCopy?.();
4117
+ }, () => undefined);
4079
4118
  };
4080
- return (jsx(Tooltip, { delayed: !showCopied, label: showCopied ? copiedLabel : copyLabel, placement: "top", children: jsx(Button, { ariaLabel: copyLabel, className: cvaIconButton({ size, className }), "data-testid": dataTestId, onBlur: () => setShowCopied(false), onClick: handleCopy, onMouseLeave: () => setShowCopied(false), prefix: jsx(Icon, { ariaHidden: true, name: "Square2Stack", size: size === "large" ? "medium" : "small" }), ref: ref, size: size, square: true, style: style, variant: "ghost-neutral" }) }));
4119
+ const icon = (jsx(Icon, { ariaHidden: true, name: showCopied ? "Check" : "Square2Stack", size: size === "large" ? "medium" : "small" }));
4120
+ if (label) {
4121
+ // Labelled mode: a regular (non-square) button whose prefix icon carries
4122
+ // the copied confirmation. `extraSmall` is icon-only, so it collapses to small.
4123
+ // The visible label stays the accessible name (WCAG 2.5.3 Label in Name);
4124
+ // since the copied feedback is otherwise visual-only, it is announced
4125
+ // through the polite live region rendered next to the button.
4126
+ return (jsxs(Fragment, { children: [jsx(Tooltip, { delayed: !showCopied, label: showCopied ? resolvedCopiedLabel : resolvedCopyLabel, placement: "top", children: jsx(Button, { className: className, "data-testid": dataTestId, onClick: handleCopy, prefix: icon, ref: ref, size: toButtonSize(size), style: style, variant: variant, children: label }) }), jsx("span", { className: "sr-only", role: "status", children: showCopied ? resolvedCopiedLabel : undefined })] }));
4127
+ }
4128
+ return (jsx(Tooltip, { delayed: !showCopied, label: showCopied ? resolvedCopiedLabel : resolvedCopyLabel, placement: "top", children: jsx(Button, { ariaLabel: showCopied ? resolvedCopiedLabel : resolvedCopyLabel, className: cvaIconButton({ size, className }), "data-testid": dataTestId, onClick: handleCopy, prefix: icon, ref: ref, size: toButtonSize(size), square: true, style: style, variant: variant }) }));
4081
4129
  };
4082
4130
  CopyButton.displayName = "CopyButton";
4131
+ const COPIED_FEEDBACK_DURATION_MS = 2000;
4083
4132
 
4084
4133
  const cvaDetailsList = cvaMerge(["flex", "w-full", "min-w-0", "items-center", "truncate", "text-xs", "text-neutral-600", "font-medium", "pt-0"], {
4085
4134
  variants: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "2.10.30-alpha-fa4af587a69.0",
3
+ "version": "2.11.0",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "migrations": "./migrations.json",
@@ -14,16 +14,16 @@
14
14
  "@floating-ui/react": "^0.26.25",
15
15
  "string-ts": "^2.0.0",
16
16
  "tailwind-merge": "^2.0.0",
17
- "@trackunit/ui-design-tokens": "1.15.14-alpha-fa4af587a69.0",
18
- "@trackunit/css-class-variance-utilities": "1.14.24-alpha-fa4af587a69.0",
19
- "@trackunit/shared-utils": "1.16.27-alpha-fa4af587a69.0",
20
- "@trackunit/ui-icons": "1.14.23-alpha-fa4af587a69.0",
17
+ "@trackunit/ui-design-tokens": "1.15.13",
18
+ "@trackunit/css-class-variance-utilities": "1.14.23",
19
+ "@trackunit/shared-utils": "1.16.26",
20
+ "@trackunit/ui-icons": "1.14.22",
21
21
  "es-toolkit": "^1.39.10",
22
22
  "@tanstack/react-virtual": "^3.14.3",
23
23
  "dequal": "^2.0.3",
24
24
  "fflate": "^0.8.2",
25
25
  "zod": "^3.25.76",
26
- "@trackunit/i18n-library-translation": "2.4.34-alpha-fa4af587a69.0"
26
+ "@trackunit/i18n-library-translation": "2.4.34"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react": "^19.0.0",
@@ -1,8 +1,9 @@
1
1
  import { type ReactElement } from "react";
2
2
  import { CommonProps } from "../../common/CommonProps";
3
3
  import { Refable } from "../../common/Refable";
4
- import { Size } from "../../common/Size";
5
4
  import type { Styleable } from "../../common/Styleable";
5
+ import { ButtonVariant } from "../buttons/shared/ButtonProps";
6
+ import { IconButtonSize } from "../buttons/shared/IconButtonSize";
6
7
  export interface CopyButtonProps extends CommonProps, Styleable, Refable<HTMLButtonElement> {
7
8
  /**
8
9
  * The value copied to the clipboard when the button is clicked.
@@ -10,22 +11,41 @@ export interface CopyButtonProps extends CommonProps, Styleable, Refable<HTMLBut
10
11
  value: string;
11
12
  /**
12
13
  * Accessible label for the button, and tooltip text shown before copying.
13
- *
14
- * @default "Copy"
14
+ * Defaults to the library's localized "Copy"; pass a value only when the
15
+ * context needs a more specific label (e.g. "Copy account ID").
15
16
  */
16
17
  copyLabel?: string;
17
18
  /**
18
- * Tooltip text shown briefly after a successful copy.
19
- *
20
- * @default "Copied!"
19
+ * Tooltip text shown briefly after a successful copy. Defaults to the
20
+ * library's localized "Copied!".
21
21
  */
22
22
  copiedLabel?: string;
23
23
  /**
24
- * The size of the icon button.
24
+ * The size of the icon button. Use `extraSmall` inside fixed-height contexts
25
+ * such as table rows, where the button must fit a single text line.
25
26
  *
26
27
  * @default "small"
27
28
  */
28
- size?: Size;
29
+ size?: IconButtonSize;
30
+ /**
31
+ * Optional visible label. When set, the button renders as a regular
32
+ * labelled button instead of an icon-only control — use this where copying
33
+ * is the primary action of a panel (e.g. invite links). The icon still
34
+ * flips to a checkmark while the copied confirmation is active.
35
+ */
36
+ label?: string;
37
+ /**
38
+ * The button's variant.
39
+ *
40
+ * @default "ghost-neutral"
41
+ */
42
+ variant?: ButtonVariant;
43
+ /**
44
+ * Called after the value has been written to the clipboard — via the
45
+ * Clipboard API or the `execCommand("copy")` fallback. Only skipped when
46
+ * both mechanisms fail.
47
+ */
48
+ onCopy?: () => void;
29
49
  }
30
50
  /**
31
51
  * CopyButton is a standalone icon button that copies a value to the clipboard when clicked,
@@ -33,11 +53,14 @@ export interface CopyButtonProps extends CommonProps, Styleable, Refable<HTMLBut
33
53
  * clickable/displayed element, so it can be placed next to plain (non-clickable) text.
34
54
  *
35
55
  * ### When to use
36
- * Use CopyButton when the copyable value is displayed as plain text (e.g. in a table cell or
37
- * row) and only a dedicated icon should trigger the copy, keeping the rest of the row clickable.
56
+ * - Icon-only (default): the copyable value is displayed as plain text (e.g. in a table cell or
57
+ * row) and only a dedicated icon should trigger the copy, keeping the rest of the row clickable.
58
+ * - Labelled (`label` set): copying is the primary action of a panel or section — e.g. an invite
59
+ * link — and deserves a full button that is itself the click target.
38
60
  *
39
61
  * ### When not to use
40
- * If the displayed text itself should be the click target, use `CopyableText` instead.
62
+ * Do not hand-roll copy affordances (custom icons, click-to-copy text) — this component is the
63
+ * one copy control; pick icon-only or labelled mode instead.
41
64
  *
42
65
  * ### Empty values
43
66
  * Like `CopyableText`, CopyButton renders nothing when `value` is empty, so consumers never
@@ -58,6 +81,6 @@ export interface CopyButtonProps extends CommonProps, Styleable, Refable<HTMLBut
58
81
  * @returns {ReactElement} CopyButton component
59
82
  */
60
83
  export declare const CopyButton: {
61
- ({ value, copyLabel, copiedLabel, size, className, style, "data-testid": dataTestId, ref, }: CopyButtonProps): ReactElement | null;
84
+ ({ value, copyLabel, copiedLabel, size, label, variant, className, style, "data-testid": dataTestId, onCopy, ref, }: CopyButtonProps): ReactElement | null;
62
85
  displayName: string;
63
86
  };
@@ -1,9 +1,9 @@
1
1
  import type { MappedOmit } from "@trackunit/shared-utils";
2
2
  import { ComponentType, JSX, ReactElement } from "react";
3
3
  import { Refable } from "../../../common/Refable";
4
- import { Size } from "../../../common/Size";
5
4
  import { Icon, IconProps } from "../../Icon/Icon";
6
5
  import { ButtonCommonProps } from "../shared/ButtonProps";
6
+ import { IconButtonSize } from "../shared/IconButtonSize";
7
7
  interface IconButtonBaseProps extends MappedOmit<ButtonCommonProps, "ariaLabel" | "size" | "title">, Refable<HTMLButtonElement>, Refable<HTMLButtonElement> {
8
8
  /**
9
9
  * The icon to display. Must be an `<Icon />` element.
@@ -16,9 +16,11 @@ interface IconButtonBaseProps extends MappedOmit<ButtonCommonProps, "ariaLabel"
16
16
  */
17
17
  renderAs?: keyof JSX.IntrinsicElements | ComponentType<any>;
18
18
  /**
19
- * The size of the button. Defaults to "medium".
19
+ * The size of the button. Defaults to "medium". Use `extraSmall` inside
20
+ * fixed-height contexts such as table rows, where the button must fit a
21
+ * single text line.
20
22
  */
21
- size?: Size;
23
+ size?: IconButtonSize;
22
24
  }
23
25
  type IconButtonAccessibleNameProps = {
24
26
  /**
@@ -11,10 +11,10 @@ export declare const cvaButtonSpinner: (props?: ({
11
11
  variant?: "filled" | "primary" | "primary-danger" | "secondary" | "secondary-danger" | "ghost" | "ghost-neutral" | null | undefined;
12
12
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
13
13
  export declare const cvaButtonPrefixSuffix: (props?: ({
14
- size?: "small" | "medium" | "large" | "extraSmall" | null | undefined;
14
+ size?: "small" | "medium" | "large" | null | undefined;
15
15
  variant?: "filled" | "primary" | "primary-danger" | "secondary" | "secondary-danger" | "ghost" | "ghost-neutral" | null | undefined;
16
16
  disabled?: boolean | null | undefined;
17
17
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
18
18
  export declare const cvaIconButton: (props?: ({
19
- size?: "small" | "medium" | "large" | null | undefined;
19
+ size?: "small" | "medium" | "large" | "extraSmall" | null | undefined;
20
20
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -39,7 +39,7 @@ export interface ButtonCommonProps extends CommonProps, Styleable, AriaProps {
39
39
  */
40
40
  variant?: ButtonVariant;
41
41
  /**
42
- * The size of the button. "ExtraSmall" is only used for IconButtons.
42
+ * The size of the button.
43
43
  */
44
44
  size?: Size;
45
45
  /**
@@ -0,0 +1,16 @@
1
+ import { Size } from "../../../common/Size";
2
+ /**
3
+ * Size scale for icon-only buttons. Extends the regular button scale with
4
+ * `extraSmall` (20px), which matches a single text line so the button fits inside
5
+ * fixed-height contexts such as table rows without clipping.
6
+ */
7
+ export type IconButtonSize = Size | "extraSmall";
8
+ /**
9
+ * Maps an icon-only size onto the text-button scale that `Button` understands.
10
+ *
11
+ * `extraSmall` has no text-button equivalent — its 20px box comes entirely from
12
+ * `cvaIconButton`, which overrides padding, width and height. `small` is the step
13
+ * whose remaining classes (`gap-1`, `text-xs`) already match, so it is what the
14
+ * icon-only components hand to `Button`.
15
+ */
16
+ export declare const toButtonSize: (size: IconButtonSize) => Size;
package/src/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export * from "./components/buttons/Button/Button";
13
13
  export * from "./components/buttons/IconButton/IconButton";
14
14
  export * from "./components/buttons/shared/Button.variants";
15
15
  export * from "./components/buttons/shared/ButtonProps";
16
+ export type { IconButtonSize } from "./components/buttons/shared/IconButtonSize";
16
17
  export * from "./components/buttons/StarButton/StarButton";
17
18
  export * from "./components/Card/Card";
18
19
  export * from "./components/Card/CardBody";