UNPKG

@helpwave/hightide

Version:

helpwave's component and theming library

724 lines (706 loc) 22.4 kB
// src/components/layout-and-navigation/SearchableList.tsx import { Search } from "lucide-react"; import clsx4 from "clsx"; // src/localization/LanguageProvider.tsx import { createContext, useContext, useEffect, useState as useState2 } from "react"; // src/hooks/useLocalStorage.ts import { useCallback, useState } from "react"; // src/localization/util.ts var languages = ["en", "de"]; var languagesLocalNames = { en: "English", de: "Deutsch" }; var DEFAULT_LANGUAGE = "en"; var LanguageUtil = { languages, DEFAULT_LANGUAGE, languagesLocalNames }; // src/localization/LanguageProvider.tsx import { jsx } from "react/jsx-runtime"; var LanguageContext = createContext({ language: LanguageUtil.DEFAULT_LANGUAGE, setLanguage: (v) => v }); var useLanguage = () => useContext(LanguageContext); // src/localization/useTranslation.ts var TranslationPluralCount = { zero: 0, one: 1, two: 2, few: 3, many: 11, other: -1 }; var useTranslation = (translations, overwriteTranslation = {}) => { const { language: languageProp, translation: overwrite } = overwriteTranslation; const { language: inferredLanguage } = useLanguage(); const usedLanguage = languageProp ?? inferredLanguage; const usedTranslations = [...translations]; if (overwrite) { usedTranslations.push(overwrite); } return (key, options) => { const { count, replacements } = { ...{ count: 0, replacements: {} }, ...options }; try { for (let i = translations.length - 1; i >= 0; i--) { const translation = translations[i]; const localizedTranslation = translation[usedLanguage]; if (!localizedTranslation) { continue; } const value = localizedTranslation[key]; if (!value) { continue; } let forProcessing; if (typeof value !== "string") { if (count === TranslationPluralCount.zero && value?.zero) { forProcessing = value.zero; } else if (count === TranslationPluralCount.one && value?.one) { forProcessing = value.one; } else if (count === TranslationPluralCount.two && value?.two) { forProcessing = value.two; } else if (TranslationPluralCount.few <= count && count < TranslationPluralCount.many && value?.few) { forProcessing = value.few; } else if (count > TranslationPluralCount.many && value?.many) { forProcessing = value.many; } else { forProcessing = value.other; } } else { forProcessing = value; } forProcessing = forProcessing.replace(/\{\{(\w+)}}/g, (_, placeholder) => { return replacements[placeholder] ?? `{{key:${placeholder}}}`; }); return forProcessing; } } catch (e) { console.error(e); } return `{{${usedLanguage}:${key}}}`; }; }; // src/components/user-action/Input.tsx import { forwardRef, useEffect as useEffect4, useImperativeHandle, useRef, useState as useState4 } from "react"; import clsx2 from "clsx"; // src/hooks/useDelay.ts import { useEffect as useEffect2, useState as useState3 } from "react"; var defaultOptions = { delay: 3e3, disabled: false }; function useDelay(options) { const [timer, setTimer] = useState3(void 0); const { delay, disabled } = { ...defaultOptions, ...options }; const clearTimer = () => { clearTimeout(timer); setTimer(void 0); }; const restartTimer = (onDelayFinish) => { if (disabled) { return; } clearTimeout(timer); setTimer(setTimeout(() => { onDelayFinish(); setTimer(void 0); }, delay)); }; useEffect2(() => { return () => { clearTimeout(timer); }; }, [timer]); useEffect2(() => { if (disabled) { clearTimeout(timer); setTimer(void 0); } }, [disabled, timer]); return { restartTimer, clearTimer, hasActiveTimer: !!timer }; } // src/util/noop.ts var noop = () => void 0; // src/components/user-action/Label.tsx import clsx from "clsx"; import { jsx as jsx2 } from "react/jsx-runtime"; var styleMapping = { labelSmall: "textstyle-label-sm", labelMedium: "textstyle-label-md", labelBig: "textstyle-label-lg" }; var Label = ({ children, name, labelType = "labelSmall", className, ...props }) => { return /* @__PURE__ */ jsx2("label", { ...props, className: clsx(styleMapping[labelType], className), children: children ? children : name }); }; // src/hooks/useFocusManagement.ts import { useCallback as useCallback2 } from "react"; function useFocusManagement() { const getFocusableElements = useCallback2(() => { return Array.from( document.querySelectorAll( 'input, button, select, textarea, a[href], [tabindex]:not([tabindex="-1"])' ) ).filter( (el) => el instanceof HTMLElement && !el.hasAttribute("disabled") && !el.hasAttribute("hidden") && el.tabIndex !== -1 ); }, []); const getNextFocusElement = useCallback2(() => { const elements = getFocusableElements(); if (elements.length === 0) { return void 0; } let nextElement = elements[0]; if (document.activeElement instanceof HTMLElement) { const currentIndex = elements.indexOf(document.activeElement); nextElement = elements[(currentIndex + 1) % elements.length]; } return nextElement; }, [getFocusableElements]); const focusNext = useCallback2(() => { const nextElement = getNextFocusElement(); nextElement?.focus(); }, [getNextFocusElement]); const getPreviousFocusElement = useCallback2(() => { const elements = getFocusableElements(); if (elements.length === 0) { return void 0; } let previousElement = elements[0]; if (document.activeElement instanceof HTMLElement) { const currentIndex = elements.indexOf(document.activeElement); if (currentIndex === 0) { previousElement = elements[elements.length - 1]; } else { previousElement = elements[currentIndex - 1]; } } return previousElement; }, [getFocusableElements]); const focusPrevious = useCallback2(() => { const previousElement = getPreviousFocusElement(); if (previousElement) previousElement.focus(); }, [getPreviousFocusElement]); return { getFocusableElements, getNextFocusElement, getPreviousFocusElement, focusNext, focusPrevious }; } // src/hooks/useFocusOnceVisible.ts import React, { useEffect as useEffect3 } from "react"; var useFocusOnceVisible = (ref, disable = false) => { const [hasUsedFocus, setHasUsedFocus] = React.useState(false); useEffect3(() => { if (disable || hasUsedFocus) { return; } const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting && !hasUsedFocus) { ref.current?.focus(); setHasUsedFocus(hasUsedFocus); } }, { threshold: 0.1 }); if (ref.current) { observer.observe(ref.current); } return () => observer.disconnect(); }, [disable, hasUsedFocus, ref]); }; // src/components/user-action/Input.tsx import { jsx as jsx3, jsxs } from "react/jsx-runtime"; var getInputClassName = ({ disabled = false, hasError = false }) => { return clsx2( "px-2 py-1.5 rounded-md border-2", { "bg-input-background text-input-text hover:border-primary focus:border-primary": !disabled && !hasError, "bg-on-negative text-negative border-negative-border hover:border-negative-border-hover": !disabled && hasError, "bg-disabled-background text-disabled-text border-disabled-border": disabled } ); }; var defaultEditCompleteOptions = { onBlur: true, afterDelay: true, delay: 2500 }; var Input = forwardRef(function Input2({ id, type = "text", value, label, onChange = noop, onChangeText = noop, onEditCompleted, className = "", allowEnterComplete = true, expanded = true, autoFocus = false, onBlur, editCompleteOptions, containerClassName, disabled, ...restProps }, forwardedRef) { const { onBlur: allowEditCompleteOnBlur, afterDelay, delay } = { ...defaultEditCompleteOptions, ...editCompleteOptions }; const { restartTimer, clearTimer } = useDelay({ delay, disabled: !afterDelay }); const innerRef = useRef(null); const { focusNext } = useFocusManagement(); useFocusOnceVisible(innerRef, !autoFocus); useImperativeHandle(forwardedRef, () => innerRef.current); const handleKeyDown = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); innerRef.current?.blur(); focusNext(); } }; return /* @__PURE__ */ jsxs("div", { className: clsx2({ "w-full": expanded }, containerClassName), children: [ label && /* @__PURE__ */ jsx3(Label, { ...label, htmlFor: id, className: clsx2("mb-1", label.className) }), /* @__PURE__ */ jsx3( "input", { ...restProps, ref: innerRef, value, id, type, disabled, className: clsx2(getInputClassName({ disabled }), className), onKeyDown: allowEnterComplete ? handleKeyDown : void 0, onBlur: (event) => { onBlur?.(event); if (onEditCompleted && allowEditCompleteOnBlur) { onEditCompleted(event.target.value); clearTimer(); } }, onChange: (e) => { const value2 = e.target.value; if (onEditCompleted) { restartTimer(() => { if (innerRef.current) { innerRef.current.blur(); if (!allowEditCompleteOnBlur) { onEditCompleted(value2); } } else { onEditCompleted(value2); } }); } onChange(e); onChangeText(value2); } } ) ] }); }); var FormInput = forwardRef(function FormInput2({ id, labelText, errorText, className, labelClassName, errorClassName, containerClassName, required, disabled, ...restProps }, ref) { const input = /* @__PURE__ */ jsx3( "input", { ...restProps, ref, id, disabled, className: clsx2( getInputClassName({ disabled, hasError: !!errorText }), className ) } ); return /* @__PURE__ */ jsxs("div", { className: clsx2("flex flex-col gap-y-1", containerClassName), children: [ labelText && /* @__PURE__ */ jsxs("label", { htmlFor: id, className: clsx2("textstyle-label-md", labelClassName), children: [ labelText, required && /* @__PURE__ */ jsx3("span", { className: "text-primary font-bold", children: "*" }) ] }), input, errorText && /* @__PURE__ */ jsx3("label", { htmlFor: id, className: clsx2("text-negative", errorClassName), children: errorText }) ] }); }); // src/components/user-action/Button.tsx import { forwardRef as forwardRef2 } from "react"; import clsx3 from "clsx"; import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime"; var ButtonColorUtil = { solid: ["primary", "secondary", "tertiary", "positive", "warning", "negative", "neutral"], text: ["primary", "negative", "neutral"], outline: ["primary"] }; var IconButtonUtil = { icon: [...ButtonColorUtil.solid, "transparent"] }; var paddingMapping = { small: "btn-sm", medium: "btn-md", large: "btn-lg" }; var iconPaddingMapping = { tiny: "icon-btn-xs", small: "icon-btn-sm", medium: "icon-btn-md", large: "icon-btn-lg" }; var ButtonUtil = { paddingMapping, iconPaddingMapping }; var SolidButton = forwardRef2(function SolidButton2({ children, color = "primary", size = "medium", startIcon, endIcon, onClick, className, ...restProps }, ref) { const colorClasses = { primary: "not-disabled:bg-button-solid-primary-background not-disabled:text-button-solid-primary-text", secondary: "not-disabled:bg-button-solid-secondary-background not-disabled:text-button-solid-secondary-text", tertiary: "not-disabled:bg-button-solid-tertiary-background not-disabled:text-button-solid-tertiary-text", positive: "not-disabled:bg-button-solid-positive-background not-disabled:text-button-solid-positive-text", warning: "not-disabled:bg-button-solid-warning-background not-disabled:text-button-solid-warning-text", negative: "not-disabled:bg-button-solid-negative-background not-disabled:text-button-solid-negative-text", neutral: "not-disabled:bg-button-solid-neutral-background not-disabled:text-button-solid-neutral-text" }[color]; const iconColorClasses = { primary: "not-group-disabled:text-button-solid-primary-icon", secondary: "not-group-disabled:text-button-solid-secondary-icon", tertiary: "not-group-disabled:text-button-solid-tertiary-icon", positive: "not-group-disabled:text-button-solid-positive-icon", warning: "not-group-disabled:text-button-solid-warning-icon", negative: "not-group-disabled:text-button-solid-negative-icon", neutral: "not-group-disabled:text-button-solid-neutral-icon" }[color]; return /* @__PURE__ */ jsxs2( "button", { ref, onClick, className: clsx3( "group font-semibold", colorClasses, "not-disabled:hover:brightness-90", "disabled:text-disabled-text disabled:bg-disabled-background", ButtonUtil.paddingMapping[size], className ), ...restProps, children: [ startIcon && /* @__PURE__ */ jsx4( "span", { className: clsx3( iconColorClasses, "group-disabled:text-disabled-icon" ), children: startIcon } ), children, endIcon && /* @__PURE__ */ jsx4( "span", { className: clsx3( iconColorClasses, "group-disabled:text-disabled-icon" ), children: endIcon } ) ] } ); }); var IconButton = ({ children, color = "primary", size = "medium", className, ...restProps }) => { const colorClasses = { primary: "not-disabled:bg-button-solid-primary-background not-disabled:text-button-solid-primary-text", secondary: "not-disabled:bg-button-solid-secondary-background not-disabled:text-button-solid-secondary-text", tertiary: "not-disabled:bg-button-solid-tertiary-background not-disabled:text-button-solid-tertiary-text", positive: "not-disabled:bg-button-solid-positive-background not-disabled:text-button-solid-positive-text", warning: "not-disabled:bg-button-solid-warning-background not-disabled:text-button-solid-warning-text", negative: "not-disabled:bg-button-solid-negative-background not-disabled:text-button-solid-negative-text", neutral: "not-disabled:bg-button-solid-neutral-background not-disabled:text-button-solid-neutral-text", transparent: "not-disabled:bg-transparent" }[color]; return /* @__PURE__ */ jsx4( "button", { className: clsx3( colorClasses, "not-disabled:hover:brightness-90", "disabled:text-disabled-text", { "disabled:bg-disabled-background": color !== "transparent", "disabled:opacity-70": color === "transparent", "not-disabled:hover:bg-button-text-hover-background": color === "transparent" }, ButtonUtil.iconPaddingMapping[size], className ), ...restProps, children } ); }; // src/hooks/useSearch.ts import { useCallback as useCallback3, useEffect as useEffect5, useMemo, useState as useState5 } from "react"; // src/util/simpleSearch.ts var MultiSubjectSearchWithMapping = (search, objects, mapping) => { return objects.filter((object) => { const mappedSearchKeywords = mapping(object)?.map((value) => value.toLowerCase().trim()); if (!mappedSearchKeywords) { return true; } return search.every((searchValue) => !!mappedSearchKeywords.find((value) => !!value && value.includes(searchValue.toLowerCase().trim()))); }); }; // src/hooks/useSearch.ts var useSearch = ({ list, initialSearch, searchMapping, additionalSearchTags, isSearchInstant = true, sortingFunction, filter, disabled = false }) => { const [search, setSearch] = useState5(initialSearch ?? ""); const [result, setResult] = useState5(list); const searchTags = useMemo(() => additionalSearchTags ?? [], [additionalSearchTags]); const updateSearch = useCallback3((newSearch) => { const usedSearch = newSearch ?? search; if (newSearch) { setSearch(search); } setResult(MultiSubjectSearchWithMapping([usedSearch, ...searchTags], list, searchMapping)); }, [searchTags, list, search, searchMapping]); useEffect5(() => { if (isSearchInstant) { setResult(MultiSubjectSearchWithMapping([search, ...searchTags], list, searchMapping)); } }, [searchTags, isSearchInstant, list, search, searchMapping, additionalSearchTags]); const filteredResult = useMemo(() => { if (!filter) { return result; } return result.filter(filter); }, [result, filter]); const sortedAndFilteredResult = useMemo(() => { if (!sortingFunction) { return filteredResult; } return filteredResult.sort(sortingFunction); }, [filteredResult, sortingFunction]); const usedResult = useMemo(() => { if (!disabled) { return sortedAndFilteredResult; } return list; }, [disabled, list, sortedAndFilteredResult]); return { result: usedResult, hasResult: usedResult.length > 0, allItems: list, updateSearch, search, setSearch }; }; // src/localization/defaults/form.ts var formTranslation = { en: { add: "Add", all: "All", apply: "Apply", back: "Back", cancel: "Cancel", change: "Change", clear: "Clear", click: "Click", clickToCopy: "Click to Copy", close: "Close", confirm: "Confirm", copy: "Copy", copied: "Copied", create: "Create", decline: "Decline", delete: "Delete", discard: "Discard", discardChanges: "Discard Changes", done: "Done", edit: "Edit", enterText: "Enter text here", error: "Error", exit: "Exit", fieldRequiredError: "This field is required.", invalidEmailError: "Please enter a valid email address.", less: "Less", loading: "Loading", maxLengthError: "Maximum length exceeded.", minLengthError: "Minimum length not met.", more: "More", next: "Next", no: "No", none: "None", of: "of", optional: "Optional", pleaseWait: "Please wait...", previous: "Previous", remove: "Remove", required: "Required", reset: "Reset", save: "Save", saved: "Saved", search: "Search", select: "Select", selectOption: "Select an option", show: "Show", showMore: "Show more", showLess: "Show less", submit: "Submit", success: "Success", update: "Update", unsavedChanges: "Unsaved Changes", unsavedChangesSaveQuestion: "Do you want to save your changes?", yes: "Yes" }, de: { add: "Hinzuf\xFCgen", all: "Alle", apply: "Anwenden", back: "Zur\xFCck", cancel: "Abbrechen", change: "\xC4ndern", clear: "L\xF6schen", click: "Klicken", clickToCopy: "Zum kopieren klicken", close: "Schlie\xDFen", confirm: "Best\xE4tigen", copy: "Kopieren", copied: "Kopiert", create: "Erstellen", decline: "Ablehnen", delete: "L\xF6schen", discard: "Verwerfen", discardChanges: "\xC4nderungen Verwerfen", done: "Fertig", edit: "Bearbeiten", enterText: "Text hier eingeben", error: "Fehler", exit: "Beenden", fieldRequiredError: "Dieses Feld ist erforderlich.", invalidEmailError: "Bitte geben Sie eine g\xFCltige E-Mail-Adresse ein.", less: "Weniger", loading: "L\xE4dt", maxLengthError: "Maximale L\xE4nge \xFCberschritten.", minLengthError: "Mindestl\xE4nge nicht erreicht.", more: "Mehr", next: "Weiter", no: "Nein", none: "Nichts", of: "von", optional: "Optional", pleaseWait: "Bitte warten...", previous: "Vorherige", remove: "Entfernen", required: "Erforderlich", reset: "Zur\xFCcksetzen", save: "Speichern", saved: "Gespeichert", search: "Suche", select: "Select", selectOption: "Option ausw\xE4hlen", show: "Anzeigen", showMore: "Mehr anzeigen", showLess: "Weniger anzeigen", submit: "Abschicken", success: "Erfolg", update: "Update", unsavedChanges: "Ungespeicherte \xC4nderungen", unsavedChangesSaveQuestion: "M\xF6chtest du die \xC4nderungen speichern?", yes: "Ja" } }; // src/components/layout-and-navigation/SearchableList.tsx import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime"; var defaultSearchableListTranslation = { en: { nothingFound: "Nothing found" }, de: { nothingFound: "Nichts gefunden" } }; var SearchableList = ({ overwriteTranslation, list, initialSearch = "", searchMapping, autoFocus, minimumItemsForSearch = 6, itemMapper, className, resultListClassName }) => { const translation = useTranslation([defaultSearchableListTranslation, formTranslation], overwriteTranslation); const { result, hasResult, search, setSearch, updateSearch } = useSearch({ list, initialSearch, searchMapping }); return /* @__PURE__ */ jsxs3("div", { className: clsx4("flex-col-2", className), children: [ list.length > minimumItemsForSearch && /* @__PURE__ */ jsxs3("div", { className: "flex-row-2 justify-between items-center", children: [ /* @__PURE__ */ jsx5( Input, { value: search, onChangeText: setSearch, placeholder: translation("search"), autoFocus, className: "w-full" } ), /* @__PURE__ */ jsx5(IconButton, { color: "neutral", onClick: () => updateSearch(), children: /* @__PURE__ */ jsx5(Search, { className: "w-full h-full" }) }) ] }), hasResult ? /* @__PURE__ */ jsx5("div", { className: clsx4("flex-col-1 overflow-y-auto", resultListClassName), children: result.map(itemMapper) }) : /* @__PURE__ */ jsx5("div", { className: "flex-row-2 text-description py-2 px-2", children: translation("nothingFound") }) ] }); }; export { SearchableList }; //# sourceMappingURL=SearchableList.mjs.map