@trackunit/react-components 1.12.13 → 1.13.2

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
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
2
- import { useRef, useMemo, useEffect, useState, useCallback, createElement, forwardRef, Fragment, memo, useReducer, Children, isValidElement, cloneElement, createContext, useContext, useLayoutEffect } from 'react';
3
- import { objectKeys, uuidv4, objectEntries, objectValues, nonNullable, filterByMultiple } from '@trackunit/shared-utils';
4
- import { intentPalette, generalPalette, criticalityPalette, activityPalette, utilizationPalette, sitesPalette, rentalStatusPalette, themeScreenSizeAsNumber, color } from '@trackunit/ui-design-tokens';
2
+ import { objectKeys, uuidv4, parseTailwindArbitraryValue, objectEntries, nonNullable, objectValues, filterByMultiple } from '@trackunit/shared-utils';
3
+ import { useRef, useMemo, useEffect, useState, useLayoutEffect, useCallback, createElement, forwardRef, Fragment, memo, useReducer, Children, isValidElement, cloneElement, createContext, useContext } from 'react';
4
+ import { intentPalette, generalPalette, criticalityPalette, activityPalette, utilizationPalette, sitesPalette, rentalStatusPalette, themeScreenSizeAsNumber, themeFontSize, color } from '@trackunit/ui-design-tokens';
5
5
  import { iconNames } from '@trackunit/ui-icons';
6
6
  import IconSpriteMicro from '@trackunit/ui-icons/icons-sprite-micro.svg';
7
7
  import IconSpriteMini from '@trackunit/ui-icons/icons-sprite-mini.svg';
