UNPKG

@helpwave/hightide

Version:

helpwave's component and theming library

1 lines 89.3 kB
{"version":3,"sources":["../../../src/components/user-action/DateAndTimePicker.tsx","../../../src/localization/LanguageProvider.tsx","../../../src/hooks/useLocalStorage.ts","../../../src/localization/util.ts","../../../src/localization/useTranslation.ts","../../../src/util/noop.ts","../../../src/util/array.ts","../../../src/util/date.ts","../../../src/components/user-action/Button.tsx","../../../src/components/date/TimePicker.tsx","../../../src/components/date/DatePicker.tsx","../../../src/components/date/YearMonthPicker.tsx","../../../src/components/layout-and-navigation/Expandable.tsx","../../../src/components/date/DayPicker.tsx","../../../src/localization/defaults/time.ts","../../../src/localization/defaults/form.ts"],"sourcesContent":["import type { ReactNode } from 'react'\nimport clsx from 'clsx'\nimport type { PropsForTranslation } from '../../localization/useTranslation'\nimport { useTranslation } from '../../localization/useTranslation'\nimport { noop } from '../../util/noop'\nimport { addDuration, subtractDuration } from '../../util/date'\nimport { SolidButton } from './Button'\nimport type { TimePickerProps } from '../date/TimePicker'\nimport { TimePicker } from '../date/TimePicker'\nimport type { DatePickerProps } from '../date/DatePicker'\nimport { DatePicker } from '../date/DatePicker'\nimport type { FormTranslationType } from '../../localization/defaults/form'\nimport { formTranslation } from '../../localization/defaults/form'\nimport type { TimeTranslationType } from '../../localization/defaults/time'\nimport { timeTranslation } from '../../localization/defaults/time'\n\ntype DateAndTimePickerTranslationType = FormTranslationType & TimeTranslationType\n\nexport type DateTimePickerMode = 'date' | 'time' | 'dateTime'\n\nexport type DateTimePickerProps = {\n mode?: DateTimePickerMode,\n value?: Date,\n start?: Date,\n end?: Date,\n onChange?: (date: Date) => void,\n onFinish?: (date: Date) => void,\n onRemove?: () => void,\n datePickerProps?: Omit<DatePickerProps, 'onChange' | 'value' | 'start' | 'end'>,\n timePickerProps?: Omit<TimePickerProps, 'onChange' | 'time' | 'maxHeight'>,\n}\n\n/**\n * A Component for picking a Date and Time\n */\nexport const DateTimePicker = ({\n overwriteTranslation,\n value = new Date(),\n start = subtractDuration(new Date(), { years: 50 }),\n end = addDuration(new Date(), { years: 50 }),\n mode = 'dateTime',\n onFinish = noop,\n onChange = noop,\n onRemove = noop,\n timePickerProps,\n datePickerProps,\n }: PropsForTranslation<DateAndTimePickerTranslationType, DateTimePickerProps>) => {\n const translation = useTranslation([formTranslation, timeTranslation], overwriteTranslation)\n\n const useDate = mode === 'dateTime' || mode === 'date'\n const useTime = mode === 'dateTime' || mode === 'time'\n\n let dateDisplay: ReactNode\n let timeDisplay: ReactNode\n\n if (useDate) {\n dateDisplay = (\n <DatePicker\n {...datePickerProps}\n className=\"min-w-[320px] min-h-[250px]\"\n yearMonthPickerProps={{ maxHeight: 218 }}\n value={value}\n start={start}\n end={end}\n onChange={onChange}\n />\n )\n }\n if (useTime) {\n timeDisplay = (\n <TimePicker\n {...timePickerProps}\n className={clsx('h-full', { 'justify-between w-full': mode === 'time' })}\n maxHeight={250}\n time={value}\n onChange={onChange}\n />\n )\n }\n\n return (\n <div className=\"flex-col-2 w-fit\">\n <div className=\"flex-row-4\">\n {dateDisplay}\n {timeDisplay}\n </div>\n <div className=\"flex-row-2 justify-end\">\n <div className=\"flex-row-2 mt-1\">\n <SolidButton size=\"medium\" color=\"negative\" onClick={onRemove}>{translation('clear')}</SolidButton>\n <SolidButton\n size=\"medium\"\n onClick={() => onFinish(value)}\n >\n {translation('change')}\n </SolidButton>\n </div>\n </div>\n </div>\n )\n}\n","import type { Dispatch, PropsWithChildren, SetStateAction } from 'react'\nimport { createContext, useContext, useEffect, useState } from 'react'\nimport { useLocalStorage } from '../hooks/useLocalStorage'\nimport type { Language } from './util'\nimport { LanguageUtil } from './util'\n\nexport type LanguageContextValue = {\n language: Language,\n setLanguage: Dispatch<SetStateAction<Language>>,\n}\n\nexport const LanguageContext = createContext<LanguageContextValue>({\n language: LanguageUtil.DEFAULT_LANGUAGE,\n setLanguage: (v) => v\n})\n\nexport const useLanguage = () => useContext(LanguageContext)\n\nexport const useLocale = (overWriteLanguage?: Language) => {\n const { language } = useLanguage()\n const mapping: Record<Language, string> = {\n en: 'en-US',\n de: 'de-DE'\n }\n return mapping[overWriteLanguage ?? language]\n}\n\ntype LanguageProviderProps = {\n initialLanguage?: Language,\n}\n\nexport const LanguageProvider = ({ initialLanguage, children }: PropsWithChildren<LanguageProviderProps>) => {\n const [language, setLanguage] = useState<Language>(initialLanguage ?? LanguageUtil.DEFAULT_LANGUAGE)\n const [storedLanguage, setStoredLanguage] = useLocalStorage<Language>('language', initialLanguage ?? LanguageUtil.DEFAULT_LANGUAGE)\n\n useEffect(() => {\n if (language !== initialLanguage && initialLanguage) {\n console.warn('LanguageProvider initial state changed: Prefer using languageProvider\\'s setLanguage instead')\n setLanguage(initialLanguage)\n }\n }, [initialLanguage]) // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n document.documentElement.setAttribute('lang', language)\n setStoredLanguage(language)\n }, [language]) // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n if (storedLanguage !== null) {\n setLanguage(storedLanguage)\n return\n }\n\n const LanguageToTestAgainst = Object.values(LanguageUtil.languages)\n\n const matchingBrowserLanguage = window.navigator.languages\n .map(language => LanguageToTestAgainst.find((test) => language === test || language.split('-')[0] === test))\n .filter(entry => entry !== undefined)\n\n if (matchingBrowserLanguage.length === 0) return\n\n const firstMatch = matchingBrowserLanguage[0] as Language\n setLanguage(firstMatch)\n }, []) // eslint-disable-line react-hooks/exhaustive-deps\n\n return (\n <LanguageContext.Provider value={{\n language,\n setLanguage\n }}>\n {children}\n </LanguageContext.Provider>\n )\n}","'use client'\n\nimport type { Dispatch, SetStateAction } from 'react'\nimport { useCallback, useState } from 'react'\nimport { LocalStorageService } from '../util/storage'\nimport { resolveSetState } from '../util/resolveSetState'\n\ntype SetValue<T> = Dispatch<SetStateAction<T>>\nexport const useLocalStorage = <T>(key: string, initValue: T): [T, SetValue<T>] => {\n const get = useCallback((): T => {\n if (typeof window === 'undefined') {\n return initValue\n }\n const storageService = new LocalStorageService()\n const value = storageService.get<T>(key)\n return value || initValue\n }, [initValue, key])\n\n const [storedValue, setStoredValue] = useState<T>(get)\n\n const setValue: SetValue<T> = useCallback(action => {\n const newValue = resolveSetState(action, storedValue)\n const storageService = new LocalStorageService()\n storageService.set(key, newValue)\n\n setStoredValue(newValue)\n }, [storedValue, setStoredValue, key])\n\n return [storedValue, setValue]\n}","/**\n * The supported languages\n */\nconst languages = ['en', 'de'] as const\n\n/**\n * The supported languages\n */\nexport type Language = typeof languages[number]\n\n/**\n * The supported languages' names in their respective language\n */\nconst languagesLocalNames: Record<Language, string> = {\n en: 'English',\n de: 'Deutsch',\n}\n\n/**\n * The default language\n */\nconst DEFAULT_LANGUAGE: Language = 'en'\n\n/**\n * A constant definition for holding data regarding languages\n */\nexport const LanguageUtil = {\n languages,\n DEFAULT_LANGUAGE,\n languagesLocalNames,\n}","import { useLanguage } from './LanguageProvider'\nimport type { Language } from './util'\n\n/**\n * A type describing the pluralization of a word\n */\nexport type TranslationPlural = {\n zero?: string,\n one?: string,\n two?: string,\n few?: string,\n many?: string,\n other: string,\n}\n\n/**\n * The type describing all values of a translation\n */\nexport type TranslationType = Record<string, string | TranslationPlural>\n\n/**\n * The type of translations\n */\nexport type Translation<T extends TranslationType> = Record<Language, T>\n\ntype OverwriteTranslationType<T extends TranslationType> = {\n language?: Language,\n translation?: Translation<Partial<T>>,\n}\n\n/**\n * Adds the `language` prop to the component props.\n *\n * @param Translation the type of the translation object\n *\n * @param Props the type of the component props, defaults to `Record<string, never>`,\n * if you don't expect any other props other than `language` and get an\n * error when using your component (because it uses `forwardRef` etc.)\n * you can try out `Record<string, unknown>`, this might resolve your\n * problem as `SomeType & never` is still `never` but `SomeType & unknown`\n * is `SomeType` which means that adding back props (like `ref` etc.)\n * works properly\n */\nexport type PropsForTranslation<\n Translation extends TranslationType,\n Props = unknown\n> = Props & {\n overwriteTranslation?: OverwriteTranslationType<Translation>,\n}\n\ntype StringKeys<T> = Extract<keyof T, string>;\n\ntype TranslationFunctionOptions = {\n replacements?: Record<string, string>,\n count?: number,\n}\ntype TranslationFunction<T extends TranslationType> = (key: StringKeys<T>, options?: TranslationFunctionOptions) => string\n\nexport const TranslationPluralCount = {\n zero: 0,\n one: 1,\n two: 2,\n few: 3,\n many: 11,\n other: -1,\n}\n\n\nexport const useTranslation = <T extends TranslationType>(\n translations: Translation<Partial<TranslationType>>[],\n overwriteTranslation: OverwriteTranslationType<T> = {}\n): TranslationFunction<T> => {\n const { language: languageProp, translation: overwrite } = overwriteTranslation\n const { language: inferredLanguage } = useLanguage()\n const usedLanguage = languageProp ?? inferredLanguage\n const usedTranslations = [...translations]\n if (overwrite) {\n usedTranslations.push(overwrite)\n }\n\n return (key: StringKeys<T>, options?: TranslationFunctionOptions): string => {\n const { count, replacements } = { ...{ count: 0, replacements: {} }, ...options }\n\n try {\n for (let i = translations.length - 1; i >= 0; i--) {\n const translation = translations[i]\n const localizedTranslation = translation[usedLanguage]\n if (!localizedTranslation) {\n continue\n }\n const value = localizedTranslation[key]\n if(!value) {\n continue\n }\n\n let forProcessing: string\n if (typeof value !== 'string') {\n if (count === TranslationPluralCount.zero && value?.zero) {\n forProcessing = value.zero\n } else if (count === TranslationPluralCount.one && value?.one) {\n forProcessing = value.one\n } else if (count === TranslationPluralCount.two && value?.two) {\n forProcessing = value.two\n } else if (TranslationPluralCount.few <= count && count < TranslationPluralCount.many && value?.few) {\n forProcessing = value.few\n } else if (count > TranslationPluralCount.many && value?.many) {\n forProcessing = value.many\n } else {\n forProcessing = value.other\n }\n } else {\n forProcessing = value\n }\n forProcessing = forProcessing.replace(/\\{\\{(\\w+)}}/g, (_, placeholder) => {\n return replacements[placeholder] ?? `{{key:${placeholder}}}` // fallback if key is missing\n })\n return forProcessing\n }\n } catch (e) {\n console.error(e)\n }\n return `{{${usedLanguage}:${key}}}`\n }\n}","export const noop = () => undefined\n","export const equalSizeGroups = <T>(array: T[], groupSize: number): T[][] => {\n if (groupSize <= 0) {\n console.warn(`group size should be greater than 0: groupSize = ${groupSize}`)\n return [[...array]]\n }\n\n const groups = []\n for (let i = 0; i < array.length; i += groupSize) {\n groups.push(array.slice(i, Math.min(i + groupSize, array.length)))\n }\n return groups\n}\n\nexport type RangeOptions = {\n /** Whether the range can be defined empty via end < start without a warning */\n allowEmptyRange: boolean,\n stepSize: number,\n exclusiveStart: boolean,\n exclusiveEnd: boolean,\n}\n\nconst defaultRangeOptions: RangeOptions = {\n allowEmptyRange: false,\n stepSize: 1,\n exclusiveStart: false,\n exclusiveEnd: true,\n}\n\n/**\n * @param endOrRange The end value or a range [start, end], end is exclusive\n * @param options the options for defining the range\n */\nexport const range = (endOrRange: number | [number, number], options?: Partial<RangeOptions>): number[] => {\n const { allowEmptyRange, stepSize, exclusiveStart, exclusiveEnd } = { ...defaultRangeOptions, ...options }\n let start = 0\n let end: number\n if (typeof endOrRange === 'number') {\n end = endOrRange\n } else {\n start = endOrRange[0]\n end = endOrRange[1]\n }\n if (!exclusiveEnd) {\n end -= 1\n }\n if (exclusiveStart) {\n start += 1\n }\n\n if (end - 1 < start) {\n if (!allowEmptyRange) {\n console.warn(`range: end (${end}) < start (${start}) should be allowed explicitly, set options.allowEmptyRange to true`)\n }\n return []\n }\n return Array.from({ length: end - start }, (_, index) => index * stepSize + start)\n}\n\n/** Finds the closest match\n * @param list The list of all possible matches\n * @param firstCloser Return whether item1 is closer than item2\n */\nexport const closestMatch = <T>(list: T[], firstCloser: (item1: T, item2: T) => boolean) => {\n return list.reduce((item1, item2) => {\n return firstCloser(item1, item2) ? item1 : item2\n })\n}\n\n/**\n * returns the item in middle of a list and its neighbours before and after\n * e.g. [1,2,3,4,5,6] for item = 1 would return [5,6,1,2,3]\n */\nexport const getNeighbours = <T>(list: T[], item: T, neighbourDistance: number = 2) => {\n const index = list.indexOf(item)\n const totalItems = neighbourDistance * 2 + 1\n if (list.length < totalItems) {\n console.warn('List is to short')\n return list\n }\n\n if (index === -1) {\n console.error('item not found in list')\n return list.splice(0, totalItems)\n }\n\n let start = index - neighbourDistance\n if (start < 0) {\n start += list.length\n }\n const end = (index + neighbourDistance + 1) % list.length\n\n const result: T[] = []\n let ignoreOnce = list.length === totalItems\n for (let i = start; i !== end || ignoreOnce; i = (i + 1) % list.length) {\n result.push(list[i]!)\n if (end === i && ignoreOnce) {\n ignoreOnce = false\n }\n }\n return result\n}\n\nexport const createLoopingListWithIndex = <T>(list: T[], startIndex: number = 0, length: number = 0, forwards: boolean = true) => {\n if (length < 0) {\n console.warn(`createLoopingList: length must be >= 0, given ${length}`)\n } else if (length === 0) {\n length = list.length\n }\n\n const returnList: [number, T][] = []\n\n if (forwards) {\n for (let i = startIndex; returnList.length < length; i = (i + 1) % list.length) {\n returnList.push([i, list[i]!])\n }\n } else {\n for (let i = startIndex; returnList.length < length; i = i === 0 ? i = list.length - 1 : i - 1) {\n returnList.push([i, list[i]!])\n }\n }\n\n return returnList\n}\n\nexport const createLoopingList = <T>(list: T[], startIndex: number = 0, length: number = 0, forwards: boolean = true) => {\n return createLoopingListWithIndex(list, startIndex, length, forwards).map(([_, item]) => item)\n}\n\nexport const ArrayUtil = {\n unique: <T>(list: T[]): T[] => {\n const seen = new Set<T>()\n return list.filter((item) => {\n if (seen.has(item)) {\n return false\n }\n seen.add(item)\n return true\n })\n },\n\n difference: <T>(list: T[], removeList: T[]): T[] => {\n const remove = new Set<T>(removeList)\n return list.filter((item) => !remove.has(item))\n }\n}\n","import { equalSizeGroups } from './array'\n\nexport const monthsList = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'] as const\nexport type Month = typeof monthsList[number]\n\nexport const weekDayList = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] as const\nexport type WeekDay = typeof weekDayList[number]\n\nexport const formatDate = (date: Date) => {\n const year = date.getFullYear().toString().padStart(4, '0')\n const month = (date.getMonth() + 1).toString().padStart(2, '0')\n const day = (date.getDate()).toString().padStart(2, '0')\n return `${year}-${month}-${day}`\n}\n\nexport const formatDateTime = (date: Date) => {\n const dateString = formatDate(date)\n const hours = date.getHours().toString().padStart(2, '0')\n const minutes = date.getMinutes().toString().padStart(2, '0')\n return `${dateString}T${hours}:${minutes}`\n}\n\nexport const getDaysInMonth = (year: number, month: number): number => {\n const lastDayOfMonth = new Date(year, month + 1, 0)\n return lastDayOfMonth.getDate()\n}\n\nexport type Duration = {\n years?: number,\n months?: number,\n days?: number,\n hours?: number,\n minutes?: number,\n seconds?: number,\n milliseconds?: number,\n}\n\nexport const changeDuration = (date: Date, duration: Duration, isAdding?: boolean): Date => {\n const {\n years = 0,\n months = 0,\n days = 0,\n hours = 0,\n minutes = 0,\n seconds = 0,\n milliseconds = 0,\n } = duration\n\n // Check ranges\n if (years < 0) {\n console.error(`Range error years must be greater than 0: received ${years}`)\n return new Date(date)\n }\n if (months < 0 || months > 11) {\n console.error(`Range error month must be 0 <= month <= 11: received ${months}`)\n return new Date(date)\n }\n if (days < 0) {\n console.error(`Range error days must be greater than 0: received ${days}`)\n return new Date(date)\n }\n if (hours < 0 || hours > 23) {\n console.error(`Range error hours must be 0 <= hours <= 23: received ${hours}`)\n return new Date(date)\n }\n if (minutes < 0 || minutes > 59) {\n console.error(`Range error minutes must be 0 <= minutes <= 59: received ${minutes}`)\n return new Date(date)\n }\n if (seconds < 0 || seconds > 59) {\n console.error(`Range error seconds must be 0 <= seconds <= 59: received ${seconds}`)\n return new Date(date)\n }\n if (milliseconds < 0) {\n console.error(`Range error seconds must be greater than 0: received ${milliseconds}`)\n return new Date(date)\n }\n\n const multiplier = isAdding ? 1 : -1\n\n const newDate = new Date(date)\n\n newDate.setFullYear(newDate.getFullYear() + multiplier * years)\n\n newDate.setMonth(newDate.getMonth() + multiplier * months)\n\n newDate.setDate(newDate.getDate() + multiplier * days)\n\n newDate.setHours(newDate.getHours() + multiplier * hours)\n\n newDate.setMinutes(newDate.getMinutes() + multiplier * minutes)\n\n newDate.setSeconds(newDate.getSeconds() + multiplier * seconds)\n\n newDate.setMilliseconds(newDate.getMilliseconds() + multiplier * milliseconds)\n\n return newDate\n}\n\nexport const addDuration = (date: Date, duration: Duration): Date => {\n return changeDuration(date, duration, true)\n}\n\nexport const subtractDuration = (date: Date, duration: Duration): Date => {\n return changeDuration(date, duration, false)\n}\n\nexport const getBetweenDuration = (startDate: Date, endDate: Date): Duration => {\n const durationInMilliseconds = endDate.getTime() - startDate.getTime()\n\n const millisecondsInSecond = 1000\n const millisecondsInMinute = 60 * millisecondsInSecond\n const millisecondsInHour = 60 * millisecondsInMinute\n const millisecondsInDay = 24 * millisecondsInHour\n const millisecondsInMonth = 30 * millisecondsInDay // Rough estimation, can be adjusted\n\n const years = Math.floor(durationInMilliseconds / (365.25 * millisecondsInDay))\n const months = Math.floor(durationInMilliseconds / millisecondsInMonth)\n const days = Math.floor(durationInMilliseconds / millisecondsInDay)\n const hours = Math.floor((durationInMilliseconds % millisecondsInDay) / millisecondsInHour)\n const seconds = Math.floor((durationInMilliseconds % millisecondsInHour) / millisecondsInSecond)\n const milliseconds = durationInMilliseconds % millisecondsInSecond\n\n return {\n years,\n months,\n days,\n hours,\n seconds,\n milliseconds,\n }\n}\n\n/** Checks if a given date is in the range of two dates\n *\n * An undefined value for startDate or endDate means no bound for the start or end respectively\n */\nexport const isInTimeSpan = (value: Date, startDate?: Date, endDate?: Date): boolean => {\n if (startDate && endDate) {\n console.assert(startDate <= endDate)\n return startDate <= value && value <= endDate\n } else if (startDate) {\n return startDate <= value\n } else if (endDate) {\n return endDate >= value\n } else {\n return true\n }\n}\n\n/** Compare two dates on the year, month, day */\nexport const equalDate = (date1: Date, date2: Date) => {\n return date1.getFullYear() === date2.getFullYear()\n && date1.getMonth() === date2.getMonth()\n && date1.getDate() === date2.getDate()\n}\n\nexport const getWeeksForCalenderMonth = (date: Date, weekStart: WeekDay, weeks: number = 6) => {\n const month = date.getMonth()\n const year = date.getFullYear()\n\n const dayList: Date[] = []\n let currentDate = new Date(year, month, 1) // Start of month\n const weekStartIndex = weekDayList.indexOf(weekStart)\n\n // Move the current day to the week before\n while (currentDate.getDay() !== weekStartIndex) {\n currentDate = subtractDuration(currentDate, { days: 1 })\n }\n\n while (dayList.length < 7 * weeks) {\n const date = new Date(currentDate)\n date.setHours(date.getHours(), date.getMinutes()) // To make sure we are not overwriting the time\n dayList.push(date)\n currentDate = addDuration(currentDate, { days: 1 })\n }\n\n // weeks\n return equalSizeGroups(dayList, 7)\n}\n","import type { ButtonHTMLAttributes, PropsWithChildren, ReactNode } from 'react'\nimport { forwardRef } from 'react'\nimport clsx from 'clsx'\n\n\nexport const ButtonColorUtil = {\n solid: ['primary', 'secondary', 'tertiary', 'positive', 'warning', 'negative', 'neutral'] as const,\n text: ['primary', 'negative', 'neutral'] as const,\n outline: ['primary'] as const,\n}\n\nexport const IconButtonUtil = {\n icon: [...ButtonColorUtil.solid, 'transparent'] as const,\n}\n\n\n/**\n * The allowed colors for the SolidButton and IconButton\n */\nexport type SolidButtonColor = typeof ButtonColorUtil.solid[number]\n/**\n * The allowed colors for the OutlineButton\n */\nexport type OutlineButtonColor = typeof ButtonColorUtil.outline[number]\n/**\n * The allowed colors for the TextButton\n */\nexport type TextButtonColor = typeof ButtonColorUtil.text[number]\n/**\n * The allowed colors for the IconButton\n */\nexport type IconButtonColor = typeof IconButtonUtil.icon[number]\n\n\n/**\n * The different sizes for a button\n */\ntype ButtonSizes = 'small' | 'medium' | 'large'\n\ntype IconButtonSize = 'tiny' | 'small' | 'medium' | 'large'\n\n/**\n * The shard properties between all button types\n */\nexport type ButtonProps = PropsWithChildren<{\n /**\n * @default 'medium'\n */\n size?: ButtonSizes,\n}> & ButtonHTMLAttributes<Element>\n\nconst paddingMapping: Record<ButtonSizes, string> = {\n small: 'btn-sm',\n medium: 'btn-md',\n large: 'btn-lg'\n}\n\nconst iconPaddingMapping: Record<IconButtonSize, string> = {\n tiny: 'icon-btn-xs',\n small: 'icon-btn-sm',\n medium: 'icon-btn-md',\n large: 'icon-btn-lg'\n}\n\nexport const ButtonUtil = {\n paddingMapping,\n iconPaddingMapping\n}\n\ntype ButtonWithIconsProps = ButtonProps & {\n startIcon?: ReactNode,\n endIcon?: ReactNode,\n}\n\nexport type SolidButtonProps = ButtonWithIconsProps & {\n color?: SolidButtonColor,\n}\n\nexport type OutlineButtonProps = ButtonWithIconsProps & {\n color?: OutlineButtonColor,\n}\n\nexport type TextButtonProps = ButtonWithIconsProps & {\n color?: TextButtonColor,\n coloredHoverBackground?: boolean,\n}\n\n/**\n * The shard properties between all button types\n */\nexport type IconButtonProps = PropsWithChildren<{\n /**\n * @default 'medium'\n */\n size?: IconButtonSize,\n color?: IconButtonColor,\n}> & ButtonHTMLAttributes<Element>\n\n/**\n * A button with a solid background and different sizes\n */\nconst SolidButton = forwardRef<HTMLButtonElement, SolidButtonProps>(function SolidButton({\n children,\n color = 'primary',\n size = 'medium',\n startIcon,\n endIcon,\n onClick,\n className,\n ...restProps\n }, ref) {\n const colorClasses = {\n primary: 'not-disabled:bg-button-solid-primary-background not-disabled:text-button-solid-primary-text',\n secondary: 'not-disabled:bg-button-solid-secondary-background not-disabled:text-button-solid-secondary-text',\n tertiary: 'not-disabled:bg-button-solid-tertiary-background not-disabled:text-button-solid-tertiary-text',\n positive: 'not-disabled:bg-button-solid-positive-background not-disabled:text-button-solid-positive-text',\n warning: 'not-disabled:bg-button-solid-warning-background not-disabled:text-button-solid-warning-text',\n negative: 'not-disabled:bg-button-solid-negative-background not-disabled:text-button-solid-negative-text',\n neutral: 'not-disabled:bg-button-solid-neutral-background not-disabled:text-button-solid-neutral-text',\n }[color]\n\n const iconColorClasses = {\n primary: 'not-group-disabled:text-button-solid-primary-icon',\n secondary: 'not-group-disabled:text-button-solid-secondary-icon',\n tertiary: 'not-group-disabled:text-button-solid-tertiary-icon',\n positive: 'not-group-disabled:text-button-solid-positive-icon',\n warning: 'not-group-disabled:text-button-solid-warning-icon',\n negative: 'not-group-disabled:text-button-solid-negative-icon',\n neutral: 'not-group-disabled:text-button-solid-neutral-icon',\n }[color]\n\n return (\n <button\n ref={ref}\n onClick={onClick}\n className={clsx(\n 'group font-semibold',\n colorClasses,\n 'not-disabled:hover:brightness-90',\n 'disabled:text-disabled-text disabled:bg-disabled-background',\n ButtonUtil.paddingMapping[size],\n className\n )}\n {...restProps}\n >\n {startIcon && (\n <span\n className={clsx(\n iconColorClasses,\n 'group-disabled:text-disabled-icon'\n )}\n >\n {startIcon}\n </span>\n )}\n {children}\n {endIcon && (\n <span\n className={clsx(\n iconColorClasses,\n 'group-disabled:text-disabled-icon'\n )}\n >\n {endIcon}\n </span>\n )}\n </button>\n )\n})\n\n/**\n * A button with an outline border and different sizes\n */\nconst OutlineButton = ({\n children,\n color = 'primary',\n size = 'medium',\n startIcon,\n endIcon,\n onClick,\n className,\n ...restProps\n }: OutlineButtonProps) => {\n const colorClasses = {\n primary: 'not-disabled:border-button-outline-primary-text not-disabled:text-button-outline-primary-text',\n }[color]\n\n const iconColorClasses = {\n primary: 'not-group-disabled:text-button-outline-primary-icon',\n }[color]\n return (\n <button\n onClick={onClick}\n className={clsx(\n 'group font-semibold bg-transparent border-2 ',\n 'not-disabled:hover:brightness-80',\n colorClasses,\n 'disabled:text-disabled-text disabled:border-disabled-outline',\n ButtonUtil.paddingMapping[size],\n className\n )}\n {...restProps}\n >\n {startIcon && (\n <span\n className={clsx(\n iconColorClasses,\n 'group-disabled:text-disabled-icon'\n )}\n >\n {startIcon}\n </span>\n )}\n {children}\n {endIcon && (\n <span\n className={clsx(\n iconColorClasses,\n 'group-disabled:text-disabled-icon'\n )}\n >\n {endIcon}\n </span>\n )}\n </button>\n )\n}\n\n/**\n * A text that is a button that can have different sizes\n */\nconst TextButton = ({\n children,\n color = 'neutral',\n size = 'medium',\n startIcon,\n endIcon,\n onClick,\n coloredHoverBackground = true,\n className,\n ...restProps\n }: TextButtonProps) => {\n const colorClasses = {\n primary: 'not-disabled:bg-transparent not-disabled:text-button-text-primary-text',\n negative: 'not-disabled:bg-transparent not-disabled:text-button-text-negative-text',\n neutral: 'not-disabled:bg-transparent not-disabled:text-button-text-neutral-text',\n }[color]\n\n const backgroundColor = {\n primary: 'not-disabled:hover:bg-button-text-primary-text/20',\n negative: 'not-disabled:hover:bg-button-text-negative-text/20',\n neutral: 'not-disabled:hover:bg-button-text-neutral-text/20',\n }[color]\n\n const iconColorClasses = {\n primary: 'not-group-disabled:text-button-text-primary-icon',\n negative: 'not-group-disabled:text-button-text-negative-icon',\n neutral: 'not-group-disabled:text-button-text-neutral-icon',\n }[color]\n\n return (\n <button\n onClick={onClick}\n className={clsx(\n 'group font-semibold',\n 'disabled:text-disabled-text',\n colorClasses,\n {\n [backgroundColor]: coloredHoverBackground,\n 'not-disabled:hover:bg-button-text-hover-background': !coloredHoverBackground,\n },\n ButtonUtil.paddingMapping[size],\n className\n )}\n {...restProps}\n >\n {startIcon && (\n <span\n className={clsx(\n iconColorClasses,\n 'group-disabled:text-disabled-icon'\n )}\n >\n {startIcon}\n </span>\n )}\n {children}\n {endIcon && (\n <span\n className={clsx(\n iconColorClasses,\n 'group-disabled:text-disabled-icon'\n )}\n >\n {endIcon}\n </span>\n )}\n </button>\n )\n}\n\n\n/**\n * A button for icons with a solid background and different sizes\n */\nconst IconButton = ({\n children,\n color = 'primary',\n size = 'medium',\n className,\n ...restProps\n }: IconButtonProps) => {\n const colorClasses = {\n primary: 'not-disabled:bg-button-solid-primary-background not-disabled:text-button-solid-primary-text',\n secondary: 'not-disabled:bg-button-solid-secondary-background not-disabled:text-button-solid-secondary-text',\n tertiary: 'not-disabled:bg-button-solid-tertiary-background not-disabled:text-button-solid-tertiary-text',\n positive: 'not-disabled:bg-button-solid-positive-background not-disabled:text-button-solid-positive-text',\n warning: 'not-disabled:bg-button-solid-warning-background not-disabled:text-button-solid-warning-text',\n negative: 'not-disabled:bg-button-solid-negative-background not-disabled:text-button-solid-negative-text',\n neutral: 'not-disabled:bg-button-solid-neutral-background not-disabled:text-button-solid-neutral-text',\n transparent: 'not-disabled:bg-transparent',\n }[color]\n\n return (\n <button\n className={clsx(\n colorClasses,\n 'not-disabled:hover:brightness-90',\n 'disabled:text-disabled-text',\n {\n 'disabled:bg-disabled-background': color !== 'transparent',\n 'disabled:opacity-70': color === 'transparent',\n 'not-disabled:hover:bg-button-text-hover-background': color === 'transparent',\n },\n ButtonUtil.iconPaddingMapping[size],\n className\n )}\n {...restProps}\n >\n {children}\n </button>\n )\n}\n\nexport { SolidButton, OutlineButton, TextButton, IconButton }\n","import { useEffect, useRef, useState } from 'react'\nimport { Scrollbars } from 'react-custom-scrollbars-2'\nimport { noop } from '../../util/noop'\nimport { closestMatch, range } from '../../util/array'\nimport clsx from 'clsx'\n\ntype MinuteIncrement = '1min' | '5min' | '10min' | '15min' | '30min'\n\nexport type TimePickerProps = {\n time?: Date,\n onChange?: (time: Date) => void,\n is24HourFormat?: boolean,\n minuteIncrement?: MinuteIncrement,\n maxHeight?: number,\n className?: string,\n}\n\nexport const TimePicker = ({\n time = new Date(),\n onChange = noop,\n is24HourFormat = true,\n minuteIncrement = '5min',\n maxHeight = 300,\n className = ''\n }: TimePickerProps) => {\n const minuteRef = useRef<HTMLButtonElement>(null)\n const hourRef = useRef<HTMLButtonElement>(null)\n\n const isPM = time.getHours() >= 11\n const hours = is24HourFormat ? range(24) : range([1, 12], { exclusiveEnd: false })\n let minutes = range(60)\n\n useEffect(() => {\n const scrollToItem = () => {\n if (minuteRef.current) {\n const container = minuteRef.current.parentElement!\n\n const hasOverflow = container.scrollHeight > maxHeight\n if (hasOverflow) {\n minuteRef.current.scrollIntoView({\n behavior: 'instant',\n block: 'nearest',\n })\n }\n }\n }\n scrollToItem()\n }, [minuteRef, minuteRef.current]) // eslint-disable-line\n\n useEffect(() => {\n const scrollToItem = () => {\n if (hourRef.current) {\n const container = hourRef.current.parentElement!\n\n const hasOverflow = container.scrollHeight > maxHeight\n if (hasOverflow) {\n hourRef.current.scrollIntoView({\n behavior: 'instant',\n block: 'nearest',\n })\n }\n }\n }\n scrollToItem()\n }, [hourRef, hourRef.current]) // eslint-disable-line\n\n switch (minuteIncrement) {\n case '5min':\n minutes = minutes.filter(value => value % 5 === 0)\n break\n case '10min':\n minutes = minutes.filter(value => value % 10 === 0)\n break\n case '15min':\n minutes = minutes.filter(value => value % 15 === 0)\n break\n case '30min':\n minutes = minutes.filter(value => value % 30 === 0)\n break\n }\n\n const closestMinute = closestMatch(minutes, (item1, item2) => Math.abs(item1 - time.getMinutes()) < Math.abs(item2 - time.getMinutes()))\n\n const style = (selected: boolean) => clsx('chip-full hover:brightness-90 hover:bg-primary hover:text-on-primary rounded-md mr-3',\n { 'bg-primary text-on-primary': selected, 'bg-white text-black': !selected })\n\n const onChangeWrapper = (transformer: (newDate: Date) => void) => {\n const newDate = new Date(time)\n transformer(newDate)\n onChange(newDate)\n }\n\n return (\n <div className={clsx('flex-row-2 w-fit min-w-[150px] select-none', className)}>\n <Scrollbars autoHeight autoHeightMax={maxHeight} style={{ height: '100%' }}>\n <div className=\"flex-col-1 h-full\">\n {hours.map(hour => {\n const currentHour = hour === time.getHours() - (!is24HourFormat && isPM ? 12 : 0)\n return (\n <button\n key={hour}\n ref={currentHour ? hourRef : undefined}\n className={style(currentHour)}\n onClick={() => onChangeWrapper(newDate => newDate.setHours(hour + (!is24HourFormat && isPM ? 12 : 0)))}\n >\n {hour.toString().padStart(2, '0')}\n </button>\n )\n })}\n </div>\n </Scrollbars>\n <Scrollbars autoHeight autoHeightMax={maxHeight} style={{ height: '100%' }}>\n <div className=\"flex-col-1 h-full\">\n {minutes.map(minute => {\n const currentMinute = minute === closestMinute\n return (\n <button\n key={minute + minuteIncrement} // minute increment so that scroll works\n ref={currentMinute ? minuteRef : undefined}\n className={style(currentMinute)}\n onClick={() => onChangeWrapper(newDate => newDate.setMinutes(minute))}\n >\n {minute.toString().padStart(2, '0')}\n </button>\n )\n })}\n </div>\n </Scrollbars>\n {!is24HourFormat && (\n <div className=\"flex-col-1\">\n <button\n className={style(!isPM)}\n onClick={() => onChangeWrapper(newDate => isPM && newDate.setHours(newDate.getHours() - 12))}\n >\n AM\n </button>\n <button\n className={style(isPM)}\n onClick={() => onChangeWrapper(newDate => !isPM && newDate.setHours(newDate.getHours() + 12))}\n >\n PM\n </button>\n </div>\n )}\n </div>\n )\n}\n\nexport const TimePickerUncontrolled = ({\n time,\n onChange = noop,\n ...props\n }: TimePickerProps) => {\n const [value, setValue] = useState(time)\n useEffect(() => setValue(time), [time])\n\n return (\n <TimePicker\n {...props}\n time={value}\n onChange={time1 => {\n setValue(time1)\n onChange(time1)\n }}\n />\n )\n}\n","import { useEffect, useState } from 'react'\nimport { ArrowDown, ArrowUp, ChevronDown } from 'lucide-react'\nimport { useLocale } from '../../localization/LanguageProvider'\nimport type { PropsForTranslation } from '../../localization/useTranslation'\nimport { useTranslation } from '../../localization/useTranslation'\nimport { noop } from '../../util/noop'\nimport { addDuration, isInTimeSpan, subtractDuration } from '../../util/date'\nimport clsx from 'clsx'\nimport { SolidButton, TextButton } from '../user-action/Button'\nimport type { YearMonthPickerProps } from './YearMonthPicker'\nimport { YearMonthPicker } from './YearMonthPicker'\nimport type { DayPickerProps } from './DayPicker'\nimport { DayPicker } from './DayPicker'\nimport type { TimeTranslationType } from '../../localization/defaults/time'\nimport { timeTranslation } from '../../localization/defaults/time'\n\ntype DatePickerTranslationType = TimeTranslationType\n\ntype DisplayMode = 'yearMonth' | 'day'\n\nexport type DatePickerProps = {\n value?: Date,\n start?: Date,\n end?: Date,\n initialDisplay?: DisplayMode,\n onChange?: (date: Date) => void,\n dayPickerProps?: Omit<DayPickerProps, 'displayedMonth' | 'onChange' | 'selected'>,\n yearMonthPickerProps?: Omit<YearMonthPickerProps, 'displayedYearMonth' | 'onChange' | 'start' | 'end'>,\n className?: string,\n}\n\n/**\n * A Component for picking a date\n */\nexport const DatePicker = ({\n overwriteTranslation,\n value = new Date(),\n start = subtractDuration(new Date(), { years: 50 }),\n end = addDuration(new Date(), { years: 50 }),\n initialDisplay = 'day',\n onChange = noop,\n yearMonthPickerProps,\n dayPickerProps,\n className = ''\n }: PropsForTranslation<DatePickerTranslationType, DatePickerProps>) => {\n const locale = useLocale()\n const translation = useTranslation([timeTranslation], overwriteTranslation)\n const [displayedMonth, setDisplayedMonth] = useState<Date>(value)\n const [displayMode, setDisplayMode] = useState<DisplayMode>(initialDisplay)\n\n useEffect(() => {\n setDisplayedMonth(value)\n }, [value])\n\n return (\n <div className={clsx('flex-col-4', className)}>\n <div className=\"flex-row-2 items-center justify-between h-7\">\n <TextButton\n className={clsx('flex-row-1 items-center cursor-pointer select-none', {\n 'text-disabled-text': displayMode !== 'day',\n })}\n onClick={() => setDisplayMode(displayMode === 'day' ? 'yearMonth' : 'day')}\n >\n {`${new Intl.DateTimeFormat(locale, { month: 'long' }).format(displayedMonth)} ${displayedMonth.getFullYear()}`}\n <ChevronDown size={16}/>\n </TextButton>\n {displayMode === 'day' && (\n <div className=\"flex-row-2 justify-end\">\n <SolidButton\n size=\"small\"\n color=\"primary\"\n disabled={!isInTimeSpan(subtractDuration(displayedMonth, { months: 1 }), start, end)}\n onClick={() => {\n setDisplayedMonth(subtractDuration(displayedMonth, { months: 1 }))\n }}\n >\n <ArrowUp size={20}/>\n </SolidButton>\n <SolidButton\n size=\"small\"\n color=\"primary\"\n disabled={!isInTimeSpan(addDuration(displayedMonth, { months: 1 }), start, end)}\n onClick={() => {\n setDisplayedMonth(addDuration(displayedMonth, { months: 1 }))\n }}\n >\n <ArrowDown size={20}/>\n </SolidButton>\n </div>\n )}\n </div>\n {displayMode === 'yearMonth' ? (\n <YearMonthPicker\n {...yearMonthPickerProps}\n displayedYearMonth={value}\n start={start}\n end={end}\n onChange={newDate => {\n setDisplayedMonth(newDate)\n setDisplayMode('day')\n }}\n />\n ) : (\n <div>\n <DayPicker\n {...dayPickerProps}\n displayedMonth={displayedMonth}\n start={start}\n end={end}\n selected={value}\n onChange={date => {\n onChange(date)\n }}\n />\n <div className=\"mt-2\">\n <TextButton\n color=\"primary\"\n onClick={() => {\n const newDate = new Date()\n newDate.setHours(value.getHours(), value.getMinutes())\n onChange(newDate)\n }}\n >\n {translation('today')}\n </TextButton>\n </div>\n </div>\n )}\n </div>\n )\n}\n\n/**\n * Example for the Date Picker\n */\nexport const DatePickerUncontrolled = ({\n value = new Date(),\n onChange = noop,\n ...props\n }: DatePickerProps) => {\n const [date, setDate] = useState<Date>(value)\n\n useEffect(() => setDate(value), [value])\n\n return (\n <DatePicker\n {...props}\n value={date}\n onChange={date1 => {\n setDate(date1)\n onChange(date1)\n }}\n />\n )\n}\n","import { useEffect, useRef, useState } from 'react'\nimport { Scrollbars } from 'react-custom-scrollbars-2'\nimport { noop } from '../../util/noop'\nimport { equalSizeGroups, range } from '../../util/array'\nimport clsx from 'clsx'\nimport { ExpandableUncontrolled } from '../layout-and-navigation/Expandable'\nimport { addDuration, monthsList, subtractDuration } from '../../util/date'\nimport { useLocale } from '../../localization/LanguageProvider'\nimport { SolidButton } from '../user-action/Button'\n\nexport type YearMonthPickerProps = {\n displayedYearMonth?: Date,\n start?: Date,\n end?: Date,\n onChange?: (date: Date) => void,\n className?: string,\n maxHeight?: number,\n showValueOpen?: boolean,\n}\n\n// TODO use a dynamically loading infinite list here\nexport const YearMonthPicker = ({\n displayedYearMonth = new Date(),\n start = subtractDuration(new Date(), { years: 50 }),\n end = addDuration(new Date(), { years: 50 }),\n onChange = noop,\n className = '',\n maxHeight = 300,\n showValueOpen = true\n }: YearMonthPickerProps) => {\n const locale = useLocale()\n const ref = useRef<HTMLDivElement>(null)\n\n useEffect(() => {\n const scrollToItem = () => {\n if (ref.current) {\n ref.current.scrollIntoView({\n behavior: 'instant',\n block: 'center',\n })\n }\n }\n\n scrollToItem()\n }, [ref])\n\n if (end < start) {\n console.error(`startYear: (${start}) less than endYear: (${end})`)\n return null\n }\n\n const years = range([start.getFullYear(), end.getFullYear()], { exclusiveEnd: false })\n\n return (\n <div className={clsx('flex-col-0 select-none', className)}>\n <Scrollbars autoHeight autoHeightMax={maxHeight} style={{ height: '100%' }}>\n <div className=\"flex-col-1 mr-3\">\n {years.map(year => {\n const selectedYear = displayedYearMonth.getFullYear() === year\n return (\n <ExpandableUncontrolled\n key={year}\n ref={(displayedYearMonth.getFullYear() ?? new Date().getFullYear()) === year ? ref : undefined}\n label={<span className={clsx({ 'text-primary font-bold': selectedYear })}>{year}</span>}\n isExpanded={showValueOpen && selectedYear}\n contentClassName=\"gap-y-1\"\n >\n {equalSizeGroups([...monthsList], 3).map((monthList, index) => (\n <div key={index} className=\"flex-row-1\">\n {monthList.map(month => {\n const monthIndex = monthsList.indexOf(month)\n const newDate = new Date(year, monthIndex)\n\n const selectedMonth = selectedYear && monthIndex === displayedYearMonth.getMonth()\n const firstOfMonth = new Date(year, monthIndex, 1)\n const lastOfMonth = new Date(year, monthIndex, 1)\n const isAfterStart = start === undefined || start <= addDuration(subtractDuration(lastOfMonth, { days: 1 }), { months: 1 })\n const isBeforeEnd = end === undefined || firstOfMonth <= end\n const isValid = isAfterStart && isBeforeEnd\n return (\n <SolidButton\n key={month}\n disabled={!isValid}\n color={selectedMonth && isValid ? 'primary' : 'neutral'}\n className=\"flex-1\"\n size=\"small\"\n onClick={() => {\n onChange(newDate)\n }}\n >\n {new Intl.DateTimeFormat(locale, { month: 'short' }).format(newDate)}\n </SolidButton>\n )\n })}\n </div>\n ))}\n </ExpandableUncontrolled>\n )\n })}\n </div>\n </Scrollbars>\n </div>\n )\n}\n\nexport const YearMonthPickerUncontrolled = ({\n displayedYearMonth,\n onChange = noop,\n