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
JavaScript
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