@@ -163,8 +163,17 @@ const Icon = ({ name, size = "medium", className, "data-testid": dataTestId, col
163
163
  return (jsx("span", { "aria-describedby": ariaDescribedBy, "aria-hidden": ariaHidden, "aria-label": ariaLabel ? ariaLabel : titleCase(iconName), "aria-labelledby": ariaLabelledBy, className: cvaIcon({ color, size, fontSize, className }), "data-testid": dataTestId, id: ICON_CONTAINER_ID, onClick: onClick, ref: forwardedRef, children: jsx("svg", { "aria-labelledby": ICON_CONTAINER_ID, "data-testid": dataTestId ? `${dataTestId}-${iconName}` : iconName, role: "img", style: style, viewBox: correctViewBox, children: jsx("use", { href: href[correctIconType], ref: useTagRef }) }) }));
164
164
  };
165
165
 
166
+ /**
167
+ * The Tailwind class for minimum width of tag text.
168
+ * This is the source of truth - use parseTailwindArbitraryValue to derive the numeric value.
169
+ */
170
+ const TAG_TEXT_MIN_WIDTH_CLASS = "min-w-[80px]";
166
171
  const cvaTag = cvaMerge([
167
- "inline-flex",
172
+ "grid",
173
+ "grid-cols-[1fr]",
174
+ "has-[[data-slot=icon]]:grid-cols-[auto_1fr]",
175
+ "has-[[data-slot=dismiss]]:grid-cols-[1fr_auto]",
176
+ "has-[[data-slot=icon]]:has-[[data-slot=dismiss]]:grid-cols-[auto_1fr_auto]",
168
177
  "justify-center",
169
178
  "items-center",
170
179
  "rounded-full",
@@ -172,11 +181,11 @@ const cvaTag = cvaMerge([
172
181
  "px-2",
173
182
  "text-center",
174
183
  "h-min",
175
- "min-w-[1.5rem]",
176
184
  "font-medium",
177
- "truncate",
178
185
  "max-w-full",
179
186
  "w-fit",
187
+ "overflow-hidden",
188
+ "min-w-min",
180
189
  "text-xs",
181
190
  ], {
182
191
  variants: {
@@ -220,10 +229,21 @@ const cvaTag = cvaMerge([
220
229
  color: "info",
221
230
  },
222
231
  });
223
- const cvaTagText = cvaMerge(["truncate"]);
224
- const cvaTagIconContainer = cvaMerge(["inline-flex", "self-center"]);
232
+ const cvaTagText = cvaMerge(["whitespace-nowrap"], {
233
+ variants: {
234
+ truncate: {
235
+ true: ["truncate", TAG_TEXT_MIN_WIDTH_CLASS],
236
+ false: "min-w-min",
237
+ },
238
+ },
239
+ defaultVariants: {
240
+ truncate: true,
241
+ },
242
+ });
243
+ const cvaTagIconContainer = cvaMerge(["shrink-0", "inline-flex", "self-center"]);
225
244
  const cvaTagIcon = cvaMerge(["cursor-pointer", "transition-opacity", "hover:opacity-70", "text-neutral-500"]);
226
245
 
246
+ const TAG_TEXT_MIN_WIDTH_PX = parseTailwindArbitraryValue(TAG_TEXT_MIN_WIDTH_CLASS);
227
247
  /**
228
248
  * The Tag component is used for labeling or categorizing items in the UI.
229
249
  * It's commonly used to indicate the status of an asset, mark a feature as Beta,
@@ -238,6 +258,19 @@ const cvaTagIcon = cvaMerge(["cursor-pointer", "transition-opacity", "hover:opac
238
258
  * @returns {ReactElement} The rendered Tag component.
239
259
  */
240
260
  const Tag = ({ className, "data-testid": dataTestId, children, size = "medium", onClose, color = "info", disabled = false, ref, icon, onMouseEnter, }) => {
261
+ const textRef = useRef(null);
262
+ const [shouldTruncate, setShouldTruncate] = useState(false);
263
+ useLayoutEffect(() => {
264
+ // Using simple DOM measurements to avoid the complexity of the useMeasure hook.
265
+ // Resize observers have some overhead and we don't need the full power of the useMeasure hook here.
266
+ // And tags could be rendered many at a time, multiplying the performance penalty of useMeasure.
267
+ // We measure the visible span directly using scrollWidth which gives the full content width
268
+ // even when truncation styles are applied.
269
+ if (textRef.current !== null) {
270
+ const width = textRef.current.scrollWidth;
271
+ setShouldTruncate(width >= TAG_TEXT_MIN_WIDTH_PX);
272
+ }
273
+ }, [children]);
241
274
  const isSupportedDismissColor = useMemo(() => {
242
275
  if (color === "neutral" || color === "primary" || color === "white" || color === "info") {
243
276
  return true;
@@ -253,11 +286,16 @@ const Tag = ({ className, "data-testid": dataTestId, children, size = "medium",
253
286
  }
254
287
  return "default";
255
288
  }, [onClose, isSupportedDismissColor, disabled, icon]);
256
- return (jsxs("div", { className: cvaTag({ className, size, color, layout, border: color === "white" ? "default" : "none" }), "data-testid": dataTestId, onMouseEnter: onMouseEnter, ref: ref, children: [icon !== null && icon !== undefined && size === "medium" ? (jsx("div", { className: cvaTagIconContainer(), children: icon })) : null, jsx("span", { className: cvaTagText(), children: children }), Boolean(onClose) && isSupportedDismissColor && size === "medium" && !disabled ? (
289
+ return (jsxs("div", { className: cvaTag({
290
+ className,
291
+ size,
292
+ color,
293
+ layout,
294
+ border: color === "white" ? "default" : "none",
295
+ }), "data-testid": dataTestId, onMouseEnter: onMouseEnter, ref: ref, children: [icon !== null && icon !== undefined && size === "medium" ? (jsx("div", { className: cvaTagIconContainer(), "data-slot": "icon", children: icon })) : null, jsx("span", { className: cvaTagText({ truncate: shouldTruncate }), ref: textRef, children: children }), Boolean(onClose) && isSupportedDismissColor && size === "medium" && !disabled ? (
257
296
  // a fix for multiselect deselecting tags working together with fade out animation
258
- jsx("div", { className: cvaTagIconContainer(), onMouseDown: onClose, children: jsx(Icon, { className: cvaTagIcon(), "data-testid": dataTestId + "Icon", name: "XCircle", size: "small", style: { WebkitTransition: "-webkit-transform 0.150s" }, type: "solid" }) })) : null] }));
297
+ jsx("div", { className: cvaTagIconContainer(), "data-slot": "dismiss", onMouseDown: onClose, children: jsx(Icon, { className: cvaTagIcon(), "data-testid": dataTestId + "Icon", name: "XCircle", size: "small", style: { WebkitTransition: "-webkit-transform 0.150s" }, type: "solid" }) })) : null] }));
259
298
  };
260
- Tag.displayName = "Tag";
261
299
 
262
300
  /**
263
301
  * A component used to display the package name and version in the Storybook docs.
@@ -1242,7 +1280,7 @@ const cvaCardHeaderContainer = cvaMerge(["flex", "border-b", "border-neutral-200
1242
1280
  padding: "default",
1243
1281
  },
1244
1282
  });
1245
- const cvaCardBodyContainer$1 = cvaMerge(["flex", "flex-grow", "overflow-auto"], {
1283
+ const cvaCardBodyContainer = cvaMerge(["flex", "flex-grow", "overflow-auto"], {
1246
1284
  variants: {
1247
1285
  direction: {
1248
1286
  row: "flex-row",
@@ -1287,7 +1325,7 @@ const Card = forwardRef(function Card({ children, onClick, fullHeight = false, o
1287
1325
  * @returns {ReactElement} CardBody component
1288
1326
  */
1289
1327
  const CardBody = ({ children, "data-testid": dataTestId, className, direction = "column", gap = "default", padding = "default", id, }) => {
1290
- return (jsx("div", { className: cvaCardBodyContainer$1({
1328
+ return (jsx("div", { className: cvaCardBodyContainer({
1291
1329
  gap,
1292
1330
  padding,
1293
1331
  className,
@@ -2370,7 +2408,7 @@ const useScrollDetection = (options) => {
2370
2408
  }), [ref, element, scrollState]);
2371
2409
  };
2372
2410
 
2373
- const cvaZStackContainer = cvaMerge(["grid", "grid-cols-1", "grid-rows-1"]);
2411
+ const cvaZStackContainer = cvaMerge(["grid", "grid-cols-1", "grid-rows-1", "isolate"]);
2374
2412
  const cvaZStackItem = cvaMerge(["col-start-1", "col-end-1", "row-start-1", "row-end-2"]);
2375
2413
 
2376
2414
  /**
@@ -3122,63 +3160,163 @@ const cvaKPI = cvaMerge(["w-full", "flex", "flex-col"], {
3122
3160
  variant: "default",
3123
3161
  },
3124
3162
  });
3125
- const cvaKPIHeader = cvaMerge(["flex", "flex-row", "justify-between", "gap-1"]);
3126
- const cvaKPITitleText = cvaMerge(["truncate", "whitespace-nowrap"]);
3127
- const cvaKPIvalueText = cvaMerge(["truncate", "whitespace-nowrap", "text-lg", "font-medium"], {
3163
+ const cvaKPITrendPercentage = cvaMerge([""], {
3128
3164
  variants: {
3129
- variant: {
3130
- small: ["mt-0.5"],
3131
- condensed: [""],
3132
- default: [""],
3165
+ color: {
3166
+ success: ["text-success-600"],
3167
+ danger: ["text-danger-600"],
3168
+ warning: ["text-warning-600"],
3169
+ neutral: ["text-neutral-600"],
3170
+ info: ["text-info-600"],
3133
3171
  },
3134
3172
  },
3135
3173
  defaultVariants: {
3136
- variant: "default",
3174
+ color: "success",
3137
3175
  },
3138
3176
  });
3139
- const cvaKPITrendPercentage = cvaMerge([""], {
3177
+
3178
+ /**
3179
+ * The KPI component is used to display KPIs.
3180
+ *
3181
+ * @param {KPIProps} props - The props for the KPI component
3182
+ * @returns {ReactElement} KPI component
3183
+ */
3184
+ const KPI = ({ title, value, loading = false, unit, className, "data-testid": dataTestId, tooltipLabel, variant = "default", style, ...rest }) => {
3185
+ const isSmallVariant = variant === "small";
3186
+ return (jsx(Tooltip, { className: "min-w-8 shrink-0", "data-testid": dataTestId ? `${dataTestId}-tooltip` : undefined, disabled: tooltipLabel === undefined || tooltipLabel === "", label: tooltipLabel, placement: "bottom", children: jsxs("div", { className: cvaKPI({ variant, className }), "data-testid": dataTestId, style: style, ...rest, children: [loading ? (jsx(SkeletonLines, { className: twMerge("flex", "items-center", "flex-row", isSmallVariant ? "h-4" : "h-5"), "data-testid": dataTestId ? `${dataTestId}-title-loading` : undefined, height: isSmallVariant ? themeFontSize.xs : themeFontSize.sm, lines: 1, width: "80%" })) : (jsx(Text, { className: twMerge("truncate", "whitespace-nowrap"), "data-testid": dataTestId ? `${dataTestId}-title` : undefined, size: isSmallVariant ? "small" : "medium", subtle: true, weight: isSmallVariant ? "normal" : "bold", children: title })), jsx("div", { className: twMerge("truncate", "whitespace-nowrap"), children: loading ? (jsx(SkeletonLines, { className: "flex h-7 flex-row items-center", "data-testid": dataTestId ? `${dataTestId}-value-loading` : undefined, height: themeFontSize.base, lines: 1, width: "100%" })) : (jsxs(Text, { className: "truncate whitespace-nowrap text-lg font-medium", "data-testid": dataTestId ? `${dataTestId}-value` : undefined, size: isSmallVariant ? "small" : "large", type: "div", weight: isSmallVariant ? "bold" : "thick", children: [value, " ", unit] })) })] }) }));
3187
+ };
3188
+
3189
+ /**
3190
+ * The TrendIndicator component is used within the KPI Card component to display the trend indicator.
3191
+ *
3192
+ * @param {TrendIndicatorProps} props - The props for the TrendIndicator component
3193
+ * @returns {ReactElement} TrendIndicator component
3194
+ */
3195
+ const TrendIndicator = ({ value, trend, label, icon = undefined, color = undefined, "data-testid": dataTestId, className, }) => {
3196
+ return (jsxs("div", { className: twMerge("flex flex-row items-center gap-1", className), "data-testid": dataTestId, children: [value !== undefined ? (jsx(Text, { "data-testid": dataTestId ? `${dataTestId}-value` : undefined, size: "small", weight: "normal", children: value })) : null, jsxs("div", { className: "flex items-center", children: [icon ? (jsx(Icon, { color: color, "data-testid": dataTestId ? `${dataTestId}-icon` : undefined, name: icon, size: "small" })) : null, jsx(Text, { className: cvaKPITrendPercentage({ color }), "data-testid": dataTestId ? `${dataTestId}-trend` : undefined, size: "small", weight: "bold", children: trend })] }), jsx(Text, { "data-testid": dataTestId ? `${dataTestId}-label` : undefined, size: "small", weight: "normal", children: label })] }));
3197
+ };
3198
+
3199
+ /**
3200
+ * The TrendIndicators component is used within the KPI Card component to display the trend indicators.
3201
+ *
3202
+ * @param {TrendIndicatorsProps} props - The props for the TrendIndicators component
3203
+ * @returns {ReactElement} TrendIndicators component
3204
+ */
3205
+ const TrendIndicators = ({ trends, "data-testid": dataTestId, className, }) => {
3206
+ return (jsx("span", { className: twMerge("flex flex-row items-center gap-1", className), "data-testid": dataTestId, children: trends.map((trend, index) => (jsx(TrendIndicator, { "data-testid": dataTestId ? `${dataTestId}-trend-indicator-${index}` : undefined, ...trend }, index))) }));
3207
+ };
3208
+
3209
+ const cvaValueBar = cvaMerge([
3210
+ "w-full",
3211
+ "overflow-hidden",
3212
+ "rounded",
3213
+ "bg-neutral-100",
3214
+ "appearance-none",
3215
+ "[&::-webkit-progress-bar]:bg-transparent",
3216
+ "[&::-webkit-progress-value]:bg-current",
3217
+ "[&::-moz-progress-bar]:bg-current",
3218
+ ], {
3140
3219
  variants: {
3141
- color: {
3142
- success: ["text-success-500"],
3143
- danger: ["text-danger-500"],
3144
- neutral: ["text-neutral-500"],
3220
+ size: {
3221
+ extraSmall: "h-1",
3222
+ small: "h-3",
3223
+ large: "h-9",
3145
3224
  },
3146
3225
  },
3147
3226
  defaultVariants: {
3148
- color: "success",
3227
+ size: "small",
3149
3228
  },
3150
3229
  });
3151
- const cvaKPIValueContainer = cvaMerge(["truncate", "whitespace-nowrap"], {
3230
+ const cvaValueBarText = cvaMerge(["whitespace-nowrap"], {
3152
3231
  variants: {
3153
- isDefaultAndHasTrendValue: {
3154
- true: [],
3155
- false: ["flex", "flex-row", "items-center", "gap-1"],
3232
+ size: {
3233
+ small: "leading-xs text-xs font-medium text-neutral-600",
3234
+ large: "absolute pl-3 text-base text-white drop-shadow-lg",
3156
3235
  },
3157
3236
  },
3158
3237
  defaultVariants: {
3159
- isDefaultAndHasTrendValue: true,
3238
+ size: "small",
3160
3239
  },
3161
3240
  });
3162
3241
 
3163
- const LoadingContent$1 = () => (jsx("div", { className: "flex h-11 flex-row items-center gap-3", "data-testid": "kpi-card-loading-content", children: jsx("div", { className: "w-full", children: jsx(SkeletonLines, { gap: 3, height: [14, 18], lines: 2, width: [80, 60] }) }) }));
3164
3242
  /**
3165
- * The KPI component is used to display KPIs.
3166
- *
3167
- * @param {KPIProps} props - The props for the KPI component
3168
- * @returns {ReactElement} KPI component
3243
+ * Helper function to get normalized score in range 0-1 used by the ValueBar
3244
+
3245
+ * @param {number} value - value for which score should be returned
3246
+ * @param {number} min - min range value
3247
+ * @param {number} max - max range value
3248
+ * @param {boolean} zeroScoreAllowed - If true, allows the score to be exactly 0 when the value is less than or equal to the minimum.
3249
+ * If false or not provided, ensures a minimal score (0.01) is returned to always render a small fragment.
3250
+ * @returns {number} normalized score
3169
3251
  */
3170
- const KPI = ({ title, value, loading = false, unit, className, "data-testid": dataTestId, tooltipLabel, variant = "default", trend, style, ...rest }) => {
3171
- const isSmallVariant = variant === "small";
3172
- return (jsx(Tooltip, { "data-testid": dataTestId ? `${dataTestId}-tooltip` : undefined, disabled: tooltipLabel === undefined || tooltipLabel === "", label: tooltipLabel, placement: "bottom", style: style, children: jsx("div", { className: cvaKPI({ variant, className }), "data-testid": dataTestId ? `${dataTestId}` : undefined, ...rest, children: loading ? (jsx(LoadingContent$1, {})) : (jsxs(Fragment$1, { children: [jsx("div", { className: cvaKPIHeader(), children: jsx(Text, { className: cvaKPITitleText(), "data-testid": dataTestId ? `${dataTestId}-title` : undefined, size: isSmallVariant ? "small" : "medium", subtle: true, weight: isSmallVariant ? "normal" : "bold", children: title }) }), jsx(Text, { className: cvaKPIvalueText({ variant }), "data-testid": dataTestId ? `${dataTestId}-value` : undefined, size: isSmallVariant ? "small" : "large", type: "div", weight: isSmallVariant ? "bold" : "thick", children: jsxs("div", { className: cvaKPIValueContainer({
3173
- isDefaultAndHasTrendValue: Boolean(trend !== undefined && trend.value !== undefined && !isSmallVariant),
3174
- className,
3175
- }), children: [jsxs("span", { className: cvaKPIvalueText({ variant }), children: [value, " ", unit] }), jsx(TrendIndicator, { isSmallVariant: isSmallVariant, trend: trend, unit: unit })] }) })] })) }) }));
3252
+ const getScore = (value, min, max, zeroScoreAllowed) => {
3253
+ if (value <= min) {
3254
+ if (zeroScoreAllowed === true) {
3255
+ return 0;
3256
+ }
3257
+ return 0.01; // always render at least some small fragment
3258
+ }
3259
+ if (value >= max) {
3260
+ return 1;
3261
+ }
3262
+ return (value - min) / (max - min);
3176
3263
  };
3177
- const TrendIndicator = ({ trend, unit, isSmallVariant }) => {
3178
- if (!trend) {
3179
- return null;
3264
+ /**
3265
+ * Helper function to get default color used by the ValueBar
3266
+
3267
+ * @param {number} score - score number for which color should be returned
3268
+ * @returns {string} color value
3269
+ */
3270
+ const getDefaultFillColor = (score) => {
3271
+ if (score < 0.3) {
3272
+ return color("DANGER", 500, "CSS");
3273
+ }
3274
+ if (score >= 0.6) {
3275
+ return color("SUCCESS", 600, "CSS");
3180
3276
  }
3181
- return (jsxs("div", { className: "flex flex-row items-center gap-1", "data-testid": "trend-indicator", children: [!isSmallVariant && trend.value !== undefined ? (jsxs(Text, { "data-testid": "trend-value", size: "small", weight: "normal", children: [trend.value, " ", unit] })) : null, trend.variant !== undefined && trend.variant.icon !== undefined ? (jsx(Icon, { color: trend.variant.color, name: trend.variant.icon, size: "small" })) : null, trend.percentage !== undefined ? (jsxs(Text, { className: cvaKPITrendPercentage({ color: trend.variant?.color }), size: "small", weight: "bold", children: [trend.percentage, "%"] })) : null] }));
3277
+ return color("WARNING", 300, "CSS");
3278
+ };
3279
+ /**
3280
+ * Helper function to get custom color used by the ValueBar
3281
+
3282
+ * @param {number} score - score number for which color should be returned
3283
+ * @param {LevelColors} levelColors - custom colors and levels definitions
3284
+ * @returns {string} color value
3285
+ */
3286
+ const getFillColor = (score, levelColors) => {
3287
+ if (levelColors.low !== undefined && score < (levelColors.low.level !== undefined ? levelColors.low.level : 0)) {
3288
+ return levelColors.low.color;
3289
+ }
3290
+ if (levelColors.high !== undefined && score >= (levelColors.high.level !== undefined ? levelColors.high.level : 0)) {
3291
+ return levelColors.high.color;
3292
+ }
3293
+ return levelColors.medium?.color ?? color("WARNING", 300);
3294
+ };
3295
+ /**
3296
+ * Helper function to get color used by the ValueBar
3297
+
3298
+ * @param {number} value - value for which color should be returned
3299
+ * @param {number} min - min range value
3300
+ * @param {number} max - max range value
3301
+ * @param {LevelColors} levelColors - level colors used to coloring the bar
3302
+ * @returns {ReactElement} ValueBar component
3303
+ */
3304
+ const getValueBarColorByValue = (value, min, max, levelColors) => {
3305
+ const score = getScore(value, min, max);
3306
+ return getFillColor(score, levelColors);
3307
+ };
3308
+
3309
+ /**
3310
+ * ValueBar component is used to display value on a colorful bar within provided range.
3311
+
3312
+ * @param {ValueBarProps} props - The props for the ValueBar component
3313
+ * @returns {ReactElement} ValueBar component
3314
+ */
3315
+ const ValueBar = ({ value, min = 0, max = 100, unit, size = "small", levelColors, valueColor, showValue = false, className, "data-testid": dataTestId, zeroScoreAllowed = false, }) => {
3316
+ const score = getScore(value, min, max, zeroScoreAllowed);
3317
+ const barFillColor = levelColors ? getFillColor(score, levelColors) : getDefaultFillColor(score);
3318
+ const valueText = `${Number(value.toFixed(1))}${nonNullable(unit) ? unit : ""}`;
3319
+ return (jsxs("span", { className: "relative flex items-center gap-2", "data-testid": dataTestId, children: [jsx("progress", { "aria-label": valueText, className: cvaValueBar({ className, size }), max: 100, style: { color: barFillColor }, value: score * 100 }), showValue && (size === "small" || size === "large") ? (jsx(Text, { className: cvaValueBarText({ size }), "data-testid": dataTestId ? `${dataTestId}-value` : undefined, children: jsx("span", { style: valueColor ? { color: valueColor } : undefined, children: valueText }) })) : null] }));
3182
3320
  };
3183
3321
 
3184
3322
  const cvaKPICard = cvaMerge([
@@ -3212,14 +3350,6 @@ const cvaKPIIconContainer = cvaMerge(["flex", "items-center", "justify-center",
3212
3350
  },
3213
3351
  },
3214
3352
  });
3215
- const cvaCardBodyContainer = cvaMerge(["grid", "grid-cols-[1fr_auto]", "px-3", "pb-2", "pt-3"], {
3216
- variants: {
3217
- iconName: {
3218
- true: "gap-2",
3219
- false: "gap-3 ",
3220
- },
3221
- },
3222
- });
3223
3353
 
3224
3354
  /**
3225
3355
  * The KPICard component is used to display KPIs.
@@ -3227,13 +3357,15 @@ const cvaCardBodyContainer = cvaMerge(["grid", "grid-cols-[1fr_auto]", "px-3", "
3227
3357
  * @param {KPICardProps} props - The props for the KPICard component
3228
3358
  * @returns {ReactElement} KPICard component
3229
3359
  */
3230
- const KPICard = ({ isActive = false, onClick, className, "data-testid": dataTestId, children, iconName = undefined, iconColor = "info", loading = false, ...rest }) => {
3360
+ const KPICard = ({ isActive = false, onClick, className, "data-testid": dataTestId, children, iconName = undefined, iconColor = "info", loading = false, notice, valueBar, trends, unit, ...rest }) => {
3231
3361
  const isClickable = Boolean(onClick !== undefined && loading !== true);
3232
3362
  return (jsx(Card, { className: cvaKPICard({
3233
3363
  isClickable,
3234
3364
  isActive,
3235
3365
  className,
3236
- }), "data-testid": dataTestId ? dataTestId : undefined, onClick: onClick, children: jsxs(CardBody, { className: cvaCardBodyContainer({ iconName: Boolean(iconName) }), gap: "none", padding: "none", children: [jsx(KPI, { ...rest, className: "p-0", "data-testid": dataTestId ? `${dataTestId}-kpi` : undefined, loading: loading }), iconName ? (jsx("div", { className: cvaKPIIconContainer({ iconColor }), children: jsx(Icon, { name: iconName, size: "small", type: "solid" }) })) : null, children] }) }));
3366
+ }), "data-testid": dataTestId ? dataTestId : undefined, onClick: onClick, children: jsxs(CardBody, { className: "grid gap-2 px-3 pb-2 pt-3", gap: "none", padding: "none", children: [jsxs("div", { className: "grid grid-cols-[1fr_auto] justify-between gap-2", children: [jsx(KPI, { ...rest, className: "p-0", "data-testid": dataTestId ? `${dataTestId}-kpi` : undefined, loading: loading, unit: unit }), iconName ? (jsx("div", { className: cvaKPIIconContainer({ iconColor }), children: jsx(Icon, { name: iconName, size: "small", type: "solid" }) })) : null] }), trends !== undefined && trends.length > 0 ? (loading ? (jsx(SkeletonLines, { className: "h-4", "data-testid": dataTestId ? `${dataTestId}-trend-indicator-loading` : undefined, height: themeFontSize.xs, lines: 1, width: "100%" })) : (jsx(TrendIndicators, { "data-testid": dataTestId ? `${dataTestId}-trend-indicators` : undefined, trends: trends }))) : null, valueBar !== undefined ? (loading ? (jsx(SkeletonLines, { className: "h-4", "data-testid": dataTestId ? `${dataTestId}-value-bar-loading` : undefined, gap: 0, height: themeFontSize.xs, lines: 1, width: "100%" })) : (jsx(ValueBar, { className: "h-2", "data-testid": dataTestId ? `${dataTestId}-value-bar` : undefined, ...valueBar }))) : null, notice !== undefined ? (loading ? (jsx(SkeletonLines, { className: "h-4", "data-testid": dataTestId ? `${dataTestId}-notice-loading` : undefined, gap: 0, height: themeFontSize.xs, lines: 1, width: "100%" })) : (
3367
+ // NOTE: Can't use Notice component here due to the non-flexible text styling options
3368
+ jsxs("div", { className: "flex items-center gap-1 truncate", "data-testid": dataTestId ? `${dataTestId}-notice` : undefined, children: [notice.iconName ? (jsx(Icon, { color: notice.iconColor, "data-testid": dataTestId ? `${dataTestId}-notice-icon` : undefined, name: notice.iconName, size: "small" })) : null, jsx(Text, { className: "truncate text-neutral-900", "data-testid": dataTestId ? `${dataTestId}-notice-label` : undefined, size: "small", children: notice.label })] }))) : null, children] }) }));
3237
3369
  };
3238
3370
 
3239
3371
  const cvaListContainer = cvaMerge(["overflow-y-auto", "overflow-x-hidden", "h-full"], {
@@ -3369,6 +3501,7 @@ const ListLoadingIndicator = ({ type, hasThumbnail, thumbnailShape, hasDescripti
3369
3501
  *
3370
3502
  * @example Basic usage
3371
3503
  * ```tsx
3504
+ * import { useList, List } from "@trackunit/react-components";
3372
3505
  * const list = useList({
3373
3506
  * count: items.length,
3374
3507
  * getItem: index => items[index],
@@ -3385,6 +3518,7 @@ const ListLoadingIndicator = ({ type, hasThumbnail, thumbnailShape, hasDescripti
3385
3518
  * ```
3386
3519
  * @example With header
3387
3520
  * ```tsx
3521
+ * import { useList, List } from "@trackunit/react-components";
3388
3522
  * const list = useList({
3389
3523
  * count: items.length,
3390
3524
  * getItem: index => items[index],
@@ -3893,7 +4027,11 @@ const cvaMenuList = cvaMerge([
3893
4027
  "overflow-y-auto",
3894
4028
  ]);
3895
4029
  const cvaMenuListDivider = cvaMerge(["mx-[-4px]", "my-0.5", "min-h-px", "bg-neutral-300"]);
3896
- const cvaMenuListMultiSelect = cvaMerge("hover:!bg-blue-200");
4030
+ const cvaMenuListMultiSelect = cvaMerge([
4031
+ "!has-[:checked]:bg-primary-100",
4032
+ "!hover:has-[:checked]:bg-primary-200",
4033
+ "!focus-within:has-[:checked]:bg-primary-100",
4034
+ ]);
3897
4035
  const cvaMenuListItem = cvaMerge("max-w-full");
3898
4036
 
3899
4037
  /**
@@ -3984,7 +4122,7 @@ const cvaMenuItemPrefix = cvaMerge([
3984
4122
  ], {
3985
4123
  variants: {
3986
4124
  selected: {
3987
- true: "text-neutral-600",
4125
+ true: "text-primary-600",
3988
4126
  false: "",
3989
4127
  },
3990
4128
  variant: {
@@ -4099,7 +4237,7 @@ const MenuList = ({ "data-testid": dataTestId, className, children, isMulti = fa
4099
4237
  : cvaMenuListItem({ className: menuItem.props.className }),
4100
4238
  selected: isSelected,
4101
4239
  suffix: menuItem.props.suffix ??
4102
- (isMulti && isSelected ? jsx(Icon, { className: "block text-blue-600", name: "Check", size: "medium" }) : null),
4240
+ (isMulti && isSelected ? jsx(Icon, { className: "text-primary-600 block", name: "Check", size: "medium" }) : null),
4103
4241
  });
4104
4242
  }
4105
4243
  return null;
@@ -4127,8 +4265,8 @@ const MoreMenu = ({ className, "data-testid": dataTestId, popoverProps, iconProp
4127
4265
  return (jsx("div", { className: cvaMoreMenu({ className }), "data-testid": dataTestId ? dataTestId : undefined, ref: actionMenuRef, children: jsxs(Popover, { placement: "bottom-end", ...popoverProps, children: [jsx(PopoverTrigger, { children: customButton ?? (jsx(IconButton, { "data-testid": "more-menu-icon", ...iconButtonProps, icon: jsx(Icon, { name: "EllipsisHorizontal", ...iconProps }) })) }), jsx(PopoverContent, { portalId: customPortalId, children: close => (typeof children === "function" ? children(close) : children) })] }) }));
4128
4266
  };
4129
4267
 
4130
- const cvaNotice = cvaMerge(["flex", "items-center"]);
4131
- const cvaNoticeLabel = cvaMerge(["pl-1", "pr-1", "font-medium", "text-sm"], {
4268
+ const cvaNotice = cvaMerge(["flex", "items-center", "gap-1"]);
4269
+ const cvaNoticeLabel = cvaMerge(["font-medium", "text-sm", "overflow-hidden", "text-ellipsis"], {
4132
4270
  variants: {
4133
4271
  color: {
4134
4272
  neutral: "text-neutral-400",
@@ -4168,12 +4306,12 @@ const cvaNoticeIcon = cvaMerge(["rounded-full", "items-center", "justify-center"
4168
4306
  * _**Do use** notices to communicate non-essential information that does not necessarily require action to be taken._
4169
4307
  *
4170
4308
  * _**Do not use** notices for essential information (use `<Alert/>` instead), or to communicate information related to the state of an asset (use `<Indicator/>` instead)._
4171
-
4309
+ *
4172
4310
  * @param {NoticeProps} props - The props for the Notice component
4173
4311
  * @returns {ReactElement} Notice component
4174
4312
  */
4175
- const Notice = ({ "data-testid": dataTestId, icon, label, color = "neutral", withLabel = true, className, tooltipLabel = label, withTooltip = false, size = "medium", ...rest }) => {
4176
- return (jsx(Tooltip, { className: className, disabled: withTooltip === false, label: tooltipLabel, placement: "bottom", children: jsxs("div", { "aria-label": label, className: cvaNotice(), "data-testid": dataTestId, ...rest, children: [jsx("div", { className: cvaNoticeIcon({ color }), "data-testid": dataTestId ? `${dataTestId}-icon` : "notice-icon", children: icon }), label && withLabel ? (jsx("div", { className: cvaNoticeLabel({ color, size }), "data-testid": dataTestId ? `${dataTestId}-label` : "notice-label", children: label })) : null] }) }));
4313
+ const Notice = ({ "data-testid": dataTestId, iconName = undefined, iconSize = "medium", iconColor = undefined, label, color = "neutral", withLabel = true, className, tooltipLabel = label, withTooltip = false, size = "medium", ...rest }) => {
4314
+ return (jsx(Tooltip, { className: className, disabled: withTooltip === false, label: tooltipLabel, placement: "bottom", children: jsxs("div", { "aria-label": label, className: cvaNotice(), "data-testid": dataTestId, ...rest, children: [nonNullable(iconName) ? (jsx("div", { className: cvaNoticeIcon({ color: iconColor || color }), "data-testid": dataTestId ? `${dataTestId}-icon` : "notice-icon", children: jsx(Icon, { name: iconName, size: iconSize }) })) : null, label && withLabel ? (jsx("div", { className: cvaNoticeLabel({ color, size }), "data-testid": dataTestId ? `${dataTestId}-label` : "notice-label", children: label })) : null] }) }));
4177
4315
  };
4178
4316
 
4179
4317
  const cvaPage = cvaMerge(["grid", "h-full"], {
@@ -4563,7 +4701,18 @@ const cvaSpacer = cvaMerge([], {
4563
4701
 
4564
4702
  /**
4565
4703
  * The Spacer component is used for adding a bit of space in the ui.
4566
-
4704
+ *
4705
+ * @example basic usage
4706
+ * ```tsx
4707
+ * import { Spacer } from "@trackunit/react-components";
4708
+ * const MySpacer = () => {
4709
+ * return (
4710
+ * <div>
4711
+ * <Spacer size="small" border data-testid="my-spacer-testid" />
4712
+ * </div>
4713
+ * );
4714
+ * };
4715
+ * ```
4567
4716
  * @param {SpacerProps} props - The props for the Spacer component
4568
4717
  * @returns {ReactElement} Spacer component
4569
4718
  */
@@ -4993,119 +5142,6 @@ const ToggleButton = ({ title, size, children, "data-testid": dataTestId, classN
4993
5142
  return (jsx("button", { className: twMerge("flex items-center justify-center gap-1 self-stretch", paddingClasses, className), "data-testid": dataTestId, title: isIconOnly ? title : undefined, type: "button", ...rest, children: isIconOnly ? (icon) : (jsxs(Fragment$1, { children: [iconPrefix, children] })) }));
4994
5143
  };
4995
5144
 
4996
- const cvaValueBar = cvaMerge([
4997
- "w-full",
4998
- "overflow-hidden",
4999
- "rounded",
5000
- "bg-neutral-100",
5001
- "appearance-none",
5002
- "[&::-webkit-progress-bar]:bg-transparent",
5003
- "[&::-webkit-progress-value]:bg-current",
5004
- "[&::-moz-progress-bar]:bg-current",
5005
- ], {
5006
- variants: {
5007
- size: {
5008
- extraSmall: "h-1",
5009
- small: "h-3",
5010
- large: "h-9",
5011
- },
5012
- },
5013
- defaultVariants: {
5014
- size: "small",
5015
- },
5016
- });
5017
- const cvaValueBarText = cvaMerge(["whitespace-nowrap"], {
5018
- variants: {
5019
- size: {
5020
- small: "text-sm font-bold text-neutral-400",
5021
- large: "absolute pl-3 text-base text-white drop-shadow-lg",
5022
- },
5023
- },
5024
- defaultVariants: {
5025
- size: "small",
5026
- },
5027
- });
5028
-
5029
- /**
5030
- * Helper function to get normalized score in range 0-1 used by the ValueBar
5031
-
5032
- * @param {number} value - value for which score should be returned
5033
- * @param {number} min - min range value
5034
- * @param {number} max - max range value
5035
- * @param {boolean} zeroScoreAllowed - If true, allows the score to be exactly 0 when the value is less than or equal to the minimum.
5036
- * If false or not provided, ensures a minimal score (0.01) is returned to always render a small fragment.
5037
- * @returns {number} normalized score
5038
- */
5039
- const getScore = (value, min, max, zeroScoreAllowed) => {
5040
- if (value <= min) {
5041
- if (zeroScoreAllowed === true) {
5042
- return 0;
5043
- }
5044
- return 0.01; // always render at least some small fragment
5045
- }
5046
- if (value >= max) {
5047
- return 1;
5048
- }
5049
- return (value - min) / (max - min);
5050
- };
5051
- /**
5052
- * Helper function to get default color used by the ValueBar
5053
-
5054
- * @param {number} score - score number for which color should be returned
5055
- * @returns {string} color value
5056
- */
5057
- const getDefaultFillColor = (score) => {
5058
- if (score < 0.3) {
5059
- return color("DANGER", 500, "CSS");
5060
- }
5061
- if (score >= 0.6) {
5062
- return color("SUCCESS", 600, "CSS");
5063
- }
5064
- return color("WARNING", 300, "CSS");
5065
- };
5066
- /**
5067
- * Helper function to get custom color used by the ValueBar
5068
-
5069
- * @param {number} score - score number for which color should be returned
5070
- * @param {LevelColors} levelColors - custom colors and levels definitions
5071
- * @returns {string} color value
5072
- */
5073
- const getFillColor = (score, levelColors) => {
5074
- if (levelColors.low !== undefined && score < (levelColors.low.level !== undefined ? levelColors.low.level : 0)) {
5075
- return levelColors.low.color;
5076
- }
5077
- if (levelColors.high !== undefined && score >= (levelColors.high.level !== undefined ? levelColors.high.level : 0)) {
5078
- return levelColors.high.color;
5079
- }
5080
- return levelColors.medium?.color ?? color("WARNING", 300);
5081
- };
5082
- /**
5083
- * Helper function to get color used by the ValueBar
5084
-
5085
- * @param {number} value - value for which color should be returned
5086
- * @param {number} min - min range value
5087
- * @param {number} max - max range value
5088
- * @param {LevelColors} levelColors - level colors used to coloring the bar
5089
- * @returns {ReactElement} ValueBar component
5090
- */
5091
- const getValueBarColorByValue = (value, min, max, levelColors) => {
5092
- const score = getScore(value, min, max);
5093
- return getFillColor(score, levelColors);
5094
- };
5095
-
5096
- /**
5097
- * ValueBar component is used to display value on a colorful bar within provided range.
5098
-
5099
- * @param {ValueBarProps} props - The props for the ValueBar component
5100
- * @returns {ReactElement} ValueBar component
5101
- */
5102
- const ValueBar = ({ value, min = 0, max = 100, unit, size = "small", levelColors, valueColor, showValue = false, className, "data-testid": dataTestId, zeroScoreAllowed = false, }) => {
5103
- const score = getScore(value, min, max, zeroScoreAllowed);
5104
- const barFillColor = levelColors ? getFillColor(score, levelColors) : getDefaultFillColor(score);
5105
- const valueText = `${Number(value.toFixed(1))}${nonNullable(unit) ? unit : ""}`;
5106
- return (jsxs("span", { className: "relative flex items-center gap-2", "data-testid": dataTestId, children: [jsx("progress", { "aria-label": valueText, className: cvaValueBar({ className, size }), max: 100, style: { color: barFillColor }, value: score * 100 }), showValue && (size === "small" || size === "large") ? (jsx(Text, { className: cvaValueBarText({ size }), "data-testid": dataTestId ? `${dataTestId}-value` : undefined, children: jsx("span", { style: valueColor ? { color: valueColor } : undefined, children: valueText }) })) : null] }));
5107
- };
5108
-
5109
5145
  /**
5110
5146
  * Base64URL encode bytes to a URL-safe string
5111
5147
  */
@@ -6047,4 +6083,4 @@ const useWindowActivity = ({ onFocus, onBlur, skip = false } = { onBlur: undefin
6047
6083
  return useMemo(() => ({ focused }), [focused]);
6048
6084
  };
6049
6085
 
6050
- export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DetailsList, EmptyState, EmptyValue, ExternalLink, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, List, ListItem, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, Prompt, ROLE_CARD, SectionHeader, Sidebar, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, ValueBar, ZStack, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getResponsiveRandomWidthPercentage, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMergeRefs, useModifierKey, useOverflowItems, usePopoverContext, usePrevious, usePrompt, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSelfUpdatingRef, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
6086
+ export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DetailsList, EmptyState, EmptyValue, ExternalLink, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, List, ListItem, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, Prompt, ROLE_CARD, SectionHeader, Sidebar, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getResponsiveRandomWidthPercentage, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMergeRefs, useModifierKey, useOverflowItems, usePopoverContext, usePrevious, usePrompt, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSelfUpdatingRef, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "1.12.13",
3
+ "version": "1.13.2",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -14,10 +14,10 @@
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.9.9",
18
- "@trackunit/css-class-variance-utilities": "1.9.9",
19
- "@trackunit/shared-utils": "1.11.9",
20
- "@trackunit/ui-icons": "1.9.9",
17
+ "@trackunit/ui-design-tokens": "1.9.12",
18
+ "@trackunit/css-class-variance-utilities": "1.9.12",
19
+ "@trackunit/shared-utils": "1.11.12",
20
+ "@trackunit/ui-icons": "1.9.12",
21
21
  "@tanstack/react-router": "1.114.29",
22
22
  "es-toolkit": "^1.39.10",
23
23
  "@tanstack/react-virtual": "3.13.12",