@helpwave/hightide
Version:
helpwave's component and theming library
1 lines • 123 kB
Source Map (JSON)
{"version":3,"sources":["../../../src/components/table/Table.tsx","../../../src/components/layout-and-navigation/Pagination.tsx","../../../src/localization/LanguageProvider.tsx","../../../src/hooks/useLocalStorage.ts","../../../src/localization/util.ts","../../../src/localization/useTranslation.ts","../../../src/localization/defaults/form.ts","../../../src/components/user-action/Input.tsx","../../../src/hooks/useDelay.ts","../../../src/util/noop.ts","../../../src/components/user-action/Label.tsx","../../../src/hooks/useFocusManagement.ts","../../../src/hooks/useFocusOnceVisible.ts","../../../src/util/math.ts","../../../src/components/user-action/Button.tsx","../../../src/util/array.ts","../../../src/components/user-action/Checkbox.tsx","../../../src/components/table/TableFilterButton.tsx","../../../src/components/user-action/Menu.tsx","../../../src/hooks/useOutsideClick.ts","../../../src/hooks/useHoverState.ts","../../../src/util/PropsWithFunctionChildren.ts","../../../src/hooks/usePopoverPosition.ts","../../../src/components/table/TableSortButton.tsx","../../../src/components/table/FillerRowElement.tsx","../../../src/components/table/Filter.ts","../../../src/hooks/useResizeCallbackWrapper.ts","../../../src/components/table/TableCell.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { Pagination } from '../layout-and-navigation/Pagination'\nimport clsx from 'clsx'\nimport type {\n ColumnDef,\n ColumnFiltersState,\n ColumnSizingInfoState,\n ColumnSizingState,\n FilterFn,\n InitialTableState,\n PaginationState,\n Row,\n RowData,\n RowSelectionState,\n Table as ReactTable,\n TableOptions,\n TableState\n} from '@tanstack/react-table'\nimport {\n flexRender,\n getCoreRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n useReactTable\n} from '@tanstack/react-table'\nimport { range } from '../../util/array'\nimport { Scrollbars } from 'react-custom-scrollbars-2'\nimport { Checkbox } from '../user-action/Checkbox'\nimport { clamp } from '../../util/math'\nimport { noop } from '../../util/noop'\nimport type { TableFilterType } from './TableFilterButton'\nimport { TableFilterButton } from './TableFilterButton'\nimport { TableSortButton } from './TableSortButton'\nimport { FillerRowElement } from './FillerRowElement'\nimport { TableFilters } from './Filter'\nimport { useResizeCallbackWrapper } from '../../hooks/useResizeCallbackWrapper'\nimport { TableCell } from './TableCell'\n\ndeclare module '@tanstack/react-table' {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n interface ColumnMeta<TData extends RowData, TValue> {\n className?: string,\n filterType?: TableFilterType,\n }\n\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n interface TableMeta<TData> {\n headerRowClassName?: TableFilterType,\n bodyRowClassName?: string,\n }\n\n\n interface FilterFns {\n dateRange: FilterFn<unknown>,\n }\n}\n\nexport type TableProps<T> = {\n data: T[],\n columns: ColumnDef<T>[],\n fillerRow?: (columnId: string, table: ReactTable<T>) => ReactNode,\n initialState?: Omit<InitialTableState, 'columnSizing' | 'columnSizingInfo'>,\n className?: string,\n onRowClick?: (row: Row<T>, table: ReactTable<T>) => void,\n state?: Omit<TableState, 'columnSizing' | 'columnSizingInfo'>,\n tableClassName?: string,\n} & Partial<TableOptions<T>>\n\n/**\n * The standard table\n */\nexport const Table = <T, >({\n data,\n fillerRow,\n initialState,\n onRowClick = noop,\n className,\n tableClassName,\n defaultColumn,\n state,\n columns,\n ...tableOptions\n }: TableProps<T>) => {\n const ref = useRef<HTMLDivElement>(null)\n const tableRef = useRef<HTMLTableElement>(null)\n\n const [columnSizing, setColumnSizing] = useState<ColumnSizingState>(columns.reduce((previousValue, currentValue) => {\n return {\n ...previousValue,\n [currentValue.id]: currentValue.minSize ?? defaultColumn.minSize,\n }\n }, {}))\n const [columnSizingInfo, setColumnSizingInfo] = useState<ColumnSizingInfoState>()\n const [pagination, setPagination] = useState<PaginationState>({\n pageSize: 10,\n pageIndex: 0,\n ...initialState?.pagination\n })\n const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(initialState?.columnFilters)\n\n const computedColumnMinWidths = useMemo(() => {\n return columns.reduce((previousValue, column) => {\n return {\n ...previousValue,\n // every column is at least 12px wide\n [column.id]: (column.minSize ?? defaultColumn?.minSize ?? 12)\n }\n }, {})\n }, [columns, defaultColumn])\n\n const computedColumnMaxWidths = useMemo(() => {\n return columns.reduce((previousValue, column) => {\n return {\n ...previousValue,\n [column.id]: (column.maxSize ?? defaultColumn?.maxSize)\n }\n }, {})\n }, [columns, defaultColumn])\n\n const tableMinWidth = useMemo(() => {\n return columns.reduce((sum, column) => {\n return sum + computedColumnMinWidths[column.id]\n }, 0)\n }, [columns, computedColumnMinWidths])\n\n const updateColumnSizes = useMemo(() => {\n return (previous: ColumnSizingState) => {\n const updateSizing = {\n ...columnSizing,\n ...previous\n }\n\n const containerWidth = ref.current.offsetWidth\n\n // enforce min and max constraints\n columns.forEach((column) => {\n updateSizing[column.id] = clamp(updateSizing[column.id], computedColumnMinWidths[column.id], computedColumnMaxWidths[column.id] ?? containerWidth)\n })\n\n // table width of the current sizing\n const width = columns\n .reduce((previousValue, currentValue) => previousValue + updateSizing[currentValue.id], 0)\n\n if (width > containerWidth) {\n if (tableMinWidth >= containerWidth) {\n return columns.reduce((previousValue, currentValue) => ({\n ...previousValue,\n [currentValue.id]: computedColumnMinWidths[currentValue.id]\n }), {})\n }\n\n let reduceableColumns = columns\n .map(value => value.id)\n .filter(id => updateSizing[id] - computedColumnMinWidths[id] > 0)\n\n let spaceToReduce = width - containerWidth\n\n while (spaceToReduce > 0 && reduceableColumns.length > 0) {\n let maxReduceAmount = reduceableColumns.reduce((previousValue, id) => Math.max(previousValue, updateSizing[id] - computedColumnMinWidths[id]), 0)\n if (maxReduceAmount * reduceableColumns.length > spaceToReduce) {\n maxReduceAmount = spaceToReduce / reduceableColumns.length\n }\n\n reduceableColumns.forEach(id => {\n updateSizing[id] -= maxReduceAmount\n })\n\n spaceToReduce -= maxReduceAmount * reduceableColumns.length\n reduceableColumns = reduceableColumns.filter(id => updateSizing[id] - computedColumnMinWidths[id] > 0)\n }\n } else if (width <= containerWidth) {\n let distributableWidth = containerWidth - width\n\n // check max width violations\n const violatingColumns = columns.filter(value =>\n computedColumnMaxWidths[value.id] && (updateSizing[value.id] > computedColumnMaxWidths[value.id]))\n\n const violationColumnsAmount = violatingColumns.reduce(\n (previousValue, column) => previousValue + updateSizing[column.id] - computedColumnMaxWidths[column.id], 0\n )\n distributableWidth += violationColumnsAmount\n\n let enlargeableColumns = columns\n .filter(col => !computedColumnMaxWidths[col.id] || updateSizing[col.id] < computedColumnMaxWidths[col.id])\n .map(value => value.id)\n\n while (distributableWidth > 0 && enlargeableColumns.length > 0) {\n let minEnlargeableAmount = enlargeableColumns.reduce((previousValue, id) => Math.min(previousValue, computedColumnMaxWidths[id] ? computedColumnMaxWidths[id] - updateSizing[id] : distributableWidth), distributableWidth)\n if (minEnlargeableAmount * enlargeableColumns.length > distributableWidth) {\n minEnlargeableAmount = distributableWidth / enlargeableColumns.length\n }\n\n enlargeableColumns.forEach(id => {\n updateSizing[id] += minEnlargeableAmount\n })\n\n distributableWidth -= minEnlargeableAmount * enlargeableColumns.length\n enlargeableColumns = enlargeableColumns.filter(id => !computedColumnMaxWidths[id] || updateSizing[id] < computedColumnMaxWidths[id])\n }\n\n if (distributableWidth > 0) {\n updateSizing[columns[columns.length - 1].id] += distributableWidth\n }\n }\n return updateSizing\n }\n }, [columns, computedColumnMaxWidths, computedColumnMinWidths, tableMinWidth]) // eslint-disable-line react-hooks/exhaustive-deps\n\n const table = useReactTable({\n data,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getPaginationRowModel: getPaginationRowModel(),\n initialState: initialState,\n defaultColumn: {\n minSize: 60,\n maxSize: 700,\n cell: ({ cell }) => {\n return (<TableCell>{cell.getValue() as string}</TableCell>)\n },\n ...defaultColumn,\n },\n columns,\n state: {\n columnSizing,\n columnSizingInfo,\n pagination,\n columnFilters,\n ...state\n },\n filterFns: {\n ...tableOptions?.filterFns,\n dateRange: TableFilters.dateRange,\n },\n onColumnSizingInfoChange: updaterOrValue => {\n setColumnSizingInfo(updaterOrValue)\n if (tableOptions.onColumnSizingInfoChange) {\n tableOptions?.onColumnSizingInfoChange(updaterOrValue)\n }\n },\n onColumnSizingChange: updaterOrValue => {\n setColumnSizing(previous => {\n const newSizing = typeof updaterOrValue === 'function' ? updaterOrValue(previous) : updaterOrValue\n return updateColumnSizes(newSizing)\n })\n if (tableOptions.onColumnSizingChange) {\n tableOptions.onColumnSizingChange(updaterOrValue)\n }\n },\n onPaginationChange: updaterOrValue => {\n setPagination(updaterOrValue)\n if (tableOptions.onPaginationChange) {\n tableOptions.onPaginationChange(updaterOrValue)\n }\n },\n onColumnFiltersChange: updaterOrValue => {\n setColumnFilters(updaterOrValue)\n table.toggleAllRowsSelected(false)\n if (tableOptions.onColumnFiltersChange) {\n tableOptions.onColumnFiltersChange(updaterOrValue)\n }\n },\n autoResetPageIndex: false,\n columnResizeMode: 'onChange',\n ...tableOptions,\n })\n\n const [hasInitializedSizing, setHasInitializedSizing] = useState(false)\n useEffect(() => {\n if (!hasInitializedSizing && ref.current) {\n setHasInitializedSizing(true)\n table.setColumnSizing(updateColumnSizes(columnSizing))\n }\n }, [columnSizing, hasInitializedSizing]) // eslint-disable-line react-hooks/exhaustive-deps\n\n useResizeCallbackWrapper(useCallback(() => {\n table.setColumnSizing(updateColumnSizes)\n }, [updateColumnSizes])) // eslint-disable-line react-hooks/exhaustive-deps\n\n const pageCount = table.getPageCount()\n useEffect(() => {\n const totalPages = pageCount\n if (totalPages === 0) {\n if (pagination.pageIndex !== 0) {\n table.setPagination(prevState => ({\n ...prevState,\n pageIndex: 0,\n }))\n }\n } else if (pagination.pageIndex >= totalPages) {\n table.setPagination((prev) => ({\n ...prev,\n pageIndex: totalPages - 1,\n }))\n }\n }, [data, pageCount, pagination.pageSize, pagination.pageIndex]) // eslint-disable-line react-hooks/exhaustive-deps\n\n const columnSizeVars = useMemo(() => {\n const headers = table.getFlatHeaders()\n const colSizes: { [key: string]: number } = {}\n for (let i = 0; i < headers.length; i++) {\n const header = headers[i]!\n colSizes[`--header-${header.id}-size`] = Math.floor(header.getSize())\n colSizes[`--col-${header.column.id}-size`] = Math.floor(header.column.getSize())\n }\n\n return colSizes\n }, [table.getState().columnSizingInfo, table.getState().columnSizing]) // eslint-disable-line react-hooks/exhaustive-deps\n\n return (\n <div ref={ref} className={clsx('flex-col-4', className)}>\n <Scrollbars\n autoHeight={true}\n autoHeightMax={tableRef.current?.offsetHeight ? (tableRef.current?.offsetHeight + 2) : undefined}\n autoHide={true}\n >\n <table\n ref={tableRef}\n className={clsx(tableClassName)}\n style={{\n ...columnSizeVars,\n width: Math.floor(Math.max(table.getTotalSize() - columns.length, ref.current?.offsetWidth ?? table.getTotalSize())),\n }}\n >\n {table.getHeaderGroups().map((headerGroup) => (\n <colgroup key={headerGroup.id}>\n {headerGroup.headers.map(header => (\n <col\n key={header.id}\n style={{\n width: `calc(var(--header-${header?.id}-size) * 1px)`,\n minWidth: header.column.columnDef.minSize,\n maxWidth: header.column.columnDef.maxSize,\n }}\n />\n ))}\n </colgroup>\n ))}\n <thead>\n {table.getHeaderGroups().map(headerGroup => (\n <tr key={headerGroup.id} className={table.options.meta?.headerRowClassName}>\n {headerGroup.headers.map(header => {\n return (\n <th\n key={header.id}\n colSpan={header.colSpan}\n className={clsx('relative group', header.column.columnDef.meta?.className)}\n >\n <div className=\"flex-row-2 w-full\">\n {header.isPlaceholder ? null : (\n <div className=\"flex-row-1 items-center\">\n {header.column.getCanSort() && (\n <TableSortButton\n sortDirection={header.column.getIsSorted()}\n onClick={() => header.column.toggleSorting()}\n />\n )}\n {header.column.getCanFilter() && header.column.columnDef.meta?.filterType ? (\n <TableFilterButton\n column={header.column}\n filterType={header.column.columnDef.meta.filterType}\n />\n ) : null}\n {flexRender(\n header.column.columnDef.header,\n header.getContext()\n )}\n </div>\n )}\n </div>\n {header.column.getCanResize() && (\n <div\n onMouseDown={header.getResizeHandler()}\n onTouchStart={header.getResizeHandler()}\n onDoubleClick={() => {\n header.column.resetSize()\n }}\n className=\"table-resize-indicator w-2 rounded bg-primary cursor-col-resize select-none touch-none opacity-0 group-hover:opacity-100 transition-opacity\"\n style={{\n opacity: !columnSizingInfo?.columnSizingStart ?\n undefined : (columnSizingInfo?.columnSizingStart?.findIndex(([id, _]) => id === header.column.id) !== -1 ?\n 1 : (columnSizingInfo?.columnSizingStart?.length !== 0 ?\n 0 : undefined)),\n }}\n />\n )}\n </th>\n )\n })}\n </tr>\n ))}\n </thead>\n <tbody>\n {table.getRowModel().rows.map(row => {\n return (\n <tr key={row.id} onClick={() => onRowClick(row, table)} className={table.options.meta?.bodyRowClassName}>\n {row.getVisibleCells().map(cell => {\n return (\n <td key={cell.id}>\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext()\n )}\n </td>\n )\n })}\n </tr>\n )\n })}\n {range(table.getState().pagination.pageSize - table.getRowModel().rows.length, { allowEmptyRange: true }).map((row, index) => {\n return (\n <tr key={'filler-row-' + index}>\n {columns.map((column) => {\n return (\n <td key={column.id}>\n {fillerRow ? fillerRow(column.id, table) : (<FillerRowElement/>)}\n </td>\n )\n })}\n </tr>\n )\n })}\n </tbody>\n </table>\n </Scrollbars>\n <div className=\"flex-row-2 justify-center\">\n <Pagination\n pageIndex={table.getState().pagination.pageIndex}\n pageCount={table.getPageCount()}\n onPageChanged={page => table.setPageIndex(page)}\n />\n </div>\n </div>\n )\n}\n\n\nexport type TableUncontrolledProps<T> = TableProps<T>\n\nexport const TableUncontrolled = <T, >({ data, ...props }: TableUncontrolledProps<T>) => {\n const [usedDate, setUsedData] = useState<T[]>(data)\n\n useEffect(() => {\n setUsedData(data)\n }, [data])\n\n return (\n <Table\n {...props}\n data={usedDate}\n />\n )\n}\n\n\nexport type TableWithSelectionProps<T> = TableProps<T> & {\n rowSelection: RowSelectionState,\n disableClickRowClickSelection?: boolean,\n selectionRowId?: string,\n}\n\nexport const TableWithSelection = <T, >({\n columns,\n state,\n fillerRow,\n rowSelection,\n disableClickRowClickSelection = false,\n selectionRowId = 'selection',\n onRowClick = noop,\n meta,\n ...props\n }: TableWithSelectionProps<T>) => {\n const columnsWithSelection = useMemo<ColumnDef<T>[]>(() => {\n return [\n {\n id: selectionRowId,\n header: ({ table }) => {\n return (\n <Checkbox\n checked={table.getIsSomeRowsSelected() ? 'indeterminate' : table.getIsAllRowsSelected()}\n onChangeTristate={value => {\n const newValue = !!value\n table.toggleAllRowsSelected(newValue)\n }}\n containerClassName=\"max-w-6\"\n />\n )\n },\n cell: ({ row }) => {\n return (\n <Checkbox\n disabled={!row.getCanSelect()}\n checked={row.getIsSelected()}\n onChange={row.getToggleSelectedHandler()}\n containerClassName=\"max-w-6\"\n />\n )\n },\n size: 60,\n minSize: 60,\n maxSize: 60,\n enableResizing: false,\n enableSorting: false,\n },\n ...columns,\n ]\n }, [columns, selectionRowId])\n\n return (\n <Table\n columns={columnsWithSelection}\n fillerRow={(columnId, table) => {\n if (columnId === selectionRowId) {\n return (<Checkbox checked={false} disabled={true} containerClassName=\"max-w-6\"/>)\n }\n return fillerRow ? fillerRow(columnId, table) : (<FillerRowElement/>)\n }}\n state={{\n rowSelection,\n ...state\n }}\n onRowClick={(row, table) => {\n if (!disableClickRowClickSelection) {\n row.toggleSelected()\n }\n onRowClick(row, table)\n }}\n meta={{\n ...meta,\n bodyRowClassName: clsx(\n { 'cursor-pointer': !disableClickRowClickSelection },\n meta?.bodyRowClassName\n )\n }}\n {...props}\n />\n )\n}","import { ChevronFirst, ChevronLast, ChevronLeft, ChevronRight } from 'lucide-react'\nimport clsx from 'clsx'\nimport type { PropsForTranslation } from '../../localization/useTranslation'\nimport { useTranslation } from '../../localization/useTranslation'\nimport type { FormTranslationType } from '../../localization/defaults/form'\nimport { formTranslation } from '../../localization/defaults/form'\nimport { Input } from '../user-action/Input'\nimport { clamp } from '../../util/math'\nimport type { CSSProperties } from 'react'\nimport { useEffect, useState } from 'react'\nimport { IconButton } from '../user-action/Button'\n\ntype PaginationTranslation = FormTranslationType\n\nexport type PaginationProps = {\n pageIndex: number, // starts with 0\n pageCount: number,\n onPageChanged: (page: number) => void,\n className?: string,\n style?: CSSProperties,\n}\n\n/**\n * A Component showing the pagination allowing first, before, next and last page navigation\n */\nexport const Pagination = ({\n overwriteTranslation,\n pageIndex,\n pageCount,\n onPageChanged,\n className,\n style,\n }: PropsForTranslation<PaginationTranslation, PaginationProps>) => {\n const translation = useTranslation([formTranslation], overwriteTranslation)\n const [value, setValue] = useState<string>((pageIndex + 1).toString())\n\n const noPages = pageCount === 0\n const onFirstPage = pageIndex === 0 && !noPages\n const onLastPage = pageIndex === pageCount - 1\n\n useEffect(() => {\n if (noPages) {\n setValue('0')\n } else {\n setValue((pageIndex + 1).toString())\n }\n }, [pageIndex, noPages])\n\n const changePage = (page: number) => {\n onPageChanged(page)\n }\n\n return (\n <div className={clsx('flex-row-1', className)} style={style}>\n <IconButton color=\"transparent\" onClick={() => changePage(0)} disabled={onFirstPage || noPages}>\n <ChevronFirst/>\n </IconButton>\n <IconButton color=\"transparent\" onClick={() => changePage(pageIndex - 1)} disabled={onFirstPage || noPages}>\n <ChevronLeft/>\n </IconButton>\n <div className=\"flex-row-2 min-w-56 items-center justify-center mx-2 text-center\">\n <Input\n value={value}\n containerClassName=\"flex flex-1 h-10\"\n className={clsx(\n 'w-full text-center font-bold input-indicator-hidden'\n )}\n type=\"number\"\n min={1}\n max={pageCount}\n disabled={noPages}\n onChangeText={value => {\n if (value) {\n setValue(clamp(Number(value), 1, pageCount).toString())\n } else {\n setValue(value)\n }\n }}\n onEditCompleted={value => {\n changePage(clamp(Number(value) - 1, 0, pageCount - 1))\n }}\n editCompleteOptions={{ delay: 800 }}\n />\n <span className=\"select-none w-10\">{translation('of')}</span>\n <span\n className=\"flex-row-2 flex-1 items-center justify-center select-none h-10 bg-input-background text-input-text rounded-md font-bold\"\n >\n {pageCount}\n </span>\n </div>\n <IconButton color=\"transparent\" onClick={() => changePage(pageIndex + 1)} disabled={onLastPage || noPages}>\n <ChevronRight/>\n </IconButton>\n <IconButton color=\"transparent\" onClick={() => changePage(pageCount - 1)} disabled={onLastPage || noPages}>\n <ChevronLast/>\n </IconButton>\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}","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 React, { forwardRef, type InputHTMLAttributes, useEffect, useImperativeHandle, useRef, useState } from 'react'\nimport clsx from 'clsx'\nimport type { UseDelayOptionsResolved } from '../../hooks/useDelay'\nimport { useDelay } from '../../hooks/useDelay'\nimport { noop } from '../../util/noop'\nimport type { LabelProps } from './Label'\nimport { Label } from './Label'\nimport { useFocusManagement } from '../../hooks/useFocusManagement'\nimport { useFocusOnceVisible } from '../../hooks/useFocusOnceVisible'\n\ntype GetInputClassNameProps = {\n disabled?: boolean,\n hasError?: boolean,\n}\nconst getInputClassName = ({ disabled = false, hasError = false }: GetInputClassNameProps) => {\n return clsx(\n 'px-2 py-1.5 rounded-md border-2',\n {\n 'bg-input-background text-input-text hover:border-primary focus:border-primary': !disabled && !hasError,\n 'bg-on-negative text-negative border-negative-border hover:border-negative-border-hover': !disabled && hasError,\n 'bg-disabled-background text-disabled-text border-disabled-border': disabled,\n }\n )\n}\n\nexport type EditCompleteOptionsResolved = {\n onBlur: boolean,\n afterDelay: boolean,\n} & Omit<UseDelayOptionsResolved, 'disabled'>\n\nexport type EditCompleteOptions = Partial<EditCompleteOptionsResolved>\n\nconst defaultEditCompleteOptions: EditCompleteOptionsResolved = {\n onBlur: true,\n afterDelay: true,\n delay: 2500\n}\n\nexport type InputProps = {\n /**\n * used for the label's `for` attribute\n */\n label?: Omit<LabelProps, 'id'>,\n /**\n * Callback for when the input's value changes\n * This is pretty much required but made optional for the rare cases where it actually isn't need such as when used with disabled\n * That could be enforced through a union type but that seems a bit overkill\n * @default noop\n */\n onChangeText?: (text: string) => void,\n className?: string,\n onEditCompleted?: (text: string) => void,\n allowEnterComplete?: boolean,\n expanded?: boolean,\n containerClassName?: string,\n editCompleteOptions?: EditCompleteOptions,\n} & Omit<InputHTMLAttributes<HTMLInputElement>, 'label'>\n\n/**\n * A Component for inputting text or other information\n *\n * Its state is managed must be managed by the parent\n */\nconst Input = forwardRef<HTMLInputElement, InputProps>(function Input({\n id,\n type = 'text',\n value,\n label,\n onChange = noop,\n onChangeText = noop,\n onEditCompleted,\n className = '',\n allowEnterComplete = true,\n expanded = true,\n autoFocus = false,\n onBlur,\n editCompleteOptions,\n containerClassName,\n disabled,\n ...restProps\n }, forwardedRef) {\n const { onBlur: allowEditCompleteOnBlur, afterDelay, delay } = { ...defaultEditCompleteOptions, ...editCompleteOptions }\n\n const {\n restartTimer,\n clearTimer\n } = useDelay({ delay, disabled: !afterDelay })\n\n const innerRef = useRef<HTMLInputElement>(null)\n const { focusNext } = useFocusManagement()\n\n useFocusOnceVisible(innerRef, !autoFocus)\n useImperativeHandle(forwardedRef, () => innerRef.current)\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault()\n innerRef.current?.blur()\n focusNext()\n }\n }\n\n return (\n <div className={clsx({ 'w-full': expanded }, containerClassName)}>\n {label && <Label {...label} htmlFor={id} className={clsx('mb-1', label.className)} />}\n <input\n {...restProps}\n ref={innerRef}\n value={value}\n id={id}\n type={type}\n disabled={disabled}\n className={clsx(getInputClassName({ disabled }), className)}\n onKeyDown={allowEnterComplete ? handleKeyDown : undefined}\n onBlur={event => {\n onBlur?.(event)\n if (onEditCompleted && allowEditCompleteOnBlur) {\n onEditCompleted(event.target.value)\n clearTimer()\n }\n }}\n onChange={e => {\n const value = e.target.value\n if (onEditCompleted) {\n restartTimer(() => {\n if(innerRef.current){\n innerRef.current.blur()\n if(!allowEditCompleteOnBlur) {\n onEditCompleted(value)\n }\n } else {\n onEditCompleted(value)\n }\n })\n }\n onChange(e)\n onChangeText(value)\n }}\n />\n </div>\n )\n})\n\n\n/**\n * A Component for inputting text or other information\n *\n * Its state is managed by the component itself\n */\nconst InputUncontrolled = ({\n value = '',\n onChangeText = noop,\n ...props\n }: InputProps) => {\n const [usedValue, setUsedValue] = useState(value)\n\n useEffect(() => {\n setUsedValue(value)\n }, [value])\n\n return (\n <Input\n {...props}\n value={usedValue}\n onChangeText={text => {\n setUsedValue(text)\n onChangeText(text)\n }}\n />\n )\n}\n\nexport type FormInputProps = InputHTMLAttributes<HTMLInputElement> & {\n id: string,\n labelText?: string,\n errorText?: string,\n labelClassName?: string,\n errorClassName?: string,\n containerClassName?: string,\n}\n\nconst FormInput = forwardRef<HTMLInputElement, FormInputProps>(function FormInput({\n id,\n labelText,\n errorText,\n className,\n labelClassName,\n errorClassName,\n containerClassName,\n required,\n disabled,\n ...restProps\n }, ref) {\n const input = (\n <input\n {...restProps}\n ref={ref}\n id={id}\n disabled={disabled}\n className={clsx(\n getInputClassName({ disabled, hasError: !!errorText }),\n className\n )}\n />\n )\n\n return (\n <div className={clsx('flex flex-col gap-y-1', containerClassName)}>\n {labelText && (\n <label htmlFor={id} className={clsx('textstyle-label-md', labelClassName)}>\n {labelText}\n {required && <span className=\"text-primary font-bold\">*</span>}\n </label>\n )}\n {input}\n {errorText && <label htmlFor={id} className={clsx('text-negative', errorClassName)}>{errorText}</label>}\n </div>\n )\n})\n\nexport {\n InputUncontrolled,\n Input,\n FormInput\n}\n","import { useEffect, useState } from 'react'\n\nexport type UseDelayOptionsResolved = {\n delay: number,\n disabled: boolean,\n}\n\nexport type UseDelayOptions = Partial<UseDelayOptionsResolved>\n\nconst defaultOptions: UseDelayOptionsResolved = {\n delay: 3000,\n disabled: false,\n}\n\nexport function useDelay(options?: UseDelayOptions) {\n const [timer, setTimer] = useState<NodeJS.Timeout | undefined>(undefined)\n const { delay, disabled }: UseDelayOptionsResolved = {\n ...defaultOptions,\n ...options\n }\n\n const clearTimer = () => {\n clearTimeout(timer)\n setTimer(undefined)\n }\n\n const restartTimer = (onDelayFinish: () => void) => {\n if(disabled) {\n return\n }\n clearTimeout(timer)\n setTimer(setTimeout(() => {\n onDelayFinish()\n setTimer(undefined)\n }, delay))\n }\n\n useEffect(() => {\n return () => {\n clearTimeout(timer)\n }\n }, [timer])\n\n useEffect(() => {\n if(disabled){\n clearTimeout(timer)\n setTimer(undefined)\n }\n }, [disabled, timer])\n\n return { restartTimer, clearTimer, hasActiveTimer: !!timer }\n}","export const noop = () => undefined\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 { useCallback } from 'react'\n\nexport function useFocusManagement() {\n const getFocusableElements = useCallback((): HTMLElement[] => {\n return Array.from(\n document.querySelectorAll(\n 'input, button, select, textarea, a[href], [tabindex]:not([tabindex=\"-1\"])'\n )\n ).filter(\n (el): el is HTMLElement =>\n el instanceof HTMLElement &&\n !el.hasAttribute('disabled') &&\n !el.hasAttribute('hidden') &&\n el.tabIndex !== -1\n )\n }, [])\n\n const getNextFocusElement = useCallback((): HTMLElement | undefined => {\n const elements = getFocusableElements()\n if(elements.length === 0) {\n return undefined\n }\n let nextElement = elements[0]\n if(document.activeElement instanceof HTMLElement) {\n const currentIndex = elements.indexOf(document.activeElement)\n nextElement = elements[(currentIndex + 1) % elements.length]\n }\n return nextElement\n }, [getFocusableElements])\n\n const focusNext = useCallback(() => {\n const nextElement = getNextFocusElement()\n nextElement?.focus()\n }, [getNextFocusElement])\n\n const getPreviousFocusElement = useCallback((): HTMLElement | undefined => {\n const elements = getFocusableElements()\n if(elements.length === 0) {\n return undefined\n }\n let previousElement = elements[0]\n if(document.activeElement instanceof HTMLElement) {\n const currentIndex = elements.indexOf(document.activeElement)\n if(currentIndex === 0) {\n previousElement = elements[elements.length - 1]\n } else {\n previousElement = elements[currentIndex - 1]\n }\n }\n return previousElement\n }, [getFocusableElements])\n\n const focusPrevious = useCallback(() => {\n const previousElement = getPreviousFocusElement()\n if (previousElement) previousElement.focus()\n }, [getPreviousFocusElement])\n\n return {\n getFocusableElements,\n getNextFocusElement,\n getPreviousFocusElement,\n focusNext,\n focusPrevious,\n }\n}","import type { MutableRefObject } from 'react'\nimport React, { useEffect } from 'react'\n\nexport const useFocusOnceVisible = (\n ref: MutableRefObject<HTMLElement>,\n disable: boolean