@trackunit/react-components 2.13.28-alpha-15da733192f.0 → 3.1.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.esm.js CHANGED
@@ -4,7 +4,7 @@ import { twMerge } from 'tailwind-merge';
4
4
  import { objectKeys, uuidv4, parseTailwindArbitraryValue, objectEntries, nonNullable, objectValues, validateState, writeToStorage, readFromStorage, storageSerializer, filterByMultiple } from '@trackunit/shared-utils';
5
5
  export { storageSerializer } from '@trackunit/shared-utils';
6
6
  import { useRef, useMemo, useEffect, useSyncExternalStore, useState, useLayoutEffect, createContext, useContext, isValidElement, cloneElement, createElement, useCallback, Fragment as Fragment$1, memo, forwardRef, useId, useReducer, Children } from 'react';
7
- import { rentalStatusPalette, sitesPalette, utilizationPalette, activityPalette, criticalityPalette, aiPalette, generalPalette, intentPalette, themeScreenSizeAsNumber, themeContainerSize, color } from '@trackunit/ui-design-tokens';
7
+ import { rentalStatusPalette, sitesPalette, utilizationPalette, activityPalette, criticalityPalette, trackunitBrandingPalette, generalPalette, intentPalette, themeScreenSizeAsNumber, themeContainerSize, color } from '@trackunit/ui-design-tokens';
8
8
  import { iconNames } from '@trackunit/ui-icons';
9
9
  import IconSpriteMicro from '@trackunit/ui-icons/icons-sprite-micro.svg';
10
10
  import IconSpriteMini from '@trackunit/ui-icons/icons-sprite-mini.svg';
@@ -19,12 +19,12 @@ import { Link, useBlocker, useNavigate, useLocation, useRouter, useSearch } from
19
19
  import { useVirtualizer } from '@tanstack/react-virtual';
20
20
  import { isTypeableElement } from '@floating-ui/react/utils';
21
21
  import { HelmetProvider, Helmet } from 'react-helmet-async';
22
- import { Trigger, Content as Content$1, List as List$1, Root } from '@radix-ui/react-tabs';
22
+ import { Trigger, Content, List as List$1, Root } from '@radix-ui/react-tabs';
23
23
  import { gzipSync, gunzipSync } from 'fflate';
24
24
  import { dequal } from 'dequal';
25
25
 
