@trackunit/react-components 2.13.28-alpha-15da733192f.0 → 3.0.1

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
@@ -25,7 +25,7 @@ var fflate = require('fflate');
25
25
  var dequal = require('dequal');
26
26
 
27
27
  var defaultTranslations = {
28
- "breadcrumb.backButton": "back",
28
+ "breadcrumb.backButton": "Back",
29
29
  "breadcrumb.expandButton": "expand",
30
30
  "copyButton.copied": "Copied!",
31
31
  "copyButton.copy": "Copy",
@@ -81,7 +81,7 @@ const cvaIcon = cssClassVarianceUtilities.cva({
81
81
  variants: {
82
82
  color: {
83
83
  primary: "text-primary-600",
84
- ai: "text-ai",
84
+ trackunit_branding: "text-trackunit_branding",
85
85
  neutral: "text-neutral-400",
86
86
  info: "text-info-600",
87
87
  success: "text-success-600",
@@ -143,7 +143,7 @@ const cvaIcon = cssClassVarianceUtilities.cva({
143
143
  const iconPalette = {
144
144
  ...uiDesignTokens.intentPalette,
145
145
  ...uiDesignTokens.generalPalette,
146
- ...uiDesignTokens.aiPalette,
146
+ ...uiDesignTokens.trackunitBrandingPalette,
147
147
  ...uiDesignTokens.criticalityPalette,
148
148
  ...uiDesignTokens.activityPalette,
149
149
  ...uiDesignTokens.utilizationPalette,
@@ -1835,15 +1835,6 @@ const cvaButtonPrefixSuffix = cssClassVarianceUtilities.cva({
1835
1835
  const cvaIconButton = cssClassVarianceUtilities.cva({
1836
1836
  base: [],
1837
1837
  variants: {
1838
- ai: {
1839
- true: [
1840
- "bg-ai",
1841
- "hover:bg-[color-mix(in_srgb,var(--color-ai),black_10%)]",
1842
- "active:bg-[color-mix(in_srgb,var(--color-ai),black_20%)]",
1843
- "focus:bg-[color-mix(in_srgb,var(--color-ai),black_20%)]",
1844
- ],
1845
- false: [],
1846
- },
1847
1838
  size: {
1848
1839
  /**
1849
1840
  * Sized to match a single text line (20px) so the button fits inside
@@ -2617,7 +2608,7 @@ const toButtonSize = (size) => (size === "extraSmall" ? "small" : size);
2617
2608
  */
2618
2609
  const IconButton = ({ icon, variant = "primary", size = "medium", square = true, loading = false, disabled = false, className, title, ariaLabel, "data-testid": dataTestId, ref, ...rest }) => {
2619
2610
  const accessibleLabel = ariaLabel ?? title;
2620
- 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, ai: variant === "ai", 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, variant: variant === "ai" ? "primary" : variant }) }));
2611
+ 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, variant: variant }) }));
2621
2612
  };
2622
2613
  IconButton.displayName = "IconButton";
2623
2614
 
@@ -3957,160 +3948,10 @@ const useCopyToClipboard = () => {
3957
3948
  return react.useMemo(() => [isCopied, copyToClipboard], [isCopied, copyToClipboard]);
3958
3949
  };
3959
3950
 
3960
- /**
3961
- * The useHover hook returns a onMouseEnter, onMouseLeave and a boolean indicating whether the element is being hovered.
3962
- * The boolean will be true if the element is being hovered, and false if it is not.
3963
- * Can be directionally debounced.
3964
- *
3965
- * @param {UseHoverProps} param0 The options
3966
- * @returns {object} The object containing the onMouseEnter, onMouseLeave and hovering props
3967
- */
3968
- const useHover = ({ debounced = false, delay = 100, direction = "out" } = { debounced: false }) => {
3969
- const [isHovering, setIsHovering] = react.useState(false);
3970
- const [debouncedIsHovering, setDebouncedIsHovering] = react.useState(false);
3971
- const onMouseEnter = react.useCallback(() => {
3972
- setIsHovering(true);
3973
- }, []);
3974
- const onMouseLeave = react.useCallback(() => {
3975
- setIsHovering(false);
3976
- }, []);
3977
- // Determine if the current transition should be debounced based on direction
3978
- const isTransitionDebounced = debounced && (direction === "both" || (direction === "in" && isHovering) || (direction === "out" && !isHovering));
3979
- // Sync debouncedIsHovering immediately for non-debounced transitions
3980
- react.useLayoutEffect(() => {
3981
- if (!debounced || isTransitionDebounced) {
3982
- return undefined;
3983
- }
3984
- // eslint-disable-next-line react-hooks/set-state-in-effect -- Synchronizing derived state for directional debouncing
3985
- setDebouncedIsHovering(isHovering);
3986
- }, [debounced, isTransitionDebounced, isHovering]);
3987
- // Apply delay for debounced transitions
3988
- react.useEffect(() => {
3989
- if (!isTransitionDebounced) {
3990
- return undefined;
3991
- }
3992
- const timer = setTimeout(() => {
3993
- setDebouncedIsHovering(isHovering);
3994
- }, delay);
3995
- return () => clearTimeout(timer);
3996
- }, [isTransitionDebounced, delay, isHovering]);
3997
- const value = debounced ? (isTransitionDebounced ? debouncedIsHovering : isHovering) : isHovering;
3998
- return react.useMemo(() => ({
3999
- onMouseEnter,
4000
- onMouseLeave,
4001
- hovering: value,
4002
- }), [onMouseEnter, onMouseLeave, value]);
4003
- };
4004
-
4005
- const cvaCopyableText = cssClassVarianceUtilities.cva({
4006
- base: [
4007
- "rounded",
4008
- "cursor-pointer",
4009
- "transition-all",
4010
- "ease-in-out",
4011
- "inline-flex",
4012
- "items-center",
4013
- "gap-1",
4014
- "bg-transparent",
4015
- "border-none",
4016
- "outline-none",
4017
- "hover:bg-neutral-200",
4018
- "py-0.5",
4019
- "px-1",
4020
- ],
4021
- variants: {
4022
- animating: {
4023
- false: "",
4024
- true: "animate-copy",
4025
- },
4026
- size: {
4027
- sm: "text-sm",
4028
- xs: "text-xs",
4029
- },
4030
- },
4031
- defaultVariants: {
4032
- animating: false,
4033
- size: "sm",
4034
- },
4035
- });
4036
-
4037
- /** Resolves the inner content for CopyableText based on withIcon. */
4038
- const Content = ({ text, withIcon, }) => {
4039
- if (withIcon) {
4040
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: "truncate", children: text }), jsxRuntime.jsx(Icon, { ariaHidden: true, className: "shrink-0 text-neutral-400", name: "Square2Stack", size: "small", type: "solid" })] }));
4041
- }
4042
- return text;
4043
- };
4044
- /**
4045
- * CopyableText displays a text value that the user can click to copy to the clipboard.
4046
- * It shows a brief animation and tooltip feedback on copy. What you see is what gets copied.
4047
- *
4048
- * ### When to use
4049
- * Use CopyableText for identifiers, serial numbers, URLs, or any value the user may want to copy (e.g., asset IDs, error codes).
4050
- *
4051
- * ### When not to use
4052
- * - Do not use CopyableText for long paragraphs or content that doesn't need to be copied.
4053
- * - If the value is rendered as plain (non-clickable) text and only a dedicated icon should copy it
4054
- * (e.g. inside a row or cell that should stay clickable), use `CopyButton` instead:
4055
- * ```tsx
4056
- * <div className="flex items-center gap-0.5">
4057
- * <Text type="span">{value}</Text>
4058
- * <CopyButton value={value} />
4059
- * </div>
4060
- * ```
4061
- * - Only use `useCopyToClipboard` directly for genuinely custom copy behavior that neither `CopyableText` nor `CopyButton` supports.
4062
- *
4063
- * @example Copyable serial number
4064
- * ```tsx
4065
- * import { CopyableText } from "@trackunit/react-components";
4066
- *
4067
- * const AssetSerial = () => (
4068
- * <CopyableText text="SN-2024-00142" data-testid="serial-number" />
4069
- * );
4070
- * ```
4071
- * @example Copyable text with small size
4072
- * ```tsx
4073
- * import { CopyableText } from "@trackunit/react-components";
4074
- *
4075
- * const CopyableId = () => (
4076
- * <CopyableText text="ABC-123" size="xs" />
4077
- * );
4078
- * ```
4079
- * @param {CopyableTextProps} props - The props for the CopyableText component
4080
- * @returns {ReactElement} CopyableText component
4081
- */
4082
- const CopyableText = ({ text, withIcon = true, size = "sm", copyLabel = "Copy", copiedLabel = "Copied!", "data-testid": dataTestId, className, style, ref, }) => {
4083
- const [animating, setAnimating] = react.useState(false);
4084
- const [showCopied, setShowCopied] = react.useState(false);
4085
- const [, copyToClipboard] = useCopyToClipboard();
4086
- const { onMouseEnter, onMouseLeave, hovering } = useHover({ debounced: false });
4087
- if (!text) {
4088
- return null;
4089
- }
4090
- const handleOnClick = (e) => {
4091
- e.stopPropagation();
4092
- void copyToClipboard(text)
4093
- .then(() => {
4094
- setShowCopied(true);
4095
- })
4096
- .catch(() => undefined);
4097
- setAnimating(true);
4098
- };
4099
- const handleMouseLeave = () => {
4100
- onMouseLeave();
4101
- setShowCopied(false);
4102
- };
4103
- return (jsxRuntime.jsx(Tooltip, { delayed: !showCopied, label: showCopied ? copiedLabel : copyLabel, placement: "top", children: jsxRuntime.jsx("button", { "aria-label": copyLabel, className: cvaCopyableText({ animating, size, className }), "data-testid": dataTestId, onAnimationEnd: () => setAnimating(false), onClick: handleOnClick, onFocus: e => {
4104
- if (hovering) {
4105
- e.currentTarget.blur();
4106
- }
4107
- }, onMouseEnter: onMouseEnter, onMouseLeave: handleMouseLeave, ref: ref, style: style, type: "button", children: jsxRuntime.jsx(Content, { text: text, withIcon: withIcon }) }) }));
4108
- };
4109
-
4110
3951
  /**
4111
3952
  * CopyButton is a standalone icon button that copies a value to the clipboard when clicked,
4112
- * showing brief tooltip feedback. Unlike CopyableText, the copied value is not itself the
4113
- * clickable/displayed element, so it can be placed next to plain (non-clickable) text.
3953
+ * showing brief tooltip feedback. The copied value is not itself the clickable/displayed
3954
+ * element, so the button can be placed next to plain (non-clickable) text.
4114
3955
  *
4115
3956
  * ### When to use
4116
3957
  * - Icon-only (default): the copyable value is displayed as plain text (e.g. in a table cell or
@@ -4123,8 +3964,8 @@ const CopyableText = ({ text, withIcon = true, size = "sm", copyLabel = "Copy",
4123
3964
  * one copy control; pick icon-only or labelled mode instead.
4124
3965
  *
4125
3966
  * ### Empty values
4126
- * Like `CopyableText`, CopyButton renders nothing when `value` is empty, so consumers never
4127
- * present a copy control that has no meaningful result.
3967
+ * CopyButton renders nothing when `value` is empty, so consumers never present a copy control
3968
+ * that has no meaningful result.
4128
3969
  *
4129
3970
  * @example Icon button next to plain text
4130
3971
  * ```tsx
@@ -8947,6 +8788,7 @@ const PageHeaderTitle = ({ title, "data-testid": dataTestId, className, style, r
8947
8788
  * @returns {ReactElement} PageHeader component
8948
8789
  */
