diff --git a/packages/lsd-react/package.json b/packages/lsd-react/package.json index b6d2a70..5c943ec 100644 --- a/packages/lsd-react/package.json +++ b/packages/lsd-react/package.json @@ -14,6 +14,7 @@ "prepublish": "yarn build" }, "dependencies": { + "@datepicker-react/hooks": "^2.8.4", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "clsx": "^1.2.1", diff --git a/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx b/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx index 8043bfc..1b4c35d 100644 --- a/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx +++ b/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx @@ -5,6 +5,7 @@ import { BadgeStyles } from '../Badge/Badge.styles' import { BreadcrumbStyles } from '../Breadcrumb/Breadcrumb.styles' import { BreadcrumbItemStyles } from '../BreadcrumbItem/BreadcrumbItem.styles' import { ButtonStyles } from '../Button/Button.styles' +import { CalendarStyles } from '../Calendar/Calendar.styles' import { CardStyles } from '../Card/Card.styles' import { CardBodyStyles } from '../CardBody/CardBody.styles' import { CardHeaderStyles } from '../CardHeader/CardHeader.styles' @@ -12,6 +13,8 @@ import { CheckboxStyles } from '../Checkbox/Checkbox.styles' import { CheckboxGroupStyles } from '../CheckboxGroup/CheckboxGroup.styles' import { CollapseStyles } from '../Collapse/Collapse.styles' import { CollapseHeaderStyles } from '../CollapseHeader/CollapseHeader.styles' +import { DateFieldStyles } from '../DateField/DateField.styles' +import { DatePickerStyles } from '../DatePicker/DatePicker.styles' import { DropdownStyles } from '../Dropdown/Dropdown.styles' import { DropdownItemStyles } from '../DropdownItem/DropdownItem.styles' import { IconButtonStyles } from '../IconButton/IconButton.styles' diff --git a/packages/lsd-react/src/components/Calendar/Calendar.classes.ts b/packages/lsd-react/src/components/Calendar/Calendar.classes.ts new file mode 100644 index 0000000..026cf75 --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/Calendar.classes.ts @@ -0,0 +1,14 @@ +export const calendarClasses = { + root: `lsd-calendar`, + container: 'lsd-calendar-container', + + open: 'lsd-calendar--open', + + header: 'lsd-calendar-header', + grid: 'lsd-calendar-body', + button: 'lsd-calendar-button', + + day: 'lsd-calendar-day', + daySelected: 'lsd-calendar-day--selected', + dayDisabled: 'lsd-calendar-day--diabled', +} diff --git a/packages/lsd-react/src/components/Calendar/Calendar.context.ts b/packages/lsd-react/src/components/Calendar/Calendar.context.ts new file mode 100644 index 0000000..0a8cb3e --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/Calendar.context.ts @@ -0,0 +1,19 @@ +import React from 'react' + +export type CalendarContextType = { + focusedDate: Date | null + isDateFocused: (date: Date) => boolean + isDateSelected: (date: Date) => boolean + isDateHovered: (date: Date) => boolean + isDateBlocked: (date: Date) => boolean + isFirstOrLastSelectedDate: (date: Date) => boolean + onDateFocus: (date: Date) => void + onDateHover: (date: Date) => void + onDateSelect: (date: Date) => void +} + +export const CalendarContext = React.createContext( + null as any, +) + +export const useCalendarContext = () => React.useContext(CalendarContext) diff --git a/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx b/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx new file mode 100644 index 0000000..a3b64b9 --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/Calendar.stories.tsx @@ -0,0 +1,29 @@ +import { Meta, Story } from '@storybook/react' +import { useRef, useState } from 'react' +import { Calendar, CalendarProps } from './Calendar' + +export default { + title: 'Calendar', + component: Calendar, +} as Meta + +export const Root: Story = (arg) => { + const ref = useRef(null) + + return ( +
+ console.log(date?.toDateString())} + open={true} + handleRef={ref} + > + Calendar + +
+ ) +} + +Root.args = { + value: '', +} diff --git a/packages/lsd-react/src/components/Calendar/Calendar.styles.ts b/packages/lsd-react/src/components/Calendar/Calendar.styles.ts new file mode 100644 index 0000000..904d0c9 --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/Calendar.styles.ts @@ -0,0 +1,70 @@ +import { css } from '@emotion/react' +import { calendarClasses } from './Calendar.classes' + +export const CalendarStyles = css` + .${calendarClasses.root} { + border: 1px solid rgb(var(--lsd-border-primary)); + visibility: hidden; + position: absolute; + top: 0; + left: 0; + opacity: 0; + visibility: hidden; + margin: 0; + padding: 0; + box-sizing: border-box; + background: rgb(var(--lsd-surface-primary)); + } + + .${calendarClasses.container} { + display: grid; + margin: 8px 2px 2px; + grid-gap: 0 64px; + } + + .${calendarClasses.open} { + opacity: 1; + visibility: visible; + } + + .${calendarClasses.header} { + display: flex; + justify-content: space-between; + align-items: center; + padding-inline: 10px; + padding-bottom: 10px; + } + + .${calendarClasses.grid} { + display: grid; + grid-template-columns: repeat(7, 1fr); + justify-content: center; + } + + .${calendarClasses.day} { + cursor: pointer; + border: none; + background: transparent; + aspect-ratio: 1 / 1; + } + + .${calendarClasses.daySelected} { + border: 1px solid rgb(var(--lsd-border-primary)); + } + + .${calendarClasses.dayDisabled} { + opacity: 0.3; + cursor: default; + } + + .${calendarClasses.button} { + border: 1px solid rgb(var(--lsd-border-primary)); + cursor: pointer; + background: transparent; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + } +` diff --git a/packages/lsd-react/src/components/Calendar/Calendar.tsx b/packages/lsd-react/src/components/Calendar/Calendar.tsx new file mode 100644 index 0000000..f6c782e --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/Calendar.tsx @@ -0,0 +1,114 @@ +import { + OnDatesChangeProps, + START_DATE, + useDatepicker, +} from '@datepicker-react/hooks' +import clsx from 'clsx' +import React, { useEffect, useRef, useState } from 'react' +import { calendarClasses } from './Calendar.classes' +import { CalendarContext } from './Calendar.context' +import { Month } from './Month' + +export type CalendarProps = Omit< + React.HTMLAttributes, + 'label' +> & { + open?: boolean + value?: string + handleDateFieldChange: (data: Date) => void + handleRef: React.RefObject +} + +export const Calendar: React.FC & { + classes: typeof calendarClasses +} = ({ + open, + handleRef, + value = null, + handleDateFieldChange, + children, + ...props +}) => { + const [style, setStyle] = useState({}) + + const handleDateChange = (data: OnDatesChangeProps) => { + handleDateFieldChange(data.startDate ?? new Date()) + onDateFocus(data.startDate ?? new Date()) + } + + const { + activeMonths, + isDateSelected, + isDateHovered, + isFirstOrLastSelectedDate, + isDateBlocked, + isDateFocused, + focusedDate, + onDateHover, + onDateSelect, + onDateFocus, + goToPreviousMonths, + goToNextMonths, + } = useDatepicker({ + startDate: value ? new Date(value) : null, + endDate: null, + focusedInput: START_DATE, + onDatesChange: handleDateChange, + numberOfMonths: 1, + }) + + const updateStyle = () => { + const { width, height, top, left } = + handleRef.current!.getBoundingClientRect() + + setStyle({ + left, + width, + top: top + height, + }) + } + + useEffect(() => { + updateStyle() + }, [open]) + + return ( + +
+
+ {activeMonths.map((month) => ( + + ))} +
+
+
+ ) +} + +Calendar.classes = calendarClasses diff --git a/packages/lsd-react/src/components/Calendar/Day.tsx b/packages/lsd-react/src/components/Calendar/Day.tsx new file mode 100644 index 0000000..64a1434 --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/Day.tsx @@ -0,0 +1,61 @@ +import { useRef, useContext } from 'react' +import { useDay } from '@datepicker-react/hooks' +import { CalendarContext } from './Calendar.context' +import clsx from 'clsx' +import { calendarClasses } from './Calendar.classes' +import { Typography } from '../Typography' + +export type DayProps = { + day?: string + date: Date +} + +export const Day = ({ day, date }: DayProps) => { + const dayRef = useRef(null) + const { + focusedDate, + isDateFocused, + isDateSelected, + isDateHovered, + isDateBlocked, + isFirstOrLastSelectedDate, + onDateSelect, + onDateFocus, + onDateHover, + } = useContext(CalendarContext) + + const { onClick, onKeyDown, onMouseEnter, tabIndex } = useDay({ + date, + focusedDate, + isDateFocused, + isDateSelected, + isDateHovered, + isDateBlocked, + isFirstOrLastSelectedDate, + onDateFocus, + onDateSelect, + onDateHover, + dayRef, + }) + + if (!day) { + return null + } + + return ( + + ) +} diff --git a/packages/lsd-react/src/components/Calendar/Month.tsx b/packages/lsd-react/src/components/Calendar/Month.tsx new file mode 100644 index 0000000..922eed5 --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/Month.tsx @@ -0,0 +1,99 @@ +import { FirstDayOfWeek, useMonth } from '@datepicker-react/hooks' +import clsx from 'clsx' +import { NavigateBeforeIcon, NavigateNextIcon } from '../Icons' +import { Typography } from '../Typography' +import { calendarClasses } from './Calendar.classes' +import { Day } from './Day' + +export type MonthProps = { + year: number + month: number + firstDayOfWeek: FirstDayOfWeek + goToPreviousMonths: () => void + goToNextMonths: () => void +} + +export const Month = ({ + year, + month, + firstDayOfWeek, + goToPreviousMonths, + goToNextMonths, +}: MonthProps) => { + const { days, weekdayLabels, monthLabel } = useMonth({ + year, + month, + firstDayOfWeek, + }) + + const renderOtherDays = (idx: number, firstDate: Date) => { + const date = new Date(firstDate) + date.setDate(date.getDate() + idx) + return date.getDate() + } + + return ( +
+
+ + {monthLabel} + +
+
+ {weekdayLabels.map((dayLabel, idx) => ( + + {dayLabel[0]} + + ))} +
+
+ {days.map((ele, idx) => + typeof ele !== 'number' ? ( + + ) : ( + + ), + )} + {days.length % 7 !== 0 && + new Array(7 - (days.length % 7)).fill(null).map((ele, idx) => ( + + ))} +
+
+ ) +} diff --git a/packages/lsd-react/src/components/Calendar/index.ts b/packages/lsd-react/src/components/Calendar/index.ts new file mode 100644 index 0000000..8c50cf8 --- /dev/null +++ b/packages/lsd-react/src/components/Calendar/index.ts @@ -0,0 +1 @@ +export * from './Calendar' diff --git a/packages/lsd-react/src/components/DateField/DateField.classes.ts b/packages/lsd-react/src/components/DateField/DateField.classes.ts new file mode 100644 index 0000000..c39607e --- /dev/null +++ b/packages/lsd-react/src/components/DateField/DateField.classes.ts @@ -0,0 +1,16 @@ +export const dateFieldClasses = { + root: `lsd-date-field`, + + inputContainer: `lsd-date-field-input-container`, + input: `lsd-date-field-input-container__input`, + icon: `lsd-date-field-input-container__icon`, + iconButton: `lsd-date-field-input-container__icon-button`, + + supportingText: 'lsd-date-field__supporting-text', + + disabled: `lsd-date-field--disabled`, + error: 'lsd-date-field--error', + + large: `lsd-date-field--large`, + medium: `lsd-date-field--medium`, +} diff --git a/packages/lsd-react/src/components/DateField/DateField.stories.tsx b/packages/lsd-react/src/components/DateField/DateField.stories.tsx new file mode 100644 index 0000000..f88b2f7 --- /dev/null +++ b/packages/lsd-react/src/components/DateField/DateField.stories.tsx @@ -0,0 +1,30 @@ +import { Meta, Story } from '@storybook/react' +import { DateField, DateFieldProps } from './DateField' + +export default { + title: 'DateField', + component: DateField, + argTypes: { + size: { + type: { + name: 'enum', + value: ['medium', 'large'], + }, + defaultValue: 'large', + }, + }, +} as Meta + +export const Root: Story = ({ ...args }) => { + return +} + +Root.args = { + size: 'large', + supportingText: 'Supporting text', + disabled: false, + error: false, + errorIcon: false, + clearButton: true, + onChange: undefined, +} diff --git a/packages/lsd-react/src/components/DateField/DateField.styles.ts b/packages/lsd-react/src/components/DateField/DateField.styles.ts new file mode 100644 index 0000000..0fd6002 --- /dev/null +++ b/packages/lsd-react/src/components/DateField/DateField.styles.ts @@ -0,0 +1,79 @@ +import { css } from '@emotion/react' +import { dateFieldClasses } from './DateField.classes' + +export const DateFieldStyles = css` + .${dateFieldClasses.root} { + width: auto; + border-bottom: 1px solid rgb(var(--lsd-border-primary)); + box-sizing: border-box; + } + + .${dateFieldClasses.inputContainer} { + display: flex; + align-items: center; + justify-content: space-between; + } + + .${dateFieldClasses.disabled} { + opacity: 0.34; + } + + .${dateFieldClasses.input} { + border: none; + outline: none; + font-size: 14px; + color: rgb(var(--lsd-text-primary)); + background: none; + width: 100%; + } + + .${dateFieldClasses.input}::-webkit-inner-spin-button, + .${dateFieldClasses.input}::-webkit-calendar-picker-indicator { + display: none; + -webkit-appearance: none; + } + + .${dateFieldClasses.input}:hover { + outline: none; + } + + .${dateFieldClasses.input}:focus::-webkit-datetime-edit { + color: rgb(var(--lsd-text-primary)); + opacity: 0.3; + } + + .${dateFieldClasses.error} + .${dateFieldClasses.input}::-webkit-datetime-edit-fields-wrapper { + text-decoration: line-through; + } + + .${dateFieldClasses.supportingText} { + width: fit-content; + margin-top: 20px; + } + + .${dateFieldClasses.large} { + width: 208px; + height: 40px; + padding: 10px 14px; + } + + .${dateFieldClasses.medium} { + width: 188px; + height: 32px; + padding: 6px 12px; + } + + .${dateFieldClasses.iconButton} { + padding: 0; + width: auto; + height: auto; + margin: 0; + border: 0; + + svg { + width: 100%; + height: auto; + } + } +` diff --git a/packages/lsd-react/src/components/DateField/DateField.tsx b/packages/lsd-react/src/components/DateField/DateField.tsx new file mode 100644 index 0000000..7dde40f --- /dev/null +++ b/packages/lsd-react/src/components/DateField/DateField.tsx @@ -0,0 +1,110 @@ +import clsx from 'clsx' +import React, { useRef } from 'react' +import { useInput } from '../../utils/useInput' +import { IconButton } from '../IconButton' +import { CloseIcon, ErrorIcon } from '../Icons' +import { Typography } from '../Typography' +import { dateFieldClasses } from './DateField.classes' + +export type DateFieldProps = Omit< + React.HTMLAttributes, + 'onChange' | 'value' +> & + Pick, 'onChange'> & { + size?: 'large' | 'medium' + error?: boolean + errorIcon?: boolean + clearButton?: boolean + disabled?: boolean + supportingText?: string + value?: string + defaultValue?: string + placeholder?: string + icon?: React.ReactNode + onIconClick?: () => void + inputProps?: React.InputHTMLAttributes + } + +export const DateField: React.FC & { + classes: typeof dateFieldClasses +} = ({ + size = 'large', + error = false, + errorIcon = false, + clearButton, + supportingText, + children, + value, + placeholder, + defaultValue, + disabled, + onChange, + icon, + onIconClick, + inputProps = {}, + ...props +}) => { + const ref = useRef(null) + const input = useInput({ defaultValue, value, onChange, ref }) + + const onCancel = () => input.setValue('') + + return ( +
+
+ + {icon ? ( + !disabled && onIconClick && onIconClick()} + > + {icon} + + ) : error && errorIcon ? ( + + ) : clearButton && input.filled ? ( + !disabled && onCancel()} + aria-label="clear" + className={dateFieldClasses.iconButton} + > + + + ) : null} +
+ {supportingText && ( +
+ + {supportingText} + +
+ )} + {children} +
+ ) +} + +DateField.classes = dateFieldClasses diff --git a/packages/lsd-react/src/components/DateField/index.ts b/packages/lsd-react/src/components/DateField/index.ts new file mode 100644 index 0000000..f865b34 --- /dev/null +++ b/packages/lsd-react/src/components/DateField/index.ts @@ -0,0 +1 @@ +export * from './DateField' diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts b/packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts new file mode 100644 index 0000000..8af429d --- /dev/null +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.classes.ts @@ -0,0 +1,4 @@ +export const datePickerClasses = { + root: `lsd-date-picker`, + withCalendar: `lsd-date-picker--with-calendar`, +} diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx b/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx new file mode 100644 index 0000000..758dd4e --- /dev/null +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.stories.tsx @@ -0,0 +1,32 @@ +import { Meta, Story } from '@storybook/react' +import { DatePicker, DatePickerProps } from './DatePicker' + +export default { + title: 'DatePicker', + component: DatePicker, + argTypes: { + size: { + type: { + name: 'enum', + value: ['medium', 'large'], + }, + defaultValue: 'large', + }, + }, +} as Meta + +export const Root: Story = ({ ...args }) => { + return DatePicker +} + +Root.args = { + size: 'large', + supportingText: 'Supporting text', + disabled: false, + error: false, + value: '', + onChange: undefined, + errorIcon: false, + clearButton: true, + withCalendar: true, +} diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts b/packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts new file mode 100644 index 0000000..1a37e7d --- /dev/null +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.styles.ts @@ -0,0 +1,13 @@ +import { css } from '@emotion/react' +import { calendarClasses } from '../Calendar/Calendar.classes' +import { datePickerClasses } from './DatePicker.classes' + +export const DatePickerStyles = css` + .${datePickerClasses.root} { + width: fit-content; + } + + #lsd-presentation .${calendarClasses.root} { + border-top: none; + } +` diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx new file mode 100644 index 0000000..447f609 --- /dev/null +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx @@ -0,0 +1,76 @@ +import clsx from 'clsx' +import React, { useRef, useState } from 'react' +import { Calendar } from '../Calendar' +import { DateField } from '../DateField' +import { CalendarIcon } from '../Icons' +import { Portal } from '../PortalProvider/Portal' +import { datePickerClasses } from './DatePicker.classes' + +export type DatePickerProps = Omit< + React.HTMLAttributes, + 'onChange' | 'value' +> & + Pick, 'onChange'> & { + size?: 'large' | 'medium' + error?: boolean + errorIcon?: boolean + clearButton?: boolean + disabled?: boolean + withCalendar?: boolean + supportingText?: string + value?: string + onChange?: (value: string) => void + defaultValue?: string + placeholder?: string + inputProps?: React.InputHTMLAttributes + } + +export const DatePicker: React.FC & { + classes: typeof datePickerClasses +} = ({ value, onChange, withCalendar = true, ...props }) => { + const ref = useRef(null) + const [openCalendar, setOpenCalendar] = useState(false) + const [date, setDate] = useState(value || '') + + const handleDateFieldChange = (date: any) => { + const offset = new Date(date).getTimezoneOffset() + const formattedDate = new Date(date.getTime() - offset * 60 * 1000) + const value = formattedDate.toISOString().split('T')[0] + setDate(value) + setOpenCalendar(false) + onChange && onChange(value) + } + + return ( +
+ } + onIconClick={() => setOpenCalendar((prev) => !prev)} + value={date} + onChange={(data) => setDate(data.target.value)} + style={{ width: '310px' }} + {...props} + > + + {withCalendar && ( + + )} + + +
+ ) +} + +DatePicker.classes = datePickerClasses diff --git a/packages/lsd-react/src/components/DatePicker/index.ts b/packages/lsd-react/src/components/DatePicker/index.ts new file mode 100644 index 0000000..192c395 --- /dev/null +++ b/packages/lsd-react/src/components/DatePicker/index.ts @@ -0,0 +1 @@ +export * from './DatePicker' diff --git a/packages/lsd-react/src/components/Icons/CalendarIcon/CalendarIcon.tsx b/packages/lsd-react/src/components/Icons/CalendarIcon/CalendarIcon.tsx new file mode 100644 index 0000000..3550d57 --- /dev/null +++ b/packages/lsd-react/src/components/Icons/CalendarIcon/CalendarIcon.tsx @@ -0,0 +1,24 @@ +import { LsdIcon } from '../LsdIcon' + +export const CalendarIcon = LsdIcon( + (props) => ( + + + + ), + { + filled: true, + }, +) diff --git a/packages/lsd-react/src/components/Icons/CalendarIcon/index.ts b/packages/lsd-react/src/components/Icons/CalendarIcon/index.ts new file mode 100644 index 0000000..e7b74eb --- /dev/null +++ b/packages/lsd-react/src/components/Icons/CalendarIcon/index.ts @@ -0,0 +1 @@ +export * from './CalendarIcon' diff --git a/packages/lsd-react/src/components/Icons/index.ts b/packages/lsd-react/src/components/Icons/index.ts index 06fe2af..b4c3312 100644 --- a/packages/lsd-react/src/components/Icons/index.ts +++ b/packages/lsd-react/src/components/Icons/index.ts @@ -19,4 +19,5 @@ export * from './SearchIcon' export * from './PickIcon' export * from './RadioButtonIcon' export * from './RadioButtonFilledIcon' +export * from './CalendarIcon' export * from './RemoveIcon' diff --git a/yarn.lock b/yarn.lock index fd51bde..8a32f70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1344,6 +1344,13 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@datepicker-react/hooks@^2.8.4": + version "2.8.4" + resolved "https://registry.yarnpkg.com/@datepicker-react/hooks/-/hooks-2.8.4.tgz#6e07aa98bf21b90b7c88fb35919cca6eb08f2c31" + integrity sha512-qaYJKK5sOSdqcL/OnCtyv3/Q6fRRljfeAyl5ISTPgEO0CM5xZzkGmTx40+6wvqjH5lEZH4ysS95nPyLwZS2tlw== + dependencies: + date-fns "^2.14.0" + "@design-systems/utils@2.12.0": version "2.12.0" resolved "https://registry.npmjs.org/@design-systems/utils/-/utils-2.12.0.tgz" @@ -6789,6 +6796,11 @@ dargs@^7.0.0: resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz" integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== +date-fns@^2.14.0: + version "2.29.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + dateformat@^3.0.0: version "3.0.3" resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz"