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,225 lines (1,190 loc) 78.1 kB
import * as React from 'react'; import React__default, { createContext, useState, useContext, useEffect, useRef, useImperativeHandle, useMemo, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { useFloating, size, autoUpdate, useClick, useDismiss, useRole, useInteractions, FloatingPortal, FloatingFocusManager } from '@floating-ui/react'; import Timeout from 'smart-timeout'; 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 Ui89Palette; (function (Ui89Palette) { Ui89Palette["black"] = "black"; Ui89Palette["darkBlue"] = "darkBlue"; Ui89Palette["darkGreen"] = "darkGreen"; Ui89Palette["darkCyan"] = "darkCyan"; Ui89Palette["darkRed"] = "darkRed"; Ui89Palette["darkPink"] = "darkPink"; Ui89Palette["darkYellow"] = "darkYellow"; Ui89Palette["lightGray"] = "lightGray"; Ui89Palette["darkGray"] = "darkGray"; Ui89Palette["lightBlue"] = "lightBlue"; Ui89Palette["lightGreen"] = "lightGreen"; Ui89Palette["lightCyan"] = "lightCyan"; Ui89Palette["lightRed"] = "lightRed"; Ui89Palette["lightPink"] = "lightPink"; Ui89Palette["lightYellow"] = "lightYellow"; Ui89Palette["white"] = "white"; })(Ui89Palette || (Ui89Palette = {})); const Ui89Context = createContext({ currentZIndex: 1, nextZIndex: () => 1, createPortal: createPortal, }); const Ui89Provider = ({ routerPush, children, createPortal: createPortal$1 = createPortal, }) => { 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, createPortal: createPortal$1 } }, children)); }; const useUi89 = () => { return useContext(Ui89Context); }; 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. e.preventDefault(); 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(); await overrides.routerPush(props.href); } } } else { // Do nothing. e.preventDefault(); } } finally { setClicking(false); } } return (React__default.createElement("a", { className: `ui-89-reset-a ${props.className}`, style: props.style, role: "link", href: props.href, onClick: onClick }, props.children)); } function Ui89BreadcrumbsItem({ index, label, url, onClick, }) { const style = { "--ui89-index": index }; return (React__default.createElement(Ui89LinkBase, { className: `ui-89-reset-a ui89-breadcrumbs__item`, href: url, style: style, onClick: onClick }, React__default.createElement("div", { className: "ui89-breadcrumbs__item__background" }), label)); } function Ui89Breadcrumbs({ theme = Ui89Theme.primary, items, getLabel, getUrl, onSelect, }) { return (React__default.createElement("div", { className: `ui89-breadcrumbs ui89-typo-special ui89-theme-${theme}` }, [...items.entries()].reverse().map(([index, item]) => { const label = getLabel ? getLabel(item) : item.label; const url = getUrl ? getUrl(item) : item.url; const onClick = (() => { if (onSelect) { return () => { return onSelect(item); }; } else { return undefined; } })(); return (React__default.createElement(Ui89BreadcrumbsItem, { key: index, index: index, label: label, url: url, onClick: onClick })); }))); } function Ui89BoxShadow({ children, gap = 1, rowGap, columnGap, }) { const style = { "--ui89-box-shadow-row-gap": `calc(var(--ui89-safe-space) * ${rowGap ?? gap})`, "--ui89-box-shadow-column-gap": `calc(var(--ui89-safe-space) * ${columnGap ?? gap})`, }; return (React__default.createElement("span", { className: "ui89-box-shadow", style: style }, React__default.createElement("span", { className: "ui89-box-shadow__bottom" }), React__default.createElement("span", { className: "ui89-box-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 (onClick !== undefined) { // Takes precedence over href. // Prevent anchor tag from taking over navigation. e.preventDefault(); await onClick(); } else if (href !== undefined) { if (href.startsWith("/")) { if (overrides.routerPush !== undefined) { // Prevent anchor tag from taking over navigation. e.preventDefault(); await 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-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(Ui89BoxShadow, 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) { const themeClasses = props.theme !== undefined ? `ui89-card--themed ui89-theme-${props.theme}` : ""; return (React__default.createElement("div", { className: `ui89-card ${themeClasses}` }, React__default.createElement("div", { className: "ui89-card__inside" }, props.children), 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.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 Ui89Scene({ children }) { return (React__default.createElement("div", { className: `ui89-scene 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, }; } const DialogRootContext = createContext(undefined); function useDialogRoot() { return useContext(DialogRootContext); } var Ui89PopoverPropsPlacement; (function (Ui89PopoverPropsPlacement) { Ui89PopoverPropsPlacement["top"] = "top"; Ui89PopoverPropsPlacement["topStart"] = "topStart"; Ui89PopoverPropsPlacement["topEnd"] = "topEnd"; Ui89PopoverPropsPlacement["right"] = "right"; Ui89PopoverPropsPlacement["rightStart"] = "rightStart"; Ui89PopoverPropsPlacement["rightEnd"] = "rightEnd"; Ui89PopoverPropsPlacement["bottom"] = "bottom"; Ui89PopoverPropsPlacement["bottomStart"] = "bottomStart"; Ui89PopoverPropsPlacement["bottomEnd"] = "bottomEnd"; Ui89PopoverPropsPlacement["left"] = "left"; Ui89PopoverPropsPlacement["leftStart"] = "leftStart"; Ui89PopoverPropsPlacement["leftEnd"] = "leftEnd"; })(Ui89PopoverPropsPlacement || (Ui89PopoverPropsPlacement = {})); function toFloatingUiPlacement(placement) { switch (placement) { case Ui89PopoverPropsPlacement.top: return "top"; case Ui89PopoverPropsPlacement.topStart: return "top-start"; case Ui89PopoverPropsPlacement.topEnd: return "top-end"; case Ui89PopoverPropsPlacement.right: return "right"; case Ui89PopoverPropsPlacement.rightStart: return "right-start"; case Ui89PopoverPropsPlacement.rightEnd: return "right-end"; case Ui89PopoverPropsPlacement.bottom: return "bottom"; case Ui89PopoverPropsPlacement.bottomStart: return "bottom-start"; case Ui89PopoverPropsPlacement.bottomEnd: return "bottom-end"; case Ui89PopoverPropsPlacement.left: return "left"; case Ui89PopoverPropsPlacement.leftStart: return "left-start"; case Ui89PopoverPropsPlacement.leftEnd: return "left-end"; default: throw new Error(`Unknown placement: ${placement}`); } } function Ui89Popover(props) { const zIndexer = useZIndexer(props.open); const portalRoot = useDialogRoot(); 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; } } const style = { maxWidth: `${width}px`, maxHeight: `${Math.max(0, availableHeight)}px`, }; if (props.popoverOverflowForce) { style.width = `${availableWidth}px`; } // Change styles, e.g. Object.assign(elements.floating.style, style); }, }), ], whileElementsMounted: autoUpdate, placement: toFloatingUiPlacement(props.placement ?? Ui89PopoverPropsPlacement.bottomStart), strategy: "fixed", }); const click = useClick(context, { keyboardHandlers: false, }); 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, { root: portalRoot ?? undefined }, React__default.createElement(FloatingFocusManager, { context: context, modal: false, initialFocus: -1 }, React__default.createElement(Ui89Scene, null, React__default.createElement("div", { ref: refs.setFloating, style: { ...floatingStyles, zIndex: zIndexer.value, display: "flex", flexDirection: "column", } }, props.renderPopover()))))))); } 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(new RegExp(key, "g"), value); } return formattedDate; } function dateParse(dateStr, format) { const placeholderRegex = { YYYY: "(\\d{4})", MM: "(\\d{2})", DD: "(\\d{2})", HH: "(\\d{2})", mm: "(\\d{2})", ss: "(\\d{2})", hh: "(\\d{2})", A: "(AM|PM)", }; const placeholderKeys = Object.keys(placeholderRegex).sort((a, b) => b.length - a.length); let regexPattern = "^"; const partTypes = []; let i = 0; while (i < format.length) { let found = false; for (const key of placeholderKeys) { if (format.startsWith(key, i)) { regexPattern += placeholderRegex[key]; partTypes.push(key); i += key.length; found = true; break; } } if (!found) { const char = format[i]; regexPattern += char.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); i++; } } regexPattern += "$"; const regex = new RegExp(regexPattern); const match = dateStr.match(regex); if (!match) { return null; } const now = new Date(); const parts = { year: now.getFullYear(), month: now.getMonth(), day: now.getDate(), hours: 0, minutes: 0, seconds: 0, }; let ampm; const parsedFields = new Set(); for (let j = 1; j < match.length; j++) { const type = partTypes[j - 1]; const value = match[j]; parsedFields.add(type); if (type === "YYYY") parts.year = parseInt(value, 10); else if (type === "MM") parts.month = parseInt(value, 10) - 1; else if (type === "DD") parts.day = parseInt(value, 10); else if (type === "HH") parts.hours = parseInt(value, 10); else if (type === "mm") parts.minutes = parseInt(value, 10); else if (type === "ss") parts.seconds = parseInt(value, 10); else if (type === "hh") parts.hours = parseInt(value, 10); else if (type === "A") ampm = value; } if (ampm) { let hours = parts.hours; if (ampm === "PM" && hours < 12) { hours += 12; } if (ampm === "AM" && hours === 12) { // 12 AM is 00 hours hours = 0; } parts.hours = hours; } const date = new Date(parts.year, parts.month, parts.day, parts.hours, parts.minutes, parts.seconds); // Validate date by checking if components overflowed for parsed fields if (parsedFields.has("YYYY") && date.getFullYear() !== parts.year) return null; if (parsedFields.has("MM") && date.getMonth() !== parts.month) return null; if (parsedFields.has("DD") && date.getDate() !== parts.day) return null; if ((parsedFields.has("HH") || parsedFields.has("hh")) && date.getHours() !== parts.hours) return null; if (parsedFields.has("mm") && date.getMinutes() !== parts.minutes) return null; if (parsedFields.has("ss") && date.getSeconds() !== parts.seconds) return null; return date; } 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; } const Ui89InputText = React__default.forwardRef(function Ui89InputText({ value, placeholder, autoTrim = true, disabled, textAlign, onChange, onTyping, onFocus, onBlur, }, ref) { const inputRef = useRef(null); const delayedState = useDelayedOnChange({ defaultValue: "", value, onChange, filter(value) { if (autoTrim) { if (typeof value === "string") { value = value.replace(/\s+/g, " ").trim(); } } return value; }, }); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current?.focus(); }, })); return (React__default.createElement("div", null, React__default.createElement("input", { ref: inputRef, className: [ "ui89-input-box", disabled ? "ui89-input-box--disabled" : "", ].join(" "), style: { textAlign }, type: "text", disabled: disabled, 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 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, disabled: props.disabled, textAlign: props.textAlign })); } function Ui89DateTimePicker(props) { const [open, setOpen] = useState(false); const [inputValue, setInputValue] = useState(""); const [viewDate, setViewDate] = useState(new Date()); const dateFormat$1 = props.dateFormat ?? "YYYY/MM/DD HH:mm:ss"; useEffect(() => { if (props.value) { setInputValue(dateFormat(props.value, dateFormat$1)); setViewDate(props.value); } else { setInputValue(""); } }, [props.value, dateFormat$1]); function handleInputChange(val) { setInputValue(val); const date = dateParse(val, dateFormat$1); if (date !== null) { if (props.onChange) props.onChange(date); setViewDate(date); } else if (val.trim() === "") { if (props.onChange) props.onChange(null); } } function adjustMonth(offset) { const newDate = new Date(viewDate); newDate.setMonth(newDate.getMonth() + offset); setViewDate(newDate); } function handleDayClick(day) { const newDate = new Date(day); if (props.value) { newDate.setHours(props.value.getHours()); newDate.setMinutes(props.value.getMinutes()); newDate.setSeconds(props.value.getSeconds()); } else { newDate.setHours(12, 0, 0, 0); } if (props.onChange) props.onChange(newDate); } function handleTimeChange(type, val) { if (!props.value) return; const num = parseInt(val, 10); if (isNaN(num)) return; const newDate = new Date(props.value); if (type === "h") newDate.setHours(Math.min(23, Math.max(0, num))); if (type === "m") newDate.setMinutes(Math.min(59, Math.max(0, num))); if (type === "s") newDate.setSeconds(Math.min(59, Math.max(0, num))); if (props.onChange) props.onChange(newDate); } function renderCalendar() { const year = viewDate.getFullYear(); const month = viewDate.getMonth(); const firstDay = new Date(year, month, 1); const startDay = firstDay.getDay(); const startDate = new Date(year, month, 1 - startDay); const days = []; const current = new Date(startDate); for (let i = 0; i < 42; i++) { days.push(new Date(current)); current.setDate(current.getDate() + 1); } return (React__default.createElement("div", { style: { width: "300px", padding: "10px" } }, React__default.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "10px", } }, React__default.createElement(Ui89Button, { size: Ui89ButtonPropsSize.square, onClick: () => adjustMonth(-1) }, "<"), React__default.createElement("div", { className: "ui89-typo-normal" }, dateFormat(viewDate, "MM/YYYY")), React__default.createElement(Ui89Button, { size: Ui89ButtonPropsSize.square, onClick: () => adjustMonth(1) }, ">")), React__default.createElement("div", { style: { display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: "2px", } }, ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map((d) => (React__default.createElement("div", { key: d, className: "ui89-typo-small-bold", style: { textAlign: "center", padding: "5px" } }, d))), days.map((d, i) => { const isSelected = props.value && d.getDate() === props.value.getDate() && d.getMonth() === props.value.getMonth() && d.getFullYear() === props.value.getFullYear(); const isCurrentMonth = d.getMonth() === month; return (React__default.createElement("div", { key: i, className: "ui89-typo-normal", onClick: () => handleDayClick(d), style: { textAlign: "center", padding: "5px", cursor: "pointer", backgroundColor: isSelected ? "var(--ui89-palette-light-blue)" : "transparent", opacity: isCurrentMonth ? 1 : 0.3, } }, d.getDate())); })), props.value && (React__default.createElement("div", { style: { display: "flex", gap: "5px", marginTop: "10px", justifyContent: "center", alignItems: "center", } }, React__default.createElement("div", { style: { width: "35px" } }, React__default.createElement(Ui89InputTextNumber, { value: props.value.getHours(), onChange: (val) => handleTimeChange("h", val), min: 0, max: 23, textAlign: "right" })), ":", React__default.createElement("div", { style: { width: "35px" } }, React__default.createElement(Ui89InputTextNumber, { value: props.value.getMinutes(), onChange: (val) => handleTimeChange("m", val), min: 0, max: 59, textAlign: "right" })), ":", React__default.createElement("div", { style: { width: "35px" } }, React__default.createElement(Ui89InputTextNumber, { value: props.value.getSeconds(), onChange: (val) => handleTimeChange("s", val), min: 0, max: 59, textAlign: "right" })))))); } return (React__default.createElement(Ui89Popover, { open: open, onOpenChange: setOpen, popoverOverflowMaxWidth: 360, renderContainer: ({ setRef, props: popoverProps }) => (React__default.createElement("div", { className: "ui89-date-time-picker", ref: setRef, ...popoverProps }, React__default.createElement(Ui89InputText, { value: inputValue, onChange: handleInputChange, placeholder: dateFormat$1 }))), renderPopover: () => (React__default.createElement("div", { style: { maxWidth: "360px" } }, React__default.createElement(Ui89BoxShadow, null, React__default.createElement(Ui89Card, null, renderCalendar())))) })); } 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 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-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-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(React__default.forwardRef((props, ref) => { const keyCounter = useRef(0); const scrollContainer = useRef(null); useImperativeHandle(ref, () => ({ getScrollTop: () => scrollContainer.current?.scrollTop ?? 0, setScrollTop: (value) => { if (scrollContainer.current) scrollContainer.current.scrollTop = value; }, })); const scrollAreaContainer = useRef(null); const { size } = useResizeObserver(scrollContainer); const scrollY = useScrollYPosition(scrollContainer); const onResize = props.onResize; useEffect(() => { const element = scrollContainer.current; if (!onResize || element === null) { return; } const report = () => onResize({ width: element.clientWidth, height: element.clientHeight, }); report(); const observer = new ResizeObserver(report); observer.observe(element); return () => observer.disconnect(); }, [onResize]); 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", { onClick: toggle, style: { display: "flex", alignItems: "center", gap: "var(--ui89-safe-space)", height: "100%", cursor: "pointer", } }, React__default.createElement(Ui89InputCheckBox, { value: value }), React__default.createElement("div", { className: "ui89-text-ellipsis", style: { minWidth: 0 } }, 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 Ui89InputSelectMultiCheck(props) { const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); useEffect(() => { if (open) { setSearch(""); } }, [open]); useEffect(() => { if (open) { props.onSearch?.(search); } }, [search]); const displayValue = props.renderDisplay ? props.renderDisplay({ value: props.value }) : props.value.map((v) => String(v)).join(", "); return (React__default.createElement(Ui89Popover, { open: open, popoverOverflowForce: true, onOpenChange: setOpen, renderContainer: ({ setRef, props: popoverProps }) => (React__default.createElement("div", { ref: setRef, ...popoverProps, onClick: () => setOpen(true), className: [ "ui89-input-box", "ui89-input-box--unselectable", "ui89-input-box--clickable", ].join(" ") }, displayValue !== "" ? displayValue : props.placeholder)), renderPopover: () => (React__default.createElement(Ui89BoxShadow, null, React__default.createElement(Ui89Card, null, props.search && (React__default.createElement(Ui89InputText, { placeholder: "Search...", value: search, onChange: setSearch })), React__default.createElement(Ui89InputCheckList, { value: props.value, onChange: props.onChange, options: props.options, getValueKey: props.getValueKey, renderOption: props.renderOption, maxHeight: props.maxHeight ?? "300px", optionHeight: props.optionHeight })))) })); } const SvgClose = (props) => /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 100 100", height: "1em", width: "1em", ...props }, /* @__PURE__ */ React.createElement("line", { x1: 10, y1: 10, x2: 90, y2: 90, stroke: "currentColor", strokeWidth: 15, strokeLinecap: "round" }), /* @__PURE__ */ React.createElement("line", { x1: 90, y1: 10, x2: 10, y2: 90, stroke: "currentColor", strokeWidth: 15, strokeLinecap: "round" })); function Ui89InputSelectMultiTag(props) { const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); useEffect(() => { if (open) { setSearch(""); } }, [open]); useEffect(() => { if (open) { props.onSearch?.(search); } }, [search]); function getValueKey(value) { return props.getValueKey !== undefined ? props.getValueKey(value) : String(value); } function removeTag(e, tagToRemove) { // Prevents the menu from opening again e.stopPropagation(); if (props.onChange) { const keyToRemove = getValueKey(tagToRemove); props.onChange(props.value.filter((v) => getValueKey(v) !== keyToRemove)); } } return (React__default.createElement(Ui89Popover, { open: open, onOpenChange: setOpen, popoverOverflowForce: true, renderContainer: ({ setRef, props: popoverProps }) => (React__default.createElement("div", { ref: setRef, ...popoverProps, onClick: () => setOpen(true), className: [ "ui89-input-box", "ui89-input-box--unselectable", "ui89-input-box--clickable", ].join(" "), style: { cursor: "pointer", minHeight: "40px", display: "flex", flexWrap: "wrap", gap: "calc(var(--ui89-safe-space) * 1)", alignItems: "center", paddingTop: "calc(var(--ui89-safe-space) * 0.5)", paddingBottom: "calc(var(--ui89-safe-space) * 0.5)", } }, props.value.length === 0 && props.placeholder && (React__default.createElement("span", null, props.placeholder)), props.value.map((tag) => (React__default.createElement("div", { key: getValueKey(tag), style: { display: "flex", alignItems: "center", border: "2px solid", padding: "2px calc(var(--ui89-safe-space) * 1)", gap: "calc(var(--ui89-safe-space) * 1)", } }, React__default.createElement("span", null, pro