@trackunit/react-components 0.1.414 → 0.2.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 +200 -1
- package/index.esm.js +198 -4
- package/package.json +3 -2
- package/src/components/ImageCollection/ImageCollection.d.ts +26 -0
- package/src/components/ImageCollection/helpers.d.ts +13 -0
- package/src/components/index.d.ts +5 -0
- package/src/hooks/index.d.ts +1 -0
- package/src/hooks/useViewportSize.d.ts +32 -0
package/index.cjs.js
CHANGED
|
@@ -16,9 +16,10 @@ var isEqual = require('lodash/isEqual');
|
|
|
16
16
|
var reactUse = require('react-use');
|
|
17
17
|
var reactRouter = require('@tanstack/react-router');
|
|
18
18
|
var reactSwipeable = require('react-swipeable');
|
|
19
|
+
var ImageGallery = require('react-image-gallery');
|
|
20
|
+
var tailwindMerge = require('tailwind-merge');
|
|
19
21
|
var react = require('@floating-ui/react');
|
|
20
22
|
var omit = require('lodash/omit');
|
|
21
|
-
var tailwindMerge = require('tailwind-merge');
|
|
22
23
|
var reactHelmetAsync = require('react-helmet-async');
|
|
23
24
|
var reactTabs = require('@radix-ui/react-tabs');
|
|
24
25
|
var dateFns = require('date-fns');
|
|
@@ -1292,6 +1293,81 @@ const useSelfUpdatingRef = (initialState) => {
|
|
|
1292
1293
|
return stateRef;
|
|
1293
1294
|
};
|
|
1294
1295
|
|
|
1296
|
+
/**
|
|
1297
|
+
* Maps viewport size keys to their corresponding state property names.
|
|
1298
|
+
*/
|
|
1299
|
+
const propsMap = {
|
|
1300
|
+
xs: "isXs",
|
|
1301
|
+
sm: "isSm",
|
|
1302
|
+
md: "isMd",
|
|
1303
|
+
lg: "isLg",
|
|
1304
|
+
xl: "isXl",
|
|
1305
|
+
"2xl": "is2xl",
|
|
1306
|
+
"3xl": "is3xl",
|
|
1307
|
+
};
|
|
1308
|
+
/**
|
|
1309
|
+
* The default state for viewport sizes, with all values set to false.
|
|
1310
|
+
*/
|
|
1311
|
+
const defaultState = {
|
|
1312
|
+
isXs: false,
|
|
1313
|
+
isSm: false,
|
|
1314
|
+
isMd: false,
|
|
1315
|
+
isLg: false,
|
|
1316
|
+
isXl: false,
|
|
1317
|
+
is2xl: false,
|
|
1318
|
+
is3xl: false,
|
|
1319
|
+
};
|
|
1320
|
+
/**
|
|
1321
|
+
* A custom React hook that provides real-time information about the current viewport size.
|
|
1322
|
+
*
|
|
1323
|
+
* This hook listens to changes in the viewport size and returns an object with boolean values
|
|
1324
|
+
* indicating which breakpoints are currently active. It's useful for creating responsive
|
|
1325
|
+
* layouts and components that need to adapt to different screen sizes.
|
|
1326
|
+
*
|
|
1327
|
+
* @returns {ViewportSizeState} An object containing boolean values for each viewport size breakpoint.
|
|
1328
|
+
* @example
|
|
1329
|
+
* function MyComponent() {
|
|
1330
|
+
* const viewportSize = useViewportSize();
|
|
1331
|
+
*
|
|
1332
|
+
* if (viewportSize.isLg) {
|
|
1333
|
+
* return <LargeScreenLayout />;
|
|
1334
|
+
* } else if (viewportSize.isMd) {
|
|
1335
|
+
* return <MediumScreenLayout />;
|
|
1336
|
+
* } else {
|
|
1337
|
+
* return <SmallScreenLayout />;
|
|
1338
|
+
* }
|
|
1339
|
+
* }
|
|
1340
|
+
*/
|
|
1341
|
+
const useViewportSize = () => {
|
|
1342
|
+
const [viewportSize, setViewportSize] = React.useState(() => defaultState);
|
|
1343
|
+
const updateViewportSize = React.useCallback(() => {
|
|
1344
|
+
const newViewportSize = sharedUtils.objectEntries(uiDesignTokens.themeScreenSizeAsNumber).reduce((acc, [size, minWidth]) => {
|
|
1345
|
+
const matches = window.matchMedia(`(min-width: ${minWidth}px)`).matches;
|
|
1346
|
+
return {
|
|
1347
|
+
...acc,
|
|
1348
|
+
[propsMap[size]]: matches,
|
|
1349
|
+
};
|
|
1350
|
+
}, Object.assign({}, defaultState));
|
|
1351
|
+
setViewportSize(newViewportSize);
|
|
1352
|
+
}, []);
|
|
1353
|
+
React.useEffect(() => {
|
|
1354
|
+
// Initial check
|
|
1355
|
+
updateViewportSize();
|
|
1356
|
+
// Set up listeners for each breakpoint
|
|
1357
|
+
const mediaQueryLists = sharedUtils.objectEntries(uiDesignTokens.themeScreenSizeAsNumber).map(([_, minWidth]) => window.matchMedia(`(min-width: ${minWidth}px)`));
|
|
1358
|
+
mediaQueryLists.forEach(mql => {
|
|
1359
|
+
mql.addEventListener("change", updateViewportSize);
|
|
1360
|
+
});
|
|
1361
|
+
// Cleanup
|
|
1362
|
+
return () => {
|
|
1363
|
+
mediaQueryLists.forEach(mql => {
|
|
1364
|
+
mql.removeEventListener("change", updateViewportSize);
|
|
1365
|
+
});
|
|
1366
|
+
};
|
|
1367
|
+
}, [updateViewportSize]);
|
|
1368
|
+
return viewportSize;
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1295
1371
|
const hasFocus = () => typeof document !== "undefined" && document.hasFocus();
|
|
1296
1372
|
/**
|
|
1297
1373
|
* Use this hook to disable functionality while the tab is hidden within the browser or to react to focus or blur events
|
|
@@ -3523,6 +3599,124 @@ const ExternalLink = ({ rel = "noreferrer", target = "_blank", href, className,
|
|
|
3523
3599
|
return (jsxRuntime.jsx("a", { className: cvaExternalLink({ className }), "data-testid": dataTestId, href: href, onClick: onClick, rel: rel, target: target, title: title, children: children }));
|
|
3524
3600
|
};
|
|
3525
3601
|
|
|
3602
|
+
const IMAGE_ENDPOINT = "https://images.iris.trackunit.com";
|
|
3603
|
+
/**
|
|
3604
|
+
* Generates an url for the original sized image from the image id
|
|
3605
|
+
*/
|
|
3606
|
+
const createOriginalUrl = (id) => `${IMAGE_ENDPOINT}/${btoa(JSON.stringify({
|
|
3607
|
+
key: id,
|
|
3608
|
+
}))}`;
|
|
3609
|
+
const createResizedUrl = (id, width, height) => {
|
|
3610
|
+
const request = {
|
|
3611
|
+
key: id,
|
|
3612
|
+
edits: {
|
|
3613
|
+
resize: {
|
|
3614
|
+
width,
|
|
3615
|
+
height,
|
|
3616
|
+
fit: "contain",
|
|
3617
|
+
background: "transparent",
|
|
3618
|
+
},
|
|
3619
|
+
},
|
|
3620
|
+
};
|
|
3621
|
+
return `${IMAGE_ENDPOINT}/${btoa(JSON.stringify(request))}`;
|
|
3622
|
+
};
|
|
3623
|
+
/**
|
|
3624
|
+
* Generates an url for the thumbnail size image
|
|
3625
|
+
*/
|
|
3626
|
+
const createThumbnailUrl = (id) => createResizedUrl(id, 100, 100);
|
|
3627
|
+
/**
|
|
3628
|
+
* Generates srcSet HTML attribute to avoid loading the full size image on small screens
|
|
3629
|
+
*/
|
|
3630
|
+
const createSrcSet = (id) => `${createResizedUrl(id, 480)} 480w, ${createResizedUrl(id, 800)} 800w`;
|
|
3631
|
+
|
|
3632
|
+
const cvaGallery = cssClassVarianceUtilities.cvaMerge([
|
|
3633
|
+
// These make the gallery grow to the height of the parent container,
|
|
3634
|
+
// which avoids jumping of the thumbnail row when going through images
|
|
3635
|
+
// with different ratios
|
|
3636
|
+
"grow",
|
|
3637
|
+
"[&_.image-gallery-content]:h-[100%]",
|
|
3638
|
+
"[&_.image-gallery-content]:flex",
|
|
3639
|
+
"[&_.image-gallery-content]:flex-col",
|
|
3640
|
+
"[&_.image-gallery-slide-wrapper]:grow",
|
|
3641
|
+
"[&_.image-gallery-slide-wrapper]:flex",
|
|
3642
|
+
"[&_.image-gallery-slide-wrapper]:flex-col",
|
|
3643
|
+
"[&_.image-gallery-slide-wrapper]:justify-center",
|
|
3644
|
+
"[&_.image-gallery-slide]:flex",
|
|
3645
|
+
// This centers thumbnails and makes the clickable area independent of image size
|
|
3646
|
+
"[&_.image-gallery-thumbnail]:h-[100px]",
|
|
3647
|
+
]);
|
|
3648
|
+
/**
|
|
3649
|
+
* Show and manage images
|
|
3650
|
+
*
|
|
3651
|
+
* Will show thumbnail selection on desktop.
|
|
3652
|
+
*
|
|
3653
|
+
* Reduces bandwidth usage by lazy loading thumbnails and loading smaller versions of images depending on screen size.
|
|
3654
|
+
*/
|
|
3655
|
+
const ImageCollection = (props) => {
|
|
3656
|
+
const { imageIds, actions, emptyPlaceholderText, additionalItemClassName } = props;
|
|
3657
|
+
const [openImageId, setOpenImageId] = React.useState(imageIds[0]);
|
|
3658
|
+
const [isDeleting, setIsDeleting] = React.useState(false);
|
|
3659
|
+
const { width } = useResize();
|
|
3660
|
+
const fileInputRef = React.useRef(null);
|
|
3661
|
+
const imageGalleryRef = React.useRef(null);
|
|
3662
|
+
const uploadButton = React.useMemo(() => (actions === null || actions === void 0 ? void 0 : actions.upload) ? (jsxRuntime.jsx("div", { className: "flex justify-end", children: jsxRuntime.jsx(Button, { onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: actions.upload.label }) })) : null, [actions === null || actions === void 0 ? void 0 : actions.upload]);
|
|
3663
|
+
const items = React.useMemo(() => imageIds.map(imageId => ({
|
|
3664
|
+
id: imageId,
|
|
3665
|
+
original: createOriginalUrl(imageId),
|
|
3666
|
+
thumbnail: createThumbnailUrl(imageId),
|
|
3667
|
+
srcSet: createSrcSet(imageId),
|
|
3668
|
+
})), [imageIds]);
|
|
3669
|
+
React.useEffect(() => {
|
|
3670
|
+
if ((!openImageId || !imageIds.includes(openImageId)) && imageIds.length > 0) {
|
|
3671
|
+
setOpenImageId(imageIds[0]);
|
|
3672
|
+
}
|
|
3673
|
+
}, [imageIds, openImageId]);
|
|
3674
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [imageIds.length > 0 ? (jsxRuntime.jsxs("div", { className: "flex h-[100vh] max-h-[100vh] flex-col", children: [jsxRuntime.jsx(ImageGallery, { additionalClass: cvaGallery(), items: items, onBeforeSlide: index => { var _a; return setOpenImageId((_a = items[index]) === null || _a === void 0 ? void 0 : _a.id); }, ref: imageGalleryRef, renderItem: ({ original, srcSet,
|
|
3675
|
+
// Typing of the library is not flexible enough to add id, which is only used for the unit tests
|
|
3676
|
+
// @ts-ignore
|
|
3677
|
+
id, }) => (jsxRuntime.jsx("img", { alt: "full", className: tailwindMerge.twMerge(additionalItemClassName, "w-[100%]", "object-contain"), "data-testid": `image-${id}`, loading: "lazy", sizes: "(max-width: 480px) 480px, 800px", src: original, srcSet: srcSet })), renderThumbInner: ({ thumbnail,
|
|
3678
|
+
// Typing of the library is not flexible enough to add id, which is only used for the unit tests
|
|
3679
|
+
// @ts-ignore
|
|
3680
|
+
id, }) => jsxRuntime.jsx("img", { alt: "thumbnail", "data-testid": `thumbnail-${id}`, loading: "lazy", src: thumbnail }), showFullscreenButton: false, showPlayButton: false, showThumbnails: width > 768,
|
|
3681
|
+
// reduce sliding around during deletion
|
|
3682
|
+
slideDuration: isDeleting ? 0 : 200, ...props }), uploadButton, (actions === null || actions === void 0 ? void 0 : actions.remove) ? (jsxRuntime.jsx(Button, { className: "absolute right-2 top-2", loading: isDeleting, onClick: async () => {
|
|
3683
|
+
var _a;
|
|
3684
|
+
if (!(((_a = actions.remove) === null || _a === void 0 ? void 0 : _a.onRemove) && openImageId)) {
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
try {
|
|
3688
|
+
let nextIndex;
|
|
3689
|
+
const indexToDelete = imageIds.indexOf(openImageId);
|
|
3690
|
+
const deletingLastImage = indexToDelete === imageIds.length - 1;
|
|
3691
|
+
if (imageIds.length === 1) {
|
|
3692
|
+
nextIndex = undefined;
|
|
3693
|
+
}
|
|
3694
|
+
else if (deletingLastImage) {
|
|
3695
|
+
nextIndex = indexToDelete - 1;
|
|
3696
|
+
}
|
|
3697
|
+
else {
|
|
3698
|
+
// we set the index after the deletion, so the index will be the same
|
|
3699
|
+
nextIndex = indexToDelete;
|
|
3700
|
+
}
|
|
3701
|
+
setIsDeleting(true);
|
|
3702
|
+
await actions.remove.onRemove(openImageId);
|
|
3703
|
+
if (nextIndex !== undefined) {
|
|
3704
|
+
imageGalleryRef.current && nextIndex && imageGalleryRef.current.slideToIndex(nextIndex);
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
finally {
|
|
3708
|
+
setIsDeleting(false);
|
|
3709
|
+
}
|
|
3710
|
+
}, prefix: jsxRuntime.jsx(Icon, { name: "Trash", size: "small" }), variant: "secondary-danger", children: actions.remove.label })) : null] })) : (jsxRuntime.jsx(EmptyState, { action: uploadButton, description: emptyPlaceholderText })), jsxRuntime.jsx("input", {
|
|
3711
|
+
// Users can still select other files on mobile devices
|
|
3712
|
+
accept: "image/jpeg, image/png, image/bmp, image/webp, image/gif", hidden: true, multiple: true, onChange: async (e) => {
|
|
3713
|
+
if (!(e.target.files && (actions === null || actions === void 0 ? void 0 : actions.upload))) {
|
|
3714
|
+
return;
|
|
3715
|
+
}
|
|
3716
|
+
await actions.upload.onUpload(e.target.files);
|
|
3717
|
+
}, ref: fileInputRef, type: "file" })] }));
|
|
3718
|
+
};
|
|
3719
|
+
|
|
3526
3720
|
/**
|
|
3527
3721
|
* The hook that powers the Popover component.
|
|
3528
3722
|
* It should not be used directly, but rather through the Popover component.
|
|
@@ -4910,8 +5104,10 @@ exports.EmptyState = EmptyState;
|
|
|
4910
5104
|
exports.EmptyValue = EmptyValue;
|
|
4911
5105
|
exports.ExternalLink = ExternalLink;
|
|
4912
5106
|
exports.Heading = Heading;
|
|
5107
|
+
exports.IMAGE_ENDPOINT = IMAGE_ENDPOINT;
|
|
4913
5108
|
exports.Icon = Icon;
|
|
4914
5109
|
exports.IconButton = IconButton;
|
|
5110
|
+
exports.ImageCollection = ImageCollection;
|
|
4915
5111
|
exports.Indicator = Indicator;
|
|
4916
5112
|
exports.KPICard = KPICard;
|
|
4917
5113
|
exports.MenuItem = MenuItem;
|
|
@@ -4949,6 +5145,8 @@ exports.Tooltip = Tooltip;
|
|
|
4949
5145
|
exports.ValueBar = ValueBar;
|
|
4950
5146
|
exports.VirtualizedList = VirtualizedList;
|
|
4951
5147
|
exports.WidgetBody = WidgetBody;
|
|
5148
|
+
exports.createOriginalUrl = createOriginalUrl;
|
|
5149
|
+
exports.createThumbnailUrl = createThumbnailUrl;
|
|
4952
5150
|
exports.cvaButton = cvaButton;
|
|
4953
5151
|
exports.cvaButtonPrefixSuffix = cvaButtonPrefixSuffix;
|
|
4954
5152
|
exports.cvaButtonSpinner = cvaButtonSpinner;
|
|
@@ -4990,4 +5188,5 @@ exports.usePrompt = usePrompt;
|
|
|
4990
5188
|
exports.useResize = useResize;
|
|
4991
5189
|
exports.useSelfUpdatingRef = useSelfUpdatingRef;
|
|
4992
5190
|
exports.useTimeout = useTimeout;
|
|
5191
|
+
exports.useViewportSize = useViewportSize;
|
|
4993
5192
|
exports.useWindowActivity = useWindowActivity;
|
package/index.esm.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import React__default, { useRef, useMemo, useEffect, useState, useReducer, useCallback, memo, forwardRef, createContext, useContext, isValidElement, cloneElement, Children } from 'react';
|
|
4
|
-
import { objectKeys, objectValues } from '@trackunit/shared-utils';
|
|
5
|
-
import { rentalStatusPalette, intentPalette, generalPalette, criticalityPalette, activityPalette, utilizationPalette, sitesPalette, color } from '@trackunit/ui-design-tokens';
|
|
4
|
+
import { objectKeys, objectEntries, objectValues } from '@trackunit/shared-utils';
|
|
5
|
+
import { rentalStatusPalette, intentPalette, generalPalette, criticalityPalette, activityPalette, utilizationPalette, sitesPalette, themeScreenSizeAsNumber, color } from '@trackunit/ui-design-tokens';
|
|
6
6
|
import { iconNames } from '@trackunit/ui-icons';
|
|
7
7
|
import IconSpriteMini from '@trackunit/ui-icons/icons-sprite-mini.svg';
|
|
8
8
|
import IconSpriteOutline from '@trackunit/ui-icons/icons-sprite-outline.svg';
|
|
@@ -15,9 +15,10 @@ import isEqual from 'lodash/isEqual';
|
|
|
15
15
|
import { usePrevious, useMeasure, useCopyToClipboard } from 'react-use';
|
|
16
16
|
import { Link, useBlocker } from '@tanstack/react-router';
|
|
17
17
|
import { useSwipeable } from 'react-swipeable';
|
|
18
|
+
import ImageGallery from 'react-image-gallery';
|
|
19
|
+
import { twMerge } from 'tailwind-merge';
|
|
18
20
|
import { useFloating, autoUpdate, offset, flip, shift, size, useClick, useDismiss, useHover as useHover$1, useRole, useInteractions, useMergeRefs, FloatingPortal, FloatingFocusManager, arrow, useTransitionStatus, FloatingArrow } from '@floating-ui/react';
|
|
19
21
|
import omit from 'lodash/omit';
|
|
20
|
-
import { twMerge } from 'tailwind-merge';
|
|
21
22
|
import { HelmetProvider, Helmet } from 'react-helmet-async';
|
|
22
23
|
import { Trigger, Content, List, Root } from '@radix-ui/react-tabs';
|
|
23
24
|
import { format, formatDistance } from 'date-fns';
|
|
@@ -1272,6 +1273,81 @@ const useSelfUpdatingRef = (initialState) => {
|
|
|
1272
1273
|
return stateRef;
|
|
1273
1274
|
};
|
|
1274
1275
|
|
|
1276
|
+
/**
|
|
1277
|
+
* Maps viewport size keys to their corresponding state property names.
|
|
1278
|
+
*/
|
|
1279
|
+
const propsMap = {
|
|
1280
|
+
xs: "isXs",
|
|
1281
|
+
sm: "isSm",
|
|
1282
|
+
md: "isMd",
|
|
1283
|
+
lg: "isLg",
|
|
1284
|
+
xl: "isXl",
|
|
1285
|
+
"2xl": "is2xl",
|
|
1286
|
+
"3xl": "is3xl",
|
|
1287
|
+
};
|
|
1288
|
+
/**
|
|
1289
|
+
* The default state for viewport sizes, with all values set to false.
|
|
1290
|
+
*/
|
|
1291
|
+
const defaultState = {
|
|
1292
|
+
isXs: false,
|
|
1293
|
+
isSm: false,
|
|
1294
|
+
isMd: false,
|
|
1295
|
+
isLg: false,
|
|
1296
|
+
isXl: false,
|
|
1297
|
+
is2xl: false,
|
|
1298
|
+
is3xl: false,
|
|
1299
|
+
};
|
|
1300
|
+
/**
|
|
1301
|
+
* A custom React hook that provides real-time information about the current viewport size.
|
|
1302
|
+
*
|
|
1303
|
+
* This hook listens to changes in the viewport size and returns an object with boolean values
|
|
1304
|
+
* indicating which breakpoints are currently active. It's useful for creating responsive
|
|
1305
|
+
* layouts and components that need to adapt to different screen sizes.
|
|
1306
|
+
*
|
|
1307
|
+
* @returns {ViewportSizeState} An object containing boolean values for each viewport size breakpoint.
|
|
1308
|
+
* @example
|
|
1309
|
+
* function MyComponent() {
|
|
1310
|
+
* const viewportSize = useViewportSize();
|
|
1311
|
+
*
|
|
1312
|
+
* if (viewportSize.isLg) {
|
|
1313
|
+
* return <LargeScreenLayout />;
|
|
1314
|
+
* } else if (viewportSize.isMd) {
|
|
1315
|
+
* return <MediumScreenLayout />;
|
|
1316
|
+
* } else {
|
|
1317
|
+
* return <SmallScreenLayout />;
|
|
1318
|
+
* }
|
|
1319
|
+
* }
|
|
1320
|
+
*/
|
|
1321
|
+
const useViewportSize = () => {
|
|
1322
|
+
const [viewportSize, setViewportSize] = useState(() => defaultState);
|
|
1323
|
+
const updateViewportSize = useCallback(() => {
|
|
1324
|
+
const newViewportSize = objectEntries(themeScreenSizeAsNumber).reduce((acc, [size, minWidth]) => {
|
|
1325
|
+
const matches = window.matchMedia(`(min-width: ${minWidth}px)`).matches;
|
|
1326
|
+
return {
|
|
1327
|
+
...acc,
|
|
1328
|
+
[propsMap[size]]: matches,
|
|
1329
|
+
};
|
|
1330
|
+
}, Object.assign({}, defaultState));
|
|
1331
|
+
setViewportSize(newViewportSize);
|
|
1332
|
+
}, []);
|
|
1333
|
+
useEffect(() => {
|
|
1334
|
+
// Initial check
|
|
1335
|
+
updateViewportSize();
|
|
1336
|
+
// Set up listeners for each breakpoint
|
|
1337
|
+
const mediaQueryLists = objectEntries(themeScreenSizeAsNumber).map(([_, minWidth]) => window.matchMedia(`(min-width: ${minWidth}px)`));
|
|
1338
|
+
mediaQueryLists.forEach(mql => {
|
|
1339
|
+
mql.addEventListener("change", updateViewportSize);
|
|
1340
|
+
});
|
|
1341
|
+
// Cleanup
|
|
1342
|
+
return () => {
|
|
1343
|
+
mediaQueryLists.forEach(mql => {
|
|
1344
|
+
mql.removeEventListener("change", updateViewportSize);
|
|
1345
|
+
});
|
|
1346
|
+
};
|
|
1347
|
+
}, [updateViewportSize]);
|
|
1348
|
+
return viewportSize;
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1275
1351
|
const hasFocus = () => typeof document !== "undefined" && document.hasFocus();
|
|
1276
1352
|
/**
|
|
1277
1353
|
* Use this hook to disable functionality while the tab is hidden within the browser or to react to focus or blur events
|
|
@@ -3503,6 +3579,124 @@ const ExternalLink = ({ rel = "noreferrer", target = "_blank", href, className,
|
|
|
3503
3579
|
return (jsx("a", { className: cvaExternalLink({ className }), "data-testid": dataTestId, href: href, onClick: onClick, rel: rel, target: target, title: title, children: children }));
|
|
3504
3580
|
};
|
|
3505
3581
|
|
|
3582
|
+
const IMAGE_ENDPOINT = "https://images.iris.trackunit.com";
|
|
3583
|
+
/**
|
|
3584
|
+
* Generates an url for the original sized image from the image id
|
|
3585
|
+
*/
|
|
3586
|
+
const createOriginalUrl = (id) => `${IMAGE_ENDPOINT}/${btoa(JSON.stringify({
|
|
3587
|
+
key: id,
|
|
3588
|
+
}))}`;
|
|
3589
|
+
const createResizedUrl = (id, width, height) => {
|
|
3590
|
+
const request = {
|
|
3591
|
+
key: id,
|
|
3592
|
+
edits: {
|
|
3593
|
+
resize: {
|
|
3594
|
+
width,
|
|
3595
|
+
height,
|
|
3596
|
+
fit: "contain",
|
|
3597
|
+
background: "transparent",
|
|
3598
|
+
},
|
|
3599
|
+
},
|
|
3600
|
+
};
|
|
3601
|
+
return `${IMAGE_ENDPOINT}/${btoa(JSON.stringify(request))}`;
|
|
3602
|
+
};
|
|
3603
|
+
/**
|
|
3604
|
+
* Generates an url for the thumbnail size image
|
|
3605
|
+
*/
|
|
3606
|
+
const createThumbnailUrl = (id) => createResizedUrl(id, 100, 100);
|
|
3607
|
+
/**
|
|
3608
|
+
* Generates srcSet HTML attribute to avoid loading the full size image on small screens
|
|
3609
|
+
*/
|
|
3610
|
+
const createSrcSet = (id) => `${createResizedUrl(id, 480)} 480w, ${createResizedUrl(id, 800)} 800w`;
|
|
3611
|
+
|
|
3612
|
+
const cvaGallery = cvaMerge([
|
|
3613
|
+
// These make the gallery grow to the height of the parent container,
|
|
3614
|
+
// which avoids jumping of the thumbnail row when going through images
|
|
3615
|
+
// with different ratios
|
|
3616
|
+
"grow",
|
|
3617
|
+
"[&_.image-gallery-content]:h-[100%]",
|
|
3618
|
+
"[&_.image-gallery-content]:flex",
|
|
3619
|
+
"[&_.image-gallery-content]:flex-col",
|
|
3620
|
+
"[&_.image-gallery-slide-wrapper]:grow",
|
|
3621
|
+
"[&_.image-gallery-slide-wrapper]:flex",
|
|
3622
|
+
"[&_.image-gallery-slide-wrapper]:flex-col",
|
|
3623
|
+
"[&_.image-gallery-slide-wrapper]:justify-center",
|
|
3624
|
+
"[&_.image-gallery-slide]:flex",
|
|
3625
|
+
// This centers thumbnails and makes the clickable area independent of image size
|
|
3626
|
+
"[&_.image-gallery-thumbnail]:h-[100px]",
|
|
3627
|
+
]);
|
|
3628
|
+
/**
|
|
3629
|
+
* Show and manage images
|
|
3630
|
+
*
|
|
3631
|
+
* Will show thumbnail selection on desktop.
|
|
3632
|
+
*
|
|
3633
|
+
* Reduces bandwidth usage by lazy loading thumbnails and loading smaller versions of images depending on screen size.
|
|
3634
|
+
*/
|
|
3635
|
+
const ImageCollection = (props) => {
|
|
3636
|
+
const { imageIds, actions, emptyPlaceholderText, additionalItemClassName } = props;
|
|
3637
|
+
const [openImageId, setOpenImageId] = useState(imageIds[0]);
|
|
3638
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
3639
|
+
const { width } = useResize();
|
|
3640
|
+
const fileInputRef = useRef(null);
|
|
3641
|
+
const imageGalleryRef = useRef(null);
|
|
3642
|
+
const uploadButton = useMemo(() => (actions === null || actions === void 0 ? void 0 : actions.upload) ? (jsx("div", { className: "flex justify-end", children: jsx(Button, { onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: actions.upload.label }) })) : null, [actions === null || actions === void 0 ? void 0 : actions.upload]);
|
|
3643
|
+
const items = useMemo(() => imageIds.map(imageId => ({
|
|
3644
|
+
id: imageId,
|
|
3645
|
+
original: createOriginalUrl(imageId),
|
|
3646
|
+
thumbnail: createThumbnailUrl(imageId),
|
|
3647
|
+
srcSet: createSrcSet(imageId),
|
|
3648
|
+
})), [imageIds]);
|
|
3649
|
+
useEffect(() => {
|
|
3650
|
+
if ((!openImageId || !imageIds.includes(openImageId)) && imageIds.length > 0) {
|
|
3651
|
+
setOpenImageId(imageIds[0]);
|
|
3652
|
+
}
|
|
3653
|
+
}, [imageIds, openImageId]);
|
|
3654
|
+
return (jsxs(Fragment, { children: [imageIds.length > 0 ? (jsxs("div", { className: "flex h-[100vh] max-h-[100vh] flex-col", children: [jsx(ImageGallery, { additionalClass: cvaGallery(), items: items, onBeforeSlide: index => { var _a; return setOpenImageId((_a = items[index]) === null || _a === void 0 ? void 0 : _a.id); }, ref: imageGalleryRef, renderItem: ({ original, srcSet,
|
|
3655
|
+
// Typing of the library is not flexible enough to add id, which is only used for the unit tests
|
|
3656
|
+
// @ts-ignore
|
|
3657
|
+
id, }) => (jsx("img", { alt: "full", className: twMerge(additionalItemClassName, "w-[100%]", "object-contain"), "data-testid": `image-${id}`, loading: "lazy", sizes: "(max-width: 480px) 480px, 800px", src: original, srcSet: srcSet })), renderThumbInner: ({ thumbnail,
|
|
3658
|
+
// Typing of the library is not flexible enough to add id, which is only used for the unit tests
|
|
3659
|
+
// @ts-ignore
|
|
3660
|
+
id, }) => jsx("img", { alt: "thumbnail", "data-testid": `thumbnail-${id}`, loading: "lazy", src: thumbnail }), showFullscreenButton: false, showPlayButton: false, showThumbnails: width > 768,
|
|
3661
|
+
// reduce sliding around during deletion
|
|
3662
|
+
slideDuration: isDeleting ? 0 : 200, ...props }), uploadButton, (actions === null || actions === void 0 ? void 0 : actions.remove) ? (jsx(Button, { className: "absolute right-2 top-2", loading: isDeleting, onClick: async () => {
|
|
3663
|
+
var _a;
|
|
3664
|
+
if (!(((_a = actions.remove) === null || _a === void 0 ? void 0 : _a.onRemove) && openImageId)) {
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3667
|
+
try {
|
|
3668
|
+
let nextIndex;
|
|
3669
|
+
const indexToDelete = imageIds.indexOf(openImageId);
|
|
3670
|
+
const deletingLastImage = indexToDelete === imageIds.length - 1;
|
|
3671
|
+
if (imageIds.length === 1) {
|
|
3672
|
+
nextIndex = undefined;
|
|
3673
|
+
}
|
|
3674
|
+
else if (deletingLastImage) {
|
|
3675
|
+
nextIndex = indexToDelete - 1;
|
|
3676
|
+
}
|
|
3677
|
+
else {
|
|
3678
|
+
// we set the index after the deletion, so the index will be the same
|
|
3679
|
+
nextIndex = indexToDelete;
|
|
3680
|
+
}
|
|
3681
|
+
setIsDeleting(true);
|
|
3682
|
+
await actions.remove.onRemove(openImageId);
|
|
3683
|
+
if (nextIndex !== undefined) {
|
|
3684
|
+
imageGalleryRef.current && nextIndex && imageGalleryRef.current.slideToIndex(nextIndex);
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
finally {
|
|
3688
|
+
setIsDeleting(false);
|
|
3689
|
+
}
|
|
3690
|
+
}, prefix: jsx(Icon, { name: "Trash", size: "small" }), variant: "secondary-danger", children: actions.remove.label })) : null] })) : (jsx(EmptyState, { action: uploadButton, description: emptyPlaceholderText })), jsx("input", {
|
|
3691
|
+
// Users can still select other files on mobile devices
|
|
3692
|
+
accept: "image/jpeg, image/png, image/bmp, image/webp, image/gif", hidden: true, multiple: true, onChange: async (e) => {
|
|
3693
|
+
if (!(e.target.files && (actions === null || actions === void 0 ? void 0 : actions.upload))) {
|
|
3694
|
+
return;
|
|
3695
|
+
}
|
|
3696
|
+
await actions.upload.onUpload(e.target.files);
|
|
3697
|
+
}, ref: fileInputRef, type: "file" })] }));
|
|
3698
|
+
};
|
|
3699
|
+
|
|
3506
3700
|
/**
|
|
3507
3701
|
* The hook that powers the Popover component.
|
|
3508
3702
|
* It should not be used directly, but rather through the Popover component.
|
|
@@ -4872,4 +5066,4 @@ const cvaClickable = cvaMerge([
|
|
|
4872
5066
|
},
|
|
4873
5067
|
});
|
|
4874
5068
|
|
|
4875
|
-
export { Alert, Badge, Breadcrumb, BreadcrumbContainer, BreadcrumbItem, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, Drawer, EmptyState, EmptyValue, ExternalLink, Heading, Icon, IconButton, Indicator, KPICard, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Prompt, ROLE_CARD, SectionHeader, Sidebar, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, Timeline, TimelineElement, ToggleGroup, Tooltip, ValueBar, VirtualizedList, WidgetBody, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaIconButton, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInteractableItem, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, setLocalStorage, useClickOutside, useContinuousTimeout, useDebounce, useDevicePixelRatio, useGeometry, useHover, useIsFirstRender, useIsFullscreen, useIsTextCutOff, useLocalStorage, useLocalStorageReducer, useOptionallyElevatedState, useOverflowItems, usePopoverContext, usePrompt, useResize, useSelfUpdatingRef, useTimeout, useWindowActivity };
|
|
5069
|
+
export { Alert, Badge, Breadcrumb, BreadcrumbContainer, BreadcrumbItem, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, Drawer, EmptyState, EmptyValue, ExternalLink, Heading, IMAGE_ENDPOINT, Icon, IconButton, ImageCollection, Indicator, KPICard, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Prompt, ROLE_CARD, SectionHeader, Sidebar, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, Timeline, TimelineElement, ToggleGroup, Tooltip, ValueBar, VirtualizedList, WidgetBody, createOriginalUrl, createThumbnailUrl, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaIconButton, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInteractableItem, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, setLocalStorage, useClickOutside, useContinuousTimeout, useDebounce, useDevicePixelRatio, useGeometry, useHover, useIsFirstRender, useIsFullscreen, useIsTextCutOff, useLocalStorage, useLocalStorageReducer, useOptionallyElevatedState, useOverflowItems, usePopoverContext, usePrompt, useResize, useSelfUpdatingRef, useTimeout, useViewportSize, useWindowActivity };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-components",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"string-ts": "^2.0.0",
|
|
28
28
|
"tailwind-merge": "^2.0.0",
|
|
29
29
|
"@trackunit/react-table-pagination": "*",
|
|
30
|
-
"uuid": "^9.0.1"
|
|
30
|
+
"uuid": "^9.0.1",
|
|
31
|
+
"react-image-gallery": "1.3.0"
|
|
31
32
|
},
|
|
32
33
|
"module": "./index.esm.js",
|
|
33
34
|
"main": "./index.cjs.js",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ComponentProps } from "react";
|
|
2
|
+
import ImageGallery from "react-image-gallery";
|
|
3
|
+
type Props = Omit<ComponentProps<typeof ImageGallery>, "items"> & {
|
|
4
|
+
imageIds: string[];
|
|
5
|
+
actions?: {
|
|
6
|
+
upload?: {
|
|
7
|
+
label: string;
|
|
8
|
+
onUpload: (pictures: FileList) => Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
remove?: {
|
|
11
|
+
label: string;
|
|
12
|
+
onRemove?: (id: string) => Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
emptyPlaceholderText: string;
|
|
16
|
+
additionalItemClassName?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Show and manage images
|
|
20
|
+
*
|
|
21
|
+
* Will show thumbnail selection on desktop.
|
|
22
|
+
*
|
|
23
|
+
* Reduces bandwidth usage by lazy loading thumbnails and loading smaller versions of images depending on screen size.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ImageCollection: (props: Props) => import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const IMAGE_ENDPOINT = "https://images.iris.trackunit.com";
|
|
2
|
+
/**
|
|
3
|
+
* Generates an url for the original sized image from the image id
|
|
4
|
+
*/
|
|
5
|
+
export declare const createOriginalUrl: (id: string) => string;
|
|
6
|
+
/**
|
|
7
|
+
* Generates an url for the thumbnail size image
|
|
8
|
+
*/
|
|
9
|
+
export declare const createThumbnailUrl: (id: string) => string;
|
|
10
|
+
/**
|
|
11
|
+
* Generates srcSet HTML attribute to avoid loading the full size image on small screens
|
|
12
|
+
*/
|
|
13
|
+
export declare const createSrcSet: (id: string) => string;
|
|
@@ -12,6 +12,7 @@ export * from "./EmptyValue/EmptyValue";
|
|
|
12
12
|
export * from "./ExternalLink/ExternalLink";
|
|
13
13
|
export * from "./Heading";
|
|
14
14
|
export * from "./Icon/Icon";
|
|
15
|
+
export * from "./ImageCollection/ImageCollection";
|
|
15
16
|
export * from "./Indicator";
|
|
16
17
|
export * from "./KPICard/KPICard";
|
|
17
18
|
export * from "./Menu";
|
|
@@ -41,3 +42,7 @@ export * from "./Tooltip";
|
|
|
41
42
|
export * from "./ValueBar";
|
|
42
43
|
export * from "./VirtualizedList/VirtualizedList";
|
|
43
44
|
export * from "./Widget";
|
|
45
|
+
export * from "./buttons";
|
|
46
|
+
export { createOriginalUrl } from "./ImageCollection/helpers";
|
|
47
|
+
export { createThumbnailUrl } from "./ImageCollection/helpers";
|
|
48
|
+
export { IMAGE_ENDPOINT } from "./ImageCollection/helpers";
|
package/src/hooks/index.d.ts
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { themeScreenSizeAsNumber } from "@trackunit/ui-design-tokens";
|
|
2
|
+
type ViewportSize = keyof typeof themeScreenSizeAsNumber;
|
|
3
|
+
/**
|
|
4
|
+
* Represents the state of viewport sizes as boolean values.
|
|
5
|
+
* Each property is true if the viewport width is greater than or equal to the corresponding breakpoint.
|
|
6
|
+
*/
|
|
7
|
+
type ViewportSizeState = {
|
|
8
|
+
[K in ViewportSize as `is${Capitalize<K>}`]: boolean;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* A custom React hook that provides real-time information about the current viewport size.
|
|
12
|
+
*
|
|
13
|
+
* This hook listens to changes in the viewport size and returns an object with boolean values
|
|
14
|
+
* indicating which breakpoints are currently active. It's useful for creating responsive
|
|
15
|
+
* layouts and components that need to adapt to different screen sizes.
|
|
16
|
+
*
|
|
17
|
+
* @returns {ViewportSizeState} An object containing boolean values for each viewport size breakpoint.
|
|
18
|
+
* @example
|
|
19
|
+
* function MyComponent() {
|
|
20
|
+
* const viewportSize = useViewportSize();
|
|
21
|
+
*
|
|
22
|
+
* if (viewportSize.isLg) {
|
|
23
|
+
* return <LargeScreenLayout />;
|
|
24
|
+
* } else if (viewportSize.isMd) {
|
|
25
|
+
* return <MediumScreenLayout />;
|
|
26
|
+
* } else {
|
|
27
|
+
* return <SmallScreenLayout />;
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
*/
|
|
31
|
+
export declare const useViewportSize: () => ViewportSizeState;
|
|
32
|
+
export {};
|