@trackunit/react-date-and-time-components 0.0.203 → 0.0.205

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs.js CHANGED
@@ -5,13 +5,14 @@ var i18nLibraryTranslation = require('@trackunit/i18n-library-translation');
5
5
  var dateAndTimeUtils = require('@trackunit/date-and-time-utils');
6
6
  var reactDateAndTimeHooks = require('@trackunit/react-date-and-time-hooks');
7
7
  var uiDesignTokens = require('@trackunit/ui-design-tokens');
8
- var react = require('react');
8
+ var React = require('react');
9
9
  var reactComponents = require('@trackunit/react-components');
10
10
  var sharedUtils = require('@trackunit/shared-utils');
11
11
  var reactDayPicker = require('react-day-picker');
12
12
  var tailwindMerge = require('tailwind-merge');
13
13
  var locale = require('date-fns/locale');
14
14
  var reactCoreHooks = require('@trackunit/react-core-hooks');
15
+ var cssClassVarianceUtilities = require('@trackunit/css-class-variance-utilities');
15
16
 
16
17
  var defaultTranslations = {
17
18
  "dateTime.instant.now": "Now",
@@ -90,18 +91,18 @@ const MS_PER_HOUR = 60 * 60 * 1000;
90
91
  const DateTime = ({ value, format, className, fromNow, withTitle, titleFormat, timezone, subtle, calendar, }) => {
91
92
  const { t } = useTranslation();
92
93
  const locale = reactDateAndTimeHooks.useLocale();
93
- const nowDate = react.useMemo(() => new Date(), []);
94
+ const nowDate = React.useMemo(() => new Date(), []);
94
95
  const date = value ? dateAndTimeUtils.toDateUtil(value) : nowDate;
95
96
  const newDateTime = dateAndTimeUtils.formatDateUtil(date, format, timezone === null || timezone === void 0 ? void 0 : timezone.id, locale);
96
97
  const titleDateTime = withTitle ? dateAndTimeUtils.formatDateUtil(date, titleFormat, timezone === null || timezone === void 0 ? void 0 : timezone.id, locale) : undefined;
97
- const getTimeSince = react.useCallback((from, to) => {
98
+ const getTimeSince = React.useCallback((from, to) => {
98
99
  const same = dateAndTimeUtils.isEqualUtil(from, to);
99
100
  if (same) {
100
101
  return t("dateTime.instant.now");
101
102
  }
102
103
  return dateAndTimeUtils.timeSinceAuto(from, to, timezone === null || timezone === void 0 ? void 0 : timezone.id, locale);
103
104
  }, [locale, t, timezone === null || timezone === void 0 ? void 0 : timezone.id]);
104
- const dateValue = react.useMemo(() => {
105
+ const dateValue = React.useMemo(() => {
105
106
  return fromNow ? getTimeSince(date, nowDate) : newDateTime;
106
107
  }, [date, fromNow, getTimeSince, newDateTime, nowDate]);
107
108
  return (jsxRuntime.jsx("time", { className: className, dateTime: newDateTime, style: subtle ? { color: uiDesignTokens.color("NEUTRAL", 600, "CSS") } : undefined, title: titleDateTime, children: dateValue }));
@@ -168,15 +169,15 @@ const DayPicker = ({ onDaySelect, disabledDays, selectedDays, language, classNam
168
169
  * @returns {JSX.Element} DayRangePicker component
169
170
  */
170
171
  const DayRangePicker = ({ onRangeSelect, selectedDays, disabledDays, dataTestId, language, className, max, showQuickOptions, timezone, onClose, }) => {
171
- const [newRange, setNewRange] = react.useState(selectedDays !== null && selectedDays !== void 0 ? selectedDays : { from: undefined, to: undefined });
172
+ const [newRange, setNewRange] = React.useState(selectedDays !== null && selectedDays !== void 0 ? selectedDays : { from: undefined, to: undefined });
172
173
  const [t] = useTranslation();
173
174
  const locale = useLocale(language);
174
- react.useEffect(() => {
175
+ React.useEffect(() => {
175
176
  if (selectedDays) {
176
177
  setNewRange(selectedDays);
177
178
  }
178
179
  }, [selectedDays]);
179
- const handleOnRangeSelect = react.useCallback((range) => {
180
+ const handleOnRangeSelect = React.useCallback((range) => {
180
181
  if (range && range.from) {
181
182
  if (range.to === undefined) {
182
183
  setNewRange({ from: range.from, to: range.from });
@@ -189,7 +190,7 @@ const DayRangePicker = ({ onRangeSelect, selectedDays, disabledDays, dataTestId,
189
190
  setNewRange({ from: undefined, to: undefined });
190
191
  }
191
192
  }, []);
192
- const handleCancel = react.useCallback(() => {
193
+ const handleCancel = React.useCallback(() => {
193
194
  if (selectedDays) {
194
195
  setNewRange(selectedDays);
195
196
  }
@@ -198,7 +199,7 @@ const DayRangePicker = ({ onRangeSelect, selectedDays, disabledDays, dataTestId,
198
199
  const clearSelectedDays = () => {
199
200
  setNewRange({ from: undefined, to: undefined });
200
201
  };
201
- const handleApply = react.useCallback(() => {
202
+ const handleApply = React.useCallback(() => {
202
203
  onRangeSelect && onRangeSelect(newRange);
203
204
  onClose && onClose();
204
205
  }, [onRangeSelect, newRange, onClose]);
@@ -221,7 +222,7 @@ const calcQuickOptions = (numberOfDays) => {
221
222
  const QuickOptionButton = ({ option, onClick, timezone }) => {
222
223
  const [t] = useTranslation();
223
224
  const { nowDate, startOf, endOf, subtract } = reactDateAndTimeHooks.useDateAndTime();
224
- const handleClick = react.useCallback(() => {
225
+ const handleClick = React.useCallback(() => {
225
226
  const from = subtract(startOf(nowDate, "day"), option.includeExtraDay ? option.last : option.last - 1, option.unit);
226
227
  onClick({
227
228
  from,
@@ -281,7 +282,7 @@ const QuickOptionButton = ({ option, onClick, timezone }) => {
281
282
  const DayRangePickerPopover = ({ interval = { from: undefined, to: undefined }, showQuickOptions, disabledDays, className, dataTestId, onRangeSelect, variant = "ghost-neutral", size = "small", placement, timezone, fullwidth, bindTo, buttonContent, max, disabled, }) => {
282
283
  const { language } = reactCoreHooks.useCurrentUserLanguage();
283
284
  const [t] = useTranslation();
284
- const handleOnRangeSelect = react.useCallback((range) => {
285
+ const handleOnRangeSelect = React.useCallback((range) => {
285
286
  if (range) {
286
287
  onRangeSelect(range);
287
288
  }
@@ -289,6 +290,189 @@ const DayRangePickerPopover = ({ interval = { from: undefined, to: undefined },
289
290
  return (jsxRuntime.jsxs(reactComponents.Popover, { placement: placement, children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx(reactComponents.Button, { className: className, dataTestId: dataTestId !== null && dataTestId !== void 0 ? dataTestId : "show-date-range", disabled: disabled, fullWidth: fullwidth, size: size, suffix: jsxRuntime.jsx(reactComponents.Icon, { ariaLabel: t("dayRangePickerPopover.icon.tooltip.calendar"), name: "Calendar", size: "small" }), variant: variant, children: buttonContent }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { children: close => (jsxRuntime.jsx(reactComponents.Card, { className: "min-w-min p-4", dataTestId: "range-popover-list", children: jsxRuntime.jsx(DayRangePicker, { disabledDays: disabledDays, language: language || "en", max: max, onClose: close, onRangeSelect: handleOnRangeSelect, selectedDays: interval, showQuickOptions: showQuickOptions, timezone: timezone }) })) })] }));
290
291
  };
291
292
 
293
+ const cvaTimelineElement = cssClassVarianceUtilities.cvaMerge([
294
+ "flex",
295
+ "group/timeline",
296
+ ], {
297
+ variants: {
298
+ selected: {
299
+ true: "bg-blue-50 rounded-[4px]",
300
+ false: "",
301
+ },
302
+ hoverBehavior: {
303
+ true: "hover:bg-gray-50 hover:rounded-[4px]",
304
+ false: "",
305
+ },
306
+ },
307
+ });
308
+ const cvaDotWrapper = cssClassVarianceUtilities.cvaMerge([
309
+ "relative",
310
+ "flex",
311
+ "flex-col",
312
+ "items-center",
313
+ "justify-items-center",
314
+ "ml-2",
315
+ "mr-2",
316
+ "pt-2",
317
+ "min-w-6",
318
+ ]);
319
+ const cvaLine = cssClassVarianceUtilities.cvaMerge([
320
+ "absolute",
321
+ "top-0",
322
+ "w-px",
323
+ "h-full",
324
+ "border-l",
325
+ "border-slate-300",
326
+ "group-first/timeline:top-3.5",
327
+ "group-last/timeline:h-4",
328
+ ], {
329
+ variants: {
330
+ lineStyle: {
331
+ dashed: "border-dashed",
332
+ solid: ""
333
+ },
334
+ },
335
+ });
336
+ const cvaCustomDot = cssClassVarianceUtilities.cvaMerge([
337
+ "flex",
338
+ "items-center",
339
+ "justify-center",
340
+ "w-5",
341
+ "h-5",
342
+ "bg-white",
343
+ "rounded-full",
344
+ "border",
345
+ "border-white",
346
+ "z-[2]"
347
+ ]);
348
+ const cvaToggleBtnIcon = cssClassVarianceUtilities.cvaMerge([
349
+ "mr-1",
350
+ "rounded-lg",
351
+ "transition-transform",
352
+ "duration-300",
353
+ "cursor-pointer",
354
+ ], {
355
+ variants: {
356
+ rotated: {
357
+ true: "rotate-180",
358
+ false: "",
359
+ },
360
+ },
361
+ });
362
+ const cvaTimelineElementTime = cssClassVarianceUtilities.cvaMerge([
363
+ "min-h-6",
364
+ "content-center",
365
+ ], {
366
+ variants: {
367
+ width: {
368
+ large: "min-w-16",
369
+ small: "min-w-10",
370
+ },
371
+ },
372
+ });
373
+
374
+ /**
375
+ * The Timeline component offers a visual representation of events or milestones in chronological order, helping users easily follow the progression of activities, tasks, or data points over time.
376
+ *
377
+ * @param {TimelineProps} props - The props for the Timeline component
378
+ * @returns {JSX.Element} Timeline component
379
+ */
380
+ const Timeline = ({ className, dataTestId, children, dateHeader, customHeader, toggleButton, }) => {
381
+ var _a;
382
+ const ref = React.useRef(null);
383
+ const [isOpen, setIsOpen] = React.useState(false);
384
+ const { formatDate, formatRange } = reactDateAndTimeHooks.useDateAndTime();
385
+ const locale = reactDateAndTimeHooks.useLocale();
386
+ const hourCycle = dateAndTimeUtils.getHourCycle(locale);
387
+ const childrenArray = React.useMemo(() => React.Children.toArray(children), [children]);
388
+ const [firstChild, ...remainingChildren] = childrenArray;
389
+ const childProps = React.isValidElement(firstChild) ? firstChild.props : {};
390
+ const lastChild = remainingChildren.pop();
391
+ const middleChildren = remainingChildren;
392
+ const renderToggleButton = () => (jsxRuntime.jsxs("div", { className: cvaTimelineElement(), children: [(childProps === null || childProps === void 0 ? void 0 : childProps.date) ? (jsxRuntime.jsx("div", { className: cvaTimelineElementTime({
393
+ width: hourCycle === "h12" || hourCycle === "h11" ? "large" : "small",
394
+ }) })) : null, jsxRuntime.jsx("div", { className: cvaDotWrapper(), children: jsxRuntime.jsx("div", { className: cvaLine({ lineStyle: childProps === null || childProps === void 0 ? void 0 : childProps.lineStyle }) }) }), jsxRuntime.jsxs("div", { className: "flex text-sm text-secondary-500 cursor-pointer items-center min-h-8 hover:text-secondary-600", "data-testid": "timeline-toggle-btn", onClick: () => setIsOpen(!isOpen), children: [jsxRuntime.jsx(reactComponents.Icon, { className: cvaToggleBtnIcon({ rotated: isOpen }), name: "ChevronDown", size: "small" }), jsxRuntime.jsx("span", { children: isOpen ? toggleButton === null || toggleButton === void 0 ? void 0 : toggleButton.collapsedLabel : toggleButton === null || toggleButton === void 0 ? void 0 : toggleButton.expandedLabel })] })] }));
395
+ const renderDateHeader = () => {
396
+ if (!dateHeader) {
397
+ return null;
398
+ }
399
+ if (customHeader) {
400
+ return null;
401
+ }
402
+ const { date, dateRange, showTime } = dateHeader;
403
+ return (jsxRuntime.jsx("div", { className: "pb-4", "data-testid": "timeline-date-header", children: date ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(reactComponents.Text, { size: "medium", type: "span", weight: "thick", children: formatDate(date, { selectFormat: !showTime ? "dateOnly" : undefined }) }), jsxRuntime.jsx(reactComponents.Text, { className: "text-secondary-500", size: "medium", type: "span", weight: "thick", children: ` (${dateAndTimeUtils.timeSinceAuto(date, new Date())})` })] })) : (jsxRuntime.jsx(reactComponents.Text, { className: "pr-1", size: "medium", type: "span", weight: "thick", children: formatRange(dateRange, { dateStyle: !showTime ? "medium" : undefined }) })) }));
404
+ };
405
+ return (jsxRuntime.jsxs("div", { className: className, "data-testid": dataTestId, children: [renderDateHeader(), customHeader ? jsxRuntime.jsx("div", { className: "pb-4 font-semibold text-sm", children: customHeader }) : null, toggleButton && middleChildren.length > 0 ? (jsxRuntime.jsxs("div", { children: [firstChild, middleChildren.length > 0 ? renderToggleButton() : null, jsxRuntime.jsxs("div", { "aria-hidden": !isOpen, className: "transition-all duration-300 overflow-hidden", ref: ref, style: { height: isOpen ? `${(_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollHeight}px` : "0px" }, children: [jsxRuntime.jsx("div", {}), middleChildren, jsxRuntime.jsx("div", {})] }), lastChild] })) : (jsxRuntime.jsx("div", { children: children }))] }));
406
+ };
407
+
408
+ /**
409
+ * TimelineElement - entry on a timeline - rendered with an icon on the timeline
410
+ * - a date as the header and the children provided as body.
411
+ *
412
+ * @param {TimeLineElementProps} props the props
413
+ * @returns {JSX.Element} component
414
+ */
415
+ const TimelineElement = ({ date, children, className, dataTestId = "timeline-element", header, onClick, actionButton, selected, customDot, lineStyle = "solid", hoverBehavior = false, }) => {
416
+ const [isHovered, setIsHovered] = React.useState(false);
417
+ const { formatDate } = reactDateAndTimeHooks.useDateAndTime();
418
+ const locale = reactDateAndTimeHooks.useLocale();
419
+ const formattedDate = date
420
+ ? formatDate(date, { selectFormat: "timeOnly", timeFormat: "short" })
421
+ : null;
422
+ const handleMouseEnter = () => {
423
+ if (hoverBehavior) {
424
+ setIsHovered(true);
425
+ }
426
+ };
427
+ const handleMouseLeave = () => {
428
+ if (hoverBehavior) {
429
+ setIsHovered(false);
430
+ }
431
+ };
432
+ return (jsxRuntime.jsxs("div", { className: cvaTimelineElement({ className, selected, hoverBehavior }), "data-date": date, "data-testid": dataTestId, onClick: onClick, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [renderDate(formattedDate, locale), jsxRuntime.jsxs("div", { className: cvaDotWrapper(), children: [renderDot(customDot, isHovered, selected), jsxRuntime.jsx("div", { className: cvaLine({ lineStyle }) })] }), jsxRuntime.jsxs("div", { className: "pt-2 pb-2", children: [renderHeader(header), renderChildren(children), renderActionButton(actionButton)] })] }));
433
+ };
434
+ const renderDate = (formattedDate, locale) => {
435
+ if (!formattedDate) {
436
+ return null;
437
+ }
438
+ return (jsxRuntime.jsx("div", { className: "pt-2 items-center", children: timelineElementTime(formattedDate, locale) }));
439
+ };
440
+ const renderDot = (customDot, isHovered, selected) => {
441
+ return (customDot === null || customDot === void 0 ? void 0 : customDot.name)
442
+ ? customDotElement(customDot.name, isHovered, selected, customDot.color)
443
+ : defaultDot(isHovered, selected);
444
+ };
445
+ const renderHeader = (header) => {
446
+ if (!header) {
447
+ return null;
448
+ }
449
+ return jsxRuntime.jsx("div", { className: "text-sm mt-0.5 content-center font-medium", children: header });
450
+ };
451
+ const renderChildren = (children) => {
452
+ if (!children) {
453
+ return null;
454
+ }
455
+ return jsxRuntime.jsx("div", { className: "text-secondary-500 mt-0.5 content-center text-sm", children: children });
456
+ };
457
+ const renderActionButton = (actionButton) => {
458
+ if (!actionButton) {
459
+ return null;
460
+ }
461
+ return (jsxRuntime.jsx("div", { className: "pt-2", children: jsxRuntime.jsx(reactComponents.Button, { prefix: jsxRuntime.jsx(reactComponents.Icon, { name: actionButton.iconName, size: "small" }), size: "small", variant: "secondary", children: actionButton.label }) }));
462
+ };
463
+ const timelineElementTime = (formattedDate, locale) => {
464
+ const hourCycle = dateAndTimeUtils.getHourCycle(locale);
465
+ return (jsxRuntime.jsx(reactComponents.Text, { align: "right", className: cvaTimelineElementTime({
466
+ width: hourCycle === "h12" || hourCycle === "h11" ? "large" : "small"
467
+ }), size: "small", subtle: true, children: formattedDate }));
468
+ };
469
+ const CircleIcon = ({ selected, hovered }) => (jsxRuntime.jsxs("svg", { className: "relative z-[2]", "data-testid": "timeline-circle-icon", height: "24", viewBox: "0 0 24 24", width: "24", children: [jsxRuntime.jsx("circle", { cx: "12", cy: "12", fill: selected ? "#EFF6FF" : hovered ? "#F9FAFB" : "white", r: "10" }), jsxRuntime.jsx("circle", { cx: "12", cy: "12", fill: "#2563EB", r: "8" }), jsxRuntime.jsx("circle", { cx: "12", cy: "12", fill: "white", r: "3" })] }));
470
+ const DotIcon = () => (jsxRuntime.jsxs("svg", { className: "relative z-[2]", "data-testid": "timeline-dot-icon", height: "24", viewBox: "0 0 24 24", width: "24", children: [jsxRuntime.jsx("circle", { cx: "12", cy: "12", fill: "white", r: "6" }), jsxRuntime.jsx("circle", { cx: "12", cy: "12", fill: "#2563EB", r: "4" })] }));
471
+ const defaultDot = (hovered, selected) => (hovered || selected) ? jsxRuntime.jsx(CircleIcon, { hovered: hovered, selected: selected }) : jsxRuntime.jsx(DotIcon, {});
472
+ const customDotElement = (name, hovered, selected, color) => (hovered || selected
473
+ ? jsxRuntime.jsx(CircleIcon, { hovered: hovered, selected: selected })
474
+ : (jsxRuntime.jsx("div", { className: "w-6 h-6 flex items-center justify-center", children: jsxRuntime.jsx("div", { className: cvaCustomDot(), children: jsxRuntime.jsx(reactComponents.Icon, { className: "block", color: color, dataTestId: "timeline-custom-dot", name: name, size: "small" }) }) })));
475
+
292
476
  /*
293
477
  * ----------------------------
294
478
  * | SETUP TRANSLATIONS START |
@@ -304,3 +488,5 @@ exports.DayPicker = DayPicker;
304
488
  exports.DayRangePicker = DayRangePicker;
305
489
  exports.DayRangePickerPopover = DayRangePickerPopover;
306
490
  exports.MS_PER_HOUR = MS_PER_HOUR;
491
+ exports.Timeline = Timeline;
492
+ exports.TimelineElement = TimelineElement;
package/index.esm.js CHANGED
@@ -1,15 +1,16 @@
1
- import { jsx, jsxs } from 'react/jsx-runtime';
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import { registerTranslations, useNamespaceTranslation } from '@trackunit/i18n-library-translation';
3
- import { toDateUtil, formatDateUtil, isEqualUtil, timeSinceAuto } from '@trackunit/date-and-time-utils';
3
+ import { toDateUtil, formatDateUtil, isEqualUtil, timeSinceAuto, getHourCycle } from '@trackunit/date-and-time-utils';
4
4
  import { useLocale as useLocale$1, useDateAndTime } from '@trackunit/react-date-and-time-hooks';
5
5
  import { color } from '@trackunit/ui-design-tokens';
6
- import { useMemo, useCallback, useState, useEffect } from 'react';
7
- import { Tooltip, IconButton, Icon, Button, Popover, PopoverTrigger, PopoverContent, Card } from '@trackunit/react-components';
6
+ import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react';
7
+ import { Tooltip, IconButton, Icon, Button, Popover, PopoverTrigger, PopoverContent, Card, Text } from '@trackunit/react-components';
8
8
  import { DateTimeFormat } from '@trackunit/shared-utils';
9
9
  import { DayPicker as DayPicker$1 } from 'react-day-picker';
10
10
  import { twMerge } from 'tailwind-merge';
11
11
  import { enGB, enUS, da, de, cs, nl, fr, fi, hu, it, nb, pl, pt, ru, ro, es, sv, ja, th } from 'date-fns/locale';
12
12
  import { useCurrentUserLanguage } from '@trackunit/react-core-hooks';
13
+ import { cvaMerge } from '@trackunit/css-class-variance-utilities';
13
14
 
14
15
  var defaultTranslations = {
15
16
  "dateTime.instant.now": "Now",
@@ -287,6 +288,189 @@ const DayRangePickerPopover = ({ interval = { from: undefined, to: undefined },
287
288
  return (jsxs(Popover, { placement: placement, children: [jsx(PopoverTrigger, { children: jsx(Button, { className: className, dataTestId: dataTestId !== null && dataTestId !== void 0 ? dataTestId : "show-date-range", disabled: disabled, fullWidth: fullwidth, size: size, suffix: jsx(Icon, { ariaLabel: t("dayRangePickerPopover.icon.tooltip.calendar"), name: "Calendar", size: "small" }), variant: variant, children: buttonContent }) }), jsx(PopoverContent, { children: close => (jsx(Card, { className: "min-w-min p-4", dataTestId: "range-popover-list", children: jsx(DayRangePicker, { disabledDays: disabledDays, language: language || "en", max: max, onClose: close, onRangeSelect: handleOnRangeSelect, selectedDays: interval, showQuickOptions: showQuickOptions, timezone: timezone }) })) })] }));
288
289
  };
289
290
 
291
+ const cvaTimelineElement = cvaMerge([
292
+ "flex",
293
+ "group/timeline",
294
+ ], {
295
+ variants: {
296
+ selected: {
297
+ true: "bg-blue-50 rounded-[4px]",
298
+ false: "",
299
+ },
300
+ hoverBehavior: {
301
+ true: "hover:bg-gray-50 hover:rounded-[4px]",
302
+ false: "",
303
+ },
304
+ },
305
+ });
306
+ const cvaDotWrapper = cvaMerge([
307
+ "relative",
308
+ "flex",
309
+ "flex-col",
310
+ "items-center",
311
+ "justify-items-center",
312
+ "ml-2",
313
+ "mr-2",
314
+ "pt-2",
315
+ "min-w-6",
316
+ ]);
317
+ const cvaLine = cvaMerge([
318
+ "absolute",
319
+ "top-0",
320
+ "w-px",
321
+ "h-full",
322
+ "border-l",
323
+ "border-slate-300",
324
+ "group-first/timeline:top-3.5",
325
+ "group-last/timeline:h-4",
326
+ ], {
327
+ variants: {
328
+ lineStyle: {
329
+ dashed: "border-dashed",
330
+ solid: ""
331
+ },
332
+ },
333
+ });
334
+ const cvaCustomDot = cvaMerge([
335
+ "flex",
336
+ "items-center",
337
+ "justify-center",
338
+ "w-5",
339
+ "h-5",
340
+ "bg-white",
341
+ "rounded-full",
342
+ "border",
343
+ "border-white",
344
+ "z-[2]"
345
+ ]);
346
+ const cvaToggleBtnIcon = cvaMerge([
347
+ "mr-1",
348
+ "rounded-lg",
349
+ "transition-transform",
350
+ "duration-300",
351
+ "cursor-pointer",
352
+ ], {
353
+ variants: {
354
+ rotated: {
355
+ true: "rotate-180",
356
+ false: "",
357
+ },
358
+ },
359
+ });
360
+ const cvaTimelineElementTime = cvaMerge([
361
+ "min-h-6",
362
+ "content-center",
363
+ ], {
364
+ variants: {
365
+ width: {
366
+ large: "min-w-16",
367
+ small: "min-w-10",
368
+ },
369
+ },
370
+ });
371
+
372
+ /**
373
+ * The Timeline component offers a visual representation of events or milestones in chronological order, helping users easily follow the progression of activities, tasks, or data points over time.
374
+ *
375
+ * @param {TimelineProps} props - The props for the Timeline component
376
+ * @returns {JSX.Element} Timeline component
377
+ */
378
+ const Timeline = ({ className, dataTestId, children, dateHeader, customHeader, toggleButton, }) => {
379
+ var _a;
380
+ const ref = useRef(null);
381
+ const [isOpen, setIsOpen] = useState(false);
382
+ const { formatDate, formatRange } = useDateAndTime();
383
+ const locale = useLocale$1();
384
+ const hourCycle = getHourCycle(locale);
385
+ const childrenArray = useMemo(() => React.Children.toArray(children), [children]);
386
+ const [firstChild, ...remainingChildren] = childrenArray;
387
+ const childProps = React.isValidElement(firstChild) ? firstChild.props : {};
388
+ const lastChild = remainingChildren.pop();
389
+ const middleChildren = remainingChildren;
390
+ const renderToggleButton = () => (jsxs("div", { className: cvaTimelineElement(), children: [(childProps === null || childProps === void 0 ? void 0 : childProps.date) ? (jsx("div", { className: cvaTimelineElementTime({
391
+ width: hourCycle === "h12" || hourCycle === "h11" ? "large" : "small",
392
+ }) })) : null, jsx("div", { className: cvaDotWrapper(), children: jsx("div", { className: cvaLine({ lineStyle: childProps === null || childProps === void 0 ? void 0 : childProps.lineStyle }) }) }), jsxs("div", { className: "flex text-sm text-secondary-500 cursor-pointer items-center min-h-8 hover:text-secondary-600", "data-testid": "timeline-toggle-btn", onClick: () => setIsOpen(!isOpen), children: [jsx(Icon, { className: cvaToggleBtnIcon({ rotated: isOpen }), name: "ChevronDown", size: "small" }), jsx("span", { children: isOpen ? toggleButton === null || toggleButton === void 0 ? void 0 : toggleButton.collapsedLabel : toggleButton === null || toggleButton === void 0 ? void 0 : toggleButton.expandedLabel })] })] }));
393
+ const renderDateHeader = () => {
394
+ if (!dateHeader) {
395
+ return null;
396
+ }
397
+ if (customHeader) {
398
+ return null;
399
+ }
400
+ const { date, dateRange, showTime } = dateHeader;
401
+ return (jsx("div", { className: "pb-4", "data-testid": "timeline-date-header", children: date ? (jsxs(Fragment, { children: [jsx(Text, { size: "medium", type: "span", weight: "thick", children: formatDate(date, { selectFormat: !showTime ? "dateOnly" : undefined }) }), jsx(Text, { className: "text-secondary-500", size: "medium", type: "span", weight: "thick", children: ` (${timeSinceAuto(date, new Date())})` })] })) : (jsx(Text, { className: "pr-1", size: "medium", type: "span", weight: "thick", children: formatRange(dateRange, { dateStyle: !showTime ? "medium" : undefined }) })) }));
402
+ };
403
+ return (jsxs("div", { className: className, "data-testid": dataTestId, children: [renderDateHeader(), customHeader ? jsx("div", { className: "pb-4 font-semibold text-sm", children: customHeader }) : null, toggleButton && middleChildren.length > 0 ? (jsxs("div", { children: [firstChild, middleChildren.length > 0 ? renderToggleButton() : null, jsxs("div", { "aria-hidden": !isOpen, className: "transition-all duration-300 overflow-hidden", ref: ref, style: { height: isOpen ? `${(_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollHeight}px` : "0px" }, children: [jsx("div", {}), middleChildren, jsx("div", {})] }), lastChild] })) : (jsx("div", { children: children }))] }));
404
+ };
405
+
406
+ /**
407
+ * TimelineElement - entry on a timeline - rendered with an icon on the timeline
408
+ * - a date as the header and the children provided as body.
409
+ *
410
+ * @param {TimeLineElementProps} props the props
411
+ * @returns {JSX.Element} component
412
+ */
413
+ const TimelineElement = ({ date, children, className, dataTestId = "timeline-element", header, onClick, actionButton, selected, customDot, lineStyle = "solid", hoverBehavior = false, }) => {
414
+ const [isHovered, setIsHovered] = useState(false);
415
+ const { formatDate } = useDateAndTime();
416
+ const locale = useLocale$1();
417
+ const formattedDate = date
418
+ ? formatDate(date, { selectFormat: "timeOnly", timeFormat: "short" })
419
+ : null;
420
+ const handleMouseEnter = () => {
421
+ if (hoverBehavior) {
422
+ setIsHovered(true);
423
+ }
424
+ };
425
+ const handleMouseLeave = () => {
426
+ if (hoverBehavior) {
427
+ setIsHovered(false);
428
+ }
429
+ };
430
+ return (jsxs("div", { className: cvaTimelineElement({ className, selected, hoverBehavior }), "data-date": date, "data-testid": dataTestId, onClick: onClick, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [renderDate(formattedDate, locale), jsxs("div", { className: cvaDotWrapper(), children: [renderDot(customDot, isHovered, selected), jsx("div", { className: cvaLine({ lineStyle }) })] }), jsxs("div", { className: "pt-2 pb-2", children: [renderHeader(header), renderChildren(children), renderActionButton(actionButton)] })] }));
431
+ };
432
+ const renderDate = (formattedDate, locale) => {
433
+ if (!formattedDate) {
434
+ return null;
435
+ }
436
+ return (jsx("div", { className: "pt-2 items-center", children: timelineElementTime(formattedDate, locale) }));
437
+ };
438
+ const renderDot = (customDot, isHovered, selected) => {
439
+ return (customDot === null || customDot === void 0 ? void 0 : customDot.name)
440
+ ? customDotElement(customDot.name, isHovered, selected, customDot.color)
441
+ : defaultDot(isHovered, selected);
442
+ };
443
+ const renderHeader = (header) => {
444
+ if (!header) {
445
+ return null;
446
+ }
447
+ return jsx("div", { className: "text-sm mt-0.5 content-center font-medium", children: header });
448
+ };
449
+ const renderChildren = (children) => {
450
+ if (!children) {
451
+ return null;
452
+ }
453
+ return jsx("div", { className: "text-secondary-500 mt-0.5 content-center text-sm", children: children });
454
+ };
455
+ const renderActionButton = (actionButton) => {
456
+ if (!actionButton) {
457
+ return null;
458
+ }
459
+ return (jsx("div", { className: "pt-2", children: jsx(Button, { prefix: jsx(Icon, { name: actionButton.iconName, size: "small" }), size: "small", variant: "secondary", children: actionButton.label }) }));
460
+ };
461
+ const timelineElementTime = (formattedDate, locale) => {
462
+ const hourCycle = getHourCycle(locale);
463
+ return (jsx(Text, { align: "right", className: cvaTimelineElementTime({
464
+ width: hourCycle === "h12" || hourCycle === "h11" ? "large" : "small"
465
+ }), size: "small", subtle: true, children: formattedDate }));
466
+ };
467
+ const CircleIcon = ({ selected, hovered }) => (jsxs("svg", { className: "relative z-[2]", "data-testid": "timeline-circle-icon", height: "24", viewBox: "0 0 24 24", width: "24", children: [jsx("circle", { cx: "12", cy: "12", fill: selected ? "#EFF6FF" : hovered ? "#F9FAFB" : "white", r: "10" }), jsx("circle", { cx: "12", cy: "12", fill: "#2563EB", r: "8" }), jsx("circle", { cx: "12", cy: "12", fill: "white", r: "3" })] }));
468
+ const DotIcon = () => (jsxs("svg", { className: "relative z-[2]", "data-testid": "timeline-dot-icon", height: "24", viewBox: "0 0 24 24", width: "24", children: [jsx("circle", { cx: "12", cy: "12", fill: "white", r: "6" }), jsx("circle", { cx: "12", cy: "12", fill: "#2563EB", r: "4" })] }));
469
+ const defaultDot = (hovered, selected) => (hovered || selected) ? jsx(CircleIcon, { hovered: hovered, selected: selected }) : jsx(DotIcon, {});
470
+ const customDotElement = (name, hovered, selected, color) => (hovered || selected
471
+ ? jsx(CircleIcon, { hovered: hovered, selected: selected })
472
+ : (jsx("div", { className: "w-6 h-6 flex items-center justify-center", children: jsx("div", { className: cvaCustomDot(), children: jsx(Icon, { className: "block", color: color, dataTestId: "timeline-custom-dot", name: name, size: "small" }) }) })));
473
+
290
474
  /*
291
475
  * ----------------------------
292
476
  * | SETUP TRANSLATIONS START |
@@ -296,4 +480,4 @@ const DayRangePickerPopover = ({ interval = { from: undefined, to: undefined },
296
480
  */
297
481
  setupLibraryTranslations();
298
482
 
299
- export { DateTime, DateTimeHumanized, DayPicker, DayRangePicker, DayRangePickerPopover, MS_PER_HOUR };
483
+ export { DateTime, DateTimeHumanized, DayPicker, DayRangePicker, DayRangePickerPopover, MS_PER_HOUR, Timeline, TimelineElement };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-date-and-time-components",
3
- "version": "0.0.203",
3
+ "version": "0.0.205",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -17,7 +17,9 @@
17
17
  "@trackunit/date-and-time-utils": "*",
18
18
  "@trackunit/ui-design-tokens": "*",
19
19
  "@trackunit/shared-utils": "*",
20
- "@trackunit/i18n-library-translation": "*"
20
+ "@trackunit/i18n-library-translation": "*",
21
+ "@trackunit/ui-icons": "*",
22
+ "@trackunit/css-class-variance-utilities": "*"
21
23
  },
22
24
  "module": "./index.esm.js",
23
25
  "main": "./index.cjs.js",
@@ -0,0 +1,27 @@
1
+ import { DateRange } from "@trackunit/date-and-time-utils";
2
+ import { CommonProps } from "@trackunit/react-components";
3
+ import React from "react";
4
+ export interface TimelineProps extends CommonProps {
5
+ children: React.ReactNode;
6
+ toggleButton?: {
7
+ expandedLabel: string;
8
+ collapsedLabel: string;
9
+ };
10
+ dateHeader?: ({
11
+ date: Date;
12
+ dateRange?: never;
13
+ } | {
14
+ date?: never;
15
+ dateRange: DateRange;
16
+ }) & {
17
+ showTime?: boolean;
18
+ };
19
+ customHeader?: React.ReactNode;
20
+ }
21
+ /**
22
+ * The Timeline component offers a visual representation of events or milestones in chronological order, helping users easily follow the progression of activities, tasks, or data points over time.
23
+ *
24
+ * @param {TimelineProps} props - The props for the Timeline component
25
+ * @returns {JSX.Element} Timeline component
26
+ */
27
+ export declare const Timeline: ({ className, dataTestId, children, dateHeader, customHeader, toggleButton, }: TimelineProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { Meta, StoryObj } from "@storybook/react";
2
+ import { Timeline } from "./Timeline";
3
+ type Story = StoryObj<typeof Timeline>;
4
+ declare const meta: Meta<typeof Timeline>;
5
+ export default meta;
6
+ export declare const packageName: () => import("react/jsx-runtime").JSX.Element;
7
+ export declare const Default: Story;
8
+ export declare const Usage: () => import("react/jsx-runtime").JSX.Element;
9
+ export declare const CustomHeader: () => import("react/jsx-runtime").JSX.Element;
10
+ export declare const CustomTimestamp: () => import("react/jsx-runtime").JSX.Element;
11
+ export declare const CustomDot: () => import("react/jsx-runtime").JSX.Element;
12
+ export declare const IncorrectUse: () => import("react/jsx-runtime").JSX.Element;
13
+ export declare const HoverAndSelectBehavior: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,15 @@
1
+ export declare const cvaTimelineElement: (props?: ({
2
+ selected?: boolean | null | undefined;
3
+ hoverBehavior?: boolean | null | undefined;
4
+ } & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
5
+ export declare const cvaDotWrapper: (props?: import("class-variance-authority/dist/types").ClassProp | undefined) => string;
6
+ export declare const cvaLine: (props?: ({
7
+ lineStyle?: "dashed" | "solid" | null | undefined;
8
+ } & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
9
+ export declare const cvaCustomDot: (props?: import("class-variance-authority/dist/types").ClassProp | undefined) => string;
10
+ export declare const cvaToggleBtnIcon: (props?: ({
11
+ rotated?: boolean | null | undefined;
12
+ } & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
13
+ export declare const cvaTimelineElementTime: (props?: ({
14
+ width?: "large" | "small" | null | undefined;
15
+ } & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import { CommonProps, IconColors } from "@trackunit/react-components";
3
+ import { IconName } from "@trackunit/ui-icons";
4
+ interface TimeLineElementProps extends CommonProps {
5
+ date?: Date;
6
+ header?: React.ReactNode;
7
+ children?: React.ReactNode;
8
+ customDot?: {
9
+ name: IconName;
10
+ color: IconColors;
11
+ };
12
+ onClick?: () => void;
13
+ actionButton?: {
14
+ label: string;
15
+ iconName: IconName;
16
+ };
17
+ selected?: boolean;
18
+ lineStyle?: "solid" | "dashed";
19
+ hoverBehavior?: boolean;
20
+ }
21
+ /**
22
+ * TimelineElement - entry on a timeline - rendered with an icon on the timeline
23
+ * - a date as the header and the children provided as body.
24
+ *
25
+ * @param {TimeLineElementProps} props the props
26
+ * @returns {JSX.Element} component
27
+ */
28
+ export declare const TimelineElement: ({ date, children, className, dataTestId, header, onClick, actionButton, selected, customDot, lineStyle, hoverBehavior, }: TimeLineElementProps) => import("react/jsx-runtime").JSX.Element;
29
+ export {};
@@ -0,0 +1,2 @@
1
+ export * from './Timeline';
2
+ export * from './TimelineElement';
package/src/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./dateTime";
2
2
  export * from "./DayPicker";
3
+ export * from "./Timeline";