@trackunit/react-components 3.5.6 → 3.7.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
@@ -28,6 +28,7 @@ var zod = require('zod');
28
28
  var defaultTranslations = {
29
29
  "breadcrumb.backButton": "Back",
30
30
  "breadcrumb.expandButton": "expand",
31
+ "copyableValue.copyValue": "Copy {{value}}",
31
32
  "copyButton.copied": "Copied!",
32
33
  "copyButton.copy": "Copy",
33
34
  "horizontalOverflowScroller.scrollLeft": "Scroll left",
@@ -4773,28 +4774,27 @@ const useCopyToClipboard = () => {
4773
4774
  * element, so the button can be placed next to plain (non-clickable) text.
4774
4775
  *
4775
4776
  * ### When to use
4776
- * - Icon-only (default): the copyable value is displayed as plain text (e.g. in a table cell or
4777
- * row) and only a dedicated icon should trigger the copy, keeping the rest of the row clickable.
4778
4777
  * - Labelled (`label` set): copying is the primary action of a panel or section — e.g. an invite
4779
4778
  * link — and deserves a full button that is itself the click target.
4779
+ * - Icon-only (default): a copy control on its own, where no value is displayed next to it — e.g.
4780
+ * in the corner of a code block.
4780
4781
  *
4781
4782
  * ### When not to use
4782
- * Do not hand-roll copy affordances (custom icons, click-to-copy text) — this component is the
4783
- * one copy control; pick icon-only or labelled mode instead.
4783
+ * - To show a value with a copy button beside it, use `CopyableValue` — it owns the layout,
4784
+ * truncation and hover highlight. Do not pair `Text` and `CopyButton` by hand.
4785
+ * - Do not hand-roll copy affordances (custom icons, click-to-copy text) — use this component or
4786
+ * `CopyableValue` instead.
4784
4787
  *
4785
4788
  * ### Empty values
4786
4789
  * CopyButton renders nothing when `value` is empty, so consumers never present a copy control
4787
4790
  * that has no meaningful result.
4788
4791
  *
4789
- * @example Icon button next to plain text
4792
+ * @example Labelled button for an invite link
4790
4793
  * ```tsx
4791
- * import { CopyButton, Text } from "@trackunit/react-components";
4794
+ * import { CopyButton } from "@trackunit/react-components";
4792
4795
  *
4793
- * const ShortCode = ({ code }: { code: string }) => (
4794
- * <div className="flex items-center gap-0.5">
4795
- * <Text type="span">{code}</Text>
4796
- * <CopyButton value={code} />
4797
- * </div>
4796
+ * const CopyInviteLink = ({ link }: { link: string }) => (
4797
+ * <CopyButton label="Copy link" value={link} variant="secondary" />
4798
4798
  * );
4799
4799
  * ```
4800
4800
  * @param {CopyButtonProps} props - The props for the CopyButton component
@@ -4826,7 +4826,7 @@ const CopyButton = ({ value, copyLabel, copiedLabel, size = "small", label, vari
4826
4826
  setShowCopied(true);
4827
4827
  clearTimeout(resetTimerRef.current);
4828
4828
  resetTimerRef.current = setTimeout(() => setShowCopied(false), COPIED_FEEDBACK_DURATION_MS);
4829
- onCopy?.();
4829
+ onCopy?.(value);
4830
4830
  }, () => undefined);
4831
4831
  };
4832
4832
  const buttonSize = toButtonSize(size);
@@ -4844,6 +4844,125 @@ const CopyButton = ({ value, copyLabel, copiedLabel, size = "small", label, vari
4844
4844
  CopyButton.displayName = "CopyButton";
4845
4845
  const COPIED_FEEDBACK_DURATION_MS = 2000;
4846
4846
 
4847
+ const cvaEmptyValue = cssClassVarianceUtilities.cva({ base: ["text-neutral-400"] });
4848
+
4849
+ /**
4850
+ * EmptyValue renders a consistent dash symbol ("–") to represent missing, null, undefined, or not applicable values.
4851
+ * It is primarily used in tables and detail lists to maintain visual consistency when data is absent.
4852
+ *
4853
+ * ### When to use
4854
+ * Use EmptyValue as a placeholder in table cells, detail lists, or any data display where values may be missing.
4855
+ *
4856
+ * ### When not to use
4857
+ * Do not use EmptyValue for empty pages or sections — use `EmptyState` instead.
4858
+ *
4859
+ * @example Empty value in a details row
4860
+ * ```tsx
4861
+ * import { EmptyValue, Text } from "@trackunit/react-components";
4862
+ *
4863
+ * const AssetDetail = ({ value }: { value?: string }) => (
4864
+ * <div className="flex justify-between">
4865
+ * <Text weight="bold">Serial Number</Text>
4866
+ * {value ? <Text>{value}</Text> : <EmptyValue />}
4867
+ * </div>
4868
+ * );
4869
+ * ```
4870
+ * @param {EmptyValueProps} props - The props for the EmptyValue component
4871
+ * @returns {ReactElement} EmptyValue component
4872
+ */
4873
+ const EmptyValue = ({ className, "data-testid": dataTestId, style, ref }) => {
4874
+ return (jsxRuntime.jsx("div", { className: cvaEmptyValue({ className }), "data-testid": dataTestId, ref: ref, style: style, children: "-" }));
4875
+ };
4876
+
4877
+ // Named group so the highlight only reacts to this pair's own button, even when pairs are nested
4878
+ // inside other `group` scopes (table rows, cards).
4879
+ // `-ml-1` offsets the value's `pl-1`, so the text lines up with plain text and column headers while
4880
+ // the highlight's left padding reaches into the surrounding space (e.g. the cell padding); the
4881
+ // widened max width keeps the right edge where it was.
4882
+ const cvaCopyableValue = cssClassVarianceUtilities.cva({
4883
+ base: ["group/copyable-value", "items-center", "-ml-1", "max-w-[calc(100%+0.25rem)]"],
4884
+ variants: {
4885
+ inline: {
4886
+ // `overflow-hidden` + the max width defeat the grid/flex `min-width: auto` blowout, so the
4887
+ // value truncates inside table cells instead of widening them.
4888
+ false: ["flex", "min-w-0", "overflow-hidden"],
4889
+ true: ["inline-flex"],
4890
+ },
4891
+ },
4892
+ defaultVariants: {
4893
+ inline: false,
4894
+ },
4895
+ });
4896
+ // The value stretches to the button's height and tints while the button is hovered or keyboard-
4897
+ // focused, so value and button read as one fused control. `focus-visible` (not `focus`) keeps the
4898
+ // tint from sticking on a row after a mouse click. Anchors inside the value do not trigger it.
4899
+ const cvaCopyableValueText = cssClassVarianceUtilities.cva({
4900
+ base: [
4901
+ "min-w-0",
4902
+ "self-stretch",
4903
+ "content-center",
4904
+ "rounded-l-md",
4905
+ "pl-1",
4906
+ "pr-0.5",
4907
+ "transition-colors",
4908
+ "group-has-[button:hover]/copyable-value:bg-primary-100",
4909
+ "group-has-[button:focus-visible]/copyable-value:bg-primary-100",
4910
+ ],
4911
+ variants: {
4912
+ inline: {
4913
+ false: ["truncate"],
4914
+ // Lets values without spaces (URLs, tokens) wrap inside their box instead of running under
4915
+ // the button.
4916
+ true: ["[overflow-wrap:anywhere]"],
4917
+ },
4918
+ },
4919
+ defaultVariants: {
4920
+ inline: false,
4921
+ },
4922
+ });
4923
+ // Squares the button's left edge against the value and matches the value's tint, completing the
4924
+ // fused look. The tint is owned here, inside the DS, rather than at call sites — the same way
4925
+ // CopyButton composes Button through cvaIconButton.
4926
+ const cvaCopyableValueButton = cssClassVarianceUtilities.cva({
4927
+ base: ["shrink-0", "rounded-l-none", "hover:bg-primary-100", "focus-visible:bg-primary-100"],
4928
+ });
4929
+
4930
+ /**
4931
+ * CopyableValue displays a value as plain (non-clickable) text with a copy button beside it. While the
4932
+ * button is hovered or focused, the value is highlighted together with it, so the two read as one control
4933
+ * and it is clear what will be copied.
4934
+ *
4935
+ * ### When to use
4936
+ * Use CopyableValue whenever a value such as an ID, short code, URL or token is shown and should be
4937
+ * copyable — in table cells, detail lists, cards and drawers. Long values truncate to the available
4938
+ * width while the full value is still copied.
4939
+ *
4940
+ * ### When not to use
4941
+ * - Do not hand-roll a `Text` + `CopyButton` pair — this component owns the layout, truncation and highlight.
4942
+ * - When copying is the primary action of a panel (e.g. an invite link), use `CopyButton` with a `label` instead.
4943
+ * - Do not make the value itself click-to-copy — only the button copies, so the surrounding row or
4944
+ * card stays clickable.
4945
+ *
4946
+ * @example Copyable account ID in a table cell
4947
+ * ```tsx
4948
+ * import { CopyableValue } from "@trackunit/react-components";
4949
+ *
4950
+ * const AccountIdCell = ({ accountId }: { accountId: string }) => (
4951
+ * <CopyableValue size="extraSmall" value={accountId} />
4952
+ * );
4953
+ * ```
4954
+ * @param {CopyableValueProps} props - The props for the CopyableValue component
4955
+ * @returns {ReactElement} CopyableValue component
4956
+ */
4957
+ const CopyableValue = ({ value, children, copyLabel, copiedLabel, size = "small", textSize = "medium", inline = false, onCopy, className, style, "data-testid": dataTestId, ref, }) => {
4958
+ const [t] = useTranslation();
4959
+ if (value === "" && children === undefined) {
4960
+ return (jsxRuntime.jsx(EmptyValue, { className: className, "data-testid": dataTestId ? `${dataTestId}-empty` : undefined, style: style }));
4961
+ }
4962
+ return (jsxRuntime.jsxs("span", { className: cvaCopyableValue({ inline, className }), "data-testid": dataTestId, ref: ref, style: style, children: [jsxRuntime.jsx(Text, { className: cvaCopyableValueText({ inline }), size: textSize, type: "span", children: children ?? value }), jsxRuntime.jsx(CopyButton, { className: cvaCopyableValueButton(), copiedLabel: copiedLabel, copyLabel: copyLabel ?? t("copyableValue.copyValue", { value }), "data-testid": dataTestId ? `${dataTestId}-button` : undefined, onCopy: onCopy, size: size, value: value })] }));
4963
+ };
4964
+ CopyableValue.displayName = "CopyableValue";
4965
+
4847
4966
  const cvaContainerStyles = cssClassVarianceUtilities.cva({
4848
4967
  base: [
4849
4968
  "flex",
@@ -4979,36 +5098,6 @@ const EmptyState = ({ description, altText, image = "SEARCH_DOCUMENT", customIma
4979
5098
  return (jsxRuntime.jsx("div", { className: cvaContainerStyles({ className }), "data-testid": dataTestId ?? "empty-state", ref: ref, style: style, children: loading ? (jsxRuntime.jsxs("div", { className: "flex w-full flex-col items-center gap-4", children: [jsxRuntime.jsx(Spinner, { centering: "centered", "data-testid": "spinner" }), jsxRuntime.jsx(SkeletonLabel, { textSize: "text-base", width: "clamp(20%, 200px, 80%)" })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [customImageSrc !== null && customImageSrc !== undefined ? (typeof customImageSrc === "string" ? (jsxRuntime.jsx("img", { alt: altText, className: cvaImgStyles(), height: 200, src: customImageSrc, width: 200 })) : (customImageSrc)) : (typeof ImageSource !== "undefined" && (jsxRuntime.jsx(ImageSource, { "data-testid": "empty-state-image", height: 200, width: 200 }, image))), description !== undefined && description !== "" ? (jsxRuntime.jsx(Text, { align: "center", size: "medium", children: description })) : null, jsxRuntime.jsxs("div", { className: "mt-4 grid gap-3", children: [jsxRuntime.jsxs("div", { className: "flex gap-3", children: [secondaryAction ? (jsxRuntime.jsx(Button, { "data-testid": "empty-state-secondary-button", disabled: secondaryAction.disabled, onClick: secondaryAction.onClick, variant: "secondary", children: secondaryAction.to ? (jsxRuntime.jsx(reactRouter.Link, { params: secondaryAction.to.parameters, to: secondaryAction.to.pathname, children: secondaryAction.title })) : (secondaryAction.title) })) : null, primaryAction ? (jsxRuntime.jsx(Button, { "data-testid": "empty-state-primary-button", disabled: primaryAction.disabled, onClick: primaryAction.onClick, children: primaryAction.to ? (jsxRuntime.jsx(reactRouter.Link, { params: primaryAction.to.parameters, to: primaryAction.to.pathname, children: primaryAction.title })) : (primaryAction.title) })) : null] }), additionalHelpAction?.to ? (jsxRuntime.jsx(Button, { asChild: true, "data-testid": "empty-state-additional-button", disabled: additionalHelpAction.disabled, onClick: additionalHelpAction.onClick, suffix: jsxRuntime.jsx(Icon, { name: "ArrowTopRightOnSquare", size: "small" }), variant: "ghost", children: jsxRuntime.jsx(reactRouter.Link, { params: additionalHelpAction.to.parameters, target: additionalHelpAction.to.target, to: additionalHelpAction.to.pathname, children: additionalHelpAction.title }) })) : null] })] })) }));
4980
5099
  };
4981
5100
 
4982
- const cvaEmptyValue = cssClassVarianceUtilities.cva({ base: ["text-neutral-400"] });
4983
-
4984
- /**
4985
- * EmptyValue renders a consistent dash symbol ("–") to represent missing, null, undefined, or not applicable values.
4986
- * It is primarily used in tables and detail lists to maintain visual consistency when data is absent.
4987
- *
4988
- * ### When to use
4989
- * Use EmptyValue as a placeholder in table cells, detail lists, or any data display where values may be missing.
4990
- *
4991
- * ### When not to use
4992
- * Do not use EmptyValue for empty pages or sections — use `EmptyState` instead.
4993
- *
4994
- * @example Empty value in a details row
4995
- * ```tsx
4996
- * import { EmptyValue, Text } from "@trackunit/react-components";
4997
- *
4998
- * const AssetDetail = ({ value }: { value?: string }) => (
4999
- * <div className="flex justify-between">
5000
- * <Text weight="bold">Serial Number</Text>
5001
- * {value ? <Text>{value}</Text> : <EmptyValue />}
5002
- * </div>
5003
- * );
5004
- * ```
5005
- * @param {EmptyValueProps} props - The props for the EmptyValue component
5006
- * @returns {ReactElement} EmptyValue component
5007
- */
5008
- const EmptyValue = ({ className, "data-testid": dataTestId, style, ref }) => {
5009
- return (jsxRuntime.jsx("div", { className: cvaEmptyValue({ className }), "data-testid": dataTestId, ref: ref, style: style, children: "-" }));
5010
- };
5011
-
5012
5101
  const cvaExternalLink = cssClassVarianceUtilities.cva({
5013
5102
  base: ["underline", "decoration-[1.5px]", "underline-offset-2", "text-sm"],
5014
5103
  variants: {
@@ -15830,6 +15919,7 @@ exports.CardHeader = CardHeader;
15830
15919
  exports.Collapse = Collapse;
15831
15920
  exports.CompletionStatusIndicator = CompletionStatusIndicator;
15832
15921
  exports.CopyButton = CopyButton;
15922
+ exports.CopyableValue = CopyableValue;
15833
15923
  exports.DEFAULT_SKELETON_PREFERENCE_CARD_PROPS = DEFAULT_SKELETON_PREFERENCE_CARD_PROPS;
15834
15924
  exports.DetailsList = DetailsList;
15835
15925
  exports.EmptyState = EmptyState;
package/index.esm.js CHANGED
@@ -27,6 +27,7 @@ import { z } from 'zod';
27
27
  var defaultTranslations = {
28
28
  "breadcrumb.backButton": "Back",
29
29
  "breadcrumb.expandButton": "expand",
30
+ "copyableValue.copyValue": "Copy {{value}}",
30
31
  "copyButton.copied": "Copied!",
31
32
  "copyButton.copy": "Copy",
32
33
  "horizontalOverflowScroller.scrollLeft": "Scroll left",
@@ -4772,28 +4773,27 @@ const useCopyToClipboard = () => {
4772
4773
  * element, so the button can be placed next to plain (non-clickable) text.
4773
4774
  *
4774
4775
  * ### When to use
4775
- * - Icon-only (default): the copyable value is displayed as plain text (e.g. in a table cell or
4776
- * row) and only a dedicated icon should trigger the copy, keeping the rest of the row clickable.
4777
4776
  * - Labelled (`label` set): copying is the primary action of a panel or section — e.g. an invite
4778
4777
  * link — and deserves a full button that is itself the click target.
4778
+ * - Icon-only (default): a copy control on its own, where no value is displayed next to it — e.g.
4779
+ * in the corner of a code block.
4779
4780
  *
4780
4781
  * ### When not to use
4781
- * Do not hand-roll copy affordances (custom icons, click-to-copy text) — this component is the
4782
- * one copy control; pick icon-only or labelled mode instead.
4782
+ * - To show a value with a copy button beside it, use `CopyableValue` — it owns the layout,
4783
+ * truncation and hover highlight. Do not pair `Text` and `CopyButton` by hand.
4784
+ * - Do not hand-roll copy affordances (custom icons, click-to-copy text) — use this component or
4785
+ * `CopyableValue` instead.
4783
4786
  *
4784
4787
  * ### Empty values
4785
4788
  * CopyButton renders nothing when `value` is empty, so consumers never present a copy control
4786
4789
  * that has no meaningful result.
4787
4790
  *
4788
- * @example Icon button next to plain text
4791
+ * @example Labelled button for an invite link
4789
4792
  * ```tsx
4790
- * import { CopyButton, Text } from "@trackunit/react-components";
4793
+ * import { CopyButton } from "@trackunit/react-components";
4791
4794
  *
4792
- * const ShortCode = ({ code }: { code: string }) => (
4793
- * <div className="flex items-center gap-0.5">
4794
- * <Text type="span">{code}</Text>
4795
- * <CopyButton value={code} />
4796
- * </div>
4795
+ * const CopyInviteLink = ({ link }: { link: string }) => (
4796
+ * <CopyButton label="Copy link" value={link} variant="secondary" />
4797
4797
  * );
4798
4798
  * ```
4799
4799
  * @param {CopyButtonProps} props - The props for the CopyButton component
@@ -4825,7 +4825,7 @@ const CopyButton = ({ value, copyLabel, copiedLabel, size = "small", label, vari
4825
4825
  setShowCopied(true);
4826
4826
  clearTimeout(resetTimerRef.current);
4827
4827
  resetTimerRef.current = setTimeout(() => setShowCopied(false), COPIED_FEEDBACK_DURATION_MS);
4828
- onCopy?.();
4828
+ onCopy?.(value);
4829
4829
  }, () => undefined);
4830
4830
  };
4831
4831
  const buttonSize = toButtonSize(size);
@@ -4843,6 +4843,125 @@ const CopyButton = ({ value, copyLabel, copiedLabel, size = "small", label, vari
4843
4843
  CopyButton.displayName = "CopyButton";
4844
4844
  const COPIED_FEEDBACK_DURATION_MS = 2000;
4845
4845
 
4846
+ const cvaEmptyValue = cva({ base: ["text-neutral-400"] });
4847
+
4848
+ /**
4849
+ * EmptyValue renders a consistent dash symbol ("–") to represent missing, null, undefined, or not applicable values.
4850
+ * It is primarily used in tables and detail lists to maintain visual consistency when data is absent.
4851
+ *
4852
+ * ### When to use
4853
+ * Use EmptyValue as a placeholder in table cells, detail lists, or any data display where values may be missing.
4854
+ *
4855
+ * ### When not to use
4856
+ * Do not use EmptyValue for empty pages or sections — use `EmptyState` instead.
4857
+ *
4858
+ * @example Empty value in a details row
4859
+ * ```tsx
4860
+ * import { EmptyValue, Text } from "@trackunit/react-components";
4861
+ *
4862
+ * const AssetDetail = ({ value }: { value?: string }) => (
4863
+ * <div className="flex justify-between">
4864
+ * <Text weight="bold">Serial Number</Text>
4865
+ * {value ? <Text>{value}</Text> : <EmptyValue />}
4866
+ * </div>
4867
+ * );
4868
+ * ```
4869
+ * @param {EmptyValueProps} props - The props for the EmptyValue component
4870
+ * @returns {ReactElement} EmptyValue component
4871
+ */
4872
+ const EmptyValue = ({ className, "data-testid": dataTestId, style, ref }) => {
4873
+ return (jsx("div", { className: cvaEmptyValue({ className }), "data-testid": dataTestId, ref: ref, style: style, children: "-" }));
4874
+ };
4875
+
4876
+ // Named group so the highlight only reacts to this pair's own button, even when pairs are nested
4877
+ // inside other `group` scopes (table rows, cards).
4878
+ // `-ml-1` offsets the value's `pl-1`, so the text lines up with plain text and column headers while
4879
+ // the highlight's left padding reaches into the surrounding space (e.g. the cell padding); the
4880
+ // widened max width keeps the right edge where it was.
4881
+ const cvaCopyableValue = cva({
4882
+ base: ["group/copyable-value", "items-center", "-ml-1", "max-w-[calc(100%+0.25rem)]"],
4883
+ variants: {
4884
+ inline: {
4885
+ // `overflow-hidden` + the max width defeat the grid/flex `min-width: auto` blowout, so the
4886
+ // value truncates inside table cells instead of widening them.
4887
+ false: ["flex", "min-w-0", "overflow-hidden"],
4888
+ true: ["inline-flex"],
4889
+ },
4890
+ },
4891
+ defaultVariants: {
4892
+ inline: false,
4893
+ },
4894
+ });
4895
+ // The value stretches to the button's height and tints while the button is hovered or keyboard-
4896
+ // focused, so value and button read as one fused control. `focus-visible` (not `focus`) keeps the
4897
+ // tint from sticking on a row after a mouse click. Anchors inside the value do not trigger it.
4898
+ const cvaCopyableValueText = cva({
4899
+ base: [
4900
+ "min-w-0",
4901
+ "self-stretch",
4902
+ "content-center",
4903
+ "rounded-l-md",
4904
+ "pl-1",
4905
+ "pr-0.5",
4906
+ "transition-colors",
4907
+ "group-has-[button:hover]/copyable-value:bg-primary-100",
4908
+ "group-has-[button:focus-visible]/copyable-value:bg-primary-100",
4909
+ ],
4910
+ variants: {
4911
+ inline: {
4912
+ false: ["truncate"],
4913
+ // Lets values without spaces (URLs, tokens) wrap inside their box instead of running under
4914
+ // the button.
4915
+ true: ["[overflow-wrap:anywhere]"],
4916
+ },
4917
+ },
4918
+ defaultVariants: {
4919
+ inline: false,
4920
+ },
4921
+ });
4922
+ // Squares the button's left edge against the value and matches the value's tint, completing the
4923
+ // fused look. The tint is owned here, inside the DS, rather than at call sites — the same way
4924
+ // CopyButton composes Button through cvaIconButton.
4925
+ const cvaCopyableValueButton = cva({
4926
+ base: ["shrink-0", "rounded-l-none", "hover:bg-primary-100", "focus-visible:bg-primary-100"],
4927
+ });
4928
+
4929
+ /**
4930
+ * CopyableValue displays a value as plain (non-clickable) text with a copy button beside it. While the
4931
+ * button is hovered or focused, the value is highlighted together with it, so the two read as one control
4932
+ * and it is clear what will be copied.
4933
+ *
4934
+ * ### When to use
4935
+ * Use CopyableValue whenever a value such as an ID, short code, URL or token is shown and should be
4936
+ * copyable — in table cells, detail lists, cards and drawers. Long values truncate to the available
4937
+ * width while the full value is still copied.
4938
+ *
4939
+ * ### When not to use
4940
+ * - Do not hand-roll a `Text` + `CopyButton` pair — this component owns the layout, truncation and highlight.
4941
+ * - When copying is the primary action of a panel (e.g. an invite link), use `CopyButton` with a `label` instead.
4942
+ * - Do not make the value itself click-to-copy — only the button copies, so the surrounding row or
4943
+ * card stays clickable.
4944
+ *
4945
+ * @example Copyable account ID in a table cell
4946
+ * ```tsx
4947
+ * import { CopyableValue } from "@trackunit/react-components";
4948
+ *
4949
+ * const AccountIdCell = ({ accountId }: { accountId: string }) => (
4950
+ * <CopyableValue size="extraSmall" value={accountId} />
4951
+ * );
4952
+ * ```
4953
+ * @param {CopyableValueProps} props - The props for the CopyableValue component
4954
+ * @returns {ReactElement} CopyableValue component
4955
+ */
4956
+ const CopyableValue = ({ value, children, copyLabel, copiedLabel, size = "small", textSize = "medium", inline = false, onCopy, className, style, "data-testid": dataTestId, ref, }) => {
4957
+ const [t] = useTranslation();
4958
+ if (value === "" && children === undefined) {
4959
+ return (jsx(EmptyValue, { className: className, "data-testid": dataTestId ? `${dataTestId}-empty` : undefined, style: style }));
4960
+ }
4961
+ return (jsxs("span", { className: cvaCopyableValue({ inline, className }), "data-testid": dataTestId, ref: ref, style: style, children: [jsx(Text, { className: cvaCopyableValueText({ inline }), size: textSize, type: "span", children: children ?? value }), jsx(CopyButton, { className: cvaCopyableValueButton(), copiedLabel: copiedLabel, copyLabel: copyLabel ?? t("copyableValue.copyValue", { value }), "data-testid": dataTestId ? `${dataTestId}-button` : undefined, onCopy: onCopy, size: size, value: value })] }));
4962
+ };
4963
+ CopyableValue.displayName = "CopyableValue";
4964
+
4846
4965
  const cvaContainerStyles = cva({
4847
4966
  base: [
4848
4967
  "flex",
@@ -4978,36 +5097,6 @@ const EmptyState = ({ description, altText, image = "SEARCH_DOCUMENT", customIma
4978
5097
  return (jsx("div", { className: cvaContainerStyles({ className }), "data-testid": dataTestId ?? "empty-state", ref: ref, style: style, children: loading ? (jsxs("div", { className: "flex w-full flex-col items-center gap-4", children: [jsx(Spinner, { centering: "centered", "data-testid": "spinner" }), jsx(SkeletonLabel, { textSize: "text-base", width: "clamp(20%, 200px, 80%)" })] })) : (jsxs(Fragment, { children: [customImageSrc !== null && customImageSrc !== undefined ? (typeof customImageSrc === "string" ? (jsx("img", { alt: altText, className: cvaImgStyles(), height: 200, src: customImageSrc, width: 200 })) : (customImageSrc)) : (typeof ImageSource !== "undefined" && (jsx(ImageSource, { "data-testid": "empty-state-image", height: 200, width: 200 }, image))), description !== undefined && description !== "" ? (jsx(Text, { align: "center", size: "medium", children: description })) : null, jsxs("div", { className: "mt-4 grid gap-3", children: [jsxs("div", { className: "flex gap-3", children: [secondaryAction ? (jsx(Button, { "data-testid": "empty-state-secondary-button", disabled: secondaryAction.disabled, onClick: secondaryAction.onClick, variant: "secondary", children: secondaryAction.to ? (jsx(Link, { params: secondaryAction.to.parameters, to: secondaryAction.to.pathname, children: secondaryAction.title })) : (secondaryAction.title) })) : null, primaryAction ? (jsx(Button, { "data-testid": "empty-state-primary-button", disabled: primaryAction.disabled, onClick: primaryAction.onClick, children: primaryAction.to ? (jsx(Link, { params: primaryAction.to.parameters, to: primaryAction.to.pathname, children: primaryAction.title })) : (primaryAction.title) })) : null] }), additionalHelpAction?.to ? (jsx(Button, { asChild: true, "data-testid": "empty-state-additional-button", disabled: additionalHelpAction.disabled, onClick: additionalHelpAction.onClick, suffix: jsx(Icon, { name: "ArrowTopRightOnSquare", size: "small" }), variant: "ghost", children: jsx(Link, { params: additionalHelpAction.to.parameters, target: additionalHelpAction.to.target, to: additionalHelpAction.to.pathname, children: additionalHelpAction.title }) })) : null] })] })) }));
4979
5098
  };
4980
5099
 
4981
- const cvaEmptyValue = cva({ base: ["text-neutral-400"] });
4982
-
4983
- /**
4984
- * EmptyValue renders a consistent dash symbol ("–") to represent missing, null, undefined, or not applicable values.
4985
- * It is primarily used in tables and detail lists to maintain visual consistency when data is absent.
4986
- *
4987
- * ### When to use
4988
- * Use EmptyValue as a placeholder in table cells, detail lists, or any data display where values may be missing.
4989
- *
4990
- * ### When not to use
4991
- * Do not use EmptyValue for empty pages or sections — use `EmptyState` instead.
4992
- *
4993
- * @example Empty value in a details row
4994
- * ```tsx
4995
- * import { EmptyValue, Text } from "@trackunit/react-components";
4996
- *
4997
- * const AssetDetail = ({ value }: { value?: string }) => (
4998
- * <div className="flex justify-between">
4999
- * <Text weight="bold">Serial Number</Text>
5000
- * {value ? <Text>{value}</Text> : <EmptyValue />}
5001
- * </div>
5002
- * );
5003
- * ```
5004
- * @param {EmptyValueProps} props - The props for the EmptyValue component
5005
- * @returns {ReactElement} EmptyValue component
5006
- */
5007
- const EmptyValue = ({ className, "data-testid": dataTestId, style, ref }) => {
5008
- return (jsx("div", { className: cvaEmptyValue({ className }), "data-testid": dataTestId, ref: ref, style: style, children: "-" }));
5009
- };
5010
-
5011
5100
  const cvaExternalLink = cva({
5012
5101
  base: ["underline", "decoration-[1.5px]", "underline-offset-2", "text-sm"],
5013
5102
  variants: {
@@ -15814,4 +15903,4 @@ const useWindowActivity = ({ onFocus, onBlur, skip = false } = { onBlur: undefin
15814
15903
  */
15815
15904
  setupLibraryTranslations();
15816
15905
 
15817
- export { Alert, Badge, Breadcrumb, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyButton, DEFAULT_SKELETON_PREFERENCE_CARD_PROPS, DetailsList, EmptyState, EmptyValue, ExternalLink, GridAreas, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, IdentityCard, IdentityCardSkeleton, Indicator, KPI, KPICard, KPICardSkeleton, KPISkeleton, LabeledValue, LabeledValueList, List, ListItem, MAX_HASH_LENGTH, MAX_URL_LENGTH, MenuContent, MenuDivider, MenuItem, MenuTree, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, PreferenceCard, PreferenceCardSkeleton, Prompt, ROLE_CARD, SHEET_TRANSITION_DURATION, SHEET_TRANSITION_DURATION_MS, SHEET_TRANSITION_EASING, SectionHeader, SegmentedValueBar, Sheet, Sidebar, SidebarContentLayout, SkeletonBlock, SkeletonLabel, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, createGrid, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaContentContainer, cvaContentWrapper, cvaDescriptionCard, cvaIconBackground, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInputContainer, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaPreferenceCard, cvaTitleCard, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, preferenceCardGrid, useBidirectionalScroll, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCursorUrlSync, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGridAreas, useHashParamSync, useHold, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useKeyboardShortcut, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMenuSearchField, useMenuTree, useMergeRefs, useModifierKey, useOptionalPopoverContext, useOverflowBorder, useOverflowItems, usePersistedState, usePopoverContext, usePrevious, usePrompt, useRandomCSSLengths, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSearchParamSync, useSelfUpdatingRef, useSessionStorage, useSessionStorageReducer, useSheet, useSheetSnap, useStorageKey, useStripForeignIdentityHash, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
15906
+ export { Alert, Badge, Breadcrumb, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyButton, CopyableValue, DEFAULT_SKELETON_PREFERENCE_CARD_PROPS, DetailsList, EmptyState, EmptyValue, ExternalLink, GridAreas, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, IdentityCard, IdentityCardSkeleton, Indicator, KPI, KPICard, KPICardSkeleton, KPISkeleton, LabeledValue, LabeledValueList, List, ListItem, MAX_HASH_LENGTH, MAX_URL_LENGTH, MenuContent, MenuDivider, MenuItem, MenuTree, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, PreferenceCard, PreferenceCardSkeleton, Prompt, ROLE_CARD, SHEET_TRANSITION_DURATION, SHEET_TRANSITION_DURATION_MS, SHEET_TRANSITION_EASING, SectionHeader, SegmentedValueBar, Sheet, Sidebar, SidebarContentLayout, SkeletonBlock, SkeletonLabel, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, createGrid, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaContentContainer, cvaContentWrapper, cvaDescriptionCard, cvaIconBackground, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInputContainer, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaPreferenceCard, cvaTitleCard, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, preferenceCardGrid, useBidirectionalScroll, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCursorUrlSync, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGridAreas, useHashParamSync, useHold, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useKeyboardShortcut, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMenuSearchField, useMenuTree, useMergeRefs, useModifierKey, useOptionalPopoverContext, useOverflowBorder, useOverflowItems, usePersistedState, usePopoverContext, usePrevious, usePrompt, useRandomCSSLengths, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSearchParamSync, useSelfUpdatingRef, useSessionStorage, useSessionStorageReducer, useSheet, useSheetSnap, useStorageKey, useStripForeignIdentityHash, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "3.5.6",
3
+ "version": "3.7.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.17.3",
18
- "@trackunit/css-class-variance-utilities": "2.2.3",
19
- "@trackunit/shared-utils": "1.18.3",
20
- "@trackunit/ui-icons": "1.16.4",
17
+ "@trackunit/ui-design-tokens": "1.18.0",
18
+ "@trackunit/css-class-variance-utilities": "2.3.0",
19
+ "@trackunit/shared-utils": "1.19.0",
20
+ "@trackunit/ui-icons": "1.17.0",
21
21
  "es-toolkit": "^1.52.0",
22
22
  "@tanstack/react-virtual": "^3.14.13",
23
23
  "dequal": "^2.0.3",
24
24
  "fflate": "^0.8.3",
25
25
  "zod": "^3.25.76",
26
- "@trackunit/i18n-library-translation": "2.7.4"
26
+ "@trackunit/i18n-library-translation": "2.8.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react": "^19.0.0",
@@ -41,11 +41,11 @@ export interface CopyButtonProps extends CommonProps, Styleable, Refable<HTMLBut
41
41
  */
42
42
  variant?: ButtonVariant;
43
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.
44
+ * Called with the copied value after it has been written to the clipboard —
45
+ * via the Clipboard API or the `execCommand("copy")` fallback. Only skipped
46
+ * when both mechanisms fail.
47
47
  */
48
- onCopy?: () => void;
48
+ onCopy?: (value: string) => void;
49
49
  }
50
50
  /**
51
51
  * CopyButton is a standalone icon button that copies a value to the clipboard when clicked,
@@ -53,28 +53,27 @@ export interface CopyButtonProps extends CommonProps, Styleable, Refable<HTMLBut
53
53
  * element, so the button can be placed next to plain (non-clickable) text.
54
54
  *
55
55
  * ### When to use
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
56
  * - Labelled (`label` set): copying is the primary action of a panel or section — e.g. an invite
59
57
  * link — and deserves a full button that is itself the click target.
58
+ * - Icon-only (default): a copy control on its own, where no value is displayed next to it — e.g.
59
+ * in the corner of a code block.
60
60
  *
61
61
  * ### When not to use
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.
62
+ * - To show a value with a copy button beside it, use `CopyableValue` — it owns the layout,
63
+ * truncation and hover highlight. Do not pair `Text` and `CopyButton` by hand.
64
+ * - Do not hand-roll copy affordances (custom icons, click-to-copy text) — use this component or
65
+ * `CopyableValue` instead.
64
66
  *
65
67
  * ### Empty values
66
68
  * CopyButton renders nothing when `value` is empty, so consumers never present a copy control
67
69
  * that has no meaningful result.
68
70
  *
69
- * @example Icon button next to plain text
71
+ * @example Labelled button for an invite link
70
72
  * ```tsx
71
- * import { CopyButton, Text } from "@trackunit/react-components";
73
+ * import { CopyButton } from "@trackunit/react-components";
72
74
  *
73
- * const ShortCode = ({ code }: { code: string }) => (
74
- * <div className="flex items-center gap-0.5">
75
- * <Text type="span">{code}</Text>
76
- * <CopyButton value={code} />
77
- * </div>
75
+ * const CopyInviteLink = ({ link }: { link: string }) => (
76
+ * <CopyButton label="Copy link" value={link} variant="secondary" />
78
77
  * );
79
78
  * ```
80
79
  * @param {CopyButtonProps} props - The props for the CopyButton component
@@ -0,0 +1,86 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ import { CommonProps } from "../../common/CommonProps";
3
+ import { Refable } from "../../common/Refable";
4
+ import { Size } from "../../common/Size";
5
+ import type { Styleable } from "../../common/Styleable";
6
+ import { IconButtonSize } from "../buttons/shared/IconButtonSize";
7
+ export interface CopyableValueProps extends CommonProps, Styleable, Refable<HTMLSpanElement> {
8
+ /**
9
+ * The value copied to the clipboard. It is also what is displayed, unless `children` is set.
10
+ * An empty string renders the `EmptyValue` dash with no copy button, so missing data looks the
11
+ * same as everywhere else.
12
+ */
13
+ value: string;
14
+ /**
15
+ * What to display in place of `value` — e.g. a link that stands in for the id being copied.
16
+ * The copy button still copies `value`.
17
+ */
18
+ children?: ReactNode;
19
+ /**
20
+ * Accessible label for the copy button, and tooltip text shown before copying. Defaults to the
21
+ * library's localized "Copy {value}", so the full value is available even when the displayed
22
+ * text is truncated, and every row in a table gets a distinct accessible name. Pass a short label
23
+ * that names the value (e.g. "Copy token") for long values such as tokens, keys or long URLs.
24
+ */
25
+ copyLabel?: string;
26
+ /**
27
+ * Tooltip text shown briefly after a successful copy. Defaults to the library's localized "Copied!".
28
+ */
29
+ copiedLabel?: string;
30
+ /**
31
+ * The size of the copy button. Use `extraSmall` inside fixed-height contexts such as table rows,
32
+ * where the button must fit a single text line.
33
+ *
34
+ * @default "small"
35
+ */
36
+ size?: IconButtonSize;
37
+ /**
38
+ * The text size of the displayed value.
39
+ *
40
+ * @default "medium"
41
+ */
42
+ textSize?: Size;
43
+ /**
44
+ * Places the pair inside running text, such as a sentence. The pair sits in the line as one inline
45
+ * box, and a long value wraps inside that box instead of truncating. Set `textSize` to match the
46
+ * surrounding text.
47
+ *
48
+ * @default false
49
+ */
50
+ inline?: boolean;
51
+ /**
52
+ * Called with the copied value after it has been written to the clipboard.
53
+ */
54
+ onCopy?: (value: string) => void;
55
+ }
56
+ /**
57
+ * CopyableValue displays a value as plain (non-clickable) text with a copy button beside it. While the
58
+ * button is hovered or focused, the value is highlighted together with it, so the two read as one control
59
+ * and it is clear what will be copied.
60
+ *
61
+ * ### When to use
62
+ * Use CopyableValue whenever a value such as an ID, short code, URL or token is shown and should be
63
+ * copyable — in table cells, detail lists, cards and drawers. Long values truncate to the available
64
+ * width while the full value is still copied.
65
+ *
66
+ * ### When not to use
67
+ * - Do not hand-roll a `Text` + `CopyButton` pair — this component owns the layout, truncation and highlight.
68
+ * - When copying is the primary action of a panel (e.g. an invite link), use `CopyButton` with a `label` instead.
69
+ * - Do not make the value itself click-to-copy — only the button copies, so the surrounding row or
70
+ * card stays clickable.
71
+ *
72
+ * @example Copyable account ID in a table cell
73
+ * ```tsx
74
+ * import { CopyableValue } from "@trackunit/react-components";
75
+ *
76
+ * const AccountIdCell = ({ accountId }: { accountId: string }) => (
77
+ * <CopyableValue size="extraSmall" value={accountId} />
78
+ * );
79
+ * ```
80
+ * @param {CopyableValueProps} props - The props for the CopyableValue component
81
+ * @returns {ReactElement} CopyableValue component
82
+ */
83
+ export declare const CopyableValue: {
84
+ ({ value, children, copyLabel, copiedLabel, size, textSize, inline, onCopy, className, style, "data-testid": dataTestId, ref, }: CopyableValueProps): ReactElement;
85
+ displayName: string;
86
+ };
@@ -0,0 +1,60 @@
1
+ export declare const cvaCopyableValue: import("cva").CVAComponent<Omit<{
2
+ base: string[];
3
+ variants: {
4
+ inline: {
5
+ false: string[];
6
+ true: string[];
7
+ };
8
+ };
9
+ defaultVariants: {
10
+ inline: false;
11
+ };
12
+ }, "defaultVariants"> & {
13
+ variants: {
14
+ inline: {
15
+ false: string[];
16
+ true: string[];
17
+ };
18
+ };
19
+ defaultVariants: Omit<{}, "inline"> & {
20
+ inline: false;
21
+ };
22
+ }, {
23
+ inline: {
24
+ false: string[];
25
+ true: string[];
26
+ };
27
+ }>;
28
+ export declare const cvaCopyableValueText: import("cva").CVAComponent<Omit<{
29
+ base: string[];
30
+ variants: {
31
+ inline: {
32
+ false: string[];
33
+ true: string[];
34
+ };
35
+ };
36
+ defaultVariants: {
37
+ inline: false;
38
+ };
39
+ }, "defaultVariants"> & {
40
+ variants: {
41
+ inline: {
42
+ false: string[];
43
+ true: string[];
44
+ };
45
+ };
46
+ defaultVariants: Omit<{}, "inline"> & {
47
+ inline: false;
48
+ };
49
+ }, {
50
+ inline: {
51
+ false: string[];
52
+ true: string[];
53
+ };
54
+ }>;
55
+ export declare const cvaCopyableValueButton: import("cva").CVAComponent<Omit<{
56
+ base: string[];
57
+ }, "defaultVariants"> & {
58
+ variants: unknown;
59
+ defaultVariants: Omit<{}, never>;
60
+ }, unknown>;
package/src/index.d.ts CHANGED
@@ -24,6 +24,7 @@ export * from "./components/Card/CardHeader";
24
24
  export * from "./components/Clickable/Clickable.variants";
25
25
  export * from "./components/Collapse/Collapse";
26
26
  export * from "./components/CompletionStatusIndicator/CompletionStatusIndicator";
27
+ export * from "./components/CopyableValue/CopyableValue";
27
28
  export * from "./components/CopyButton/CopyButton";
28
29
  export * from "./components/DetailsList/DetailsList";
29
30
  export * from "./components/EmptyState/EmptyState";