8949
8790
  const PageHeader = ({ className, "data-testid": dataTestId, showLoading = false, description, title, tagLabel, backTo, tagColor, tabsList, descriptionIcon = "QuestionMarkCircle", tagTooltipLabel, style, ref, ...discriminatedProps }) => {
8791
+ const [t] = useTranslation();
8950
8792
  const tagRenderer = react.useMemo(() => {
8951
8793
  if (tagLabel === undefined || tagLabel === "" || showLoading) {
8952
8794
  return null;
@@ -8957,7 +8799,7 @@ const PageHeader = ({ className, "data-testid": dataTestId, showLoading = false,
8957
8799
  return (jsxRuntime.jsxs("div", { className: cvaPageHeaderContainer({
8958
8800
  className,
8959
8801
  withBorder: tabsList === undefined,
8960
- }), "data-testid": dataTestId, ref: ref, style: style, children: [jsxRuntime.jsxs("div", { className: cvaPageHeader(), children: [backTo ? (jsxRuntime.jsx(reactRouter.Link, { to: backTo, children: jsxRuntime.jsx(Button, { className: "mr-4 bg-black/5 hover:bg-black/10", prefix: jsxRuntime.jsx(Icon, { name: "ArrowLeft", size: "small" }), size: "small", square: true, variant: "ghost-neutral" }) })) : undefined, typeof title === "string" ? jsxRuntime.jsx(PageHeaderTitle, { "data-testid": dataTestId, title: title }) : title, tagRenderer || (description !== null && description !== undefined) ? (jsxRuntime.jsxs("div", { className: "mx-2 flex items-center gap-2", children: [description !== null && description !== undefined && !showLoading ? (jsxRuntime.jsx(Tooltip, { "data-testid": dataTestId ? `${dataTestId}-description-tooltip` : undefined, iconProps: {
8802
+ }), "data-testid": dataTestId, ref: ref, style: style, children: [jsxRuntime.jsxs("div", { className: cvaPageHeader(), children: [backTo ? (jsxRuntime.jsx(reactRouter.Link, { to: backTo, children: jsxRuntime.jsx(Button, { className: "mr-4 bg-black/5 hover:bg-black/10", "data-testid": dataTestId ? `${dataTestId}-back-button` : undefined, prefix: jsxRuntime.jsx(Icon, { name: "ArrowLeft", size: "small" }), size: "small", square: true, title: t("breadcrumb.backButton"), variant: "ghost-neutral" }) })) : undefined, typeof title === "string" ? jsxRuntime.jsx(PageHeaderTitle, { "data-testid": dataTestId, title: title }) : title, tagRenderer || (description !== null && description !== undefined) ? (jsxRuntime.jsxs("div", { className: "mx-2 flex items-center gap-2", children: [description !== null && description !== undefined && !showLoading ? (jsxRuntime.jsx(Tooltip, { "data-testid": dataTestId ? `${dataTestId}-description-tooltip` : undefined, iconProps: {
8961
8803
  name: descriptionIcon,
8962
8804
  "data-testid": "page-header-description-icon",
8963
8805
  }, label: description, placement: "bottom" })) : undefined, tagRenderer] })) : null, jsxRuntime.jsxs("div", { className: "ml-auto flex gap-2", children: [discriminatedProps.accessoryType === "kpi-metrics" ? (jsxRuntime.jsx(PageHeaderKpiMetrics, { kpiMetrics: discriminatedProps.kpiMetrics })) : null, discriminatedProps.accessoryType === "actions" ? (Array.isArray(discriminatedProps.secondaryActions) ? (jsxRuntime.jsx(PageHeaderSecondaryActions, { actions: discriminatedProps.secondaryActions, groupActions: discriminatedProps.groupSecondaryActions ?? false, hasPrimaryAction: !!discriminatedProps.primaryAction })) : discriminatedProps.secondaryActions !== null && discriminatedProps.secondaryActions !== undefined ? (discriminatedProps.secondaryActions) : null) : null, discriminatedProps.accessoryType === "actions" &&
@@ -10027,6 +9869,51 @@ const sheetAnimationReducer = (state, action) => {
10027
9869
  }
10028
9870
  };
10029
9871
 
9872
+ /**
9873
+ * The useHover hook returns a onMouseEnter, onMouseLeave and a boolean indicating whether the element is being hovered.
9874
+ * The boolean will be true if the element is being hovered, and false if it is not.
9875
+ * Can be directionally debounced.
9876
+ *
9877
+ * @param {UseHoverProps} param0 The options
9878
+ * @returns {object} The object containing the onMouseEnter, onMouseLeave and hovering props
9879
+ */
9880
+ const useHover = ({ debounced = false, delay = 100, direction = "out" } = { debounced: false }) => {
9881
+ const [isHovering, setIsHovering] = react.useState(false);
9882
+ const [debouncedIsHovering, setDebouncedIsHovering] = react.useState(false);
9883
+ const onMouseEnter = react.useCallback(() => {
9884
+ setIsHovering(true);
9885
+ }, []);
9886
+ const onMouseLeave = react.useCallback(() => {
9887
+ setIsHovering(false);
9888
+ }, []);
9889
+ // Determine if the current transition should be debounced based on direction
9890
+ const isTransitionDebounced = debounced && (direction === "both" || (direction === "in" && isHovering) || (direction === "out" && !isHovering));
9891
+ // Sync debouncedIsHovering immediately for non-debounced transitions
9892
+ react.useLayoutEffect(() => {
9893
+ if (!debounced || isTransitionDebounced) {
9894
+ return undefined;
9895
+ }
9896
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Synchronizing derived state for directional debouncing
9897
+ setDebouncedIsHovering(isHovering);
9898
+ }, [debounced, isTransitionDebounced, isHovering]);
9899
+ // Apply delay for debounced transitions
9900
+ react.useEffect(() => {
9901
+ if (!isTransitionDebounced) {
9902
+ return undefined;
9903
+ }
9904
+ const timer = setTimeout(() => {
9905
+ setDebouncedIsHovering(isHovering);
9906
+ }, delay);
9907
+ return () => clearTimeout(timer);
9908
+ }, [isTransitionDebounced, delay, isHovering]);
9909
+ const value = debounced ? (isTransitionDebounced ? debouncedIsHovering : isHovering) : isHovering;
9910
+ return react.useMemo(() => ({
9911
+ onMouseEnter,
9912
+ onMouseLeave,
9913
+ hovering: value,
9914
+ }), [onMouseEnter, onMouseLeave, value]);
9915
+ };
9916
+
10030
9917
  /**
10031
9918
  * Manages the dismiss-intent visual on the sheet handle.
10032
9919
  *
@@ -13105,8 +12992,8 @@ const writeHashKey = (hash, key, value) => {
13105
12992
 
13106
12993
  /**
13107
12994
  * Default maximum length of the resulting URL fragment after adding one
13108
- * persisted entry. Writes are skipped (localStorage continues to hold the
13109
- * full state) when adding the entry would push the fragment past this cap.
12995
+ * persisted entry. Oversized new entries are skipped; oversized replacements
12996
+ * remove the stale entry so localStorage remains authoritative.
13110
12997
  *
13111
12998
  * The fragment is never sent to the server, so it is not subject to the
13112
12999
  * AWS WAF managed-rule `SizeRestrictions_QUERYSTRING` cap that applies to
@@ -13121,8 +13008,8 @@ const MAX_HASH_LENGTH = 64 * 1024;
13121
13008
  *
13122
13009
  * Behaviour mirrors `useSearchParamSync`, with two differences:
13123
13010
  * - Reads from and writes to `location.hash` instead of `location.search`.
13124
- * - Writes are guarded against {@link MAX_HASH_LENGTH} rather than the
13125
- * AWS-WAF-driven search-param cap.
13011
+ * - Oversized replacements remove stale values using
13012
+ * {@link MAX_HASH_LENGTH}, rather than the AWS-WAF-driven search-param cap.
13126
13013
  *
13127
13014
  * The fragment is treated as a sequence of `&`-separated tokens via the
13128
13015
  * pure utilities in `./hashFragment`. Foreign tokens (bare anchors, other
@@ -13204,15 +13091,18 @@ const useHashParamSync = ({ key, enabled = true, onExternalChange, replace: repl
13204
13091
  }
13205
13092
  pendingWriteRef.current = null;
13206
13093
  const { encodedValue, options } = pending;
13207
- const nextHash = writeHashKey(location.hash, key, encodedValue);
13094
+ let nextHash = writeHashKey(location.hash, key, encodedValue);
13095
+ let removedOversizedValue = false;
13208
13096
  if (nextHash.length > MAX_HASH_LENGTH) {
13209
- // Drop the write so the fragment never grows past the cap. The
13210
- // localStorage write performed by `usePersistedState` still holds
13211
- // the full state, so the layout survives a reload on the same
13212
- // browser; only the shareable URL is degraded.
13213
- return;
13097
+ if (currentHashValue === undefined) {
13098
+ return;
13099
+ }
13100
+ nextHash = writeHashKey(location.hash, key, undefined);
13101
+ removedOversizedValue = true;
13214
13102
  }
13215
- const shouldReplace = options?.replace ?? replaceOption ?? !Boolean(currentHashValue);
13103
+ const shouldReplace = removedOversizedValue
13104
+ ? true
13105
+ : (options?.replace ?? replaceOption ?? !Boolean(currentHashValue));
13216
13106
  // Tanstack Router's `navigate({ hash })` expects the fragment body
13217
13107
  // *without* the leading `#` — the router adds the delimiter itself
13218
13108
  // when serialising the URL. Forwarding the `#`-prefixed value from
@@ -15151,7 +15041,6 @@ exports.CardHeader = CardHeader;
15151
15041
  exports.Collapse = Collapse;
15152
15042
  exports.CompletionStatusIndicator = CompletionStatusIndicator;
15153
15043
  exports.CopyButton = CopyButton;
15154
- exports.CopyableText = CopyableText;
15155
15044
  exports.DEFAULT_SKELETON_PREFERENCE_CARD_PROPS = DEFAULT_SKELETON_PREFERENCE_CARD_PROPS;
15156
15045
  exports.DetailsList = DetailsList;
15157
15046
  exports.EmptyState = EmptyState;