UNPKG

react-ui89

Version:

A collection of React components that mimic a common style of user interfaces from the late 80s and early 90s.

1,201 lines (1,146 loc) 51.4 kB
import * as React from 'react'; import React__default, { createContext, useState, useContext, useEffect, useRef, useMemo, useCallback } from 'react'; import DatePicker from 'react-datepicker'; import Timeout from 'smart-timeout'; import { useFloating, size, autoUpdate, useClick, useDismiss, useRole, useInteractions, FloatingPortal, FloatingFocusManager } from '@floating-ui/react'; import { createPortal } from 'react-dom'; import { toast, ToastContainer } from 'react-toastify'; var Ui89Theme; (function (Ui89Theme) { Ui89Theme["primary"] = "primary"; Ui89Theme["secondary"] = "secondary"; Ui89Theme["info"] = "info"; Ui89Theme["success"] = "success"; Ui89Theme["warning"] = "warning"; Ui89Theme["danger"] = "danger"; })(Ui89Theme || (Ui89Theme = {})); var Ui89Look; (function (Ui89Look) { Ui89Look["main"] = "main"; Ui89Look["side"] = "side"; })(Ui89Look || (Ui89Look = {})); const Ui89Context = createContext({ currentZIndex: 1, nextZIndex: () => 1, }); const Ui89Provider = ({ routerPush, children, }) => { const [zIndex, setZIndex] = useState(1); function nextZIndex() { const next = zIndex + 1; setZIndex(next); return next; } return (React__default.createElement(Ui89Context.Provider, { value: { routerPush, currentZIndex: zIndex, nextZIndex } }, children)); }; const useUi89 = () => { return useContext(Ui89Context); }; function Ui89BreadcrumbsItem({ index, item, onSelect, }) { const style = { "--ui89-index": index }; item.url !== undefined ? "a" : "div"; const overrides = useUi89(); const onClick = (e) => { if (item.url !== undefined) { if (item.url.startsWith("/")) { if (overrides.routerPush !== undefined) { e.preventDefault(); overrides.routerPush(item.url); } } } }; return (React__default.createElement("a", { className: `ui-89-reset-a ui89-breadcrumbs__item`, href: item.url, style: style, onClick: onClick }, React__default.createElement("div", { className: "ui89-breadcrumbs__item__background" }), item.label)); } function Ui89Breadcrumbs({ theme = Ui89Theme.primary, items, onSelect, }) { return (React__default.createElement("div", { className: `ui89-breadcrumbs ui89-typo-special ui89-chosen-theme-${theme}` }, [...items.entries()].reverse().map(([index, item]) => (React__default.createElement(Ui89BreadcrumbsItem, { key: index, index: index, item: item, onSelect: onSelect }))))); } function HoverShadow({ children, }) { return (React__default.createElement("span", { className: "ui89-hover-shadow" }, React__default.createElement("span", { className: "ui89-hover-shadow__bottom" }), React__default.createElement("span", { className: "ui89-hover-shadow__right" }), children)); } var Ui89ButtonPropsSize; (function (Ui89ButtonPropsSize) { Ui89ButtonPropsSize["standard"] = "standard"; Ui89ButtonPropsSize["square"] = "square"; })(Ui89ButtonPropsSize || (Ui89ButtonPropsSize = {})); var Ui89ButtonPropsType; (function (Ui89ButtonPropsType) { Ui89ButtonPropsType["submit"] = "submit"; Ui89ButtonPropsType["reset"] = "reset"; })(Ui89ButtonPropsType || (Ui89ButtonPropsType = {})); function Ui89Button({ theme = Ui89Theme.primary, size = Ui89ButtonPropsSize.standard, type, block, onClick, href, children, autoDisableOnClick = true, disabled, activated, }) { const overrides = useUi89(); const [clicking, setClicking] = useState(false); let localDisabled = disabled || (autoDisableOnClick && clicking); async function onAnchorClick(e) { if (localDisabled) { // The anchor tag does not support the disabled attribute so we do this. return; } if (clicking) { // No double clicking allowed. return; } try { setClicking(true); if (href !== undefined) { if (href.startsWith("/")) { if (overrides.routerPush !== undefined) { e.preventDefault(); overrides.routerPush(href); } } } } finally { setClicking(false); } } async function onButtonClick(e) { if (localDisabled) { // The anchor tag does not support the disabled attribute so we do this. return; } if (clicking) { // No double clicking allowed. return; } try { setClicking(true); if (onClick === undefined) { // No handler. return; } await onClick(); } finally { setClicking(false); } } let containerClass = ["ui89-button", `ui89-button--size-${size}`].join(" "); let buttonClass = [ "ui89-button__button", "ui89-typo-special", "ui89-text-ellipsis", `ui89-chosen-theme-${theme}`, activated ? "ui89-button__button--active" : undefined, block ? "ui89-button__button--block" : undefined, disabled ? "ui89-button__button--disabled" : undefined, clicking ? "ui89-button__button--active" : undefined, ].join(" "); return (React__default.createElement("div", { className: containerClass }, React__default.createElement(HoverShadow, null, href ? (React__default.createElement("a", { className: "ui-89-reset-a", role: "button", href: href, onClick: onAnchorClick }, React__default.createElement("span", { className: buttonClass }, React__default.createElement("div", { className: "ui89-button__button__content" }, children)))) : (React__default.createElement("button", { className: "ui-89-reset-button", type: type || "button", onClick: onButtonClick, disabled: localDisabled }, React__default.createElement("span", { className: buttonClass }, React__default.createElement("div", { className: "ui89-button__button__content" }, children))))))); } function Ui89Card(props) { return (React__default.createElement("div", { className: `ui89-card` }, React__default.createElement("div", { className: "ui89-card__inside" }, props.topLeft && (React__default.createElement("div", { className: "ui89-card__top-left" }, props.topLeft)), props.topCenter && (React__default.createElement("div", { className: "ui89-card__top-center" }, props.topCenter)), props.topRight && (React__default.createElement("div", { className: "ui89-card__top-right" }, props.topRight)), props.children, props.bottomLeft && (React__default.createElement("div", { className: "ui89-card__bottom-left" }, props.bottomLeft)), props.bottomCenter && (React__default.createElement("div", { className: "ui89-card__bottom-center" }, props.bottomCenter)), props.bottomRight && (React__default.createElement("div", { className: "ui89-card__bottom-right" }, props.bottomRight))))); } function Ui89CardHorizontalConnection({ children, overflow, }) { return (React__default.createElement("div", { className: `ui89-card__horizontal-connection ${overflow ? "ui89-card__horizontal-connection--overflow" : ""}` }, children)); } function Ui89DateTimePicker(props) { function datepickerOnChange(value) { if (props.onChange) { props.onChange(value); } } return (React__default.createElement("span", { className: "ui89-date-time-picker" }, React__default.createElement(DatePicker, { className: ["ui89-input-box", "ui89-typo-normal"].join(" "), calendarClassName: "ui89-typo-normal", showTimeSelect: true, dateFormat: props.dateFormat ?? "yyyy/MM/dd HH:mm:ss", timeFormat: "HH:mm", selected: props.value, onChange: datepickerOnChange, popperPlacement: "bottom-start" }))); } const TimeAnimation = ({ children }) => { const [currentTime, setCurrentTime] = useState(new Date()); useEffect(() => { const tick = () => { const now = new Date(); setCurrentTime(now); // Calculate time until the next exact second const msUntilNextSecond = 1000 - now.getMilliseconds(); setTimeout(tick, msUntilNextSecond); }; // Start the first tick const msUntilNextSecond = 1000 - currentTime.getMilliseconds(); const timeoutId = setTimeout(tick, msUntilNextSecond); return () => clearTimeout(timeoutId); }, []); return React__default.createElement(React__default.Fragment, null, children({ now: currentTime })); }; function dateFormat(date, format) { const placeholders = { YYYY: date.getFullYear().toString(), MM: (date.getMonth() + 1).toString().padStart(2, "0"), DD: date.getDate().toString().padStart(2, "0"), HH: date.getHours().toString().padStart(2, "0"), mm: date.getMinutes().toString().padStart(2, "0"), ss: date.getSeconds().toString().padStart(2, "0"), hh: (date.getHours() % 12 || 12).toString().padStart(2, "0"), A: date.getHours() >= 12 ? "PM" : "AM", }; // Replace placeholders in the format string let formattedDate = format; for (const [key, value] of Object.entries(placeholders)) { formattedDate = formattedDate.replace(key, value); } return formattedDate; } function Ui89DigitalClock({ format = "HH:mm:ss", }) { function render({ now }) { return dateFormat(now, format); } return (React__default.createElement("span", { className: "ui89-typo-special" }, React__default.createElement(TimeAnimation, null, render))); } function Ui89HighlightText({ theme, block, children, }) { return (React__default.createElement("span", { className: `ui89-highlight-text ui89-chosen-theme-${theme} ${block ? "ui89-highlight-text--block" : null}` }, children)); } var Ui89HrPropsLook; (function (Ui89HrPropsLook) { Ui89HrPropsLook["straight"] = "straight"; Ui89HrPropsLook["dotted"] = "dotted"; Ui89HrPropsLook["dashed"] = "dashed"; Ui89HrPropsLook["double"] = "double"; })(Ui89HrPropsLook || (Ui89HrPropsLook = {})); function Ui89Hr({ look = "straight", theme }) { return (React__default.createElement("div", { className: `ui-89-hr ${`ui-89-hr--${look}`} ${theme !== undefined ? "ui-89-hr--use-theme" : ""} ${theme !== undefined ? `ui89-chosen-theme-${theme}` : ""}` }, React__default.createElement("div", { className: "ui-89-hr__double" }))); } function Ui89SpacePadding(props) { const gap = props.gap ?? 1; const style = { paddingTop: `calc(var(--ui89-safe-space) * ${props.gapTop ?? props.gapVertical ?? gap})`, paddingRight: `calc(var(--ui89-safe-space) * ${props.gapRight ?? props.gapHorizontal ?? gap})`, paddingBottom: `calc(var(--ui89-safe-space) * ${props.gapBottom ?? props.gapVertical ?? gap})`, paddingLeft: `calc(var(--ui89-safe-space) * ${props.gapLeft ?? props.gapHorizontal ?? gap})`, }; return React__default.createElement("div", { style: style }, props.children); } function Ui89Indent(props) { const gap = props.gap ?? 1; return (React__default.createElement(Ui89SpacePadding, { gap: 0, gapLeft: gap }, props.children)); } function Ui89InputCheckBox(props) { function toggle() { if (props.onChange) { props.onChange(!props.value); } } return (React__default.createElement("span", { className: "ui89-input-check-box", onClick: toggle, role: "checkbox", "aria-checked": props.value ? "true" : "false" }, React__default.createElement("span", { className: "ui89-input-check-box__x" }, props.value ? React__default.createElement(React__default.Fragment, null, "X") : React__default.createElement(React__default.Fragment, null, "\u00A0")))); } const useResizeObserver = (ref) => { const [size, setSize] = useState({ width: 0, height: 0 }); useEffect(() => { const observer = new ResizeObserver((entries) => { for (let entry of entries) { setSize({ width: entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width, height: entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height, }); } }); if (ref.current) { observer.observe(ref.current); } return () => { observer.disconnect(); }; }, [ref.current]); return { size, }; }; const useScrollYPosition = (ref) => { const [scrollY, setScrollY] = useState(0); const ticking = useRef(false); const observer = useRef(null); useEffect(() => { const element = ref.current; if (!element) return; const handleScroll = () => { if (!ticking.current) { ticking.current = true; requestAnimationFrame(() => { setScrollY(element.scrollTop); ticking.current = false; }); } }; element.addEventListener("scroll", handleScroll, { passive: true }); observer.current = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === "childList" && ref.current !== element) { element.removeEventListener("scroll", handleScroll); ref.current?.addEventListener("scroll", handleScroll, { passive: true, }); } } }); observer.current.observe(document.body, { childList: true, subtree: true }); return () => { element.removeEventListener("scroll", handleScroll); observer.current?.disconnect(); observer.current = null; }; }, [ref]); return scrollY; }; function popAnyEntry(map) { const iterator = map.keys().next(); if (!iterator.done) { const key = iterator.value; const value = map.get(key); map.delete(key); return value; } return undefined; } /** * Virtualization at row level. Great for long lists. * * Can be used to implement table components with few columns. */ const Ui89VirtualList = React__default.memo((props) => { const keyCounter = useRef(0); const scrollContainer = useRef(null); const scrollAreaContainer = useRef(null); const { size } = useResizeObserver(scrollContainer); const scrollY = useScrollYPosition(scrollContainer); const rowHeight = props.rowHeight ?? 50; const totalHeight = rowHeight * props.rows.length; const [visibleRows, setVisibleRows] = useState(new Map()); function updateVisibleRows(cache) { if (size.height === 0) { setVisibleRows(new Map()); return; } const firstIndex = Math.max(0, Math.floor(scrollY / rowHeight) - 1); const length = Math.min(props.rows.length - firstIndex, Math.ceil(size.height / rowHeight) + 2); const deletedRows = new Map(cache); // Must find the ones that are no longer visible. for (let index = firstIndex; index < firstIndex + length; index++) { let row = props.rows[index]; let key = props.getRowKey ? props.getRowKey(row) : String(index); deletedRows.delete(key); } const newVisibleRows = new Map(); for (let index = firstIndex; index < firstIndex + length; index++) { let row = props.rows[index]; let key = props.getRowKey ? props.getRowKey(row) : String(index); let existingRow = cache.get(key); if (existingRow !== undefined) { if (existingRow.row === row) ; else { // Data has changed, must update render. existingRow.render = props.renderRow({ index, row }); } newVisibleRows.set(key, existingRow); continue; } let oldRow = popAnyEntry(deletedRows); if (oldRow !== undefined) { oldRow.index = index; oldRow.row = row; oldRow.userKey = key; oldRow.render = props.renderRow({ index, row }); oldRow.style = { transform: `translateY(${index * rowHeight}px)`, height: `${rowHeight}px`, }; newVisibleRows.set(key, oldRow); } else { // New row. newVisibleRows.set(key, { index, row, key: keyCounter.current++, userKey: key, render: props.renderRow({ index, row }), style: { transform: `translateY(${index * rowHeight}px)`, height: `${rowHeight}px`, }, }); } } setVisibleRows(newVisibleRows); } useEffect(() => { // The renderRow function may have a reference to the rows array. if we // do not throw away our cache we risk leaving rows referencing // stale data. updateVisibleRows(new Map()); }, [props.rows, props.renderRow]); useEffect(() => { updateVisibleRows(visibleRows); }, [scrollY, size.height]); const orderedVisibleRows = useMemo(() => { return Array.from(visibleRows.values()).sort((a, b) => a.index - b.index); }, [visibleRows]); return (React__default.createElement("div", { ref: scrollContainer, className: "ui89-virtual-list", style: { maxHeight: props.maxHeight } }, React__default.createElement("div", { ref: scrollAreaContainer, className: "ui89-virtual-list__scroll-area", style: { height: `${totalHeight}px` } }, orderedVisibleRows.map((visibleRow) => (React__default.createElement("div", { key: visibleRow.userKey, className: "ui89-virtual-list__row", style: visibleRow.style }, visibleRow.render)))))); }); function Ui89InputCheckList(props) { function getValueKey(value) { return props.getValueKey !== undefined ? props.getValueKey(value) : String(value); } const valueSet = useMemo(() => { const set = new Set(); for (const value of props.value) { set.add(getValueKey(value)); } return set; }, [props.value]); const renderRow = (props2) => { const key = getValueKey(props2.row); const value = valueSet.has(key); const label = props.renderOption !== undefined ? props.renderOption({ option: props2.row, index: props2.index }) : String(props2.row); function onChange(checked) { if (checked) { if (props.onSelect !== undefined) { props.onSelect(props2.row); } } else { if (props.onDeselect !== undefined) { props.onDeselect(props2.row); } } if (props.onChange !== undefined) { if (checked) { props.onChange([...props.value, props2.row]); } else { props.onChange(props.value.filter((item) => getValueKey(item) !== key)); } } } function toggle() { onChange(!value); } return (React__default.createElement("div", { style: { display: "flex", alignItems: "center", gap: "var(--ui89-safe-space)", height: "100%", } }, React__default.createElement(Ui89InputCheckBox, { value: value, onChange: onChange }), React__default.createElement("div", { className: "ui89-text-ellipsis", style: { minWidth: 0, cursor: "pointer" }, onClick: toggle }, label))); }; return (React__default.createElement("div", { style: { display: "flex", flexDirection: "column", maxHeight: props.maxHeight, } }, React__default.createElement(Ui89VirtualList, { rows: props.options, renderRow: renderRow, getRowKey: props.getValueKey, rowHeight: props.optionHeight ?? 40, maxHeight: "100%" }))); } function Ui89InputCheckText(props) { function toggle() { if (props.onChange) { props.onChange(!props.value); } } return (React__default.createElement("span", { className: "ui89-input-check-text", onClick: toggle }, props.value ? "[X]" : "[ ]")); } function Ui89InputFileUpload(props) { const inputRef = useRef(null); function implOnChange(e) { if (!props.onChange) { return; } if (e.target.files === null) { props.onChange(null); return; } if (e.target.files.length === 0) { props.onChange(null); return; } props.onChange(e.target.files[0]); // Reset the file input's value to allow selecting the same file again. e.target.value = ""; } function onClick() { if (inputRef.current === null) { return; } inputRef.current.click(); } return (React__default.createElement("div", null, React__default.createElement("input", { ref: inputRef, className: "ui89-typo-special", type: "file", onChange: implOnChange, accept: props.accept, hidden: true }), props.value ? (React__default.createElement("div", { className: "ui89-input-file-upload" }, React__default.createElement(Ui89Button, { onClick: onClick }, "Change"), React__default.createElement("span", { className: `ui89-input-file-upload__info ui89-text-single-line ui89-text-single-line--ellipsis-left`, title: props.value.name }, props.value.name))) : (React__default.createElement(Ui89Button, { onClick: onClick }, "Upload")))); } let uniqueId = 0; function throttledTimeout() { const id = String(uniqueId++); let callback; return { call(delay, f) { callback = f; if (Timeout.pending(id)) { Timeout.restart(id); } else { Timeout.set(id, () => callback(), delay); } }, /** * If there is a call that has been scheduled, remove it from the queue. */ abort() { Timeout.clear(id, true); }, /** * Lets you know whether a call is pending. */ isPending() { return Timeout.pending(id); }, }; } function useUpdatedRef(value) { const valueRef = useRef(value); useEffect(() => { valueRef.current = value; }, [value]); return valueRef; } const LAST_VALUE_CHANGED = Symbol("LAST_VALUE_CHANGED"); const OVERRIDEN_VALUE_UNDEFINED = Symbol("OVERRIDEN_VALUE_UNDEFINED"); function useDelayedOnChange(props) { const valueRef = useUpdatedRef(props.value); const onChangeRef = useUpdatedRef(props.onChange); const [intermediateValue, setIntermediateValue] = useState(props.defaultValue || props.value); useEffect(() => { stateRef.current.setValue(props.value); }, [props.value]); function callOnChange() { let newVal = stateRef.current.value; if (props.filter !== undefined) { newVal = props.filter(newVal); } if (newVal !== valueRef.current) { onChangeRef.current?.call(null, newVal); } return newVal; } class StateUnknown { state = "unknown"; value; throttledTimeout; constructor() { this.throttledTimeout = throttledTimeout(); } setValue(newVal) { setIntermediateValue(newVal); } onChange(newVal) { setIntermediateValue(newVal); stateRef.current.throttledTimeout.call(300, callOnChange); } onFocus() { let newState = new StateFocus(); newState.value = stateRef.current.value; newState.lastValue = newState.value; setState(newState); } onBlur() { } onConfirm() { callOnChange(); } } class StateFocus { state = "focus"; value; lastValue; /** * If we receive a new value, we keep track of it here. We set this to * OVERRIDEN_VALUE_UNDEFINED if the value is meant to be discarded. */ overridenValue; throttledTimeout; constructor() { this.overridenValue = OVERRIDEN_VALUE_UNDEFINED; this.throttledTimeout = throttledTimeout(); } setValue(newVal) { if (stateRef.current.value === stateRef.current.lastValue) { // No changes. setIntermediateValue(newVal); // Making sure we keep updating. stateRef.current.lastValue = newVal; } else { // There have been changes. Let's not bother the user. stateRef.current.overridenValue = newVal; } } onChange(newVal) { // Discard last setValue call stateRef.current.overridenValue = OVERRIDEN_VALUE_UNDEFINED; stateRef.current.lastValue = LAST_VALUE_CHANGED; setIntermediateValue(newVal); stateRef.current.throttledTimeout.call(300, callOnChange); } onFocus() { } onBlur() { // We need to make sure that we emit immediately. Do not want to leave // user waiting. // We can cancel this one. stateRef.current.throttledTimeout.abort(); if (stateRef.current.overridenValue !== OVERRIDEN_VALUE_UNDEFINED) { stateRef.current.value = stateRef.current.overridenValue; } let newVal = callOnChange(); setIntermediateValue(newVal); let newState = new StateUnknown(); newState.value = newVal; setState(newState); } onConfirm() { callOnChange(); } } const [state, setState] = useState(() => { let newState = new StateUnknown(); newState.value = intermediateValue; return newState; }); const stateRef = useUpdatedRef(state); stateRef.current.value = intermediateValue; return state; } function Ui89InputText({ value, placeholder, autoTrim = true, onChange, onTyping, onFocus, onBlur, }) { const delayedState = useDelayedOnChange({ defaultValue: "", value, onChange, filter(value) { if (autoTrim) { if (typeof value === "string") { value = value.replace(/\s+/g, " ").trim(); } } return value; }, }); return (React__default.createElement("div", null, React__default.createElement("input", { className: `ui89-input-box ui89-typo-special`, type: "text", value: delayedState.value, onChange: (e) => delayedState.onChange(e.target.value), onBlur: delayedState.onBlur, onFocus: delayedState.onFocus, onKeyDown: (e) => { if (e.key === "Enter") { delayedState.onConfirm(); } }, placeholder: placeholder }))); } function Ui89Scene({ look = Ui89Look.main, children }) { return (React__default.createElement("div", { className: `ui89-scene ui-89-look-${look} ui89-typo-normal ui89-scrollbar` }, children)); } function useZIndexer(open) { const overrides = useUi89(); const [value, setValue] = useState(overrides.currentZIndex); useEffect(() => { if (open) { setValue(overrides.nextZIndex()); } }, [open]); return { value, }; } function Ui89Popover(props) { const zIndexer = useZIndexer(props.open); const { refs, floatingStyles, context } = useFloating({ open: props.open, onOpenChange: props.onOpenChange, middleware: [ size({ apply({ availableWidth, availableHeight, elements }) { let width = elements.reference.getBoundingClientRect().width; if (props.popoverOverflowMaxWidth !== undefined) { if (props.popoverOverflowMaxWidth > width) { width = props.popoverOverflowMaxWidth; } } // Change styles, e.g. Object.assign(elements.floating.style, { width: `${availableWidth}px`, maxWidth: `${width}px`, maxHeight: `${Math.max(0, availableHeight)}px`, }); }, }), ], whileElementsMounted: autoUpdate, placement: "bottom-start", strategy: "fixed", }); const click = useClick(context); const dismiss = useDismiss(context); const role = useRole(context); const { getReferenceProps, getFloatingProps } = useInteractions([ click, dismiss, role, ]); return (React__default.createElement(React__default.Fragment, null, props.renderContainer({ setRef: refs.setReference, props: getReferenceProps(), }), props.open && (React__default.createElement(FloatingPortal, null, React__default.createElement(FloatingFocusManager, { context: context, modal: false }, React__default.createElement(Ui89Scene, null, React__default.createElement("div", { ref: refs.setFloating, style: { ...floatingStyles, zIndex: zIndexer.value, display: "flex", flexDirection: "column", } }, props.renderPopover()))))))); } /** * This is a very performant and customizable dropdown selector that * allows you to choose from a list of options. */ function Ui89InputSelect(props) { const [search, setSearch] = useState(""); const [isOpen, setIsOpen] = useState(false); const getOptionKey = useMemo(() => { return props.getOptionKey ?? ((option) => option); }, [props.getOptionKey]); const options = useMemo(() => { if (props.options === undefined) { return []; } return props.options; }, [props.options]); function isOptionSelected(option) { if (props.value === undefined) { // Definitely not selected. return false; } return getOptionKey(option) === getOptionKey(props.value); } function optionTitle(option) { return String(option); } const selectOption = useCallback((option) => { if (props.onChange !== undefined) { props.onChange(option); } setIsOpen(false); }, [props.onChange]); const renderOption = useCallback(({ row }) => { const isSelected = isOptionSelected(row); return (React__default.createElement("div", { className: [ "ui89-input-select__menu__item", isSelected ? "ui89-input-select__menu__item--selected" : null, ].join(" "), title: optionTitle(row), key: getOptionKey(row), onClick: () => selectOption(row) }, props.renderOption !== undefined ? props.renderOption(row) : row)); }, [options, selectOption]); useEffect(() => { props.onOpenChange?.(isOpen); if (isOpen) { setSearch(""); } }, [isOpen]); useEffect(() => { if (isOpen) { if (props.onSearch !== undefined) { props.onSearch(search); } } }, [search]); return (React__default.createElement("div", { className: "ui89-input-select" }, React__default.createElement(Ui89Popover, { open: isOpen, onOpenChange: setIsOpen, popoverOverflowMaxWidth: props.menuOverflowMaxWidth, renderContainer: (props2) => (React__default.createElement("div", { ref: props2.setRef, className: [ "ui89-input-box", "ui89-input-box--unselectable", "ui89-input-box--clickable", "ui89-text-single-line", ].join(" "), tabIndex: 0, title: props.value !== undefined ? optionTitle(props.value) : undefined, ...props2.props }, props.value !== undefined ? (React__default.createElement(React__default.Fragment, null, props.renderOption !== undefined ? props.renderOption(props.value) : props.value)) : (React__default.createElement(React__default.Fragment, null, "Select...")))), renderPopover: () => (React__default.createElement("div", { className: "ui89-input-select__menu" }, props.search && (React__default.createElement(Ui89InputText, { placeholder: "Search...", value: search, onChange: setSearch })), options.length > 0 ? (React__default.createElement(Ui89VirtualList, { maxHeight: "300px", rowHeight: props.optionHeight ?? 32, rows: options, renderRow: renderOption })) : (React__default.createElement("div", { className: [ "ui89-input-select__menu__item", "ui89-input-select__menu__item--disabled", ].join(" ") }, "<empty>")))) }))); } function stringRemoveAllWhitespace(str) { return str.replace(/\s+/g, ""); } function isTextNumber(text) { return /^\d+(\.\d+)?$/.test(text); } function displayText(value) { if (value === undefined) { // No idea how to display this. return ""; } else if (isNaN(value)) { // No idea what to display. return ""; } return value.toString(); } function dotsMakeMoreSense(value) { return value.replaceAll(",", "."); } function Ui89InputTextNumber(props) { const wrappedValue = useMemo(() => { return displayText(props.value); }, [props.value]); function implOnChange(value) { if (props.onChange === undefined) { return; } if (value === "") { // Use empty value. props.onChange(props.emptyValue); return; } value = stringRemoveAllWhitespace(value); value = dotsMakeMoreSense(value); if (!isTextNumber(value)) { // We end here. return; } const numberValue = Number(value); if (props.min !== undefined) { if (numberValue <= props.min) { value = String(props.min); } } if (props.max !== undefined) { if (numberValue >= props.max) { value = String(props.max); } } props.onChange(value); } return React__default.createElement(Ui89InputText, { value: wrappedValue, onChange: implOnChange }); } function Ui89InputNumber(props) { const wrappedValue = useMemo(() => { if (props.value !== undefined && props.value !== null) { return Number(props.value); } else { return undefined; } }, [props.value]); function wrappedOnChange(value) { if (props.onChange === undefined) { return; } if (value === props.emptyValue) { // Pass along. props.onChange(value); } else { if (value !== undefined && value !== null) { props.onChange(Number(value)); } else { props.onChange(null); } } } return (React__default.createElement(Ui89InputTextNumber, { emptyValue: props.emptyValue, value: wrappedValue, onChange: wrappedOnChange, min: props.min, max: props.max, precision: props.precision })); } function Ui89InputPassword({ value, placeholder, onChange, }) { const [intermediateValue, setIntermediateValue] = useState(value ?? ""); const implOnChange = (e) => { const newValue = e.target.value; setIntermediateValue(newValue); if (onChange) { onChange(newValue); } }; return (React__default.createElement("div", null, React__default.createElement("input", { type: "password", className: `ui89-input-box ui89-typo-special`, role: "textbox", value: intermediateValue, onChange: implOnChange, placeholder: placeholder }))); } function Ui89InputTextArea(props) { const delayedState = useDelayedOnChange({ defaultValue: "", value: props.value, onChange: props.onChange, filter(value) { if (props.autoTrim) { if (typeof value === "string") { value = value.replace(/\s+/g, " ").trim(); } } return value; }, }); return (React__default.createElement("div", null, React__default.createElement("textarea", { className: `ui89-input-box ui89-input-box--resizable ui89-typo-special`, value: delayedState.value, onChange: (e) => delayedState.onChange(e.target.value), onBlur: delayedState.onBlur, onFocus: delayedState.onFocus, rows: props.rows ?? 4, placeholder: props.placeholder }))); } function Ui89LinkBase(props) { const overrides = useUi89(); const [clicking, setClicking] = useState(false); let localDisabled = props.disabled || ((props.autoDisableOnClick ?? true) && clicking); async function onClick(e) { if (localDisabled) { // The anchor tag does not support the disabled attribute so we do this. return; } try { setClicking(true); if (props.onClick !== undefined) { // A function takes over control. e.preventDefault(); await props.onClick(); } else if (props.href !== undefined) { if (props.href.startsWith("/")) { if (overrides.routerPush !== undefined) { e.preventDefault(); overrides.routerPush(props.href); } } } else { // Do nothing. e.preventDefault(); } } finally { setClicking(false); } } return (React__default.createElement("a", { className: `ui-89-reset-a ${props.className}`, role: "link", href: props.href, onClick: onClick }, props.children)); } function Ui89LinkStealth(props) { return React__default.createElement(Ui89LinkBase, { className: "ui89-link-stealth", ...props }); } function Ui89LinkUnderline(props) { return React__default.createElement(Ui89LinkBase, { className: "ui89-link-underline", ...props }); } function Ui89MenuBar({ items }) { return (React__default.createElement("div", { className: `ui89-menu-bar ui89-typo-special ui89-scrollbar` }, items.map((item, index) => { function onNativeClick() { if (item.onClick !== undefined) { item.onClick(); } } return (React__default.createElement("div", { key: index, className: "ui89-menu-bar__item", onClick: onNativeClick }, item.label)); }))); } function GridExpandTrick({ children, }) { return React__default.createElement("span", { className: "ui89-grid-expand-trick" }, children); } function ScrollContainer({ children, }) { return React__default.createElement("span", { className: "ui89-scroll-container" }, children); } const portalRoot = typeof document !== "undefined" ? document.body : null; function Ui89ModalDialog({ open, size = "medium", children, topCenter, onRequestClose, }) { const zIndexer = useZIndexer(open); const dialogClass = useMemo(() => { return ["ui89-modal-dialog", open ? "ui89-modal-dialog--open" : ""].join(" "); }, [size, open]); const dialogBoxClass = useMemo(() => { return [ "ui89-modal-dialog__box", `ui89-modal-dialog__box--size-${size}`, ].join(" "); }, [size, open]); function onClickBackdrop() { if (onRequestClose !== undefined) { onRequestClose(); } } const vdom = (React__default.createElement("div", { className: dialogClass, role: "dialog", style: { zIndex: zIndexer.value } }, React__default.createElement("div", { className: "ui89-modal-dialog__backdrop", role: "presentation", onClick: onClickBackdrop }), React__default.createElement("div", { className: dialogBoxClass }, React__default.createElement("div", { className: "ui89-modal-dialog__spacer" }), React__default.createElement(HoverShadow, null, React__default.createElement(GridExpandTrick, null, React__default.createElement(Ui89Scene, null, React__default.createElement(Ui89Card, { topCenter: topCenter }, React__default.createElement(ScrollContainer, null, children)))))))); return portalRoot !== null ? createPortal(vdom, portalRoot) : vdom; } function Ui89NameValuePair({ name, value, leftMaxWidth, }) { return (React__default.createElement("div", { className: "ui-89-name-value-pair" }, React__default.createElement("div", { className: "ui-89-name-value-pair__name-wrapper", style: { maxWidth: `${leftMaxWidth}px` } }, React__default.createElement("div", { className: "ui-89-name-value-pair__name" }, name), React__default.createElement("div", { className: "ui-89-name-value-pair__dots" })), React__default.createElement("div", { className: "ui-89-name-value-pair__value" }, value))); } const SvgShortcut = (props) => /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: 100, height: 100, viewBox: "0 0 100 100", ...props }, /* @__PURE__ */ React.createElement("rect", { x: 0, y: 0, width: 100, height: 100, fill: "white" }), /* @__PURE__ */ React.createElement("line", { x1: 80, y1: 80, x2: 20, y2: 20, stroke: "black", strokeWidth: 15 }), /* @__PURE__ */ React.createElement("line", { x1: 20, y1: 20, x2: 50, y2: 20, stroke: "black", strokeWidth: 15, strokeLinecap: "round" }), /* @__PURE__ */ React.createElement("line", { x1: 20, y1: 20, x2: 20, y2: 50, stroke: "black", strokeWidth: 15, strokeLinecap: "round" })); function Ui89SpaceVertical({ gap = 1 }) { const style = { paddingTop: `calc(var(--ui89-safe-space) * ${gap})`, }; return React__default.createElement("div", { style: style }); } function Ui89Shortcut({ imageUrl, label, onClick = () => { }, }) { function onNativeClick() { onClick(); } return (React__default.createElement("div", { className: "ui89-shortcut", onClick: onNativeClick }, React__default.createElement("div", { className: "ui89-shortcut__image-container" }, React__default.createElement("img", { className: "ui89-shortcut__image", src: imageUrl }), React__default.createElement("div", { className: "ui89-shortcut__shortcut-icon-container" }, React__default.createElement(SvgShortcut, { className: "ui89-shortcut__shortcut-icon", width: 16, height: 16 }))), React__default.createElement(Ui89SpaceVertical, { gap: 1 }), React__default.createElement("div", { className: `ui89-shortcut__label ui89-typo-small-bold`, onClick: onNativeClick }, label))); } function Ui89Tabs({ selected, onChange = () => { }, options = [], stretch, }) { function handleOnChange(value) { onChange(value); } return (React__default.createElement("div", { className: ["ui89-tabs", stretch ? "ui89-tabs--stretch" : ""].join(" ") }, options.map((option) => (React__default.createElement("div", { className: [ "ui89-tabs__item", "ui89-typo-small-bold", selected === option.value ? "ui89-tabs__item--selected" : "", ].join(" "), key: option.value, onClick: () => handleOnChange(option.value) }, option.label))))); } function Ui89TabbedCard({ selected, onChange, options = [], }) { const selectedItem = useMemo(() => { return options.find((item) => item.value === selected) ?? null; }, [selected, options]); const render = useMemo(() => { return selectedItem !== null ? selectedItem.render : () => React__default.createElement(React__default.Fragment, null); }, [selectedItem]); const renderKey = useMemo(() => { return selectedItem !== null ? selectedItem.value : undefined; }, [selectedItem]); return (React__default.createElement(Ui89Card, { topCenter: React__default.createElement(Ui89Tabs, { selected: selected, options: options, onChange: onChange }) }, React__default.createElement(React__default.Fragment, { key: renderKey }, render()))); } function Ui89TagBox({ theme, children }) { return (React__default.createElement("div", { className: `ui89-tag-box ui89-typo-special ui89-chosen-theme-${theme}` }, children)); } function Ui89ThemeBackground({ theme = Ui89Theme.primary, children, }) { return (React__default.createElement("div", { className: `ui89-theme-background ui89-chosen-theme-${theme}` }, children)); } function Ui89TitleBox({ children }) { return (React__default.createElement("div", { className: `ui89-title-box ui89-typo-special` }, React__default.createElement("div", { className: `ui89-title-box__inside ui89-text-single-line` }, children))); } function Ui89TitleUnderline({ children }) { return (React__default.createElement("div", { className: `ui89-title-underline ui89-typo-special` }, React__default.createElement("div", { className: `ui89-title-underline__inside ui89-text-single-line` }, children))); } function useUi89Toaster() { return { toast(content, options = { theme: Ui89Theme.primary }) { const classNames = ["ui89-toaster", "ui89-typo-normal"]; if (options.theme !== undefined) { classNames.push(`ui89-chosen-theme-${options.theme}`); } let autoClose = 5000; if (options.duration !== undefined) { autoClose = options.duration; } if (options.autoClose !== undefined) { if (!options.autoClose) { autoClose = false; } } return toast(() => content, { className: classNames.join(" "), type: "default", autoClose, closeButton: false, hideProgressBar: true, closeOnClick: true, }); }, }; } function Ui89Toaster() { return React__default.createElement(ToastContainer, null); } var Ui89VirtualTablePropsColumnAlign; (function (Ui89VirtualTablePropsColumnAlign) { Ui89VirtualTablePropsColumnAlign["left"] = "left"; Ui89VirtualTablePropsColumnAlign["right"] = "right"; Ui89VirtualTablePropsColumnAlign["center"] = "center"; })(Ui89VirtualTablePropsColumnAlign || (Ui89VirtualTablePropsColumnAlign = {})); const Ui89VirtualTable = React__default.memo((props) => { const rows = useMemo(() => { let rows = props.rows !== undefined ? props.rows.slice() : []; rows.unshift(undefined); return rows; }, [props.rows]); const columns = useMemo(() => { return props.columns !== undefined ? props.columns : []; }, [props.columns]); const rowHeight = props.rowHeight ?? 20; function getColumnWidth(index) { return columns[index].width ?? 100; } function getColumnHorizontalOffset(columnIndex) { let offset = 0; for (let i = 0; i < columnIndex; i++) { offset += getColumnWidth(i); } return offset; } function isLastColumn(columnIndex) { return columnIndex === columns.length - 1; } function getRowClass(rowIndex) { const classes = ["ui89-virtual-table__row"]; if (rowIndex === 0) { classes.push("ui89-virtual-table__row--first"); classes.push("ui89-typo-normal-bold"); } if (rowIndex === rows.length) { classes.push("ui89-virtual-table__row--last"); } return classes.join(" "); } function getColumnClass(columnIndex) { const halign = columns[columnIndex].halign ?? "left"; const classes = [ "ui89-virtual-table__cell", `ui89-virtual-table__cell--halign-${halign}`, ]; if (columnIndex === 0) { classes.push("ui89-virtual-table__cell--column-first"); } if (isLastColumn(columnIndex)) { classes.push("ui89-virtual-table__cell--column-last"); } return classes.join(" "); } /** * The width of an entire row. */ function rowWidth() { return getColumnHorizontalOffset(columns.length); } const renderRow = useCallback(({ index, row }) => { return (React__default.createElement("div", { className: getRowClass(index), style: { minWidth: rowWidth() + "px", height: "100%" } }, columns.map((column, columnIndex) => { return (React__default.createElement("div", { key: columnIndex, className: getColumnClass(columnIndex), style: { top: 0, height: "100%", width: getColumnWidth(columnIndex) + "px", left: getColumnHorizontalOffset(columnIndex) + "px", } }, index === 0 ? columns[columnIndex].renderHeader ? columns[columnIndex].renderHeader({