26
26
  var defaultTranslations = {
27
- "breadcrumb.backButton": "back",
27
+ "breadcrumb.backButton": "Back",
28
28
  "breadcrumb.expandButton": "expand",
29
29
  "copyButton.copied": "Copied!",
30
30
  "copyButton.copy": "Copy",
@@ -80,7 +80,7 @@ const cvaIcon = cva({
80
80
  variants: {
81
81
  color: {
82
82
  primary: "text-primary-600",
83
- ai: "text-ai",
83
+ trackunit_branding: "text-trackunit_branding",
84
84
  neutral: "text-neutral-400",
85
85
  info: "text-info-600",
86
86
  success: "text-success-600",
@@ -142,7 +142,7 @@ const cvaIcon = cva({
142
142
  const iconPalette = {
143
143
  ...intentPalette,
144
144
  ...generalPalette,
145
- ...aiPalette,
145
+ ...trackunitBrandingPalette,
146
146
  ...criticalityPalette,
147
147
  ...activityPalette,
148
148
  ...utilizationPalette,
@@ -1834,15 +1834,6 @@ const cvaButtonPrefixSuffix = cva({
1834
1834
  const cvaIconButton = cva({
1835
1835
  base: [],
1836
1836
  variants: {
1837
- ai: {
1838
- true: [
1839
- "bg-ai",
1840
- "hover:bg-[color-mix(in_srgb,var(--color-ai),black_10%)]",
1841
- "active:bg-[color-mix(in_srgb,var(--color-ai),black_20%)]",
1842
- "focus:bg-[color-mix(in_srgb,var(--color-ai),black_20%)]",
1843
- ],
1844
- false: [],
1845
- },
1846
1837
  size: {
1847
1838
  /**
1848
1839
  * Sized to match a single text line (20px) so the button fits inside
@@ -2616,7 +2607,7 @@ const toButtonSize = (size) => (size === "extraSmall" ? "small" : size);
2616
2607
  */
2617
2608
  const IconButton = ({ icon, variant = "primary", size = "medium", square = true, loading = false, disabled = false, className, title, ariaLabel, "data-testid": dataTestId, ref, ...rest }) => {
2618
2609
  const accessibleLabel = ariaLabel ?? title;
2619
- 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, ai: variant === "ai", 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, variant: variant === "ai" ? "primary" : variant }) }));
2610
+ 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, variant: variant }) }));
2620
2611
  };
2621
2612
  IconButton.displayName = "IconButton";
2622
2613
 
@@ -3956,160 +3947,10 @@ const useCopyToClipboard = () => {
3956
3947
  return useMemo(() => [isCopied, copyToClipboard], [isCopied, copyToClipboard]);
3957
3948
  };
3958
3949
 
3959
- /**
3960
- * The useHover hook returns a onMouseEnter, onMouseLeave and a boolean indicating whether the element is being hovered.
3961
- * The boolean will be true if the element is being hovered, and false if it is not.
3962
- * Can be directionally debounced.
3963
- *
3964
- * @param {UseHoverProps} param0 The options
3965
- * @returns {object} The object containing the onMouseEnter, onMouseLeave and hovering props
3966
- */
3967
- const useHover = ({ debounced = false, delay = 100, direction = "out" } = { debounced: false }) => {
3968
- const [isHovering, setIsHovering] = useState(false);
3969
- const [debouncedIsHovering, setDebouncedIsHovering] = useState(false);
3970
- const onMouseEnter = useCallback(() => {
3971
- setIsHovering(true);
3972
- }, []);
3973
- const onMouseLeave = useCallback(() => {
3974
- setIsHovering(false);
3975
- }, []);
3976
- // Determine if the current transition should be debounced based on direction
3977
- const isTransitionDebounced = debounced && (direction === "both" || (direction === "in" && isHovering) || (direction === "out" && !isHovering));
3978
- // Sync debouncedIsHovering immediately for non-debounced transitions
3979
- useLayoutEffect(() => {
3980
- if (!debounced || isTransitionDebounced) {
3981
- return undefined;
3982
- }
3983
- // eslint-disable-next-line react-hooks/set-state-in-effect -- Synchronizing derived state for directional debouncing
3984
- setDebouncedIsHovering(isHovering);
3985
- }, [debounced, isTransitionDebounced, isHovering]);
3986
- // Apply delay for debounced transitions
3987
- useEffect(() => {
3988
- if (!isTransitionDebounced) {
3989
- return undefined;
3990
- }
3991
- const timer = setTimeout(() => {
3992
- setDebouncedIsHovering(isHovering);
3993
- }, delay);
3994
- return () => clearTimeout(timer);
3995
- }, [isTransitionDebounced, delay, isHovering]);
3996
- const value = debounced ? (isTransitionDebounced ? debouncedIsHovering : isHovering) : isHovering;
3997
- return useMemo(() => ({
3998
- onMouseEnter,
3999
- onMouseLeave,
4000
- hovering: value,
4001
- }), [onMouseEnter, onMouseLeave, value]);
4002
- };
4003
-
4004
- const cvaCopyableText = cva({
4005
- base: [
4006
- "rounded",
4007
- "cursor-pointer",
4008
- "transition-all",
4009
- "ease-in-out",
4010
- "inline-flex",
4011
- "items-center",
4012
- "gap-1",
4013
- "bg-transparent",
4014
- "border-none",
4015
- "outline-none",
4016
- "hover:bg-neutral-200",
4017
- "py-0.5",
4018
- "px-1",
4019
- ],
4020
- variants: {
4021
- animating: {
4022
- false: "",
4023
- true: "animate-copy",
4024
- },
4025
- size: {
4026
- sm: "text-sm",
4027
- xs: "text-xs",
4028
- },
4029
- },
4030
- defaultVariants: {
4031
- animating: false,
4032
- size: "sm",
4033
- },
4034
- });
4035
-
4036
- /** Resolves the inner content for CopyableText based on withIcon. */
4037
- const Content = ({ text, withIcon, }) => {
4038
- if (withIcon) {
4039
- return (jsxs(Fragment, { children: [jsx("span", { className: "truncate", children: text }), jsx(Icon, { ariaHidden: true, className: "shrink-0 text-neutral-400", name: "Square2Stack", size: "small", type: "solid" })] }));
4040
- }
4041
- return text;
4042
- };
4043
- /**
4044
- * CopyableText displays a text value that the user can click to copy to the clipboard.
4045
- * It shows a brief animation and tooltip feedback on copy. What you see is what gets copied.
4046
- *
4047
- * ### When to use
4048
- * Use CopyableText for identifiers, serial numbers, URLs, or any value the user may want to copy (e.g., asset IDs, error codes).
4049
- *
4050
- * ### When not to use
4051
- * - Do not use CopyableText for long paragraphs or content that doesn't need to be copied.
4052
- * - If the value is rendered as plain (non-clickable) text and only a dedicated icon should copy it
4053
- * (e.g. inside a row or cell that should stay clickable), use `CopyButton` instead:
4054
- * ```tsx
4055
- * <div className="flex items-center gap-0.5">
4056
- * <Text type="span">{value}</Text>
4057
- * <CopyButton value={value} />
4058
- * </div>
4059
- * ```
4060
- * - Only use `useCopyToClipboard` directly for genuinely custom copy behavior that neither `CopyableText` nor `CopyButton` supports.
4061
- *
4062
- * @example Copyable serial number
4063
- * ```tsx
4064
- * import { CopyableText } from "@trackunit/react-components";
4065
- *
4066
- * const AssetSerial = () => (
4067
- * <CopyableText text="SN-2024-00142" data-testid="serial-number" />
4068
- * );
4069
- * ```
4070
- * @example Copyable text with small size
4071
- * ```tsx
4072
- * import { CopyableText } from "@trackunit/react-components";
4073
- *
4074
- * const CopyableId = () => (
4075
- * <CopyableText text="ABC-123" size="xs" />
4076
- * );
4077
- * ```
4078
- * @param {CopyableTextProps} props - The props for the CopyableText component
4079
- * @returns {ReactElement} CopyableText component
4080
- */
4081
- const CopyableText = ({ text, withIcon = true, size = "sm", copyLabel = "Copy", copiedLabel = "Copied!", "data-testid": dataTestId, className, style, ref, }) => {
4082
- const [animating, setAnimating] = useState(false);
4083
- const [showCopied, setShowCopied] = useState(false);
4084
- const [, copyToClipboard] = useCopyToClipboard();
4085
- const { onMouseEnter, onMouseLeave, hovering } = useHover({ debounced: false });
4086
- if (!text) {
4087
- return null;
4088
- }
4089
- const handleOnClick = (e) => {
4090
- e.stopPropagation();
4091
- void copyToClipboard(text)
4092
- .then(() => {
4093
- setShowCopied(true);
4094
- })
4095
- .catch(() => undefined);
4096
- setAnimating(true);
4097
- };
4098
- const handleMouseLeave = () => {
4099
- onMouseLeave();
4100
- setShowCopied(false);
4101
- };
4102
- return (jsx(Tooltip, { delayed: !showCopied, label: showCopied ? copiedLabel : copyLabel, placement: "top", children: jsx("button", { "aria-label": copyLabel, className: cvaCopyableText({ animating, size, className }), "data-testid": dataTestId, onAnimationEnd: () => setAnimating(false), onClick: handleOnClick, onFocus: e => {
4103
- if (hovering) {
4104
- e.currentTarget.blur();
4105
- }
4106
- }, onMouseEnter: onMouseEnter, onMouseLeave: handleMouseLeave, ref: ref, style: style, type: "button", children: jsx(Content, { text: text, withIcon: withIcon }) }) }));
4107
- };
4108
-
4109
3950
  /**
4110
3951
  * CopyButton is a standalone icon button that copies a value to the clipboard when clicked,
4111
- * showing brief tooltip feedback. Unlike CopyableText, the copied value is not itself the
4112
- * clickable/displayed element, so it can be placed next to plain (non-clickable) text.
3952
+ * showing brief tooltip feedback. The copied value is not itself the clickable/displayed
3953
+ * element, so the button can be placed next to plain (non-clickable) text.
4113
3954
  *
4114
3955
  * ### When to use
4115
3956
  * - Icon-only (default): the copyable value is displayed as plain text (e.g. in a table cell or
@@ -4122,8 +3963,8 @@ const CopyableText = ({ text, withIcon = true, size = "sm", copyLabel = "Copy",
4122
3963
  * one copy control; pick icon-only or labelled mode instead.
4123
3964
  *
4124
3965
  * ### Empty values
4125
- * Like `CopyableText`, CopyButton renders nothing when `value` is empty, so consumers never
4126
- * present a copy control that has no meaningful result.
3966
+ * CopyButton renders nothing when `value` is empty, so consumers never present a copy control
3967
+ * that has no meaningful result.
4127
3968
  *
4128
3969
  * @example Icon button next to plain text
4129
3970
  * ```tsx
@@ -8946,6 +8787,7 @@ const PageHeaderTitle = ({ title, "data-testid": dataTestId, className, style, r
8946
8787
  * @returns {ReactElement} PageHeader component
8947
8788
  */
8948
8789
  const PageHeader = ({ className, "data-testid": dataTestId, showLoading = false, description, title, tagLabel, backTo, tagColor, tabsList, descriptionIcon = "QuestionMarkCircle", tagTooltipLabel, style, ref, ...discriminatedProps }) => {
8790
+ const [t] = useTranslation();
8949
8791
  const tagRenderer = useMemo(() => {
8950
8792
  if (tagLabel === undefined || tagLabel === "" || showLoading) {
8951
8793
  return null;
@@ -8956,7 +8798,7 @@ const PageHeader = ({ className, "data-testid": dataTestId, showLoading = false,
8956
8798
  return (jsxs("div", { className: cvaPageHeaderContainer({
8957
8799
  className,
8958
8800
  withBorder: tabsList === undefined,
8959
- }), "data-testid": dataTestId, ref: ref, style: style, children: [jsxs("div", { className: cvaPageHeader(), children: [backTo ? (jsx(Link, { to: backTo, children: jsx(Button, { className: "mr-4 bg-black/5 hover:bg-black/10", prefix: jsx(Icon, { name: "ArrowLeft", size: "small" }), size: "small", square: true, variant: "ghost-neutral" }) })) : undefined, typeof title === "string" ? jsx(PageHeaderTitle, { "data-testid": dataTestId, title: title }) : title, tagRenderer || (description !== null && description !== undefined) ? (jsxs("div", { className: "mx-2 flex items-center gap-2", children: [description !== null && description !== undefined && !showLoading ? (jsx(Tooltip, { "data-testid": dataTestId ? `${dataTestId}-description-tooltip` : undefined, iconProps: {
8801
+ }), "data-testid": dataTestId, ref: ref, style: style, children: [jsxs("div", { className: cvaPageHeader(), children: [backTo ? (jsx(Link, { to: backTo, children: jsx(Button, { className: "mr-4 bg-black/5 hover:bg-black/10", "data-testid": dataTestId ? `${dataTestId}-back-button` : undefined, prefix: jsx(Icon, { name: "ArrowLeft", size: "small" }), size: "small", square: true, title: t("breadcrumb.backButton"), variant: "ghost-neutral" }) })) : undefined, typeof title === "string" ? jsx(PageHeaderTitle, { "data-testid": dataTestId, title: title }) : title, tagRenderer || (description !== null && description !== undefined) ? (jsxs("div", { className: "mx-2 flex items-center gap-2", children: [description !== null && description !== undefined && !showLoading ? (jsx(Tooltip, { "data-testid": dataTestId ? `${dataTestId}-description-tooltip` : undefined, iconProps: {
8960
8802
  name: descriptionIcon,
8961
8803
  "data-testid": "page-header-description-icon",
8962
8804
  }, label: description, placement: "bottom" })) : undefined, tagRenderer] })) : null, jsxs("div", { className: "ml-auto flex gap-2", children: [discriminatedProps.accessoryType === "kpi-metrics" ? (jsx(PageHeaderKpiMetrics, { kpiMetrics: discriminatedProps.kpiMetrics })) : null, discriminatedProps.accessoryType === "actions" ? (Array.isArray(discriminatedProps.secondaryActions) ? (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" &&
@@ -10026,6 +9868,51 @@ const sheetAnimationReducer = (state, action) => {
10026
9868
  }
10027
9869
  };
10028
9870
 
9871
+ /**
9872
+ * The useHover hook returns a onMouseEnter, onMouseLeave and a boolean indicating whether the element is being hovered.
9873
+ * The boolean will be true if the element is being hovered, and false if it is not.
9874
+ * Can be directionally debounced.
9875
+ *
9876
+ * @param {UseHoverProps} param0 The options
9877
+ * @returns {object} The object containing the onMouseEnter, onMouseLeave and hovering props
9878
+ */
9879
+ const useHover = ({ debounced = false, delay = 100, direction = "out" } = { debounced: false }) => {
9880
+ const [isHovering, setIsHovering] = useState(false);
9881
+ const [debouncedIsHovering, setDebouncedIsHovering] = useState(false);
9882
+ const onMouseEnter = useCallback(() => {
9883
+ setIsHovering(true);
9884
+ }, []);
9885
+ const onMouseLeave = useCallback(() => {
9886
+ setIsHovering(false);
9887
+ }, []);
9888
+ // Determine if the current transition should be debounced based on direction
9889
+ const isTransitionDebounced = debounced && (direction === "both" || (direction === "in" && isHovering) || (direction === "out" && !isHovering));
9890
+ // Sync debouncedIsHovering immediately for non-debounced transitions
9891
+ useLayoutEffect(() => {
9892
+ if (!debounced || isTransitionDebounced) {
9893
+ return undefined;
9894
+ }
9895
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- Synchronizing derived state for directional debouncing
9896
+ setDebouncedIsHovering(isHovering);
9897
+ }, [debounced, isTransitionDebounced, isHovering]);
9898
+ // Apply delay for debounced transitions
9899
+ useEffect(() => {
9900
+ if (!isTransitionDebounced) {
9901
+ return undefined;
9902
+ }
9903
+ const timer = setTimeout(() => {
9904
+ setDebouncedIsHovering(isHovering);
9905
+ }, delay);
9906
+ return () => clearTimeout(timer);
9907
+ }, [isTransitionDebounced, delay, isHovering]);
9908
+ const value = debounced ? (isTransitionDebounced ? debouncedIsHovering : isHovering) : isHovering;
9909
+ return useMemo(() => ({
9910
+ onMouseEnter,
9911
+ onMouseLeave,
9912
+ hovering: value,
9913
+ }), [onMouseEnter, onMouseLeave, value]);
9914
+ };
9915
+
10029
9916
  /**
10030
9917
  * Manages the dismiss-intent visual on the sheet handle.
10031
9918
  *
@@ -12122,7 +12009,7 @@ const Tab = ({ value, isFullWidth = false, iconName = undefined, "data-testid":
12122
12009
  * @returns {ReactElement} TabContent component
12123
12010
  */
12124
12011
  const TabContent = ({ className, "data-testid": dataTestId, children, ref, ...rest }) => {
12125
- return (jsx(Content$1, { className: cvaTabContent({ className }), "data-testid": dataTestId ? `${dataTestId}-content` : undefined, ref: ref, ...rest, children: children }));
12012
+ return (jsx(Content, { className: cvaTabContent({ className }), "data-testid": dataTestId ? `${dataTestId}-content` : undefined, ref: ref, ...rest, children: children }));
12126
12013
  };
12127
12014
 
12128
12015
  /**
@@ -13104,8 +12991,8 @@ const writeHashKey = (hash, key, value) => {
13104
12991
 
13105
12992
  /**
13106
12993
  * Default maximum length of the resulting URL fragment after adding one
13107
- * persisted entry. Writes are skipped (localStorage continues to hold the
13108
- * full state) when adding the entry would push the fragment past this cap.
12994
+ * persisted entry. Oversized new entries are skipped; oversized replacements
12995
+ * remove the stale entry so localStorage remains authoritative.
13109
12996
  *
13110
12997
  * The fragment is never sent to the server, so it is not subject to the
13111
12998
  * AWS WAF managed-rule `SizeRestrictions_QUERYSTRING` cap that applies to
@@ -13120,8 +13007,8 @@ const MAX_HASH_LENGTH = 64 * 1024;
13120
13007
  *
13121
13008
  * Behaviour mirrors `useSearchParamSync`, with two differences:
13122
13009
  * - Reads from and writes to `location.hash` instead of `location.search`.
13123
- * - Writes are guarded against {@link MAX_HASH_LENGTH} rather than the
13124
- * AWS-WAF-driven search-param cap.
13010
+ * - Oversized replacements remove stale values using
13011
+ * {@link MAX_HASH_LENGTH}, rather than the AWS-WAF-driven search-param cap.
13125
13012
  *
13126
13013
  * The fragment is treated as a sequence of `&`-separated tokens via the
13127
13014
  * pure utilities in `./hashFragment`. Foreign tokens (bare anchors, other
@@ -13203,15 +13090,18 @@ const useHashParamSync = ({ key, enabled = true, onExternalChange, replace: repl
13203
13090
  }
13204
13091
  pendingWriteRef.current = null;
13205
13092
  const { encodedValue, options } = pending;
13206
- const nextHash = writeHashKey(location.hash, key, encodedValue);
13093
+ let nextHash = writeHashKey(location.hash, key, encodedValue);
13094
+ let removedOversizedValue = false;
13207
13095
  if (nextHash.length > MAX_HASH_LENGTH) {
13208
- // Drop the write so the fragment never grows past the cap. The
13209
- // localStorage write performed by `usePersistedState` still holds
13210
- // the full state, so the layout survives a reload on the same
13211
- // browser; only the shareable URL is degraded.
13212
- return;
13096
+ if (currentHashValue === undefined) {
13097
+ return;
13098
+ }
13099
+ nextHash = writeHashKey(location.hash, key, undefined);
13100
+ removedOversizedValue = true;
13213
13101
  }
13214
- const shouldReplace = options?.replace ?? replaceOption ?? !Boolean(currentHashValue);
13102
+ const shouldReplace = removedOversizedValue
13103
+ ? true
13104
+ : (options?.replace ?? replaceOption ?? !Boolean(currentHashValue));
13215
13105
  // Tanstack Router's `navigate({ hash })` expects the fragment body
13216
13106
  // *without* the leading `#` — the router adds the delimiter itself
13217
13107
  // when serialising the URL. Forwarding the `#`-prefixed value from
@@ -15135,4 +15025,4 @@ const useWindowActivity = ({ onFocus, onBlur, skip = false } = { onBlur: undefin
15135
15025
  */
15136
15026
  setupLibraryTranslations();
15137
15027
 
15138
- export { Alert, Badge, Breadcrumb, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyButton, CopyableText, DEFAULT_SKELETON_PREFERENCE_CARD_PROPS, DetailsList, EmptyState, EmptyValue, ExternalLink, GridAreas, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, 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, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
15028
+ 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, 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, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.copyableTextToTextCopyButton = void 0;
37
+ const ts = __importStar(require("typescript"));
38
+ const jsx_utils_1 = require("../utils/jsx-utils");
39
+ const PACKAGE_NAME = "@trackunit/react-components";
40
+ const COMPONENT_NAME = "CopyableText";
41
+ /** Attribute initializer rendered back to source, e.g. `"abc"` or `{value}`. */
42
+ const attributeText = (attr) => {
43
+ const initializer = attr.initializer;
44
+ if (initializer === undefined)
45
+ return null;
46
+ return initializer.getText();
47
+ };
48
+ /**
49
+ * The expression the copied text resolves to: `text="abc"` yields `"abc"`,
50
+ * `text={value}` yields `value`. Used both as the CopyButton value and as the
51
+ * rendered children, so it must be a side-effect-free expression.
52
+ */
53
+ const textExpression = (attr) => {
54
+ const initializer = attr.initializer;
55
+ if (initializer === undefined)
56
+ return null;
57
+ if (ts.isStringLiteral(initializer))
58
+ return initializer.getText();
59
+ if (ts.isJsxExpression(initializer) && initializer.expression !== undefined) {
60
+ return initializer.expression.getText();
61
+ }
62
+ return null;
63
+ };
64
+ const renderReplacement = ({ textExpr, size, copyLabelAttr, copiedLabelAttr, classNameLiteral, styleAttr, testIdAttr, textAlias, copyButtonAlias, }) => {
65
+ const wrapperClasses = ["flex", "min-w-0", "max-w-full", "items-center", "gap-0.5", classNameLiteral]
66
+ .filter(Boolean)
67
+ .join(" ");
68
+ const wrapperAttrs = [
69
+ `className="${wrapperClasses}"`,
70
+ styleAttr !== null ? `style=${styleAttr}` : null,
71
+ testIdAttr !== null ? `data-testid=${testIdAttr}` : null,
72
+ ]
73
+ .filter(Boolean)
74
+ .join(" ");
75
+ const textProps = [`className="min-w-0 truncate"`, size === "xs" ? `size="small"` : null, `type="span"`]
76
+ .filter(Boolean)
77
+ .join(" ");
78
+ const buttonProps = [
79
+ `className="shrink-0"`,
80
+ copiedLabelAttr !== null ? `copiedLabel=${copiedLabelAttr}` : null,
81
+ copyLabelAttr !== null ? `copyLabel=${copyLabelAttr}` : null,
82
+ size === "xs" ? `size="extraSmall"` : null,
83
+ `value={${textExpr}}`,
84
+ ]
85
+ .filter(Boolean)
86
+ .join(" ");
87
+ return `<span ${wrapperAttrs}><${textAlias} ${textProps}>{${textExpr}}</${textAlias}><${copyButtonAlias} ${buttonProps} /></span>`;
88
+ };
89
+ /** Removes `name` (possibly aliased) from the package's named import list. */
90
+ const removeNamedImport = (content, packageName, name) => {
91
+ const sourceFile = (0, jsx_utils_1.parseTsx)(content, "source.tsx");
92
+ for (const stmt of sourceFile.statements) {
93
+ if (!ts.isImportDeclaration(stmt))
94
+ continue;
95
+ const moduleSpecifier = stmt.moduleSpecifier;
96
+ if (!ts.isStringLiteral(moduleSpecifier) || moduleSpecifier.text !== packageName)
97
+ continue;
98
+ const namedBindings = stmt.importClause?.namedBindings;
99
+ if (namedBindings === undefined || !ts.isNamedImports(namedBindings))
100
+ continue;
101
+ const remaining = namedBindings.elements.filter(el => (el.propertyName?.text ?? el.name.text) !== name);
102
+ if (remaining.length === namedBindings.elements.length)
103
+ continue;
104
+ if (remaining.length === 0) {
105
+ // The import only carried this component; drop the whole statement.
106
+ const start = stmt.getFullStart();
107
+ const end = stmt.getEnd();
108
+ const trailing = content.slice(end).startsWith("\n") ? end + 1 : end;
109
+ return `${content.slice(0, start)}${content.slice(trailing)}`;
110
+ }
111
+ const elementsText = remaining.map(el => el.getText()).join(", ");
112
+ return `${content.slice(0, namedBindings.getStart())}{ ${elementsText} }${content.slice(namedBindings.getEnd())}`;
113
+ }
114
+ return content;
115
+ };
116
+ const ensureNamedImport = (content, packageName, namesToAdd) => {
117
+ const sourceFile = (0, jsx_utils_1.parseTsx)(content, "source.tsx");
118
+ const aliases = (0, jsx_utils_1.getImportedAliases)(sourceFile, packageName) ?? {};
119
+ const missing = namesToAdd.filter(name => !Object.values(aliases).includes(name));
120
+ if (missing.length === 0)
121
+ return content;
122
+ for (const stmt of sourceFile.statements) {
123
+ if (!ts.isImportDeclaration(stmt))
124
+ continue;
125
+ const moduleSpecifier = stmt.moduleSpecifier;
126
+ if (!ts.isStringLiteral(moduleSpecifier) || moduleSpecifier.text !== packageName)
127
+ continue;
128
+ const namedBindings = stmt.importClause?.namedBindings;
129
+ if (namedBindings === undefined || !ts.isNamedImports(namedBindings))
130
+ continue;
131
+ const elementsText = namedBindings.elements.map(el => el.getText()).join(", ");
132
+ const merged = [elementsText, ...missing].join(", ");
133
+ return `${content.slice(0, namedBindings.getStart())}{ ${merged} }${content.slice(namedBindings.getEnd())}`;
134
+ }
135
+ const importLine = `import { ${missing.join(", ")} } from "${packageName}";\n`;
136
+ return `${importLine}${content}`;
137
+ };
138
+ let manualReviewCount = 0;
139
+ const transformCopyableTextUsage = (filePath, content) => {
140
+ const sourceFile = (0, jsx_utils_1.parseTsx)(content, filePath);
141
+ const aliases = (0, jsx_utils_1.getImportedAliases)(sourceFile, PACKAGE_NAME);
142
+ if (aliases === null)
143
+ return null;
144
+ const copyableTextAlias = Object.entries(aliases).find(([, name]) => name === COMPONENT_NAME)?.[0];
145
+ if (copyableTextAlias === undefined)
146
+ return null;
147
+ // A referenced CopyableTextProps type cannot be rewritten mechanically;
148
+ // leave the whole file for a human so it isn't half-migrated.
149
+ if (Object.values(aliases).includes("CopyableTextProps")) {
150
+ manualReviewCount += 1;
151
+ return null;
152
+ }
153
+ const matches = (0, jsx_utils_1.findJsxElements)(sourceFile, [copyableTextAlias]);
154
+ if (matches.length === 0)
155
+ return null;
156
+ const edits = [];
157
+ for (const { openingElement, element } of matches) {
158
+ if ((0, jsx_utils_1.hasSpreadAttribute)(openingElement) || (0, jsx_utils_1.findJsxAttribute)(openingElement, "ref") !== null) {
159
+ manualReviewCount += 1;
160
+ continue;
161
+ }
162
+ const textAttr = (0, jsx_utils_1.findJsxAttribute)(openingElement, "text");
163
+ const textExpr = textAttr !== null ? textExpression(textAttr) : null;
164
+ if (textExpr === null) {
165
+ manualReviewCount += 1;
166
+ continue;
167
+ }
168
+ const sizeAttr = (0, jsx_utils_1.findJsxAttribute)(openingElement, "size");
169
+ const sizeInitializer = sizeAttr?.initializer;
170
+ const size = sizeInitializer !== undefined && sizeInitializer.getText().includes("xs") ? "xs" : "sm";
171
+ const classNameAttr = (0, jsx_utils_1.findJsxAttribute)(openingElement, "className");
172
+ let classNameLiteral = null;
173
+ if (classNameAttr !== null) {
174
+ const initializer = classNameAttr.initializer;
175
+ if (initializer !== undefined && ts.isStringLiteral(initializer)) {
176
+ classNameLiteral = initializer.text;
177
+ }
178
+ else {
179
+ // Dynamic classNames can't be merged into the wrapper's string safely.
180
+ manualReviewCount += 1;
181
+ continue;
182
+ }
183
+ }
184
+ const copyLabelAttr = (0, jsx_utils_1.findJsxAttribute)(openingElement, "copyLabel");
185
+ const copiedLabelAttr = (0, jsx_utils_1.findJsxAttribute)(openingElement, "copiedLabel");
186
+ const styleAttr = (0, jsx_utils_1.findJsxAttribute)(openingElement, "style");
187
+ const testIdAttr = (0, jsx_utils_1.findJsxAttribute)(openingElement, "data-testid");
188
+ const replacement = renderReplacement({
189
+ textExpr,
190
+ size,
191
+ copyLabelAttr: copyLabelAttr !== null ? attributeText(copyLabelAttr) : null,
192
+ copiedLabelAttr: copiedLabelAttr !== null ? attributeText(copiedLabelAttr) : null,
193
+ classNameLiteral,
194
+ styleAttr: styleAttr !== null ? attributeText(styleAttr) : null,
195
+ testIdAttr: testIdAttr !== null ? attributeText(testIdAttr) : null,
196
+ textAlias: "Text",
197
+ copyButtonAlias: "CopyButton",
198
+ });
199
+ edits.push({ start: element.getStart(), end: element.getEnd(), replacement });
200
+ }
201
+ if (edits.length === 0)
202
+ return null;
203
+ edits.sort((a, b) => b.start - a.start);
204
+ let updated = content;
205
+ for (const { start, end, replacement } of edits) {
206
+ updated = `${updated.slice(0, start)}${replacement}${updated.slice(end)}`;
207
+ }
208
+ // Only drop the import when every usage in the file was rewritten.
209
+ const remaining = (0, jsx_utils_1.findJsxElements)((0, jsx_utils_1.parseTsx)(updated, filePath), [copyableTextAlias]);
210
+ if (remaining.length === 0) {
211
+ updated = removeNamedImport(updated, PACKAGE_NAME, COMPONENT_NAME);
212
+ }
213
+ else {
214
+ manualReviewCount += 1;
215
+ }
216
+ updated = ensureNamedImport(updated, PACKAGE_NAME, ["Text", "CopyButton"]);
217
+ return updated;
218
+ };
219
+ /**
220
+ * Rewrites `<CopyableText text={x} …/>` (removed in v3) into the plain
221
+ * value/copy pair: a `Text` next to a `CopyButton` that copies the same
222
+ * expression. `size="xs"` maps to `Text size="small"` + `CopyButton
223
+ * size="extraSmall"`; `copyLabel`/`copiedLabel` carry over (CopyButton also
224
+ * ships localized defaults). Usages with spreads, refs, dynamic classNames,
225
+ * or a referenced `CopyableTextProps` type are left for manual review.
226
+ */
227
+ const copyableTextToTextCopyButton = (tree) => {
228
+ manualReviewCount = 0;
229
+ const touched = (0, jsx_utils_1.visitTsxFiles)(tree, COMPONENT_NAME, transformCopyableTextUsage);
230
+ (0, jsx_utils_1.logSummary)("copyabletext-to-text-copybutton", touched, manualReviewCount);
231
+ };
232
+ exports.copyableTextToTextCopyButton = copyableTextToTextCopyButton;
233
+ exports.default = exports.copyableTextToTextCopyButton;
234
+ //# sourceMappingURL=copyabletext-to-text-copybutton.js.map
package/migrations.json CHANGED
@@ -44,6 +44,11 @@
44
44
  "version": "3.0.0",
45
45
  "description": "Rename MenuList/MenuListProps to MenuContent/MenuContentProps (import specifiers, JSX tags, and type references).",
46
46
  "implementation": "./migrations/v3-0-0/menulist-rename-to-menucontent"
47
+ },
48
+ "v3-0-0-copyabletext-to-text-copybutton": {
49
+ "version": "3.0.0",
50
+ "description": "Replace the removed CopyableText with the Text + CopyButton pair (the displayed value stays plain text; only the button copies).",
51
+ "implementation": "./migrations/v3-0-0/copyabletext-to-text-copybutton"
47
52
  }
48
53
  }
49
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "2.13.28-alpha-15da733192f.0",
3
+ "version": "3.1.1",
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.36-alpha-15da733192f.0",
18
- "@trackunit/css-class-variance-utilities": "2.0.16-alpha-15da733192f.0",
19
- "@trackunit/shared-utils": "1.16.51-alpha-15da733192f.0",
20
- "@trackunit/ui-icons": "1.14.45-alpha-15da733192f.0",
17
+ "@trackunit/ui-design-tokens": "1.15.37",
18
+ "@trackunit/css-class-variance-utilities": "2.0.16",
19
+ "@trackunit/shared-utils": "1.16.51",
20
+ "@trackunit/ui-icons": "1.14.45",
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.66-alpha-15da733192f.0"
26
+ "@trackunit/i18n-library-translation": "2.5.1"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react": "^19.0.0",