UNPKG

@helpwave/hightide

Version:

helpwave's component and theming library

786 lines (763 loc) 23.9 kB
// src/components/properties/MultiSelectProperty.tsx import { List } from "lucide-react"; import clsx8 from "clsx"; // src/hooks/useLanguage.tsx import { createContext, useContext, useEffect as useEffect2, useState as useState2 } from "react"; // src/hooks/useLocalStorage.tsx import { useCallback, useEffect, useState } from "react"; // src/hooks/useLanguage.tsx import { jsx } from "react/jsx-runtime"; var DEFAULT_LANGUAGE = "en"; var LanguageContext = createContext({ language: DEFAULT_LANGUAGE, setLanguage: (v) => v }); var useLanguage = () => useContext(LanguageContext); // src/hooks/useTranslation.ts var useTranslation = (defaults, translationOverwrite = {}) => { const { language: languageProp, translation: overwrite } = translationOverwrite; const { language: inferredLanguage } = useLanguage(); const usedLanguage = languageProp ?? inferredLanguage; let defaultValues = defaults[usedLanguage]; if (overwrite && overwrite[usedLanguage]) { defaultValues = { ...defaultValues, ...overwrite[usedLanguage] }; } return defaultValues; }; // src/components/user-input/MultiSelect.tsx import { useState as useState7 } from "react"; import { Search } from "lucide-react"; // src/util/simpleSearch.ts var MultiSearchWithMapping = (search, objects, mapping) => { return objects.filter((object) => { const mappedSearchKeywords = mapping(object).map((value) => value.toLowerCase().trim()); return !!mappedSearchKeywords.find((value) => value.includes(search.toLowerCase().trim())); }); }; // src/components/user-input/MultiSelect.tsx import clsx4 from "clsx"; // src/components/user-input/Menu.tsx import { useRef } from "react"; import clsx from "clsx"; // src/hooks/useOutsideClick.ts import { useEffect as useEffect3 } from "react"; var useOutsideClick = (refs, handler) => { useEffect3(() => { const listener = (event) => { if (event.target === null) return; if (refs.some((ref) => !ref.current || ref.current.contains(event.target))) { return; } handler(); }; document.addEventListener("mousedown", listener); document.addEventListener("touchstart", listener); return () => { document.removeEventListener("mousedown", listener); document.removeEventListener("touchstart", listener); }; }, [refs, handler]); }; // src/hooks/useHoverState.ts import { useEffect as useEffect4, useState as useState3 } from "react"; var defaultUseHoverStateProps = { closingDelay: 200, isDisabled: false }; var useHoverState = (props = void 0) => { const { closingDelay, isDisabled } = { ...defaultUseHoverStateProps, ...props }; const [isHovered, setIsHovered] = useState3(false); const [timer, setTimer] = useState3(); const onMouseEnter = () => { if (isDisabled) { return; } clearTimeout(timer); setIsHovered(true); }; const onMouseLeave = () => { if (isDisabled) { return; } setTimer(setTimeout(() => { setIsHovered(false); }, closingDelay)); }; useEffect4(() => { if (timer) { return () => { clearTimeout(timer); }; } }); useEffect4(() => { if (timer) { clearTimeout(timer); } }, [isDisabled]); return { isHovered, setIsHovered, handlers: { onMouseEnter, onMouseLeave } }; }; // src/components/user-input/Menu.tsx import { jsx as jsx2, jsxs } from "react/jsx-runtime"; var MenuItem = ({ children, onClick, alignment = "left", className }) => /* @__PURE__ */ jsx2( "div", { className: clsx("block px-3 py-1 bg-menu-background text-menu-text hover:brightness-90", { "text-right": alignment === "right", "text-left": alignment === "left" }, className), onClick, children } ); var Menu = ({ trigger, children, alignment = "tl", showOnHover = false, menuClassName = "" }) => { const { isHovered: isOpen, setIsHovered: setIsOpen, handlers } = useHoverState({ isDisabled: !showOnHover }); const triggerRef = useRef(null); const menuRef = useRef(null); useOutsideClick([triggerRef, menuRef], () => setIsOpen(false)); return /* @__PURE__ */ jsxs( "div", { className: "relative", ...handlers, children: [ trigger(() => setIsOpen(!isOpen), triggerRef), isOpen ? /* @__PURE__ */ jsx2( "div", { ref: menuRef, onClick: (e) => e.stopPropagation(), className: clsx("absolute top-full mt-1 py-2 w-60 rounded-lg bg-menu-background text-menu-text ring-1 ring-slate-900/5 text-sm leading-6 font-semibold shadow-md z-[1]", { " top-[8px]": alignment[0] === "t", " bottom-[8px]": alignment[0] === "b", " left-[-8px]": alignment[1] === "l", " right-[-8px]": alignment[1] === "r" }, menuClassName), children } ) : null ] } ); }; // src/components/user-input/Input.tsx import { useEffect as useEffect6, useRef as useRef2, useState as useState5, forwardRef } from "react"; import clsx2 from "clsx"; // src/hooks/useSaveDelay.ts import { useEffect as useEffect5, useState as useState4 } from "react"; function useSaveDelay(setNotificationStatus, delay) { const [updateTimer, setUpdateTimer] = useState4(void 0); const [notificationTimer, setNotificationTimer] = useState4(void 0); const restartTimer = (onSave) => { clearTimeout(updateTimer); setUpdateTimer(setTimeout(() => { onSave(); setNotificationStatus(true); clearTimeout(notificationTimer); setNotificationTimer(setTimeout(() => { setNotificationStatus(false); clearTimeout(notificationTimer); }, delay)); clearTimeout(updateTimer); }, delay)); }; const clearUpdateTimer = (hasSaved = true) => { clearTimeout(updateTimer); if (hasSaved) { setNotificationStatus(true); clearTimeout(notificationTimer); setNotificationTimer(setTimeout(() => { setNotificationStatus(false); clearTimeout(notificationTimer); }, delay)); } else { setNotificationStatus(false); } }; useEffect5(() => { return () => { clearTimeout(updateTimer); clearTimeout(notificationTimer); }; }, []); return { restartTimer, clearUpdateTimer }; } // src/util/noop.ts var noop = () => void 0; // src/components/user-input/Label.tsx import { jsx as jsx3 } from "react/jsx-runtime"; var styleMapping = { labelSmall: "textstyle-label-sm", labelMedium: "textstyle-label-md", labelBig: "textstyle-label-lg" }; var Label = ({ children, name, labelType = "labelSmall", ...props }) => { return /* @__PURE__ */ jsx3("label", { ...props, children: children ? children : /* @__PURE__ */ jsx3("span", { className: styleMapping[labelType], children: name }) }); }; // src/components/user-input/Input.tsx import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime"; var ControlledInput = ({ id, type = "text", value, label, onChange = noop, onChangeEvent = noop, className = "", onEditCompleted, expanded = true, onBlur, containerClassName, ...restProps }) => { const { restartTimer, clearUpdateTimer } = useSaveDelay(() => void 0, 3e3); const ref = useRef2(null); useEffect6(() => { if (restProps.autoFocus) { ref.current?.focus(); } }, [restProps.autoFocus]); return /* @__PURE__ */ jsxs2("div", { className: clsx2({ "w-full": expanded }, containerClassName), children: [ label && /* @__PURE__ */ jsx4(Label, { ...label, htmlFor: id, className: clsx2("mb-1", label.className) }), /* @__PURE__ */ jsx4( "input", { ref, value, id, type, className: clsx2("block bg-surface text-on-surface px-3 py-2 rounded-md w-full border-2 border-gray-200 hover:border-primary focus:outline-none focus:border-primary focus:ring-primary", className), onBlur: (event) => { if (onBlur) { onBlur(event); } if (onEditCompleted) { onEditCompleted(event.target.value, event); clearUpdateTimer(); } }, onChange: (e) => { const value2 = e.target.value; if (onEditCompleted) { restartTimer(() => { onEditCompleted(value2, e); clearUpdateTimer(); }); } onChange(value2, e); onChangeEvent(e); }, ...restProps } ) ] }); }; var FormInput = forwardRef(function FormInput2({ id, labelText, errorText, className, labelClassName, errorClassName, containerClassName, required, ...restProps }, ref) { const input = /* @__PURE__ */ jsx4( "input", { ref, id, ...restProps, className: clsx2( "block bg-surface text-on-surface px-3 py-2 rounded-md w-full border-2 border-gray-200 hover:border-primary focus:outline-none focus:border-primary focus:ring-primary", { "focus:border-primary focus:ring-primary": !errorText, "focus:border-negative focus:ring-negative text-negative": !!errorText }, className ) } ); return /* @__PURE__ */ jsxs2("div", { className: clsx2("flex flex-col gap-y-1", containerClassName), children: [ labelText && /* @__PURE__ */ jsxs2("label", { htmlFor: id, className: clsx2("textstyle-label-md", labelClassName), children: [ labelText, required && /* @__PURE__ */ jsx4("span", { className: "text-primary font-bold", children: "*" }) ] }), input, errorText && /* @__PURE__ */ jsx4("label", { htmlFor: id, className: clsx2("text-negative", errorClassName), children: errorText }) ] }); }); // src/components/user-input/Checkbox.tsx import { useState as useState6 } from "react"; import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; import { Check, Minus } from "lucide-react"; import clsx3 from "clsx"; import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime"; var checkboxSizeMapping = { small: "size-4", medium: "size-6", large: "size-8" }; var checkboxIconSizeMapping = { small: "size-3", medium: "size-5", large: "size-7" }; var ControlledCheckbox = ({ id, label, checked, disabled, onChange, onChangeTristate, size = "medium", className = "", containerClassName }) => { const usedSizeClass = checkboxSizeMapping[size]; const innerIconSize = checkboxIconSizeMapping[size]; const propagateChange = (checked2) => { if (onChangeTristate) { onChangeTristate(checked2); } if (onChange) { onChange(checked2 === "indeterminate" ? false : checked2); } }; const changeValue = () => { const newValue = checked === "indeterminate" ? false : !checked; propagateChange(newValue); }; return /* @__PURE__ */ jsxs3("div", { className: clsx3("row justify-center items-center", containerClassName), children: [ /* @__PURE__ */ jsx5( CheckboxPrimitive.Root, { onCheckedChange: propagateChange, checked, disabled, id, className: clsx3(usedSizeClass, `items-center border-2 rounded outline-none focus:border-primary`, { "text-disabled-text border-disabled-text": disabled, "border-on-background": !disabled, "bg-primary/30 border-primary text-primary": checked === true || checked === "indeterminate", "hover:border-gray-400 focus:hover:border-primary": !checked }, className), children: /* @__PURE__ */ jsxs3(CheckboxPrimitive.Indicator, { children: [ checked === true && /* @__PURE__ */ jsx5(Check, { className: innerIconSize }), checked === "indeterminate" && /* @__PURE__ */ jsx5(Minus, { className: innerIconSize }) ] }) } ), label && /* @__PURE__ */ jsx5(Label, { ...label, className: clsx3("cursor-pointer", label.className), htmlFor: id, onClick: changeValue }) ] }); }; // src/components/user-input/MultiSelect.tsx import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime"; var defaultMultiSelectTranslation = { en: { select: "Select", search: "Search", selected: "selected" }, de: { select: "Ausw\xE4hlen", search: "Suche", selected: "ausgew\xE4hlt" } }; var MultiSelect = ({ overwriteTranslation, options, onChange, search, disabled = false, selectedDisplay, label, hintText, showDisabledOptions = true, className = "", triggerClassName = "" }) => { const translation = useTranslation(defaultMultiSelectTranslation, overwriteTranslation); const [searchText, setSearchText] = useState7(search?.initialSearch ?? ""); let filteredOptions = options; const enableSearch = !!search; if (enableSearch && !!searchText) { filteredOptions = MultiSearchWithMapping( searchText, filteredOptions, (value) => search.searchMapping(value) ); } if (!showDisabledOptions) { filteredOptions = filteredOptions.filter((value) => !value.disabled); } const selectedItems = options.filter((value) => value.selected); const menuButtonText = selectedItems.length === 0 ? hintText ?? translation.select : /* @__PURE__ */ jsx6("span", { children: `${selectedItems.length} ${translation.selected}` }); const borderColor = "border-menu-border"; return /* @__PURE__ */ jsxs4("div", { className: clsx4(className), children: [ label && /* @__PURE__ */ jsx6( Label, { ...label, htmlFor: label.name, className: clsx4(" mb-1", label.className), labelType: label.labelType ?? "labelBig" } ), /* @__PURE__ */ jsxs4( Menu, { alignment: "t_", trigger: (onClick, ref) => /* @__PURE__ */ jsx6( "div", { ref, onClick: disabled ? void 0 : onClick, className: clsx4( borderColor, "bg-menu-background text-menu-text inline-w-full justify-between items-center rounded-lg border-2 px-4 py-2 font-medium", { "hover:brightness-90 hover:border-primary cursor-pointer": !disabled, "bg-disabled-background text-disabled cursor-not-allowed": disabled }, triggerClassName ), children: selectedDisplay ? selectedDisplay({ items: options, disabled }) : menuButtonText } ), menuClassName: clsx4( "!rounded-lg !shadow-lg !max-h-[500px] !min-w-[400px] !max-w-[70vh] !overflow-y-auto !border !border-2", borderColor, { "!py-0": !enableSearch, "!pb-0": enableSearch } ), children: [ enableSearch && /* @__PURE__ */ jsxs4("div", { className: "row gap-x-2 items-center px-2 py-2", children: [ /* @__PURE__ */ jsx6(ControlledInput, { autoFocus: true, value: searchText, onChange: setSearchText }), /* @__PURE__ */ jsx6(Search, {}) ] }, "selectSearch"), filteredOptions.map((option, index) => /* @__PURE__ */ jsx6( MenuItem, { className: clsx4({ "cursor-not-allowed !bg-disabled-background !text-disabled-text hover:brightness-100": !!option.disabled, "cursor-pointer": !option.disabled }), children: /* @__PURE__ */ jsxs4( "div", { className: clsx4("overflow-hidden whitespace-nowrap text-ellipsis row items-center gap-x-2", option.className), onClick: () => { if (!option.disabled) { onChange(options.map((value) => value.value === option.value ? { ...option, selected: !value.selected } : value)); } }, children: [ /* @__PURE__ */ jsx6(ControlledCheckbox, { checked: option.selected, disabled: option.disabled, size: "small" }), option.label ] } ) }, `item${index}` )) ] } ) ] }); }; // src/components/ChipList.tsx import clsx5 from "clsx"; import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime"; var Chip = ({ children, trailingIcon, color = "default", variant = "normal", className = "", ...restProps }) => { const colorMapping = { default: "text-tag-default-text bg-tag-default-background", dark: "text-tag-dark-text bg-tag-dark-background", red: "text-tag-red-text bg-tag-red-background", yellow: "text-tag-yellow-text bg-tag-yellow-background", green: "text-tag-green-text bg-tag-green-background", blue: "text-tag-blue-text bg-tag-blue-background", pink: "text-tag-pink-text bg-tag-pink-background" }[color]; const colorMappingIcon = { default: "text-tag-default-icon", dark: "text-tag-dark-icon", red: "text-tag-red-icon", yellow: "text-tag-yellow-icon", green: "text-tag-green-icon", blue: "text-tag-blue-icon", pink: "text-tag-pink-icon" }[color]; return /* @__PURE__ */ jsxs5( "div", { ...restProps, className: clsx5( `row w-fit px-2 py-1`, colorMapping, { "rounded-md": variant === "normal", "rounded-full": variant === "fullyRounded" }, className ), children: [ children, trailingIcon && /* @__PURE__ */ jsx7("span", { className: colorMappingIcon, children: trailingIcon }) ] } ); }; var ChipList = ({ list, className = "" }) => { return /* @__PURE__ */ jsx7("div", { className: clsx5("flex flex-wrap gap-x-4 gap-y-2", className), children: list.map((value, index) => /* @__PURE__ */ jsx7( Chip, { ...value, color: value.color ?? "dark", variant: value.variant ?? "normal", children: value.children }, index )) }); }; // src/components/properties/PropertyBase.tsx import { AlertTriangle } from "lucide-react"; import clsx7 from "clsx"; // src/components/Button.tsx import clsx6 from "clsx"; import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime"; var ButtonSizePaddings = { small: "btn-sm", medium: "btn-md", large: "btn-lg" }; var TextButton = ({ children, disabled = false, color = "neutral", size = "medium", startIcon, endIcon, onClick, className, ...restProps }) => { const colorClasses = { negative: "bg-transparent text-button-text-negative-text", neutral: "bg-transparent text-button-text-neutral-text" }[color]; const iconColorClasses = { negative: "text-button-text-negative-icon", neutral: "text-button-text-neutral-icon" }[color]; return /* @__PURE__ */ jsxs6( "button", { onClick: disabled ? void 0 : onClick, disabled: disabled || onClick === void 0, className: clsx6( className, { "text-disabled-text": disabled, [clsx6(colorClasses, "hover:bg-button-text-hover-background rounded-full")]: !disabled }, ButtonSizePaddings[size] ), ...restProps, children: [ startIcon && /* @__PURE__ */ jsx8( "span", { className: clsx6({ [iconColorClasses]: !disabled, [`text-disabled-icon`]: disabled }), children: startIcon } ), children, endIcon && /* @__PURE__ */ jsx8( "span", { className: clsx6({ [iconColorClasses]: !disabled, [`text-disabled-icon`]: disabled }), children: endIcon } ) ] } ); }; // src/components/properties/PropertyBase.tsx import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime"; var defaultPropertyBaseTranslation = { en: { remove: "Remove" }, de: { remove: "Entfernen" } }; var PropertyBase = ({ overwriteTranslation, name, input, softRequired = false, hasValue, icon, readOnly, onRemove, className = "" }) => { const translation = useTranslation(defaultPropertyBaseTranslation, overwriteTranslation); const requiredAndNoValue = softRequired && !hasValue; return /* @__PURE__ */ jsxs7("div", { className: clsx7("row gap-x-0 group", className), children: [ /* @__PURE__ */ jsxs7( "div", { className: clsx7("row gap-x-2 !w-[200px] px-4 py-2 items-center rounded-l-xl border-2 border-r-0", { "bg-gray-100 text-black group-hover:border-primary border-gray-400": !requiredAndNoValue, "bg-warning text-surface-warning group-hover:border-warning border-warning/90": requiredAndNoValue }, className), children: [ icon, name ] } ), /* @__PURE__ */ jsxs7( "div", { className: clsx7("row grow justify-between items-center rounded-r-xl border-2 border-l-0", { "bg-white group-hover:border-primary border-gray-400": !requiredAndNoValue, "bg-surface-warning group-hover:border-warning border-warning/90": requiredAndNoValue }, className), children: [ input({ softRequired, hasValue }), requiredAndNoValue && /* @__PURE__ */ jsx9("div", { className: "text-warning pr-4", children: /* @__PURE__ */ jsx9(AlertTriangle, { size: 24 }) }), onRemove && /* @__PURE__ */ jsx9( TextButton, { onClick: onRemove, color: "negative", className: clsx7("pr-4 items-center", { "!text-transparent": !hasValue || readOnly }), disabled: !hasValue || readOnly, children: translation.remove } ) ] } ) ] }); }; // src/components/properties/MultiSelectProperty.tsx import { jsx as jsx10 } from "react/jsx-runtime"; var defaultMultiSelectPropertyTranslation = { en: { select: "Select" }, de: { select: "Ausw\xE4hlen" } }; var MultiSelectProperty = ({ overwriteTranslation, options, name, readOnly = false, softRequired, onRemove, ...multiSelectProps }) => { const translation = useTranslation(defaultMultiSelectPropertyTranslation, overwriteTranslation); const hasValue = options.some((value) => value.selected); let triggerClassName; if (softRequired && !hasValue) { triggerClassName = "border-warning hover:brightness-90"; } return /* @__PURE__ */ jsx10( PropertyBase, { name, onRemove, readOnly, softRequired, hasValue, icon: /* @__PURE__ */ jsx10(List, { size: 16 }), input: ({ softRequired: softRequired2 }) => /* @__PURE__ */ jsx10( "div", { className: clsx8("row grow py-2 px-4 cursor-pointer", { "text-warning": softRequired2 && !hasValue }), children: /* @__PURE__ */ jsx10( MultiSelect, { ...multiSelectProps, className: clsx8("w-full", { "bg-surface-warning": softRequired2 && !hasValue }), triggerClassName, selectedDisplay: ({ items }) => { const selected = items.filter((value) => value.selected); if (selected.length === 0) { return /* @__PURE__ */ jsx10("span", { children: "Select" }); } return /* @__PURE__ */ jsx10( ChipList, { list: selected.map((value) => ({ children: value.label })) } ); }, options, disabled: readOnly, hintText: `${translation.select}...` } ) } ) } ); }; export { MultiSelectProperty }; //# sourceMappingURL=MultiSelectProperty.mjs.map