@helpwave/hightide
Version:
helpwave's component and theming library
1 lines • 105 kB
Source Map (JSON)
{"version":3,"sources":["../../../src/components/user-action/MultiSelect.tsx","../../../src/localization/LanguageProvider.tsx","../../../src/hooks/useLocalStorage.ts","../../../src/localization/util.ts","../../../src/localization/useTranslation.ts","../../../src/components/user-action/Label.tsx","../../../src/components/user-action/Button.tsx","../../../src/components/layout-and-navigation/Chip.tsx","../../../src/localization/defaults/form.ts","../../../src/components/user-action/Menu.tsx","../../../src/hooks/useOutsideClick.ts","../../../src/hooks/useHoverState.ts","../../../src/util/PropsWithFunctionChildren.ts","../../../src/hooks/usePopoverPosition.ts","../../../src/components/layout-and-navigation/Expandable.tsx","../../../src/util/noop.ts","../../../src/components/user-action/Input.tsx","../../../src/hooks/useDelay.ts","../../../src/hooks/useFocusManagement.ts","../../../src/hooks/useFocusOnceVisible.ts","../../../src/components/user-action/SearchBar.tsx","../../../src/hooks/useSearch.ts","../../../src/util/simpleSearch.ts","../../../src/components/user-action/Checkbox.tsx","../../../src/components/layout-and-navigation/Tile.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport { useCallback } from 'react'\nimport { useEffect, useState } from 'react'\nimport type { PropsForTranslation, Translation } from '../../localization/useTranslation'\nimport { useTranslation } from '../../localization/useTranslation'\nimport clsx from 'clsx'\nimport type { LabelProps } from './Label'\nimport { Label } from './Label'\nimport type { SelectOption } from './Select'\nimport { SolidButton } from './Button'\nimport { ChipList } from '../layout-and-navigation/Chip'\nimport type { FormTranslationType } from '../../localization/defaults/form'\nimport { formTranslation } from '../../localization/defaults/form'\nimport type { MenuBag, MenuProps } from './Menu'\nimport { Menu } from './Menu'\nimport { ExpansionIcon } from '../layout-and-navigation/Expandable'\nimport { SearchBar } from './SearchBar'\nimport type { UseSearchProps } from '../../hooks/useSearch'\nimport { useSearch } from '../../hooks/useSearch'\nimport { Checkbox } from './Checkbox'\nimport { Plus } from 'lucide-react'\nimport { ListTile } from '../layout-and-navigation/Tile'\n\ntype MultiSelectAddonTranslation = {\n selected: string,\n}\n\ntype MultiSelectTranslation = MultiSelectAddonTranslation & FormTranslationType\n\nconst defaultMultiSelectTranslation: Translation<MultiSelectAddonTranslation> = {\n en: {\n selected: `{{amount}} selected`\n },\n de: {\n selected: `{{amount}} ausgewählt`\n }\n}\n\nexport type MultiSelectOption<T> = SelectOption<T> & {\n selected: boolean,\n}\n\nexport type MultiSelectBag = MenuBag & {\n search: string,\n}\n\n\nexport type MultiSelectProps<T> = Omit<MenuProps<HTMLButtonElement>, 'trigger' | 'children'> & {\n options: MultiSelectOption<T>[],\n label?: LabelProps,\n onChange: (options: MultiSelectOption<T>[]) => void,\n hintText?: string,\n selectedDisplayOverwrite?: ReactNode,\n searchOptions?: Omit<UseSearchProps<SelectOption<T>>, 'list' | 'searchMapping'>,\n additionalItems?: (bag: MultiSelectBag) => ReactNode,\n useChipDisplay?: boolean,\n className?: string,\n triggerClassName?: string,\n hintTextClassName?: string,\n}\n\n/**\n * A Component for multi selection\n */\nexport const MultiSelect = <T, >({\n overwriteTranslation,\n label,\n options,\n onChange,\n hintText,\n selectedDisplayOverwrite,\n searchOptions,\n additionalItems,\n useChipDisplay = false,\n className,\n triggerClassName,\n hintTextClassName,\n ...menuProps\n }:\n PropsForTranslation<MultiSelectTranslation, MultiSelectProps<T>>\n) => {\n const translation = useTranslation([formTranslation, defaultMultiSelectTranslation], overwriteTranslation)\n const { result, search, setSearch } = useSearch<MultiSelectOption<T>>({\n list: options,\n searchMapping: useCallback((item: MultiSelectOption<T>) => item.searchTags, []),\n ...searchOptions,\n })\n\n const selectedItems = options.filter(value => value.selected)\n\n const isShowingHint = !selectedDisplayOverwrite && selectedItems.length === 0\n\n return (\n <div className={clsx(className)}>\n {label && (\n <Label\n {...label}\n htmlFor={label.name}\n className={clsx(' mb-1', label.className)}\n labelType={label.labelType ?? 'labelSmall'}\n />\n )}\n <Menu<HTMLButtonElement>\n {...menuProps}\n trigger={({ toggleOpen, isOpen, disabled }, ref) => (\n <button\n ref={ref}\n className={clsx(\n 'group btn-md justify-between w-full border-2 h-auto',\n 'not-disabled:bg-input-background not-disabled:text-input-text not-disabled:hover:border-primary',\n 'disabled:bg-disabled-background disabled:text-disabled-text disabled:border-disabled-border',\n {\n 'min-h-14': useChipDisplay,\n },\n triggerClassName\n )}\n onClick={toggleOpen}\n disabled={disabled}\n >\n {useChipDisplay ? (\n <>\n {isShowingHint ? (\n <div\n className={clsx('icon-btn-sm ',\n {\n 'bg-button-solid-neutral-background text-button-solid-neutral-text hover:brightness-90 group-hover:brightness-90': !disabled,\n 'bg-disabled-background text-disabled-text': disabled,\n })}\n >\n <Plus/>\n </div>\n ) : (\n <ChipList list={selectedItems.map(value => ({ children: value.label }))}/>\n )}\n </>\n ) : (\n <>\n {!isShowingHint && (\n <span className=\"font-semibold\">\n {selectedDisplayOverwrite ?? translation('selected', { replacements: { amount: selectedItems.length.toString() } })}\n </span>\n )}\n {isShowingHint && (\n <span className={clsx('textstyle-description', hintTextClassName)}>\n {hintText ?? translation('select')}\n </span>\n )}\n <ExpansionIcon isExpanded={isOpen}/>\n </>\n )}\n </button>\n )}\n menuClassName={clsx('flex-col-2 p-2 max-h-96 overflow-hidden', menuProps.menuClassName)}\n >\n {(bag) => {\n const { close } = bag\n return (\n <>\n {!searchOptions?.disabled && (\n <SearchBar\n value={search}\n onChangeText={setSearch}\n autoFocus={true}\n />\n )}\n <div className=\"flex-col-2 overflow-y-auto\">\n {result.map((option, index) => {\n const update = () => {\n onChange(options.map(value => value.value === option.value ? ({\n ...option,\n selected: !value.selected\n }) : value))\n }\n return (\n <ListTile\n key={index}\n prefix={(\n <Checkbox\n checked={option.selected}\n onChange={update} size=\"small\"\n disabled={option.disabled}\n />\n )}\n title={option.label}\n onClick={update}\n disabled={option.disabled}\n />\n )\n })}\n {additionalItems && additionalItems({ ...bag, search })}\n </div>\n <div className=\"flex-row-2 justify-between\">\n <div className=\"flex-row-2\">\n <SolidButton\n color=\"neutral\"\n size=\"small\"\n onClick={() => {\n onChange(options.map(option => ({\n ...option,\n selected: !option.disabled\n })))\n }}\n disabled={options.every(value => value.selected || value.disabled)}\n >\n {translation('all')}\n </SolidButton>\n <SolidButton\n color=\"neutral\"\n size=\"small\"\n onClick={() => {\n onChange(options.map(option => ({\n ...option,\n selected: false\n })))\n }}\n >\n {translation('none')}\n </SolidButton>\n </div>\n <SolidButton size=\"small\" onClick={close}>Done</SolidButton>\n </div>\n </>\n )\n }}\n </Menu>\n </div>\n )\n}\n\nexport const MultiSelectUncontrolled = <T, >({\n options,\n onChange,\n ...props\n }:\n PropsForTranslation<MultiSelectTranslation, MultiSelectProps<T>>) => {\n const [usedOptions, setUsedOptions] = useState<MultiSelectOption<T>[]>(options)\n\n useEffect(() => {\n setUsedOptions(options)\n }, [options])\n\n return (\n <MultiSelect\n {...props}\n options={usedOptions}\n onChange={options => {\n setUsedOptions(options)\n onChange(options)\n }}\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}","import type { LabelHTMLAttributes } from 'react'\nimport clsx from 'clsx'\n\nexport type LabelType = 'labelSmall' | 'labelMedium' | 'labelBig'\n\nconst styleMapping: Record<LabelType, string> = {\n labelSmall: 'textstyle-label-sm',\n labelMedium: 'textstyle-label-md',\n labelBig: 'textstyle-label-lg',\n}\n\nexport type LabelProps = {\n /** The text for the label */\n name?: string,\n /** The styling for the label */\n labelType?: LabelType,\n} & LabelHTMLAttributes<HTMLLabelElement>\n\n/**\n * A Label component\n */\nexport const Label = ({\n children,\n name,\n labelType = 'labelSmall',\n className,\n ...props\n }: LabelProps) => {\n return (\n <label {...props} className={clsx(styleMapping[labelType], className)}>\n {children ? children : name}\n </label>\n )\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 type { HTMLProps, PropsWithChildren, ReactNode } from 'react'\nimport clsx from 'clsx'\n\nconst chipColors = ['default', 'dark', 'red', 'yellow', 'green', 'blue', 'pink'] as const\nexport type ChipColor = typeof chipColors[number]\n\ntype ChipVariant = 'normal' | 'fullyRounded'\n\nexport const ChipUtil = {\n colors: chipColors,\n}\n\nexport type ChipProps = HTMLProps<HTMLDivElement> & PropsWithChildren<{\n color?: ChipColor,\n variant?: ChipVariant,\n trailingIcon?: ReactNode,\n}>\n\n/**\n * A component for displaying a single chip\n */\nexport const Chip = ({\n children,\n trailingIcon,\n color = 'default',\n variant = 'normal',\n className = '',\n ...restProps\n }: ChipProps) => {\n const colorMapping: string = {\n default: 'text-tag-default-text bg-tag-default-background',\n dark: 'text-tag-dark-text bg-tag-dark-background',\n red: 'text-tag-red-text bg-tag-red-background',\n yellow: 'text-tag-yellow-text bg-tag-yellow-background',\n green: 'text-tag-green-text bg-tag-green-background',\n blue: 'text-tag-blue-text bg-tag-blue-background',\n pink: 'text-tag-pink-text bg-tag-pink-background',\n }[color]\n\n const colorMappingIcon: string = {\n default: 'text-tag-default-icon',\n dark: 'text-tag-dark-icon',\n red: 'text-tag-red-icon',\n yellow: 'text-tag-yellow-icon',\n green: 'text-tag-green-icon',\n blue: 'text-tag-blue-icon',\n pink: 'text-tag-pink-icon',\n }[color]\n\n return (\n <div\n {...restProps}\n className={clsx(\n `row w-fit px-2 py-1 font-semibold`,\n colorMapping,\n {\n 'rounded-md': variant === 'normal',\n 'rounded-full': variant === 'fullyRounded',\n },\n className\n )}\n >\n {children}\n {trailingIcon && (<span className={colorMappingIcon}>{trailingIcon}</span>)}\n </div>\n )\n}\n\nexport type ChipListProps = {\n list: ChipProps[],\n className?: string,\n}\n\n/**\n * A component for displaying a list of chips\n */\nexport const ChipList = ({\n list,\n className = ''\n }: ChipListProps) => {\n return (\n <div className={clsx('flex flex-wrap gap-x-2 gap-y-2', className)}>\n {list.map((value, index) => (\n <Chip\n key={index}\n {...value}\n color={value.color ?? 'default'}\n variant={value.variant ?? 'normal'}\n >\n {value.children}\n </Chip>\n ))}\n </div>\n )\n}\n","import type { Translation } from '../useTranslation'\n\nexport type FormTranslationType = {\n add: string,\n all: string,\n apply: string,\n back: string,\n cancel: string,\n change: string,\n clear: string,\n click: string,\n clickToCopy: string,\n close: string,\n confirm: string,\n copy: string,\n copied: string,\n create: string,\n decline: string,\n delete: string,\n discard: string,\n discardChanges: string,\n done: string,\n edit: string,\n enterText: string,\n error: string,\n exit: string,\n fieldRequiredError: string,\n invalidEmailError: string,\n less: string,\n loading: string,\n maxLengthError: string,\n minLengthError: string,\n more: string,\n next: string,\n no: string,\n none: string,\n of: string,\n optional: string,\n pleaseWait: string,\n previous: string,\n remove: string,\n required: string,\n reset: string,\n save: string,\n saved: string,\n search: string,\n select: string,\n selectOption: string,\n show: string,\n showMore: string,\n showLess: string,\n submit: string,\n success: string,\n unsavedChanges: string,\n unsavedChangesSaveQuestion: string,\n update: string,\n yes: string,\n}\n\nexport const formTranslation: Translation<FormTranslationType> = {\n en: {\n add: 'Add',\n all: 'All',\n apply: 'Apply',\n back: 'Back',\n cancel: 'Cancel',\n change: 'Change',\n clear: 'Clear',\n click: 'Click',\n clickToCopy: 'Click to Copy',\n close: 'Close',\n confirm: 'Confirm',\n copy: 'Copy',\n copied: 'Copied',\n create: 'Create',\n decline: 'Decline',\n delete: 'Delete',\n discard: 'Discard',\n discardChanges: 'Discard Changes',\n done: 'Done',\n edit: 'Edit',\n enterText: 'Enter text here',\n error: 'Error',\n exit: 'Exit',\n fieldRequiredError: 'This field is required.',\n invalidEmailError: 'Please enter a valid email address.',\n less: 'Less',\n loading: 'Loading',\n maxLengthError: 'Maximum length exceeded.',\n minLengthError: 'Minimum length not met.',\n more: 'More',\n next: 'Next',\n no: 'No',\n none: 'None',\n of: 'of',\n optional: 'Optional',\n pleaseWait: 'Please wait...',\n previous: 'Previous',\n remove: 'Remove',\n required: 'Required',\n reset: 'Reset',\n save: 'Save',\n saved: 'Saved',\n search: 'Search',\n select: 'Select',\n selectOption: 'Select an option',\n show: 'Show',\n showMore: 'Show more',\n showLess: 'Show less',\n submit: 'Submit',\n success: 'Success',\n update: 'Update',\n unsavedChanges: 'Unsaved Changes',\n unsavedChangesSaveQuestion: 'Do you want to save your changes?',\n yes: 'Yes',\n },\n de: {\n add: 'Hinzufügen',\n all: 'Alle',\n apply: 'Anwenden',\n back: 'Zurück',\n cancel: 'Abbrechen',\n change: 'Ändern',\n clear: 'Löschen',\n click: 'Klicken',\n clickToCopy: 'Zum kopieren klicken',\n close: 'Schließen',\n confirm: 'Bestätigen',\n copy: 'Kopieren',\n copied: 'Kopiert',\n create: 'Erstellen',\n decline: 'Ablehnen',\n delete: 'Löschen',\n discard: 'Verwerfen',\n discardChanges: 'Änderungen Verwerfen',\n done: 'Fertig',\n edit: 'Bearbeiten',\n enterText: 'Text hier eingeben',\n error: 'Fehler',\n exit: 'Beenden',\n fieldRequiredError: 'Dieses Feld ist erforderlich.',\n invalidEmailError: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',\n less: 'Weniger',\n loading: 'Lädt',\n maxLengthError: 'Maximale Länge überschritten.',\n minLengthError: 'Mindestlänge nicht erreicht.',\n more: 'Mehr',\n next: 'Weiter',\n no: 'Nein',\n none: 'Nichts',\n of: 'von',\n optional: 'Optional',\n pleaseWait: 'Bitte warten...',\n previous: 'Vorherige',\n remove: 'Entfernen',\n required: 'Erforderlich',\n reset: 'Zurücksetzen',\n save: 'Speichern',\n saved: 'Gespeichert',\n search: 'Suche',\n select: 'Select',\n selectOption: 'Option auswählen',\n show: 'Anzeigen',\n showMore: 'Mehr anzeigen',\n showLess: 'Weniger anzeigen',\n submit: 'Abschicken',\n success: 'Erfolg',\n update: 'Update',\n unsavedChanges: 'Ungespeicherte Änderungen',\n unsavedChangesSaveQuestion: 'Möchtest du die Änderungen speichern?',\n yes: 'Ja',\n }\n}\n","import { type PropsWithChildren, type ReactNode, type RefObject, useEffect, useRef, useState } from 'react'\nimport clsx from 'clsx'\nimport { useOutsideClick } from '../../hooks/useOutsideClick'\nimport { useHoverState } from '../../hooks/useHoverState'\nimport type { PropsWithBagFunctionOrChildren } from '../../util/PropsWithFunctionChildren'\nimport { BagFunctionUtil } from '../../util/PropsWithFunctionChildren'\nimport type { PopoverHorizontalAlignment, PopoverVerticalAlignment } from '../../hooks/usePopoverPosition'\nimport { usePopoverPosition } from '../../hooks/usePopoverPosition'\nimport { createPortal } from 'react-dom'\n\nexport type MenuItemProps = {\n onClick?: () => void,\n alignment?: 'left' | 'right',\n isDisabled?: boolean,\n className?: string,\n}\nexport const MenuItem = ({\n children,\n onClick,\n alignment = 'left',\n isDisabled = false,\n className\n }: PropsWithChildren<MenuItemProps>) => (\n <div\n className={clsx('block px-3 py-1.5 first:rounded-t-md last:rounded-b-md text-sm font-semibold text-nowrap', {\n 'text-right': alignment === 'right',\n 'text-left': alignment === 'left',\n 'text-disabled-text cursor-not-allowed': isDisabled,\n 'text-menu-text hover:bg-primary/20': !isDisabled,\n 'cursor-pointer': !!onClick,\n }, className)}\n onClick={onClick}\n >\n {children}\n </div>\n)\n\nfunction getScrollableParents(element) {\n const scrollables = []\n let parent = element.parentElement\n while (parent) {\n scrollables.push(parent)\n parent = parent.parentElement\n }\n return scrollables\n}\n\nexport type MenuBag = {\n isOpen: boolean,\n disabled: boolean,\n toggleOpen: () => void,\n close: () => void,\n}\n\nexport type MenuProps<T> = PropsWithBagFunctionOrChildren<MenuBag> & {\n trigger: (bag: MenuBag, ref: RefObject<T>) => ReactNode,\n /**\n * @default 'l'\n */\n alignmentHorizontal?: PopoverHorizontalAlignment,\n alignmentVertical?: PopoverVerticalAlignment,\n showOnHover?: boolean,\n menuClassName?: string,\n disabled?: boolean,\n}\n\n/**\n * A Menu Component to allow the user to see different functions\n */\nexport const Menu = <T extends HTMLElement>({\n trigger,\n children,\n alignmentHorizontal = 'leftInside',\n alignmentVertical = 'bottomOutside',\n showOnHover = false,\n disabled = false,\n menuClassName = '',\n }: MenuProps<T>) => {\n const { isHovered: isOpen, setIsHovered: setIsOpen } = useHoverState({ isDisabled: !showOnHover || disabled })\n const triggerRef = useRef<T>(null)\n const menuRef = useRef<HTMLDivElement>(null)\n useOutsideClick([triggerRef, menuRef], () => setIsOpen(false))\n\n const [isHidden, setIsHidden] = useState<boolean>(true)\n const bag: MenuBag = {\n isOpen,\n close: () => setIsOpen(false),\n toggleOpen: () => setIsOpen(prevState => !prevState),\n disabled,\n }\n\n const menuPosition = usePopoverPosition(\n triggerRef.current?.getBoundingClientRect(),\n { verticalAlignment: alignmentVertical, horizontalAlignment: alignmentHorizontal, disabled }\n )\n\n useEffect(() => {\n if (!isOpen) return\n\n const triggerEl = triggerRef.current\n if (!triggerEl) return\n\n const scrollableParents = getScrollableParents(triggerEl)\n\n const close = () => setIsOpen(false)\n scrollableParents.forEach((parent) => {\n parent.addEventListener('scroll', close)\n })\n window.addEventListener('resize', close)\n\n return () => {\n scrollableParents.forEach((parent) => {\n parent.removeEventListener('scroll', close)\n })\n window.removeEventListener('resize', close)\n }\n }, [isOpen, setIsOpen])\n\n useEffect(() => {\n if (isOpen) {\n setIsHidden(false)\n }\n }, [isOpen])\n\n return (\n <>\n {trigger(bag, triggerRef)}\n {createPortal((\n <div\n ref={menuRef}\n onClick={e => e.stopPropagation()}\n className={clsx(\n 'absolute rounded-md bg-menu-background text-menu-text shadow-around-lg shadow-strong z-[300]',\n {\n 'animate-pop-in': isOpen,\n 'animate-pop-out': !isOpen,\n 'hidden': isHidden,\n },\n menuClassName\n )}\n onAnimationEnd={() => {\n if (!isOpen) {\n setIsHidden(true)\n }\n }}\n style={{\n ...menuPosition\n }}\n >\n {BagFunctionUtil.resolve<MenuBag>(children, bag)}\n </div>\n ), document.body)}\n </>\n )\n}\n\n","import type { RefObject } from 'react'\nimport { useEffect } from 'react'\n\nexport const useOutsideClick = <Ts extends RefObject<HTMLElement>[]>(refs: Ts, handler: () => void) => {\n useEffect(() => {\n const listener = (event: MouseEvent | TouchEvent) => {\n // returning means not \"not clicking outside\"\n\n // if no target exists, return\n if (event.target === null) return\n // if the target is a ref's element or descendent thereof, return\n if (refs.some((ref) => !ref.current || ref.current.contains(event.target as Node))) {\n return\n }\n\n handler()\n }\n document.addEventListener('mousedown', listener)\n document.addEventListener('touchstart', listener)\n return () => {\n document.removeEventListener('mousedown', listener)\n document.removeEventListener('touchstart', listener)\n }\n }, [refs, handler])\n}\n","import type { Dispatch, SetStateAction } from 'react'\nimport { useEffect, useState } from 'react'\n\ntype UseHoverStateProps = {\n /**\n * The delay after which the menu is closed in milliseconds\n *\n * default: 200ms\n */\n closingDelay: number,\n /**\n * Whether the hover state management should be disabled\n *\n * default: false\n */\n isDisabled: boolean,\n}\n\ntype UseHoverStateReturnType = {\n /**\n * Whether the element is hovered\n */\n isHovered: boolean,\n /**\n * Function to change the current hover status\n */\n setIsHovered: Dispatch<SetStateAction<boolean>>,\n /**\n * Handlers to pass on to the component that should be hovered\n */\n handlers: {\n onMouseEnter: () => void,\n onMouseLeave: () => void,\n },\n}\n\nconst defaultUseHoverStateProps: UseHoverStateProps = {\n closingDelay: 200,\n isDisabled: false,\n}\n\n/**\n * @param props See UseHoverStateProps\n *\n * A react hook for managing the hover state of a component. The handlers provided should be\n * forwarded to the component which should be hovered over\n */\nexport const useHoverState = (props: Partial<UseHoverStateProps> | undefined = undefined): UseHoverStateReturnType => {\n const { closingDelay, isDisabled } = { ...defaultUseHoverStateProps, ...props }\n\n const [isHovered, setIsHovered] = useState(false)\n const [timer, setTimer] = useState<NodeJS.Timeout>()\n\n const onMouseEnter = () => {\n if (isDisabled) {\n return\n }\n clearTimeout(timer)\n setIsHovered(true)\n }\n\n const onMouseLeave = () => {\n if (isDisabled) {\n return\n }\n setTimer(setTimeout(() => {\n setIsHovered(false)\n }, closingDelay))\n }\n\n useEffect(() => {\n if (timer) {\n return () => {\n clearTimeout(timer)\n }\n }\n })\n\n useEffect(() => {\n if (timer) {\n clearTimeout(timer)\n }\n }, [isDisabled]) // eslint-disable-line react-hooks/exhaustive-deps\n\n return {\n isHovered, setIsHovered, handlers: { onMouseEnter, onMouseLeave }\n }\n}\n","import type { ReactNode } from 'react'\n\nexport type BagFunction<T> = (bag: T) => ReactNode\n\nexport type PropsWithBagFunction<T, P = unknown> = P & { children?: BagFunction<T> }\n\nexport type PropsWithBagFunctionOrChildren<T, P = unknown> = P & { children?: BagFunction<T> | ReactNode }\n\nconst resolve = <T>(children: BagFunction<T> | ReactNode, bag: T): ReactNode => {\n if (typeof children === 'function') {\n return (children as BagFunction<T>)(bag)\n }\n\n return children ?? undefined\n}\n\nexport const BagFunctionUtil = {\n resolve\n}","import type { CSSProperties } from 'react'\n\nexport type PopoverHorizontalAlignment = 'leftOutside' | 'leftInside' | 'rightOutside' | 'rightInside' | 'center'\nexport type PopoverVerticalAlignment = 'topOutside' | 'topInside' | 'bottomOutside' | 'bottomInside' | 'center'\n\ntype PopoverPositionOptionsResolved = {\n edgePadding: number,\n outerGap: number,\n verticalAlignment: PopoverVerticalAlignment,\n horizontalAlignment: PopoverHorizontalAlignment,\n disabled: boolean,\n}\n\ntype PopoverPositionOptions = Partial<PopoverPositionOptionsResolved>\n\nconst defaultPopoverPositionOptions: PopoverPositionOptionsResolved = {\n edgePadding: 16,\n outerGap: 4,\n horizontalAlignment: 'leftInside',\n verticalAlignment: 'bottomOutside',\n disabled: false,\n}\n\nexport const usePopoverPosition = (trigger?: DOMRect, options?: PopoverPositionOptions): CSSProperties => {\n const {\n edgePadding,\n outerGap,\n verticalAlignment,\n horizontalAlignment,\n disabled\n }: PopoverPositionOptionsResolved = { ...defaultPopoverPositionOptions, ...options }\n\n if (disabled || !trigger) {\n return {}\n }\n\n const left: number = {\n leftOutside: trigger.left - outerGap,\n leftInside: trigger.left,\n rightOutside: trigger.right + outerGap,\n rightInside: trigger.right,\n center: trigger.left + trigger.width / 2,\n }[horizontalAlignment]\n\n const top: number = {\n topOutside: trigger.top - outerGap,\n topInside: trigger.top,\n bottomOutside: trigger.bottom + outerGap,\n bottomInside: trigger.bottom,\n center: trigger.top + trigger.height / 2,\n }[verticalAlignment]\n\n const translateX: string | undefined = {\n leftOutside: '-100%',\n leftInside: undefined,\n rightOutside: undefined,\n rightInside: '-100%',\n center: '-50%',\n }[horizontalAlignment]\n\n const translateY: string | undefined = {\n topOutside: '-100%',\n topInside: undefined,\n bottomOutside: undefined,\n bottomInside: '-100%',\n center: '-50%',\n }[verticalAlignment]\n\n return {\n left: Math.max(left, edgePadding),\n top: Math.max(top, edgePadding),\n translate: [translateX ?? '0', translateY ?? '0'].join(' ')\n }\n}","import type { PropsWithChildren, ReactNode } from 'react'\nimport { forwardRef, useCallback, useEffect, useState } from 'react'\nimport { ChevronDown } from 'lucide-react'\nimport clsx from 'clsx'\nimport { noop } from '../../util/noop'\n\ntype IconBuilder = (expanded: boolean) => ReactNode\n\nexport type ExpandableProps = PropsWithChildren<{\n label: ReactNode,\n icon?: IconBuilder,\n isExpanded?: boolean,\n onChange?: (isExpanded: boolean) => void,\n /**\n * Whether the expansion should only happen when the header is clicked or on the entire component\n */\n clickOnlyOnHeader?: boolean,\n disabled?: boolean,\n className?: string,\n headerClassName?: string,\n contentClassName?: string,\n contentExpandedClassName?: string,\n}>\n\n\nexport type ExpansionIconProps = {\n isExpanded: boolean,\n className?: string,\n}\n\nexport const ExpansionIcon = ({ isExpanded, className }: ExpansionIconProps) => {\n return (\n <ChevronDown\n className={clsx(\n 'min-w-6 w-6 min-h-6 h-6 transition-transform duration-200 ease-in-out',\n { 'rotate-180': isExpanded },\n className\n )}\n />\n )\n}\n\n\n/**\n * A Component for showing and hiding content\n */\nexport const Expandable = forwardRef<HTMLDivElement, ExpandableP